feat: add categories and icons (#107)

* feat: add search data type

* feat: add search store

* feat: search result add category

* chore: UI adjustments

* chore: UI adjustments

* chore: add css duration

* feat: add type icon & add categories

* feat: add doc click open url

* chore: remove debug

* chore: optimize page details
This commit is contained in:
BiggerRain
2025-01-21 19:37:15 +08:00
committed by GitHub
parent c8914a457d
commit cb4f1ac4b1
33 changed files with 1299 additions and 306 deletions

4
.env
View File

@@ -1,3 +1,3 @@
COCO_SERVER_URL=https://coco.infini.cloud # http://localhost:2900 COCO_SERVER_URL=https://infini.tpddns.cn:27200 #https://coco.infini.cloud # http://localhost:2900
COCO_WEBSOCKET_URL=wss://coco.infini.cloud/ws # ws://localhost:2900/ws COCO_WEBSOCKET_URL=wss://infini.tpddns.cn:27200/ws #wss://coco.infini.cloud/ws # ws://localhost:2900/ws

View File

@@ -1,5 +1,6 @@
{ {
"cSpell.words": [ "cSpell.words": [
"ahooks",
"autolaunch", "autolaunch",
"Avenir", "Avenir",
"callout", "callout",
@@ -30,6 +31,7 @@
"tailwindcss", "tailwindcss",
"tauri", "tauri",
"titlebar", "titlebar",
"tpddns",
"traptitech", "traptitech",
"unlisten", "unlisten",
"unlistener", "unlistener",

View File

@@ -21,6 +21,7 @@
"@tauri-apps/plugin-shell": ">=2.0.0", "@tauri-apps/plugin-shell": ">=2.0.0",
"@tauri-apps/plugin-websocket": "~2", "@tauri-apps/plugin-websocket": "~2",
"@tauri-apps/plugin-window": "2.0.0-alpha.1", "@tauri-apps/plugin-window": "2.0.0-alpha.1",
"ahooks": "^3.8.4",
"axios": "^1.7.7", "axios": "^1.7.7",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"dotenv": "^16.4.7", "dotenv": "^16.4.7",

62
pnpm-lock.yaml generated
View File

@@ -41,6 +41,9 @@ importers:
'@tauri-apps/plugin-window': '@tauri-apps/plugin-window':
specifier: 2.0.0-alpha.1 specifier: 2.0.0-alpha.1
version: 2.0.0-alpha.1 version: 2.0.0-alpha.1
ahooks:
specifier: ^3.8.4
version: 3.8.4(react@18.3.1)
axios: axios:
specifier: ^1.7.7 specifier: ^1.7.7
version: 1.7.7 version: 1.7.7
@@ -551,46 +554,55 @@ packages:
resolution: {integrity: sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==} resolution: {integrity: sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.24.0': '@rollup/rollup-linux-arm-musleabihf@4.24.0':
resolution: {integrity: sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==} resolution: {integrity: sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.24.0': '@rollup/rollup-linux-arm64-gnu@4.24.0':
resolution: {integrity: sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==} resolution: {integrity: sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.24.0': '@rollup/rollup-linux-arm64-musl@4.24.0':
resolution: {integrity: sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==} resolution: {integrity: sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-linux-powerpc64le-gnu@4.24.0': '@rollup/rollup-linux-powerpc64le-gnu@4.24.0':
resolution: {integrity: sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==} resolution: {integrity: sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==}
cpu: [ppc64] cpu: [ppc64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-gnu@4.24.0': '@rollup/rollup-linux-riscv64-gnu@4.24.0':
resolution: {integrity: sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==} resolution: {integrity: sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-s390x-gnu@4.24.0': '@rollup/rollup-linux-s390x-gnu@4.24.0':
resolution: {integrity: sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==} resolution: {integrity: sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==}
cpu: [s390x] cpu: [s390x]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.24.0': '@rollup/rollup-linux-x64-gnu@4.24.0':
resolution: {integrity: sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==} resolution: {integrity: sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.24.0': '@rollup/rollup-linux-x64-musl@4.24.0':
resolution: {integrity: sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==} resolution: {integrity: sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-win32-arm64-msvc@4.24.0': '@rollup/rollup-win32-arm64-msvc@4.24.0':
resolution: {integrity: sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==} resolution: {integrity: sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==}
@@ -649,24 +661,28 @@ packages:
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
'@tauri-apps/cli-linux-arm64-musl@2.0.3': '@tauri-apps/cli-linux-arm64-musl@2.0.3':
resolution: {integrity: sha512-I4MVD7nf6lLLRmNQPpe5beEIFM6q7Zkmh77ROA5BNu/+vHNL5kiTMD+bmd10ZL2r753A6pO7AvqkIxcBuIl0tg==} resolution: {integrity: sha512-I4MVD7nf6lLLRmNQPpe5beEIFM6q7Zkmh77ROA5BNu/+vHNL5kiTMD+bmd10ZL2r753A6pO7AvqkIxcBuIl0tg==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
'@tauri-apps/cli-linux-x64-gnu@2.0.3': '@tauri-apps/cli-linux-x64-gnu@2.0.3':
resolution: {integrity: sha512-C6Jkx2zZGKkoi+sg5FK9GoH/0EvAaOgrZfF5azV5EALGba46g7VpWcZgp9zFUd7K2IzTi+0OOY8TQ2OVfKZgew==} resolution: {integrity: sha512-C6Jkx2zZGKkoi+sg5FK9GoH/0EvAaOgrZfF5azV5EALGba46g7VpWcZgp9zFUd7K2IzTi+0OOY8TQ2OVfKZgew==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
'@tauri-apps/cli-linux-x64-musl@2.0.3': '@tauri-apps/cli-linux-x64-musl@2.0.3':
resolution: {integrity: sha512-qi4ghmTfSAl+EEUDwmwI9AJUiOLNSmU1RgiGgcPRE+7A/W+Am9UnxYySAiRbB/gJgTl9sj/pqH5Y9duP1/sqHg==} resolution: {integrity: sha512-qi4ghmTfSAl+EEUDwmwI9AJUiOLNSmU1RgiGgcPRE+7A/W+Am9UnxYySAiRbB/gJgTl9sj/pqH5Y9duP1/sqHg==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
'@tauri-apps/cli-win32-arm64-msvc@2.0.3': '@tauri-apps/cli-win32-arm64-msvc@2.0.3':
resolution: {integrity: sha512-UXxHkYmFesC97qVmZre4vY7oDxRDtC2OeKNv0bH+iSnuUp/ROxzJYGyaelnv9Ybvgl4YVqDCnxgB28qMM938TA==} resolution: {integrity: sha512-UXxHkYmFesC97qVmZre4vY7oDxRDtC2OeKNv0bH+iSnuUp/ROxzJYGyaelnv9Ybvgl4YVqDCnxgB28qMM938TA==}
@@ -901,6 +917,12 @@ packages:
engines: {node: '>=0.4.0'} engines: {node: '>=0.4.0'}
hasBin: true hasBin: true
ahooks@3.8.4:
resolution: {integrity: sha512-39wDEw2ZHvypaT14EpMMk4AzosHWt0z9bulY0BeDsvc9PqJEV+Kjh/4TZfftSsotBMq52iYIOFPd3PR56e0ZJg==}
engines: {node: '>=8.0.0'}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
ansi-regex@5.0.1: ansi-regex@5.0.1:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -1428,6 +1450,9 @@ packages:
resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
engines: {node: '>=12'} engines: {node: '>=12'}
intersection-observer@0.12.2:
resolution: {integrity: sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg==}
is-alphabetical@2.0.1: is-alphabetical@2.0.1:
resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==}
@@ -1478,6 +1503,10 @@ packages:
resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==} resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==}
hasBin: true hasBin: true
js-cookie@3.0.5:
resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==}
engines: {node: '>=14'}
js-tokens@4.0.0: js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
@@ -1867,6 +1896,9 @@ packages:
peerDependencies: peerDependencies:
react: ^18.3.1 react: ^18.3.1
react-fast-compare@3.2.2:
resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==}
react-hotkeys-hook@4.5.1: react-hotkeys-hook@4.5.1:
resolution: {integrity: sha512-scAEJOh3Irm0g95NIn6+tQVf/OICCjsQsC9NBHfQws/Vxw4sfq1tDQut5fhTEvPraXhu/sHxRd9lOtxzyYuNAg==} resolution: {integrity: sha512-scAEJOh3Irm0g95NIn6+tQVf/OICCjsQsC9NBHfQws/Vxw4sfq1tDQut5fhTEvPraXhu/sHxRd9lOtxzyYuNAg==}
peerDependencies: peerDependencies:
@@ -1947,6 +1979,9 @@ packages:
remark-stringify@11.0.0: remark-stringify@11.0.0:
resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==}
resize-observer-polyfill@1.5.1:
resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==}
resolve@1.22.8: resolve@1.22.8:
resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==}
hasBin: true hasBin: true
@@ -1978,6 +2013,10 @@ packages:
scheduler@0.23.2: scheduler@0.23.2:
resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==}
screenfull@5.2.0:
resolution: {integrity: sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==}
engines: {node: '>=0.10.0'}
semver@6.3.1: semver@6.3.1:
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
hasBin: true hasBin: true
@@ -2985,6 +3024,19 @@ snapshots:
acorn@8.14.0: {} acorn@8.14.0: {}
ahooks@3.8.4(react@18.3.1):
dependencies:
'@babel/runtime': 7.25.9
dayjs: 1.11.13
intersection-observer: 0.12.2
js-cookie: 3.0.5
lodash: 4.17.21
react: 18.3.1
react-fast-compare: 3.2.2
resize-observer-polyfill: 1.5.1
screenfull: 5.2.0
tslib: 2.8.0
ansi-regex@5.0.1: {} ansi-regex@5.0.1: {}
ansi-regex@6.1.0: {} ansi-regex@6.1.0: {}
@@ -3570,6 +3622,8 @@ snapshots:
internmap@2.0.3: {} internmap@2.0.3: {}
intersection-observer@0.12.2: {}
is-alphabetical@2.0.1: {} is-alphabetical@2.0.1: {}
is-alphanumerical@2.0.1: is-alphanumerical@2.0.1:
@@ -3611,6 +3665,8 @@ snapshots:
jiti@1.21.6: {} jiti@1.21.6: {}
js-cookie@3.0.5: {}
js-tokens@4.0.0: {} js-tokens@4.0.0: {}
jsesc@3.0.2: {} jsesc@3.0.2: {}
@@ -4223,6 +4279,8 @@ snapshots:
react: 18.3.1 react: 18.3.1
scheduler: 0.23.2 scheduler: 0.23.2
react-fast-compare@3.2.2: {}
react-hotkeys-hook@4.5.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): react-hotkeys-hook@4.5.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies: dependencies:
react: 18.3.1 react: 18.3.1
@@ -4349,6 +4407,8 @@ snapshots:
mdast-util-to-markdown: 2.1.2 mdast-util-to-markdown: 2.1.2
unified: 11.0.5 unified: 11.0.5
resize-observer-polyfill@1.5.1: {}
resolve@1.22.8: resolve@1.22.8:
dependencies: dependencies:
is-core-module: 2.15.1 is-core-module: 2.15.1
@@ -4400,6 +4460,8 @@ snapshots:
dependencies: dependencies:
loose-envify: 1.4.0 loose-envify: 1.4.0
screenfull@5.2.0: {}
semver@6.3.1: {} semver@6.3.1: {}
shebang-command@2.0.0: shebang-command@2.0.0:

View File

@@ -83,7 +83,6 @@ pub fn run() {
// show_panel, // show_panel,
// hide_panel, // hide_panel,
// close_panel // close_panel
shortcut::check_shortcut_available,
]) ])
.setup(|app| { .setup(|app| {
init(app.app_handle()); init(app.app_handle());
@@ -168,7 +167,7 @@ fn hide_coco(app: tauri::AppHandle) {
} }
fn handle_open_coco(app: &AppHandle) { fn handle_open_coco(app: &AppHandle) {
println!("Open Coco menu clicked!"); // println!("Open Coco menu clicked!");
if let Some(window) = app.get_window("main") { if let Some(window) = app.get_window("main") {
window.show().unwrap(); window.show().unwrap();
@@ -179,7 +178,7 @@ fn handle_open_coco(app: &AppHandle) {
} }
fn handle_hide_coco(app: &AppHandle) { fn handle_hide_coco(app: &AppHandle) {
println!("Hide Coco menu clicked!"); // println!("Hide Coco menu clicked!");
if let Some(window) = app.get_window("main") { if let Some(window) = app.get_window("main") {
if let Err(err) = window.hide() { if let Err(err) = window.hide() {
@@ -196,7 +195,7 @@ fn handle_hide_coco(app: &AppHandle) {
fn switch_tray_icon(app: tauri::AppHandle, is_dark_mode: bool) { fn switch_tray_icon(app: tauri::AppHandle, is_dark_mode: bool) {
let app_handle = app.app_handle(); let app_handle = app.app_handle();
println!("is_dark_mode: {}", is_dark_mode); // println!("is_dark_mode: {}", is_dark_mode);
const DARK_ICON_PATH: &[u8] = include_bytes!("../icons/dark@2x.png"); const DARK_ICON_PATH: &[u8] = include_bytes!("../icons/dark@2x.png");
const LIGHT_ICON_PATH: &[u8] = include_bytes!("../icons/light@2x.png"); const LIGHT_ICON_PATH: &[u8] = include_bytes!("../icons/light@2x.png");

View File

@@ -160,31 +160,3 @@ pub fn _get_shortcut<R: Runtime>(app: &AppHandle<R>) -> String {
), ),
} }
} }
#[tauri::command]
pub async fn check_shortcut_available(key: String) -> bool {
// 这里可以实现系统级的快捷键检查
// 可以检查是否与其他应用的全局快捷键冲突
// 返回 true 表示可用false 表示已被占用
// 简单实现示例:
!is_system_shortcut(&key)
}
fn is_system_shortcut(key: &str) -> bool {
let system_shortcuts = vec![
"Command+C",
"Command+V",
"Command+X",
"Command+A",
"Command+Z",
"Control+C",
"Control+V",
"Control+X",
"Control+A",
"Control+Z",
// 添加更多系统快捷键
];
system_shortcuts.contains(&key)
}

BIN
src/assets/app-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 460 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 753 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 621 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 710 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 439 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -1,28 +1,67 @@
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { CircleAlert, Bolt, X } from "lucide-react"; import {
CircleAlert,
Bolt,
X,
SquareArrowRight,
// UserRoundPen,
} from "lucide-react";
import { isTauri } from "@tauri-apps/api/core"; import { isTauri } from "@tauri-apps/api/core";
import { open } from "@tauri-apps/plugin-shell"; import { open } from "@tauri-apps/plugin-shell";
import { useAppStore } from "@/stores/appStore"; import { useAppStore } from "@/stores/appStore";
import { useSearchStore } from "@/stores/searchStore";
import source_default_img from "@/assets/images/source_default.png";
import source_default_dark_img from "@/assets/images/source_default_dark.png";
import file_efault_img from "@/assets/images/file_efault.png";
import { useTheme } from "@/contexts/ThemeContext";
type ISearchData = Record<string, any[]>;
interface DropdownListProps { interface DropdownListProps {
selected: (item: any) => void; selected: (item: any) => void;
suggests: any[]; suggests: any[];
SearchData: ISearchData;
IsError: boolean; IsError: boolean;
isSearchComplete: boolean; isSearchComplete: boolean;
isChatMode: boolean;
} }
function DropdownList({ selected, suggests, IsError }: DropdownListProps) { function DropdownList({
selected,
suggests,
SearchData,
IsError,
isChatMode,
}: DropdownListProps) {
let globalIndex = 0;
const globalItemIndexMap: any[] = [];
// const letterFirstIndex: any = {
// a: 0,
// s: 0,
// d: 0,
// f: 0,
// };
const { theme } = useTheme();
const connector_data = useAppStore((state) => state.connector_data); const connector_data = useAppStore((state) => state.connector_data);
const datasourceData = useAppStore((state) => state.datasourceData); const datasourceData = useAppStore((state) => state.datasourceData);
const endpoint_http = useAppStore((state) => state.endpoint_http); const endpoint_http = useAppStore((state) => state.endpoint_http);
const setSourceData = useSearchStore((state) => state.setSourceData);
const [showError, setShowError] = useState<boolean>(IsError); const [showError, setShowError] = useState<boolean>(IsError);
const [selectedItem, setSelectedItem] = useState<number | null>(null); const [selectedItem, setSelectedItem] = useState<number | null>(null);
const [selectedName, setSelectedName] = useState<string>("");
const [showIndex, setShowIndex] = useState<boolean>(false); const [showIndex, setShowIndex] = useState<boolean>(false);
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const itemRefs = useRef<(HTMLDivElement | null)[]>([]); const itemRefs = useRef<(HTMLDivElement | null)[]>([]);
useEffect(() => {
isChatMode && setSelectedItem(null);
}, [isChatMode]);
const handleOpenURL = async (url: string) => { const handleOpenURL = async (url: string) => {
if (!url) return; if (!url) return;
try { try {
@@ -46,9 +85,12 @@ function DropdownList({ selected, suggests, IsError }: DropdownListProps) {
if (e.key === "ArrowUp") { if (e.key === "ArrowUp") {
e.preventDefault(); e.preventDefault();
setSelectedItem((prev) => setSelectedItem((prev) => {
prev === null || prev === 0 ? suggests.length - 1 : prev - 1 const res =
); prev === null || prev === 0 ? suggests.length - 1 : prev - 1;
return res;
});
} else if (e.key === "ArrowDown") { } else if (e.key === "ArrowDown") {
e.preventDefault(); e.preventDefault();
setSelectedItem((prev) => setSelectedItem((prev) =>
@@ -56,12 +98,22 @@ function DropdownList({ selected, suggests, IsError }: DropdownListProps) {
); );
} else if (e.key === "Meta") { } else if (e.key === "Meta") {
e.preventDefault(); e.preventDefault();
if (selectedItem !== null) {
const item = globalItemIndexMap[selectedItem];
setSelectedName(item?._source?.source?.name);
}
setShowIndex(true); setShowIndex(true);
} }
if (e.key === "ArrowRight" && selectedItem !== null) {
e.preventDefault();
const item = globalItemIndexMap[selectedItem];
goToTwoPage(item);
}
if (e.key === "Enter" && selectedItem !== null) { if (e.key === "Enter" && selectedItem !== null) {
// console.log("Enter key pressed", selectedItem); // console.log("Enter key pressed", selectedItem);
const item = suggests[selectedItem]; const item = globalItemIndexMap[selectedItem];
if (item?._source?.url) { if (item?._source?.url) {
handleOpenURL(item?._source?.url); handleOpenURL(item?._source?.url);
} else { } else {
@@ -71,7 +123,7 @@ function DropdownList({ selected, suggests, IsError }: DropdownListProps) {
if (e.key >= "0" && e.key <= "9" && showIndex) { if (e.key >= "0" && e.key <= "9" && showIndex) {
// console.log(`number ${e.key}`); // console.log(`number ${e.key}`);
const item = suggests[parseInt(e.key, 10)]; const item = globalItemIndexMap[parseInt(e.key, 10)];
if (item?._source?.url) { if (item?._source?.url) {
handleOpenURL(item?._source?.url); handleOpenURL(item?._source?.url);
} else { } else {
@@ -108,26 +160,87 @@ function DropdownList({ selected, suggests, IsError }: DropdownListProps) {
} }
}, [selectedItem]); }, [selectedItem]);
function getIcon(_source: any) { function findConnectorIcon(item: any) {
const id = _source?.source?.id || ""; const id = item?._source?.source?.id || "";
const result = datasourceData.find((item: any) => item._source.id === id); const result_source = datasourceData.find(
(data: any) => data._source.id === id
const connector_id = result?._source?.connector?.id;
const result1 = connector_data.find(
(item: any) => item._source.id === connector_id
); );
const icons = result1?._source?.assets?.icons || {}; const connector_id = result_source?._source?.connector?.id;
if (icons[_source.icon]?.includes("http")) { const result_connector = connector_data.find(
return icons[_source.icon]; (data: any) => data._source.id === connector_id
);
return result_connector?._source;
}
function getTypeIcon(item: any) {
const connectorSource = findConnectorIcon(item);
const icons = connectorSource?.icon;
if (!icons) {
return theme === "dark" ? source_default_dark_img : source_default_img;
}
if (icons?.includes("http")) {
return icons;
} else { } else {
return endpoint_http + icons[_source.icon]; return endpoint_http + icons;
} }
} }
function getIcon(item: any) {
const connectorSource = findConnectorIcon(item);
const icons = connectorSource?.assets?.icons || {};
const selectedIcon = icons[item?._source?.icon];
if (!selectedIcon) {
return file_efault_img;
}
if (selectedIcon?.includes("http")) {
return selectedIcon;
} else {
return endpoint_http + selectedIcon;
}
}
function getRichIcon(item: any) {
const connectorSource = findConnectorIcon(item);
const icons = connectorSource?.assets?.icons || {};
const selectedIcon = icons[item?._source?.rich_categories?.[0]?.icon];
if (!selectedIcon) {
return theme === "dark" ? source_default_dark_img : source_default_img;
}
if (selectedIcon?.includes("http")) {
return selectedIcon;
} else {
return endpoint_http + selectedIcon;
}
}
function goToTwoPage(item: any) {
setSourceData(item);
selected && selected(item);
}
// function numberToLetter(num: number): string {
// const mapping = ["A", "S", "D", "F"];
// if (num >= 0 && num < mapping.length) {
// const letter = mapping[num];
// letterFirstIndex[letter.toLocaleLowerCase()] = globalIndex
// return letter
// } else {
// return "";
// }
// }
return ( return (
<div <div
ref={containerRef} ref={containerRef}
@@ -147,52 +260,127 @@ function DropdownList({ selected, suggests, IsError }: DropdownListProps) {
/> />
</div> </div>
) : null} ) : null}
<div className="p-2 text-xs text-[#999] dark:text-[#666]">Results</div> {Object.entries(SearchData).map(([sourceName, items]) => (
{suggests?.map((item, index) => { <div key={sourceName}>
const isSelected = selectedItem === index; {items.length > 2 ? (
return ( <div className="p-2 text-xs text-[#999] dark:text-[#666] flex items-center gap-2.5 relative">
<div <img className="w-4 h-4" src={getTypeIcon(items[0])} alt="icon" />
key={item._id} {sourceName}
ref={(el) => (itemRefs.current[index] = el)} <div className="flex-1 border-b border-b-[#e6e6e6] dark:border-b-[#272626]"></div>
onMouseEnter={() => setSelectedItem(index)} <SquareArrowRight
onClick={() => { className="w-4 h-4 cursor-pointer"
if (item?._source?.url) { onClick={() => goToTwoPage(items[0])}
handleOpenURL(item?._source?.url);
} else {
selected(item);
}
}}
className={`w-full px-2 py-2.5 text-sm flex items-center justify-between rounded-lg transition-colors ${
isSelected
? "bg-[rgba(0,0,0,0.1)] dark:bg-[rgba(255,255,255,0.1)] hover:bg-[rgba(0,0,0,0.1)] dark:hover:bg-[rgba(255,255,255,0.1)]"
: ""
}`}
>
<div className="flex gap-2 items-center">
<img
className="w-5 h-5"
src={getIcon(item?._source)}
alt="icon"
/> />
<span className="text-[#333] dark:text-[#d8d8d8] truncate w-80 text-left"> {showIndex && sourceName === selectedName ? (
{item?._source?.title}
</span>
</div>
<div className="flex gap-2 items-center relative">
<span className="text-sm text-[#666] dark:text-[#666] truncate w-52 text-right">
{item?._source?.source?.name}
</span>
{showIndex && index < 10 ? (
<div <div
className={`absolute right-0 w-4 h-4 flex items-center justify-center font-normal text-xs text-[#333] leading-[14px] bg-[#ccc] dark:bg-[#6B6B6B] shadow-[-6px_0px_6px_2px_#e6e6e6] dark:shadow-[-6px_0px_6px_2px_#000] rounded-md`} className={`absolute right-2
w-4 h-4 flex items-end justify-center font-normal text-xs text-[#333] leading-[14px] bg-[#ccc] dark:bg-[#6B6B6B] rounded-md shadow-[-6px_0px_6px_2px_#fff] dark:shadow-[-6px_0px_6px_2px_#000]`}
> >
{index}
</div> </div>
) : null} ) : null}
</div> </div>
</div> ) : null}
); {items.map((item: any) => {
})} const isSelected = selectedItem === globalIndex;
const currentIndex = globalIndex;
globalItemIndexMap.push(item);
globalIndex++;
return (
<div
key={item._id}
ref={(el) => (itemRefs.current[currentIndex] = el)}
onMouseEnter={() => setSelectedItem(currentIndex)}
onClick={() => {
if (item?._source?.url) {
handleOpenURL(item?._source?.url);
} else {
selected(item);
}
}}
className={`w-full px-2 py-2.5 text-sm flex items-center justify-between rounded-lg transition-colors ${
isSelected
? "text-white bg-[#950599] hover:bg-[#950599]"
: "text-[#333] dark:text-[#d8d8d8]"
}`}
>
<div className="flex gap-2 items-center justify-start max-w-[450px]">
<img className="w-5 h-5" src={getIcon(item)} alt="icon" />
<span
className={`text-sm truncate text-left ${
isSelected ? "font-medium" : ""
}`}
>
{item?._source?.title}
</span>
</div>
<div className="flex-1 min-w-[180px] h-full text-[12px] flex gap-2 items-center justify-end relative">
<span
className={`text-[12px] truncate ${
isSelected
? "text-[#DCDCDC]"
: "text-[#999] dark:text-[#666]"
}`}
>
{(item?._source?.category || "") +
(item?._source?.subcategory
? `/${item?._source?.subcategory}`
: "")}
</span>
{item?._source?.rich_categories ? (
<div className="truncate flex gap-2">
<img
className="w-4 h-4 cursor-pointer"
src={getRichIcon(item)}
alt="icon"
onClick={(e) => {
e.stopPropagation();
goToTwoPage(item);
}}
/>
{item?._source?.rich_categories?.map((rich_item: any) => (
<span
className={`${
isSelected ? "text-[#C8C8C8]" : "text-[#666]"
} text-right mr-1`}
>
{rich_item?.label}
</span>
))}
</div>
) : null}
{isSelected ? (
<div
className={`absolute ${
showIndex && currentIndex < 10 ? "right-7" : "right-0"
} w-4 h-4 flex items-end justify-center font-normal text-xs text-[#333] leading-[14px] bg-[#ccc] dark:bg-[#6B6B6B] rounded-md ${
isSelected
? "shadow-[-6px_0px_6px_2px_#950599]"
: "shadow-[-6px_0px_6px_2px_#fff] dark:shadow-[-6px_0px_6px_2px_#000]"
}`}
>
</div>
) : null}
{showIndex && currentIndex < 10 ? (
<div
className={`absolute right-0 w-4 h-4 flex items-center justify-center font-normal text-xs text-[#333] leading-[14px] bg-[#ccc] dark:bg-[#6B6B6B] rounded-md ${
isSelected
? "shadow-[-6px_0px_6px_2px_#950599]"
: "shadow-[-6px_0px_6px_2px_#fff] dark:shadow-[-6px_0px_6px_2px_#000]"
}`}
>
{currentIndex}
</div>
) : null}
</div>
</div>
);
})}
</div>
))}
</div> </div>
); );
} }

View File

@@ -5,7 +5,12 @@ import {
CornerDownLeft, CornerDownLeft,
} from "lucide-react"; } from "lucide-react";
import logoImg from "@/assets/32x32.png"; import logoImg from "@/assets/app-icon.png";
import source_default_img from "@/assets/images/source_default.png";
import source_default_dark_img from "@/assets/images/source_default_dark.png";
import { useSearchStore } from "@/stores/searchStore";
import { useAppStore } from "@/stores/appStore";
import { useTheme } from "@/contexts/ThemeContext";
interface FooterProps { interface FooterProps {
isChat: boolean; isChat: boolean;
@@ -13,16 +18,58 @@ interface FooterProps {
} }
export default function Footer({ name }: FooterProps) { export default function Footer({ name }: FooterProps) {
const sourceData = useSearchStore((state) => state.sourceData);
const connector_data = useAppStore((state) => state.connector_data);
const datasourceData = useAppStore((state) => state.datasourceData);
const endpoint_http = useAppStore((state) => state.endpoint_http);
const { theme } = useTheme();
function findConnectorIcon(item: any) {
const id = item?._source?.source?.id || "";
const result_source = datasourceData.find(
(data: any) => data._source.id === id
);
const connector_id = result_source?._source?.connector?.id;
const result_connector = connector_data.find(
(data: any) => data._source.id === connector_id
);
return result_connector?._source;
}
function getTypeIcon(item: any) {
const connectorSource = findConnectorIcon(item);
const icons = connectorSource?.icon;
if (!icons) {
return theme === "dark" ? source_default_dark_img : source_default_img;
}
if (icons?.includes("http")) {
return icons;
} else {
return endpoint_http + icons;
}
}
return ( return (
<div <div
data-tauri-drag-region data-tauri-drag-region
className="px-4 z-999 mx-[1px] h-10 absolute bottom-0 left-0 right-0 border-t border-gray-200 dark:border-gray-700 flex items-center justify-between rounded-xl rounded-t-none overflow-hidden" className="px-4 z-999 mx-[1px] h-10 absolute bottom-0 left-0 right-0 border-t border-gray-200 dark:border-gray-700 flex items-center justify-between rounded-xl rounded-t-none overflow-hidden"
> >
<div className="flex items-center"> <div className="flex items-center">
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-2">
<img src={logoImg} className="w-5 h-5" /> {sourceData?._source?.source?.name ? (
<img className="w-5 h-5" src={getTypeIcon(sourceData)} alt="icon" />
) : (
<img src={logoImg} className="w-5 h-5" />
)}
<span className="text-xs text-gray-500 dark:text-gray-400"> <span className="text-xs text-gray-500 dark:text-gray-400">
Version 1.0.0 {sourceData?._source?.source?.name || "Version 1.0.0"}
</span> </span>
</div> </div>
@@ -36,12 +83,18 @@ export default function Footer({ name }: FooterProps) {
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="gap-1 flex items-center text-[#666] dark:text-[#666] text-sm"> <div className="gap-1 flex items-center text-[#666] dark:text-[#666] text-sm">
<span className="mr-1.5 ">Quick open</span> <span className="mr-1.5 ">Quick open</span>
<Command className="w-5 h-5 p-1 border rounded-[6px] dark:text-[#666] dark:border-[rgba(255,255,255,0.15)]" /> <kbd className="docsearch-modal-footer-commands-key pr-1">
<ArrowDown01 className="w-5 h-5 p-1 border rounded-[6px] dark:text-[#666] dark:border-[rgba(255,255,255,0.15)]" /> <Command className="w-3 h-3" />
</kbd>
<kbd className="docsearch-modal-footer-commands-key pr-1">
<ArrowDown01 className="w-3 h-3" />
</kbd>
</div> </div>
<div className="flex items-center text-[#666] dark:text-[#666] text-sm"> <div className="flex items-center text-[#666] dark:text-[#666] text-sm">
<span className="mr-1.5 ">Open</span> <span className="mr-1.5 ">Open</span>
<CornerDownLeft className="w-5 h-5 p-1 border rounded-[6px] dark:text-[#666] dark:border-[rgba(255,255,255,0.15)]" /> <kbd className="docsearch-modal-footer-commands-key pr-1">
<CornerDownLeft className="w-3 h-3" />
</kbd>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,4 +1,12 @@
import { Library, Mic, Send, Plus, AudioLines, Image } from "lucide-react"; import {
Library,
Mic,
Send,
Plus,
AudioLines,
Image,
SquareArrowLeft,
} from "lucide-react";
import { useRef, useState, useEffect, useCallback } from "react"; import { useRef, useState, useEffect, useCallback } from "react";
import { listen } from "@tauri-apps/api/event"; import { listen } from "@tauri-apps/api/event";
import { isTauri } from "@tauri-apps/api/core"; import { isTauri } from "@tauri-apps/api/core";
@@ -8,6 +16,7 @@ import AutoResizeTextarea from "./AutoResizeTextarea";
import { useChatStore } from "@/stores/chatStore"; import { useChatStore } from "@/stores/chatStore";
import StopIcon from "@/icons/Stop"; import StopIcon from "@/icons/Stop";
import { useAppStore } from "@/stores/appStore"; import { useAppStore } from "@/stores/appStore";
import { useSearchStore } from "@/stores/searchStore";
interface ChatInputProps { interface ChatInputProps {
onSend: (message: string) => void; onSend: (message: string) => void;
@@ -32,6 +41,13 @@ export default function ChatInput({
}: ChatInputProps) { }: ChatInputProps) {
const showTooltip = useAppStore((state) => state.showTooltip); const showTooltip = useAppStore((state) => state.showTooltip);
const sourceData = useSearchStore((state) => state.sourceData);
const setSourceData = useSearchStore((state) => state.setSourceData);
useEffect(() => {
setSourceData(undefined);
}, []);
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
const textareaRef = useRef<{ reset: () => void; focus: () => void }>(null); const textareaRef = useRef<{ reset: () => void; focus: () => void }>(null);
@@ -65,11 +81,14 @@ export default function ChatInput({
case "KeyI": case "KeyI":
handleToggleFocus(); handleToggleFocus();
break; break;
case "ArrowLeft":
setSourceData(undefined);
break;
case "KeyM": case "KeyM":
console.log("KeyM"); console.log("KeyM");
break; break;
case "Enter": case "Enter":
isChatMode && handleSubmit(); isChatMode && (curChatEnd ? handleSubmit() : disabledChange());
break; break;
case "KeyO": case "KeyO":
console.log("KeyO"); console.log("KeyO");
@@ -139,7 +158,7 @@ export default function ChatInput({
const [countdown, setCountdown] = useState(5); const [countdown, setCountdown] = useState(5);
useEffect(() => { useEffect(() => {
if (connected) return if (connected) return;
if (countdown <= 0) { if (countdown <= 0) {
ReconnectClick(); ReconnectClick();
return; return;
@@ -153,14 +172,21 @@ export default function ChatInput({
}, [countdown, connected]); }, [countdown, connected]);
const ReconnectClick = () => { const ReconnectClick = () => {
setCountdown(5) setCountdown(5);
reconnect() reconnect();
} };
return ( return (
<div className="w-full relative"> <div className="w-full relative">
<div className="p-[12px] flex items-center dark:text-[#D8D8D8] bg-[#ededed] dark:bg-[#202126] rounded transition-all relative"> <div className="p-2 flex items-center dark:text-[#D8D8D8] bg-[#ededed] dark:bg-[#202126] rounded transition-all relative">
<div className="flex flex-wrap gap-2 flex-1 items-center relative"> <div className="flex flex-wrap gap-2 flex-1 items-center relative">
{!isChatMode && sourceData ? (
<SquareArrowLeft
className="w-4 h-4 text-[#000] dark:text-[#d8d8d8] cursor-pointer"
onClick={() => setSourceData(undefined)}
/>
) : null}
{isChatMode ? ( {isChatMode ? (
<AutoResizeTextarea <AutoResizeTextarea
ref={textareaRef} ref={textareaRef}
@@ -190,11 +216,20 @@ export default function ChatInput({
}} }}
/> />
)} )}
{showTooltip && isCommandPressed && !isChatMode && sourceData ? (
<div
className={`absolute left-0 w-4 h-4 flex items-center justify-center font-normal text-xs text-[#333] leading-[14px] bg-[#ccc] dark:bg-[#6B6B6B] rounded-md shadow-[-6px_0px_6px_2px_#ededed] dark:shadow-[-6px_0px_6px_2px_#202126]`}
>
</div>
) : null}
{showTooltip && isCommandPressed ? ( {showTooltip && isCommandPressed ? (
<div <div
className={`absolute bg-black bg-opacity-70 text-white font-bold px-2 py-0.5 rounded-md text-xs transition-opacity duration-200`} className={`absolute ${
!isChatMode && sourceData ? "left-7" : ""
} w-4 h-4 flex items-center justify-center font-normal text-xs text-[#333] leading-[14px] bg-[#ccc] dark:bg-[#6B6B6B] rounded-md shadow-[-6px_0px_6px_2px_#ededed] dark:shadow-[-6px_0px_6px_2px_#202126]`}
> >
+ I I
</div> </div>
) : null} ) : null}
</div> </div>
@@ -237,28 +272,31 @@ export default function ChatInput({
{showTooltip && isChatMode && isCommandPressed ? ( {showTooltip && isChatMode && isCommandPressed ? (
<div <div
className={`absolute right-16 bg-black bg-opacity-70 text-white font-bold px-2 py-0.5 rounded-md text-xs transition-opacity duration-200`} className={`absolute right-10 w-4 h-4 flex items-center justify-center font-normal text-xs text-[#333] leading-[14px] bg-[#ccc] dark:bg-[#6B6B6B] rounded-md shadow-[-6px_0px_6px_2px_#fff] dark:shadow-[-6px_0px_6px_2px_#000]`}
> >
+ M M
</div> </div>
) : null} ) : null}
{showTooltip && isChatMode && isCommandPressed ? ( {showTooltip && isChatMode && isCommandPressed ? (
<div <div
className={`absolute right-1 bg-black bg-opacity-70 text-white font-bold px-2 py-0.5 rounded-md text-xs transition-opacity duration-200`} className={`absolute right-3 w-4 h-4 flex items-end justify-center font-normal text-xs text-[#333] leading-[14px] bg-[#ccc] dark:bg-[#6B6B6B] rounded-md shadow-[-6px_0px_6px_2px_#fff] dark:shadow-[-6px_0px_6px_2px_#000]`}
> >
+
</div> </div>
) : null} ) : null}
{!connected && isChatMode ? ( {!connected && isChatMode ? (
<div className="absolute top-0 right-0 bottom-0 left-0 px-2 py-4 bg-red-500/10 rounded-md font-normal text-xs text-gray-400 flex items-center gap-4"> <div className="absolute top-0 right-0 bottom-0 left-0 px-2 py-4 bg-red-500/10 rounded-md font-normal text-xs text-gray-400 flex items-center gap-4">
Unable to connect to the server Unable to connect to the server
<div className="w-[96px] h-[24px] bg-[#0061FF] rounded-[12px] font-normal text-xs text-white flex items-center justify-center cursor-pointer" onClick={ReconnectClick}> <div
className="w-[96px] h-[24px] bg-[#0061FF] rounded-[12px] font-normal text-xs text-white flex items-center justify-center cursor-pointer"
onClick={ReconnectClick}
>
Reconnect ({countdown}) Reconnect ({countdown})
</div> </div>
</div> </div>
): null} ) : null}
</div> </div>
<div <div
@@ -280,16 +318,16 @@ export default function ChatInput({
</button> </button>
{showTooltip && isCommandPressed ? ( {showTooltip && isCommandPressed ? (
<div <div
className={`absolute left-2 bg-black bg-opacity-70 text-white font-bold px-2 py-0.5 rounded-md text-xs transition-opacity duration-200`} className={`absolute left-2 w-4 h-4 flex items-center justify-center font-normal text-xs text-[#333] leading-[14px] bg-[#ccc] dark:bg-[#6B6B6B] rounded-md shadow-[-6px_0px_6px_2px_#fff] dark:shadow-[-6px_0px_6px_2px_#000]`}
> >
+ O O
</div> </div>
) : null} ) : null}
{showTooltip && isCommandPressed ? ( {showTooltip && isCommandPressed ? (
<div <div
className={`absolute left-16 bg-black bg-opacity-70 text-white font-bold px-2 py-0.5 rounded-md text-xs transition-opacity duration-200`} className={`absolute left-16 w-4 h-4 flex items-center justify-center font-normal text-xs text-[#333] leading-[14px] bg-[#ccc] dark:bg-[#6B6B6B] rounded-md shadow-[-6px_0px_6px_2px_#fff] dark:shadow-[-6px_0px_6px_2px_#000]`}
> >
+ U U
</div> </div>
) : null} ) : null}
</div> </div>
@@ -306,27 +344,27 @@ export default function ChatInput({
</button> </button>
{showTooltip && isCommandPressed ? ( {showTooltip && isCommandPressed ? (
<div <div
className={`absolute left-0 bg-black bg-opacity-70 text-white font-bold px-2 py-0.5 rounded-md text-xs transition-opacity duration-200`} className={`absolute left-0 w-4 h-4 flex items-center justify-center font-normal text-xs text-[#333] leading-[14px] bg-[#ccc] dark:bg-[#6B6B6B] rounded-md shadow-[-6px_0px_6px_2px_#fff] dark:shadow-[-6px_0px_6px_2px_#000]`}
> >
+ N N
</div> </div>
) : null} ) : null}
{showTooltip && isCommandPressed ? ( {showTooltip && isCommandPressed ? (
<div <div
className={`absolute left-14 bg-black bg-opacity-70 text-white font-bold px-2 py-0.5 rounded-md text-xs transition-opacity duration-200`} className={`absolute left-6 w-4 h-4 flex items-center justify-center font-normal text-xs text-[#333] leading-[14px] bg-[#ccc] dark:bg-[#6B6B6B] rounded-md shadow-[-6px_0px_6px_2px_#fff] dark:shadow-[-6px_0px_6px_2px_#000]`}
> >
+ G G
</div> </div>
) : null} ) : null}
</div> </div>
)} )}
<div className="relative w-24 flex justify-end items-center"> <div className="relative w-16 flex justify-end items-center">
{showTooltip && isCommandPressed ? ( {showTooltip && isCommandPressed ? (
<div <div
className={`absolute left-0 z-10 bg-black bg-opacity-70 text-white font-bold px-2 py-0.5 rounded-md text-xs transition-opacity duration-200`} className={`absolute left-1 z-10 w-4 h-4 flex items-center justify-center font-normal text-xs text-[#333] leading-[14px] bg-[#ccc] dark:bg-[#6B6B6B] rounded-md shadow-[-6px_0px_6px_2px_#fff] dark:shadow-[-6px_0px_6px_2px_#000]`}
> >
+ T T
</div> </div>
) : null} ) : null}
<ChatSwitch <ChatSwitch
@@ -334,6 +372,7 @@ export default function ChatInput({
onChange={(value) => { onChange={(value) => {
value && disabledChange(); value && disabledChange();
changeMode(value); changeMode(value);
setSourceData(undefined);
}} }}
/> />
</div> </div>

View File

@@ -8,6 +8,9 @@ import Footer from "./Footer";
import { tauriFetch } from "@/api/tauriFetchClient"; import { tauriFetch } from "@/api/tauriFetchClient";
import noDataImg from "@/assets/coconut-tree.png"; import noDataImg from "@/assets/coconut-tree.png";
import { useAppStore } from "@/stores/appStore"; import { useAppStore } from "@/stores/appStore";
// import { res_search } from "@/mock/index";
import { SearchResults } from "../SearchChat/SearchResults";
import { useSearchStore } from "@/stores/searchStore";
interface SearchProps { interface SearchProps {
changeInput: (val: string) => void; changeInput: (val: string) => void;
@@ -18,8 +21,11 @@ interface SearchProps {
function Search({ isChatMode, input }: SearchProps) { function Search({ isChatMode, input }: SearchProps) {
const appStore = useAppStore(); const appStore = useAppStore();
const sourceData = useSearchStore((state) => state.sourceData);
const [IsError, setIsError] = useState<boolean>(false); const [IsError, setIsError] = useState<boolean>(false);
const [suggests, setSuggests] = useState<any[]>([]); const [suggests, setSuggests] = useState<any[]>([]);
const [SearchData, setSearchData] = useState<any>({});
const [isSearchComplete, setIsSearchComplete] = useState(false); const [isSearchComplete, setIsSearchComplete] = useState(false);
const [selectedItem, setSelectedItem] = useState<any>(); const [selectedItem, setSelectedItem] = useState<any>();
@@ -55,13 +61,18 @@ function Search({ isChatMode, input }: SearchProps) {
const getSuggest = async () => { const getSuggest = async () => {
if (!input) return; if (!input) return;
// //
// const list = []; // mock
// for (let i = 0; i < input.length; i++) { // let list = res_search?.hits?.hits;
// list.push({
// _source: { url: `https://www.google.com/search?q=${i}`, _id: i },
// });
// }
// setSuggests(list); // setSuggests(list);
// const search_data = list.reduce((acc: any, item) => {
// const name = item._source.source.name;
// if (!acc[name]) {
// acc[name] = [];
// }
// acc[name].push(item);
// return acc;
// }, {});
// setSearchData(search_data);
// return; // return;
// //
try { try {
@@ -72,10 +83,20 @@ function Search({ isChatMode, input }: SearchProps) {
}); });
console.log("_suggest", input, response); console.log("_suggest", input, response);
const data = response.data?.hits?.hits || []; let data = response.data?.hits?.hits || [];
setSuggests(data); setSuggests(data);
setIsError(false); const search_data = data.reduce((acc: any, item: any) => {
const name = item?._source?.source?.name;
if (!acc[name]) {
acc[name] = [];
}
acc[name].push(item);
return acc;
}, {});
setSearchData(search_data);
setIsError(false);
setIsSearchComplete(true); setIsSearchComplete(true);
} catch (error) { } catch (error) {
setSuggests([]); setSuggests([]);
@@ -95,7 +116,7 @@ function Search({ isChatMode, input }: SearchProps) {
const debouncedSearch = useCallback(debounce(getSuggest, 300), [input]); const debouncedSearch = useCallback(debounce(getSuggest, 300), [input]);
useEffect(() => { useEffect(() => {
!isChatMode && debouncedSearch(); !isChatMode && !sourceData && debouncedSearch();
if (!input) setSuggests([]); if (!input) setSuggests([]);
}, [input]); }, [input]);
@@ -103,12 +124,18 @@ function Search({ isChatMode, input }: SearchProps) {
<div ref={mainWindowRef} className={`h-[500px] pb-10 w-full relative`}> <div ref={mainWindowRef} className={`h-[500px] pb-10 w-full relative`}>
{/* Search Results Panel */} {/* Search Results Panel */}
{suggests.length > 0 ? ( {suggests.length > 0 ? (
<DropdownList sourceData ? (
suggests={suggests} <SearchResults input={input} isChatMode={isChatMode} />
IsError={IsError} ) : (
isSearchComplete={isSearchComplete} <DropdownList
selected={(item) => setSelectedItem(item)} suggests={suggests}
/> SearchData={SearchData}
IsError={IsError}
isSearchComplete={isSearchComplete}
isChatMode={isChatMode}
selected={(item) => setSelectedItem(item)}
/>
)
) : ( ) : (
<div <div
data-tauri-drag-region data-tauri-drag-region

View File

@@ -34,7 +34,7 @@ const ChatSwitch: React.FC<ChatSwitchProps> = ({ isChatMode, onChange }) => {
role="switch" role="switch"
aria-checked={isChatMode} aria-checked={isChatMode}
className={`relative flex items-center justify-between w-10 h-[18px] rounded-full cursor-pointer transition-colors duration-300 ${ className={`relative flex items-center justify-between w-10 h-[18px] rounded-full cursor-pointer transition-colors duration-300 ${
isChatMode ? "bg-[#0072ff]" : "bg-[#6000FF]" isChatMode ? "bg-[#0072ff]" : "bg-[#950599]"
}`} }`}
onClick={handleToggle} onClick={handleToggle}
> >

View File

@@ -1,78 +1,138 @@
import React from "react"; import React from "react";
import { Calendar, User, Clock } from "lucide-react";
import { useAppStore } from "@/stores/appStore";
import {formatter} from "@/utils/index"
import source_default_img from "@/assets/images/source_default.png";
import source_default_dark_img from "@/assets/images/source_default_dark.png";
import { useTheme } from "@/contexts/ThemeContext";
interface DocumentDetailProps { interface DocumentDetailProps {
documentId?: string; document: any;
} }
export const DocumentDetail: React.FC<DocumentDetailProps> = ({ export const DocumentDetail: React.FC<DocumentDetailProps> = ({ document }) => {
documentId, const connector_data = useAppStore((state) => state.connector_data);
}) => { const datasourceData = useAppStore((state) => state.datasourceData);
if (!documentId) { const endpoint_http = useAppStore((state) => state.endpoint_http);
return (
<div className="h-full flex items-center justify-center text-gray-400 dark:text-gray-500"> const { theme } = useTheme();
</div> function findConnectorIcon(item: any) {
const id = item?._source?.source?.id || "";
const result_source = datasourceData.find(
(data: any) => data._source.id === id
); );
const connector_id = result_source?._source?.connector?.id;
const result_connector = connector_data.find(
(data: any) => data._source.id === connector_id
);
return result_connector?._source;
}
function getTypeIcon(item: any) {
const connectorSource = findConnectorIcon(item);
const icons = connectorSource?.icon;
if (!icons) {
return theme === "dark" ? source_default_dark_img : source_default_img;
}
if (icons?.includes("http")) {
return icons;
} else {
return endpoint_http + icons;
}
} }
return ( return (
<div className="p-8 space-y-8"> <div className="p-4">
<div className="space-y-6"> <div className="font-normal text-xs text-[#666] dark:text-[#999] mb-2">
<h2 className="text-2xl font-semibold text-gray-900 dark:text-gray-100"> Details
</h2>
<div>
<div className="flex items-center gap-6 text-sm text-gray-500 dark:text-gray-400">
<div className="flex items-center gap-2">
<Calendar className="w-4 h-4" />
<span>2024-02-20</span>
</div>
<div className="flex items-center gap-2">
<User className="w-4 h-4" />
<span></span>
</div>
</div>
<div className="flex items-center gap-6 text-sm text-gray-500 dark:text-gray-400">
<div className="flex items-center gap-2">
<Clock className="w-4 h-4" />
<span> 2</span>
</div>
</div>
</div>
</div> </div>
<img {/* <div className="mb-4">
<iframe
src={document?._source?.metadata?.web_view_link}
style={{ width: "100%", height: "500px" }}
title="Text Preview"
/>
</div> */}
{/* <img
src="https://images.unsplash.com/photo-1664575602276-acd073f104c1" src="https://images.unsplash.com/photo-1664575602276-acd073f104c1"
alt="Document preview" alt="Document preview"
className="w-full aspect-video object-cover rounded-xl shadow-md" className="w-full aspect-video object-cover rounded-xl shadow-md"
/> /> */}
<div className="prose prose-gray dark:prose-invert max-w-none"> <div className="py-4 mt-4">
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100"> <div className="flex justify-between flex-wrap font-normal text-xs mb-2.5">
<div className="text-[#666]">Name</div>
</h3> <div className="text-[#333] dark:text-[#D8D8D8] text-right w-60 break-words">
<p className="text-gray-600 dark:text-gray-300 leading-relaxed"> {document?._source?.title || "-"}
2024Q1的产品规划方向和具体功能需求 </div>
</div>
</p>
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mt-6"> <div className="flex justify-between flex-wrap font-normal text-xs mb-2.5">
<div className="text-[#666]">Source</div>
</h3> <div className="text-[#333] dark:text-[#D8D8D8] flex justify-end text-right w-56 break-words">
<ul className="list-disc pl-4 text-gray-600 dark:text-gray-300 space-y-2"> <img
<li></li> className="w-4 h-4 mr-1"
<li></li> src={getTypeIcon(document)}
<li></li> alt="icon"
<li></li> />
<li></li> {document?._source?.source?.name || "-"}
</ul> </div>
</div>
<p className="text-gray-600 dark:text-gray-300 leading-relaxed mt-6"> {/* <div className="flex justify-between font-normal text-xs mb-2.5">
<div className="text-[#666]">Where</div>
Q1的业务增长目标 <div className="text-[#333] dark:text-[#D8D8D8] text-right w-56 break-words">
</p> -
</div>
</div> */}
{document?._source?.updated ? (
<div className="flex justify-between flex-wrap font-normal text-xs mb-2.5">
<div className="text-[#666]">Updated at</div>
<div className="text-[#333] dark:text-[#D8D8D8] text-right w-56 break-words">
{document?._source?.updated || "-"}
</div>
</div>
) : null}
{document?._source?.last_updated_by?.user?.username ? (
<div className="flex justify-between flex-wrap font-normal text-xs mb-2.5">
<div className="text-[#666]">Update by</div>
<div className="text-[#333] dark:text-[#D8D8D8] text-right w-56 break-words">
{document?._source?.last_updated_by?.user?.username || "-"}
</div>
</div>
) : null}
{document?._source?.owner?.username ? (
<div className="flex justify-between flex-wrap font-normal text-xs mb-2.5">
<div className="text-[#666]">Created by</div>
<div className="text-[#333] dark:text-[#D8D8D8] text-right w-56 break-words">
{document?._source?.owner?.username || "-"}
</div>
</div>
) : null}
{document?._source?.type ? (
<div className="flex justify-between flex-wrap font-normal text-xs mb-2.5">
<div className="text-[#666]">Type</div>
<div className="text-[#333] dark:text-[#D8D8D8] text-right w-56 break-words">
{document?._source?.type || "-"}
</div>
</div>
) : null}
{document?._source?.size ? (
<div className="flex justify-between flex-wrap font-normal text-xs mb-2.5">
<div className="text-[#666]">Size</div>
<div className="text-[#333] dark:text-[#D8D8D8] text-right w-56 break-words">
{formatter.bytes(document?._source?.size || 0)}
</div>
</div>
) : null}
</div> </div>
</div> </div>
); );

View File

@@ -1,91 +1,269 @@
import React from 'react'; import React, { useState, useRef, useEffect } from "react";
import { FileText, Image, FileCode, Users, User, Globe } from 'lucide-react'; import { useInfiniteScroll } from "ahooks";
import { isTauri } from "@tauri-apps/api/core";
import { open } from "@tauri-apps/plugin-shell";
interface Document { import { useAppStore } from "@/stores/appStore";
id: string; import { tauriFetch } from "@/api/tauriFetchClient";
title: string; import { useSearchStore } from "@/stores/searchStore";
type: 'text' | 'image' | 'code'; import { SearchHeader } from "./SearchHeader";
owner: 'personal' | 'team' | 'public'; import file_efault_img from "@/assets/images/file_efault.png";
description: string; import noDataImg from "@/assets/coconut-tree.png";
date: string;
}
const documents: Document[] = [
{
id: '1',
title: '产品需求规划文档.doc',
type: 'text',
owner: 'team',
description: '2024年Q1产品规划及功能需求文档包含详细的功能描述和交互设计说明。',
date: '2024-02-20'
},
{
id: '2',
title: 'UI设计规范.fig',
type: 'image',
owner: 'public',
description: '最新的设计系统规范文档,包含组件库使用说明和设计标准。',
date: '2024-02-19'
},
{
id: '3',
title: 'API接口文档.ts',
type: 'code',
owner: 'personal',
description: 'TypeScript版本的API接口定义文档包含所有接口的请求和响应类型。',
date: '2024-02-18'
},
];
const getIcon = (type: Document['type']) => {
switch (type) {
case 'image':
return <Image className="w-5 h-5 text-blue-500 dark:text-blue-400" />;
case 'code':
return <FileCode className="w-5 h-5 text-green-500 dark:text-green-400" />;
default:
return <FileText className="w-5 h-5 text-purple-500 dark:text-purple-400" />;
}
};
const getOwnerIcon = (owner: Document['owner']) => {
switch (owner) {
case 'team':
return <Users className="w-4 h-4 text-blue-500 dark:text-blue-400" />;
case 'public':
return <Globe className="w-4 h-4 text-green-500 dark:text-green-400" />;
default:
return <User className="w-4 h-4 text-gray-500 dark:text-gray-400" />;
}
};
interface DocumentListProps { interface DocumentListProps {
onSelectDocument: (id: string) => void; onSelectDocument: (id: string) => void;
getDocDetail: (detail: any) => void;
input: string;
isChatMode: boolean;
selectedId?: string; selectedId?: string;
} }
export const DocumentList: React.FC<DocumentListProps> = ({ onSelectDocument, selectedId }) => { const PAGE_SIZE = 20;
export const DocumentList: React.FC<DocumentListProps> = ({
input,
getDocDetail,
isChatMode,
}) => {
const connector_data = useAppStore((state) => state.connector_data);
const datasourceData = useAppStore((state) => state.datasourceData);
const sourceData = useSearchStore((state) => state.sourceData);
const endpoint_http = useAppStore((state) => state.endpoint_http);
const [selectedItem, setSelectedItem] = useState<number | null>(null);
const [total, setTotal] = useState(0);
const containerRef = useRef<HTMLDivElement>(null);
const itemRefs = useRef<(HTMLDivElement | null)[]>([]);
const { data, loading } = useInfiniteScroll(
async (d) => {
const page = d ? Math.ceil(d.list.length / PAGE_SIZE) + 1 : 1;
const from = (page - 1) * PAGE_SIZE;
let url = `/query/_search?query=${input}&datasource=${sourceData?._source?.source?.id}&from=${from}&size=${PAGE_SIZE}`;
if (sourceData?._source?.rich_categories) {
url = `/query/_search?query=${input}&rich_category=${sourceData?._source?.rich_categories[0]?.key}&from=${from}&size=${PAGE_SIZE}`;
}
try {
const response = await tauriFetch({
url,
method: "GET",
});
const list = response.data?.hits?.hits || [];
const total = response.data?.hits?.total?.value || 0;
console.log("doc", url, response.data?.hits)
setTotal(total);
getDocDetail(list[0] || {});
return {
list,
hasMore: from + list.length < total,
};
} catch (error) {
console.error("Failed to fetch documents:", error);
return {
list: [],
hasMore: false,
};
}
},
{
target: containerRef,
isNoMore: (d) => (d?.list.length || 0) >= total,
reloadDeps: [input, JSON.stringify(sourceData)],
onBefore: () => {
setTimeout(() => {
const parentRef = containerRef.current;
if (parentRef && parentRef.childElementCount > 10) {
const itemHeight = (parentRef.firstChild as HTMLElement)?.offsetHeight || 80;
parentRef.scrollTo({
top: (parentRef.lastChild as HTMLElement)?.offsetTop - itemHeight,
behavior: 'instant',
});
}
});
},
onFinally: (data) => onFinally(data, containerRef),
}
);
const onFinally = (data: any, ref: any) => {
if (data?.page === 1) return;
const parentRef = ref.current;
if (!parentRef) return;
const itemHeight = parentRef.firstChild?.offsetHeight || 80;
parentRef.scrollTo({
top:
parentRef.lastChild?.offsetTop - (data?.list?.length + 1) * itemHeight,
behavior: 'instant',
});
};
function findConnectorIcon(item: any) {
const id = item?._source?.source?.id || "";
const result_source = datasourceData.find(
(data: any) => data._source.id === id
);
const connector_id = result_source?._source?.connector?.id;
const result_connector = connector_data.find(
(data: any) => data._source.id === connector_id
);
return result_connector?._source;
}
function getIcon(item: any) {
const connectorSource = findConnectorIcon(item);
const icons = connectorSource?.assets?.icons || {};
const selectedIcon = icons[item?._source?.icon];
if (!selectedIcon) {
return file_efault_img;
}
if (selectedIcon?.includes("http")) {
return selectedIcon;
} else {
return endpoint_http + selectedIcon;
}
}
function onMouseEnter(index: number, item: any) {
getDocDetail(item);
setSelectedItem(index);
}
useEffect(() => {
setSelectedItem(null);
}, [isChatMode, input]);
const handleOpenURL = async (url: string) => {
if (!url) return;
try {
if (isTauri()) {
await open(url);
// console.log("URL opened in default browser");
}
} catch (error) {
console.error("Failed to open URL:", error);
}
};
const handleKeyDown = (e: KeyboardEvent) => {
if (!data?.list?.length) return;
if (e.key === "ArrowUp") {
e.preventDefault();
setSelectedItem((prev) => (prev === null || prev === 0 ? 0 : prev - 1));
} else if (e.key === "ArrowDown") {
e.preventDefault();
setSelectedItem((prev) =>
prev === null ? 0 : prev === data?.list?.length - 1 ? prev : prev + 1
);
} else if (e.key === "Meta") {
e.preventDefault();
}
if (e.key === "Enter" && selectedItem !== null) {
const item = data?.list?.[selectedItem];
if (item?._source?.url) {
handleOpenURL(item?._source?.url);
}
}
};
useEffect(() => {
window.addEventListener("keydown", handleKeyDown);
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, [selectedItem]);
useEffect(() => {
if (selectedItem !== null && itemRefs.current[selectedItem]) {
itemRefs.current[selectedItem]?.scrollIntoView({
behavior: "smooth",
block: "nearest",
});
}
}, [selectedItem]);
return ( return (
<div className="space-y-1 py-2"> <div className="w-[50%] border-r border-gray-200 dark:border-gray-700 flex flex-col h-full">
{documents.map((doc) => ( <div className="px-2 flex-shrink-0">
<button <SearchHeader total={total} />
key={doc.id} </div>
onClick={() => onSelectDocument(doc.id)}
className={`w-full flex items-start px-4 py-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors ${ <div
selectedId === doc.id ? 'bg-blue-50 dark:bg-blue-900/50' : '' ref={containerRef}
}`} className="flex-1 overflow-y-auto custom-scrollbar"
> >
<span className="mr-3 mt-0.5">{getIcon(doc.type)}</span> {data?.list.map((item: any, index: number) => {
<div className="flex-1 text-left"> const isSelected = selectedItem === index;
<div className="flex items-center gap-2"> return (
<span className="text-sm font-medium text-gray-900 dark:text-gray-100">{doc.title}</span> <div
<span className="mt-0.5">{getOwnerIcon(doc.owner)}</span> key={item._id + index}
ref={(el) => (itemRefs.current[index] = el)}
onMouseEnter={() => onMouseEnter(index, item)}
onClick={() => {
if (item?._source?.url) {
handleOpenURL(item?._source?.url);
}
}}
className={`w-full px-2 py-2.5 text-sm flex items-center gap-3 rounded-lg transition-colors cursor-pointer ${
isSelected
? "text-white bg-[#950599] hover:bg-[#950599]"
: "text-[#333] dark:text-[#d8d8d8]"
}`}
>
<div className="flex gap-2 items-center flex-1 min-w-0">
<img
className="w-5 h-5 flex-shrink-0"
src={getIcon(item)}
alt="icon"
/>
<span
className={`text-sm truncate ${
isSelected ? "font-medium" : ""
}`}
>
{item?._source?.title}
</span>
</div>
</div> </div>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1 line-clamp-2">{doc.description}</p> );
<span className="text-xs text-gray-400 dark:text-gray-500 mt-1 block">{doc.date}</span> })}
{loading && (
<div className="flex justify-center py-4">
<span>Loading...</span>
</div> </div>
</button> )}
))}
{!loading && data?.list.length === 0 && (
<div
data-tauri-drag-region
className="h-full w-full flex flex-col items-center"
>
<img src={noDataImg} alt="no-data" className="w-16 h-16 mt-24" />
<div className="mt-4 text-sm text-[#999] dark:text-[#666]">
No Results
</div>
</div>
)}
</div>
</div> </div>
); );
}; };

View File

@@ -118,7 +118,7 @@ function Search({ isTransitioned, isChatMode, input }: SearchProps) {
/> />
) : null} ) : null}
{selectedItem ? <SearchResults /> : null} {selectedItem ? <SearchResults input={input} isChatMode={isChatMode} /> : null}
{suggests.length > 0 || selectedItem ? ( {suggests.length > 0 || selectedItem ? (
<Footer isChat={false} name={selectedItem?.source} /> <Footer isChat={false} name={selectedItem?.source} />

View File

@@ -58,15 +58,19 @@ const typeOptions: FilterOption[] = [
{ id: "code", label: "Code" }, { id: "code", label: "Code" },
]; ];
export const SearchHeader: React.FC = () => { interface SearchHeaderProps {
total: number;
}
export const SearchHeader: React.FC<SearchHeaderProps> = ({ total }) => {
const [typeFilter, setTypeFilter] = useState("all"); const [typeFilter, setTypeFilter] = useState("all");
return ( return (
<div className="flex items-center justify-between py-1"> <div className="flex items-center justify-between py-1">
<div className="text-xs text-gray-600 dark:text-gray-400"> <div className="text-xs text-gray-600 dark:text-gray-400">
Find Found
<span className="px-1 font-medium text-gray-900 dark:text-gray-100"> <span className="px-1 font-medium text-gray-900 dark:text-gray-100">
200 {total}
</span> </span>
results results
</div> </div>

View File

@@ -1,32 +1,38 @@
import React, { useState } from "react"; import { useState } from "react";
import { SearchHeader } from "./SearchHeader";
import { DocumentList } from "./DocumentList"; import { DocumentList } from "./DocumentList";
import { DocumentDetail } from "./DocumentDetail"; import { DocumentDetail } from "./DocumentDetail";
export const SearchResults: React.FC = () => { interface SearchResultsProps {
input: string;
isChatMode: boolean;
}
export function SearchResults({ input, isChatMode }: SearchResultsProps) {
const [selectedDocumentId, setSelectedDocumentId] = useState("1"); const [selectedDocumentId, setSelectedDocumentId] = useState("1");
const [detailData, setDetailData] = useState<any>({});
function getDocDetail(detail: any) {
setDetailData(detail)
}
return ( return (
<div className="max-h-[458px] w-full p-2 flex flex-col rounded-xl overflow-y-auto overflow-hidden custom-scrollbar focus:outline-none"> <div className="h-[458px] w-full p-2 pr-0 flex flex-col rounded-xl focus:outline-none">
<div className="flex"> <div className="h-full flex">
{/* Left Panel */} {/* Left Panel */}
<div className="w-[50%] border-r border-gray-200 dark:border-gray-700 flex flex-col overflow-hidden"> <DocumentList
<div className="px-4 flex-shrink-0"> onSelectDocument={setSelectedDocumentId}
<SearchHeader /> selectedId={selectedDocumentId}
</div> input={input}
<div className="overflow-y-auto flex-1 custom-scrollbar"> getDocDetail={getDocDetail}
<DocumentList isChatMode={isChatMode}
onSelectDocument={setSelectedDocumentId} />
selectedId={selectedDocumentId}
/>
</div>
</div>
{/* Right Panel */} {/* Right Panel */}
<div className="flex-1 overflow-y-auto custom-scrollbar"> <div className="flex-1 overflow-y-auto custom-scrollbar">
<DocumentDetail documentId={selectedDocumentId} /> <DocumentDetail document={detailData}/>
</div> </div>
</div> </div>
</div> </div>
); );
}; }

View File

@@ -0,0 +1,30 @@
import { useEffect, useRef } from "react";
const useInfiniteScroll = (callback: () => void) => {
const loaderRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) {
callback();
}
},
{ threshold: 1.0 }
);
if (loaderRef.current) {
observer.observe(loaderRef.current);
}
return () => {
if (loaderRef.current) {
observer.unobserve(loaderRef.current);
}
};
}, [callback]);
return loaderRef;
};
export default useInfiniteScroll;

View File

@@ -27,6 +27,19 @@ const RESERVED_SHORTCUTS = [
["Command", "M"], ["Command", "M"],
["Command", "Enter"], ["Command", "Enter"],
["Command", "ArrowLeft"], ["Command", "ArrowLeft"],
["Command", "ArrowRight"],
["Command", "ArrowUp"],
["Command", "ArrowDown"],
["Command", "0"],
["Command", "1"],
["Command", "2"],
["Command", "3"],
["Command", "4"],
["Command", "5"],
["Command", "6"],
["Command", "7"],
["Command", "8"],
["Command", "9"],
]; ];
export function useShortcutEditor(shortcut: Shortcut, onChange: (shortcut: Shortcut) => void) { export function useShortcutEditor(shortcut: Shortcut, onChange: (shortcut: Shortcut) => void) {

View File

@@ -7,12 +7,53 @@
--background: #ffffff; --background: #ffffff;
--foreground: #09090b; --foreground: #09090b;
--border: #e3e3e7; --border: #e3e3e7;
--docsearch-primary-color: rgb(149, 5, 153);
--docsearch-text-color: rgb(28, 30, 33);
--docsearch-spacing: 12px;
--docsearch-icon-stroke-width: 1.4;
--docsearch-highlight-color: var(--docsearch-primary-color);
--docsearch-muted-color: rgb(150, 159, 175);
--docsearch-modal-container-background: rgba(101, 108, 133, .8);
--docsearch-modal-width: 560px;
--docsearch-modal-height: 600px;
--docsearch-modal-background: rgb(245, 246, 247);
--docsearch-modal-shadow: inset 1px 1px 0 0 rgba(255, 255, 255, .5), 0 3px 8px 0 rgba(85, 90, 100, 1);
--docsearch-searchbox-height: 56px;
--docsearch-searchbox-background: rgb(235, 237, 240);
--docsearch-searchbox-focus-background: #fff;
--docsearch-searchbox-shadow: inset 0 0 0 2px var(--docsearch-primary-color);
--docsearch-hit-height: 56px;
--docsearch-hit-color: rgb(68, 73, 80);
--docsearch-hit-active-color: #fff;
--docsearch-hit-background: #fff;
--docsearch-hit-shadow: 0 1px 3px 0 rgb(212, 217, 225);
--docsearch-key-gradient: linear-gradient(-225deg, rgb(213, 219, 228) 0%, rgb(248, 248, 248) 100%);
--docsearch-key-shadow: inset 0 -2px 0 0 rgb(205, 205, 230), inset 0 0 1px 1px #fff, 0 1px 2px 1px rgba(30, 35, 90, .4);
--docsearch-footer-height: 44px;
--docsearch-footer-background: #fff;
--docsearch-footer-shadow: 0 -1px 0 0 rgb(224, 227, 232), 0 -3px 6px 0 rgba(69, 98, 155, .12);
--docsearch-icon-color: rgb(21, 21, 21);
} }
.dark { .dark {
--background: #09090b; --background: #09090b;
--foreground: #f9f9f9; --foreground: #f9f9f9;
--border: #27272a; --border: #27272a;
--docsearch-text-color: rgb(245, 246, 247);
--docsearch-modal-container-background: rgba(9, 10, 17, .8);
--docsearch-modal-background: rgb(21, 23, 42);
--docsearch-modal-shadow: inset 1px 1px 0 0 rgb(44, 46, 64), 0 3px 8px 0 rgb(0, 3, 9);
--docsearch-searchbox-background: rgb(9, 10, 17);
--docsearch-searchbox-focus-background: #000;
--docsearch-hit-color: rgb(190, 195, 201);
--docsearch-hit-shadow: none;
--docsearch-hit-background: rgb(9, 10, 17);
--docsearch-key-gradient: linear-gradient(-26.5deg, rgb(86, 88, 114) 0%, rgb(49, 53, 91) 100%);
--docsearch-key-shadow: inset 0 -2px 0 0 rgb(40, 45, 85), inset 0 0 1px 1px rgb(81, 87, 125), 0 2px 2px 0 rgba(3, 4, 9, .3);
--docsearch-footer-background: rgb(30, 33, 54);
--docsearch-footer-shadow: inset 0 1px 0 0 rgba(73, 76, 106, .5), 0 -4px 8px 0 rgba(0, 0, 0, .2);
--docsearch-muted-color: rgb(127, 132, 151);
--docsearch-icon-color: rgb(255, 255, 255);
} }
} }
@@ -32,7 +73,7 @@
} }
.input-body { .input-body {
@apply rounded-xl overflow-hidden @apply rounded-xl overflow-hidden;
} }
} }
@@ -142,6 +183,18 @@
background-color: #f79c42; background-color: #f79c42;
} }
.docsearch-modal-footer-commands-key {
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
border: 0px;
padding: 2px;
background: var(--docsearch-key-gradient);
/* box-shadow: var(--docsearch-key-shadow); */
color: var(--docsearch-muted-color);
}
.user-select{ .user-select{
-webkit-touch-callout: none; -webkit-touch-callout: none;
-webkit-user-select: none; -webkit-user-select: none;

263
src/mock/index.ts Normal file
View File

@@ -0,0 +1,263 @@
// mock
export const res_search = {
took: 2590,
timed_out: false,
_shards: {
total: 1,
successful: 1,
skipped: 0,
failed: 0
},
hits: {
total: {
value: 253,
relation: "eq"
},
max_score: 32.709457,
hits: [
{
_index: "coco_document",
_type: "_doc",
_id: "3ac857ef30d101b1e5880b53b1438b1a",
_score: 32.709457,
_source: {
icon: "web",
id: "3ac857ef30d101b1e5880b53b1438b1a",
source: {
name: "hugo_site",
type: "connector"
},
type: "web_page",
category: "Aggregation",
subcategory: "Metric",
title: "Avg aggregation",
content: "",
author: "liaosy",
url: "https://pizza.rs/docs/references/aggregation/avg/",
tags: [
"avg",
"aggregation"
]
}
},
{
_index: "coco_document",
_type: "_doc",
_id: "2485a744c5dae1278a01c04d39bf60a6",
_score: 32.37022,
_source: {
icon: "web",
id: "2485a744c5dae1278a01c04d39bf60a6",
source: {
name: "hugo_site",
type: "connector"
},
type: "web_page",
title: "auto_generate_doc_id",
content: "",
author: "liaosy",
url: "https://infinilabs.cn/docs/latest/gateway/references/filters/auto_generate_doc_id/"
}
},
{
_index: "coco_document",
_type: "_doc",
_id: "15aa340fa9ddfcfbf793b8707a4fa16b",
_score: 21.983166,
_source: {
icon: "web",
id: "15aa340fa9ddfcfbf793b8707a4fa16b",
source: {
name: "hugo_site",
type: "connector"
},
type: "web_page",
category: "Overview",
subcategory: "Architecture",
title: "Architecture",
content: "",
author: "yangfan",
url: "https://pizza.rs/docs/overview/architecture/",
tags: [
"architecture"
]
}
},
{
_index: "coco_document",
_type: "_doc",
_id: "f96b1af318d62a43a44f54731409ff52",
_score: 21.887964,
_source: {
icon: "web",
id: "f96b1af318d62a43a44f54731409ff52",
source: {
name: "hugo_site",
type: "connector"
},
type: "web_page",
title: "Introducing Coco AI in Two Minutes - A Quick Start Video 🥥",
content: "",
author: "yangfan",
url: "https://blog.infinilabs.com/posts/2024/a-quick-start-viideo-to-introduce-coco-ai-in-two-minutes/",
tags: [
"Coco AI",
"Search",
"Gen-AI",
"Enterprise"
]
}
},
{
_index: "coco_document",
_type: "_doc",
_id: "3a806937f9e7fe55905a7f71d111e523",
_score: 21.286049,
_source: {
icon: "web",
id: "3a806937f9e7fe55905a7f71d111e523",
source: {
name: "hugo_site",
type: "connector"
},
type: "web_page",
category: "Catalog",
subcategory: "Namespace",
title: "Create a namespace",
content: "",
author: "yangfan",
url: "https://pizza.rs/docs/references/namespace/create/",
tags: [
"create",
"namespace"
]
}
},
{
_index: "coco_document",
_type: "_doc",
_id: "04f48643c2c52b872c149e077765f8cb",
_score: 21.286049,
_source: {
icon: "web",
id: "04f48643c2c52b872c149e077765f8cb",
source: {
name: "hugo_site",
type: "connector"
},
type: "web_page",
category: "Document",
subcategory: "Index",
title: "Create a document",
content: "",
author: "zouwenan",
url: "https://pizza.rs/docs/references/document/create/",
tags: [
"create",
"index"
]
}
},
{
_index: "coco_document",
_type: "_doc",
_id: "d101818b1e6d2eb23ca2f813ef3a9648",
_score: 21.213268,
_source: {
icon: "web",
id: "d101818b1e6d2eb23ca2f813ef3a9648",
source: {
name: "hugo_site",
type: "connector"
},
type: "web_page",
category: "Catalog",
subcategory: "Collection",
title: "Create a collection",
content: " ",
author: "zouwenan",
url: "https://pizza.rs/docs/references/collection/create/",
tags: [
"create",
"collection"
]
}
},
{
_index: "coco_document",
_type: "_doc",
_id: "ee2228755039808c199e6812d09c745e",
_score: 20.967154,
_source: {
icon: "web",
id: "ee2228755039808c199e6812d09c745e",
source: {
name: "hugo_site",
type: "connector"
},
type: "web_page",
category: "Document",
subcategory: "Index",
title: "Delete a document",
content: "",
author: "zouwenan",
url: "https://pizza.rs/docs/references/document/delete/",
tags: [
"delete",
"index"
]
}
},
{
_index: "coco_document",
_type: "_doc",
_id: "60584d12aba0ff569e7b79a7be168810",
_score: 20.967154,
_source: {
icon: "web",
id: "60584d12aba0ff569e7b79a7be168810",
source: {
name: "hugo_site",
type: "connector"
},
type: "web_page",
category: "Catalog",
subcategory: "Namespace",
title: "Delete a namespace",
content: "",
author: "zouwenan",
url: "https://pizza.rs/docs/references/namespace/delete/",
tags: [
"delete",
"namespace"
]
}
},
{
_index: "coco_document",
_type: "_doc",
_id: "73e31d0feeb8d3d97e4b06a98de54672",
_score: 20.934437,
_source: {
icon: "web",
id: "73e31d0feeb8d3d97e4b06a98de54672",
source: {
name: "hugo_site",
type: "connector"
},
type: "web_page",
category: "Document",
subcategory: "Index",
title: "Replace a document",
content: " ",
author: "medcl",
url: "https://pizza.rs/docs/references/document/replace/",
tags: [
"replace",
"index"
]
}
}
]
}
}

View File

@@ -99,7 +99,7 @@ export default function DesktopApp() {
> >
<div <div
data-tauri-drag-region data-tauri-drag-region
className={`p-[7px] pb-0 absolute w-full flex items-center justify-center transition-all duration-500 ${ className={`p-2 pb-0 absolute w-full flex items-center justify-center transition-all duration-500 ${
isTransitioned isTransitioned
? "top-[500px] h-[90px] border-t" ? "top-[500px] h-[90px] border-t"
: "top-0 h-[90px] border-b" : "top-0 h-[90px] border-b"
@@ -123,7 +123,7 @@ export default function DesktopApp() {
data-tauri-drag-region data-tauri-drag-region
className={`absolute w-full transition-opacity duration-500 ${ className={`absolute w-full transition-opacity duration-500 ${
isTransitioned ? "opacity-0 pointer-events-none" : "opacity-100" isTransitioned ? "opacity-0 pointer-events-none" : "opacity-100"
} bottom-0 h-[500px] user-select`} } bottom-0 h-[500px] `}
> >
<Search <Search
key="Search" key="Search"
@@ -141,7 +141,7 @@ export default function DesktopApp() {
: "-top-[506px] opacity-0 pointer-events-none" : "-top-[506px] opacity-0 pointer-events-none"
} h-[500px]`} } h-[500px]`}
> >
{isTransitioned ? ( {isTransitioned && isChatMode ? (
<ChatAI <ChatAI
ref={chatAIRef} ref={chatAIRef}
key="ChatAI" key="ChatAI"

22
src/stores/searchStore.ts Normal file
View File

@@ -0,0 +1,22 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
export type ISearchStore = {
sourceData: any;
setSourceData: (sourceData: any) => void;
};
export const useSearchStore = create<ISearchStore>()(
persist(
(set) => ({
sourceData: undefined,
setSourceData: (sourceData: any) => set({ sourceData }),
}),
{
name: "search-store",
partialize: (state) => ({
sourceData: state.sourceData,
}),
}
)
);

View File

@@ -81,3 +81,19 @@ export const authWitheGithub = (uid: string) => {
location.href = `${authorizeUrl}?client_id=${"Ov23li4IcdbbWp2RgLTN"}&redirect_uri=${"http://localhost:1420/login"}`; location.href = `${authorizeUrl}?client_id=${"Ov23li4IcdbbWp2RgLTN"}&redirect_uri=${"http://localhost:1420/login"}`;
}; };
const unitArr = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"] as const;
export const formatter = {
bytes: (value: number): string => {
if (!Number.isFinite(value) || value <= 0) {
return "0B";
}
const index = Math.floor(Math.log(value) / Math.log(1024));
const size = (value / Math.pow(1024, index)).toFixed(1);
return size + (unitArr[index] ?? "B")
},
};

View File

@@ -47,6 +47,11 @@ export default defineConfig(async () => ({
changeOrigin: true, changeOrigin: true,
secure: false, secure: false,
}, },
"/connector": {
target: process.env.COCO_SERVER_URL,
changeOrigin: true,
secure: false,
},
}, },
}, },
build: { build: {