Compare commits
93 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c9a2ff441 | ||
|
|
2251b0af95 | ||
|
|
560a12ab93 | ||
|
|
2ff66c0b91 | ||
|
|
ef4a184233 | ||
|
|
8422bc03e7 | ||
|
|
370113129c | ||
|
|
cb758ef452 | ||
|
|
12b9b4bb81 | ||
|
|
562db19f16 | ||
|
|
dc5cd9aecb | ||
|
|
0b018cd24f | ||
|
|
2ed22d3d7c | ||
|
|
4ce9561eb7 | ||
|
|
3aeb39b3af | ||
|
|
27e99d4629 | ||
|
|
df70276a54 | ||
|
|
6553a8f5d3 | ||
|
|
4ebbc9ec6e | ||
|
|
4208633556 | ||
|
|
fc43fbe798 | ||
|
|
b5bb9105d4 | ||
|
|
b6ebd6e5f8 | ||
|
|
22216491b6 | ||
|
|
44ca66259c | ||
|
|
be3cae36e2 | ||
|
|
35ea30626f | ||
|
|
4bcae5cffb | ||
|
|
76458db8ab | ||
|
|
5b41e190d3 | ||
|
|
43ac9a054c | ||
|
|
ac485a32cc | ||
|
|
e10908a095 | ||
|
|
78b8908ac8 | ||
|
|
3c54cb84a8 | ||
|
|
8ed808c591 | ||
|
|
7a2dde7448 | ||
|
|
65451fc63e | ||
|
|
5d108a46d3 | ||
|
|
f9567c2d46 | ||
|
|
da917e6012 | ||
|
|
335a906674 | ||
|
|
a50a636d59 | ||
|
|
2dd3f776e6 | ||
|
|
40f6aa0ccd | ||
|
|
4da9e024e0 | ||
|
|
c20bba51f5 | ||
|
|
0a62a2095b | ||
|
|
5677995185 | ||
|
|
ec4e5e7d1d | ||
|
|
1df5265b1a | ||
|
|
fb8a4684dc | ||
|
|
0b609e570d | ||
|
|
f91f6bdc17 | ||
|
|
57590f3b57 | ||
|
|
c18f9ea154 | ||
|
|
441875d9b4 | ||
|
|
eddf9075bb | ||
|
|
9eac8f8a8e | ||
|
|
515260c43f | ||
|
|
118de0e80b | ||
|
|
19ce896fdc | ||
|
|
4a41ea5d8b | ||
|
|
880e1206ce | ||
|
|
1e6d9f9550 | ||
|
|
ff0faf425f | ||
|
|
1fbf5d6552 | ||
|
|
db41e817c3 | ||
|
|
1296755bc5 | ||
|
|
d410f20864 | ||
|
|
61d0a3b79a | ||
|
|
b24319b649 | ||
|
|
3c0fb24548 | ||
|
|
2fcbed0381 | ||
|
|
7444347e0c | ||
|
|
725ce042de | ||
|
|
3b67de5387 | ||
|
|
9b53a026ff | ||
|
|
9ea7dbf3aa | ||
|
|
55622911ac | ||
|
|
92f78ad08c | ||
|
|
f690dbaab2 | ||
|
|
210efe763d | ||
|
|
f23498afa0 | ||
|
|
a80a5d928f | ||
|
|
b733bb5516 | ||
|
|
5046754534 | ||
|
|
f557f7e780 | ||
|
|
18feb2d690 | ||
|
|
af59f2fe9f | ||
|
|
5e1bb54d5e | ||
|
|
33fa516aad | ||
|
|
d2c1cf513d |
8
.github/workflows/release.yml
vendored
@@ -105,10 +105,14 @@ jobs:
|
|||||||
chmod 600 ~/.ssh/known_hosts
|
chmod 600 ~/.ssh/known_hosts
|
||||||
|
|
||||||
- name: Pizza engine features setup
|
- name: Pizza engine features setup
|
||||||
if: matrix.target != 'i686-pc-windows-msvc'
|
|
||||||
run: |
|
run: |
|
||||||
make add-dep-pizza-engine
|
if [[ ${{ matrix.target }} == "i686-pc-windows-msvc" ]]; then
|
||||||
|
rustup target add i686-pc-windows-msvc --toolchain stable
|
||||||
|
else
|
||||||
|
make add-dep-pizza-engine-linux
|
||||||
rustup target add ${{ matrix.target}} --toolchain nightly-2025-02-28
|
rustup target add ${{ matrix.target}} --toolchain nightly-2025-02-28
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
- name: Build the app with ${{ matrix.platform }}
|
- name: Build the app with ${{ matrix.platform }}
|
||||||
uses: tauri-apps/tauri-action@v0
|
uses: tauri-apps/tauri-action@v0
|
||||||
|
|||||||
@@ -1,21 +1,35 @@
|
|||||||
---
|
---
|
||||||
weight: 10
|
weight: 10
|
||||||
title: "Mac OS"
|
title: "macOS"
|
||||||
asciinema: true
|
asciinema: true
|
||||||
---
|
---
|
||||||
|
|
||||||
# Mac OS
|
# macOS
|
||||||
|
|
||||||
## Download Coco AI
|
## Download Coco AI
|
||||||
|
|
||||||
Goto [https://coco.rs/](https://coco.rs/)
|
Go to [coco.rs](https://coco.rs/) and download the package of your architecture:
|
||||||
|
|
||||||
{{% load-img "/img/download-mac-app.png" "" %}}
|
{{% load-img "/img/macos/mac-download-app.png" "" %}}
|
||||||
|
|
||||||
|
It should be placed in your "Downloads" folder:
|
||||||
|
|
||||||
|
{{% load-img "/img/macos/mac-zip-file.png" "" %}}
|
||||||
|
|
||||||
## Unzip DMG file
|
## Unzip DMG file
|
||||||
|
|
||||||
{{% load-img "/img/unzip-dmg-file.png" "" %}}
|
Unzip the file:
|
||||||
|
|
||||||
|
{{% load-img "/img/macos/mac-unzip-zip-file.png" "" %}}
|
||||||
|
|
||||||
|
You will get a `dmg` file:
|
||||||
|
|
||||||
|
{{% load-img "/img/macos/mac-dmg.png" "" %}}
|
||||||
|
|
||||||
## Drag to Application Folder
|
## Drag to Application Folder
|
||||||
|
|
||||||
{{% load-img "/img/drag-to-application-folder.png" "" %}}
|
Double click the `dmg` file, a window will pop up. Then drag the "Coco-AI" app to
|
||||||
|
your "Applications" folder:
|
||||||
|
|
||||||
|
{{% load-img "/img/macos/drag-to-app-folder.png" "" %}}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,9 @@ asciinema: true
|
|||||||
[if_x11]: https://unix.stackexchange.com/q/202891/498440
|
[if_x11]: https://unix.stackexchange.com/q/202891/498440
|
||||||
|
|
||||||
|
|
||||||
## Goto [https://coco.rs/](https://coco.rs/)
|
## Go to the download page
|
||||||
|
|
||||||
|
Download page: [link](https://coco.rs/#install)
|
||||||
|
|
||||||
## Download the package
|
## Download the package
|
||||||
|
|
||||||
|
|||||||
@@ -8,11 +8,57 @@ title: "Release Notes"
|
|||||||
Information about release notes of Coco Server is provided here.
|
Information about release notes of Coco Server is provided here.
|
||||||
|
|
||||||
## Latest (In development)
|
## Latest (In development)
|
||||||
|
|
||||||
### ❌ Breaking changes
|
### ❌ Breaking changes
|
||||||
|
|
||||||
### 🚀 Features
|
### 🚀 Features
|
||||||
|
|
||||||
### 🐛 Bug fix
|
### 🐛 Bug fix
|
||||||
|
|
||||||
### ✈️ Improvements
|
### ✈️ Improvements
|
||||||
|
|
||||||
|
## 0.5.2 (2025-06-13)
|
||||||
|
|
||||||
|
### ❌ Breaking changes
|
||||||
|
|
||||||
|
### 🚀 Features
|
||||||
|
|
||||||
|
- feat: ai overview support is enabled with shortcut #597
|
||||||
|
- feat: add key monitoring during reset #615
|
||||||
|
- feat: calculator extension add description #623
|
||||||
|
- feat: support right-click actions after text selection #624
|
||||||
|
- feat: add ai overview minimum number of search results configuration #625
|
||||||
|
- feat: add internationalized translations of AI-related extensions #632
|
||||||
|
- feat: context menu support for secondary pages #680
|
||||||
|
|
||||||
|
### 🐛 Bug fix
|
||||||
|
|
||||||
|
- fix: fixed issue with incorrect login status #600
|
||||||
|
- fix: new chat assistant id not found #603
|
||||||
|
- fix: resolve regex error on older macOS versions #605
|
||||||
|
- fix: fix chat log update and sorting issues #612
|
||||||
|
- fix: resolved an issue where number keys were not working on the web #616
|
||||||
|
- fix: do not panic when the datasource specified does not exist #618
|
||||||
|
- fix: fixed modifier keys not working with continue chat #619
|
||||||
|
- fix: invalid DSL error if input contains multiple lines #620
|
||||||
|
- fix: fix ai overview hidden height before message #622
|
||||||
|
- fix: tab key hides window in chat mode #641
|
||||||
|
- fix: arrow keys still navigated search when menu opened with Cmd+K #642
|
||||||
|
- fix: input lost when reopening dialog after search #644
|
||||||
|
- fix: web page unmount event #645
|
||||||
|
- fix: fix the problem of local path not opening #650
|
||||||
|
- fix: number keys not following settings #661
|
||||||
|
- fix: fix problem with up and down key indexing #676
|
||||||
|
- fix: arrow inserting escape sequences #683
|
||||||
|
|
||||||
|
### ✈️ Improvements
|
||||||
|
|
||||||
|
- chore: initialize current assistant from history #606
|
||||||
|
- chore: add onContextMenu event #629
|
||||||
|
- chore: more logs for the setup process #634
|
||||||
|
- chore: copy supports http protocol #639
|
||||||
|
- chore: add special character filtering #668
|
||||||
|
|
||||||
## 0.5.1 (2025-05-31)
|
## 0.5.1 (2025-05-31)
|
||||||
|
|
||||||
### ❌ Breaking changes
|
### ❌ Breaking changes
|
||||||
|
|||||||
BIN
docs/static/img/download-mac-app.png
vendored
|
Before Width: | Height: | Size: 155 KiB |
BIN
docs/static/img/drag-to-application-folder.png
vendored
|
Before Width: | Height: | Size: 69 KiB |
BIN
docs/static/img/macos/drag-to-app-folder.png
vendored
Normal file
|
After Width: | Height: | Size: 239 KiB |
BIN
docs/static/img/macos/mac-dmg.png
vendored
Normal file
|
After Width: | Height: | Size: 586 KiB |
BIN
docs/static/img/macos/mac-download-app.png
vendored
Normal file
|
After Width: | Height: | Size: 299 KiB |
BIN
docs/static/img/macos/mac-unzip-zip-file.png
vendored
Normal file
|
After Width: | Height: | Size: 650 KiB |
BIN
docs/static/img/macos/mac-zip-file.png
vendored
Normal file
|
After Width: | Height: | Size: 441 KiB |
BIN
docs/static/img/unzip-dmg-file.png
vendored
|
Before Width: | Height: | Size: 121 KiB |
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "coco",
|
"name": "coco",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.5.2",
|
"version": "0.5.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
@@ -27,6 +27,7 @@
|
|||||||
"@tauri-apps/plugin-global-shortcut": "~2.0.0",
|
"@tauri-apps/plugin-global-shortcut": "~2.0.0",
|
||||||
"@tauri-apps/plugin-http": "~2.0.2",
|
"@tauri-apps/plugin-http": "~2.0.2",
|
||||||
"@tauri-apps/plugin-log": "~2.4.0",
|
"@tauri-apps/plugin-log": "~2.4.0",
|
||||||
|
"@tauri-apps/plugin-opener": "^2.2.7",
|
||||||
"@tauri-apps/plugin-os": "^2.2.1",
|
"@tauri-apps/plugin-os": "^2.2.1",
|
||||||
"@tauri-apps/plugin-process": "^2.2.1",
|
"@tauri-apps/plugin-process": "^2.2.1",
|
||||||
"@tauri-apps/plugin-shell": "^2.2.1",
|
"@tauri-apps/plugin-shell": "^2.2.1",
|
||||||
@@ -44,6 +45,7 @@
|
|||||||
"i18next-browser-languagedetector": "^8.1.0",
|
"i18next-browser-languagedetector": "^8.1.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"lucide-react": "^0.461.0",
|
"lucide-react": "^0.461.0",
|
||||||
|
"mdast-util-gfm-autolink-literal": "2.0.0",
|
||||||
"mermaid": "^11.6.0",
|
"mermaid": "^11.6.0",
|
||||||
"nanoid": "^5.1.5",
|
"nanoid": "^5.1.5",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
|
|||||||
75
pnpm-lock.yaml
generated
@@ -35,6 +35,9 @@ importers:
|
|||||||
'@tauri-apps/plugin-log':
|
'@tauri-apps/plugin-log':
|
||||||
specifier: ~2.4.0
|
specifier: ~2.4.0
|
||||||
version: 2.4.0
|
version: 2.4.0
|
||||||
|
'@tauri-apps/plugin-opener':
|
||||||
|
specifier: ^2.2.7
|
||||||
|
version: 2.2.7
|
||||||
'@tauri-apps/plugin-os':
|
'@tauri-apps/plugin-os':
|
||||||
specifier: ^2.2.1
|
specifier: ^2.2.1
|
||||||
version: 2.2.1
|
version: 2.2.1
|
||||||
@@ -86,6 +89,9 @@ importers:
|
|||||||
lucide-react:
|
lucide-react:
|
||||||
specifier: ^0.461.0
|
specifier: ^0.461.0
|
||||||
version: 0.461.0(react@18.3.1)
|
version: 0.461.0(react@18.3.1)
|
||||||
|
mdast-util-gfm-autolink-literal:
|
||||||
|
specifier: 2.0.0
|
||||||
|
version: 2.0.0
|
||||||
mermaid:
|
mermaid:
|
||||||
specifier: ^11.6.0
|
specifier: ^11.6.0
|
||||||
version: 11.6.0
|
version: 11.6.0
|
||||||
@@ -185,7 +191,7 @@ importers:
|
|||||||
version: 1.8.8
|
version: 1.8.8
|
||||||
'@vitejs/plugin-react':
|
'@vitejs/plugin-react':
|
||||||
specifier: ^4.4.1
|
specifier: ^4.4.1
|
||||||
version: 4.4.1(vite@5.4.19(@types/node@22.15.17)(sass@1.87.0))
|
version: 4.4.1(vite@5.4.19(@types/node@22.15.17)(sass@1.87.0)(terser@5.40.0))
|
||||||
autoprefixer:
|
autoprefixer:
|
||||||
specifier: ^10.4.21
|
specifier: ^10.4.21
|
||||||
version: 10.4.21(postcss@8.5.3)
|
version: 10.4.21(postcss@8.5.3)
|
||||||
@@ -218,7 +224,7 @@ importers:
|
|||||||
version: 5.8.3
|
version: 5.8.3
|
||||||
vite:
|
vite:
|
||||||
specifier: ^5.4.19
|
specifier: ^5.4.19
|
||||||
version: 5.4.19(@types/node@22.15.17)(sass@1.87.0)
|
version: 5.4.19(@types/node@22.15.17)(sass@1.87.0)(terser@5.40.0)
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@@ -816,6 +822,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==}
|
resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==}
|
||||||
engines: {node: '>=6.0.0'}
|
engines: {node: '>=6.0.0'}
|
||||||
|
|
||||||
|
'@jridgewell/source-map@0.3.6':
|
||||||
|
resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==}
|
||||||
|
|
||||||
'@jridgewell/sourcemap-codec@1.5.0':
|
'@jridgewell/sourcemap-codec@1.5.0':
|
||||||
resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==}
|
resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==}
|
||||||
|
|
||||||
@@ -1259,6 +1268,9 @@ packages:
|
|||||||
'@tauri-apps/plugin-log@2.4.0':
|
'@tauri-apps/plugin-log@2.4.0':
|
||||||
resolution: {integrity: sha512-j7yrDtLNmayCBOO2esl3aZv9jSXy2an8MDLry3Ys9ZXerwUg35n1Y2uD8HoCR+8Ng/EUgx215+qOUfJasjYrHw==}
|
resolution: {integrity: sha512-j7yrDtLNmayCBOO2esl3aZv9jSXy2an8MDLry3Ys9ZXerwUg35n1Y2uD8HoCR+8Ng/EUgx215+qOUfJasjYrHw==}
|
||||||
|
|
||||||
|
'@tauri-apps/plugin-opener@2.2.7':
|
||||||
|
resolution: {integrity: sha512-uduEyvOdjpPOEeDRrhwlCspG/f9EQalHumWBtLBnp3fRp++fKGLqDOyUhSIn7PzX45b/rKep//ZQSAQoIxobLA==}
|
||||||
|
|
||||||
'@tauri-apps/plugin-os@2.2.1':
|
'@tauri-apps/plugin-os@2.2.1':
|
||||||
resolution: {integrity: sha512-cNYpNri2CCc6BaNeB6G/mOtLvg8dFyFQyCUdf2y0K8PIAKGEWdEcu8DECkydU2B+oj4OJihDPD2de5K6cbVl9A==}
|
resolution: {integrity: sha512-cNYpNri2CCc6BaNeB6G/mOtLvg8dFyFQyCUdf2y0K8PIAKGEWdEcu8DECkydU2B+oj4OJihDPD2de5K6cbVl9A==}
|
||||||
|
|
||||||
@@ -1586,6 +1598,9 @@ packages:
|
|||||||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
buffer-from@1.1.2:
|
||||||
|
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
||||||
|
|
||||||
bundle-name@4.1.0:
|
bundle-name@4.1.0:
|
||||||
resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==}
|
resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -1698,6 +1713,9 @@ packages:
|
|||||||
comma-separated-tokens@2.0.3:
|
comma-separated-tokens@2.0.3:
|
||||||
resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
|
resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
|
||||||
|
|
||||||
|
commander@2.20.3:
|
||||||
|
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
|
||||||
|
|
||||||
commander@4.1.1:
|
commander@4.1.1:
|
||||||
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
|
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
@@ -2643,8 +2661,8 @@ packages:
|
|||||||
mdast-util-from-markdown@2.0.2:
|
mdast-util-from-markdown@2.0.2:
|
||||||
resolution: {integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==}
|
resolution: {integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==}
|
||||||
|
|
||||||
mdast-util-gfm-autolink-literal@2.0.1:
|
mdast-util-gfm-autolink-literal@2.0.0:
|
||||||
resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==}
|
resolution: {integrity: sha512-FyzMsduZZHSc3i0Px3PQcBT4WJY/X/RCtEJKuybiC6sjPqLv7h1yqAkmILZtuxMSsUyaLUWNp71+vQH2zqp5cg==}
|
||||||
|
|
||||||
mdast-util-gfm-footnote@2.1.0:
|
mdast-util-gfm-footnote@2.1.0:
|
||||||
resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==}
|
resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==}
|
||||||
@@ -3349,6 +3367,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
source-map-support@0.5.21:
|
||||||
|
resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==}
|
||||||
|
|
||||||
source-map@0.6.1:
|
source-map@0.6.1:
|
||||||
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
|
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -3443,6 +3464,11 @@ packages:
|
|||||||
tauri-plugin-windows-version-api@2.0.0:
|
tauri-plugin-windows-version-api@2.0.0:
|
||||||
resolution: {integrity: sha512-tty5n4ASYbXpnsD5ws2iTcTTpDCrSbzRTVp5Bo3UTpYGqlN1gBn2Zk8s3oO4w7VIM5WtJhDM9Jr/UgoTk7tFJQ==}
|
resolution: {integrity: sha512-tty5n4ASYbXpnsD5ws2iTcTTpDCrSbzRTVp5Bo3UTpYGqlN1gBn2Zk8s3oO4w7VIM5WtJhDM9Jr/UgoTk7tFJQ==}
|
||||||
|
|
||||||
|
terser@5.40.0:
|
||||||
|
resolution: {integrity: sha512-cfeKl/jjwSR5ar7d0FGmave9hFGJT8obyo0z+CrQOylLDbk7X81nPU6vq9VORa5jU30SkDnT2FXjLbR8HLP+xA==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
thenify-all@1.6.0:
|
thenify-all@1.6.0:
|
||||||
resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
|
resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
|
||||||
engines: {node: '>=0.8'}
|
engines: {node: '>=0.8'}
|
||||||
@@ -4263,6 +4289,12 @@ snapshots:
|
|||||||
|
|
||||||
'@jridgewell/set-array@1.2.1': {}
|
'@jridgewell/set-array@1.2.1': {}
|
||||||
|
|
||||||
|
'@jridgewell/source-map@0.3.6':
|
||||||
|
dependencies:
|
||||||
|
'@jridgewell/gen-mapping': 0.3.8
|
||||||
|
'@jridgewell/trace-mapping': 0.3.25
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@jridgewell/sourcemap-codec@1.5.0': {}
|
'@jridgewell/sourcemap-codec@1.5.0': {}
|
||||||
|
|
||||||
'@jridgewell/trace-mapping@0.3.25':
|
'@jridgewell/trace-mapping@0.3.25':
|
||||||
@@ -4640,6 +4672,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@tauri-apps/api': 2.5.0
|
'@tauri-apps/api': 2.5.0
|
||||||
|
|
||||||
|
'@tauri-apps/plugin-opener@2.2.7':
|
||||||
|
dependencies:
|
||||||
|
'@tauri-apps/api': 2.5.0
|
||||||
|
|
||||||
'@tauri-apps/plugin-os@2.2.1':
|
'@tauri-apps/plugin-os@2.2.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tauri-apps/api': 2.5.0
|
'@tauri-apps/api': 2.5.0
|
||||||
@@ -4881,14 +4917,14 @@ snapshots:
|
|||||||
|
|
||||||
'@ungap/structured-clone@1.3.0': {}
|
'@ungap/structured-clone@1.3.0': {}
|
||||||
|
|
||||||
'@vitejs/plugin-react@4.4.1(vite@5.4.19(@types/node@22.15.17)(sass@1.87.0))':
|
'@vitejs/plugin-react@4.4.1(vite@5.4.19(@types/node@22.15.17)(sass@1.87.0)(terser@5.40.0))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.27.1
|
'@babel/core': 7.27.1
|
||||||
'@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.27.1)
|
'@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.27.1)
|
||||||
'@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.27.1)
|
'@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.27.1)
|
||||||
'@types/babel__core': 7.20.5
|
'@types/babel__core': 7.20.5
|
||||||
react-refresh: 0.17.0
|
react-refresh: 0.17.0
|
||||||
vite: 5.4.19(@types/node@22.15.17)(sass@1.87.0)
|
vite: 5.4.19(@types/node@22.15.17)(sass@1.87.0)(terser@5.40.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -5017,6 +5053,9 @@ snapshots:
|
|||||||
node-releases: 2.0.19
|
node-releases: 2.0.19
|
||||||
update-browserslist-db: 1.1.3(browserslist@4.24.5)
|
update-browserslist-db: 1.1.3(browserslist@4.24.5)
|
||||||
|
|
||||||
|
buffer-from@1.1.2:
|
||||||
|
optional: true
|
||||||
|
|
||||||
bundle-name@4.1.0:
|
bundle-name@4.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
run-applescript: 7.0.0
|
run-applescript: 7.0.0
|
||||||
@@ -5113,6 +5152,9 @@ snapshots:
|
|||||||
|
|
||||||
comma-separated-tokens@2.0.3: {}
|
comma-separated-tokens@2.0.3: {}
|
||||||
|
|
||||||
|
commander@2.20.3:
|
||||||
|
optional: true
|
||||||
|
|
||||||
commander@4.1.1: {}
|
commander@4.1.1: {}
|
||||||
|
|
||||||
commander@7.2.0: {}
|
commander@7.2.0: {}
|
||||||
@@ -6117,7 +6159,7 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
mdast-util-gfm-autolink-literal@2.0.1:
|
mdast-util-gfm-autolink-literal@2.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/mdast': 4.0.4
|
'@types/mdast': 4.0.4
|
||||||
ccount: 2.0.1
|
ccount: 2.0.1
|
||||||
@@ -6165,7 +6207,7 @@ snapshots:
|
|||||||
mdast-util-gfm@3.1.0:
|
mdast-util-gfm@3.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
mdast-util-from-markdown: 2.0.2
|
mdast-util-from-markdown: 2.0.2
|
||||||
mdast-util-gfm-autolink-literal: 2.0.1
|
mdast-util-gfm-autolink-literal: 2.0.0
|
||||||
mdast-util-gfm-footnote: 2.1.0
|
mdast-util-gfm-footnote: 2.1.0
|
||||||
mdast-util-gfm-strikethrough: 2.0.0
|
mdast-util-gfm-strikethrough: 2.0.0
|
||||||
mdast-util-gfm-table: 2.0.0
|
mdast-util-gfm-table: 2.0.0
|
||||||
@@ -7124,6 +7166,12 @@ snapshots:
|
|||||||
|
|
||||||
source-map-js@1.2.1: {}
|
source-map-js@1.2.1: {}
|
||||||
|
|
||||||
|
source-map-support@0.5.21:
|
||||||
|
dependencies:
|
||||||
|
buffer-from: 1.1.2
|
||||||
|
source-map: 0.6.1
|
||||||
|
optional: true
|
||||||
|
|
||||||
source-map@0.6.1:
|
source-map@0.6.1:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -7243,6 +7291,14 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@tauri-apps/api': 2.5.0
|
'@tauri-apps/api': 2.5.0
|
||||||
|
|
||||||
|
terser@5.40.0:
|
||||||
|
dependencies:
|
||||||
|
'@jridgewell/source-map': 0.3.6
|
||||||
|
acorn: 8.14.1
|
||||||
|
commander: 2.20.3
|
||||||
|
source-map-support: 0.5.21
|
||||||
|
optional: true
|
||||||
|
|
||||||
thenify-all@1.6.0:
|
thenify-all@1.6.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
thenify: 3.3.1
|
thenify: 3.3.1
|
||||||
@@ -7429,7 +7485,7 @@ snapshots:
|
|||||||
'@types/unist': 3.0.3
|
'@types/unist': 3.0.3
|
||||||
vfile-message: 4.0.2
|
vfile-message: 4.0.2
|
||||||
|
|
||||||
vite@5.4.19(@types/node@22.15.17)(sass@1.87.0):
|
vite@5.4.19(@types/node@22.15.17)(sass@1.87.0)(terser@5.40.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.21.5
|
esbuild: 0.21.5
|
||||||
postcss: 8.5.3
|
postcss: 8.5.3
|
||||||
@@ -7438,6 +7494,7 @@ snapshots:
|
|||||||
'@types/node': 22.15.17
|
'@types/node': 22.15.17
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
sass: 1.87.0
|
sass: 1.87.0
|
||||||
|
terser: 5.40.0
|
||||||
|
|
||||||
void-elements@3.1.0: {}
|
void-elements@3.1.0: {}
|
||||||
|
|
||||||
|
|||||||
1
scripts/devWeb.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
(() => {})();
|
||||||
25
src-tauri/Cargo.lock
generated
@@ -821,7 +821,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "coco"
|
name = "coco"
|
||||||
version = "0.5.2"
|
version = "0.5.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"applications",
|
"applications",
|
||||||
@@ -866,6 +866,7 @@ dependencies = [
|
|||||||
"tauri-plugin-http",
|
"tauri-plugin-http",
|
||||||
"tauri-plugin-log",
|
"tauri-plugin-log",
|
||||||
"tauri-plugin-macos-permissions",
|
"tauri-plugin-macos-permissions",
|
||||||
|
"tauri-plugin-opener",
|
||||||
"tauri-plugin-os",
|
"tauri-plugin-os",
|
||||||
"tauri-plugin-process",
|
"tauri-plugin-process",
|
||||||
"tauri-plugin-screenshots",
|
"tauri-plugin-screenshots",
|
||||||
@@ -6279,6 +6280,28 @@ dependencies = [
|
|||||||
"thiserror 2.0.12",
|
"thiserror 2.0.12",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-plugin-opener"
|
||||||
|
version = "2.2.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "66644b71a31ec1a8a52c4a16575edd28cf763c87cf4a7da24c884122b5c77097"
|
||||||
|
dependencies = [
|
||||||
|
"dunce",
|
||||||
|
"glob",
|
||||||
|
"objc2-app-kit 0.3.1",
|
||||||
|
"objc2-foundation 0.3.1",
|
||||||
|
"open",
|
||||||
|
"schemars",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tauri",
|
||||||
|
"tauri-plugin",
|
||||||
|
"thiserror 2.0.12",
|
||||||
|
"url",
|
||||||
|
"windows 0.61.1",
|
||||||
|
"zbus",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-os"
|
name = "tauri-plugin-os"
|
||||||
version = "2.2.1"
|
version = "2.2.1"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "coco"
|
name = "coco"
|
||||||
version = "0.5.2"
|
version = "0.5.0"
|
||||||
description = "Search, connect, collaborate – all in one place."
|
description = "Search, connect, collaborate – all in one place."
|
||||||
authors = ["INFINI Labs"]
|
authors = ["INFINI Labs"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
@@ -44,7 +44,7 @@ use_pizza_engine = []
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
pizza-common = { git = "https://github.com/infinilabs/pizza-common", branch = "main" }
|
pizza-common = { git = "https://github.com/infinilabs/pizza-common", branch = "main" }
|
||||||
|
|
||||||
tauri = { version = "2", features = ["protocol-asset", "macos-private-api", "tray-icon", "image-ico", "image-png", "unstable"] }
|
tauri = { version = "2", features = ["protocol-asset", "macos-private-api", "tray-icon", "image-ico", "image-png"] }
|
||||||
tauri-plugin-shell = "2"
|
tauri-plugin-shell = "2"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
# Need `arbitrary_precision` feature to support storing u128
|
# Need `arbitrary_precision` feature to support storing u128
|
||||||
@@ -98,6 +98,7 @@ derive_more = { version = "2.0.1", features = ["display"] }
|
|||||||
anyhow = "1.0.98"
|
anyhow = "1.0.98"
|
||||||
function_name = "0.3.0"
|
function_name = "0.3.0"
|
||||||
regex = "1.11.1"
|
regex = "1.11.1"
|
||||||
|
tauri-plugin-opener = "2"
|
||||||
|
|
||||||
[target."cfg(target_os = \"macos\")".dependencies]
|
[target."cfg(target_os = \"macos\")".dependencies]
|
||||||
tauri-nspanel = { git = "https://github.com/ahkohd/tauri-nspanel", branch = "v2" }
|
tauri-nspanel = { git = "https://github.com/ahkohd/tauri-nspanel", branch = "v2" }
|
||||||
|
|||||||
@@ -71,6 +71,7 @@
|
|||||||
"process:default",
|
"process:default",
|
||||||
"updater:default",
|
"updater:default",
|
||||||
"windows-version:default",
|
"windows-version:default",
|
||||||
"log:default"
|
"log:default",
|
||||||
|
"opener:default"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,38 +3,43 @@ use std::{fs::create_dir, io::Read};
|
|||||||
use tauri::{Manager, Runtime};
|
use tauri::{Manager, Runtime};
|
||||||
use tauri_plugin_autostart::ManagerExt;
|
use tauri_plugin_autostart::ManagerExt;
|
||||||
|
|
||||||
// Start or stop according to configuration
|
/// If the state reported from the OS and the state stored by us differ, our state is
|
||||||
pub fn enable_autostart(app: &mut tauri::App) {
|
/// prioritized and seen as the correct one. Update the OS state to make them consistent.
|
||||||
use tauri_plugin_autostart::MacosLauncher;
|
pub fn ensure_autostart_state_consistent(app: &mut tauri::App) -> Result<(), String> {
|
||||||
use tauri_plugin_autostart::ManagerExt;
|
|
||||||
|
|
||||||
app.handle()
|
|
||||||
.plugin(tauri_plugin_autostart::init(
|
|
||||||
MacosLauncher::AppleScript,
|
|
||||||
None,
|
|
||||||
))
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let autostart_manager = app.autolaunch();
|
let autostart_manager = app.autolaunch();
|
||||||
|
|
||||||
// close autostart
|
let os_state = autostart_manager.is_enabled().map_err(|e| e.to_string())?;
|
||||||
// autostart_manager.disable().unwrap();
|
let coco_stored_state = current_autostart(app.app_handle()).map_err(|e| e.to_string())?;
|
||||||
// return;
|
|
||||||
|
|
||||||
match (
|
if os_state != coco_stored_state {
|
||||||
autostart_manager.is_enabled(),
|
log::warn!(
|
||||||
current_autostart(app.app_handle()),
|
"autostart inconsistent states, OS state [{}], Coco state [{}], config file could be deleted or corrupted",
|
||||||
) {
|
os_state,
|
||||||
(Ok(false), Ok(true)) => match autostart_manager.enable() {
|
coco_stored_state
|
||||||
Ok(_) => println!("Autostart enabled successfully."),
|
);
|
||||||
Err(err) => eprintln!("Failed to enable autostart: {}", err),
|
log::info!("trying to correct the inconsistent states");
|
||||||
},
|
|
||||||
(Ok(true), Ok(false)) => match autostart_manager.disable() {
|
let result = if coco_stored_state {
|
||||||
Ok(_) => println!("Autostart disable successfully."),
|
autostart_manager.enable()
|
||||||
Err(err) => eprintln!("Failed to disable autostart: {}", err),
|
} else {
|
||||||
},
|
autostart_manager.disable()
|
||||||
_ => (),
|
};
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(_) => {
|
||||||
|
log::info!("inconsistent autostart states fixed");
|
||||||
}
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!(
|
||||||
|
"failed to fix inconsistent autostart state due to error [{}]",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
return Err(e.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn current_autostart<R: Runtime>(app: &tauri::AppHandle<R>) -> Result<bool, String> {
|
fn current_autostart<R: Runtime>(app: &tauri::AppHandle<R>) -> Result<bool, String> {
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use crate::hide_coco;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct RichLabel {
|
pub struct RichLabel {
|
||||||
pub label: Option<String>,
|
pub label: Option<String>,
|
||||||
@@ -93,7 +91,6 @@ pub(crate) async fn open(on_opened: OnOpened) -> Result<(), String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hide_coco(global_tauri_app_handle.clone()).await;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ const TAURI_STORE_APP_ALIAS: &str = "app_alias";
|
|||||||
const TAURI_STORE_KEY_SEARCH_PATH: &str = "search_path";
|
const TAURI_STORE_KEY_SEARCH_PATH: &str = "search_path";
|
||||||
const TAURI_STORE_KEY_DISABLED_APP_LIST: &str = "disabled_app_list";
|
const TAURI_STORE_KEY_DISABLED_APP_LIST: &str = "disabled_app_list";
|
||||||
|
|
||||||
|
|
||||||
/// We use this as:
|
/// We use this as:
|
||||||
///
|
///
|
||||||
/// 1. querysource ID
|
/// 1. querysource ID
|
||||||
@@ -330,8 +329,19 @@ impl<R: Runtime> Task for SearchApplicationsTask<R> {
|
|||||||
let disabled_app_list = get_disabled_app_list(&self.tauri_app_handle);
|
let disabled_app_list = get_disabled_app_list(&self.tauri_app_handle);
|
||||||
|
|
||||||
// TODO: search via alias, implement this when Pizza engine supports update
|
// TODO: search via alias, implement this when Pizza engine supports update
|
||||||
|
//
|
||||||
|
// NOTE: we use the Debug impl rather than Display for `self.query_string` as String's Debug
|
||||||
|
// impl won't interrupt escape characters. So for input like:
|
||||||
|
//
|
||||||
|
// ```text
|
||||||
|
// Google
|
||||||
|
// Chrome
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// It will be passed to Pizza like "Google\nChrome". Using Display impl would result
|
||||||
|
// in an invalid query DSL and serde will complain.
|
||||||
let dsl = format!(
|
let dsl = format!(
|
||||||
"{{ \"query\": {{ \"bool\": {{ \"should\": [ {{ \"match\": {{ \"{FIELD_APP_NAME}\": \"{}\" }} }}, {{ \"prefix\": {{ \"{FIELD_APP_NAME}\": \"{}\" }} }} ] }} }} }}", self.query_string, self.query_string);
|
"{{ \"query\": {{ \"bool\": {{ \"should\": [ {{ \"match\": {{ \"{FIELD_APP_NAME}\": {:?} }} }}, {{ \"prefix\": {{ \"{FIELD_APP_NAME}\": {:?} }} }} ] }} }} }}", self.query_string, self.query_string);
|
||||||
|
|
||||||
let state = state
|
let state = state
|
||||||
.as_mut()
|
.as_mut()
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ impl CalculatorSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_query(query: String) -> Value {
|
fn parse_query(query: &str) -> Value {
|
||||||
let mut query_json = serde_json::Map::new();
|
let mut query_json = serde_json::Map::new();
|
||||||
|
|
||||||
let operators = ["+", "-", "*", "/", "%"];
|
let operators = ["+", "-", "*", "/", "%"];
|
||||||
@@ -48,7 +48,7 @@ fn parse_query(query: String) -> Value {
|
|||||||
query_json.insert("type".to_string(), Value::String("expression".to_string()));
|
query_json.insert("type".to_string(), Value::String("expression".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
query_json.insert("value".to_string(), Value::String(query));
|
query_json.insert("value".to_string(), Value::String(query.to_string()));
|
||||||
|
|
||||||
Value::Object(query_json)
|
Value::Object(query_json)
|
||||||
}
|
}
|
||||||
@@ -128,11 +128,17 @@ impl SearchSource for CalculatorSource {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
match meval::eval_str(query_string) {
|
let query_string_clone = query_string.to_string();
|
||||||
|
let query_source = self.get_type();
|
||||||
|
let base_score = self.base_score;
|
||||||
|
let closure = move || -> QueryResponse {
|
||||||
|
let res_num = meval::eval_str(&query_string_clone);
|
||||||
|
|
||||||
|
match res_num {
|
||||||
Ok(num) => {
|
Ok(num) => {
|
||||||
let mut payload: HashMap<String, Value> = HashMap::new();
|
let mut payload: HashMap<String, Value> = HashMap::new();
|
||||||
|
|
||||||
let payload_query = parse_query(query_string.into());
|
let payload_query = parse_query(&query_string_clone);
|
||||||
let payload_result = parse_result(num);
|
let payload_result = parse_result(num);
|
||||||
|
|
||||||
payload.insert("query".to_string(), payload_query);
|
payload.insert("query".to_string(), payload_query);
|
||||||
@@ -151,19 +157,27 @@ impl SearchSource for CalculatorSource {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
return Ok(QueryResponse {
|
QueryResponse {
|
||||||
source: self.get_type(),
|
source: query_source,
|
||||||
hits: vec![(doc, self.base_score)],
|
hits: vec![(doc, base_score)],
|
||||||
total_hits: 1,
|
total_hits: 1,
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
return Ok(QueryResponse {
|
QueryResponse {
|
||||||
source: self.get_type(),
|
source: query_source,
|
||||||
hits: Vec::new(),
|
hits: Vec::new(),
|
||||||
total_hits: 0,
|
total_hits: 0,
|
||||||
});
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let spawn_result = tokio::task::spawn_blocking(closure).await;
|
||||||
|
|
||||||
|
match spawn_result {
|
||||||
|
Ok(response) => Ok(response),
|
||||||
|
Err(e) => std::panic::resume_unwind(e.into_panic()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,18 +27,32 @@ pub(crate) trait Task: Send + Sync {
|
|||||||
pub(crate) static RUNTIME_TX: OnceLock<tokio::sync::mpsc::UnboundedSender<Box<dyn Task>>> =
|
pub(crate) static RUNTIME_TX: OnceLock<tokio::sync::mpsc::UnboundedSender<Box<dyn Task>>> =
|
||||||
OnceLock::new();
|
OnceLock::new();
|
||||||
|
|
||||||
pub(crate) fn start_pizza_engine_runtime() {
|
/// This function blocks until the runtime thread is ready for accepting tasks.
|
||||||
std::thread::spawn(|| {
|
pub(crate) async fn start_pizza_engine_runtime() {
|
||||||
|
const THREAD_NAME: &str = "Pizza engine runtime thread";
|
||||||
|
|
||||||
|
log::trace!("starting Pizza engine runtime");
|
||||||
|
let (engine_start_signal_tx, engine_start_signal_rx) = tokio::sync::oneshot::channel();
|
||||||
|
|
||||||
|
std::thread::Builder::new()
|
||||||
|
.name(THREAD_NAME.into())
|
||||||
|
.spawn(move || {
|
||||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||||
|
|
||||||
let main = async {
|
let main = async {
|
||||||
let mut states: HashMap<String, Option<Box<dyn SearchSourceState>>> = HashMap::new();
|
let mut states: HashMap<String, Option<Box<dyn SearchSourceState>>> =
|
||||||
|
HashMap::new();
|
||||||
|
|
||||||
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
|
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
|
||||||
RUNTIME_TX.set(tx).unwrap();
|
RUNTIME_TX.set(tx).unwrap();
|
||||||
|
|
||||||
|
engine_start_signal_tx
|
||||||
|
.send(())
|
||||||
|
.expect("engine_start_signal_rx dropped");
|
||||||
|
|
||||||
while let Some(mut task) = rx.recv().await {
|
while let Some(mut task) = rx.recv().await {
|
||||||
let opt_search_source_state = match states.entry(task.search_source_id().into()) {
|
let opt_search_source_state = match states.entry(task.search_source_id().into())
|
||||||
|
{
|
||||||
Entry::Occupied(o) => o.into_mut(),
|
Entry::Occupied(o) => o.into_mut(),
|
||||||
Entry::Vacant(v) => v.insert(None),
|
Entry::Vacant(v) => v.insert(None),
|
||||||
};
|
};
|
||||||
@@ -47,5 +61,16 @@ pub(crate) fn start_pizza_engine_runtime() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
rt.block_on(main);
|
rt.block_on(main);
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|e| {
|
||||||
|
panic!(
|
||||||
|
"failed to start thread [{}] due to error [{}]",
|
||||||
|
THREAD_NAME, e
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
engine_start_signal_rx
|
||||||
|
.await
|
||||||
|
.expect("engine_start_signal_tx dropped, the runtime thread could be dead");
|
||||||
|
log::trace!("Pizza engine runtime started");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -492,17 +492,21 @@ impl SearchSource for ThirdPartyExtensionsSearchSource {
|
|||||||
let opt_data_source = query
|
let opt_data_source = query
|
||||||
.query_strings
|
.query_strings
|
||||||
.get("datasource")
|
.get("datasource")
|
||||||
.map(|owned_str| owned_str.as_str());
|
.map(|owned_str| owned_str.to_string());
|
||||||
|
|
||||||
let mut hits = Vec::new();
|
|
||||||
let extensions_read_lock = self.inner.extensions.read().await;
|
|
||||||
let query_lower = query_string.to_lowercase();
|
let query_lower = query_string.to_lowercase();
|
||||||
|
let inner_clone = Arc::clone(&self.inner);
|
||||||
|
|
||||||
|
let closure = move || {
|
||||||
|
let mut hits = Vec::new();
|
||||||
|
let extensions_read_lock = futures::executor::block_on(async { inner_clone.extensions.read().await });
|
||||||
|
|
||||||
for extension in extensions_read_lock.iter().filter(|ext| ext.enabled) {
|
for extension in extensions_read_lock.iter().filter(|ext| ext.enabled) {
|
||||||
if extension.r#type.contains_sub_items() {
|
if extension.r#type.contains_sub_items() {
|
||||||
if let Some(ref commands) = extension.commands {
|
if let Some(ref commands) = extension.commands {
|
||||||
for command in commands.iter().filter(|cmd| cmd.enabled) {
|
for command in commands.iter().filter(|cmd| cmd.enabled) {
|
||||||
if let Some(hit) = extension_to_hit(command, &query_lower, opt_data_source)
|
if let Some(hit) =
|
||||||
|
extension_to_hit(command, &query_lower, opt_data_source.as_deref())
|
||||||
{
|
{
|
||||||
hits.push(hit);
|
hits.push(hit);
|
||||||
}
|
}
|
||||||
@@ -511,7 +515,9 @@ impl SearchSource for ThirdPartyExtensionsSearchSource {
|
|||||||
|
|
||||||
if let Some(ref scripts) = extension.scripts {
|
if let Some(ref scripts) = extension.scripts {
|
||||||
for script in scripts.iter().filter(|script| script.enabled) {
|
for script in scripts.iter().filter(|script| script.enabled) {
|
||||||
if let Some(hit) = extension_to_hit(script, &query_lower, opt_data_source) {
|
if let Some(hit) =
|
||||||
|
extension_to_hit(script, &query_lower, opt_data_source.as_deref())
|
||||||
|
{
|
||||||
hits.push(hit);
|
hits.push(hit);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -520,19 +526,28 @@ impl SearchSource for ThirdPartyExtensionsSearchSource {
|
|||||||
if let Some(ref quick_links) = extension.quick_links {
|
if let Some(ref quick_links) = extension.quick_links {
|
||||||
for quick_link in quick_links.iter().filter(|link| link.enabled) {
|
for quick_link in quick_links.iter().filter(|link| link.enabled) {
|
||||||
if let Some(hit) =
|
if let Some(hit) =
|
||||||
extension_to_hit(quick_link, &query_lower, opt_data_source)
|
extension_to_hit(quick_link, &query_lower, opt_data_source.as_deref())
|
||||||
{
|
{
|
||||||
hits.push(hit);
|
hits.push(hit);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if let Some(hit) = extension_to_hit(extension, &query_lower, opt_data_source) {
|
if let Some(hit) = extension_to_hit(extension, &query_lower, opt_data_source.as_deref()) {
|
||||||
hits.push(hit);
|
hits.push(hit);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hits
|
||||||
|
};
|
||||||
|
|
||||||
|
let join_result = tokio::task::spawn_blocking(closure).await;
|
||||||
|
|
||||||
|
let hits = match join_result {
|
||||||
|
Ok(hits) => hits,
|
||||||
|
Err(e) => std::panic::resume_unwind(e.into_panic()),
|
||||||
|
};
|
||||||
let total_hits = hits.len();
|
let total_hits = hits.len();
|
||||||
|
|
||||||
Ok(QueryResponse {
|
Ok(QueryResponse {
|
||||||
|
|||||||
@@ -13,16 +13,14 @@ use crate::common::register::SearchSourceRegistry;
|
|||||||
// use crate::common::traits::SearchSource;
|
// use crate::common::traits::SearchSource;
|
||||||
use crate::common::{MAIN_WINDOW_LABEL, SETTINGS_WINDOW_LABEL};
|
use crate::common::{MAIN_WINDOW_LABEL, SETTINGS_WINDOW_LABEL};
|
||||||
use crate::server::servers::{load_or_insert_default_server, load_servers_token};
|
use crate::server::servers::{load_or_insert_default_server, load_servers_token};
|
||||||
use autostart::{change_autostart, enable_autostart};
|
use autostart::{change_autostart, ensure_autostart_state_consistent};
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
use tauri::async_runtime::block_on;
|
use tauri::async_runtime::block_on;
|
||||||
use tauri::plugin::TauriPlugin;
|
use tauri::plugin::TauriPlugin;
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
use tauri::ActivationPolicy;
|
|
||||||
use tauri::{
|
use tauri::{
|
||||||
AppHandle, Emitter, Manager, PhysicalPosition, Runtime, WebviewWindow, Window, WindowEvent,
|
AppHandle, Emitter, Manager, PhysicalPosition, Runtime, WebviewWindow, WindowEvent,
|
||||||
};
|
};
|
||||||
use tauri_plugin_autostart::MacosLauncher;
|
use tauri_plugin_autostart::MacosLauncher;
|
||||||
|
|
||||||
@@ -64,6 +62,8 @@ pub fn run() {
|
|||||||
let ctx = tauri::generate_context!();
|
let ctx = tauri::generate_context!();
|
||||||
|
|
||||||
let mut app_builder = tauri::Builder::default();
|
let mut app_builder = tauri::Builder::default();
|
||||||
|
// Set up logger first
|
||||||
|
app_builder = app_builder.plugin(set_up_tauri_logger());
|
||||||
|
|
||||||
#[cfg(desktop)]
|
#[cfg(desktop)]
|
||||||
{
|
{
|
||||||
@@ -77,7 +77,7 @@ pub fn run() {
|
|||||||
.plugin(tauri_plugin_http::init())
|
.plugin(tauri_plugin_http::init())
|
||||||
.plugin(tauri_plugin_shell::init())
|
.plugin(tauri_plugin_shell::init())
|
||||||
.plugin(tauri_plugin_autostart::init(
|
.plugin(tauri_plugin_autostart::init(
|
||||||
MacosLauncher::AppleScript,
|
MacosLauncher::LaunchAgent,
|
||||||
None,
|
None,
|
||||||
))
|
))
|
||||||
.plugin(tauri_plugin_deep_link::init())
|
.plugin(tauri_plugin_deep_link::init())
|
||||||
@@ -89,7 +89,7 @@ pub fn run() {
|
|||||||
.plugin(tauri_plugin_process::init())
|
.plugin(tauri_plugin_process::init())
|
||||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||||
.plugin(tauri_plugin_windows_version::init())
|
.plugin(tauri_plugin_windows_version::init())
|
||||||
.plugin(set_up_tauri_logger());
|
.plugin(tauri_plugin_opener::init());
|
||||||
|
|
||||||
// Conditional compilation for macOS
|
// Conditional compilation for macOS
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
@@ -162,10 +162,18 @@ pub fn run() {
|
|||||||
crate::common::document::open,
|
crate::common::document::open,
|
||||||
])
|
])
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
log::trace!("hiding Dock icon on macOS");
|
||||||
|
app.set_activation_policy(tauri::ActivationPolicy::Accessory);
|
||||||
|
log::trace!("Dock icon should be hidden now");
|
||||||
|
}
|
||||||
|
|
||||||
let app_handle = app.handle().clone();
|
let app_handle = app.handle().clone();
|
||||||
GLOBAL_TAURI_APP_HANDLE
|
GLOBAL_TAURI_APP_HANDLE
|
||||||
.set(app_handle.clone())
|
.set(app_handle.clone())
|
||||||
.expect("variable already initialized");
|
.expect("variable already initialized");
|
||||||
|
log::trace!("global Tauri app handle set");
|
||||||
|
|
||||||
let registry = SearchSourceRegistry::default();
|
let registry = SearchSourceRegistry::default();
|
||||||
|
|
||||||
@@ -178,10 +186,7 @@ pub fn run() {
|
|||||||
|
|
||||||
shortcut::enable_shortcut(app);
|
shortcut::enable_shortcut(app);
|
||||||
|
|
||||||
enable_autostart(app);
|
ensure_autostart_state_consistent(app)?;
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
app.set_activation_policy(ActivationPolicy::Accessory);
|
|
||||||
|
|
||||||
// app.listen("theme-changed", move |event| {
|
// app.listen("theme-changed", move |event| {
|
||||||
// if let Ok(payload) = serde_json::from_str::<ThemeChangedPayload>(event.payload()) {
|
// if let Ok(payload) = serde_json::from_str::<ThemeChangedPayload>(event.payload()) {
|
||||||
@@ -261,12 +266,12 @@ pub async fn init<R: Runtime>(app_handle: &AppHandle<R>) {
|
|||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
extension::built_in::pizza_engine_runtime::start_pizza_engine_runtime();
|
extension::built_in::pizza_engine_runtime::start_pizza_engine_runtime().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn show_coco<R: Runtime>(app_handle: AppHandle<R>) {
|
async fn show_coco<R: Runtime>(app_handle: AppHandle<R>) {
|
||||||
if let Some(window) = app_handle.get_window(MAIN_WINDOW_LABEL) {
|
if let Some(window) = app_handle.get_webview_window(MAIN_WINDOW_LABEL) {
|
||||||
move_window_to_active_monitor(&window);
|
move_window_to_active_monitor(&window);
|
||||||
|
|
||||||
let _ = window.show();
|
let _ = window.show();
|
||||||
@@ -279,7 +284,7 @@ async fn show_coco<R: Runtime>(app_handle: AppHandle<R>) {
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn hide_coco<R: Runtime>(app: AppHandle<R>) {
|
async fn hide_coco<R: Runtime>(app: AppHandle<R>) {
|
||||||
if let Some(window) = app.get_window(MAIN_WINDOW_LABEL) {
|
if let Some(window) = app.get_webview_window(MAIN_WINDOW_LABEL) {
|
||||||
if let Err(err) = window.hide() {
|
if let Err(err) = window.hide() {
|
||||||
log::error!("Failed to hide the window: {}", err);
|
log::error!("Failed to hide the window: {}", err);
|
||||||
} else {
|
} else {
|
||||||
@@ -290,7 +295,7 @@ async fn hide_coco<R: Runtime>(app: AppHandle<R>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn move_window_to_active_monitor<R: Runtime>(window: &Window<R>) {
|
fn move_window_to_active_monitor<R: Runtime>(window: &WebviewWindow<R>) {
|
||||||
//dbg!("Moving window to active monitor");
|
//dbg!("Moving window to active monitor");
|
||||||
// Try to get the available monitors, handle failure gracefully
|
// Try to get the available monitors, handle failure gracefully
|
||||||
let available_monitors = match window.available_monitors() {
|
let available_monitors = match window.available_monitors() {
|
||||||
@@ -383,38 +388,6 @@ fn move_window_to_active_monitor<R: Runtime>(window: &Window<R>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
fn open_settings(app: &tauri::AppHandle) {
|
|
||||||
use tauri::webview::WebviewBuilder;
|
|
||||||
log::debug!("settings menu item was clicked");
|
|
||||||
let window = app.get_webview_window("settings");
|
|
||||||
if let Some(window) = window {
|
|
||||||
let _ = window.show();
|
|
||||||
let _ = window.unminimize();
|
|
||||||
let _ = window.set_focus();
|
|
||||||
} else {
|
|
||||||
let window = tauri::window::WindowBuilder::new(app, "settings")
|
|
||||||
.title("Settings Window")
|
|
||||||
.fullscreen(false)
|
|
||||||
.resizable(false)
|
|
||||||
.minimizable(false)
|
|
||||||
.maximizable(false)
|
|
||||||
.inner_size(800.0, 600.0)
|
|
||||||
.build()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let webview_builder =
|
|
||||||
WebviewBuilder::new("settings", tauri::WebviewUrl::App("/ui/settings".into()));
|
|
||||||
let _webview = window
|
|
||||||
.add_child(
|
|
||||||
webview_builder,
|
|
||||||
tauri::LogicalPosition::new(0, 0),
|
|
||||||
window.inner_size().unwrap(),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn get_app_search_source<R: Runtime>(app_handle: AppHandle<R>) -> Result<(), String> {
|
async fn get_app_search_source<R: Runtime>(app_handle: AppHandle<R>) -> Result<(), String> {
|
||||||
let (_found_invalid_extensions, extensions) = extension::list_extensions()
|
let (_found_invalid_extensions, extensions) = extension::list_extensions()
|
||||||
@@ -430,7 +403,14 @@ async fn get_app_search_source<R: Runtime>(app_handle: AppHandle<R>) -> Result<(
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn show_settings(app_handle: AppHandle) {
|
async fn show_settings(app_handle: AppHandle) {
|
||||||
open_settings(&app_handle);
|
log::debug!("settings menu item was clicked");
|
||||||
|
let window = app_handle
|
||||||
|
.get_webview_window(SETTINGS_WINDOW_LABEL)
|
||||||
|
.expect("we have a settings window");
|
||||||
|
|
||||||
|
window.show().unwrap();
|
||||||
|
window.unminimize().unwrap();
|
||||||
|
window.set_focus().unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -585,6 +565,12 @@ fn set_up_tauri_logger() -> TauriPlugin<tauri::Wry> {
|
|||||||
builder
|
builder
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// When running the built binary, set `COCO_LOG` to `coco_lib=trace` to capture all logs
|
||||||
|
// that come from Coco in the log file, which helps with debugging.
|
||||||
|
if !tauri::is_dev() {
|
||||||
|
std::env::set_var("COCO_LOG", "coco_lib=trace");
|
||||||
|
}
|
||||||
|
|
||||||
let mut builder = tauri_plugin_log::Builder::new();
|
let mut builder = tauri_plugin_log::Builder::new();
|
||||||
builder = builder.format(|out, message, record| {
|
builder = builder.format(|out, message, record| {
|
||||||
let now = chrono::Local::now().format("%m-%d %H:%M:%S");
|
let now = chrono::Local::now().format("%m-%d %H:%M:%S");
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ fn same_type_futures(
|
|||||||
timeout_duration: Duration,
|
timeout_duration: Duration,
|
||||||
search_query: SearchQuery,
|
search_query: SearchQuery,
|
||||||
) -> impl Future<
|
) -> impl Future<
|
||||||
Output=(
|
Output = (
|
||||||
QuerySource,
|
QuerySource,
|
||||||
Result<Result<QueryResponse, SearchError>, Elapsed>,
|
Result<Result<QueryResponse, SearchError>, Elapsed>,
|
||||||
),
|
),
|
||||||
@@ -92,11 +92,30 @@ pub async fn query_coco_fusion<R: Runtime>(
|
|||||||
query_source_id
|
query_source_id
|
||||||
);
|
);
|
||||||
|
|
||||||
let query_source_trait_object_index = sources_list
|
let opt_query_source_trait_object_index = sources_list
|
||||||
.iter()
|
.iter()
|
||||||
.position(|query_source| &query_source.get_type().id == query_source_id).unwrap_or_else(|| {
|
.position(|query_source| &query_source.get_type().id == query_source_id);
|
||||||
panic!("frontend code invoked {}() with parameter [querysource={}], but we do not have this query source, the states are inconsistent! Available query sources {:?}", function_name!(), query_source_id, sources_list.iter().map(|qs| qs.get_type().id).collect::<Vec<_>>());
|
|
||||||
|
let Some(query_source_trait_object_index) = opt_query_source_trait_object_index else {
|
||||||
|
// It is possible (an edge case) that the frontend invokes `query_coco_fusion()` with a
|
||||||
|
// datasource that does not exist in the source list:
|
||||||
|
//
|
||||||
|
// 1. Search applications
|
||||||
|
// 2. Navigate to the application sub page
|
||||||
|
// 3. Disable the application extension in settings
|
||||||
|
// 4. hide the search window
|
||||||
|
// 5. Re-open the search window and search for something
|
||||||
|
//
|
||||||
|
// The application search source is not in the source list because the extension
|
||||||
|
// has been disabled, but the last search is indeed invoked with parameter
|
||||||
|
// `datasource=application`.
|
||||||
|
return Ok(MultiSourceQueryResponse {
|
||||||
|
failed: Vec::new(),
|
||||||
|
hits: Vec::new(),
|
||||||
|
total_hits: 0,
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
let query_source_trait_object = sources_list.remove(query_source_trait_object_index);
|
let query_source_trait_object = sources_list.remove(query_source_trait_object_index);
|
||||||
let query_source = query_source_trait_object.get_type();
|
let query_source = query_source_trait_object.get_type();
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
//credits to: https://github.com/ayangweb/ayangweb-EcoPaste/blob/169323dbe6365ffe4abb64d867439ed2ea84c6d1/src-tauri/src/core/setup/mac.rs
|
//credits to: https://github.com/ayangweb/ayangweb-EcoPaste/blob/169323dbe6365ffe4abb64d867439ed2ea84c6d1/src-tauri/src/core/setup/mac.rs
|
||||||
use tauri::{ActivationPolicy, App, Emitter, EventTarget, WebviewWindow};
|
use tauri::{App, Emitter, EventTarget, WebviewWindow};
|
||||||
use tauri_nspanel::{cocoa::appkit::NSWindowCollectionBehavior, panel_delegate, WebviewWindowExt};
|
use tauri_nspanel::{cocoa::appkit::NSWindowCollectionBehavior, panel_delegate, WebviewWindowExt};
|
||||||
|
|
||||||
use crate::common::MAIN_WINDOW_LABEL;
|
use crate::common::MAIN_WINDOW_LABEL;
|
||||||
@@ -12,9 +12,7 @@ const WINDOW_BLUR_EVENT: &str = "tauri://blur";
|
|||||||
const WINDOW_MOVED_EVENT: &str = "tauri://move";
|
const WINDOW_MOVED_EVENT: &str = "tauri://move";
|
||||||
const WINDOW_RESIZED_EVENT: &str = "tauri://resize";
|
const WINDOW_RESIZED_EVENT: &str = "tauri://resize";
|
||||||
|
|
||||||
pub fn platform(app: &mut App, main_window: WebviewWindow, _settings_window: WebviewWindow) {
|
pub fn platform(_app: &mut App, main_window: WebviewWindow, _settings_window: WebviewWindow) {
|
||||||
app.set_activation_policy(ActivationPolicy::Accessory);
|
|
||||||
|
|
||||||
// Convert ns_window to ns_panel
|
// Convert ns_window to ns_panel
|
||||||
let panel = main_window.to_panel().unwrap();
|
let panel = main_window.to_panel().unwrap();
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ pub use linux::*;
|
|||||||
|
|
||||||
pub fn default(app: &mut App, main_window: WebviewWindow, settings_window: WebviewWindow) {
|
pub fn default(app: &mut App, main_window: WebviewWindow, settings_window: WebviewWindow) {
|
||||||
// Development mode automatically opens the console: https://tauri.app/develop/debug
|
// Development mode automatically opens the console: https://tauri.app/develop/debug
|
||||||
#[cfg(all(dev, debug_assertions))]
|
#[cfg(debug_assertions)]
|
||||||
main_window.open_devtools();
|
main_window.open_devtools();
|
||||||
|
|
||||||
platform(app, main_window.clone(), settings_window.clone());
|
platform(app, main_window.clone(), settings_window.clone());
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ const DEFAULT_SHORTCUT: &str = "ctrl+shift+space";
|
|||||||
|
|
||||||
/// Set up the shortcut upon app start.
|
/// Set up the shortcut upon app start.
|
||||||
pub fn enable_shortcut(app: &App) {
|
pub fn enable_shortcut(app: &App) {
|
||||||
|
log::trace!("setting up Coco hotkey");
|
||||||
let store = app
|
let store = app
|
||||||
.store(COCO_TAURI_STORE)
|
.store(COCO_TAURI_STORE)
|
||||||
.expect("creating a store should not fail");
|
.expect("creating a store should not fail");
|
||||||
@@ -43,6 +44,7 @@ pub fn enable_shortcut(app: &App) {
|
|||||||
.expect("default shortcut should never be invalid");
|
.expect("default shortcut should never be invalid");
|
||||||
_register_shortcut_upon_start(app, default_shortcut);
|
_register_shortcut_upon_start(app, default_shortcut);
|
||||||
}
|
}
|
||||||
|
log::trace!("Coco hotkey has been set");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the stored shortcut as a string, same as [`_get_shortcut()`], except that
|
/// Get the stored shortcut as a string, same as [`_get_shortcut()`], except that
|
||||||
@@ -97,7 +99,7 @@ fn _register_shortcut<R: Runtime>(app: &AppHandle<R>, shortcut: Shortcut) {
|
|||||||
.on_shortcut(shortcut, move |app, scut, event| {
|
.on_shortcut(shortcut, move |app, scut, event| {
|
||||||
if scut == &shortcut {
|
if scut == &shortcut {
|
||||||
dbg!("shortcut pressed");
|
dbg!("shortcut pressed");
|
||||||
let main_window = app.get_window(MAIN_WINDOW_LABEL).unwrap();
|
let main_window = app.get_webview_window(MAIN_WINDOW_LABEL).unwrap();
|
||||||
if let ShortcutState::Pressed = event.state() {
|
if let ShortcutState::Pressed = event.state() {
|
||||||
let app_handle = app.clone();
|
let app_handle = app.clone();
|
||||||
if main_window.is_visible().unwrap() {
|
if main_window.is_visible().unwrap() {
|
||||||
@@ -126,7 +128,7 @@ fn _register_shortcut_upon_start(app: &App, shortcut: Shortcut) {
|
|||||||
tauri_plugin_global_shortcut::Builder::new()
|
tauri_plugin_global_shortcut::Builder::new()
|
||||||
.with_handler(move |app, scut, event| {
|
.with_handler(move |app, scut, event| {
|
||||||
if scut == &shortcut {
|
if scut == &shortcut {
|
||||||
let window = app.get_window(MAIN_WINDOW_LABEL).unwrap();
|
let window = app.get_webview_window(MAIN_WINDOW_LABEL).unwrap();
|
||||||
if let ShortcutState::Pressed = event.state() {
|
if let ShortcutState::Pressed = event.state() {
|
||||||
let app_handle = app.clone();
|
let app_handle = app.clone();
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ const WHITELIST_SERVERS = [
|
|||||||
"refresh_coco_server_info",
|
"refresh_coco_server_info",
|
||||||
"handle_sso_callback",
|
"handle_sso_callback",
|
||||||
"query_coco_fusion",
|
"query_coco_fusion",
|
||||||
|
"open_session_chat", // TODO: quick ai access is a configured service, even if the current service is not logged in, it should not affect the configured service.
|
||||||
];
|
];
|
||||||
|
|
||||||
async function invokeWithErrorHandler<T>(
|
async function invokeWithErrorHandler<T>(
|
||||||
@@ -50,7 +51,7 @@ async function invokeWithErrorHandler<T>(
|
|||||||
const failedResult = result as any;
|
const failedResult = result as any;
|
||||||
if (failedResult.failed?.length > 0 && failedResult?.hits?.length == 0) {
|
if (failedResult.failed?.length > 0 && failedResult?.hits?.length == 0) {
|
||||||
failedResult.failed.forEach((error: any) => {
|
failedResult.failed.forEach((error: any) => {
|
||||||
addError(error.error, 'error');
|
addError(error.error, "error");
|
||||||
// console.error(error.error);
|
// console.error(error.error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,13 @@ export const AssistantFetcher = ({
|
|||||||
serverId?: string;
|
serverId?: string;
|
||||||
}) => {
|
}) => {
|
||||||
try {
|
try {
|
||||||
|
if (isTauri && !currentService?.enabled) {
|
||||||
|
return {
|
||||||
|
total: 0,
|
||||||
|
list: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const { pageSize, current, serverId = currentService?.id } = params;
|
const { pageSize, current, serverId = currentService?.id } = params;
|
||||||
|
|
||||||
const from = (current - 1) * pageSize;
|
const from = (current - 1) * pageSize;
|
||||||
@@ -78,6 +85,7 @@ export const AssistantFetcher = ({
|
|||||||
|
|
||||||
response = await platformAdapter.commands("assistant_search", body);
|
response = await platformAdapter.commands("assistant_search", body);
|
||||||
} else {
|
} else {
|
||||||
|
body.serverId = undefined;
|
||||||
const [error, res] = await Post(`/assistant/_search`, body);
|
const [error, res] = await Post(`/assistant/_search`, body);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import { AssistantFetcher } from "./AssistantFetcher";
|
|||||||
import AssistantItem from "./AssistantItem";
|
import AssistantItem from "./AssistantItem";
|
||||||
import Pagination from "@/components/Common/Pagination";
|
import Pagination from "@/components/Common/Pagination";
|
||||||
import { useSearchStore } from "@/stores/searchStore";
|
import { useSearchStore } from "@/stores/searchStore";
|
||||||
|
import { useChatStore } from "@/stores/chatStore";
|
||||||
|
import { specialCharacterFiltering } from "@/utils"
|
||||||
|
|
||||||
interface AssistantListProps {
|
interface AssistantListProps {
|
||||||
assistantIDs?: string[];
|
assistantIDs?: string[];
|
||||||
@@ -43,17 +45,40 @@ export function AssistantList({ assistantIDs = [] }: AssistantListProps) {
|
|||||||
return state.setAskAiAssistantId;
|
return state.setAskAiAssistantId;
|
||||||
});
|
});
|
||||||
const assistantList = useConnectStore((state) => state.assistantList);
|
const assistantList = useConnectStore((state) => state.assistantList);
|
||||||
|
const connected = useChatStore((state) => {
|
||||||
|
return state.connected;
|
||||||
|
});
|
||||||
|
|
||||||
const { fetchAssistant } = AssistantFetcher({
|
const { fetchAssistant } = AssistantFetcher({
|
||||||
debounceKeyword,
|
debounceKeyword,
|
||||||
assistantIDs,
|
assistantIDs,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { pagination, runAsync } = usePagination(fetchAssistant, {
|
const getAssistants = (params: { current: number; pageSize: number }) => {
|
||||||
|
if (!connected) {
|
||||||
|
return Promise.resolve({
|
||||||
|
total: 0,
|
||||||
|
list: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetchAssistant(params);
|
||||||
|
};
|
||||||
|
|
||||||
|
const { pagination, runAsync } = usePagination(getAssistants, {
|
||||||
defaultPageSize: 5,
|
defaultPageSize: 5,
|
||||||
refreshDeps: [currentService?.id, debounceKeyword],
|
refreshDeps: [
|
||||||
|
currentService?.id,
|
||||||
|
debounceKeyword,
|
||||||
|
currentService?.enabled,
|
||||||
|
connected,
|
||||||
|
],
|
||||||
onSuccess(data) {
|
onSuccess(data) {
|
||||||
setAssistants(data.list);
|
setAssistants(data.list);
|
||||||
|
|
||||||
|
if (data.list.length === 0) {
|
||||||
|
setCurrentAssistant(void 0);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -215,7 +240,8 @@ export function AssistantList({ assistantIDs = [] }: AssistantListProps) {
|
|||||||
placeholder={t("assistant.popover.search")}
|
placeholder={t("assistant.popover.search")}
|
||||||
className="w-full h-8 px-2 bg-transparent border rounded-md dark:border-white/10"
|
className="w-full h-8 px-2 bg-transparent border rounded-md dark:border-white/10"
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
setKeyword(event.target.value.trim());
|
const value = specialCharacterFiltering(event.target.value.trim())
|
||||||
|
setKeyword(value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</VisibleKey>
|
</VisibleKey>
|
||||||
|
|||||||
@@ -80,7 +80,12 @@ const ChatAI = memo(
|
|||||||
const { curChatEnd, setCurChatEnd, connected, setConnected } =
|
const { curChatEnd, setCurChatEnd, connected, setConnected } =
|
||||||
useChatStore();
|
useChatStore();
|
||||||
|
|
||||||
|
const isTauri = useAppStore((state) => state.isTauri);
|
||||||
|
|
||||||
const isCurrentLogin = useAuthStore((state) => state.isCurrentLogin);
|
const isCurrentLogin = useAuthStore((state) => state.isCurrentLogin);
|
||||||
|
const setIsCurrentLogin = useAuthStore((state) => {
|
||||||
|
return state.setIsCurrentLogin;
|
||||||
|
});
|
||||||
|
|
||||||
const visibleStartPage = useConnectStore((state) => {
|
const visibleStartPage = useConnectStore((state) => {
|
||||||
return state.visibleStartPage;
|
return state.visibleStartPage;
|
||||||
@@ -102,11 +107,27 @@ const ChatAI = memo(
|
|||||||
const askAiServerId = useSearchStore((state) => {
|
const askAiServerId = useSearchStore((state) => {
|
||||||
return state.askAiServerId;
|
return state.askAiServerId;
|
||||||
});
|
});
|
||||||
|
const currentService = useConnectStore((state) => {
|
||||||
|
return state.currentService;
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
activeChatProp && setActiveChat(activeChatProp);
|
activeChatProp && setActiveChat(activeChatProp);
|
||||||
}, [activeChatProp]);
|
}, [activeChatProp]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isTauri) return;
|
||||||
|
|
||||||
|
if (!currentService?.enabled) {
|
||||||
|
setActiveChat(void 0);
|
||||||
|
setIsCurrentLogin(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showChatHistory && connected) {
|
||||||
|
getChatHistory();
|
||||||
|
}
|
||||||
|
}, [currentService?.enabled, showChatHistory, connected]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (askAiServerId || !askAiSessionId || chats.length === 0) return;
|
if (askAiServerId || !askAiSessionId || chats.length === 0) return;
|
||||||
|
|
||||||
@@ -318,6 +339,7 @@ const ChatAI = memo(
|
|||||||
(chatId: string, title: string) => {
|
(chatId: string, title: string) => {
|
||||||
setChats((prev) => {
|
setChats((prev) => {
|
||||||
const chatIndex = prev.findIndex((chat) => chat._id === chatId);
|
const chatIndex = prev.findIndex((chat) => chat._id === chatId);
|
||||||
|
|
||||||
if (chatIndex === -1) return prev;
|
if (chatIndex === -1) return prev;
|
||||||
|
|
||||||
const modifiedChat = {
|
const modifiedChat = {
|
||||||
@@ -326,8 +348,8 @@ const ChatAI = memo(
|
|||||||
};
|
};
|
||||||
|
|
||||||
const result = [...prev];
|
const result = [...prev];
|
||||||
result.splice(chatIndex, 1);
|
result.splice(chatIndex, 1, modifiedChat);
|
||||||
return [modifiedChat, ...result];
|
return result;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (activeChat?._id === chatId) {
|
if (activeChat?._id === chatId) {
|
||||||
@@ -372,7 +394,8 @@ const ChatAI = memo(
|
|||||||
assistantIDs={assistantIDs}
|
assistantIDs={assistantIDs}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{isCurrentLogin ? (<>
|
{isCurrentLogin ? (
|
||||||
|
<>
|
||||||
<ChatContent
|
<ChatContent
|
||||||
activeChat={activeChat}
|
activeChat={activeChat}
|
||||||
curChatEnd={curChatEnd}
|
curChatEnd={curChatEnd}
|
||||||
@@ -391,10 +414,8 @@ const ChatAI = memo(
|
|||||||
}
|
}
|
||||||
getFileUrl={getFileUrl}
|
getFileUrl={getFileUrl}
|
||||||
/>
|
/>
|
||||||
<Splash assistantIDs={assistantIDs} startPage={startPage}/>
|
<Splash assistantIDs={assistantIDs} startPage={startPage} />
|
||||||
</>
|
</>
|
||||||
|
|
||||||
|
|
||||||
) : (
|
) : (
|
||||||
<ConnectPrompt />
|
<ConnectPrompt />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ export const ChatContent = ({
|
|||||||
const { scrollToBottom } = useChatScroll(messagesEndRef);
|
const { scrollToBottom } = useChatScroll(messagesEndRef);
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
const [isAtBottom, setIsAtBottom] = useState(true);
|
const [isAtBottom, setIsAtBottom] = useState(true);
|
||||||
|
const visibleStartPage = useConnectStore((state) => state.visibleStartPage);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsAtBottom(true);
|
setIsAtBottom(true);
|
||||||
@@ -100,7 +101,8 @@ export const ChatContent = ({
|
|||||||
className="flex-1 w-full overflow-x-hidden overflow-y-auto border-t border-[rgba(0,0,0,0.1)] dark:border-[rgba(255,255,255,0.15)] custom-scrollbar relative"
|
className="flex-1 w-full overflow-x-hidden overflow-y-auto border-t border-[rgba(0,0,0,0.1)] dark:border-[rgba(255,255,255,0.15)] custom-scrollbar relative"
|
||||||
onScroll={handleScroll}
|
onScroll={handleScroll}
|
||||||
>
|
>
|
||||||
{(!activeChat || activeChat?.messages?.length === 0) && <Greetings />}
|
{(!activeChat || activeChat?.messages?.length === 0) &&
|
||||||
|
!visibleStartPage && <Greetings />}
|
||||||
|
|
||||||
{activeChat?.messages?.map((message, index) => (
|
{activeChat?.messages?.map((message, index) => (
|
||||||
<ChatMessage
|
<ChatMessage
|
||||||
|
|||||||
@@ -39,8 +39,8 @@ export const ChatSidebar: React.FC<ChatSidebarProps> = ({
|
|||||||
>
|
>
|
||||||
{isSidebarOpen && (
|
{isSidebarOpen && (
|
||||||
<HistoryList
|
<HistoryList
|
||||||
id={HISTORY_PANEL_ID}
|
historyPanelId={HISTORY_PANEL_ID}
|
||||||
list={chats}
|
chats={chats}
|
||||||
active={activeChat}
|
active={activeChat}
|
||||||
onSearch={onSearch}
|
onSearch={onSearch}
|
||||||
onRefresh={fetchChatHistory}
|
onRefresh={fetchChatHistory}
|
||||||
|
|||||||
@@ -76,6 +76,10 @@ export function ServerList({ reconnect, clearChat }: ServerListProps) {
|
|||||||
[currentService?.id]
|
[currentService?.id]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchServers(true);
|
||||||
|
}, [currentService?.enabled]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!askAiServerId || serverList.length === 0) return;
|
if (!askAiServerId || serverList.length === 0) return;
|
||||||
|
|
||||||
@@ -180,7 +184,7 @@ export function ServerList({ reconnect, clearChat }: ServerListProps) {
|
|||||||
<div className="p-3">
|
<div className="p-3">
|
||||||
<div className="flex items-center justify-between mb-3 whitespace-nowrap">
|
<div className="flex items-center justify-between mb-3 whitespace-nowrap">
|
||||||
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||||
Servers
|
{t("assistant.chat.servers")}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
@@ -233,7 +237,8 @@ export function ServerList({ reconnect, clearChat }: ServerListProps) {
|
|||||||
{server.name}
|
{server.name}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-500 dark:text-gray-400 truncate max-w-[200px]">
|
<div className="text-xs text-gray-500 dark:text-gray-400 truncate max-w-[200px]">
|
||||||
AI Assistant: {server.stats?.assistant_count || 1}
|
{t("assistant.chat.aiAssistant")}:{" "}
|
||||||
|
{server.stats?.assistant_count || 1}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -72,6 +72,12 @@ const Splash = ({ assistantIDs = [], startPage }: SplashProps) => {
|
|||||||
fetchData();
|
fetchData();
|
||||||
}, [currentService?.id]);
|
}, [currentService?.id]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentService?.enabled) return;
|
||||||
|
|
||||||
|
isTauri && setVisibleStartPage(false);
|
||||||
|
}, [currentService?.enabled]);
|
||||||
|
|
||||||
const settingsAssistantList = useMemo(() => {
|
const settingsAssistantList = useMemo(() => {
|
||||||
return assistantList.filter((item) => {
|
return assistantList.filter((item) => {
|
||||||
return settings?.display_assistants?.includes(item?._source?.id);
|
return settings?.display_assistants?.includes(item?._source?.id);
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { COPY_BUTTON_ID } from "@/constants";
|
import { useState } from "react";
|
||||||
import { useSearchStore } from "@/stores/searchStore";
|
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {
|
import {
|
||||||
Check,
|
Check,
|
||||||
@@ -9,7 +8,8 @@ import {
|
|||||||
Volume2,
|
Volume2,
|
||||||
RotateCcw,
|
RotateCcw,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useState } from "react";
|
|
||||||
|
import { copyToClipboard } from "@/utils";
|
||||||
|
|
||||||
interface MessageActionsProps {
|
interface MessageActionsProps {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -17,6 +17,7 @@ interface MessageActionsProps {
|
|||||||
question?: string;
|
question?: string;
|
||||||
actionClassName?: string;
|
actionClassName?: string;
|
||||||
actionIconSize?: number;
|
actionIconSize?: number;
|
||||||
|
copyButtonId?: string;
|
||||||
onResend?: () => void;
|
onResend?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,6 +29,7 @@ export const MessageActions = ({
|
|||||||
question,
|
question,
|
||||||
actionClassName,
|
actionClassName,
|
||||||
actionIconSize,
|
actionIconSize,
|
||||||
|
copyButtonId,
|
||||||
onResend,
|
onResend,
|
||||||
}: MessageActionsProps) => {
|
}: MessageActionsProps) => {
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
@@ -40,7 +42,7 @@ export const MessageActions = ({
|
|||||||
|
|
||||||
const handleCopy = async () => {
|
const handleCopy = async () => {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(content);
|
await copyToClipboard(content);
|
||||||
setCopied(true);
|
setCopied(true);
|
||||||
const timerID = setTimeout(() => {
|
const timerID = setTimeout(() => {
|
||||||
setCopied(false);
|
setCopied(false);
|
||||||
@@ -91,13 +93,12 @@ export const MessageActions = ({
|
|||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const goAskAi = useSearchStore((state) => state.goAskAi);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx("flex items-center gap-1 mt-2", actionClassName)}>
|
<div className={clsx("flex items-center gap-1 mt-2", actionClassName)}>
|
||||||
{!isRefreshOnly && (
|
{!isRefreshOnly && (
|
||||||
<button
|
<button
|
||||||
id={goAskAi ? COPY_BUTTON_ID : ""}
|
id={copyButtonId}
|
||||||
onClick={handleCopy}
|
onClick={handleCopy}
|
||||||
className="p-1 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg transition-colors"
|
className="p-1 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg transition-colors"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export const UserMessage = ({ messageContent }: UserMessageProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="flex gap-1 items-center"
|
className="flex gap-1 items-center justify-end"
|
||||||
onMouseEnter={() => setShowCopyButton(true)}
|
onMouseEnter={() => setShowCopyButton(true)}
|
||||||
onMouseLeave={() => setShowCopyButton(false)}
|
onMouseLeave={() => setShowCopyButton(false)}
|
||||||
>
|
>
|
||||||
@@ -24,7 +24,7 @@ export const UserMessage = ({ messageContent }: UserMessageProps) => {
|
|||||||
<CopyButton textToCopy={messageContent} />
|
<CopyButton textToCopy={messageContent} />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="px-3 py-2 bg-white dark:bg-[#202126] rounded-xl border border-black/12 dark:border-black/15 font-normal text-sm text-[#333333] dark:text-[#D8D8D8] cursor-pointer select-none"
|
className="max-w-[85%] overflow-auto text-left px-3 py-2 bg-white dark:bg-[#202126] rounded-xl border border-black/12 dark:border-black/15 font-normal text-sm text-[#333333] dark:text-[#D8D8D8] cursor-pointer user-select-text whitespace-pre-wrap"
|
||||||
onDoubleClick={(e) => {
|
onDoubleClick={(e) => {
|
||||||
const selection = window.getSelection();
|
const selection = window.getSelection();
|
||||||
const range = document.createRange();
|
const range = document.createRange();
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ interface ChatMessageProps {
|
|||||||
rootClassName?: string;
|
rootClassName?: string;
|
||||||
actionClassName?: string;
|
actionClassName?: string;
|
||||||
actionIconSize?: number;
|
actionIconSize?: number;
|
||||||
|
copyButtonId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ChatMessage = memo(function ChatMessage({
|
export const ChatMessage = memo(function ChatMessage({
|
||||||
@@ -51,6 +52,7 @@ export const ChatMessage = memo(function ChatMessage({
|
|||||||
rootClassName,
|
rootClassName,
|
||||||
actionClassName,
|
actionClassName,
|
||||||
actionIconSize,
|
actionIconSize,
|
||||||
|
copyButtonId,
|
||||||
}: ChatMessageProps) {
|
}: ChatMessageProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -65,16 +67,17 @@ export const ChatMessage = memo(function ChatMessage({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (assistant_item) {
|
if (assistant_item) {
|
||||||
setAssistant(assistant_item);
|
setAssistant(assistant_item);
|
||||||
} else {
|
return;
|
||||||
let target = currentAssistant;
|
}
|
||||||
|
|
||||||
if (isAssistant && assistant_id && Array.isArray(assistantList)) {
|
if (isAssistant && assistant_id && Array.isArray(assistantList)) {
|
||||||
const found = assistantList.find((item) => item._id === assistant_id);
|
setAssistant(
|
||||||
if (found) {
|
assistantList.find((item) => item._id === assistant_id) ?? {}
|
||||||
target = found;
|
);
|
||||||
}
|
return;
|
||||||
}
|
|
||||||
setAssistant(target);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setAssistant(currentAssistant);
|
||||||
}, [
|
}, [
|
||||||
isAssistant,
|
isAssistant,
|
||||||
assistant_item,
|
assistant_item,
|
||||||
@@ -91,7 +94,6 @@ export const ChatMessage = memo(function ChatMessage({
|
|||||||
isTyping === false && (messageContent || response?.message_chunk);
|
isTyping === false && (messageContent || response?.message_chunk);
|
||||||
|
|
||||||
const [suggestion, setSuggestion] = useState<string[]>([]);
|
const [suggestion, setSuggestion] = useState<string[]>([]);
|
||||||
const visibleStartPage = useConnectStore((state) => state.visibleStartPage);
|
|
||||||
|
|
||||||
const getSuggestion = (suggestion: string[]) => {
|
const getSuggestion = (suggestion: string[]) => {
|
||||||
setSuggestion(suggestion);
|
setSuggestion(suggestion);
|
||||||
@@ -152,6 +154,7 @@ export const ChatMessage = memo(function ChatMessage({
|
|||||||
question={question}
|
question={question}
|
||||||
actionClassName={actionClassName}
|
actionClassName={actionClassName}
|
||||||
actionIconSize={actionIconSize}
|
actionIconSize={actionIconSize}
|
||||||
|
copyButtonId={copyButtonId}
|
||||||
onResend={() => {
|
onResend={() => {
|
||||||
onResend && onResend(question);
|
onResend && onResend(question);
|
||||||
}}
|
}}
|
||||||
@@ -172,9 +175,6 @@ export const ChatMessage = memo(function ChatMessage({
|
|||||||
className={clsx(
|
className={clsx(
|
||||||
"py-8 flex",
|
"py-8 flex",
|
||||||
[isAssistant ? "justify-start" : "justify-end"],
|
[isAssistant ? "justify-start" : "justify-end"],
|
||||||
{
|
|
||||||
hidden: visibleStartPage,
|
|
||||||
},
|
|
||||||
rootClassName
|
rootClassName
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import React, { useState } from "react";
|
|||||||
import { ChevronLeft } from "lucide-react";
|
import { ChevronLeft } from "lucide-react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import { specialCharacterFiltering } from "@/utils/index"
|
||||||
|
|
||||||
interface ConnectServiceProps {
|
interface ConnectServiceProps {
|
||||||
setIsConnect: (isConnect: boolean) => void;
|
setIsConnect: (isConnect: boolean) => void;
|
||||||
onAddServer: (endpoint: string) => void;
|
onAddServer: (endpoint: string) => void;
|
||||||
@@ -26,6 +28,11 @@ export function Connect({ setIsConnect, onAddServer }: ConnectServiceProps) {
|
|||||||
setIsConnect(true);
|
setIsConnect(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onChangeEndpoint = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const value = specialCharacterFiltering(e.target.value)
|
||||||
|
setEndpointLink(value)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-4xl">
|
<div className="max-w-4xl">
|
||||||
<div className="flex items-center gap-2 mb-8">
|
<div className="flex items-center gap-2 mb-8">
|
||||||
@@ -60,7 +67,7 @@ export function Connect({ setIsConnect, onAddServer }: ConnectServiceProps) {
|
|||||||
id="endpoint"
|
id="endpoint"
|
||||||
value={endpointLink}
|
value={endpointLink}
|
||||||
placeholder={t("cloud.connect.serverPlaceholder")}
|
placeholder={t("cloud.connect.serverPlaceholder")}
|
||||||
onChange={(e) => setEndpointLink(e.target.value)}
|
onChange={onChangeEndpoint}
|
||||||
className="text-[#101010] dark:text-white flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-800"
|
className="text-[#101010] dark:text-white flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-800"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -4,10 +4,7 @@ import { RefreshCcw } from "lucide-react";
|
|||||||
|
|
||||||
import { DataSourceItem } from "./DataSourceItem";
|
import { DataSourceItem } from "./DataSourceItem";
|
||||||
import { useConnectStore } from "@/stores/connectStore";
|
import { useConnectStore } from "@/stores/connectStore";
|
||||||
import {
|
import { get_connectors_by_server, datasource_search } from "@/commands";
|
||||||
get_connectors_by_server,
|
|
||||||
datasource_search,
|
|
||||||
} from "@/commands";
|
|
||||||
|
|
||||||
export function DataSourcesList({ server }: { server: string }) {
|
export function DataSourcesList({ server }: { server: string }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -17,8 +14,9 @@ export function DataSourcesList({ server }: { server: string }) {
|
|||||||
const setDatasourceData = useConnectStore((state) => state.setDatasourceData);
|
const setDatasourceData = useConnectStore((state) => state.setDatasourceData);
|
||||||
const setConnectorData = useConnectStore((state) => state.setConnectorData);
|
const setConnectorData = useConnectStore((state) => state.setConnectorData);
|
||||||
|
|
||||||
function initServerAppData({ server }: { server: string }) {
|
function initServerAppData() {
|
||||||
//fetch datasource data
|
setRefreshLoading(true);
|
||||||
|
// fetch connectors data
|
||||||
get_connectors_by_server(server)
|
get_connectors_by_server(server)
|
||||||
.then((res: any) => {
|
.then((res: any) => {
|
||||||
// console.log("get_connectors_by_server", res);
|
// console.log("get_connectors_by_server", res);
|
||||||
@@ -26,31 +24,20 @@ export function DataSourcesList({ server }: { server: string }) {
|
|||||||
})
|
})
|
||||||
.finally(() => {});
|
.finally(() => {});
|
||||||
|
|
||||||
//fetch datasource data
|
// fetch datasource data
|
||||||
datasource_search(server)
|
datasource_search(server)
|
||||||
.then((res: any) => {
|
.then((res: any) => {
|
||||||
// console.log("datasource_search", res);
|
// console.log("datasource_search", res);
|
||||||
setDatasourceData(res, server);
|
setDatasourceData(res, server);
|
||||||
})
|
})
|
||||||
.finally(() => {});
|
.finally(() => {
|
||||||
}
|
|
||||||
|
|
||||||
async function getDatasourceData() {
|
|
||||||
setRefreshLoading(true);
|
|
||||||
try {
|
|
||||||
initServerAppData({ server });
|
|
||||||
} finally {
|
|
||||||
setRefreshLoading(false);
|
setRefreshLoading(false);
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getDatasourceData();
|
initServerAppData();
|
||||||
}, []);
|
}, [server]);
|
||||||
|
|
||||||
// const handleToggle = (id: string, enabled: boolean) => {
|
|
||||||
// console.log("handleToggle", id, enabled);
|
|
||||||
// };
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@@ -58,10 +45,12 @@ export function DataSourcesList({ server }: { server: string }) {
|
|||||||
{t("cloud.dataSource.title")}
|
{t("cloud.dataSource.title")}
|
||||||
<button
|
<button
|
||||||
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 rounded-[6px] bg-white dark:bg-gray-800 border border-[rgba(228,229,239,1)] dark:border-gray-700"
|
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 rounded-[6px] bg-white dark:bg-gray-800 border border-[rgba(228,229,239,1)] dark:border-gray-700"
|
||||||
onClick={() => getDatasourceData()}
|
onClick={() => initServerAppData()}
|
||||||
>
|
>
|
||||||
<RefreshCcw
|
<RefreshCcw
|
||||||
className={`w-3.5 h-3.5 ${refreshLoading ? "animate-spin" : ""}`}
|
className={`w-3.5 h-3.5 transition-transform duration-1000 ${
|
||||||
|
refreshLoading ? "animate-spin" : ""
|
||||||
|
}`}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</h2>
|
</h2>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { memo, useCallback, useEffect, useState } from "react";
|
import { FC, memo, useCallback, useEffect, useState } from "react";
|
||||||
import { Copy } from "lucide-react";
|
import { Copy } from "lucide-react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
@@ -14,6 +14,7 @@ import { OpenURLWithBrowser } from "@/utils";
|
|||||||
import { useConnectStore } from "@/stores/connectStore";
|
import { useConnectStore } from "@/stores/connectStore";
|
||||||
import { useAppStore } from "@/stores/appStore";
|
import { useAppStore } from "@/stores/appStore";
|
||||||
import { logout_coco_server, handle_sso_callback } from "@/commands";
|
import { logout_coco_server, handle_sso_callback } from "@/commands";
|
||||||
|
import { copyToClipboard } from "@/utils";
|
||||||
|
|
||||||
interface ServiceAuthProps {
|
interface ServiceAuthProps {
|
||||||
setRefreshLoading: (loading: boolean) => void;
|
setRefreshLoading: (loading: boolean) => void;
|
||||||
@@ -30,7 +31,9 @@ const ServiceAuth = memo(
|
|||||||
const addError = useAppStore((state) => state.addError);
|
const addError = useAppStore((state) => state.addError);
|
||||||
|
|
||||||
const currentService = useConnectStore((state) => state.currentService);
|
const currentService = useConnectStore((state) => state.currentService);
|
||||||
const setCurrentService = useConnectStore((state) => state.setCurrentService);
|
const setCurrentService = useConnectStore(
|
||||||
|
(state) => state.setCurrentService
|
||||||
|
);
|
||||||
const serverList = useConnectStore((state) => state.serverList);
|
const serverList = useConnectStore((state) => state.serverList);
|
||||||
const setServerList = useConnectStore((state) => state.setServerList);
|
const setServerList = useConnectStore((state) => state.setServerList);
|
||||||
|
|
||||||
@@ -63,7 +66,7 @@ const ServiceAuth = memo(
|
|||||||
emit("login_or_logout", false);
|
emit("login_or_logout", false);
|
||||||
// update server profile
|
// update server profile
|
||||||
setCurrentService({ ...currentService, profile: null });
|
setCurrentService({ ...currentService, profile: null });
|
||||||
const updatedServerList = serverList.map(server =>
|
const updatedServerList = serverList.map((server) =>
|
||||||
server.id === id ? { ...server, profile: null } : server
|
server.id === id ? { ...server, profile: null } : server
|
||||||
);
|
);
|
||||||
console.log("updatedServerList", updatedServerList);
|
console.log("updatedServerList", updatedServerList);
|
||||||
@@ -130,7 +133,6 @@ const ServiceAuth = memo(
|
|||||||
|
|
||||||
// Fetch the initial deep link intent
|
// Fetch the initial deep link intent
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLoading(false);
|
|
||||||
// Function to handle pasted URL
|
// Function to handle pasted URL
|
||||||
const handlePaste = (event: any) => {
|
const handlePaste = (event: any) => {
|
||||||
const pastedText = event.clipboardData.getData("text").trim();
|
const pastedText = event.clipboardData.getData("text").trim();
|
||||||
@@ -172,6 +174,10 @@ const ServiceAuth = memo(
|
|||||||
};
|
};
|
||||||
}, [ssoRequestID]);
|
}, [ssoRequestID]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLoading(false);
|
||||||
|
}, [currentService]);
|
||||||
|
|
||||||
if (!currentService?.auth_provider?.sso?.url) {
|
if (!currentService?.auth_provider?.sso?.url) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -197,7 +203,7 @@ const ServiceAuth = memo(
|
|||||||
<LoadingState
|
<LoadingState
|
||||||
onCancel={() => setLoading(false)}
|
onCancel={() => setLoading(false)}
|
||||||
onCopy={() => {
|
onCopy={() => {
|
||||||
navigator.clipboard.writeText(
|
copyToClipboard(
|
||||||
`${currentService?.auth_provider?.sso?.url}/?provider=${currentService?.id}&product=coco&request_id=${ssoRequestID}`
|
`${currentService?.auth_provider?.sso?.url}/?provider=${currentService?.id}&product=coco&request_id=${ssoRequestID}`
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
@@ -222,8 +228,14 @@ const ServiceAuth = memo(
|
|||||||
|
|
||||||
export default ServiceAuth;
|
export default ServiceAuth;
|
||||||
|
|
||||||
const LoginButton = memo(({ LoginClick }: { LoginClick: () => void }) => {
|
interface LoginButtonProps {
|
||||||
|
LoginClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LoginButton: FC<LoginButtonProps> = memo((props) => {
|
||||||
|
const { LoginClick } = props;
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className="px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors mb-3"
|
className="px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors mb-3"
|
||||||
@@ -235,9 +247,15 @@ const LoginButton = memo(({ LoginClick }: { LoginClick: () => void }) => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const LoadingState = memo(
|
interface LoadingStateProps {
|
||||||
({ onCancel, onCopy }: { onCancel: () => void; onCopy: () => void }) => {
|
onCancel: () => void;
|
||||||
|
onCopy: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LoadingState: FC<LoadingStateProps> = memo((props) => {
|
||||||
|
const { onCancel, onCopy } = props;
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<button
|
<button
|
||||||
@@ -257,5 +275,4 @@ const LoadingState = memo(
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { User, LogOut } from "lucide-react";
|
import { User, LogOut } from "lucide-react";
|
||||||
|
|
||||||
import { UserProfile as UserInfo } from "@/types/server";
|
import { UserProfile as UserInfo } from "@/types/server";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
interface UserProfileProps {
|
interface UserProfileProps {
|
||||||
server: string; //server's id
|
server: string; //server's id
|
||||||
@@ -14,12 +15,21 @@ export function UserProfile({ server, userInfo, onLogout }: UserProfileProps) {
|
|||||||
console.log("Logout", server);
|
console.log("Logout", server);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const [imageLoadError, setImageLoadError] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<div className="w-12 h-12 bg-gray-100 dark:bg-gray-700 rounded-full flex items-center justify-center">
|
<div className="w-12 h-12 bg-gray-100 dark:bg-gray-700 rounded-full flex items-center justify-center">
|
||||||
{userInfo?.avatar ? (
|
{userInfo?.avatar && !imageLoadError ? (
|
||||||
<img src={userInfo?.avatar} alt="" className="w-6 h-6" />
|
<img
|
||||||
|
src={userInfo?.avatar}
|
||||||
|
alt=""
|
||||||
|
className="w-6 h-6"
|
||||||
|
onError={() => {
|
||||||
|
setImageLoadError(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<User className="w-6 h-6 text-gray-500 dark:text-gray-400" />
|
<User className="w-6 h-6 text-gray-500 dark:text-gray-400" />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Copy, Check } from "lucide-react";
|
import { Copy, Check } from "lucide-react";
|
||||||
|
|
||||||
|
import { copyToClipboard } from "@/utils";
|
||||||
|
|
||||||
interface CopyButtonProps {
|
interface CopyButtonProps {
|
||||||
textToCopy: string;
|
textToCopy: string;
|
||||||
}
|
}
|
||||||
@@ -10,7 +12,7 @@ export const CopyButton = ({ textToCopy }: CopyButtonProps) => {
|
|||||||
|
|
||||||
const handleCopy = async () => {
|
const handleCopy = async () => {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(textToCopy);
|
await copyToClipboard(textToCopy);
|
||||||
setCopied(true);
|
setCopied(true);
|
||||||
const timerID = setTimeout(() => {
|
const timerID = setTimeout(() => {
|
||||||
setCopied(false);
|
setCopied(false);
|
||||||
|
|||||||
86
src/components/Common/HistoryList/DeleteDialog.tsx
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import {
|
||||||
|
Description,
|
||||||
|
Dialog,
|
||||||
|
DialogPanel,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@headlessui/react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import VisibleKey from "@/components/Common/VisibleKey";
|
||||||
|
import { Chat } from "@/types/chat";
|
||||||
|
|
||||||
|
interface DeleteDialogProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
active?: Chat;
|
||||||
|
setIsOpen: (isOpen: boolean) => void;
|
||||||
|
handleRemove: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DeleteDialog = ({
|
||||||
|
isOpen,
|
||||||
|
active,
|
||||||
|
setIsOpen,
|
||||||
|
handleRemove,
|
||||||
|
}: DeleteDialogProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open={isOpen}
|
||||||
|
onClose={() => setIsOpen(false)}
|
||||||
|
className="relative z-1000"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
id="headlessui-popover-panel:delete-history"
|
||||||
|
className="fixed inset-0 flex items-center justify-center w-screen"
|
||||||
|
>
|
||||||
|
<DialogPanel className="flex flex-col justify-between w-[360px] h-[160px] p-3 text-[#333] dark:text-white/90 border border-[#e6e6e6] bg-white dark:bg-[#202126] dark:border-white/10 shadow-xl rounded-lg">
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<DialogTitle className="text-base font-bold">
|
||||||
|
{t("history_list.delete_modal.title")}
|
||||||
|
</DialogTitle>
|
||||||
|
<Description className="text-sm">
|
||||||
|
{t("history_list.delete_modal.description", {
|
||||||
|
replace: [
|
||||||
|
active?._source?.title ||
|
||||||
|
active?._source?.message ||
|
||||||
|
active?._id,
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
</Description>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-4 self-end">
|
||||||
|
<VisibleKey
|
||||||
|
shortcut="N"
|
||||||
|
shortcutClassName="left-[unset] right-0"
|
||||||
|
onKeyPress={() => setIsOpen(false)}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="h-8 px-4 text-sm text-[#666666] bg-[#F8F9FA] dark:text-white dark:bg-[#202126] border border-[#E6E6E6] dark:border-white/10 rounded-lg"
|
||||||
|
onClick={() => setIsOpen(false)}
|
||||||
|
>
|
||||||
|
{t("history_list.delete_modal.button.cancel")}
|
||||||
|
</button>
|
||||||
|
</VisibleKey>
|
||||||
|
|
||||||
|
<VisibleKey
|
||||||
|
shortcut="Y"
|
||||||
|
shortcutClassName="left-[unset] right-0"
|
||||||
|
onKeyPress={handleRemove}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="h-8 px-4 text-sm text-white bg-[#EF4444] rounded-lg"
|
||||||
|
onClick={handleRemove}
|
||||||
|
>
|
||||||
|
{t("history_list.delete_modal.button.delete")}
|
||||||
|
</button>
|
||||||
|
</VisibleKey>
|
||||||
|
</div>
|
||||||
|
</DialogPanel>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DeleteDialog;
|
||||||
246
src/components/Common/HistoryList/HistoryListContent.tsx
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
import { FC, useCallback, useState, useEffect, useRef, useMemo } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useKeyPress } from "ahooks";
|
||||||
|
import { debounce, groupBy, isNil } from "lodash-es";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import isSameOrAfter from "dayjs/plugin/isSameOrAfter";
|
||||||
|
|
||||||
|
import type { Chat } from "@/types/chat";
|
||||||
|
import NoDataImage from "../NoDataImage";
|
||||||
|
import DeleteDialog from "./DeleteDialog";
|
||||||
|
import HistoryListItem from "./HistoryListItem";
|
||||||
|
|
||||||
|
dayjs.extend(isSameOrAfter);
|
||||||
|
|
||||||
|
interface HistoryListContentProps {
|
||||||
|
chats: Chat[];
|
||||||
|
active?: Chat;
|
||||||
|
onSelect: (chat: Chat) => void;
|
||||||
|
onRename: (chatId: string, title: string) => void;
|
||||||
|
onRemove: (chatId: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const HistoryListContent: FC<HistoryListContentProps> = ({
|
||||||
|
chats,
|
||||||
|
active,
|
||||||
|
onSelect,
|
||||||
|
onRename,
|
||||||
|
onRemove,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const listRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [highlightId, setHighlightId] = useState<string>("");
|
||||||
|
const [highlightItem, setHighlightItem] = useState<Chat>({} as Chat);
|
||||||
|
|
||||||
|
const sortedList = useMemo(() => {
|
||||||
|
if (isNil(chats)) return {};
|
||||||
|
|
||||||
|
const now = dayjs();
|
||||||
|
|
||||||
|
return groupBy(chats, (chat) => {
|
||||||
|
const date = dayjs(chat._source?.created);
|
||||||
|
|
||||||
|
if (date.isSame(now, "day")) {
|
||||||
|
return "history_list.date.today";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (date.isSame(now.subtract(1, "day"), "day")) {
|
||||||
|
return "history_list.date.yesterday";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (date.isSameOrAfter(now.subtract(7, "day"), "day")) {
|
||||||
|
return "history_list.date.last7Days";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (date.isSameOrAfter(now.subtract(30, "day"), "day")) {
|
||||||
|
return "history_list.date.last30Days";
|
||||||
|
}
|
||||||
|
|
||||||
|
return date.format("YYYY-MM");
|
||||||
|
});
|
||||||
|
}, [chats]);
|
||||||
|
|
||||||
|
// Flatten sorted list for navigation while keeping original structure for display
|
||||||
|
const flattenedChats = useMemo(() => {
|
||||||
|
return Object.values(sortedList).flat();
|
||||||
|
}, [sortedList]);
|
||||||
|
|
||||||
|
useKeyPress(["uparrow", "downarrow", "enter"], (_, key) => {
|
||||||
|
const currentIndex = flattenedChats.findIndex(
|
||||||
|
(chat) => chat._id === highlightId
|
||||||
|
);
|
||||||
|
const length = flattenedChats.length;
|
||||||
|
|
||||||
|
if (length === 0) return;
|
||||||
|
|
||||||
|
let nextIndex = currentIndex;
|
||||||
|
|
||||||
|
switch (key) {
|
||||||
|
case "uparrow":
|
||||||
|
nextIndex = currentIndex <= 0 ? length - 1 : currentIndex - 1;
|
||||||
|
setHighlightId(flattenedChats[nextIndex]._id);
|
||||||
|
break;
|
||||||
|
case "downarrow":
|
||||||
|
nextIndex = currentIndex >= length - 1 ? 0 : currentIndex + 1;
|
||||||
|
setHighlightId(flattenedChats[nextIndex]._id);
|
||||||
|
break;
|
||||||
|
case "enter":
|
||||||
|
if (document.activeElement instanceof HTMLTextAreaElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (currentIndex >= 0) {
|
||||||
|
onSelect(flattenedChats[currentIndex]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleRemove = () => {
|
||||||
|
if (!highlightId) return;
|
||||||
|
|
||||||
|
onRemove(highlightId);
|
||||||
|
|
||||||
|
setIsOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add ref for observer
|
||||||
|
const observerRef = useRef<IntersectionObserver | null>(null);
|
||||||
|
|
||||||
|
// Separate scroll handlers for keyboard and mouse
|
||||||
|
const scrollToElement = useCallback(
|
||||||
|
(elementId: string, isKeyboardNav: boolean) => {
|
||||||
|
if (!listRef.current) return;
|
||||||
|
const element = listRef.current.querySelector(`#${elementId}`);
|
||||||
|
if (!element) return;
|
||||||
|
|
||||||
|
// Check if element is in viewport
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
const isVisible =
|
||||||
|
rect.top >= 0 &&
|
||||||
|
rect.bottom <=
|
||||||
|
(window.innerHeight || document.documentElement.clientHeight);
|
||||||
|
|
||||||
|
// Only scroll if element is not visible
|
||||||
|
if (!isVisible) {
|
||||||
|
element.scrollIntoView({
|
||||||
|
behavior: isKeyboardNav ? "smooth" : "auto",
|
||||||
|
block: isKeyboardNav ? "nearest" : "center",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Debounced scroll for mouse hover
|
||||||
|
const debouncedMouseScroll = useCallback(
|
||||||
|
debounce((elementId: string) => scrollToElement(elementId, false), 150),
|
||||||
|
[scrollToElement]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Immediate scroll for keyboard navigation
|
||||||
|
const keyboardScroll = useCallback(
|
||||||
|
(elementId: string) => {
|
||||||
|
scrollToElement(elementId, true);
|
||||||
|
},
|
||||||
|
[scrollToElement]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Setup intersection observer
|
||||||
|
useEffect(() => {
|
||||||
|
if (!listRef.current) return;
|
||||||
|
|
||||||
|
observerRef.current = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
entries.forEach((entry) => {
|
||||||
|
if (!entry.isIntersecting && entry.target.id === highlightId) {
|
||||||
|
scrollToElement(highlightId, false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ threshold: 0.5 }
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
observerRef.current?.disconnect();
|
||||||
|
};
|
||||||
|
}, [scrollToElement]);
|
||||||
|
|
||||||
|
// Handle highlight changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (!highlightId) return;
|
||||||
|
|
||||||
|
// Clear previous observations
|
||||||
|
observerRef.current?.disconnect();
|
||||||
|
|
||||||
|
const element = listRef.current?.querySelector(`#${highlightId}`);
|
||||||
|
if (element) {
|
||||||
|
observerRef.current?.observe(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isKeyboardNav = document.activeElement?.tagName !== "LI";
|
||||||
|
if (isKeyboardNav) {
|
||||||
|
keyboardScroll(highlightId);
|
||||||
|
} else {
|
||||||
|
debouncedMouseScroll(highlightId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
debouncedMouseScroll.cancel();
|
||||||
|
};
|
||||||
|
}, [highlightId, keyboardScroll, debouncedMouseScroll]);
|
||||||
|
|
||||||
|
const handleDelete = useCallback(() => {
|
||||||
|
if (!highlightId) return;
|
||||||
|
const currentIndex = flattenedChats.findIndex(
|
||||||
|
(chat) => chat._id === highlightId
|
||||||
|
);
|
||||||
|
setHighlightItem(flattenedChats[currentIndex]);
|
||||||
|
setIsOpen(true);
|
||||||
|
}, [highlightId]);
|
||||||
|
|
||||||
|
if (chats.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center flex-1 pt-8">
|
||||||
|
<NoDataImage />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div ref={listRef} className="py-4">
|
||||||
|
{Object.entries(sortedList).map(([label, list]) => (
|
||||||
|
<div key={label}>
|
||||||
|
<span className="text-xs text-[#999] px-3">{t(label)}</span>
|
||||||
|
<ul className="p-0">
|
||||||
|
{list.map((item) => (
|
||||||
|
<HistoryListItem
|
||||||
|
key={item._id}
|
||||||
|
item={item}
|
||||||
|
active={active}
|
||||||
|
onSelect={onSelect}
|
||||||
|
onRename={onRename}
|
||||||
|
onMouseEnter={() => setHighlightId(item._id)}
|
||||||
|
highlightId={highlightId}
|
||||||
|
handleDelete={handleDelete}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DeleteDialog
|
||||||
|
isOpen={isOpen}
|
||||||
|
active={active || highlightItem}
|
||||||
|
setIsOpen={setIsOpen}
|
||||||
|
handleRemove={handleRemove}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HistoryListContent;
|
||||||
195
src/components/Common/HistoryList/HistoryListItem.tsx
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
import { FC, useRef, useCallback, useState } from "react";
|
||||||
|
import { Input, Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
|
||||||
|
import { Ellipsis } from "lucide-react";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Pencil, Trash2 } from "lucide-react";
|
||||||
|
|
||||||
|
import type { Chat } from "@/types/chat";
|
||||||
|
import VisibleKey from "../VisibleKey";
|
||||||
|
import { specialCharacterFiltering } from "@/utils/index"
|
||||||
|
|
||||||
|
interface HistoryListItemProps {
|
||||||
|
item: Chat;
|
||||||
|
active?: Chat;
|
||||||
|
onSelect: (chat: Chat) => void;
|
||||||
|
onRename: (chatId: string, title: string) => void;
|
||||||
|
onMouseEnter: () => void;
|
||||||
|
handleDelete: () => void;
|
||||||
|
highlightId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const HistoryListItem: FC<HistoryListItemProps> = ({
|
||||||
|
item,
|
||||||
|
active,
|
||||||
|
onSelect,
|
||||||
|
onRename,
|
||||||
|
onMouseEnter,
|
||||||
|
highlightId,
|
||||||
|
handleDelete,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const moreButtonRef = useRef<HTMLButtonElement>(null);
|
||||||
|
const { _id, _source } = item;
|
||||||
|
const title = _source?.title ?? _id;
|
||||||
|
const isActive = item._id === active?._id || item._id === highlightId;
|
||||||
|
|
||||||
|
const [isEdit, setIsEdit] = useState(false);
|
||||||
|
|
||||||
|
const onContextMenu = useCallback(
|
||||||
|
(e: React.MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
moreButtonRef.current?.click();
|
||||||
|
},
|
||||||
|
[moreButtonRef.current]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleRename = useCallback(() => {
|
||||||
|
if (highlightId) {
|
||||||
|
setIsEdit(true);
|
||||||
|
}
|
||||||
|
}, [highlightId]);
|
||||||
|
|
||||||
|
const menuItems = [
|
||||||
|
// {
|
||||||
|
// label: "history_list.menu.share",
|
||||||
|
// icon: Share2,
|
||||||
|
// onClick: () => {},
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
label: "history_list.menu.rename",
|
||||||
|
icon: Pencil,
|
||||||
|
shortcut: "R",
|
||||||
|
onClick: handleRename,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "history_list.menu.delete",
|
||||||
|
icon: Trash2,
|
||||||
|
shortcut: "D",
|
||||||
|
iconColor: "#FF2018",
|
||||||
|
onClick: handleDelete,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
key={_id}
|
||||||
|
id={_id}
|
||||||
|
className={clsx(
|
||||||
|
"flex items-center mt-1 h-10 rounded-lg cursor-pointer hover:bg-[#EDEDED] dark:hover:bg-[#353F4D] transition",
|
||||||
|
{
|
||||||
|
"!bg-[#E5E7EB] dark:!bg-[#2B3444]": isActive,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
if (!isActive) {
|
||||||
|
setIsEdit(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
onSelect(item);
|
||||||
|
}}
|
||||||
|
onMouseEnter={onMouseEnter}
|
||||||
|
onContextMenu={onContextMenu}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={clsx("w-1 h-6 rounded-sm bg-[#0072FF]", {
|
||||||
|
"opacity-0": item._id !== active?._id,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex-1 flex items-center justify-between gap-2 px-2 overflow-hidden">
|
||||||
|
{isEdit && isActive ? (
|
||||||
|
<Input
|
||||||
|
autoFocus
|
||||||
|
defaultValue={title}
|
||||||
|
className="flex-1 -mx-px outline-none bg-transparent border border-[#0061FF] rounded-[4px]"
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key !== "Enter") return;
|
||||||
|
|
||||||
|
const value = specialCharacterFiltering(event.currentTarget.value)
|
||||||
|
|
||||||
|
onRename(item._id, value);
|
||||||
|
|
||||||
|
setIsEdit(false);
|
||||||
|
}}
|
||||||
|
onBlur={(event) => {
|
||||||
|
const value = specialCharacterFiltering(event.target.value)
|
||||||
|
|
||||||
|
onRename(item._id, value);
|
||||||
|
|
||||||
|
setIsEdit(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span className="truncate">{title}</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{isActive && !isEdit && (
|
||||||
|
<VisibleKey
|
||||||
|
shortcut="↑↓"
|
||||||
|
rootClassName="w-6"
|
||||||
|
shortcutClassName="w-6"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Popover>
|
||||||
|
{isActive && !isEdit && (
|
||||||
|
<PopoverButton ref={moreButtonRef} className="flex gap-2">
|
||||||
|
<VisibleKey
|
||||||
|
shortcut="O"
|
||||||
|
onKeyPress={() => {
|
||||||
|
moreButtonRef.current?.click();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Ellipsis className="size-4 text-[#979797]" />
|
||||||
|
</VisibleKey>
|
||||||
|
</PopoverButton>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<PopoverPanel
|
||||||
|
anchor="bottom"
|
||||||
|
className="flex flex-col rounded-lg shadow-md z-100 bg-white dark:bg-[#202126] p-1 border border-black/2 dark:border-white/10"
|
||||||
|
onClick={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{menuItems.map((menuItem) => {
|
||||||
|
const {
|
||||||
|
label,
|
||||||
|
icon: Icon,
|
||||||
|
shortcut,
|
||||||
|
iconColor,
|
||||||
|
onClick,
|
||||||
|
} = menuItem;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={label}
|
||||||
|
className="flex items-center gap-2 px-3 py-2 text-sm rounded-md hover:bg-[#EDEDED] dark:hover:bg-[#2B2C31] transition"
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<VisibleKey shortcut={shortcut} onKeyPress={onClick}>
|
||||||
|
<Icon
|
||||||
|
className="size-4"
|
||||||
|
style={{
|
||||||
|
color: iconColor,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</VisibleKey>
|
||||||
|
|
||||||
|
<span>{t(label)}</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</PopoverPanel>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HistoryListItem;
|
||||||
@@ -1,39 +1,18 @@
|
|||||||
import { useKeyPress } from "ahooks";
|
import { Input } from "@headlessui/react";
|
||||||
import {
|
import { debounce } from "lodash-es";
|
||||||
Description,
|
import { FC, useMemo, useRef, useState } from "react";
|
||||||
Dialog,
|
|
||||||
DialogPanel,
|
|
||||||
DialogTitle,
|
|
||||||
Input,
|
|
||||||
Popover,
|
|
||||||
PopoverButton,
|
|
||||||
PopoverPanel,
|
|
||||||
} from "@headlessui/react";
|
|
||||||
import { debounce, groupBy, isNil } from "lodash-es";
|
|
||||||
import { FC, useEffect, useMemo, useRef, useState } from "react";
|
|
||||||
import dayjs from "dayjs";
|
|
||||||
import isSameOrAfter from "dayjs/plugin/isSameOrAfter";
|
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {
|
import { PanelLeftClose, RefreshCcw, Search } from "lucide-react";
|
||||||
Ellipsis,
|
|
||||||
PanelLeftClose,
|
|
||||||
Pencil,
|
|
||||||
RefreshCcw,
|
|
||||||
Search,
|
|
||||||
Trash2,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import VisibleKey from "../VisibleKey";
|
import VisibleKey from "../VisibleKey";
|
||||||
import { Chat } from "@/types/chat";
|
import { Chat } from "@/types/chat";
|
||||||
import NoDataImage from "../NoDataImage";
|
import { closeHistoryPanel, specialCharacterFiltering } from "@/utils";
|
||||||
import { closeHistoryPanel } from "@/utils";
|
import HistoryListContent from "./HistoryListContent";
|
||||||
|
|
||||||
dayjs.extend(isSameOrAfter);
|
|
||||||
|
|
||||||
interface HistoryListProps {
|
interface HistoryListProps {
|
||||||
id?: string;
|
historyPanelId?: string;
|
||||||
list: Chat[];
|
chats: Chat[];
|
||||||
active?: Chat;
|
active?: Chat;
|
||||||
onSearch: (keyword: string) => void;
|
onSearch: (keyword: string) => void;
|
||||||
onRefresh: () => void;
|
onRefresh: () => void;
|
||||||
@@ -44,8 +23,8 @@ interface HistoryListProps {
|
|||||||
|
|
||||||
const HistoryList: FC<HistoryListProps> = (props) => {
|
const HistoryList: FC<HistoryListProps> = (props) => {
|
||||||
const {
|
const {
|
||||||
id,
|
historyPanelId,
|
||||||
list,
|
chats,
|
||||||
active,
|
active,
|
||||||
onSearch,
|
onSearch,
|
||||||
onRefresh,
|
onRefresh,
|
||||||
@@ -54,104 +33,13 @@ const HistoryList: FC<HistoryListProps> = (props) => {
|
|||||||
onRemove,
|
onRemove,
|
||||||
} = props;
|
} = props;
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [isEdit, setIsEdit] = useState(false);
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
const listRef = useRef<HTMLDivElement>(null);
|
|
||||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||||
const moreButtonRef = useRef<HTMLButtonElement>(null);
|
|
||||||
const [isRefresh, setIsRefresh] = useState(false);
|
const [isRefresh, setIsRefresh] = useState(false);
|
||||||
|
|
||||||
const sortedList = useMemo(() => {
|
|
||||||
if (isNil(list)) return {};
|
|
||||||
|
|
||||||
const now = dayjs();
|
|
||||||
|
|
||||||
return groupBy(list, (chat) => {
|
|
||||||
const date = dayjs(chat._source?.updated);
|
|
||||||
|
|
||||||
if (date.isSame(now, "day")) {
|
|
||||||
return "history_list.date.today";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (date.isSame(now.subtract(1, "day"), "day")) {
|
|
||||||
return "history_list.date.yesterday";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (date.isSameOrAfter(now.subtract(7, "day"), "day")) {
|
|
||||||
return "history_list.date.last7Days";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (date.isSameOrAfter(now.subtract(30, "day"), "day")) {
|
|
||||||
return "history_list.date.last30Days";
|
|
||||||
}
|
|
||||||
|
|
||||||
return date.format("YYYY-MM");
|
|
||||||
});
|
|
||||||
}, [list]);
|
|
||||||
|
|
||||||
const menuItems = [
|
|
||||||
// {
|
|
||||||
// label: "history_list.menu.share",
|
|
||||||
// icon: Share2,
|
|
||||||
// onClick: () => {},
|
|
||||||
// },
|
|
||||||
{
|
|
||||||
label: "history_list.menu.rename",
|
|
||||||
icon: Pencil,
|
|
||||||
shortcut: "R",
|
|
||||||
onClick: () => {
|
|
||||||
setIsEdit(true);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "history_list.menu.delete",
|
|
||||||
icon: Trash2,
|
|
||||||
shortcut: "D",
|
|
||||||
iconColor: "#FF2018",
|
|
||||||
onClick: () => {
|
|
||||||
setIsOpen(true);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const debouncedSearch = useMemo(() => {
|
const debouncedSearch = useMemo(() => {
|
||||||
return debounce((value: string) => onSearch(value), 300);
|
return debounce((value: string) => onSearch(value), 300);
|
||||||
}, [onSearch]);
|
}, [onSearch]);
|
||||||
|
|
||||||
useKeyPress(["uparrow", "downarrow"], (_, key) => {
|
|
||||||
const index = list.findIndex((item) => item._id === active?._id);
|
|
||||||
const length = list.length;
|
|
||||||
|
|
||||||
let nextIndex = index;
|
|
||||||
|
|
||||||
switch (key) {
|
|
||||||
case "uparrow":
|
|
||||||
nextIndex = index === 0 ? length - 1 : index - 1;
|
|
||||||
break;
|
|
||||||
case "downarrow":
|
|
||||||
nextIndex = index === length - 1 ? 0 : index + 1;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
onSelect(list[nextIndex]);
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!active?._id || !listRef.current) return;
|
|
||||||
|
|
||||||
const activeEl = listRef.current.querySelector(`#${active._id}`);
|
|
||||||
|
|
||||||
activeEl?.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
||||||
}, [active?._id]);
|
|
||||||
|
|
||||||
const handleRemove = () => {
|
|
||||||
if (!active?._id) return;
|
|
||||||
|
|
||||||
onRemove(active._id);
|
|
||||||
|
|
||||||
setIsOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRefresh = async () => {
|
const handleRefresh = async () => {
|
||||||
setIsRefresh(true);
|
setIsRefresh(true);
|
||||||
|
|
||||||
@@ -164,8 +52,7 @@ const HistoryList: FC<HistoryListProps> = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={listRef}
|
id={historyPanelId}
|
||||||
id={id}
|
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"flex flex-col h-screen text-sm bg-[#F3F4F6] dark:bg-[#1F2937]"
|
"flex flex-col h-screen text-sm bg-[#F3F4F6] dark:bg-[#1F2937]"
|
||||||
)}
|
)}
|
||||||
@@ -187,7 +74,9 @@ const HistoryList: FC<HistoryListProps> = (props) => {
|
|||||||
className="w-full bg-transparent outline-none"
|
className="w-full bg-transparent outline-none"
|
||||||
placeholder={t("history_list.search.placeholder")}
|
placeholder={t("history_list.search.placeholder")}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
debouncedSearch(event.target.value);
|
const value = specialCharacterFiltering(event.target.value)
|
||||||
|
|
||||||
|
debouncedSearch(value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -207,210 +96,16 @@ const HistoryList: FC<HistoryListProps> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 px-2 overflow-auto custom-scrollbar">
|
<div className="flex-1 px-2 overflow-auto custom-scrollbar">
|
||||||
{list.length > 0 ? (
|
<HistoryListContent
|
||||||
<>
|
chats={chats}
|
||||||
<div className="mt-6">
|
active={active}
|
||||||
{Object.entries(sortedList).map(([label, list]) => {
|
onSelect={onSelect}
|
||||||
return (
|
onRename={onRename}
|
||||||
<div key={label}>
|
onRemove={onRemove}
|
||||||
<span className="text-xs text-[#999] px-3">{t(label)}</span>
|
|
||||||
|
|
||||||
<ul className="p-0">
|
|
||||||
{list.map((item) => {
|
|
||||||
const { _id, _source } = item;
|
|
||||||
|
|
||||||
const isActive = _id === active?._id;
|
|
||||||
const title = _source?.title ?? _id;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<li
|
|
||||||
key={_id}
|
|
||||||
id={_id}
|
|
||||||
className={clsx(
|
|
||||||
"flex items-center mt-1 h-10 rounded-lg cursor-pointer hover:bg-[#EDEDED] dark:hover:bg-[#353F4D] transition",
|
|
||||||
{
|
|
||||||
"!bg-[#E5E7EB] dark:!bg-[#2B3444]": isActive,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
onClick={() => {
|
|
||||||
if (!isActive) {
|
|
||||||
setIsEdit(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
onSelect(item);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={clsx(
|
|
||||||
"w-1 h-6 rounded-sm bg-[#0072FF]",
|
|
||||||
{
|
|
||||||
"opacity-0": _id !== active?._id,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex-1 flex items-center justify-between gap-2 px-2 overflow-hidden">
|
|
||||||
{isEdit && isActive ? (
|
|
||||||
<Input
|
|
||||||
autoFocus
|
|
||||||
defaultValue={title}
|
|
||||||
className="flex-1 -mx-px outline-none bg-transparent border border-[#0061FF] rounded-[4px]"
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
if (event.key !== "Enter") return;
|
|
||||||
|
|
||||||
onRename(
|
|
||||||
item._id,
|
|
||||||
event.currentTarget.value
|
|
||||||
);
|
|
||||||
|
|
||||||
setIsEdit(false);
|
|
||||||
}}
|
|
||||||
onBlur={(event) => {
|
|
||||||
onRename(item._id, event.target.value);
|
|
||||||
|
|
||||||
setIsEdit(false);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<span className="truncate">{title}</span>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{isActive && !isEdit && (
|
|
||||||
<VisibleKey
|
|
||||||
shortcut="↑↓"
|
|
||||||
rootClassName="w-6"
|
|
||||||
shortcutClassName="w-6"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Popover>
|
|
||||||
{isActive && !isEdit && (
|
|
||||||
<PopoverButton
|
|
||||||
ref={moreButtonRef}
|
|
||||||
className="flex gap-2"
|
|
||||||
>
|
|
||||||
<VisibleKey
|
|
||||||
shortcut="O"
|
|
||||||
onKeyPress={() => {
|
|
||||||
moreButtonRef.current?.click();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Ellipsis className="size-4 text-[#979797]" />
|
|
||||||
</VisibleKey>
|
|
||||||
</PopoverButton>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<PopoverPanel
|
|
||||||
anchor="bottom"
|
|
||||||
className="flex flex-col rounded-lg shadow-md z-100 bg-white dark:bg-[#202126] p-1 border border-black/2 dark:border-white/10"
|
|
||||||
onClick={(event) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{menuItems.map((menuItem) => {
|
|
||||||
const {
|
|
||||||
label,
|
|
||||||
icon: Icon,
|
|
||||||
shortcut,
|
|
||||||
iconColor,
|
|
||||||
onClick,
|
|
||||||
} = menuItem;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={label}
|
|
||||||
className="flex items-center gap-2 px-3 py-2 text-sm rounded-md hover:bg-[#EDEDED] dark:hover:bg-[#2B2C31] transition"
|
|
||||||
onClick={onClick}
|
|
||||||
>
|
|
||||||
<VisibleKey
|
|
||||||
shortcut={shortcut}
|
|
||||||
onKeyPress={onClick}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
className="size-4"
|
|
||||||
style={{
|
|
||||||
color: iconColor,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</VisibleKey>
|
|
||||||
|
|
||||||
<span>{t(label)}</span>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</PopoverPanel>
|
|
||||||
</Popover>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Dialog
|
|
||||||
open={isOpen}
|
|
||||||
onClose={() => setIsOpen(false)}
|
|
||||||
className="relative z-1000"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
id="headlessui-popover-panel:delete-history"
|
|
||||||
className="fixed inset-0 flex items-center justify-center w-screen"
|
|
||||||
>
|
|
||||||
<DialogPanel className="flex flex-col justify-between w-[360px] h-[160px] p-3 text-[#333] dark:text-white/90 border border-[#e6e6e6] bg-white dark:bg-[#202126] dark:border-white/10 shadow-xl rounded-lg">
|
|
||||||
<div className="flex flex-col gap-3">
|
|
||||||
<DialogTitle className="text-base font-bold">
|
|
||||||
{t("history_list.delete_modal.title")}
|
|
||||||
</DialogTitle>
|
|
||||||
<Description className="text-sm">
|
|
||||||
{t("history_list.delete_modal.description", {
|
|
||||||
replace: [active?._source?.title || active?._source?.message || active?._id],
|
|
||||||
})}
|
|
||||||
</Description>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex gap-4 self-end">
|
|
||||||
<VisibleKey
|
|
||||||
shortcut="N"
|
|
||||||
shortcutClassName="left-[unset] right-0"
|
|
||||||
onKeyPress={() => setIsOpen(false)}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
className="h-8 px-4 text-sm text-[#666666] bg-[#F8F9FA] dark:text-white dark:bg-[#202126] border border-[#E6E6E6] dark:border-white/10 rounded-lg"
|
|
||||||
onClick={() => setIsOpen(false)}
|
|
||||||
>
|
|
||||||
{t("history_list.delete_modal.button.cancel")}
|
|
||||||
</button>
|
|
||||||
</VisibleKey>
|
|
||||||
|
|
||||||
<VisibleKey
|
|
||||||
shortcut="Y"
|
|
||||||
shortcutClassName="left-[unset] right-0"
|
|
||||||
onKeyPress={handleRemove}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
className="h-8 px-4 text-sm text-white bg-[#EF4444] rounded-lg"
|
|
||||||
onClick={handleRemove}
|
|
||||||
>
|
|
||||||
{t("history_list.delete_modal.button.delete")}
|
|
||||||
</button>
|
|
||||||
</VisibleKey>
|
|
||||||
</div>
|
|
||||||
</DialogPanel>
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<div className="flex items-center justify-center flex-1 pt-8">
|
|
||||||
<NoDataImage />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{historyPanelId && (
|
||||||
<div className="flex justify-end p-2 border-t dark:border-[#343D4D]">
|
<div className="flex justify-end p-2 border-t dark:border-[#343D4D]">
|
||||||
<VisibleKey shortcut="Esc" shortcutClassName="w-7">
|
<VisibleKey shortcut="Esc" shortcutClassName="w-7">
|
||||||
<PanelLeftClose
|
<PanelLeftClose
|
||||||
@@ -419,6 +114,7 @@ const HistoryList: FC<HistoryListProps> = (props) => {
|
|||||||
/>
|
/>
|
||||||
</VisibleKey>
|
</VisibleKey>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { isEmpty } from "lodash-es";
|
import { isNil, isEmpty } from "lodash-es";
|
||||||
import { useAsyncEffect } from "ahooks";
|
import { useAsyncEffect } from "ahooks";
|
||||||
import { Box } from "lucide-react";
|
import { Box } from "lucide-react";
|
||||||
|
|
||||||
@@ -27,9 +27,6 @@ function CommonIcon({
|
|||||||
const connectorSource = useFindConnectorIcon(item);
|
const connectorSource = useFindConnectorIcon(item);
|
||||||
|
|
||||||
const [isAbsolute, setIsAbsolute] = useState<boolean>();
|
const [isAbsolute, setIsAbsolute] = useState<boolean>();
|
||||||
const [defaultIconState, setDefaultIconState] = useState<
|
|
||||||
React.FC | string | undefined
|
|
||||||
>(defaultIcon);
|
|
||||||
|
|
||||||
useAsyncEffect(async () => {
|
useAsyncEffect(async () => {
|
||||||
if (isEmpty(item)) return;
|
if (isEmpty(item)) return;
|
||||||
@@ -39,7 +36,6 @@ function CommonIcon({
|
|||||||
omitSize: true,
|
omitSize: true,
|
||||||
});
|
});
|
||||||
setIsAbsolute(Boolean(isAbsolute));
|
setIsAbsolute(Boolean(isAbsolute));
|
||||||
setDefaultIconState(defaultIcon || Box);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setIsAbsolute(false);
|
setIsAbsolute(false);
|
||||||
}
|
}
|
||||||
@@ -47,6 +43,8 @@ function CommonIcon({
|
|||||||
|
|
||||||
// Handle regular icon types
|
// Handle regular icon types
|
||||||
const renderIconByType = (renderType: string) => {
|
const renderIconByType = (renderType: string) => {
|
||||||
|
if (isNil(isAbsolute)) return null;
|
||||||
|
|
||||||
switch (renderType) {
|
switch (renderType) {
|
||||||
case "special_icon": {
|
case "special_icon": {
|
||||||
if (item.id === "Calculator") {
|
if (item.id === "Calculator") {
|
||||||
@@ -95,7 +93,7 @@ function CommonIcon({
|
|||||||
case "default_icon":
|
case "default_icon":
|
||||||
return (
|
return (
|
||||||
<UniversalIcon
|
<UniversalIcon
|
||||||
defaultIcon={defaultIconState}
|
defaultIcon={defaultIcon || Box}
|
||||||
className={className}
|
className={className}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useAppStore } from "@/stores/appStore";
|
import { useAppStore } from "@/stores/appStore";
|
||||||
import logoImg from "@/assets/icon.svg";
|
import logoImg from "@/assets/icon.svg";
|
||||||
|
import { FC } from "react";
|
||||||
|
|
||||||
interface FontIconProps {
|
interface FontIconProps {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -7,7 +8,7 @@ interface FontIconProps {
|
|||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FontIcon = ({ name, className, style, ...rest }: FontIconProps) => {
|
const FontIcon: FC<FontIconProps> = ({ name, className, style, ...rest }) => {
|
||||||
const isTauri = useAppStore((state) => state.isTauri);
|
const isTauri = useAppStore((state) => state.isTauri);
|
||||||
|
|
||||||
if (isTauri) {
|
if (isTauri) {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { POPOVER_PANEL_SELECTOR } from "@/constants";
|
|
||||||
import { Input, InputProps } from "@headlessui/react";
|
import { Input, InputProps } from "@headlessui/react";
|
||||||
import { useKeyPress } from "ahooks";
|
import { useKeyPress } from "ahooks";
|
||||||
import { forwardRef, useImperativeHandle, useRef } from "react";
|
import { forwardRef, useImperativeHandle, useRef } from "react";
|
||||||
|
|
||||||
|
import { POPOVER_PANEL_SELECTOR } from "@/constants";
|
||||||
|
|
||||||
const PopoverInput = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
|
const PopoverInput = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { last } from "lodash-es";
|
|||||||
|
|
||||||
import { POPOVER_PANEL_SELECTOR } from "@/constants";
|
import { POPOVER_PANEL_SELECTOR } from "@/constants";
|
||||||
import { useShortcutsStore } from "@/stores/shortcutsStore";
|
import { useShortcutsStore } from "@/stores/shortcutsStore";
|
||||||
|
import { useAppStore } from "@/stores/appStore";
|
||||||
|
|
||||||
interface VisibleKeyProps extends HTMLAttributes<HTMLDivElement> {
|
interface VisibleKeyProps extends HTMLAttributes<HTMLDivElement> {
|
||||||
shortcut: string;
|
shortcut: string;
|
||||||
@@ -23,6 +24,8 @@ const VisibleKey: FC<VisibleKeyProps> = (props) => {
|
|||||||
...rest
|
...rest
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
const showTooltip = useAppStore((state) => state.showTooltip);
|
||||||
|
|
||||||
const modifierKey = useShortcutsStore((state) => {
|
const modifierKey = useShortcutsStore((state) => {
|
||||||
return state.modifierKey;
|
return state.modifierKey;
|
||||||
});
|
});
|
||||||
@@ -82,11 +85,11 @@ const VisibleKey: FC<VisibleKeyProps> = (props) => {
|
|||||||
<div
|
<div
|
||||||
{...rest}
|
{...rest}
|
||||||
ref={childrenRef}
|
ref={childrenRef}
|
||||||
className={clsx(rootClassName, "relative inline-block")}
|
className={clsx(rootClassName, "relative inline-block leading-[100%]")}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
||||||
{visibleShortcut ? (
|
{showTooltip && visibleShortcut ? (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"size-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] absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2",
|
"size-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] absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ChevronUp, Sparkles } from "lucide-react";
|
import { Sparkles, X } from "lucide-react";
|
||||||
import { FC, useState } from "react";
|
import { FC, useState } from "react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { useStreamChat } from "@/hooks/useStreamChat";
|
import { useStreamChat } from "@/hooks/useStreamChat";
|
||||||
@@ -18,7 +18,6 @@ const AiOverview: FC<AiSummaryProps> = (props) => {
|
|||||||
return state.aiOverviewAssistant;
|
return state.aiOverviewAssistant;
|
||||||
});
|
});
|
||||||
|
|
||||||
const [expand, setExpand] = useState(true);
|
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
|
|
||||||
const { isTyping, chunkData, loadingStep } = useStreamChat({
|
const { isTyping, chunkData, loadingStep } = useStreamChat({
|
||||||
@@ -30,21 +29,22 @@ const AiOverview: FC<AiSummaryProps> = (props) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div className={clsx({ "p-2": visible })}>
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"flex flex-col gap-2 relative max-h-[210px] px-4 py-3 rounded-[4px] text-[#333] dark:text-[#D8D8D8] bg-white dark:bg-[#141414] shadow-[0_4px_8px_rgba(0,0,0,0.1)] dark:shadow-[0_4px_20px_rgba(255,255,255,0.2)]",
|
"flex flex-col gap-2 relative max-h-[210px] px-4 py-3 rounded-[4px] text-[#333] dark:text-[#D8D8D8] bg-white dark:bg-[#141414] shadow-[0_4px_8px_rgba(0,0,0,0.1)] dark:shadow-[0_4px_20px_rgba(255,255,255,0.2)]",
|
||||||
{
|
{
|
||||||
"hidden -m-2": !visible,
|
hidden: !visible,
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="absolute top-2 right-2 flex items-center justify-center size-[20px] border rounded-md cursor-pointer dark:border-[#282828]"
|
className="absolute top-2 right-2 flex items-center justify-center size-[20px] border rounded-md cursor-pointer dark:border-[#282828]"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setExpand(!expand);
|
setVisible(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ChevronUp className="size-4" />
|
<X className="size-4" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex item-center gap-1">
|
<div className="flex item-center gap-1">
|
||||||
@@ -52,11 +52,7 @@ const AiOverview: FC<AiSummaryProps> = (props) => {
|
|||||||
<span className="text-xs font-semibold">AI Overview</span>
|
<span className="text-xs font-semibold">AI Overview</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div className="flex-1 overflow-auto text-sm hide-scrollbar">
|
||||||
className={clsx("flex-1 overflow-auto text-sm hide-scrollbar", {
|
|
||||||
hidden: !expand,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<div className="-ml-11 -mr-4 user-select-text">
|
<div className="-ml-11 -mr-4 user-select-text">
|
||||||
<ChatMessage
|
<ChatMessage
|
||||||
key="current"
|
key="current"
|
||||||
@@ -81,10 +77,11 @@ const AiOverview: FC<AiSummaryProps> = (props) => {
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
className={clsx("min-h-[20px]", {
|
className={clsx("min-h-[20px]", {
|
||||||
hidden: !expand || isTyping,
|
hidden: isTyping,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -9,17 +9,18 @@ import { useEffect, useRef, useState } from "react";
|
|||||||
import { noop } from "lodash-es";
|
import { noop } from "lodash-es";
|
||||||
|
|
||||||
import { ChatMessage } from "../ChatMessage";
|
import { ChatMessage } from "../ChatMessage";
|
||||||
import { COPY_BUTTON_ID } from "@/constants";
|
|
||||||
import { useSearchStore } from "@/stores/searchStore";
|
import { useSearchStore } from "@/stores/searchStore";
|
||||||
import platformAdapter from "@/utils/platformAdapter";
|
import platformAdapter from "@/utils/platformAdapter";
|
||||||
import useMessageChunkData from "@/hooks/useMessageChunkData";
|
import useMessageChunkData from "@/hooks/useMessageChunkData";
|
||||||
import { useAppStore } from "@/stores/appStore";
|
import { useAppStore } from "@/stores/appStore";
|
||||||
import { isMac } from "@/utils/platform";
|
|
||||||
import { useExtensionsStore } from "@/stores/extensionsStore";
|
import { useExtensionsStore } from "@/stores/extensionsStore";
|
||||||
|
import { useShortcutsStore } from "@/stores/shortcutsStore";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
serverId?: string;
|
serverId?: string;
|
||||||
assistantId?: string;
|
assistantId?: string;
|
||||||
|
copyButtonId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AskAi = () => {
|
const AskAi = () => {
|
||||||
@@ -78,6 +79,7 @@ const AskAi = () => {
|
|||||||
const setAskAiAssistantId = useSearchStore((state) => {
|
const setAskAiAssistantId = useSearchStore((state) => {
|
||||||
return state.setAskAiAssistantId;
|
return state.setAskAiAssistantId;
|
||||||
});
|
});
|
||||||
|
const modifierKey = useShortcutsStore((state) => state.modifierKey);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (state.serverId) return;
|
if (state.serverId) return;
|
||||||
@@ -116,11 +118,6 @@ const AskAi = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the chunk data does not contain a message_chunk, we ignore it
|
|
||||||
if (!chunkData.message_chunk) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsTyping(true);
|
setIsTyping(true);
|
||||||
|
|
||||||
setLoadingStep(() => ({
|
setLoadingStep(() => ({
|
||||||
@@ -172,6 +169,8 @@ const AskAi = () => {
|
|||||||
|
|
||||||
const { serverId, assistantId } = state;
|
const { serverId, assistantId } = state;
|
||||||
|
|
||||||
|
state.copyButtonId = nanoid();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await platformAdapter.invokeBackend("ask_ai", {
|
await platformAdapter.invokeBackend("ask_ai", {
|
||||||
message: askAiMessage,
|
message: askAiMessage,
|
||||||
@@ -184,14 +183,13 @@ const AskAi = () => {
|
|||||||
}
|
}
|
||||||
}, [askAiMessage]);
|
}, [askAiMessage]);
|
||||||
|
|
||||||
useKeyPress("enter", async (event) => {
|
useKeyPress(
|
||||||
const { metaKey, ctrlKey } = event;
|
`${modifierKey}.enter`,
|
||||||
|
async () => {
|
||||||
if (isTyping) return;
|
if (isTyping) return;
|
||||||
|
|
||||||
const { serverId, assistantId } = state;
|
const { serverId, assistantId } = state;
|
||||||
|
|
||||||
if ((isMac && metaKey) || (!isMac && ctrlKey)) {
|
|
||||||
await platformAdapter.commands("open_session_chat", {
|
await platformAdapter.commands("open_session_chat", {
|
||||||
serverId,
|
serverId,
|
||||||
sessionId: sessionIdRef.current,
|
sessionId: sessionIdRef.current,
|
||||||
@@ -201,13 +199,26 @@ const AskAi = () => {
|
|||||||
|
|
||||||
setAskAiServerId(serverId);
|
setAskAiServerId(serverId);
|
||||||
setAskAiSessionId(sessionIdRef.current);
|
setAskAiSessionId(sessionIdRef.current);
|
||||||
return setAskAiAssistantId(assistantId);
|
setAskAiAssistantId(assistantId);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
exactMatch: true,
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const copyButton = document.getElementById(COPY_BUTTON_ID);
|
useKeyPress(
|
||||||
|
"enter",
|
||||||
|
() => {
|
||||||
|
if (isTyping || !state.copyButtonId) return;
|
||||||
|
|
||||||
copyButton?.click();
|
const copyButton = document.getElementById(state.copyButtonId);
|
||||||
});
|
|
||||||
|
copyButton?.click?.();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
exactMatch: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
askAiMessage && (
|
askAiMessage && (
|
||||||
@@ -238,6 +249,7 @@ const AskAi = () => {
|
|||||||
think={think}
|
think={think}
|
||||||
response={response}
|
response={response}
|
||||||
loadingStep={loadingStep}
|
loadingStep={loadingStep}
|
||||||
|
copyButtonId={state.copyButtonId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,12 +10,14 @@ import { useAppStore } from "@/stores/appStore";
|
|||||||
|
|
||||||
interface AssistantManagerProps {
|
interface AssistantManagerProps {
|
||||||
isChatMode: boolean;
|
isChatMode: boolean;
|
||||||
|
inputValue: string;
|
||||||
handleSubmit: () => void;
|
handleSubmit: () => void;
|
||||||
changeInput: (value: string) => void;
|
changeInput: (value: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useAssistantManager({
|
export function useAssistantManager({
|
||||||
isChatMode,
|
isChatMode,
|
||||||
|
inputValue,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
changeInput,
|
changeInput,
|
||||||
}: AssistantManagerProps) {
|
}: AssistantManagerProps) {
|
||||||
@@ -56,21 +58,23 @@ export function useAssistantManager({
|
|||||||
}
|
}
|
||||||
}, [askAI]);
|
}, [askAI]);
|
||||||
|
|
||||||
const handleAskAi = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
const handleAskAi = () => {
|
||||||
if (!isTauri) return;
|
if (!isTauri) return;
|
||||||
|
|
||||||
askAIRef.current = cloneDeep(askAI);
|
askAIRef.current = cloneDeep(askAI);
|
||||||
|
|
||||||
if (!askAIRef.current) return;
|
if (!askAIRef.current) return;
|
||||||
|
|
||||||
event.preventDefault();
|
let value = inputValue.trim();
|
||||||
|
|
||||||
const { value } = event.currentTarget;
|
if (isEmpty(value)) return;
|
||||||
|
|
||||||
if (!selectedAssistant && isEmpty(value)) return;
|
if (!goAskAi && selectedAssistant) {
|
||||||
|
value = "";
|
||||||
|
}
|
||||||
|
|
||||||
changeInput("");
|
changeInput("");
|
||||||
setAskAiMessage(!goAskAi && selectedAssistant ? "" : value);
|
setAskAiMessage(value);
|
||||||
setGoAskAi(true);
|
setGoAskAi(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -85,18 +89,25 @@ export function useAssistantManager({
|
|||||||
return setGoAskAi(false);
|
return setGoAskAi(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key === "Tab" && !isChatMode && isTauri) {
|
if (key === "Tab" && isTauri) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (isChatMode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
assistant_get();
|
assistant_get();
|
||||||
|
|
||||||
return handleAskAi(e);
|
return handleAskAi();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key === "Enter" && !shiftKey && !isChatMode && isTauri) {
|
if (key === "Enter" && !shiftKey && !isChatMode && isTauri) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
if (goAskAi) {
|
if (goAskAi) {
|
||||||
return handleAskAi(e);
|
return handleAskAi();
|
||||||
}
|
}
|
||||||
|
|
||||||
e.preventDefault();
|
|
||||||
handleSubmit();
|
handleSubmit();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,14 +5,18 @@ import {
|
|||||||
forwardRef,
|
forwardRef,
|
||||||
KeyboardEvent,
|
KeyboardEvent,
|
||||||
useEffect,
|
useEffect,
|
||||||
|
useCallback,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import { specialCharacterFiltering } from "@/utils/index";
|
||||||
|
|
||||||
const LINE_HEIGHT = 24; // 1.5rem
|
const LINE_HEIGHT = 24; // 1.5rem
|
||||||
const MAX_FIRST_LINE_WIDTH = 470; // Width in pixels for first line
|
const MAX_FIRST_LINE_WIDTH = 470; // Width in pixels for first line
|
||||||
const MAX_HEIGHT = 240; // 15rem
|
const MAX_HEIGHT = 240; // 15rem
|
||||||
|
|
||||||
interface AutoResizeTextareaProps {
|
interface AutoResizeTextareaProps {
|
||||||
|
isChatMode: boolean;
|
||||||
input: string;
|
input: string;
|
||||||
setInput: (value: string) => void;
|
setInput: (value: string) => void;
|
||||||
handleKeyDown?: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void;
|
handleKeyDown?: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void;
|
||||||
@@ -28,6 +32,7 @@ const AutoResizeTextarea = forwardRef<
|
|||||||
>(
|
>(
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
|
isChatMode,
|
||||||
input,
|
input,
|
||||||
setInput,
|
setInput,
|
||||||
handleKeyDown,
|
handleKeyDown,
|
||||||
@@ -130,9 +135,18 @@ const AutoResizeTextarea = forwardRef<
|
|||||||
handleKeyDown?.(event);
|
handleKeyDown?.(event);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleChange = useCallback(
|
||||||
|
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
const value = specialCharacterFiltering(e.target.value);
|
||||||
|
setInput(value);
|
||||||
|
},
|
||||||
|
[setInput]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<textarea
|
<textarea
|
||||||
ref={textareaRef}
|
ref={textareaRef}
|
||||||
|
id={isChatMode ? "chat-textarea" : "search-textarea"}
|
||||||
autoFocus
|
autoFocus
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
@@ -141,7 +155,7 @@ const AutoResizeTextarea = forwardRef<
|
|||||||
placeholder={chatPlaceholder || t("search.textarea.placeholder")}
|
placeholder={chatPlaceholder || t("search.textarea.placeholder")}
|
||||||
aria-label={t("search.textarea.ariaLabel")}
|
aria-label={t("search.textarea.ariaLabel")}
|
||||||
value={input}
|
value={input}
|
||||||
onChange={(e) => setInput(e.target.value)}
|
onChange={handleChange}
|
||||||
onKeyDown={handleKeyPress}
|
onKeyDown={handleKeyPress}
|
||||||
onCompositionStart={setTrue}
|
onCompositionStart={setTrue}
|
||||||
onCompositionEnd={() => {
|
onCompositionEnd={() => {
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ const ChatIcons: React.FC<ChatIconsProps> = ({
|
|||||||
<button
|
<button
|
||||||
className={`ml-1 p-1 ${
|
className={`ml-1 p-1 ${
|
||||||
inputValue ? "bg-[#0072FF]" : "bg-[#E4E5F0] dark:bg-[rgb(84,84,84)]"
|
inputValue ? "bg-[#0072FF]" : "bg-[#E4E5F0] dark:bg-[rgb(84,84,84)]"
|
||||||
} rounded-full transition-colors`}
|
} rounded-full transition-colors h-6`}
|
||||||
type="submit"
|
type="submit"
|
||||||
onClick={() => onSend(inputValue.trim())}
|
onClick={() => onSend(inputValue.trim())}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -4,14 +4,14 @@ import { isNil, lowerCase, noop } from "lodash-es";
|
|||||||
import { Copy, Link, SquareArrowOutUpRight } from "lucide-react";
|
import { Copy, Link, SquareArrowOutUpRight } from "lucide-react";
|
||||||
import { cloneElement, FC, useEffect, useRef, useState } from "react";
|
import { cloneElement, FC, useEffect, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Input } from "@headlessui/react";
|
||||||
|
|
||||||
import { useOSKeyPress } from "@/hooks/useOSKeyPress";
|
import { useOSKeyPress } from "@/hooks/useOSKeyPress";
|
||||||
import { useSearchStore } from "@/stores/searchStore";
|
import { useSearchStore } from "@/stores/searchStore";
|
||||||
import { copyToClipboard, OpenURLWithBrowser } from "@/utils";
|
import { copyToClipboard, specialCharacterFiltering } from "@/utils";
|
||||||
import { isMac } from "@/utils/platform";
|
import { isMac } from "@/utils/platform";
|
||||||
import { CONTEXT_MENU_PANEL_ID } from "@/constants";
|
import { CONTEXT_MENU_PANEL_ID } from "@/constants";
|
||||||
import { useShortcutsStore } from "@/stores/shortcutsStore";
|
import { useShortcutsStore } from "@/stores/shortcutsStore";
|
||||||
import { Input } from "@headlessui/react";
|
|
||||||
import VisibleKey from "../Common/VisibleKey";
|
import VisibleKey from "../Common/VisibleKey";
|
||||||
import platformAdapter from "@/utils/platformAdapter";
|
import platformAdapter from "@/utils/platformAdapter";
|
||||||
|
|
||||||
@@ -19,9 +19,7 @@ interface State {
|
|||||||
activeMenuIndex: number;
|
activeMenuIndex: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ContextMenuProps {
|
interface ContextMenuProps {}
|
||||||
hideCoco?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ContextMenu: FC<ContextMenuProps> = () => {
|
const ContextMenu: FC<ContextMenuProps> = () => {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -53,7 +51,7 @@ const ContextMenu: FC<ContextMenuProps> = () => {
|
|||||||
const menus = useCreation(() => {
|
const menus = useCreation(() => {
|
||||||
if (isNil(selectedSearchContent)) return [];
|
if (isNil(selectedSearchContent)) return [];
|
||||||
|
|
||||||
const { url, category, payload, on_opened } = selectedSearchContent;
|
const { url, category, type, payload } = selectedSearchContent;
|
||||||
const { query, result } = payload ?? {};
|
const { query, result } = payload ?? {};
|
||||||
|
|
||||||
if (category === "AI Overview") {
|
if (category === "AI Overview") {
|
||||||
@@ -70,15 +68,7 @@ const ContextMenu: FC<ContextMenuProps> = () => {
|
|||||||
shortcut: "enter",
|
shortcut: "enter",
|
||||||
hide: category === "Calculator",
|
hide: category === "Calculator",
|
||||||
clickEvent: () => {
|
clickEvent: () => {
|
||||||
if (on_opened) {
|
platformAdapter.openSearchItem(selectedSearchContent as any);
|
||||||
return platformAdapter.invokeBackend("open", {
|
|
||||||
onOpened: on_opened,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (url) {
|
|
||||||
OpenURLWithBrowser(url);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -86,7 +76,7 @@ const ContextMenu: FC<ContextMenuProps> = () => {
|
|||||||
icon: <Link />,
|
icon: <Link />,
|
||||||
keys: isMac ? ["⌘", "L"] : ["Ctrl", "L"],
|
keys: isMac ? ["⌘", "L"] : ["Ctrl", "L"],
|
||||||
shortcut: isMac ? "meta.l" : "ctrl.l",
|
shortcut: isMac ? "meta.l" : "ctrl.l",
|
||||||
hide: category === "Calculator",
|
hide: category === "Calculator" || type === "AI Assistant",
|
||||||
clickEvent() {
|
clickEvent() {
|
||||||
copyToClipboard(url);
|
copyToClipboard(url);
|
||||||
},
|
},
|
||||||
@@ -280,7 +270,7 @@ const ContextMenu: FC<ContextMenuProps> = () => {
|
|||||||
placeholder={t("search.contextMenu.search")}
|
placeholder={t("search.contextMenu.search")}
|
||||||
className="w-full bg-transparent"
|
className="w-full bg-transparent"
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
const value = event.target.value;
|
const value = specialCharacterFiltering(event.target.value);
|
||||||
|
|
||||||
const searchMenus = menus.filter((item) => {
|
const searchMenus = menus.filter((item) => {
|
||||||
return lowerCase(item.name).includes(lowerCase(value));
|
return lowerCase(item.name).includes(lowerCase(value));
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import platformAdapter from "@/utils/platformAdapter";
|
|||||||
import { Get } from "@/api/axiosRequest";
|
import { Get } from "@/api/axiosRequest";
|
||||||
import { useAppStore } from "@/stores/appStore";
|
import { useAppStore } from "@/stores/appStore";
|
||||||
import { useConnectStore } from "@/stores/connectStore";
|
import { useConnectStore } from "@/stores/connectStore";
|
||||||
import { OpenURLWithBrowser } from "@/utils";
|
|
||||||
|
|
||||||
interface DocumentListProps {
|
interface DocumentListProps {
|
||||||
onSelectDocument: (id: string) => void;
|
onSelectDocument: (id: string) => void;
|
||||||
@@ -141,11 +140,15 @@ export const DocumentList: React.FC<DocumentListProps> = ({
|
|||||||
setIsKeyboardMode(false);
|
setIsKeyboardMode(false);
|
||||||
}, [isChatMode, input]);
|
}, [isChatMode, input]);
|
||||||
|
|
||||||
|
const { visibleContextMenu } = useSearchStore();
|
||||||
|
|
||||||
const handleKeyDown = useCallback(
|
const handleKeyDown = useCallback(
|
||||||
(e: KeyboardEvent) => {
|
(e: KeyboardEvent) => {
|
||||||
if (!data?.list?.length) return;
|
if (!data?.list?.length) return;
|
||||||
|
|
||||||
const handleArrowKeys = () => {
|
const handleArrowKeys = () => {
|
||||||
|
if (visibleContextMenu) return;
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setIsKeyboardMode(true);
|
setIsKeyboardMode(true);
|
||||||
|
|
||||||
@@ -170,15 +173,8 @@ export const DocumentList: React.FC<DocumentListProps> = ({
|
|||||||
const handleEnter = () => {
|
const handleEnter = () => {
|
||||||
if (selectedItem === null) return;
|
if (selectedItem === null) return;
|
||||||
const item = data.list[selectedItem]?.document;
|
const item = data.list[selectedItem]?.document;
|
||||||
if (item?.on_opened) {
|
|
||||||
return platformAdapter.invokeBackend("open", {
|
|
||||||
onOpened: item.on_opened,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item?.url) {
|
platformAdapter.openSearchItem(item);
|
||||||
OpenURLWithBrowser(item.url);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
switch (e.key) {
|
switch (e.key) {
|
||||||
@@ -237,20 +233,12 @@ export const DocumentList: React.FC<DocumentListProps> = ({
|
|||||||
<SearchListItem
|
<SearchListItem
|
||||||
key={hit.document.id + index}
|
key={hit.document.id + index}
|
||||||
itemRef={(el) => (itemRefs.current[index] = el)}
|
itemRef={(el) => (itemRefs.current[index] = el)}
|
||||||
item={hit.document}
|
item={{ ...hit.document, querySource: hit.source }}
|
||||||
isSelected={selectedItem === index}
|
isSelected={selectedItem === index}
|
||||||
currentIndex={index}
|
currentIndex={index}
|
||||||
onMouseEnter={() => onMouseEnter(index, hit.document)}
|
onMouseEnter={() => onMouseEnter(index, hit.document)}
|
||||||
onItemClick={() => {
|
onItemClick={() => {
|
||||||
if (hit.document?.on_opened) {
|
platformAdapter.openSearchItem(hit.document);
|
||||||
return platformAdapter.invokeBackend("open", {
|
|
||||||
onOpened: hit.document.on_opened,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hit.document?.url) {
|
|
||||||
OpenURLWithBrowser(hit.document.url);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
showListRight={viewMode === "list"}
|
showListRight={viewMode === "list"}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
MouseEvent,
|
MouseEvent,
|
||||||
useMemo,
|
useMemo,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { useDebounceFn, useUnmount } from "ahooks";
|
import { useDebounceFn } from "ahooks";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { useSearchStore } from "@/stores/searchStore";
|
import { useSearchStore } from "@/stores/searchStore";
|
||||||
@@ -16,7 +16,6 @@ import { useKeyboardNavigation } from "@/hooks/useKeyboardNavigation";
|
|||||||
import { SearchSource } from "./SearchSource";
|
import { SearchSource } from "./SearchSource";
|
||||||
import DropdownListItem from "./DropdownListItem";
|
import DropdownListItem from "./DropdownListItem";
|
||||||
import platformAdapter from "@/utils/platformAdapter";
|
import platformAdapter from "@/utils/platformAdapter";
|
||||||
import { OpenURLWithBrowser } from "@/utils";
|
|
||||||
|
|
||||||
type ISearchData = Record<string, QueryHits[]>;
|
type ISearchData = Record<string, QueryHits[]>;
|
||||||
|
|
||||||
@@ -44,12 +43,8 @@ function DropdownList({
|
|||||||
const [selectedName, setSelectedName] = useState<string>("");
|
const [selectedName, setSelectedName] = useState<string>("");
|
||||||
const [showIndex, setShowIndex] = useState<boolean>(false);
|
const [showIndex, setShowIndex] = useState<boolean>(false);
|
||||||
|
|
||||||
const {
|
const { setSourceData, setSelectedSearchContent, setVisibleContextMenu } =
|
||||||
setSourceData,
|
useSearchStore();
|
||||||
setSelectedSearchContent,
|
|
||||||
setSelectedAssistant,
|
|
||||||
setVisibleContextMenu,
|
|
||||||
} = useSearchStore();
|
|
||||||
|
|
||||||
const showSource = useMemo(
|
const showSource = useMemo(
|
||||||
() => Object.keys(searchData).length < 5,
|
() => Object.keys(searchData).length < 5,
|
||||||
@@ -82,15 +77,7 @@ function DropdownList({
|
|||||||
setSelectedSearchContent(item);
|
setSelectedSearchContent(item);
|
||||||
},
|
},
|
||||||
onItemClick: (item: SearchDocument) => {
|
onItemClick: (item: SearchDocument) => {
|
||||||
if (item?.on_opened) {
|
platformAdapter.openSearchItem(item);
|
||||||
return platformAdapter.invokeBackend("open", {
|
|
||||||
onOpened: item.on_opened,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item?.url) {
|
|
||||||
OpenURLWithBrowser(item.url);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
goToTwoPage: (item: SearchDocument) => {
|
goToTwoPage: (item: SearchDocument) => {
|
||||||
setSourceData(item);
|
setSourceData(item);
|
||||||
@@ -98,11 +85,6 @@ function DropdownList({
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useUnmount(() => {
|
|
||||||
setSelectedIndex(null);
|
|
||||||
setSelectedSearchContent(undefined);
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedIndex === null) {
|
if (selectedIndex === null) {
|
||||||
setSelectedSearchContent(undefined);
|
setSelectedSearchContent(undefined);
|
||||||
@@ -111,14 +93,6 @@ function DropdownList({
|
|||||||
|
|
||||||
const item = globalItemIndexMap[selectedIndex];
|
const item = globalItemIndexMap[selectedIndex];
|
||||||
setSelectedSearchContent(item);
|
setSelectedSearchContent(item);
|
||||||
if (item?.source?.id === "assistant") {
|
|
||||||
setSelectedAssistant({
|
|
||||||
...item,
|
|
||||||
name: item.title,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setSelectedAssistant(undefined);
|
|
||||||
}
|
|
||||||
}, [selectedIndex]);
|
}, [selectedIndex]);
|
||||||
|
|
||||||
// Scroll selected item into view
|
// Scroll selected item into view
|
||||||
@@ -150,6 +124,13 @@ function DropdownList({
|
|||||||
initializeSelection();
|
initializeSelection();
|
||||||
}, [searchData]);
|
}, [searchData]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
setSelectedIndex(null);
|
||||||
|
setSelectedSearchContent(undefined);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Keyboard navigation
|
// Keyboard navigation
|
||||||
useKeyboardNavigation({
|
useKeyboardNavigation({
|
||||||
suggests,
|
suggests,
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ const DropdownListItem = memo(
|
|||||||
id={`search-item-${currentIndex}`}
|
id={`search-item-${currentIndex}`}
|
||||||
className={clsx("p-2 transition rounded-lg", {
|
className={clsx("p-2 transition rounded-lg", {
|
||||||
"bg-[#EDEDED] dark:bg-[#202126]": isSelected,
|
"bg-[#EDEDED] dark:bg-[#202126]": isSelected,
|
||||||
|
"!p-0": isAiOverview,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{isCalculator && <Calculator item={item} isSelected={isSelected} />}
|
{isCalculator && <Calculator item={item} isSelected={isSelected} />}
|
||||||
|
|||||||
@@ -46,7 +46,6 @@ interface ChatInputProps {
|
|||||||
}) => Promise<string | string[] | null>;
|
}) => Promise<string | string[] | null>;
|
||||||
getFileMetadata: (path: string) => Promise<any>;
|
getFileMetadata: (path: string) => Promise<any>;
|
||||||
getFileIcon: (path: string, size: number) => Promise<string>;
|
getFileIcon: (path: string, size: number) => Promise<string>;
|
||||||
hideCoco?: () => void;
|
|
||||||
hasModules?: string[];
|
hasModules?: string[];
|
||||||
searchPlaceholder?: string;
|
searchPlaceholder?: string;
|
||||||
chatPlaceholder?: string;
|
chatPlaceholder?: string;
|
||||||
@@ -78,7 +77,6 @@ export default function ChatInput({
|
|||||||
const currentAssistant = useConnectStore((state) => state.currentAssistant);
|
const currentAssistant = useConnectStore((state) => state.currentAssistant);
|
||||||
// const sessionId = useConnectStore((state) => state.currentSessionId);
|
// const sessionId = useConnectStore((state) => state.currentSessionId);
|
||||||
|
|
||||||
const showTooltip = useAppStore((state) => state.showTooltip);
|
|
||||||
const setBlurred = useAppStore((state) => state.setBlurred);
|
const setBlurred = useAppStore((state) => state.setBlurred);
|
||||||
const isTauri = useAppStore((state) => state.isTauri);
|
const isTauri = useAppStore((state) => state.isTauri);
|
||||||
|
|
||||||
@@ -86,6 +84,9 @@ export default function ChatInput({
|
|||||||
|
|
||||||
const { modifierKey, returnToInput, setModifierKeyPressed } =
|
const { modifierKey, returnToInput, setModifierKeyPressed } =
|
||||||
useShortcutsStore();
|
useShortcutsStore();
|
||||||
|
const language = useAppStore((state) => {
|
||||||
|
return state.language;
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
@@ -126,7 +127,6 @@ export default function ChatInput({
|
|||||||
useKeyboardHandlers({
|
useKeyboardHandlers({
|
||||||
isChatMode,
|
isChatMode,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
disabledChange,
|
|
||||||
curChatEnd,
|
curChatEnd,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -159,6 +159,7 @@ export default function ChatInput({
|
|||||||
const { askAI, askAIRef, assistantDetail, handleKeyDownAutoResizeTextarea } =
|
const { askAI, askAIRef, assistantDetail, handleKeyDownAutoResizeTextarea } =
|
||||||
useAssistantManager({
|
useAssistantManager({
|
||||||
isChatMode,
|
isChatMode,
|
||||||
|
inputValue,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
changeInput,
|
changeInput,
|
||||||
});
|
});
|
||||||
@@ -183,6 +184,18 @@ export default function ChatInput({
|
|||||||
return state.disabledExtensions;
|
return state.disabledExtensions;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const akiAiTooltipPrefix = useMemo(() => {
|
||||||
|
if (language === "zh") {
|
||||||
|
if (/^[a-zA-Z]/.test(askAI?.name)) {
|
||||||
|
return "问 ";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "问";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Ask";
|
||||||
|
}, [language, askAI]);
|
||||||
|
|
||||||
const renderSearchIcon = () => (
|
const renderSearchIcon = () => (
|
||||||
<SearchIcons
|
<SearchIcons
|
||||||
lineCount={lineCount}
|
lineCount={lineCount}
|
||||||
@@ -202,7 +215,7 @@ export default function ChatInput({
|
|||||||
disabledChange={disabledChange}
|
disabledChange={disabledChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{showTooltip && !isChatMode && sourceData && (
|
{!isChatMode && sourceData && (
|
||||||
<div
|
<div
|
||||||
className={`absolute ${
|
className={`absolute ${
|
||||||
lineCount === 1 ? "-top-[5px]" : "top-[calc(100%-25px)]"
|
lineCount === 1 ? "-top-[5px]" : "top-[calc(100%-25px)]"
|
||||||
@@ -212,7 +225,7 @@ export default function ChatInput({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* {showTooltip && (
|
{/*
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
`absolute ${
|
`absolute ${
|
||||||
@@ -225,7 +238,7 @@ export default function ChatInput({
|
|||||||
>
|
>
|
||||||
<VisibleKey shortcut={returnToInput} />
|
<VisibleKey shortcut={returnToInput} />
|
||||||
</div>
|
</div>
|
||||||
)} */}
|
*/}
|
||||||
|
|
||||||
{!isChatMode &&
|
{!isChatMode &&
|
||||||
isTauri &&
|
isTauri &&
|
||||||
@@ -235,7 +248,7 @@ export default function ChatInput({
|
|||||||
<div className="flex items-center gap-2 text-sm text-[#AEAEAE] dark:text-[#545454] whitespace-nowrap">
|
<div className="flex items-center gap-2 text-sm text-[#AEAEAE] dark:text-[#545454] whitespace-nowrap">
|
||||||
<span>
|
<span>
|
||||||
{t("search.askCocoAi.title", {
|
{t("search.askCocoAi.title", {
|
||||||
replace: [askAI.name],
|
replace: [akiAiTooltipPrefix, askAI.name],
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex items-center justify-center w-8 h-[20px] text-xs rounded-md border border-black/10 dark:border-[#545454]">
|
<div className="flex items-center justify-center w-8 h-[20px] text-xs rounded-md border border-black/10 dark:border-[#545454]">
|
||||||
@@ -251,7 +264,7 @@ export default function ChatInput({
|
|||||||
}}
|
}}
|
||||||
/> */}
|
/> */}
|
||||||
|
|
||||||
{showTooltip && isChatMode && (
|
{isChatMode && curChatEnd && (
|
||||||
<div
|
<div
|
||||||
className={`absolute ${
|
className={`absolute ${
|
||||||
lineCount === 1 ? "-top-[5px]" : "top-[calc(100%-30px)]"
|
lineCount === 1 ? "-top-[5px]" : "top-[calc(100%-30px)]"
|
||||||
@@ -275,6 +288,7 @@ export default function ChatInput({
|
|||||||
>
|
>
|
||||||
<AutoResizeTextarea
|
<AutoResizeTextarea
|
||||||
ref={textareaRef}
|
ref={textareaRef}
|
||||||
|
isChatMode={isChatMode}
|
||||||
input={inputValue}
|
input={inputValue}
|
||||||
setInput={handleInputChange}
|
setInput={handleInputChange}
|
||||||
handleKeyDown={handleKeyDownAutoResizeTextarea}
|
handleKeyDown={handleKeyDownAutoResizeTextarea}
|
||||||
@@ -329,7 +343,6 @@ export default function ChatInput({
|
|||||||
setIsDeepThinkActive={setIsDeepThinkActive}
|
setIsDeepThinkActive={setIsDeepThinkActive}
|
||||||
isMCPActive={isMCPActive}
|
isMCPActive={isMCPActive}
|
||||||
setIsMCPActive={setIsMCPActive}
|
setIsMCPActive={setIsMCPActive}
|
||||||
showTooltip={showTooltip}
|
|
||||||
changeMode={changeMode}
|
changeMode={changeMode}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ interface InputControlsProps {
|
|||||||
hasModules?: string[];
|
hasModules?: string[];
|
||||||
searchPlaceholder?: string;
|
searchPlaceholder?: string;
|
||||||
chatPlaceholder?: string;
|
chatPlaceholder?: string;
|
||||||
showTooltip?: boolean;
|
|
||||||
changeMode?: (isChatMode: boolean) => void;
|
changeMode?: (isChatMode: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,7 +44,6 @@ const InputControls = ({
|
|||||||
setIsMCPActive,
|
setIsMCPActive,
|
||||||
isChatPage,
|
isChatPage,
|
||||||
hasModules,
|
hasModules,
|
||||||
showTooltip,
|
|
||||||
changeMode,
|
changeMode,
|
||||||
}: InputControlsProps) => {
|
}: InputControlsProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -78,14 +76,6 @@ const InputControls = ({
|
|||||||
query?: string;
|
query?: string;
|
||||||
}
|
}
|
||||||
): Promise<DataSource[]> => {
|
): Promise<DataSource[]> => {
|
||||||
if (
|
|
||||||
!(
|
|
||||||
assistantConfig.datasourceEnabled && assistantConfig.datasourceVisible
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const body: Record<string, any> = {
|
const body: Record<string, any> = {
|
||||||
id: serverId,
|
id: serverId,
|
||||||
from: options?.from || 0,
|
from: options?.from || 0,
|
||||||
@@ -119,6 +109,7 @@ const InputControls = ({
|
|||||||
options: body,
|
options: body,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
body.id = undefined;
|
||||||
const [error, res]: any = await Post("/datasource/_search", body);
|
const [error, res]: any = await Post("/datasource/_search", body);
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error("_search", error);
|
console.error("_search", error);
|
||||||
@@ -150,9 +141,6 @@ const InputControls = ({
|
|||||||
query?: string;
|
query?: string;
|
||||||
}
|
}
|
||||||
): Promise<DataSource[]> => {
|
): Promise<DataSource[]> => {
|
||||||
if (!(assistantConfig.mcpEnabled && assistantConfig.mcpVisible)) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const body: Record<string, any> = {
|
const body: Record<string, any> = {
|
||||||
id: serverId,
|
id: serverId,
|
||||||
from: options?.from || 0,
|
from: options?.from || 0,
|
||||||
@@ -185,6 +173,7 @@ const InputControls = ({
|
|||||||
body
|
body
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
body.id = undefined;
|
||||||
const [error, res]: any = await Post("/mcp_server/_search", body);
|
const [error, res]: any = await Post("/mcp_server/_search", body);
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error("_search", error);
|
console.error("_search", error);
|
||||||
@@ -222,6 +211,7 @@ const InputControls = ({
|
|||||||
const aiOverviewAssistant = useExtensionsStore((state) => {
|
const aiOverviewAssistant = useExtensionsStore((state) => {
|
||||||
return state.aiOverviewAssistant;
|
return state.aiOverviewAssistant;
|
||||||
});
|
});
|
||||||
|
const aiOverviewShortcut = useShortcutsStore((state) => state.aiOverview);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -259,8 +249,7 @@ const InputControls = ({
|
|||||||
onKeyPress={setIsDeepThinkActive}
|
onKeyPress={setIsDeepThinkActive}
|
||||||
>
|
>
|
||||||
<Brain
|
<Brain
|
||||||
className={`size-3 ${
|
className={`size-3 ${isDeepThinkActive
|
||||||
isDeepThinkActive
|
|
||||||
? "text-[#0072FF] dark:text-[#0072FF]"
|
? "text-[#0072FF] dark:text-[#0072FF]"
|
||||||
: "text-[#333] dark:text-white"
|
: "text-[#333] dark:text-white"
|
||||||
}`}
|
}`}
|
||||||
@@ -268,8 +257,7 @@ const InputControls = ({
|
|||||||
</VisibleKey>
|
</VisibleKey>
|
||||||
{isDeepThinkActive && (
|
{isDeepThinkActive && (
|
||||||
<span
|
<span
|
||||||
className={`${
|
className={`${isDeepThinkActive ? "text-[#0072FF]" : "dark:text-white"
|
||||||
isDeepThinkActive ? "text-[#0072FF]" : "dark:text-white"
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{t("search.input.deepThink")}
|
{t("search.input.deepThink")}
|
||||||
@@ -278,21 +266,19 @@ const InputControls = ({
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{source?.datasource?.enabled && source?.datasource?.visible && (
|
|
||||||
<SearchPopover
|
<SearchPopover
|
||||||
|
datasource={source?.datasource}
|
||||||
isSearchActive={isSearchActive}
|
isSearchActive={isSearchActive}
|
||||||
setIsSearchActive={setIsSearchActive}
|
setIsSearchActive={setIsSearchActive}
|
||||||
getDataSourcesByServer={getDataSourcesByServer}
|
getDataSourcesByServer={getDataSourcesByServer}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
|
|
||||||
{source?.mcp_servers?.enabled && source?.mcp_servers?.visible && (
|
|
||||||
<MCPPopover
|
<MCPPopover
|
||||||
|
mcp_servers={source?.mcp_servers}
|
||||||
isMCPActive={isMCPActive}
|
isMCPActive={isMCPActive}
|
||||||
setIsMCPActive={setIsMCPActive}
|
setIsMCPActive={setIsMCPActive}
|
||||||
getMCPByServer={getMCPByServer}
|
getMCPByServer={getMCPByServer}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
|
|
||||||
{!(source?.datasource?.enabled && source?.datasource?.visible) &&
|
{!(source?.datasource?.enabled && source?.datasource?.visible) &&
|
||||||
(source?.type !== "deep_think" || !source?.config?.visible) &&
|
(source?.type !== "deep_think" || !source?.config?.visible) &&
|
||||||
@@ -323,8 +309,16 @@ const InputControls = ({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setEnabledAiOverview(!enabledAiOverview);
|
setEnabledAiOverview(!enabledAiOverview);
|
||||||
}}
|
}}
|
||||||
|
>
|
||||||
|
<VisibleKey
|
||||||
|
shortcut={aiOverviewShortcut}
|
||||||
|
onKeyPress={() => {
|
||||||
|
setEnabledAiOverview(!enabledAiOverview);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Sparkles className="size-4" />
|
<Sparkles className="size-4" />
|
||||||
|
</VisibleKey>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
className={clsx("text-xs", { hidden: !enabledAiOverview })}
|
className={clsx("text-xs", { hidden: !enabledAiOverview })}
|
||||||
>
|
>
|
||||||
@@ -337,7 +331,6 @@ const InputControls = ({
|
|||||||
|
|
||||||
{isChatPage || hasModules?.length !== 2 ? null : (
|
{isChatPage || hasModules?.length !== 2 ? null : (
|
||||||
<div className="relative w-16 flex justify-end items-center">
|
<div className="relative w-16 flex justify-end items-center">
|
||||||
{showTooltip && (
|
|
||||||
<div className="absolute right-[52px] -top-2 z-10">
|
<div className="absolute right-[52px] -top-2 z-10">
|
||||||
<VisibleKey
|
<VisibleKey
|
||||||
shortcut={modeSwitch}
|
shortcut={modeSwitch}
|
||||||
@@ -346,7 +339,6 @@ const InputControls = ({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
<ChatSwitch
|
<ChatSwitch
|
||||||
isChatMode={isChatMode}
|
isChatMode={isChatMode}
|
||||||
|
|||||||
@@ -16,8 +16,10 @@ import { useChatStore } from "@/stores/chatStore";
|
|||||||
import NoDataImage from "@/components/Common/NoDataImage";
|
import NoDataImage from "@/components/Common/NoDataImage";
|
||||||
import PopoverInput from "@/components/Common/PopoverInput";
|
import PopoverInput from "@/components/Common/PopoverInput";
|
||||||
import Pagination from "@/components/Common/Pagination";
|
import Pagination from "@/components/Common/Pagination";
|
||||||
|
import { specialCharacterFiltering } from "@/utils"
|
||||||
|
|
||||||
interface MCPPopoverProps {
|
interface MCPPopoverProps {
|
||||||
|
mcp_servers: any;
|
||||||
isMCPActive: boolean;
|
isMCPActive: boolean;
|
||||||
setIsMCPActive: () => void;
|
setIsMCPActive: () => void;
|
||||||
getMCPByServer: (
|
getMCPByServer: (
|
||||||
@@ -31,6 +33,7 @@ interface MCPPopoverProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function MCPPopover({
|
export default function MCPPopover({
|
||||||
|
mcp_servers,
|
||||||
isMCPActive,
|
isMCPActive,
|
||||||
setIsMCPActive,
|
setIsMCPActive,
|
||||||
getMCPByServer,
|
getMCPByServer,
|
||||||
@@ -44,6 +47,7 @@ export default function MCPPopover({
|
|||||||
const MCPIds = useSearchStore((state) => state.MCPIds);
|
const MCPIds = useSearchStore((state) => state.MCPIds);
|
||||||
const setMCPIds = useSearchStore((state) => state.setMCPIds);
|
const setMCPIds = useSearchStore((state) => state.setMCPIds);
|
||||||
|
|
||||||
|
const currentAssistant = useConnectStore((state) => state.currentAssistant);
|
||||||
const currentService = useConnectStore((state) => state.currentService);
|
const currentService = useConnectStore((state) => state.currentService);
|
||||||
|
|
||||||
const [keyword, setKeyword] = useState("");
|
const [keyword, setKeyword] = useState("");
|
||||||
@@ -98,7 +102,7 @@ export default function MCPPopover({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
connected && getDataSourceList();
|
connected && getDataSourceList();
|
||||||
}, [connected, currentService?.id, debouncedKeyword]);
|
}, [connected, currentService?.id, debouncedKeyword, currentAssistant]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTotalPage(Math.max(Math.ceil(dataList.length / 10), 1));
|
setTotalPage(Math.max(Math.ceil(dataList.length / 10), 1));
|
||||||
@@ -161,6 +165,10 @@ export default function MCPPopover({
|
|||||||
setPage(page + 1);
|
setPage(page + 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!(mcp_servers?.enabled && mcp_servers?.visible)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
@@ -173,8 +181,7 @@ export default function MCPPopover({
|
|||||||
>
|
>
|
||||||
<VisibleKey shortcut={mcpSearch} onKeyPress={setIsMCPActive}>
|
<VisibleKey shortcut={mcpSearch} onKeyPress={setIsMCPActive}>
|
||||||
<Hammer
|
<Hammer
|
||||||
className={`size-3 ${
|
className={`size-3 ${isMCPActive
|
||||||
isMCPActive
|
|
||||||
? "text-[#0072FF] dark:text-[#0072FF]"
|
? "text-[#0072FF] dark:text-[#0072FF]"
|
||||||
: "text-[#333] dark:text-white"
|
: "text-[#333] dark:text-white"
|
||||||
}`}
|
}`}
|
||||||
@@ -224,8 +231,7 @@ export default function MCPPopover({
|
|||||||
>
|
>
|
||||||
<VisibleKey shortcut="R" onKeyPress={handleRefresh}>
|
<VisibleKey shortcut="R" onKeyPress={handleRefresh}>
|
||||||
<RefreshCw
|
<RefreshCw
|
||||||
className={`size-3 text-[#0287FF] transition-transform duration-1000 ${
|
className={`size-3 text-[#0287FF] transition-transform duration-1000 ${isRefreshDataSource ? "animate-spin" : ""
|
||||||
isRefreshDataSource ? "animate-spin" : ""
|
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
</VisibleKey>
|
</VisibleKey>
|
||||||
@@ -245,10 +251,12 @@ export default function MCPPopover({
|
|||||||
|
|
||||||
<PopoverInput
|
<PopoverInput
|
||||||
autoFocus
|
autoFocus
|
||||||
|
value={keyword}
|
||||||
ref={searchInputRef}
|
ref={searchInputRef}
|
||||||
className="size-full px-2 rounded-lg border dark:border-white/10 bg-transparent"
|
className="size-full px-2 rounded-lg border dark:border-white/10 bg-transparent"
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setKeyword(e.target.value);
|
const value = specialCharacterFiltering(e.target.value.trim())
|
||||||
|
setKeyword(value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -33,6 +33,19 @@ const SearchResultsPanel = memo<{
|
|||||||
}
|
}
|
||||||
}, [input, isChatMode, performSearch, sourceData]);
|
}, [input, isChatMode, performSearch, sourceData]);
|
||||||
|
|
||||||
|
const { setSelectedAssistant, selectedSearchContent } = useSearchStore();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedSearchContent?.type === "AI Assistant") {
|
||||||
|
setSelectedAssistant({
|
||||||
|
...selectedSearchContent,
|
||||||
|
name: selectedSearchContent.title,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setSelectedAssistant(void 0);
|
||||||
|
}
|
||||||
|
}, [selectedSearchContent]);
|
||||||
|
|
||||||
if (goAskAi) return <AskAi />;
|
if (goAskAi) return <AskAi />;
|
||||||
if (suggests.length === 0) return <NoResults />;
|
if (suggests.length === 0) return <NoResults />;
|
||||||
|
|
||||||
@@ -54,11 +67,10 @@ interface SearchProps {
|
|||||||
changeInput: (val: string) => void;
|
changeInput: (val: string) => void;
|
||||||
isChatMode: boolean;
|
isChatMode: boolean;
|
||||||
input: string;
|
input: string;
|
||||||
hideCoco?: () => void;
|
|
||||||
setIsPinned?: (value: boolean) => void;
|
setIsPinned?: (value: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Search({ isChatMode, input, hideCoco, setIsPinned }: SearchProps) {
|
function Search({ isChatMode, input, setIsPinned }: SearchProps) {
|
||||||
const mainWindowRef = useRef<HTMLDivElement>(null);
|
const mainWindowRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -67,7 +79,7 @@ function Search({ isChatMode, input, hideCoco, setIsPinned }: SearchProps) {
|
|||||||
|
|
||||||
<Footer setIsPinnedWeb={setIsPinned} />
|
<Footer setIsPinnedWeb={setIsPinned} />
|
||||||
|
|
||||||
<ContextMenu hideCoco={hideCoco} />
|
<ContextMenu />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React from "react";
|
import React, { MouseEvent } from "react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { File } from "lucide-react";
|
import { File } from "lucide-react";
|
||||||
|
|
||||||
@@ -7,6 +7,7 @@ import ListRight from "./ListRight";
|
|||||||
import { useAppStore } from "@/stores/appStore";
|
import { useAppStore } from "@/stores/appStore";
|
||||||
import { useIsMobile } from "@/hooks/useIsMobile";
|
import { useIsMobile } from "@/hooks/useIsMobile";
|
||||||
import type { SearchDocument } from "@/types/search";
|
import type { SearchDocument } from "@/types/search";
|
||||||
|
import { useSearchStore } from "@/stores/searchStore";
|
||||||
|
|
||||||
interface SearchListItemProps {
|
interface SearchListItemProps {
|
||||||
item: SearchDocument;
|
item: SearchDocument;
|
||||||
@@ -36,10 +37,23 @@ const SearchListItem: React.FC<SearchListItemProps> = React.memo(
|
|||||||
|
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
|
const { setSelectedSearchContent, setVisibleContextMenu } =
|
||||||
|
useSearchStore();
|
||||||
|
|
||||||
|
const handleContextMenu = (event: MouseEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
setVisibleContextMenu(true);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={itemRef}
|
ref={itemRef}
|
||||||
onMouseEnter={onMouseEnter}
|
onMouseEnter={() => {
|
||||||
|
setSelectedSearchContent(item);
|
||||||
|
|
||||||
|
onMouseEnter();
|
||||||
|
}}
|
||||||
onClick={onItemClick}
|
onClick={onItemClick}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"w-full px-2 py-2.5 text-sm flex mb-0 flex-row items-center mobile:mb-2 mobile:flex-col mobile:items-start justify-between rounded-lg transition-colors cursor-pointer text-[#333] dark:text-[#d8d8d8]",
|
"w-full px-2 py-2.5 text-sm flex mb-0 flex-row items-center mobile:mb-2 mobile:flex-col mobile:items-start justify-between rounded-lg transition-colors cursor-pointer text-[#333] dark:text-[#d8d8d8]",
|
||||||
@@ -51,6 +65,7 @@ const SearchListItem: React.FC<SearchListItemProps> = React.memo(
|
|||||||
role="option"
|
role="option"
|
||||||
aria-selected={isSelected}
|
aria-selected={isSelected}
|
||||||
id={`search-item-${currentIndex}`}
|
id={`search-item-${currentIndex}`}
|
||||||
|
onContextMenu={handleContextMenu}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`${
|
className={`${
|
||||||
@@ -60,7 +75,12 @@ const SearchListItem: React.FC<SearchListItemProps> = React.memo(
|
|||||||
} min-w-0 flex gap-2 items-center justify-start `}
|
} min-w-0 flex gap-2 items-center justify-start `}
|
||||||
>
|
>
|
||||||
<CommonIcon
|
<CommonIcon
|
||||||
renderOrder={["special_icon", "item_icon", "connector_icon", "default_icon"]}
|
renderOrder={[
|
||||||
|
"special_icon",
|
||||||
|
"item_icon",
|
||||||
|
"connector_icon",
|
||||||
|
"default_icon",
|
||||||
|
]}
|
||||||
item={item}
|
item={item}
|
||||||
itemIcon={item?.icon}
|
itemIcon={item?.icon}
|
||||||
defaultIcon={File}
|
defaultIcon={File}
|
||||||
|
|||||||
@@ -16,8 +16,10 @@ import { useChatStore } from "@/stores/chatStore";
|
|||||||
import NoDataImage from "@/components/Common/NoDataImage";
|
import NoDataImage from "@/components/Common/NoDataImage";
|
||||||
import PopoverInput from "@/components/Common/PopoverInput";
|
import PopoverInput from "@/components/Common/PopoverInput";
|
||||||
import Pagination from "@/components/Common/Pagination";
|
import Pagination from "@/components/Common/Pagination";
|
||||||
|
import { specialCharacterFiltering } from "@/utils"
|
||||||
|
|
||||||
interface SearchPopoverProps {
|
interface SearchPopoverProps {
|
||||||
|
datasource: any;
|
||||||
isSearchActive: boolean;
|
isSearchActive: boolean;
|
||||||
setIsSearchActive: () => void;
|
setIsSearchActive: () => void;
|
||||||
getDataSourcesByServer: (
|
getDataSourcesByServer: (
|
||||||
@@ -31,6 +33,7 @@ interface SearchPopoverProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function SearchPopover({
|
export default function SearchPopover({
|
||||||
|
datasource,
|
||||||
isSearchActive,
|
isSearchActive,
|
||||||
setIsSearchActive,
|
setIsSearchActive,
|
||||||
getDataSourcesByServer,
|
getDataSourcesByServer,
|
||||||
@@ -44,6 +47,7 @@ export default function SearchPopover({
|
|||||||
const sourceDataIds = useSearchStore((state) => state.sourceDataIds);
|
const sourceDataIds = useSearchStore((state) => state.sourceDataIds);
|
||||||
const setSourceDataIds = useSearchStore((state) => state.setSourceDataIds);
|
const setSourceDataIds = useSearchStore((state) => state.setSourceDataIds);
|
||||||
|
|
||||||
|
const currentAssistant = useConnectStore((state) => state.currentAssistant);
|
||||||
const currentService = useConnectStore((state) => state.currentService);
|
const currentService = useConnectStore((state) => state.currentService);
|
||||||
|
|
||||||
const [keyword, setKeyword] = useState("");
|
const [keyword, setKeyword] = useState("");
|
||||||
@@ -101,7 +105,7 @@ export default function SearchPopover({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
connected && getDataSourceList();
|
connected && getDataSourceList();
|
||||||
}, [connected, currentService?.id, debouncedKeyword]);
|
}, [connected, currentService?.id, debouncedKeyword, currentAssistant]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTotalPage(Math.max(Math.ceil(dataSourceList.length / 10), 1));
|
setTotalPage(Math.max(Math.ceil(dataSourceList.length / 10), 1));
|
||||||
@@ -164,6 +168,10 @@ export default function SearchPopover({
|
|||||||
setPage(page + 1);
|
setPage(page + 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!(datasource?.enabled && datasource?.visible)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
@@ -176,8 +184,7 @@ export default function SearchPopover({
|
|||||||
>
|
>
|
||||||
<VisibleKey shortcut={internetSearch} onKeyPress={setIsSearchActive}>
|
<VisibleKey shortcut={internetSearch} onKeyPress={setIsSearchActive}>
|
||||||
<Globe
|
<Globe
|
||||||
className={`size-3 ${
|
className={`size-3 ${isSearchActive
|
||||||
isSearchActive
|
|
||||||
? "text-[#0072FF] dark:text-[#0072FF]"
|
? "text-[#0072FF] dark:text-[#0072FF]"
|
||||||
: "text-[#333] dark:text-white"
|
: "text-[#333] dark:text-white"
|
||||||
}`}
|
}`}
|
||||||
@@ -187,8 +194,7 @@ export default function SearchPopover({
|
|||||||
{isSearchActive && (
|
{isSearchActive && (
|
||||||
<>
|
<>
|
||||||
<span
|
<span
|
||||||
className={`${
|
className={`${isSearchActive ? "text-[#0072FF]" : "dark:text-white"
|
||||||
isSearchActive ? "text-[#0072FF]" : "dark:text-white"
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{t("search.input.search")}
|
{t("search.input.search")}
|
||||||
@@ -229,8 +235,7 @@ export default function SearchPopover({
|
|||||||
>
|
>
|
||||||
<VisibleKey shortcut="R" onKeyPress={handleRefresh}>
|
<VisibleKey shortcut="R" onKeyPress={handleRefresh}>
|
||||||
<RefreshCw
|
<RefreshCw
|
||||||
className={`size-3 text-[#0287FF] transition-transform duration-1000 ${
|
className={`size-3 text-[#0287FF] transition-transform duration-1000 ${isRefreshDataSource ? "animate-spin" : ""
|
||||||
isRefreshDataSource ? "animate-spin" : ""
|
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
</VisibleKey>
|
</VisibleKey>
|
||||||
@@ -250,10 +255,12 @@ export default function SearchPopover({
|
|||||||
|
|
||||||
<PopoverInput
|
<PopoverInput
|
||||||
autoFocus
|
autoFocus
|
||||||
|
value={keyword}
|
||||||
ref={searchInputRef}
|
ref={searchInputRef}
|
||||||
className="size-full px-2 rounded-lg border dark:border-white/10 bg-transparent"
|
className="size-full px-2 rounded-lg border dark:border-white/10 bg-transparent"
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setKeyword(e.target.value);
|
const value = specialCharacterFiltering(e.target.value.trim())
|
||||||
|
setKeyword(value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
Suspense,
|
Suspense,
|
||||||
memo,
|
memo,
|
||||||
useState,
|
useState,
|
||||||
|
useMemo,
|
||||||
} from "react";
|
} from "react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { useMount } from "ahooks";
|
import { useMount } from "ahooks";
|
||||||
@@ -36,7 +37,6 @@ interface SearchChatProps {
|
|||||||
searchPlaceholder?: string;
|
searchPlaceholder?: string;
|
||||||
chatPlaceholder?: string;
|
chatPlaceholder?: string;
|
||||||
|
|
||||||
hideCoco?: () => void;
|
|
||||||
setIsPinned?: (value: boolean) => void;
|
setIsPinned?: (value: boolean) => void;
|
||||||
onModeChange?: (isChatMode: boolean) => void;
|
onModeChange?: (isChatMode: boolean) => void;
|
||||||
isMobile?: boolean;
|
isMobile?: boolean;
|
||||||
@@ -49,7 +49,6 @@ function SearchChat({
|
|||||||
hasModules = ["search", "chat"],
|
hasModules = ["search", "chat"],
|
||||||
defaultModule = "search",
|
defaultModule = "search",
|
||||||
theme,
|
theme,
|
||||||
hideCoco,
|
|
||||||
searchPlaceholder,
|
searchPlaceholder,
|
||||||
chatPlaceholder,
|
chatPlaceholder,
|
||||||
showChatHistory = true,
|
showChatHistory = true,
|
||||||
@@ -63,12 +62,12 @@ function SearchChat({
|
|||||||
|
|
||||||
const source = currentAssistant?._source;
|
const source = currentAssistant?._source;
|
||||||
|
|
||||||
const customInitialState = {
|
const customInitialState = useMemo(() => ({
|
||||||
...initialAppState,
|
...initialAppState,
|
||||||
isDeepThinkActive: source?.type === "deep_think",
|
isDeepThinkActive: source?.type === "deep_think",
|
||||||
isSearchActive: source?.datasource?.enabled_by_default === true,
|
isSearchActive: source?.datasource?.enabled_by_default === true,
|
||||||
isMCPActive: source?.mcp_servers?.enabled_by_default === true,
|
isMCPActive: source?.mcp_servers?.enabled_by_default === true,
|
||||||
};
|
}), [source]);
|
||||||
|
|
||||||
const [state, dispatch] = useReducer(appReducer, customInitialState);
|
const [state, dispatch] = useReducer(appReducer, customInitialState);
|
||||||
const {
|
const {
|
||||||
@@ -80,17 +79,23 @@ function SearchChat({
|
|||||||
isMCPActive,
|
isMCPActive,
|
||||||
isTyping,
|
isTyping,
|
||||||
} = state;
|
} = state;
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch({ type: "SET_SEARCH_ACTIVE", payload: customInitialState.isSearchActive });
|
||||||
|
dispatch({ type: "SET_DEEP_THINK_ACTIVE", payload: customInitialState.isDeepThinkActive });
|
||||||
|
dispatch({ type: "SET_MCP_ACTIVE", payload: customInitialState.isMCPActive });
|
||||||
|
}, [customInitialState]);
|
||||||
|
|
||||||
const [isWin10, setIsWin10] = useState(false);
|
const [isWin10, setIsWin10] = useState(false);
|
||||||
const blurred = useAppStore((state) => state.blurred);
|
const blurred = useAppStore((state) => state.blurred);
|
||||||
|
|
||||||
useWindowEvents();
|
useWindowEvents();
|
||||||
|
|
||||||
const initializeListeners = useAppStore((state) => state.initializeListeners);
|
const initializeListeners_auth = useAuthStore((state) => {
|
||||||
const initializeListeners_auth = useAuthStore(
|
return state.initializeListeners;
|
||||||
(state) => state.initializeListeners
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const setTheme = useThemeStore((state) => state.setTheme);
|
const setTheme = useThemeStore((state) => state.setTheme);
|
||||||
|
const setIsDark = useThemeStore((state) => state.setIsDark);
|
||||||
|
|
||||||
const isChatModeRef = useRef(false);
|
const isChatModeRef = useRef(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -103,7 +108,7 @@ function SearchChat({
|
|||||||
setIsWin10(isWin10);
|
setIsWin10(isWin10);
|
||||||
|
|
||||||
const unlisten = platformAdapter.listenEvent("show-coco", () => {
|
const unlisten = platformAdapter.listenEvent("show-coco", () => {
|
||||||
console.log("show-coco");
|
//console.log("show-coco");
|
||||||
|
|
||||||
platformAdapter.invokeBackend("simulate_mouse_click", {
|
platformAdapter.invokeBackend("simulate_mouse_click", {
|
||||||
isChatMode: isChatModeRef.current,
|
isChatMode: isChatModeRef.current,
|
||||||
@@ -118,7 +123,6 @@ function SearchChat({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const init = async () => {
|
const init = async () => {
|
||||||
await initializeListeners();
|
|
||||||
await initializeListeners_auth();
|
await initializeListeners_auth();
|
||||||
await platformAdapter.invokeBackend("get_app_search_source");
|
await platformAdapter.invokeBackend("get_app_search_source");
|
||||||
};
|
};
|
||||||
@@ -129,6 +133,7 @@ function SearchChat({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!theme) return;
|
if (!theme) return;
|
||||||
|
|
||||||
|
setIsDark(theme === "dark");
|
||||||
setTheme(theme);
|
setTheme(theme);
|
||||||
}, [theme]);
|
}, [theme]);
|
||||||
|
|
||||||
@@ -231,10 +236,10 @@ function SearchChat({
|
|||||||
if (isTauri) {
|
if (isTauri) {
|
||||||
changeMode(defaultStartupWindow === "chatMode");
|
changeMode(defaultStartupWindow === "chatMode");
|
||||||
} else {
|
} else {
|
||||||
if (hasModules?.length === 1 && hasModules?.includes("chat")) {
|
if (hasModules?.length > 1) {
|
||||||
changeMode(true);
|
|
||||||
} else {
|
|
||||||
changeMode(defaultModule === "chat");
|
changeMode(defaultModule === "chat");
|
||||||
|
} else {
|
||||||
|
changeMode(hasModules?.includes("chat") ?? false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
@@ -259,10 +264,13 @@ function SearchChat({
|
|||||||
)}
|
)}
|
||||||
style={{ opacity: blurred ? (opacity ?? 30) / 100 : 1 }}
|
style={{ opacity: blurred ? (opacity ?? 30) / 100 : 1 }}
|
||||||
>
|
>
|
||||||
{isTransitioned && (
|
|
||||||
<div
|
<div
|
||||||
data-tauri-drag-region={isTauri}
|
data-tauri-drag-region={isTauri}
|
||||||
className="flex-1 w-full overflow-hidden"
|
className={clsx(
|
||||||
|
"flex-1 w-full overflow-auto",
|
||||||
|
{ "hidden": !isTransitioned }
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<Suspense fallback={<LoadingFallback />}>
|
<Suspense fallback={<LoadingFallback />}>
|
||||||
<ChatAI
|
<ChatAI
|
||||||
@@ -279,12 +287,10 @@ function SearchChat({
|
|||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
data-tauri-drag-region={isTauri}
|
data-tauri-drag-region={isTauri}
|
||||||
className={`p-2 w-full flex justify-center transition-all duration-500 min-h-[82px] ${
|
className={`p-2 w-full flex justify-center transition-all duration-500 min-h-[82px] ${isTransitioned ? "border-t" : "border-b"
|
||||||
isTransitioned ? "border-t" : "border-b"
|
|
||||||
} border-[#E6E6E6] dark:border-[#272626]`}
|
} border-[#E6E6E6] dark:border-[#272626]`}
|
||||||
>
|
>
|
||||||
<InputBox
|
<InputBox
|
||||||
@@ -315,14 +321,15 @@ function SearchChat({
|
|||||||
hasModules={hasModules}
|
hasModules={hasModules}
|
||||||
searchPlaceholder={searchPlaceholder}
|
searchPlaceholder={searchPlaceholder}
|
||||||
chatPlaceholder={chatPlaceholder}
|
chatPlaceholder={chatPlaceholder}
|
||||||
hideCoco={hideCoco}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!isTransitioned && (
|
|
||||||
<div
|
<div
|
||||||
data-tauri-drag-region={isTauri}
|
data-tauri-drag-region={isTauri}
|
||||||
className="flex-1 w-full overflow-auto"
|
className={clsx(
|
||||||
|
"flex-1 w-full overflow-auto",
|
||||||
|
{ "hidden": isTransitioned }
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<Suspense fallback={<LoadingFallback />}>
|
<Suspense fallback={<LoadingFallback />}>
|
||||||
<Search
|
<Search
|
||||||
@@ -330,12 +337,10 @@ function SearchChat({
|
|||||||
input={input}
|
input={input}
|
||||||
isChatMode={isChatMode}
|
isChatMode={isChatMode}
|
||||||
changeInput={setInput}
|
changeInput={setInput}
|
||||||
hideCoco={hideCoco}
|
|
||||||
setIsPinned={setIsPinned}
|
setIsPinned={setIsPinned}
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import { isMac } from "@/utils/platform";
|
|||||||
import {
|
import {
|
||||||
INITIAL_MODE_SWITCH,
|
INITIAL_MODE_SWITCH,
|
||||||
INITIAL_RETURN_TO_INPUT,
|
INITIAL_RETURN_TO_INPUT,
|
||||||
INITIAL_VOICE_INPUT,
|
// INITIAL_VOICE_INPUT,
|
||||||
INITIAL_ADD_FILE,
|
// INITIAL_ADD_FILE,
|
||||||
INITIAL_DEEP_THINKING,
|
INITIAL_DEEP_THINKING,
|
||||||
INITIAL_INTERNET_SEARCH,
|
INITIAL_INTERNET_SEARCH,
|
||||||
INITIAL_INTERNET_SEARCH_SCOPE,
|
INITIAL_INTERNET_SEARCH_SCOPE,
|
||||||
@@ -22,11 +22,14 @@ import {
|
|||||||
INITIAL_SERVICE_LIST,
|
INITIAL_SERVICE_LIST,
|
||||||
INITIAL_EXTERNAL,
|
INITIAL_EXTERNAL,
|
||||||
useShortcutsStore,
|
useShortcutsStore,
|
||||||
|
INITIAL_AI_OVERVIEW,
|
||||||
} from "@/stores/shortcutsStore";
|
} from "@/stores/shortcutsStore";
|
||||||
import { ModifierKey } from "@/types/index";
|
import { ModifierKey } from "@/types/index";
|
||||||
import platformAdapter from "@/utils/platformAdapter";
|
import platformAdapter from "@/utils/platformAdapter";
|
||||||
import { useAppStore } from "@/stores/appStore";
|
import { useAppStore } from "@/stores/appStore";
|
||||||
import SettingsInput from "@/components/Settings/SettingsInput";
|
import SettingsInput from "@/components/Settings/SettingsInput";
|
||||||
|
import { Button } from "@headlessui/react";
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
export const modifierKeys: ModifierKey[] = isMac
|
export const modifierKeys: ModifierKey[] = isMac
|
||||||
? ["meta", "ctrl"]
|
? ["meta", "ctrl"]
|
||||||
@@ -40,10 +43,10 @@ const Shortcuts = () => {
|
|||||||
const setModeSwitch = useShortcutsStore((state) => state.setModeSwitch);
|
const setModeSwitch = useShortcutsStore((state) => state.setModeSwitch);
|
||||||
const returnToInput = useShortcutsStore((state) => state.returnToInput);
|
const returnToInput = useShortcutsStore((state) => state.returnToInput);
|
||||||
const setReturnToInput = useShortcutsStore((state) => state.setReturnToInput);
|
const setReturnToInput = useShortcutsStore((state) => state.setReturnToInput);
|
||||||
const voiceInput = useShortcutsStore((state) => state.voiceInput);
|
// const voiceInput = useShortcutsStore((state) => state.voiceInput);
|
||||||
const setVoiceInput = useShortcutsStore((state) => state.setVoiceInput);
|
// const setVoiceInput = useShortcutsStore((state) => state.setVoiceInput);
|
||||||
const addFile = useShortcutsStore((state) => state.addFile);
|
// const addFile = useShortcutsStore((state) => state.addFile);
|
||||||
const setAddFile = useShortcutsStore((state) => state.setAddFile);
|
// const setAddFile = useShortcutsStore((state) => state.setAddFile);
|
||||||
const deepThinking = useShortcutsStore((state) => state.deepThinking);
|
const deepThinking = useShortcutsStore((state) => state.deepThinking);
|
||||||
const setDeepThinking = useShortcutsStore((state) => state.setDeepThinking);
|
const setDeepThinking = useShortcutsStore((state) => state.setDeepThinking);
|
||||||
const internetSearch = useShortcutsStore((state) => state.internetSearch);
|
const internetSearch = useShortcutsStore((state) => state.internetSearch);
|
||||||
@@ -87,6 +90,8 @@ const Shortcuts = () => {
|
|||||||
const external = useShortcutsStore((state) => state.external);
|
const external = useShortcutsStore((state) => state.external);
|
||||||
const setExternal = useShortcutsStore((state) => state.setExternal);
|
const setExternal = useShortcutsStore((state) => state.setExternal);
|
||||||
const addError = useAppStore((state) => state.addError);
|
const addError = useAppStore((state) => state.addError);
|
||||||
|
const aiOverview = useShortcutsStore((state) => state.aiOverview);
|
||||||
|
const setAiOverview = useShortcutsStore((state) => state.setAiOverview);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unlisten = useShortcutsStore.subscribe((state) => {
|
const unlisten = useShortcutsStore.subscribe((state) => {
|
||||||
@@ -100,138 +105,119 @@ const Shortcuts = () => {
|
|||||||
{
|
{
|
||||||
title: "settings.advanced.shortcuts.modeSwitch.title",
|
title: "settings.advanced.shortcuts.modeSwitch.title",
|
||||||
description: "settings.advanced.shortcuts.modeSwitch.description",
|
description: "settings.advanced.shortcuts.modeSwitch.description",
|
||||||
|
initialValue: INITIAL_MODE_SWITCH,
|
||||||
value: modeSwitch,
|
value: modeSwitch,
|
||||||
setValue: setModeSwitch,
|
setValue: setModeSwitch,
|
||||||
reset: () => {
|
|
||||||
setModeSwitch(INITIAL_MODE_SWITCH);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "settings.advanced.shortcuts.returnToInput.title",
|
title: "settings.advanced.shortcuts.returnToInput.title",
|
||||||
description: "settings.advanced.shortcuts.returnToInput.description",
|
description: "settings.advanced.shortcuts.returnToInput.description",
|
||||||
|
initialValue: INITIAL_RETURN_TO_INPUT,
|
||||||
value: returnToInput,
|
value: returnToInput,
|
||||||
setValue: setReturnToInput,
|
setValue: setReturnToInput,
|
||||||
reset: () => {
|
|
||||||
setReturnToInput(INITIAL_RETURN_TO_INPUT);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "settings.advanced.shortcuts.voiceInput.title",
|
|
||||||
description: "settings.advanced.shortcuts.voiceInput.description",
|
|
||||||
value: voiceInput,
|
|
||||||
setValue: setVoiceInput,
|
|
||||||
reset: () => {
|
|
||||||
setVoiceInput(INITIAL_VOICE_INPUT);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "settings.advanced.shortcuts.addFile.title",
|
|
||||||
description: "settings.advanced.shortcuts.addFile.description",
|
|
||||||
value: addFile,
|
|
||||||
setValue: setAddFile,
|
|
||||||
reset: () => {
|
|
||||||
setAddFile(INITIAL_ADD_FILE);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
// {
|
||||||
|
// title: "settings.advanced.shortcuts.voiceInput.title",
|
||||||
|
// description: "settings.advanced.shortcuts.voiceInput.description",
|
||||||
|
// value: voiceInput,
|
||||||
|
// setValue: setVoiceInput,
|
||||||
|
// reset: () => {
|
||||||
|
// handleChange(INITIAL_VOICE_INPUT, setVoiceInput);
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// title: "settings.advanced.shortcuts.addFile.title",
|
||||||
|
// description: "settings.advanced.shortcuts.addFile.description",
|
||||||
|
// value: addFile,
|
||||||
|
// setValue: setAddFile,
|
||||||
|
// reset: () => {
|
||||||
|
// handleChange(INITIAL_ADD_FILE, setAddFile);
|
||||||
|
// },
|
||||||
|
// },
|
||||||
{
|
{
|
||||||
title: "settings.advanced.shortcuts.deepThinking.title",
|
title: "settings.advanced.shortcuts.deepThinking.title",
|
||||||
description: "settings.advanced.shortcuts.deepThinking.description",
|
description: "settings.advanced.shortcuts.deepThinking.description",
|
||||||
|
initialValue: INITIAL_DEEP_THINKING,
|
||||||
value: deepThinking,
|
value: deepThinking,
|
||||||
setValue: setDeepThinking,
|
setValue: setDeepThinking,
|
||||||
reset: () => {
|
|
||||||
setDeepThinking(INITIAL_DEEP_THINKING);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "settings.advanced.shortcuts.internetSearch.title",
|
title: "settings.advanced.shortcuts.internetSearch.title",
|
||||||
description: "settings.advanced.shortcuts.internetSearch.description",
|
description: "settings.advanced.shortcuts.internetSearch.description",
|
||||||
|
initialValue: INITIAL_INTERNET_SEARCH,
|
||||||
value: internetSearch,
|
value: internetSearch,
|
||||||
setValue: setInternetSearch,
|
setValue: setInternetSearch,
|
||||||
reset: () => {
|
|
||||||
setInternetSearch(INITIAL_INTERNET_SEARCH);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "settings.advanced.shortcuts.internetSearchScope.title",
|
title: "settings.advanced.shortcuts.internetSearchScope.title",
|
||||||
description:
|
description:
|
||||||
"settings.advanced.shortcuts.internetSearchScope.description",
|
"settings.advanced.shortcuts.internetSearchScope.description",
|
||||||
|
initialValue: INITIAL_INTERNET_SEARCH_SCOPE,
|
||||||
value: internetSearchScope,
|
value: internetSearchScope,
|
||||||
setValue: setInternetSearchScope,
|
setValue: setInternetSearchScope,
|
||||||
reset: () => {
|
|
||||||
setInternetSearchScope(INITIAL_INTERNET_SEARCH_SCOPE);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "settings.advanced.shortcuts.mcpSearch.title",
|
title: "settings.advanced.shortcuts.mcpSearch.title",
|
||||||
description: "settings.advanced.shortcuts.mcpSearch.description",
|
description: "settings.advanced.shortcuts.mcpSearch.description",
|
||||||
|
initialValue: INITIAL_MCP_SEARCH,
|
||||||
value: mcpSearch,
|
value: mcpSearch,
|
||||||
setValue: setMcpSearch,
|
setValue: setMcpSearch,
|
||||||
reset: () => {
|
|
||||||
setMcpSearch(INITIAL_MCP_SEARCH);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "settings.advanced.shortcuts.mcpSearchScope.title",
|
title: "settings.advanced.shortcuts.mcpSearchScope.title",
|
||||||
description: "settings.advanced.shortcuts.mcpSearchScope.description",
|
description: "settings.advanced.shortcuts.mcpSearchScope.description",
|
||||||
|
initialValue: INITIAL_MCP_SEARCH_SCOPE,
|
||||||
value: mcpSearchScope,
|
value: mcpSearchScope,
|
||||||
setValue: setMcpSearchScope,
|
setValue: setMcpSearchScope,
|
||||||
reset: () => {
|
|
||||||
setMcpSearchScope(INITIAL_MCP_SEARCH_SCOPE);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "settings.advanced.shortcuts.historicalRecords.title",
|
title: "settings.advanced.shortcuts.historicalRecords.title",
|
||||||
description: "settings.advanced.shortcuts.historicalRecords.description",
|
description: "settings.advanced.shortcuts.historicalRecords.description",
|
||||||
|
initialValue: INITIAL_HISTORICAL_RECORDS,
|
||||||
value: historicalRecords,
|
value: historicalRecords,
|
||||||
setValue: setHistoricalRecords,
|
setValue: setHistoricalRecords,
|
||||||
reset: () => {
|
|
||||||
setHistoricalRecords(INITIAL_HISTORICAL_RECORDS);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "settings.advanced.shortcuts.aiAssistant.title",
|
title: "settings.advanced.shortcuts.aiAssistant.title",
|
||||||
description: "settings.advanced.shortcuts.aiAssistant.description",
|
description: "settings.advanced.shortcuts.aiAssistant.description",
|
||||||
|
initialValue: INITIAL_AI_ASSISTANT,
|
||||||
value: aiAssistant,
|
value: aiAssistant,
|
||||||
setValue: setAiAssistant,
|
setValue: setAiAssistant,
|
||||||
reset: () => {
|
|
||||||
setAiAssistant(INITIAL_AI_ASSISTANT);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "settings.advanced.shortcuts.newSession.title",
|
title: "settings.advanced.shortcuts.newSession.title",
|
||||||
description: "settings.advanced.shortcuts.newSession.description",
|
description: "settings.advanced.shortcuts.newSession.description",
|
||||||
|
initialValue: INITIAL_NEW_SESSION,
|
||||||
value: newSession,
|
value: newSession,
|
||||||
setValue: setNewSession,
|
setValue: setNewSession,
|
||||||
reset: () => {
|
|
||||||
setNewSession(INITIAL_NEW_SESSION);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "settings.advanced.shortcuts.fixedWindow.title",
|
title: "settings.advanced.shortcuts.fixedWindow.title",
|
||||||
description: "settings.advanced.shortcuts.fixedWindow.description",
|
description: "settings.advanced.shortcuts.fixedWindow.description",
|
||||||
|
initialValue: INITIAL_FIXED_WINDOW,
|
||||||
value: fixedWindow,
|
value: fixedWindow,
|
||||||
setValue: setFixedWindow,
|
setValue: setFixedWindow,
|
||||||
reset: () => {
|
|
||||||
setFixedWindow(INITIAL_FIXED_WINDOW);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "settings.advanced.shortcuts.serviceList.title",
|
title: "settings.advanced.shortcuts.serviceList.title",
|
||||||
description: "settings.advanced.shortcuts.serviceList.description",
|
description: "settings.advanced.shortcuts.serviceList.description",
|
||||||
|
initialValue: INITIAL_SERVICE_LIST,
|
||||||
value: serviceList,
|
value: serviceList,
|
||||||
setValue: setServiceList,
|
setValue: setServiceList,
|
||||||
reset: () => {
|
|
||||||
setServiceList(INITIAL_SERVICE_LIST);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "settings.advanced.shortcuts.external.title",
|
title: "settings.advanced.shortcuts.external.title",
|
||||||
description: "settings.advanced.shortcuts.external.description",
|
description: "settings.advanced.shortcuts.external.description",
|
||||||
|
initialValue: INITIAL_EXTERNAL,
|
||||||
value: external,
|
value: external,
|
||||||
setValue: setExternal,
|
setValue: setExternal,
|
||||||
reset: () => {
|
|
||||||
setExternal(INITIAL_EXTERNAL);
|
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "settings.advanced.shortcuts.aiOverview.title",
|
||||||
|
description: "settings.advanced.shortcuts.aiOverview.description",
|
||||||
|
initialValue: INITIAL_AI_OVERVIEW,
|
||||||
|
value: aiOverview,
|
||||||
|
setValue: setAiOverview,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -290,7 +276,9 @@ const Shortcuts = () => {
|
|||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
|
|
||||||
{list.map((item) => {
|
{list.map((item) => {
|
||||||
const { title, description, value, setValue, reset } = item;
|
const { title, description, initialValue, value, setValue } = item;
|
||||||
|
|
||||||
|
const disabled = value === initialValue;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsItem
|
<SettingsItem
|
||||||
@@ -310,12 +298,25 @@ const Shortcuts = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<button
|
<Button
|
||||||
className="flex items-center justify-center size-8 rounded-md border border-black/5 dark:border-white/10 hover:border-[#0072FF] transition"
|
disabled={disabled}
|
||||||
onClick={reset}
|
className={clsx(
|
||||||
|
"flex items-center justify-center size-8 rounded-md border border-black/5 dark:border-white/10 transition",
|
||||||
|
{
|
||||||
|
"hover:border-[#0072FF]": !disabled,
|
||||||
|
"opacity-70 cursor-not-allowed": disabled,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
handleChange(initialValue, setValue);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<RotateCcw className="size-4 text-[#0072FF]" />
|
<RotateCcw
|
||||||
</button>
|
className={clsx("size-4 text-[#999]", {
|
||||||
|
"!text-[#0072FF]": !disabled,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -71,13 +71,8 @@ const Advanced = () => {
|
|||||||
platformAdapter.emitEvent("change-startup-store", state);
|
platformAdapter.emitEvent("change-startup-store", state);
|
||||||
});
|
});
|
||||||
|
|
||||||
const unsubscribeConnect = useConnectStore.subscribe((state) => {
|
|
||||||
platformAdapter.emitEvent("change-connect-store", state);
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
unsubscribeStartup();
|
unsubscribeStartup();
|
||||||
unsubscribeConnect();
|
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useExtensionsStore } from "@/stores/extensionsStore";
|
import { useExtensionsStore } from "@/stores/extensionsStore";
|
||||||
import SharedAi from "../SharedAi";
|
import SharedAi from "../SharedAi";
|
||||||
import SettingsInput from "@/components/Settings/SettingsInput";
|
import SettingsInput from "@/components/Settings/SettingsInput";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
const AiOverview = () => {
|
const AiOverview = () => {
|
||||||
const aiOverviewServer = useExtensionsStore((state) => {
|
const aiOverviewServer = useExtensionsStore((state) => {
|
||||||
@@ -27,15 +28,33 @@ const AiOverview = () => {
|
|||||||
const setAiOverviewDelay = useExtensionsStore((state) => {
|
const setAiOverviewDelay = useExtensionsStore((state) => {
|
||||||
return state.setAiOverviewDelay;
|
return state.setAiOverviewDelay;
|
||||||
});
|
});
|
||||||
|
const aiOverviewMinQuantity = useExtensionsStore((state) => {
|
||||||
|
return state.aiOverviewMinQuantity;
|
||||||
|
});
|
||||||
|
const setAiOverviewMinQuantity = useExtensionsStore((state) => {
|
||||||
|
return state.setAiOverviewMinQuantity;
|
||||||
|
});
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const inputList = [
|
const inputList = [
|
||||||
{
|
{
|
||||||
label: "Minimum Input Length(characters)",
|
label: t(
|
||||||
|
"settings.extensions.aiOverview.details.aiOverviewTrigger.label.minQuantity"
|
||||||
|
),
|
||||||
|
value: aiOverviewMinQuantity,
|
||||||
|
onChange: setAiOverviewMinQuantity,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t(
|
||||||
|
"settings.extensions.aiOverview.details.aiOverviewTrigger.label.minCharLen"
|
||||||
|
),
|
||||||
value: aiOverviewCharLen,
|
value: aiOverviewCharLen,
|
||||||
onChange: setAiOverviewCharLen,
|
onChange: setAiOverviewCharLen,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Delay After Typing Stops(seconds)",
|
label: t(
|
||||||
|
"settings.extensions.aiOverview.details.aiOverviewTrigger.label.minDelay"
|
||||||
|
),
|
||||||
value: aiOverviewDelay,
|
value: aiOverviewDelay,
|
||||||
onChange: setAiOverviewDelay,
|
onChange: setAiOverviewDelay,
|
||||||
},
|
},
|
||||||
@@ -52,13 +71,15 @@ const AiOverview = () => {
|
|||||||
setAssistant={setAiOverviewAssistant}
|
setAssistant={setAiOverviewAssistant}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="text-sm">
|
<>
|
||||||
<div className="mt-6 text-[#333] dark:text-white/90">
|
<div className="mt-6 text-[#333] dark:text-white/90">
|
||||||
AI Overview Trigger
|
{t("settings.extensions.aiOverview.details.aiOverviewTrigger.title")}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="pt-2 pb-4 text-[#999]">
|
<div className="pt-2 pb-4 text-[#999]">
|
||||||
AI Overview will be triggered when both conditions are met.
|
{t(
|
||||||
|
"settings.extensions.aiOverview.details.aiOverviewTrigger.description"
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
@@ -83,7 +104,7 @@ const AiOverview = () => {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ const Applications = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="text-sm">
|
<>
|
||||||
<div className="text-[#999]">
|
<div className="text-[#999]">
|
||||||
<p className="font-bold mb-2">
|
<p className="font-bold mb-2">
|
||||||
{t("settings.extensions.application.details.searchScope")}
|
{t("settings.extensions.application.details.searchScope")}
|
||||||
@@ -106,7 +106,7 @@ const Applications = () => {
|
|||||||
<SquareArrowOutUpRight
|
<SquareArrowOutUpRight
|
||||||
className="size-4 cursor-pointer"
|
className="size-4 cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
platformAdapter.openExternal(item);
|
platformAdapter.revealItemInDir(item);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -119,7 +119,7 @@ const Applications = () => {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
const Calculator = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="text-[#999]">
|
||||||
|
{t("settings.extensions.calculator.description")}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Calculator;
|
||||||
@@ -6,6 +6,8 @@ import platformAdapter from "@/utils/platformAdapter";
|
|||||||
import { useAsyncEffect, useMount } from "ahooks";
|
import { useAsyncEffect, useMount } from "ahooks";
|
||||||
import { FC, useMemo, useState } from "react";
|
import { FC, useMemo, useState } from "react";
|
||||||
import { ExtensionId } from "../../..";
|
import { ExtensionId } from "../../..";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { isArray } from "lodash-es";
|
||||||
|
|
||||||
interface SharedAiProps {
|
interface SharedAiProps {
|
||||||
id: ExtensionId;
|
id: ExtensionId;
|
||||||
@@ -22,6 +24,7 @@ const SharedAi: FC<SharedAiProps> = (props) => {
|
|||||||
const [assistantList, setAssistantList] = useState<any[]>([assistant]);
|
const [assistantList, setAssistantList] = useState<any[]>([assistant]);
|
||||||
const addError = useAppStore((state) => state.addError);
|
const addError = useAppStore((state) => state.addError);
|
||||||
const { fetchAssistant } = AssistantFetcher({});
|
const { fetchAssistant } = AssistantFetcher({});
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
useMount(async () => {
|
useMount(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -29,11 +32,25 @@ const SharedAi: FC<SharedAiProps> = (props) => {
|
|||||||
"list_coco_servers"
|
"list_coco_servers"
|
||||||
);
|
);
|
||||||
|
|
||||||
setServerList(data);
|
if (isArray(data)) {
|
||||||
|
const enabledServers = data.filter(
|
||||||
|
(s) => s.enabled && s.available && (s.public || s.profile)
|
||||||
|
);
|
||||||
|
|
||||||
if (server) return;
|
setServerList(enabledServers);
|
||||||
|
|
||||||
setServer(data[0]);
|
if (server) {
|
||||||
|
const matchServer = enabledServers.find((item) => {
|
||||||
|
return item.id === server.id;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (matchServer) {
|
||||||
|
return setServer(matchServer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setServer(enabledServers[0]);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
addError(String(error));
|
addError(String(error));
|
||||||
}
|
}
|
||||||
@@ -72,7 +89,9 @@ const SharedAi: FC<SharedAiProps> = (props) => {
|
|||||||
const selectList = useMemo(() => {
|
const selectList = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
label: "Coco Server",
|
label: t(
|
||||||
|
"settings.extensions.shardAi.details.linkedAssistant.label.cocoServer"
|
||||||
|
),
|
||||||
value: server?.id,
|
value: server?.id,
|
||||||
icon: server?.provider?.icon,
|
icon: server?.provider?.icon,
|
||||||
data: serverList,
|
data: serverList,
|
||||||
@@ -83,7 +102,9 @@ const SharedAi: FC<SharedAiProps> = (props) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "AI Assistant",
|
label: t(
|
||||||
|
"settings.extensions.shardAi.details.linkedAssistant.label.aiAssistant"
|
||||||
|
),
|
||||||
value: assistant?.id,
|
value: assistant?.id,
|
||||||
icon: assistant?.icon,
|
icon: assistant?.icon,
|
||||||
data: assistantList,
|
data: assistantList,
|
||||||
@@ -98,19 +119,21 @@ const SharedAi: FC<SharedAiProps> = (props) => {
|
|||||||
|
|
||||||
const renderDescription = () => {
|
const renderDescription = () => {
|
||||||
if (id === "QuickAIAccess") {
|
if (id === "QuickAIAccess") {
|
||||||
return "Quick AI access allows you to start a conversation immediately from the search box using the tab key.";
|
return t("settings.extensions.quickAiAccess.description");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (id === "AIOverview") {
|
if (id === "AIOverview") {
|
||||||
return "AI Summarize generates concise summaries based on your search results, helping you quickly grasp key information without reading every document.";
|
return t("settings.extensions.aiOverview.description");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="text-sm">
|
<>
|
||||||
<div className="text-[#999]">{renderDescription()}</div>
|
<div className="text-[#999]">{renderDescription()}</div>
|
||||||
|
|
||||||
<div className="mt-6 text-[#333] dark:text-white/90">LinkedAssistant</div>
|
<div className="mt-6 text-[#333] dark:text-white/90">
|
||||||
|
{t("settings.extensions.shardAi.details.linkedAssistant.title")}
|
||||||
|
</div>
|
||||||
|
|
||||||
{selectList.map((item) => {
|
{selectList.map((item) => {
|
||||||
const { label, value, icon, data, onChange } = item;
|
const { label, value, icon, data, onChange } = item;
|
||||||
@@ -138,7 +161,7 @@ const SharedAi: FC<SharedAiProps> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import Application from "./Application";
|
|||||||
import { useExtensionsStore } from "@/stores/extensionsStore";
|
import { useExtensionsStore } from "@/stores/extensionsStore";
|
||||||
import SharedAi from "./SharedAi";
|
import SharedAi from "./SharedAi";
|
||||||
import AiOverview from "./AiOverview";
|
import AiOverview from "./AiOverview";
|
||||||
|
import Calculator from "./Calculator";
|
||||||
|
|
||||||
const Details = () => {
|
const Details = () => {
|
||||||
const { rootState } = useContext(ExtensionsContext);
|
const { rootState } = useContext(ExtensionsContext);
|
||||||
@@ -51,6 +52,10 @@ const Details = () => {
|
|||||||
if (id === "AIOverview") {
|
if (id === "AIOverview") {
|
||||||
return <AiOverview />;
|
return <AiOverview />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (id === "Calculator") {
|
||||||
|
return <Calculator />;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -59,7 +64,7 @@ const Details = () => {
|
|||||||
{rootState.activeExtension?.title}
|
{rootState.activeExtension?.title}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div className="pr-4 pb-4">{renderContent()}</div>
|
<div className="pr-4 pb-4 text-sm">{renderContent()}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,9 +12,7 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { isTauri } from "@tauri-apps/api/core";
|
import { isTauri } from "@tauri-apps/api/core";
|
||||||
import {
|
import { isEnabled } from "@tauri-apps/plugin-autostart";
|
||||||
isEnabled,
|
|
||||||
} from "@tauri-apps/plugin-autostart";
|
|
||||||
import { emit } from "@tauri-apps/api/event";
|
import { emit } from "@tauri-apps/api/event";
|
||||||
import { useCreation } from "ahooks";
|
import { useCreation } from "ahooks";
|
||||||
|
|
||||||
@@ -26,7 +24,12 @@ import { useShortcutEditor } from "@/hooks/useShortcutEditor";
|
|||||||
import { useAppStore } from "@/stores/appStore";
|
import { useAppStore } from "@/stores/appStore";
|
||||||
import { AppTheme } from "@/types/index";
|
import { AppTheme } from "@/types/index";
|
||||||
import { useThemeStore } from "@/stores/themeStore";
|
import { useThemeStore } from "@/stores/themeStore";
|
||||||
import { change_autostart, get_current_shortcut, change_shortcut, unregister_shortcut } from "@/commands"
|
import {
|
||||||
|
change_autostart,
|
||||||
|
get_current_shortcut,
|
||||||
|
change_shortcut,
|
||||||
|
unregister_shortcut,
|
||||||
|
} from "@/commands";
|
||||||
|
|
||||||
export function ThemeOption({
|
export function ThemeOption({
|
||||||
icon: Icon,
|
icon: Icon,
|
||||||
@@ -132,9 +135,6 @@ export default function GeneralSettings() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchAutoStartStatus();
|
fetchAutoStartStatus();
|
||||||
getCurrentShortcut();
|
getCurrentShortcut();
|
||||||
if (language) {
|
|
||||||
i18n.changeLanguage(language);
|
|
||||||
}
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const changeShortcut = (key: Shortcut) => {
|
const changeShortcut = (key: Shortcut) => {
|
||||||
@@ -180,17 +180,6 @@ export default function GeneralSettings() {
|
|||||||
|
|
||||||
const currentLanguage = language || i18n.language;
|
const currentLanguage = language || i18n.language;
|
||||||
|
|
||||||
const changeLanguage = async (lang: string) => {
|
|
||||||
i18n.changeLanguage(lang);
|
|
||||||
setLanguage(lang);
|
|
||||||
//
|
|
||||||
try {
|
|
||||||
await emit("language-changed", { language: lang });
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to emit language change event:", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
<div>
|
<div>
|
||||||
@@ -262,7 +251,7 @@ export default function GeneralSettings() {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<select
|
<select
|
||||||
value={currentLanguage}
|
value={currentLanguage}
|
||||||
onChange={(e) => changeLanguage(e.target.value)}
|
onChange={(e) => setLanguage(e.target.value)}
|
||||||
className="px-3 py-1.5 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
className="px-3 py-1.5 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
>
|
>
|
||||||
<option value="en">{t("settings.language.english")}</option>
|
<option value="en">{t("settings.language.english")}</option>
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import clsx from "clsx";
|
|||||||
import { isNumber } from "lodash-es";
|
import { isNumber } from "lodash-es";
|
||||||
import { FC, FocusEvent } from "react";
|
import { FC, FocusEvent } from "react";
|
||||||
|
|
||||||
|
import { specialCharacterFiltering } from "@/utils"
|
||||||
|
|
||||||
interface SettingsInputProps extends Omit<InputProps, "onChange"> {
|
interface SettingsInputProps extends Omit<InputProps, "onChange"> {
|
||||||
onChange: (value?: string | number) => void;
|
onChange: (value?: string | number) => void;
|
||||||
}
|
}
|
||||||
@@ -38,7 +40,8 @@ const SettingsInput: FC<SettingsInputProps> = (props) => {
|
|||||||
)}
|
)}
|
||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
onChange?.(event.target.value);
|
const value = specialCharacterFiltering(event.target.value)
|
||||||
|
onChange?.(value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export default function SettingsToggle(props: SettingsToggleProps) {
|
|||||||
{...rest}
|
{...rest}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
`group relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent
|
`group relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent
|
||||||
transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 bg-gray-200 dark:bg-gray-700 data-[checked]:bg-blue-600`,
|
transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 bg-gray-200 data-[checked]:bg-blue-600`,
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -3,5 +3,3 @@ export const POPOVER_PANEL_SELECTOR = '[id^="headlessui-popover-panel"]';
|
|||||||
export const HISTORY_PANEL_ID = "headlessui-popover-panel:history-panel";
|
export const HISTORY_PANEL_ID = "headlessui-popover-panel:history-panel";
|
||||||
|
|
||||||
export const CONTEXT_MENU_PANEL_ID = "headlessui-popover-panel:context-menu";
|
export const CONTEXT_MENU_PANEL_ID = "headlessui-popover-panel:context-menu";
|
||||||
|
|
||||||
export const COPY_BUTTON_ID = "copy-button";
|
|
||||||
|
|||||||
@@ -22,20 +22,22 @@ export function useChatActions(
|
|||||||
isMCPActive?: boolean,
|
isMCPActive?: boolean,
|
||||||
changeInput?: (val: string) => void,
|
changeInput?: (val: string) => void,
|
||||||
websocketSessionId?: string,
|
websocketSessionId?: string,
|
||||||
showChatHistory?: boolean,
|
showChatHistory?: boolean
|
||||||
) {
|
) {
|
||||||
const isCurrentLogin = useAuthStore(state => state.isCurrentLogin);
|
const isCurrentLogin = useAuthStore((state) => state.isCurrentLogin);
|
||||||
|
|
||||||
const isTauri = useAppStore((state) => state.isTauri);
|
const isTauri = useAppStore((state) => state.isTauri);
|
||||||
const addError = useAppStore((state) => state.addError);
|
const addError = useAppStore((state) => state.addError);
|
||||||
const currentAssistant = useConnectStore((state) => state.currentAssistant);
|
const {
|
||||||
|
currentAssistant,
|
||||||
|
setCurrentAssistant,
|
||||||
|
assistantList,
|
||||||
|
setVisibleStartPage,
|
||||||
|
currentService,
|
||||||
|
} = useConnectStore();
|
||||||
const { connected } = useChatStore();
|
const { connected } = useChatStore();
|
||||||
const sourceDataIds = useSearchStore((state) => state.sourceDataIds);
|
const sourceDataIds = useSearchStore((state) => state.sourceDataIds);
|
||||||
const MCPIds = useSearchStore((state) => state.MCPIds);
|
const MCPIds = useSearchStore((state) => state.MCPIds);
|
||||||
const setVisibleStartPage = useConnectStore((state) => {
|
|
||||||
return state.setVisibleStartPage;
|
|
||||||
});
|
|
||||||
const currentService = useConnectStore((state) => state.currentService);
|
|
||||||
|
|
||||||
const [keyword, setKeyword] = useState("");
|
const [keyword, setKeyword] = useState("");
|
||||||
|
|
||||||
@@ -52,14 +54,10 @@ export function useChatActions(
|
|||||||
});
|
});
|
||||||
response = response ? JSON.parse(response) : null;
|
response = response ? JSON.parse(response) : null;
|
||||||
} else {
|
} else {
|
||||||
const [_error, res] = await Post(
|
const [_error, res] = await Post(`/chat/${activeChat?._id}/_close`, {});
|
||||||
`/chat/${activeChat?._id}/_close`,
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
response = res;
|
response = res;
|
||||||
}
|
}
|
||||||
console.log("_close", response);
|
console.log("_close", response);
|
||||||
|
|
||||||
},
|
},
|
||||||
[currentService?.id, isTauri]
|
[currentService?.id, isTauri]
|
||||||
);
|
);
|
||||||
@@ -88,6 +86,8 @@ export function useChatActions(
|
|||||||
[currentService?.id, isTauri]
|
[currentService?.id, isTauri]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 1. handleSendMessage callback
|
||||||
|
// 2. onSelectChat no callback
|
||||||
const chatHistory = useCallback(
|
const chatHistory = useCallback(
|
||||||
async (chat: Chat, callback?: (chat: Chat) => void) => {
|
async (chat: Chat, callback?: (chat: Chat) => void) => {
|
||||||
if (!chat?._id) return;
|
if (!chat?._id) return;
|
||||||
@@ -111,6 +111,15 @@ export function useChatActions(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const hits = response?.hits?.hits || [];
|
const hits = response?.hits?.hits || [];
|
||||||
|
// set current assistant
|
||||||
|
const lastAssistantId = hits[hits.length - 1]?._source?.assistant_id;
|
||||||
|
const matchedAssistant = assistantList?.find(
|
||||||
|
(assistant) => assistant._id === lastAssistantId
|
||||||
|
);
|
||||||
|
if (matchedAssistant && !callback) {
|
||||||
|
setCurrentAssistant(matchedAssistant);
|
||||||
|
}
|
||||||
|
//
|
||||||
const updatedChat: Chat = {
|
const updatedChat: Chat = {
|
||||||
...chat,
|
...chat,
|
||||||
messages: hits,
|
messages: hits,
|
||||||
@@ -120,7 +129,7 @@ export function useChatActions(
|
|||||||
callback && callback(updatedChat);
|
callback && callback(updatedChat);
|
||||||
setVisibleStartPage(false);
|
setVisibleStartPage(false);
|
||||||
},
|
},
|
||||||
[currentService?.id, isTauri]
|
[currentService?.id, isTauri, assistantList]
|
||||||
);
|
);
|
||||||
|
|
||||||
const createNewChat = useCallback(
|
const createNewChat = useCallback(
|
||||||
@@ -144,7 +153,7 @@ export function useChatActions(
|
|||||||
mcp: isMCPActive,
|
mcp: isMCPActive,
|
||||||
datasource: sourceDataIds?.join(",") || "",
|
datasource: sourceDataIds?.join(",") || "",
|
||||||
mcp_servers: MCPIds?.join(",") || "",
|
mcp_servers: MCPIds?.join(",") || "",
|
||||||
assistant_id: currentAssistant?._id || '',
|
assistant_id: currentAssistant?._id || "",
|
||||||
};
|
};
|
||||||
let response: any;
|
let response: any;
|
||||||
if (isTauri) {
|
if (isTauri) {
|
||||||
@@ -169,11 +178,12 @@ export function useChatActions(
|
|||||||
response = res;
|
response = res;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("_new", response);
|
console.log("_new", response, queryParams);
|
||||||
const newChat: Chat = response;
|
const newChat: Chat = response;
|
||||||
curIdRef.current = response?.payload?.id;
|
curIdRef.current = response?.payload?.id;
|
||||||
|
|
||||||
newChat._source = {
|
newChat._source = {
|
||||||
|
...response?.payload,
|
||||||
message: value,
|
message: value,
|
||||||
};
|
};
|
||||||
const updatedChat: Chat = {
|
const updatedChat: Chat = {
|
||||||
@@ -220,8 +230,8 @@ export function useChatActions(
|
|||||||
mcp: isMCPActive,
|
mcp: isMCPActive,
|
||||||
datasource: sourceDataIds?.join(",") || "",
|
datasource: sourceDataIds?.join(",") || "",
|
||||||
mcp_servers: MCPIds?.join(",") || "",
|
mcp_servers: MCPIds?.join(",") || "",
|
||||||
assistant_id: currentAssistant?._id || '',
|
assistant_id: currentAssistant?._id || "",
|
||||||
}
|
};
|
||||||
let response: any;
|
let response: any;
|
||||||
if (isTauri) {
|
if (isTauri) {
|
||||||
if (!currentService?.id) return;
|
if (!currentService?.id) return;
|
||||||
@@ -247,7 +257,7 @@ export function useChatActions(
|
|||||||
response = res;
|
response = res;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("_send", response);
|
console.log("_send", response, queryParams);
|
||||||
curIdRef.current = response[0]?._id;
|
curIdRef.current = response[0]?._id;
|
||||||
|
|
||||||
const updatedChat: Chat = {
|
const updatedChat: Chat = {
|
||||||
@@ -284,10 +294,7 @@ export function useChatActions(
|
|||||||
|
|
||||||
await chatHistory(activeChat, (chat) => sendMessage(content, chat, id));
|
await chatHistory(activeChat, (chat) => sendMessage(content, chat, id));
|
||||||
},
|
},
|
||||||
[
|
[chatHistory, sendMessage]
|
||||||
chatHistory,
|
|
||||||
sendMessage,
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const openSessionChat = useCallback(
|
const openSessionChat = useCallback(
|
||||||
@@ -310,7 +317,6 @@ export function useChatActions(
|
|||||||
|
|
||||||
console.log("_open", response);
|
console.log("_open", response);
|
||||||
return response;
|
return response;
|
||||||
|
|
||||||
},
|
},
|
||||||
[currentService?.id, isTauri]
|
[currentService?.id, isTauri]
|
||||||
);
|
);
|
||||||
@@ -318,9 +324,8 @@ export function useChatActions(
|
|||||||
const getChatHistory = useCallback(async () => {
|
const getChatHistory = useCallback(async () => {
|
||||||
let response: any;
|
let response: any;
|
||||||
if (isTauri) {
|
if (isTauri) {
|
||||||
if (!currentService?.id || !isCurrentLogin) {
|
if (!currentService?.id || !isCurrentLogin || !currentService?.enabled) {
|
||||||
setChats([]);
|
return setChats([]);
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
response = await platformAdapter.commands("chat_history", {
|
response = await platformAdapter.commands("chat_history", {
|
||||||
@@ -342,13 +347,16 @@ export function useChatActions(
|
|||||||
console.log("_history", response);
|
console.log("_history", response);
|
||||||
const hits = response?.hits?.hits || [];
|
const hits = response?.hits?.hits || [];
|
||||||
setChats(hits);
|
setChats(hits);
|
||||||
}, [currentService?.id, keyword, isTauri]);
|
}, [currentService?.id, keyword, isTauri, currentService?.enabled, isCurrentLogin]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
showChatHistory && connected && getChatHistory();
|
if (showChatHistory && connected) {
|
||||||
|
getChatHistory()
|
||||||
|
}
|
||||||
}, [showChatHistory, connected, getChatHistory, currentService?.id]);
|
}, [showChatHistory, connected, getChatHistory, currentService?.id]);
|
||||||
|
|
||||||
const createChatWindow = useCallback(async (createWin: any) => {
|
const createChatWindow = useCallback(
|
||||||
|
async (createWin: any) => {
|
||||||
if (isTauri) {
|
if (isTauri) {
|
||||||
createWin &&
|
createWin &&
|
||||||
createWin({
|
createWin({
|
||||||
@@ -367,13 +375,16 @@ export function useChatActions(
|
|||||||
url: "/ui/chat",
|
url: "/ui/chat",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [isTauri]);
|
},
|
||||||
|
[isTauri]
|
||||||
|
);
|
||||||
|
|
||||||
const handleSearch = (keyword: string) => {
|
const handleSearch = (keyword: string) => {
|
||||||
setKeyword(keyword);
|
setKeyword(keyword);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRename = useCallback(async (chatId: string, title: string) => {
|
const handleRename = useCallback(
|
||||||
|
async (chatId: string, title: string) => {
|
||||||
if (!currentService?.id) return;
|
if (!currentService?.id) return;
|
||||||
|
|
||||||
await platformAdapter.commands("update_session_chat", {
|
await platformAdapter.commands("update_session_chat", {
|
||||||
@@ -381,13 +392,22 @@ export function useChatActions(
|
|||||||
sessionId: chatId,
|
sessionId: chatId,
|
||||||
title,
|
title,
|
||||||
});
|
});
|
||||||
}, [currentService?.id]);
|
},
|
||||||
|
[currentService?.id]
|
||||||
|
);
|
||||||
|
|
||||||
const handleDelete = useCallback(async (chatId: string) => {
|
const handleDelete = useCallback(
|
||||||
|
async (chatId: string) => {
|
||||||
if (!currentService?.id) return;
|
if (!currentService?.id) return;
|
||||||
|
|
||||||
await platformAdapter.commands("delete_session_chat", currentService?.id, chatId);
|
await platformAdapter.commands(
|
||||||
}, [currentService?.id]);
|
"delete_session_chat",
|
||||||
|
currentService?.id,
|
||||||
|
chatId
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[currentService?.id]
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
chatClose,
|
chatClose,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect } from 'react';
|
import { useCallback, useEffect } from "react";
|
||||||
|
|
||||||
import { isMetaOrCtrlKey } from "@/utils/keyboardUtils";
|
import { isMetaOrCtrlKey } from "@/utils/keyboardUtils";
|
||||||
import { useSearchStore } from "@/stores/searchStore";
|
import { useSearchStore } from "@/stores/searchStore";
|
||||||
@@ -6,21 +6,18 @@ import { useSearchStore } from "@/stores/searchStore";
|
|||||||
interface KeyboardHandlersProps {
|
interface KeyboardHandlersProps {
|
||||||
isChatMode: boolean;
|
isChatMode: boolean;
|
||||||
handleSubmit: () => void;
|
handleSubmit: () => void;
|
||||||
disabledChange?: () => void;
|
|
||||||
curChatEnd?: boolean;
|
curChatEnd?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useKeyboardHandlers({
|
export function useKeyboardHandlers({
|
||||||
isChatMode,
|
isChatMode,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
disabledChange,
|
|
||||||
curChatEnd,
|
curChatEnd,
|
||||||
}: KeyboardHandlersProps) {
|
}: KeyboardHandlersProps) {
|
||||||
const { setSourceData } = useSearchStore();
|
const { setSourceData } = useSearchStore();
|
||||||
|
|
||||||
const handleKeyDown = useCallback(
|
const handleKeyDown = useCallback(
|
||||||
(e: KeyboardEvent) => {
|
(e: KeyboardEvent) => {
|
||||||
|
|
||||||
// Handle ArrowLeft with meta key
|
// Handle ArrowLeft with meta key
|
||||||
if (e.code === "ArrowLeft" && isMetaOrCtrlKey(e)) {
|
if (e.code === "ArrowLeft" && isMetaOrCtrlKey(e)) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -29,12 +26,12 @@ export function useKeyboardHandlers({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle Enter without meta key requirement
|
// Handle Enter without meta key requirement
|
||||||
if (e.code === "Enter" && isChatMode) {
|
if (e.code === "Enter" && !e.shiftKey && isChatMode) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
curChatEnd ? handleSubmit() : disabledChange?.();
|
curChatEnd && handleSubmit();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[isChatMode, handleSubmit, setSourceData, disabledChange, curChatEnd]
|
[isChatMode, handleSubmit, setSourceData, curChatEnd]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { useCallback, useEffect } from "react";
|
import { useCallback, useEffect } from "react";
|
||||||
|
|
||||||
import { useShortcutsStore } from "@/stores/shortcutsStore";
|
import { useShortcutsStore } from "@/stores/shortcutsStore";
|
||||||
import { isMetaOrCtrlKey, metaOrCtrlKey } from "@/utils/keyboardUtils";
|
|
||||||
import { copyToClipboard, OpenURLWithBrowser } from "@/utils/index";
|
|
||||||
import type { QueryHits, SearchDocument } from "@/types/search";
|
import type { QueryHits, SearchDocument } from "@/types/search";
|
||||||
import platformAdapter from "@/utils/platformAdapter";
|
import platformAdapter from "@/utils/platformAdapter";
|
||||||
|
import { useSearchStore } from "@/stores/searchStore";
|
||||||
|
|
||||||
interface UseKeyboardNavigationProps {
|
interface UseKeyboardNavigationProps {
|
||||||
suggests: QueryHits[];
|
suggests: QueryHits[];
|
||||||
@@ -30,25 +29,64 @@ export function useKeyboardNavigation({
|
|||||||
isChatMode,
|
isChatMode,
|
||||||
}: UseKeyboardNavigationProps) {
|
}: UseKeyboardNavigationProps) {
|
||||||
const openPopover = useShortcutsStore((state) => state.openPopover);
|
const openPopover = useShortcutsStore((state) => state.openPopover);
|
||||||
|
const visibleContextMenu = useSearchStore((state) => {
|
||||||
|
return state.visibleContextMenu;
|
||||||
|
});
|
||||||
|
const modifierKey = useShortcutsStore((state) => {
|
||||||
|
return state.modifierKey;
|
||||||
|
});
|
||||||
|
|
||||||
|
const getModifierKeyPressed = (event: KeyboardEvent) => {
|
||||||
|
const metaKeyPressed = event.metaKey && modifierKey === "meta";
|
||||||
|
const ctrlKeyPressed = event.ctrlKey && modifierKey === "ctrl";
|
||||||
|
const altKeyPressed = event.altKey && modifierKey === "alt";
|
||||||
|
|
||||||
|
return metaKeyPressed || ctrlKeyPressed || altKeyPressed;
|
||||||
|
};
|
||||||
|
|
||||||
const handleKeyDown = useCallback(
|
const handleKeyDown = useCallback(
|
||||||
(e: KeyboardEvent) => {
|
(e: KeyboardEvent) => {
|
||||||
if (!suggests.length || openPopover) return;
|
if (isChatMode || !suggests.length || openPopover || visibleContextMenu) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const modifierKeyPressed = getModifierKeyPressed(e);
|
||||||
|
|
||||||
|
const indexes = suggests.map((item) => item.document.index!);
|
||||||
|
|
||||||
if (e.key === "ArrowUp") {
|
if (e.key === "ArrowUp") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
//console.log("ArrowUp pressed", selectedIndex, suggests.length);
|
// console.log("ArrowUp pressed", selectedIndex, suggests.length);
|
||||||
setSelectedIndex((prev) =>
|
setSelectedIndex((prev) => {
|
||||||
prev === null || prev === 0 ? suggests.length - 1 : prev - 1
|
if (prev == null) {
|
||||||
);
|
return Math.min(...indexes);
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextIndex = prev - 1;
|
||||||
|
|
||||||
|
if (indexes.includes(nextIndex)) {
|
||||||
|
return nextIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.max(...indexes);
|
||||||
|
});
|
||||||
} else if (e.key === "ArrowDown") {
|
} else if (e.key === "ArrowDown") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
//console.log("ArrowDown pressed", selectedIndex, suggests.length);
|
//console.log("ArrowDown pressed", selectedIndex, suggests.length);
|
||||||
setSelectedIndex((prev) =>
|
setSelectedIndex((prev) => {
|
||||||
prev === null || prev === suggests.length - 1 ? 0 : prev + 1
|
if (prev == null) {
|
||||||
);
|
return Math.min(...indexes);
|
||||||
} else if (e.key === metaOrCtrlKey()) {
|
}
|
||||||
e.preventDefault();
|
|
||||||
|
const nextIndex = prev + 1;
|
||||||
|
|
||||||
|
if (indexes.includes(nextIndex)) {
|
||||||
|
return nextIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.min(...indexes);
|
||||||
|
});
|
||||||
|
} else if (modifierKeyPressed) {
|
||||||
if (selectedIndex !== null) {
|
if (selectedIndex !== null) {
|
||||||
const item = globalItemIndexMap[selectedIndex];
|
const item = globalItemIndexMap[selectedIndex];
|
||||||
setSelectedName(item?.source?.name || "");
|
setSelectedName(item?.source?.name || "");
|
||||||
@@ -57,9 +95,9 @@ export function useKeyboardNavigation({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
modifierKeyPressed &&
|
||||||
e.key === "ArrowRight" &&
|
e.key === "ArrowRight" &&
|
||||||
selectedIndex !== null &&
|
selectedIndex !== null
|
||||||
isMetaOrCtrlKey(e)
|
|
||||||
) {
|
) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
@@ -70,29 +108,20 @@ export function useKeyboardNavigation({
|
|||||||
|
|
||||||
if (e.key === "Enter" && !e.shiftKey && selectedIndex !== null) {
|
if (e.key === "Enter" && !e.shiftKey && selectedIndex !== null) {
|
||||||
const item = globalItemIndexMap[selectedIndex];
|
const item = globalItemIndexMap[selectedIndex];
|
||||||
if (item?.on_opened) {
|
|
||||||
return platformAdapter.invokeBackend("open", {
|
return platformAdapter.openSearchItem(item);
|
||||||
onOpened: item.on_opened,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item?.url) {
|
if (e.key >= "0" && e.key <= "9" && showIndex && modifierKeyPressed) {
|
||||||
return OpenURLWithBrowser(item.url);
|
e.preventDefault();
|
||||||
}
|
|
||||||
|
|
||||||
copyToClipboard(item?.payload?.result?.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.key >= "0" && e.key <= "9" && showIndex && isMetaOrCtrlKey(e)) {
|
|
||||||
let index = parseInt(e.key, 10);
|
let index = parseInt(e.key, 10);
|
||||||
|
|
||||||
index = index === 0 ? 9 : index - 1;
|
index = index === 0 ? 9 : index - 1;
|
||||||
|
|
||||||
const item = globalItemIndexMap[index];
|
const item = globalItemIndexMap[index];
|
||||||
|
|
||||||
if (item?.on_opened) {
|
platformAdapter.openSearchItem(item);
|
||||||
platformAdapter.invokeBackend("open", { onOpened: item.on_opened });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[suggests, selectedIndex, showIndex, globalItemIndexMap, openPopover]
|
[suggests, selectedIndex, showIndex, globalItemIndexMap, openPopover]
|
||||||
@@ -100,9 +129,11 @@ export function useKeyboardNavigation({
|
|||||||
|
|
||||||
const handleKeyUp = useCallback(
|
const handleKeyUp = useCallback(
|
||||||
(e: KeyboardEvent) => {
|
(e: KeyboardEvent) => {
|
||||||
if (!suggests.length) return;
|
if (isChatMode || !suggests.length) return;
|
||||||
|
|
||||||
if (!isMetaOrCtrlKey(e)) {
|
const modifierKeyPressed = getModifierKeyPressed(e);
|
||||||
|
|
||||||
|
if (modifierKeyPressed) {
|
||||||
setShowIndex(false);
|
setShowIndex(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -110,8 +141,6 @@ export function useKeyboardNavigation({
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isChatMode) return;
|
|
||||||
|
|
||||||
window.addEventListener("keydown", handleKeyDown);
|
window.addEventListener("keydown", handleKeyDown);
|
||||||
window.addEventListener("keyup", handleKeyUp);
|
window.addEventListener("keyup", handleKeyUp);
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect } from "react";
|
||||||
|
|
||||||
const useScript = (src: string, onError?: () => void) => {
|
const useScript = (src: string, onError?: () => void) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (document.querySelector(`script[src="${src}"]`)) {
|
if (document.querySelector(`script[src="${src}"]`)) {
|
||||||
return; // Prevent duplicate script loading
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const script = document.createElement("script");
|
const script = document.createElement("script");
|
||||||
@@ -12,7 +12,8 @@ const useScript = (src: string, onError?: () => void) => {
|
|||||||
|
|
||||||
script.onerror = () => {
|
script.onerror = () => {
|
||||||
console.error(`Failed to load script: ${src}`);
|
console.error(`Failed to load script: ${src}`);
|
||||||
if (onError) onError();
|
|
||||||
|
onError?.();
|
||||||
};
|
};
|
||||||
|
|
||||||
document.body.appendChild(script);
|
document.body.appendChild(script);
|
||||||
@@ -26,23 +27,8 @@ const useScript = (src: string, onError?: () => void) => {
|
|||||||
export default useScript;
|
export default useScript;
|
||||||
|
|
||||||
export const useIconfontScript = () => {
|
export const useIconfontScript = () => {
|
||||||
const appStore = JSON.parse(localStorage.getItem("app-store") || "{}");
|
// Coco Server Icons
|
||||||
|
useScript("https://at.alicdn.com/t/c/font_4878526_cykw3et0ezd.js");
|
||||||
const [_useLocalFallback, setUseLocalFallback] = useState(false);
|
// Coco App Icons
|
||||||
|
useScript("https://at.alicdn.com/t/c/font_4934333_j1t3b1xyxkk.js");
|
||||||
let baseURL = appStore.state?.endpoint_http;
|
|
||||||
if (!baseURL || baseURL === "undefined") {
|
|
||||||
baseURL = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
useScript("/assets/fonts/icons/iconfont.js");
|
|
||||||
|
|
||||||
useScript(`${baseURL}/assets/fonts/icons/iconfont.js`, () => {
|
|
||||||
console.log(
|
|
||||||
"Remote iconfont loading failed, falling back to local resource"
|
|
||||||
);
|
|
||||||
setUseLocalFallback(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
useScript("/assets/fonts/icons/extension.js");
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -47,6 +47,9 @@ export function useSearch() {
|
|||||||
const aiOverviewDelay = useExtensionsStore((state) => {
|
const aiOverviewDelay = useExtensionsStore((state) => {
|
||||||
return state.aiOverviewDelay;
|
return state.aiOverviewDelay;
|
||||||
});
|
});
|
||||||
|
const aiOverviewMinQuantity = useExtensionsStore((state) => {
|
||||||
|
return state.aiOverviewMinQuantity;
|
||||||
|
});
|
||||||
|
|
||||||
const { querySourceTimeout } = useConnectStore();
|
const { querySourceTimeout } = useConnectStore();
|
||||||
|
|
||||||
@@ -94,9 +97,8 @@ export function useSearch() {
|
|||||||
|
|
||||||
const filteredData = data.filter((item: any) => {
|
const filteredData = data.filter((item: any) => {
|
||||||
return (
|
return (
|
||||||
item?.document?.type !== "AI Assistant" &&
|
item?.source?.type === "coco-servers" &&
|
||||||
item?.document?.category !== "Calculator" &&
|
item?.document?.type !== "AI Assistant"
|
||||||
item?.document?.category !== "Application"
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -106,7 +108,7 @@ export function useSearch() {
|
|||||||
enabledAiOverview &&
|
enabledAiOverview &&
|
||||||
aiOverviewServer &&
|
aiOverviewServer &&
|
||||||
aiOverviewAssistant &&
|
aiOverviewAssistant &&
|
||||||
filteredData.length > 5 &&
|
filteredData.length >= aiOverviewMinQuantity &&
|
||||||
!disabledExtensions.includes("AIOverview")
|
!disabledExtensions.includes("AIOverview")
|
||||||
) {
|
) {
|
||||||
timerRef.current = setTimeout(() => {
|
timerRef.current = setTimeout(() => {
|
||||||
@@ -118,7 +120,7 @@ export function useSearch() {
|
|||||||
type: id,
|
type: id,
|
||||||
},
|
},
|
||||||
document: {
|
document: {
|
||||||
index: 1000000,
|
index: -1,
|
||||||
id,
|
id,
|
||||||
category: id,
|
category: id,
|
||||||
payload: {
|
payload: {
|
||||||
@@ -210,6 +212,7 @@ export function useSearch() {
|
|||||||
disabledExtensions,
|
disabledExtensions,
|
||||||
aiOverviewCharLen,
|
aiOverviewCharLen,
|
||||||
aiOverviewDelay,
|
aiOverviewDelay,
|
||||||
|
aiOverviewMinQuantity,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export const useStreamChat = (options: Options) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If the chunk data does not contain a message_chunk, we ignore it
|
// If the chunk data does not contain a message_chunk, we ignore it
|
||||||
if (chunkData.message_chunk) {
|
if (chunkData.message_chunk.trim()) {
|
||||||
setVisible(true);
|
setVisible(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useAppearanceStore } from "@/stores/appearanceStore";
|
import { useAppearanceStore } from "@/stores/appearanceStore";
|
||||||
|
import { useAppStore } from "@/stores/appStore";
|
||||||
import { useConnectStore } from "@/stores/connectStore";
|
import { useConnectStore } from "@/stores/connectStore";
|
||||||
import { useExtensionsStore } from "@/stores/extensionsStore";
|
import { useExtensionsStore } from "@/stores/extensionsStore";
|
||||||
import { useShortcutsStore } from "@/stores/shortcutsStore";
|
import { useShortcutsStore } from "@/stores/shortcutsStore";
|
||||||
@@ -108,6 +109,14 @@ export const useSyncStore = () => {
|
|||||||
const setAiOverviewDelay = useExtensionsStore((state) => {
|
const setAiOverviewDelay = useExtensionsStore((state) => {
|
||||||
return state.setAiOverviewDelay;
|
return state.setAiOverviewDelay;
|
||||||
});
|
});
|
||||||
|
const setAiOverview = useShortcutsStore((state) => state.setAiOverview);
|
||||||
|
const setAiOverviewMinQuantity = useExtensionsStore((state) => {
|
||||||
|
return state.setAiOverviewMinQuantity;
|
||||||
|
});
|
||||||
|
const setCurrentService = useConnectStore((state) => state.setCurrentService);
|
||||||
|
const setShowTooltip = useAppStore((state) => state.setShowTooltip);
|
||||||
|
const setEndpoint = useAppStore((state) => state.setEndpoint);
|
||||||
|
const setLanguage = useAppStore((state) => state.setLanguage);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!resetFixedWindow) {
|
if (!resetFixedWindow) {
|
||||||
@@ -137,7 +146,9 @@ export const useSyncStore = () => {
|
|||||||
fixedWindow,
|
fixedWindow,
|
||||||
serviceList,
|
serviceList,
|
||||||
external,
|
external,
|
||||||
|
aiOverview,
|
||||||
} = payload;
|
} = payload;
|
||||||
|
|
||||||
setModifierKey(modifierKey);
|
setModifierKey(modifierKey);
|
||||||
setModeSwitch(modeSwitch);
|
setModeSwitch(modeSwitch);
|
||||||
setReturnToInput(returnToInput);
|
setReturnToInput(returnToInput);
|
||||||
@@ -154,6 +165,7 @@ export const useSyncStore = () => {
|
|||||||
setFixedWindow(fixedWindow);
|
setFixedWindow(fixedWindow);
|
||||||
setServiceList(serviceList);
|
setServiceList(serviceList);
|
||||||
setExternal(external);
|
setExternal(external);
|
||||||
|
setAiOverview(aiOverview);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
platformAdapter.listenEvent("change-startup-store", ({ payload }) => {
|
platformAdapter.listenEvent("change-startup-store", ({ payload }) => {
|
||||||
@@ -168,8 +180,12 @@ export const useSyncStore = () => {
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
platformAdapter.listenEvent("change-connect-store", ({ payload }) => {
|
platformAdapter.listenEvent("change-connect-store", ({ payload }) => {
|
||||||
const { connectionTimeout, querySourceTimeout, allowSelfSignature } =
|
const {
|
||||||
payload;
|
connectionTimeout,
|
||||||
|
querySourceTimeout,
|
||||||
|
allowSelfSignature,
|
||||||
|
currentService,
|
||||||
|
} = payload;
|
||||||
if (isNumber(connectionTimeout)) {
|
if (isNumber(connectionTimeout)) {
|
||||||
setConnectionTimeout(connectionTimeout);
|
setConnectionTimeout(connectionTimeout);
|
||||||
}
|
}
|
||||||
@@ -177,6 +193,7 @@ export const useSyncStore = () => {
|
|||||||
setQueryTimeout(querySourceTimeout);
|
setQueryTimeout(querySourceTimeout);
|
||||||
}
|
}
|
||||||
setAllowSelfSignature(allowSelfSignature);
|
setAllowSelfSignature(allowSelfSignature);
|
||||||
|
setCurrentService(currentService);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
platformAdapter.listenEvent("change-appearance-store", ({ payload }) => {
|
platformAdapter.listenEvent("change-appearance-store", ({ payload }) => {
|
||||||
@@ -197,6 +214,7 @@ export const useSyncStore = () => {
|
|||||||
disabledExtensions,
|
disabledExtensions,
|
||||||
aiOverviewCharLen,
|
aiOverviewCharLen,
|
||||||
aiOverviewDelay,
|
aiOverviewDelay,
|
||||||
|
aiOverviewMinQuantity,
|
||||||
} = payload;
|
} = payload;
|
||||||
|
|
||||||
setQuickAiAccessServer(quickAiAccessServer);
|
setQuickAiAccessServer(quickAiAccessServer);
|
||||||
@@ -206,6 +224,15 @@ export const useSyncStore = () => {
|
|||||||
setDisabledExtensions(disabledExtensions);
|
setDisabledExtensions(disabledExtensions);
|
||||||
setAiOverviewCharLen(aiOverviewCharLen);
|
setAiOverviewCharLen(aiOverviewCharLen);
|
||||||
setAiOverviewDelay(aiOverviewDelay);
|
setAiOverviewDelay(aiOverviewDelay);
|
||||||
|
setAiOverviewMinQuantity(aiOverviewMinQuantity);
|
||||||
|
}),
|
||||||
|
|
||||||
|
platformAdapter.listenEvent("change-app-store", ({ payload }) => {
|
||||||
|
const { showTooltip, endpoint, language } = payload;
|
||||||
|
|
||||||
|
setShowTooltip(showTooltip);
|
||||||
|
setEndpoint(endpoint);
|
||||||
|
setLanguage(language);
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -150,6 +150,10 @@
|
|||||||
"hits": {
|
"hits": {
|
||||||
"isSystem": "This shortcut is reserved by the system, please choose another key.",
|
"isSystem": "This shortcut is reserved by the system, please choose another key.",
|
||||||
"isUse": "This shortcut is already in use, please choose another key."
|
"isUse": "This shortcut is already in use, please choose another key."
|
||||||
|
},
|
||||||
|
"aiOverview": {
|
||||||
|
"title": "AI Overview",
|
||||||
|
"description": "Shortcut button to enable AI Overview in chat mode."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"connect": {
|
"connect": {
|
||||||
@@ -227,7 +231,36 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"calculator": {
|
"calculator": {
|
||||||
"title": "Calculator"
|
"title": "Calculator",
|
||||||
|
"description": "A calculator you can quickly invoke in the search bar, supporting basic math operations."
|
||||||
|
},
|
||||||
|
"shardAi": {
|
||||||
|
"details": {
|
||||||
|
"linkedAssistant": {
|
||||||
|
"title": "Linked Assistant",
|
||||||
|
"label": {
|
||||||
|
"cocoServer": "Coco Server",
|
||||||
|
"aiAssistant": "AI Assistant"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"quickAiAccess": {
|
||||||
|
"description": "Quick AI access allows you to start a conversation immediately from the search box using the Tab key."
|
||||||
|
},
|
||||||
|
"aiOverview": {
|
||||||
|
"description": "AI Overview generates concise summaries based on your search results, helping you quickly grasp key information without reading every document.",
|
||||||
|
"details": {
|
||||||
|
"aiOverviewTrigger": {
|
||||||
|
"title": "AI Overview Trigger",
|
||||||
|
"description": "AI Overview will be triggered only when all of the following conditions are met.(search results are sourced from Coco Server's result statistics)",
|
||||||
|
"label": {
|
||||||
|
"minCharLen": "Minimum Input Length(characters)",
|
||||||
|
"minDelay": "Delay After Typing Stops(seconds)",
|
||||||
|
"minQuantity": "Minimum Number of Search Results"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -310,7 +343,7 @@
|
|||||||
"search": "Search Operation"
|
"search": "Search Operation"
|
||||||
},
|
},
|
||||||
"askCocoAi": {
|
"askCocoAi": {
|
||||||
"title": "Ask {{0}}",
|
"title": "{{0}} {{1}}",
|
||||||
"placeholder": "Ask More",
|
"placeholder": "Ask More",
|
||||||
"continueInChat": "Continue in chat",
|
"continueInChat": "Continue in chat",
|
||||||
"copy": "Copy"
|
"copy": "Copy"
|
||||||
@@ -330,6 +363,8 @@
|
|||||||
"timedout": "Request timed out. Please try again later.",
|
"timedout": "Request timed out. Please try again later.",
|
||||||
"noServers": "No servers found",
|
"noServers": "No servers found",
|
||||||
"addServer": "Add Server",
|
"addServer": "Add Server",
|
||||||
|
"servers": "Servers",
|
||||||
|
"aiAssistant": "AI Assistant",
|
||||||
"error": "Request error. Please try again later.",
|
"error": "Request error. Please try again later.",
|
||||||
"logo_alt": "Login Logo",
|
"logo_alt": "Login Logo",
|
||||||
"welcome": "Welcome to Coco AI",
|
"welcome": "Welcome to Coco AI",
|
||||||
|
|||||||
@@ -150,6 +150,10 @@
|
|||||||
"hits": {
|
"hits": {
|
||||||
"isSystem": "该快捷键已被系统保留,请选择其他按键。",
|
"isSystem": "该快捷键已被系统保留,请选择其他按键。",
|
||||||
"isUse": "该快捷键已被占用,请选择其他按键。"
|
"isUse": "该快捷键已被占用,请选择其他按键。"
|
||||||
|
},
|
||||||
|
"aiOverview": {
|
||||||
|
"title": "AI 总结",
|
||||||
|
"description": "在搜索模式下启用 AI 总结的快捷按键。"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"connect": {
|
"connect": {
|
||||||
@@ -227,7 +231,36 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"calculator": {
|
"calculator": {
|
||||||
"title": "计算器"
|
"title": "计算器",
|
||||||
|
"description": "在搜索框中快速调用的计算工具,支持基本数学运算。"
|
||||||
|
},
|
||||||
|
"shardAi": {
|
||||||
|
"details": {
|
||||||
|
"linkedAssistant": {
|
||||||
|
"title": "关联助手",
|
||||||
|
"label": {
|
||||||
|
"cocoServer": "Coco Server",
|
||||||
|
"aiAssistant": "AI 助手"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"quickAiAccess": {
|
||||||
|
"description": "通过 Tab 键,你可以立即从搜索框中启动与 AI 的对话。"
|
||||||
|
},
|
||||||
|
"aiOverview": {
|
||||||
|
"description": "AI Overview 根据你的搜索结果生成简洁的摘要,帮助你快速掌握关键信息,无需逐一阅读每份文档。",
|
||||||
|
"details": {
|
||||||
|
"aiOverviewTrigger": {
|
||||||
|
"title": "AI Overview 触发条件",
|
||||||
|
"description": "仅当满足以下所有条件时,AI Overview 才会被触发。(搜索结果来源为 Coco Server 的结果统计)",
|
||||||
|
"label": {
|
||||||
|
"minCharLen": "最小输入长度(字符)",
|
||||||
|
"minDelay": "输入停止延迟(秒)",
|
||||||
|
"minQuantity": "最小搜索结果数"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -310,7 +343,7 @@
|
|||||||
"search": "搜索操作"
|
"search": "搜索操作"
|
||||||
},
|
},
|
||||||
"askCocoAi": {
|
"askCocoAi": {
|
||||||
"title": "问{{0}}",
|
"title": "{{0}}{{1}}",
|
||||||
"placeholder": "问更多",
|
"placeholder": "问更多",
|
||||||
"continueInChat": "继续聊天",
|
"continueInChat": "继续聊天",
|
||||||
"copy": "复制"
|
"copy": "复制"
|
||||||
@@ -330,6 +363,8 @@
|
|||||||
"timedout": "请求超时,请稍后再试。",
|
"timedout": "请求超时,请稍后再试。",
|
||||||
"noServers": "暂无可用服务器",
|
"noServers": "暂无可用服务器",
|
||||||
"addServer": "添加服务器",
|
"addServer": "添加服务器",
|
||||||
|
"servers": "服务器",
|
||||||
|
"aiAssistant": "AI 助手",
|
||||||
"error": "请求错误,请稍后再试。",
|
"error": "请求错误,请稍后再试。",
|
||||||
"logo_alt": "登录图标",
|
"logo_alt": "登录图标",
|
||||||
"welcome": "欢迎使用 Coco AI",
|
"welcome": "欢迎使用 Coco AI",
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import {
|
|||||||
import HistoryList from "@/components/Common/HistoryList";
|
import HistoryList from "@/components/Common/HistoryList";
|
||||||
import { useSyncStore } from "@/hooks/useSyncStore";
|
import { useSyncStore } from "@/hooks/useSyncStore";
|
||||||
import { useAppStore } from "@/stores/appStore";
|
import { useAppStore } from "@/stores/appStore";
|
||||||
|
import { useChatStore } from "@/stores/chatStore";
|
||||||
|
|
||||||
interface ChatProps {}
|
interface ChatProps {}
|
||||||
|
|
||||||
@@ -38,10 +39,12 @@ export default function Chat({}: ChatProps) {
|
|||||||
setIsTauri(true);
|
setIsTauri(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const currentService = useConnectStore((state) => state.currentService);
|
const {
|
||||||
const setVisibleStartPage = useConnectStore((state) => {
|
setCurrentAssistant,
|
||||||
return state.setVisibleStartPage;
|
assistantList,
|
||||||
});
|
setVisibleStartPage,
|
||||||
|
currentService,
|
||||||
|
} = useConnectStore();
|
||||||
|
|
||||||
const chatAIRef = useRef<ChatAIRef>(null);
|
const chatAIRef = useRef<ChatAIRef>(null);
|
||||||
|
|
||||||
@@ -57,16 +60,22 @@ export default function Chat({}: ChatProps) {
|
|||||||
const [isMCPActive, setIsMCPActive] = useState(false);
|
const [isMCPActive, setIsMCPActive] = useState(false);
|
||||||
const [keyword, setKeyword] = useState("");
|
const [keyword, setKeyword] = useState("");
|
||||||
|
|
||||||
|
const { connected } = useChatStore();
|
||||||
|
|
||||||
const isChatPage = true;
|
const isChatPage = true;
|
||||||
|
|
||||||
useSyncStore();
|
useSyncStore();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getChatHistory();
|
getChatHistory();
|
||||||
}, [keyword]);
|
}, [keyword, currentService?.id]);
|
||||||
|
|
||||||
const getChatHistory = async () => {
|
const getChatHistory = async () => {
|
||||||
try {
|
try {
|
||||||
|
if (!currentService.enabled) {
|
||||||
|
return setChats([]);
|
||||||
|
}
|
||||||
|
|
||||||
let response: any = await chat_history({
|
let response: any = await chat_history({
|
||||||
serverId: currentService?.id,
|
serverId: currentService?.id,
|
||||||
from: 0,
|
from: 0,
|
||||||
@@ -77,16 +86,19 @@ export default function Chat({}: ChatProps) {
|
|||||||
console.log("_history", response);
|
console.log("_history", response);
|
||||||
const hits = response?.hits?.hits || [];
|
const hits = response?.hits?.hits || [];
|
||||||
setChats(hits);
|
setChats(hits);
|
||||||
if (hits[0]) {
|
|
||||||
onSelectChat(hits[0]);
|
|
||||||
} else {
|
|
||||||
chatAIRef.current?.init("");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("chat_history:", error);
|
console.error("chat_history:", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!currentService?.enabled) {
|
||||||
|
setActiveChat(void 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
getChatHistory();
|
||||||
|
}, [currentService?.enabled, connected]);
|
||||||
|
|
||||||
const deleteChat = (chatId: string) => {
|
const deleteChat = (chatId: string) => {
|
||||||
handleDelete(chatId);
|
handleDelete(chatId);
|
||||||
|
|
||||||
@@ -117,6 +129,15 @@ export default function Chat({}: ChatProps) {
|
|||||||
response = response ? JSON.parse(response) : null;
|
response = response ? JSON.parse(response) : null;
|
||||||
console.log("id_history", response);
|
console.log("id_history", response);
|
||||||
const hits = response?.hits?.hits || [];
|
const hits = response?.hits?.hits || [];
|
||||||
|
// set current assistant
|
||||||
|
const lastAssistantId = hits[hits.length - 1]?._source?.assistant_id;
|
||||||
|
const matchedAssistant = assistantList?.find(
|
||||||
|
(assistant) => assistant._id === lastAssistantId
|
||||||
|
);
|
||||||
|
if (matchedAssistant) {
|
||||||
|
setCurrentAssistant(matchedAssistant);
|
||||||
|
}
|
||||||
|
//
|
||||||
const updatedChat: typeChat = {
|
const updatedChat: typeChat = {
|
||||||
...chat,
|
...chat,
|
||||||
messages: hits,
|
messages: hits,
|
||||||
@@ -236,10 +257,7 @@ export default function Chat({}: ChatProps) {
|
|||||||
return updatedChats;
|
return updatedChats;
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return updatedChats;
|
||||||
modifiedChat,
|
|
||||||
...updatedChats.filter((item) => item._id !== chatId),
|
|
||||||
];
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (activeChat?._id === chatId) {
|
if (activeChat?._id === chatId) {
|
||||||
@@ -274,7 +292,7 @@ export default function Chat({}: ChatProps) {
|
|||||||
} transition-transform duration-300 ease-in-out md:translate-x-0 md:static md:block bg-gray-100 dark:bg-gray-800`}
|
} transition-transform duration-300 ease-in-out md:translate-x-0 md:static md:block bg-gray-100 dark:bg-gray-800`}
|
||||||
>
|
>
|
||||||
<HistoryList
|
<HistoryList
|
||||||
list={chats}
|
chats={chats}
|
||||||
active={activeChat}
|
active={activeChat}
|
||||||
onSearch={handleSearch}
|
onSearch={handleSearch}
|
||||||
onRefresh={getChatHistory}
|
onRefresh={getChatHistory}
|
||||||
|
|||||||
@@ -15,10 +15,6 @@ function MainApp() {
|
|||||||
setIsTauri(true);
|
setIsTauri(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const hideCoco = useCallback(() => {
|
|
||||||
return platformAdapter.hideWindow();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useSyncStore();
|
useSyncStore();
|
||||||
|
|
||||||
const snapshotUpdate = useAppearanceStore((state) => state.snapshotUpdate);
|
const snapshotUpdate = useAppearanceStore((state) => state.snapshotUpdate);
|
||||||
@@ -41,11 +37,7 @@ function MainApp() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SearchChat
|
<SearchChat isTauri={true} hasModules={["search", "chat"]} />
|
||||||
isTauri={true}
|
|
||||||
hideCoco={hideCoco}
|
|
||||||
hasModules={["search", "chat"]}
|
|
||||||
/>
|
|
||||||
<UpdateApp checkUpdate={checkUpdate} relaunchApp={relaunchApp} />
|
<UpdateApp checkUpdate={checkUpdate} relaunchApp={relaunchApp} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ import Footer from "@/components/Common/UI/SettingsFooter";
|
|||||||
import { useTray } from "@/hooks/useTray";
|
import { useTray } from "@/hooks/useTray";
|
||||||
import Advanced from "@/components/Settings/Advanced";
|
import Advanced from "@/components/Settings/Advanced";
|
||||||
import Extensions from "@/components/Settings/Extensions";
|
import Extensions from "@/components/Settings/Extensions";
|
||||||
|
import { useConnectStore } from "@/stores/connectStore";
|
||||||
|
import platformAdapter from "@/utils/platformAdapter";
|
||||||
|
import { useAppStore } from "@/stores/appStore";
|
||||||
|
|
||||||
const tabIndexMap: { [key: string]: number } = {
|
const tabIndexMap: { [key: string]: number } = {
|
||||||
general: 0,
|
general: 0,
|
||||||
@@ -45,8 +48,18 @@ function SettingsPage() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const unsubscribeConnect = useConnectStore.subscribe((state) => {
|
||||||
|
platformAdapter.emitEvent("change-connect-store", state);
|
||||||
|
});
|
||||||
|
|
||||||
|
const unsubscribeAppStore = useAppStore.subscribe((state) => {
|
||||||
|
platformAdapter.emitEvent("change-app-store", state);
|
||||||
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
unlisten.then((fn) => fn());
|
unlisten.then((fn) => fn());
|
||||||
|
unsubscribeConnect();
|
||||||
|
unsubscribeAppStore();
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -1,118 +1,143 @@
|
|||||||
# SearchChat Web Component API
|
# SearchChat Web Component API
|
||||||
|
|
||||||
## Props
|
A customizable search and chat interface component for web applications.
|
||||||
|
|
||||||
### `serverUrl`
|
## Installation
|
||||||
- **Type**: `string`
|
|
||||||
- **Optional**: Yes
|
|
||||||
- **Default**: `""`
|
|
||||||
- **Description**: Set the server address
|
|
||||||
|
|
||||||
### `headers`
|
```bash
|
||||||
- **Type**: `Record<string, unknown>`
|
npm install @infini/coco-app
|
||||||
- **Optional**: Yes
|
```
|
||||||
- **Default**: `{}`
|
|
||||||
- **Description**: Request header configuration for API calls, such as X-API-TOKEN and APP-INTEGRATION-ID
|
|
||||||
|
|
||||||
### `width`
|
## Basic Usage
|
||||||
- **Type**: `number`
|
|
||||||
- **Optional**: Yes
|
|
||||||
- **Default**: `680`
|
|
||||||
- **Description**: Width of the component container in pixels
|
|
||||||
|
|
||||||
### `height`
|
```jsx
|
||||||
- **Type**: `number`
|
import SearchChat from '@infini/coco-app';
|
||||||
- **Optional**: Yes
|
|
||||||
- **Default**: `590`
|
|
||||||
- **Description**: Height of the component container in pixels. On mobile devices, it automatically adapts to the viewport height
|
|
||||||
|
|
||||||
### `hasModules`
|
|
||||||
- **Type**: `string[]`
|
|
||||||
- **Optional**: Yes
|
|
||||||
- **Default**: `['search', 'chat']`
|
|
||||||
- **Description**: List of enabled feature modules, currently supports 'search' and 'chat' modules
|
|
||||||
|
|
||||||
### `defaultModule`
|
|
||||||
- **Type**: `'search' | 'chat'`
|
|
||||||
- **Optional**: Yes
|
|
||||||
- **Default**: `'search'`
|
|
||||||
- **Description**: The default module to display
|
|
||||||
|
|
||||||
### `assistantIDs`
|
|
||||||
- **Type**: `string[]`
|
|
||||||
- **Optional**: Yes
|
|
||||||
- **Default**: `[]`
|
|
||||||
- **Description**: List of available assistant IDs
|
|
||||||
|
|
||||||
### `hideCoco`
|
|
||||||
- **Type**: `() => void`
|
|
||||||
- **Optional**: Yes
|
|
||||||
- **Default**: `() => {}`
|
|
||||||
- **Description**: Callback function to hide the search window
|
|
||||||
|
|
||||||
### `theme`
|
|
||||||
- **Type**: `"auto" | "light" | "dark"`
|
|
||||||
- **Optional**: Yes
|
|
||||||
- **Default**: `"dark"`
|
|
||||||
- **Description**: Theme setting, supports auto (follows system), light, and dark modes
|
|
||||||
|
|
||||||
### `searchPlaceholder`
|
|
||||||
- **Type**: `string`
|
|
||||||
- **Optional**: Yes
|
|
||||||
- **Default**: `""`
|
|
||||||
- **Description**: Placeholder text for the search input
|
|
||||||
|
|
||||||
### `chatPlaceholder`
|
|
||||||
- **Type**: `string`
|
|
||||||
- **Optional**: Yes
|
|
||||||
- **Default**: `""`
|
|
||||||
- **Description**: Placeholder text for the chat input
|
|
||||||
|
|
||||||
### `showChatHistory`
|
|
||||||
- **Type**: `boolean`
|
|
||||||
- **Optional**: Yes
|
|
||||||
- **Default**: `false`
|
|
||||||
- **Description**: Whether to display chat history
|
|
||||||
|
|
||||||
### `setIsPinned`
|
|
||||||
- **Type**: `(value: boolean) => void`
|
|
||||||
- **Optional**: Yes
|
|
||||||
- **Default**: `undefined`
|
|
||||||
- **Description**: Callback function to set window pin status
|
|
||||||
|
|
||||||
### `onCancel`
|
|
||||||
- **Type**: `() => void`
|
|
||||||
- **Optional**: Yes
|
|
||||||
- **Default**: `undefined`
|
|
||||||
- **Description**: Callback function for clicking the close button on mobile devices
|
|
||||||
|
|
||||||
## Usage Example
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
import SearchChat from 'search-chat';
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<SearchChat
|
<SearchChat
|
||||||
serverUrl=""
|
serverUrl="https://your-server.com"
|
||||||
headers={{
|
headers={{
|
||||||
"X-API-TOKEN": "your-api-token",
|
"X-API-TOKEN": "your-token",
|
||||||
"APP-INTEGRATION-ID": "your-integration-id"
|
"APP-INTEGRATION-ID": "your-app-id"
|
||||||
}}
|
}}
|
||||||
width={680}
|
|
||||||
height={590}
|
|
||||||
hasModules={['search', 'chat']}
|
|
||||||
defaultModule="search"
|
|
||||||
assistantIDs={[]}
|
|
||||||
hideCoco={() => console.log('hide')}
|
|
||||||
theme="dark"
|
|
||||||
searchPlaceholder=""
|
|
||||||
chatPlaceholder=""
|
|
||||||
showChatHistory={false}
|
|
||||||
setIsPinned={(isPinned) => console.log('isPinned:', isPinned)}
|
|
||||||
onCancel={() => console.log('cancel')}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Props
|
||||||
|
|
||||||
|
### `width`
|
||||||
|
- **Type**: `number`
|
||||||
|
- **Default**: `680`
|
||||||
|
- **Description**: Maximum width of the component in pixels
|
||||||
|
|
||||||
|
### `height`
|
||||||
|
- **Type**: `number`
|
||||||
|
- **Default**: `590`
|
||||||
|
- **Description**: Height of the component in pixels
|
||||||
|
|
||||||
|
### `headers`
|
||||||
|
- **Type**: `Record<string, unknown>`
|
||||||
|
- **Default**:
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
"X-API-TOKEN": "default-token",
|
||||||
|
"APP-INTEGRATION-ID": "default-id"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **Description**: HTTP headers for API requests
|
||||||
|
|
||||||
|
### `serverUrl`
|
||||||
|
- **Type**: `string`
|
||||||
|
- **Default**: `""`
|
||||||
|
- **Description**: Base URL for the server API
|
||||||
|
|
||||||
|
### `hasModules`
|
||||||
|
- **Type**: `string[]`
|
||||||
|
- **Default**: `["search", "chat"]`
|
||||||
|
- **Description**: Available modules to show
|
||||||
|
|
||||||
|
### `defaultModule`
|
||||||
|
- **Type**: `"search" | "chat"`
|
||||||
|
- **Default**: `"search"`
|
||||||
|
- **Description**: Initial active module
|
||||||
|
|
||||||
|
### `assistantIDs`
|
||||||
|
- **Type**: `string[]`
|
||||||
|
- **Default**: `[]`
|
||||||
|
- **Description**: List of assistant IDs to use
|
||||||
|
|
||||||
|
### `theme`
|
||||||
|
- **Type**: `"auto" | "light" | "dark"`
|
||||||
|
- **Default**: `"dark"`
|
||||||
|
- **Description**: UI theme setting
|
||||||
|
|
||||||
|
### `searchPlaceholder`
|
||||||
|
- **Type**: `string`
|
||||||
|
- **Default**: `""`
|
||||||
|
- **Description**: Placeholder text for search input
|
||||||
|
|
||||||
|
### `chatPlaceholder`
|
||||||
|
- **Type**: `string`
|
||||||
|
- **Default**: `""`
|
||||||
|
- **Description**: Placeholder text for chat input
|
||||||
|
|
||||||
|
### `showChatHistory`
|
||||||
|
- **Type**: `boolean`
|
||||||
|
- **Default**: `false`
|
||||||
|
- **Description**: Whether to display chat history panel
|
||||||
|
|
||||||
|
### `startPage`
|
||||||
|
- **Type**: `StartPage`
|
||||||
|
- **Optional**: Yes
|
||||||
|
- **Description**: Initial page configuration
|
||||||
|
|
||||||
|
### `setIsPinned`
|
||||||
|
- **Type**: `(value: boolean) => void`
|
||||||
|
- **Optional**: Yes
|
||||||
|
- **Description**: Callback when pin status changes
|
||||||
|
|
||||||
|
### `onCancel`
|
||||||
|
- **Type**: `() => void`
|
||||||
|
- **Optional**: Yes
|
||||||
|
- **Description**: Callback when close button is clicked (mobile only)
|
||||||
|
|
||||||
|
### `isOpen`
|
||||||
|
- **Type**: `boolean`
|
||||||
|
- **Optional**: Yes
|
||||||
|
- **Description**: Control component visibility
|
||||||
|
|
||||||
|
## Events
|
||||||
|
|
||||||
|
The component emits the following events:
|
||||||
|
|
||||||
|
- `onModeChange`: Triggered when switching between search and chat modes
|
||||||
|
- `onCancel`: Triggered when the close button is clicked (mobile only)
|
||||||
|
|
||||||
|
## Mobile Support
|
||||||
|
|
||||||
|
The component is responsive and includes mobile-specific features:
|
||||||
|
- Automatic height adjustment
|
||||||
|
- Close button in top-right corner
|
||||||
|
- Touch-friendly interface
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
<SearchChat
|
||||||
|
width={800}
|
||||||
|
height={600}
|
||||||
|
serverUrl="https://api.example.com"
|
||||||
|
headers={{
|
||||||
|
"X-API-TOKEN": "your-token",
|
||||||
|
"APP-INTEGRATION-ID": "your-app-id"
|
||||||
|
}}
|
||||||
|
theme="dark"
|
||||||
|
showChatHistory={true}
|
||||||
|
hasModules={["search", "chat"]}
|
||||||
|
defaultModule="chat"
|
||||||
|
setIsPinned={(isPinned) => console.log('Pinned:', isPinned)}
|
||||||
|
/>
|
||||||
|
```
|
||||||
@@ -21,7 +21,6 @@ interface WebAppProps {
|
|||||||
hasModules?: string[];
|
hasModules?: string[];
|
||||||
defaultModule?: "search" | "chat";
|
defaultModule?: "search" | "chat";
|
||||||
assistantIDs?: string[];
|
assistantIDs?: string[];
|
||||||
hideCoco?: () => void;
|
|
||||||
theme?: "auto" | "light" | "dark";
|
theme?: "auto" | "light" | "dark";
|
||||||
searchPlaceholder?: string;
|
searchPlaceholder?: string;
|
||||||
chatPlaceholder?: string;
|
chatPlaceholder?: string;
|
||||||
@@ -29,6 +28,7 @@ interface WebAppProps {
|
|||||||
startPage?: StartPage;
|
startPage?: StartPage;
|
||||||
setIsPinned?: (value: boolean) => void;
|
setIsPinned?: (value: boolean) => void;
|
||||||
onCancel?: () => void;
|
onCancel?: () => void;
|
||||||
|
isOpen?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function WebApp({
|
function WebApp({
|
||||||
@@ -43,7 +43,6 @@ function WebApp({
|
|||||||
// token = "cvqt6r02sdb2v3bkgip0x3ixv01f3r2lhnxoz1efbn160wm9og58wtv8t6wrv1ebvnvypuc23dx9pb33aemh", // http://localhost:9000
|
// token = "cvqt6r02sdb2v3bkgip0x3ixv01f3r2lhnxoz1efbn160wm9og58wtv8t6wrv1ebvnvypuc23dx9pb33aemh", // http://localhost:9000
|
||||||
// token = "cv5djeb9om602jdvtnmg6kc1muyn2vcadr6te48j9t9pvt59ewrnwj7fwvxrw3va84j2a0lb5y8194fbr3jd", // http://43.153.113.88:9000
|
// token = "cv5djeb9om602jdvtnmg6kc1muyn2vcadr6te48j9t9pvt59ewrnwj7fwvxrw3va84j2a0lb5y8194fbr3jd", // http://43.153.113.88:9000
|
||||||
serverUrl = "",
|
serverUrl = "",
|
||||||
hideCoco = () => {},
|
|
||||||
hasModules = ["search", "chat"],
|
hasModules = ["search", "chat"],
|
||||||
defaultModule = "search",
|
defaultModule = "search",
|
||||||
assistantIDs = [],
|
assistantIDs = [],
|
||||||
@@ -110,7 +109,6 @@ function WebApp({
|
|||||||
)}
|
)}
|
||||||
<SearchChat
|
<SearchChat
|
||||||
isTauri={false}
|
isTauri={false}
|
||||||
hideCoco={hideCoco}
|
|
||||||
hasModules={hasModules}
|
hasModules={hasModules}
|
||||||
defaultModule={defaultModule}
|
defaultModule={defaultModule}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
|
|||||||
@@ -15,10 +15,13 @@ export type AppAction =
|
|||||||
| { type: "SET_CHAT_MODE"; payload: boolean }
|
| { type: "SET_CHAT_MODE"; payload: boolean }
|
||||||
| { type: "SET_INPUT"; payload: string }
|
| { type: "SET_INPUT"; payload: string }
|
||||||
| { type: "TOGGLE_SEARCH_ACTIVE" }
|
| { type: "TOGGLE_SEARCH_ACTIVE" }
|
||||||
|
| { type: "SET_SEARCH_ACTIVE"; payload: boolean }
|
||||||
| { type: "TOGGLE_DEEP_THINK_ACTIVE" }
|
| { type: "TOGGLE_DEEP_THINK_ACTIVE" }
|
||||||
|
| { type: "SET_DEEP_THINK_ACTIVE"; payload: boolean }
|
||||||
| { type: "TOGGLE_MCP_ACTIVE" }
|
| { type: "TOGGLE_MCP_ACTIVE" }
|
||||||
|
| { type: "SET_MCP_ACTIVE"; payload: boolean }
|
||||||
| { type: "SET_TYPING"; payload: boolean }
|
| { type: "SET_TYPING"; payload: boolean }
|
||||||
| { type: "SET_LOADING"; payload: boolean };
|
| { type: "SET_LOADING"; payload: boolean }
|
||||||
|
|
||||||
const getCachedChatMode = (): boolean => {
|
const getCachedChatMode = (): boolean => {
|
||||||
const { defaultStartupWindow } = useStartupStore.getState();
|
const { defaultStartupWindow } = useStartupStore.getState();
|
||||||
@@ -49,10 +52,16 @@ export function appReducer(state: AppState, action: AppAction): AppState {
|
|||||||
return { ...state, input: action.payload };
|
return { ...state, input: action.payload };
|
||||||
case "TOGGLE_SEARCH_ACTIVE":
|
case "TOGGLE_SEARCH_ACTIVE":
|
||||||
return { ...state, isSearchActive: !state.isSearchActive };
|
return { ...state, isSearchActive: !state.isSearchActive };
|
||||||
|
case "SET_SEARCH_ACTIVE":
|
||||||
|
return { ...state, isSearchActive: action.payload };
|
||||||
case "TOGGLE_DEEP_THINK_ACTIVE":
|
case "TOGGLE_DEEP_THINK_ACTIVE":
|
||||||
return { ...state, isDeepThinkActive: !state.isDeepThinkActive };
|
return { ...state, isDeepThinkActive: !state.isDeepThinkActive };
|
||||||
|
case "SET_DEEP_THINK_ACTIVE":
|
||||||
|
return { ...state, isDeepThinkActive: action.payload };
|
||||||
case "TOGGLE_MCP_ACTIVE":
|
case "TOGGLE_MCP_ACTIVE":
|
||||||
return { ...state, isMCPActive: !state.isMCPActive };
|
return { ...state, isMCPActive: !state.isMCPActive };
|
||||||
|
case "SET_MCP_ACTIVE":
|
||||||
|
return { ...state, isMCPActive: action.payload };
|
||||||
case "SET_TYPING":
|
case "SET_TYPING":
|
||||||
return { ...state, isTyping: action.payload };
|
return { ...state, isTyping: action.payload };
|
||||||
case "SET_LOADING":
|
case "SET_LOADING":
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { Outlet, useLocation } from "react-router-dom";
|
import { Outlet, useLocation } from "react-router-dom";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useAsyncEffect, useEventListener, useMount } from "ahooks";
|
import {
|
||||||
|
useAsyncEffect,
|
||||||
|
useEventListener,
|
||||||
|
useMount,
|
||||||
|
useTextSelection,
|
||||||
|
} from "ahooks";
|
||||||
import { isArray, isString } from "lodash-es";
|
import { isArray, isString } from "lodash-es";
|
||||||
import { error } from "@tauri-apps/plugin-log";
|
import { error } from "@tauri-apps/plugin-log";
|
||||||
|
|
||||||
@@ -40,6 +45,7 @@ export default function Layout() {
|
|||||||
const unlistenTheme = await platformAdapter.listenThemeChanged(
|
const unlistenTheme = await platformAdapter.listenThemeChanged(
|
||||||
(theme: AppTheme) => {
|
(theme: AppTheme) => {
|
||||||
setTheme(theme);
|
setTheme(theme);
|
||||||
|
setIsDark(theme === "dark");
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -82,31 +88,15 @@ export default function Layout() {
|
|||||||
|
|
||||||
const { i18n } = useTranslation();
|
const { i18n } = useTranslation();
|
||||||
const language = useAppStore((state) => state.language);
|
const language = useAppStore((state) => state.language);
|
||||||
|
const { text: selectionText } = useTextSelection();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (language) {
|
|
||||||
i18n.changeLanguage(language);
|
i18n.changeLanguage(language);
|
||||||
}
|
}, [language]);
|
||||||
|
|
||||||
const setupLanguageListener = async () => {
|
|
||||||
const unlisten = await platformAdapter.listenEvent(
|
|
||||||
"language-changed",
|
|
||||||
(event) => {
|
|
||||||
i18n.changeLanguage(event.payload.language);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return unlisten;
|
|
||||||
};
|
|
||||||
|
|
||||||
const unlistenPromise = setupLanguageListener();
|
|
||||||
return () => {
|
|
||||||
unlistenPromise.then((unlisten) => unlisten());
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Disable right-click for production environment
|
// Disable right-click for production environment
|
||||||
useEventListener("contextmenu", (event) => {
|
useEventListener("contextmenu", (event) => {
|
||||||
if (import.meta.env.DEV) return;
|
if (import.meta.env.DEV || selectionText) return;
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import { persist } from "zustand/middleware";
|
import { persist, subscribeWithSelector } from "zustand/middleware";
|
||||||
|
|
||||||
import { AppEndpoint } from "@/types/index";
|
import { AppEndpoint } from "@/types/index";
|
||||||
import platformAdapter from "@/utils/platformAdapter";
|
|
||||||
|
|
||||||
const ENDPOINT_CHANGE_EVENT = "endpoint-changed";
|
|
||||||
|
|
||||||
interface ErrorMessage {
|
interface ErrorMessage {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -33,7 +30,6 @@ export type IAppStore = {
|
|||||||
setLanguage: (language: string) => void;
|
setLanguage: (language: string) => void;
|
||||||
isPinned: boolean;
|
isPinned: boolean;
|
||||||
setIsPinned: (isPinned: boolean) => void;
|
setIsPinned: (isPinned: boolean) => void;
|
||||||
initializeListeners: () => Promise<() => void>;
|
|
||||||
|
|
||||||
showCocoShortcuts: string[];
|
showCocoShortcuts: string[];
|
||||||
setShowCocoShortcuts: (showCocoShortcuts: string[]) => void;
|
setShowCocoShortcuts: (showCocoShortcuts: string[]) => void;
|
||||||
@@ -49,10 +45,13 @@ export type IAppStore = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const useAppStore = create<IAppStore>()(
|
export const useAppStore = create<IAppStore>()(
|
||||||
|
subscribeWithSelector(
|
||||||
persist(
|
persist(
|
||||||
(set) => ({
|
(set) => ({
|
||||||
showTooltip: true,
|
showTooltip: true,
|
||||||
setShowTooltip: (showTooltip: boolean) => set({ showTooltip }),
|
setShowTooltip: async (showTooltip: boolean) => {
|
||||||
|
return set({ showTooltip });
|
||||||
|
},
|
||||||
errors: [],
|
errors: [],
|
||||||
addError: (
|
addError: (
|
||||||
message: string,
|
message: string,
|
||||||
@@ -89,13 +88,7 @@ export const useAppStore = create<IAppStore>()(
|
|||||||
? `wss://${withoutProtocol}/ws`
|
? `wss://${withoutProtocol}/ws`
|
||||||
: `ws://${withoutProtocol}/ws`;
|
: `ws://${withoutProtocol}/ws`;
|
||||||
|
|
||||||
set({
|
return set({
|
||||||
endpoint,
|
|
||||||
endpoint_http,
|
|
||||||
endpoint_websocket,
|
|
||||||
});
|
|
||||||
|
|
||||||
await platformAdapter.emitEvent(ENDPOINT_CHANGE_EVENT, {
|
|
||||||
endpoint,
|
endpoint,
|
||||||
endpoint_http,
|
endpoint_http,
|
||||||
endpoint_websocket,
|
endpoint_websocket,
|
||||||
@@ -105,16 +98,6 @@ export const useAppStore = create<IAppStore>()(
|
|||||||
setLanguage: (language: string) => set({ language }),
|
setLanguage: (language: string) => set({ language }),
|
||||||
isPinned: false,
|
isPinned: false,
|
||||||
setIsPinned: (isPinned: boolean) => set({ isPinned }),
|
setIsPinned: (isPinned: boolean) => set({ isPinned }),
|
||||||
initializeListeners: () => {
|
|
||||||
return platformAdapter.listenEvent(
|
|
||||||
ENDPOINT_CHANGE_EVENT,
|
|
||||||
(event: any) => {
|
|
||||||
const { endpoint, endpoint_http, endpoint_websocket } =
|
|
||||||
event.payload;
|
|
||||||
set({ endpoint, endpoint_http, endpoint_websocket });
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
|
||||||
showCocoShortcuts: [],
|
showCocoShortcuts: [],
|
||||||
setShowCocoShortcuts: (showCocoShortcuts: string[]) => {
|
setShowCocoShortcuts: (showCocoShortcuts: string[]) => {
|
||||||
console.log("set showCocoShortcuts", showCocoShortcuts);
|
console.log("set showCocoShortcuts", showCocoShortcuts);
|
||||||
@@ -150,4 +133,5 @@ export const useAppStore = create<IAppStore>()(
|
|||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export const useAppearanceStore = create<IAppearanceStore>()(
|
|||||||
(set) => ({
|
(set) => ({
|
||||||
opacity: 30,
|
opacity: 30,
|
||||||
setOpacity: (opacity) => {
|
setOpacity: (opacity) => {
|
||||||
return set({ opacity: opacity || 30 });
|
return set({ opacity: opacity });
|
||||||
},
|
},
|
||||||
snapshotUpdate: false,
|
snapshotUpdate: false,
|
||||||
setSnapshotUpdate: (snapshotUpdate) => {
|
setSnapshotUpdate: (snapshotUpdate) => {
|
||||||
|
|||||||