23 Commits

Author SHA1 Message Date
BiggerRain
bfc7b488ad fix: store data is not shared among multiple windows (#298)
* fix: store data is not shared among multiple windows

* docs: update release notes
2025-03-14 18:41:48 +08:00
Hardy
249cc2eae4 chore: add settings to output docs json (#297)
Co-authored-by: hardy <luohf@infinilabs.com>
2025-03-14 18:37:02 +08:00
BiggerRain
388dac6452 chore: chat input border & clear input (#295)
* chore: chat input border & clear input

* docs: update release notes
2025-03-14 17:19:39 +08:00
ayangweb
dc8d1b5054 refactor: hide voice input buttons (#294) 2025-03-14 17:07:46 +08:00
ayangweb
046c3dda82 chore: update release notes (#293) 2025-03-14 16:27:51 +08:00
medcl
60ce678e3e v0.2.1 2025-03-14 16:06:08 +08:00
Medcl
8d79b9ba1a chore: update release notes (#290) 2025-03-14 15:49:24 +08:00
BiggerRain
969126ed89 chore: websocket timeout increased to 2 minutes (#289)
* chore: websocket timeout increased to 2 minutes

* docs: update release notes
2025-03-14 11:14:03 +08:00
ayangweb
e2df2b583a refactor: optimize the outline of the button (#288) 2025-03-14 11:07:19 +08:00
BiggerRain
9d948d4fc6 chore: remove selected function (#286)
* chore: remove selected function

* docs: update release notes
2025-03-14 10:50:51 +08:00
ayangweb
81c770ba7e refactor: optimize voice input (#285)
* refactor: optimize voice input

* refactor: `useState` instead of `useReactive`
2025-03-14 10:49:44 +08:00
BiggerRain
c9e9a72a0e chore: chat window min width & remove input bg (#284)
* chore: chat window min width & remove input bg

* docs: update release notes

* chore: remove error
2025-03-13 14:41:41 +08:00
SteveLauC
96e6aae30b ci: remove unneeded rust-toolchain action (#283) 2025-03-12 18:06:36 +08:00
BiggerRain
d319f5ebc7 fix: the chat scrolling and chat rendering (#282)
* fix: the chat scrolling and chat rendering

* docs: update release notes
2025-03-12 16:50:35 +08:00
BiggerRain
04ff358dc7 fix: chat end type (#280)
* fix: chat end type

* docs: update release notes
2025-03-12 14:24:24 +08:00
Medcl
22872ab02f fix: incorrect version type (#279)
* fix: incorrect version type

* chore: update release notes
2025-03-12 13:28:43 +08:00
ayangweb
fcfd21be45 refactor: disable manual input during voice input (#278) 2025-03-12 10:46:16 +08:00
ayangweb
0044e9a536 feat: chat supports voice input (#276)
* feat: chat supports voice input

* refactor: hide window out of focus

* feat: search supports voice input
2025-03-11 16:36:51 +08:00
BiggerRain
44a3ea3868 fix: add history reload for coco chat (#275) 2025-03-11 15:48:45 +08:00
BiggerRain
b444dc35ae refactor: chat components (#273)
* refactor: chat components

* refactor: chat components

* docs: update release notes

* docs: update release notes

* chore: history reload
2025-03-11 11:02:30 +08:00
ayangweb
8c9ccef218 feat: support for automatic app updates (#274)
* feat: support for automatic app updates

* refactor: add force update instructions

* refactor: optimize version update alerts

* chore: updating configuration files
2025-03-11 10:36:42 +08:00
ayangweb
a3bc997efe refactor: window not hidden after copying (#272) 2025-03-10 14:52:47 +08:00
Medcl
910841013f fix: fusion search should excluded disabled servers (#271) 2025-03-10 12:08:39 +08:00
52 changed files with 1966 additions and 1194 deletions

View File

@@ -73,7 +73,7 @@ jobs:
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
run: rustup toolchain install stable
- name: Rust cache
uses: swatinem/rust-cache@v2

12
.vscode/settings.json vendored
View File

@@ -4,34 +4,45 @@
"autolaunch",
"Avenir",
"callout",
"changelogithub",
"clsx",
"codegen",
"dtolnay",
"dyld",
"elif",
"fullscreen",
"headlessui",
"Icdbb",
"icns",
"INFINI",
"infinilabs",
"inputbox",
"katex",
"khtml",
"languagedetector",
"libappindicator",
"librsvg",
"libwebkit",
"localstorage",
"lucide",
"maximizable",
"Minimizable",
"msvc",
"nord",
"nowrap",
"nspanel",
"nsstring",
"overscan",
"partialize",
"patchelf",
"Raycast",
"rehype",
"reqwest",
"rgba",
"rustup",
"screenshotable",
"serde",
"swatinem",
"tailwindcss",
"tauri",
"thiserror",
@@ -46,6 +57,7 @@
"VITE",
"walkdir",
"webviews",
"xzvf",
"yuque",
"zustand"
],

View File

@@ -7,6 +7,12 @@ theme: book
disablePathToLower: true
enableGitInfo: false
outputs:
home:
- HTML
- RSS
- JSON
# Needed for mermaid/katex shortcodes
markup:
goldmark:

View File

@@ -9,14 +9,39 @@ Information about release notes of Coco Server is provided here.
## Latest (In development)
### Breaking changes
### Features
### Bug fix
### Improvements
## 0.2.1 (2025-03-14)
### Features
- support for automatic in-app updates #274
### Breaking changes
### Bug fix
- Fix the issue that the fusion search include disabled servers
- Fix incorrect version type: should be string instead of u32
- Fix the chat end judgment type #280
- Fix the chat scrolling and chat rendering #282
- Fix: store data is not shared among multiple windows #298
### Improvements
- Refactor: chat components #273
- Featadd endpoint display #282
- Chore: chat window min width & remove input bg #284
- Chore: remove selected function & add hide_coco #286
- Chorewebsocket timeout increased to 2 minutes #289
- Chore: remove chat input border & clear input #295
## 0.2.0 (2025-03-07)
### Features
@@ -25,7 +50,7 @@ Information about release notes of Coco Server is provided here.
- Add api to disable or enable server #185
- Networked search supports selection of data sources #209
- Add deepthink and knowledge search options to RAG based chat
- Support i18n, add Chinese language support
- Support i18n, add Chinese language support
- Support Windows platform
- etc.
@@ -54,7 +79,6 @@ Information about release notes of Coco Server is provided here.
- Allow to switch servers in the settings page
- etc.
## 0.1.0 (2025-02-16)
### Features

View File

@@ -1,7 +1,7 @@
{
"name": "coco",
"private": true,
"version": "0.2.0",
"version": "0.2.1",
"type": "module",
"scripts": {
"dev": "vite",
@@ -20,10 +20,10 @@
"@tauri-apps/plugin-dialog": "^2.2.0",
"@tauri-apps/plugin-global-shortcut": "~2.0.0",
"@tauri-apps/plugin-http": "~2.0.2",
"@tauri-apps/plugin-os": "^2.2.0",
"@tauri-apps/plugin-os": "^2.2.1",
"@tauri-apps/plugin-process": "^2.2.0",
"@tauri-apps/plugin-shell": "^2.2.0",
"@tauri-apps/plugin-updater": "^2.5.1",
"@tauri-apps/plugin-updater": "^2.6.0",
"@tauri-apps/plugin-websocket": "~2.3.0",
"@tauri-apps/plugin-window": "2.0.0-alpha.1",
"ahooks": "^3.8.4",
@@ -35,7 +35,7 @@
"lodash-es": "^4.17.21",
"lucide-react": "^0.461.0",
"mermaid": "^11.4.1",
"nanoid": "^5.1.2",
"nanoid": "^5.1.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hotkeys-hook": "^4.6.1",
@@ -57,16 +57,17 @@
},
"devDependencies": {
"@tauri-apps/cli": "^2.3.1",
"@types/dom-speech-recognition": "^0.0.4",
"@types/lodash-es": "^4.17.12",
"@types/markdown-it": "^14.1.2",
"@types/node": "^22.13.9",
"@types/node": "^22.13.10",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"@types/react-i18next": "^8.1.0",
"@types/react-katex": "^3.0.4",
"@types/react-window": "^1.8.8",
"@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.4.20",
"autoprefixer": "^10.4.21",
"immer": "^10.1.1",
"postcss": "^8.5.3",
"release-it": "^18.1.2",

766
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

329
src-tauri/Cargo.lock generated
View File

@@ -181,7 +181,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.99",
"syn 2.0.100",
]
[[package]]
@@ -272,7 +272,7 @@ dependencies = [
"futures-lite",
"parking",
"polling",
"rustix",
"rustix 0.38.44",
"slab",
"tracing",
"windows-sys 0.59.0",
@@ -304,7 +304,7 @@ dependencies = [
"cfg-if",
"event-listener",
"futures-lite",
"rustix",
"rustix 0.38.44",
"tracing",
]
@@ -316,7 +316,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.99",
"syn 2.0.100",
]
[[package]]
@@ -331,7 +331,7 @@ dependencies = [
"cfg-if",
"futures-core",
"futures-io",
"rustix",
"rustix 0.38.44",
"signal-hook-registry",
"slab",
"windows-sys 0.59.0",
@@ -351,7 +351,7 @@ checksum = "d556ec1359574147ec0c4fc5eb525f3f23263a592b1a9c07e0a75b427de55c97"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.99",
"syn 2.0.100",
]
[[package]]
@@ -582,9 +582,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
[[package]]
name = "bytes"
version = "1.10.0"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9"
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
dependencies = [
"serde",
]
@@ -733,7 +733,7 @@ dependencies = [
[[package]]
name = "coco"
version = "0.2.0"
version = "0.2.1"
dependencies = [
"applications",
"async-trait",
@@ -1103,7 +1103,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331"
dependencies = [
"quote",
"syn 2.0.99",
"syn 2.0.100",
]
[[package]]
@@ -1113,7 +1113,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501"
dependencies = [
"quote",
"syn 2.0.99",
"syn 2.0.100",
]
[[package]]
@@ -1137,7 +1137,7 @@ dependencies = [
"proc-macro2",
"quote",
"strsim",
"syn 2.0.99",
"syn 2.0.100",
]
[[package]]
@@ -1148,7 +1148,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806"
dependencies = [
"darling_core",
"quote",
"syn 2.0.99",
"syn 2.0.100",
]
[[package]]
@@ -1192,7 +1192,7 @@ checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.99",
"syn 2.0.100",
]
[[package]]
@@ -1205,7 +1205,7 @@ dependencies = [
"proc-macro2",
"quote",
"rustc_version",
"syn 2.0.99",
"syn 2.0.100",
]
[[package]]
@@ -1294,7 +1294,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.99",
"syn 2.0.100",
]
[[package]]
@@ -1326,7 +1326,7 @@ checksum = "f2b99bf03862d7f545ebc28ddd33a665b50865f4dfd84031a393823879bd4c54"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.99",
"syn 2.0.100",
]
[[package]]
@@ -1412,9 +1412,9 @@ checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005"
[[package]]
name = "either"
version = "1.14.0"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7914353092ddf589ad78f25c5c1c21b7f80b0ff8621e7c814c3485b5306da9d"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "embed-resource"
@@ -1469,7 +1469,7 @@ checksum = "fc4caf64a58d7a6d65ab00639b046ff54399a39f5f2554728895ace4b297cd79"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.99",
"syn 2.0.100",
]
[[package]]
@@ -1484,14 +1484,14 @@ dependencies = [
[[package]]
name = "env_logger"
version = "0.11.6"
version = "0.11.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcaee3d8e3cfc3fd92428d477bc97fc29ec8716d180c0d74c643bb26166660e0"
checksum = "c3716d7a920fb4fac5d84e9d4bce8ceb321e9414b4409da61b07b75c1e3d0697"
dependencies = [
"anstream",
"anstyle",
"env_filter",
"humantime",
"jiff",
"log",
]
@@ -1652,7 +1652,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.99",
"syn 2.0.100",
]
[[package]]
@@ -1770,7 +1770,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.99",
"syn 2.0.100",
]
[[package]]
@@ -1929,11 +1929,11 @@ dependencies = [
[[package]]
name = "gethostname"
version = "0.5.0"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc3655aa6818d65bc620d6911f05aa7b6aeb596291e1e9f79e52df85583d1e30"
checksum = "4fd4b8790c0792e3b11895efdf5f289ebe8b59107a6624f1cce68f24ff8c7035"
dependencies = [
"rustix",
"rustix 0.38.44",
"windows-targets 0.52.6",
]
@@ -2106,7 +2106,7 @@ dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
"syn 2.0.99",
"syn 2.0.100",
]
[[package]]
@@ -2116,10 +2116,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "715601f8f02e71baef9c1f94a657a9a77c192aea6097cf9ae7e5e177cd8cde68"
dependencies = [
"heck 0.5.0",
"proc-macro-crate 3.2.0",
"proc-macro-crate 3.3.0",
"proc-macro2",
"quote",
"syn 2.0.99",
"syn 2.0.100",
]
[[package]]
@@ -2236,7 +2236,7 @@ dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
"syn 2.0.99",
"syn 2.0.100",
]
[[package]]
@@ -2251,7 +2251,7 @@ dependencies = [
"futures-core",
"futures-sink",
"http 1.2.0",
"indexmap 2.7.1",
"indexmap 2.8.0",
"slab",
"tokio",
"tokio-util",
@@ -2413,12 +2413,6 @@ version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "humantime"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]]
name = "hyper"
version = "0.14.32"
@@ -2662,7 +2656,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.99",
"syn 2.0.100",
]
[[package]]
@@ -2744,9 +2738,9 @@ dependencies = [
[[package]]
name = "indexmap"
version = "2.7.1"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652"
checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058"
dependencies = [
"equivalent",
"hashbrown 0.15.2",
@@ -2799,7 +2793,7 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.99",
"syn 2.0.100",
]
[[package]]
@@ -2877,6 +2871,30 @@ dependencies = [
"system-deps 6.2.2",
]
[[package]]
name = "jiff"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d699bc6dfc879fb1bf9bdff0d4c56f0884fc6f0d0eb0fba397a6d00cd9a6b85e"
dependencies = [
"jiff-static",
"log",
"portable-atomic",
"portable-atomic-util",
"serde",
]
[[package]]
name = "jiff-static"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d16e75759ee0aa64c57a56acbf43916987b20c77373cb7e808979e02b93c9f9"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.100",
]
[[package]]
name = "jni"
version = "0.21.1"
@@ -3088,6 +3106,12 @@ version = "0.4.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
[[package]]
name = "linux-raw-sys"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db9c683daf087dc577b7506e9695b3d556a9f3849903fa28186283afd6809e9"
[[package]]
name = "litemap"
version = "0.7.5"
@@ -3450,7 +3474,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.99",
"syn 2.0.100",
]
[[package]]
@@ -3497,10 +3521,10 @@ version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56"
dependencies = [
"proc-macro-crate 3.2.0",
"proc-macro-crate 3.3.0",
"proc-macro2",
"quote",
"syn 2.0.99",
"syn 2.0.100",
]
[[package]]
@@ -3824,9 +3848,9 @@ dependencies = [
[[package]]
name = "once_cell"
version = "1.20.3"
version = "1.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e"
checksum = "cde51589ab56b20a6f686b2c68f7a0bd6add753d697abf720d63f8db3ab7b1ad"
[[package]]
name = "open"
@@ -3863,7 +3887,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.99",
"syn 2.0.100",
]
[[package]]
@@ -4143,7 +4167,7 @@ dependencies = [
"phf_shared 0.11.3",
"proc-macro2",
"quote",
"syn 2.0.99",
"syn 2.0.100",
]
[[package]]
@@ -4227,7 +4251,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016"
dependencies = [
"base64 0.22.1",
"indexmap 2.7.1",
"indexmap 2.8.0",
"quick-xml 0.32.0",
"serde",
"time",
@@ -4256,11 +4280,26 @@ dependencies = [
"concurrent-queue",
"hermit-abi",
"pin-project-lite",
"rustix",
"rustix 0.38.44",
"tracing",
"windows-sys 0.59.0",
]
[[package]]
name = "portable-atomic"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e"
[[package]]
name = "portable-atomic-util"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507"
dependencies = [
"portable-atomic",
]
[[package]]
name = "powerfmt"
version = "0.2.0"
@@ -4269,11 +4308,11 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "ppv-lite86"
version = "0.2.20"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
"zerocopy 0.7.35",
"zerocopy 0.8.23",
]
[[package]]
@@ -4303,9 +4342,9 @@ dependencies = [
[[package]]
name = "proc-macro-crate"
version = "3.2.0"
version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b"
checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35"
dependencies = [
"toml_edit 0.22.24",
]
@@ -4365,7 +4404,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a65f2e60fbf1063868558d69c6beacf412dc755f9fc020f514b7955fc914fe30"
dependencies = [
"quote",
"syn 2.0.99",
"syn 2.0.100",
]
[[package]]
@@ -4520,7 +4559,7 @@ checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94"
dependencies = [
"rand_chacha 0.9.0",
"rand_core 0.9.3",
"zerocopy 0.8.21",
"zerocopy 0.8.23",
]
[[package]]
@@ -4820,9 +4859,9 @@ checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a"
[[package]]
name = "ring"
version = "0.17.11"
version = "0.17.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da5349ae27d3887ca812fb375b45a4fbb36d8d12d2df394968cd86e35683fe73"
checksum = "70ac5d832aa16abd7d1def883a8545280c20a60f523a370aa3a9617c2b8550ee"
dependencies = [
"cc",
"cfg-if",
@@ -4895,7 +4934,20 @@ dependencies = [
"bitflags 2.9.0",
"errno",
"libc",
"linux-raw-sys",
"linux-raw-sys 0.4.15",
"windows-sys 0.59.0",
]
[[package]]
name = "rustix"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7178faa4b75a30e269c71e61c353ce2748cf3d76f0c44c393f4e60abf49b825"
dependencies = [
"bitflags 2.9.0",
"errno",
"libc",
"linux-raw-sys 0.9.2",
"windows-sys 0.59.0",
]
@@ -5018,7 +5070,7 @@ dependencies = [
"proc-macro2",
"quote",
"serde_derive_internals",
"syn 2.0.99",
"syn 2.0.100",
]
[[package]]
@@ -5097,9 +5149,9 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.218"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60"
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
dependencies = [
"serde_derive",
]
@@ -5117,13 +5169,13 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.218"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b"
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.99",
"syn 2.0.100",
]
[[package]]
@@ -5134,7 +5186,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.99",
"syn 2.0.100",
]
[[package]]
@@ -5157,7 +5209,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.99",
"syn 2.0.100",
]
[[package]]
@@ -5191,7 +5243,7 @@ dependencies = [
"chrono",
"hex",
"indexmap 1.9.3",
"indexmap 2.7.1",
"indexmap 2.8.0",
"serde",
"serde_derive",
"serde_json",
@@ -5208,7 +5260,7 @@ dependencies = [
"darling",
"proc-macro2",
"quote",
"syn 2.0.99",
"syn 2.0.100",
]
[[package]]
@@ -5475,9 +5527,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.99"
version = "2.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e02e925281e18ffd9d640e234264753c43edc62d64b2d4cf898f1bc5e75f3fc2"
checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0"
dependencies = [
"proc-macro2",
"quote",
@@ -5501,7 +5553,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.99",
"syn 2.0.100",
]
[[package]]
@@ -5562,9 +5614,9 @@ dependencies = [
[[package]]
name = "tao"
version = "0.32.7"
version = "0.32.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e7f38988a68dfb559899ea307b97577f008d3254f6cfdd219a67e27ce34c95b"
checksum = "63c8b1020610b9138dd7b1e06cf259ae91aa05c30f3bd0d6b42a03997b92dec1"
dependencies = [
"bitflags 2.9.0",
"core-foundation 0.10.0",
@@ -5607,7 +5659,7 @@ checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.99",
"syn 2.0.100",
]
[[package]]
@@ -5719,7 +5771,7 @@ dependencies = [
"serde",
"serde_json",
"sha2",
"syn 2.0.99",
"syn 2.0.100",
"tauri-utils",
"thiserror 2.0.12",
"time",
@@ -5747,7 +5799,7 @@ dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.99",
"syn 2.0.100",
"tauri-codegen",
"tauri-utils",
]
@@ -5755,7 +5807,7 @@ dependencies = [
[[package]]
name = "tauri-nspanel"
version = "2.0.1"
source = "git+https://github.com/ahkohd/tauri-nspanel?branch=v2#d20d53af786f1e2bdd48caac0145cd8ec3990c9b"
source = "git+https://github.com/ahkohd/tauri-nspanel?branch=v2#16111b14441716350b6dc8157d926a5fda481687"
dependencies = [
"bitflags 2.9.0",
"block",
@@ -5911,9 +5963,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-http"
version = "2.3.0"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a8137a106e0741fdd357366178fc6e0597abe7d20796f53f44171a1bcec1683"
checksum = "028093def653e1f9da23a80beedfd33b88899427693b2c8357ce0c1cc26284b2"
dependencies = [
"data-url",
"http 1.2.0",
@@ -5947,9 +5999,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-os"
version = "2.2.0"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dda2d571a9baf0664c1f2088db227e3072f9028602fafa885deade7547c3b738"
checksum = "424f19432397850c2ddd42aa58078630c15287bbce3866eb1d90e7dbee680637"
dependencies = [
"gethostname",
"log",
@@ -6041,9 +6093,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-updater"
version = "2.5.1"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69b7db616844d73d55e4d00190be101b29de463d5cb70321c2840fa4e9c414c4"
checksum = "67cd78a6cbd1255e989e96eedec004e9e8949e6c6359b41f861279aba64ea306"
dependencies = [
"base64 0.22.1",
"dirs 6.0.0",
@@ -6051,6 +6103,7 @@ dependencies = [
"futures-util",
"http 1.2.0",
"infer",
"log",
"minisign-verify",
"osakit",
"percent-encoding",
@@ -6184,15 +6237,15 @@ dependencies = [
[[package]]
name = "tempfile"
version = "3.17.1"
version = "3.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22e5a0acb1f3f55f65cc4a866c361b2fb2a0ff6366785ae6fbb5f85df07ba230"
checksum = "2c317e0a526ee6120d8dabad239c8dadca62b24b6f168914bbbc8e2fb1f0e567"
dependencies = [
"cfg-if",
"fastrand",
"getrandom 0.3.1",
"once_cell",
"rustix",
"rustix 1.0.2",
"windows-sys 0.59.0",
]
@@ -6239,7 +6292,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.99",
"syn 2.0.100",
]
[[package]]
@@ -6250,7 +6303,7 @@ checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.99",
"syn 2.0.100",
]
[[package]]
@@ -6266,9 +6319,9 @@ dependencies = [
[[package]]
name = "time"
version = "0.3.37"
version = "0.3.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21"
checksum = "dad298b01a40a23aac4580b67e3dbedb7cc8402f3592d7f49469de2ea4aecdd8"
dependencies = [
"deranged",
"itoa 1.0.15",
@@ -6281,15 +6334,15 @@ dependencies = [
[[package]]
name = "time-core"
version = "0.1.2"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
checksum = "765c97a5b985b7c11d7bc27fa927dc4fe6af3a6dfb021d28deb60d3bf51e76ef"
[[package]]
name = "time-macros"
version = "0.2.19"
version = "0.2.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de"
checksum = "e8093bc3e81c3bc5f7879de09619d06c9a5a5e45ca44dfeeb7225bae38005c5c"
dependencies = [
"num-conv",
"time-core",
@@ -6331,9 +6384,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.43.0"
version = "1.44.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e"
checksum = "9975ea0f48b5aa3972bf2d888c238182458437cc2a19374b81b25cdf1023fb3a"
dependencies = [
"backtrace",
"bytes",
@@ -6356,7 +6409,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.99",
"syn 2.0.100",
]
[[package]]
@@ -6460,7 +6513,7 @@ version = "0.19.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
dependencies = [
"indexmap 2.7.1",
"indexmap 2.8.0",
"toml_datetime",
"winnow 0.5.40",
]
@@ -6471,7 +6524,7 @@ version = "0.20.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81"
dependencies = [
"indexmap 2.7.1",
"indexmap 2.8.0",
"toml_datetime",
"winnow 0.5.40",
]
@@ -6482,7 +6535,7 @@ version = "0.22.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474"
dependencies = [
"indexmap 2.7.1",
"indexmap 2.8.0",
"serde",
"serde_spanned",
"toml_datetime",
@@ -6535,7 +6588,7 @@ checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.99",
"syn 2.0.100",
]
[[package]]
@@ -6895,7 +6948,7 @@ dependencies = [
"log",
"proc-macro2",
"quote",
"syn 2.0.99",
"syn 2.0.100",
"wasm-bindgen-shared",
]
@@ -6930,7 +6983,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.99",
"syn 2.0.100",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@@ -6965,7 +7018,7 @@ checksum = "b7208998eaa3870dad37ec8836979581506e0c5c64c20c9e79e9d2a10d6f47bf"
dependencies = [
"cc",
"downcast-rs",
"rustix",
"rustix 0.38.44",
"scoped-tls",
"smallvec",
"wayland-sys",
@@ -6978,7 +7031,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2120de3d33638aaef5b9f4472bff75f07c56379cf76ea320bd3a3d65ecaf73f"
dependencies = [
"bitflags 2.9.0",
"rustix",
"rustix 0.38.44",
"wayland-backend",
"wayland-scanner",
]
@@ -7118,7 +7171,7 @@ checksum = "1d228f15bba3b9d56dde8bddbee66fa24545bd17b48d5128ccf4a8742b18e431"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.99",
"syn 2.0.100",
]
[[package]]
@@ -7329,7 +7382,7 @@ checksum = "12168c33176773b86799be25e2a2ba07c7aab9968b37541f1094dbd7a60c8946"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.99",
"syn 2.0.100",
]
[[package]]
@@ -7340,7 +7393,7 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.99",
"syn 2.0.100",
]
[[package]]
@@ -7351,7 +7404,7 @@ checksum = "83577b051e2f49a058c308f17f273b570a6a758386fc291b5f6a934dd84e48c1"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.99",
"syn 2.0.100",
]
[[package]]
@@ -7362,7 +7415,7 @@ checksum = "9d8dc32e0095a7eeccebd0e3f09e9509365ecb3fc6ac4d6f5f14a3f6392942d1"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.99",
"syn 2.0.100",
]
[[package]]
@@ -7373,7 +7426,7 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.99",
"syn 2.0.100",
]
[[package]]
@@ -7384,7 +7437,7 @@ checksum = "cb26fd936d991781ea39e87c3a27285081e3c0da5ca0fcbc02d368cc6f52ff01"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.99",
"syn 2.0.100",
]
[[package]]
@@ -7818,9 +7871,9 @@ checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
[[package]]
name = "wry"
version = "0.50.3"
version = "0.50.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2ec139df5102db821f92a42033c3fa0467c5ab434511e79c65881d6bdf2b369"
checksum = "804a7d1613bd699beccaa60f3b3c679acee21cebba1945a693f5eab95c08d1fa"
dependencies = [
"base64 0.22.1",
"block2 0.6.0",
@@ -7883,13 +7936,12 @@ dependencies = [
[[package]]
name = "xattr"
version = "1.4.0"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e105d177a3871454f754b33bb0ee637ecaaac997446375fd3e5d43a2ed00c909"
checksum = "0d65cbf2f12c15564212d48f4e3dfb87923d25d611f2aed18f4cb23f0413d89e"
dependencies = [
"libc",
"linux-raw-sys",
"rustix",
"rustix 1.0.2",
]
[[package]]
@@ -7955,7 +8007,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.99",
"syn 2.0.100",
"synstructure",
]
@@ -8002,10 +8054,10 @@ version = "5.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f325ad10eb0d0a3eb060203494c3b7ec3162a01a59db75d2deee100339709fc0"
dependencies = [
"proc-macro-crate 3.2.0",
"proc-macro-crate 3.3.0",
"proc-macro2",
"quote",
"syn 2.0.99",
"syn 2.0.100",
"zbus_names",
"zvariant",
"zvariant_utils",
@@ -8029,17 +8081,16 @@ version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
dependencies = [
"byteorder",
"zerocopy-derive 0.7.35",
]
[[package]]
name = "zerocopy"
version = "0.8.21"
version = "0.8.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcf01143b2dd5d134f11f545cf9f1431b13b749695cb33bcce051e7568f99478"
checksum = "fd97444d05a4328b90e75e503a34bad781f14e28a823ad3557f0750df1ebcbc6"
dependencies = [
"zerocopy-derive 0.8.21",
"zerocopy-derive 0.8.23",
]
[[package]]
@@ -8050,18 +8101,18 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.99",
"syn 2.0.100",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.21"
version = "0.8.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712c8386f4f4299382c9abee219bee7084f78fb939d88b6840fcc1320d5f6da2"
checksum = "6352c01d0edd5db859a63e2605f4ea3183ddbd15e2c4a9e7d32184df75e4f154"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.99",
"syn 2.0.100",
]
[[package]]
@@ -8081,7 +8132,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.99",
"syn 2.0.100",
"synstructure",
]
@@ -8110,7 +8161,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.99",
"syn 2.0.100",
]
[[package]]
@@ -8123,7 +8174,7 @@ dependencies = [
"crc32fast",
"crossbeam-utils",
"displaydoc",
"indexmap 2.7.1",
"indexmap 2.8.0",
"memchr",
"thiserror 2.0.12",
]
@@ -8174,10 +8225,10 @@ version = "5.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74170caa85b8b84cc4935f2d56a57c7a15ea6185ccdd7eadb57e6edd90f94b2f"
dependencies = [
"proc-macro-crate 3.2.0",
"proc-macro-crate 3.3.0",
"proc-macro2",
"quote",
"syn 2.0.99",
"syn 2.0.100",
"zvariant_utils",
]
@@ -8191,6 +8242,6 @@ dependencies = [
"quote",
"serde",
"static_assertions",
"syn 2.0.99",
"syn 2.0.100",
"winnow 0.7.3",
]

View File

@@ -1,6 +1,6 @@
[package]
name = "coco"
version = "0.2.0"
version = "0.2.1"
description = "Search, connect, collaborate all in one place."
authors = ["INFINI Labs"]
edition = "2021"
@@ -89,3 +89,4 @@ strip = true # Ensures debug symbols are removed.
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-autostart = "^2.2"
tauri-plugin-global-shortcut = "2"
tauri-plugin-updater = "2"

View File

@@ -31,5 +31,12 @@
</array>
</dict>
</array>
<key>NSMicrophoneUsageDescription</key>
<string>Coco AI needs access to your microphone for voice input and audio recording features.</string>
<key>NSCameraUsageDescription</key>
<string>Coco AI requires camera access for scanning documents and capturing images.</string>
<key>NSSpeechRecognitionUsageDescription</key>
<string>Coco AI uses speech recognition to convert your voice into text for a hands-free experience.</string>
</dict>
</plist>

View File

@@ -67,6 +67,7 @@
"macos-permissions:default",
"screenshots:default",
"core:window:allow-set-theme",
"process:default"
"process:default",
"updater:default"
]
}

View File

@@ -9,6 +9,7 @@
"autostart:allow-enable",
"autostart:allow-disable",
"autostart:allow-is-enabled",
"global-shortcut:default"
"global-shortcut:default",
"updater:default"
]
}

View File

@@ -29,6 +29,11 @@ pub struct AuthProvider {
pub sso: Sso,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MinimalClientVersion {
number: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Server {
#[serde(default = "default_empty_string")] // Custom default function for empty string
@@ -39,12 +44,13 @@ pub struct Server {
pub endpoint: String,
pub provider: Provider,
pub version: Version,
pub minimal_client_version: Option<MinimalClientVersion>,
pub updated: String,
#[serde(default = "default_enabled_type")]
pub enabled: bool,
#[serde(default = "default_bool_type")]
pub public: bool,
#[serde(default = "default_available_type")]
pub available: bool,
@@ -70,7 +76,6 @@ impl Hash for Server {
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerAccessToken {
#[serde(default = "default_empty_string")] // Custom default function for empty string
@@ -104,11 +109,11 @@ impl Hash for ServerAccessToken {
}
fn default_empty_string() -> String {
"".to_string() // Default to empty string if not provided
"".to_string() // Default to empty string if not provided
}
fn default_bool_type() -> bool {
false // Default to false if not provided
false // Default to false if not provided
}
fn default_enabled_type() -> bool {
@@ -123,4 +128,4 @@ fn default_priority_type() -> u32 {
}
fn default_user_profile_type() -> Option<UserProfile> {
None
}
}

View File

@@ -11,12 +11,9 @@ mod util;
use crate::common::register::SearchSourceRegistry;
// use crate::common::traits::SearchSource;
use crate::common::{MAIN_WINDOW_LABEL, SETTINGS_WINDOW_LABEL};
use crate::server::search::CocoSearchSource;
use crate::server::servers::{load_or_insert_default_server, load_servers_token};
use autostart::{change_autostart, enable_autostart};
use lazy_static::lazy_static;
use reqwest::Client;
use std::path::PathBuf;
use std::sync::Mutex;
#[cfg(target_os = "macos")]
use tauri::ActivationPolicy;
@@ -83,7 +80,8 @@ pub fn run() {
.plugin(tauri_plugin_fs_pro::init())
.plugin(tauri_plugin_macos_permissions::init())
.plugin(tauri_plugin_screenshots::init())
.plugin(tauri_plugin_process::init());
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_updater::Builder::new().build());
// Conditional compilation for macOS
#[cfg(target_os = "macos")]
@@ -226,8 +224,8 @@ pub async fn init<R: Runtime>(app_handle: &AppHandle<R>) {
let registry: State<SearchSourceRegistry> = app_handle.state::<SearchSourceRegistry>();
for server in coco_servers {
let source = CocoSearchSource::new(server.clone(), Client::new());
registry.register_source(source).await;
crate::server::servers::try_register_server_to_search_source(app_handle.clone(), &server)
.await;
}
}

View File

@@ -1,13 +1,11 @@
use crate::common::auth::RequestAccessTokenResponse;
use crate::common::register::SearchSourceRegistry;
use crate::common::server::ServerAccessToken;
use crate::server::http_client::HttpClient;
use crate::server::profile::get_user_profiles;
use crate::server::search::CocoSearchSource;
use crate::server::servers::{get_server_by_id, persist_servers, persist_servers_token, save_access_token, save_server};
use reqwest::{Client, StatusCode};
use crate::server::servers::{get_server_by_id, persist_servers, persist_servers_token, save_access_token, save_server, try_register_server_to_search_source};
use reqwest::StatusCode;
use std::collections::HashMap;
use tauri::{AppHandle, Manager, Runtime};
use tauri::{AppHandle, Runtime};
fn request_access_token_url(request_id: &str) -> String {
// Remove the endpoint part and keep just the path for the request
format!("/auth/request_access_token?request_id={}", request_id)
@@ -54,9 +52,8 @@ pub async fn handle_sso_callback<R: Runtime>(
save_access_token(server_id.clone(), access_token);
persist_servers_token(&app_handle)?;
let registry = app_handle.state::<SearchSourceRegistry>();
let source = CocoSearchSource::new(server.clone(), Client::new());
registry.register_source(source).await;
// Register the server to the search source
try_register_server_to_search_source(app_handle.clone(), &server).await;
// Update the server's profile using the util::http::HttpClient::get method
let profile = get_user_profiles(app_handle.clone(), server_id.clone()).await;

View File

@@ -132,6 +132,7 @@ fn get_default_server() -> Server {
version: Version {
number: "1.0.0_SNAPSHOT".to_string(),
},
minimal_client_version: None,
updated: "2025-01-24T12:12:17.326286927+08:00".to_string(),
public: false,
available: true,
@@ -259,7 +260,6 @@ pub async fn load_or_insert_default_server<R: Runtime>(
pub async fn list_coco_servers<R: Runtime>(
_app_handle: AppHandle<R>,
) -> Result<Vec<Server>, String> {
//hard fresh all server's info, in order to get the actual health
refresh_all_coco_server_info(_app_handle.clone()).await;
@@ -282,9 +282,7 @@ pub const COCO_SERVERS: &str = "coco_servers";
const COCO_SERVER_TOKENS: &str = "coco_server_tokens";
pub async fn refresh_all_coco_server_info<R: Runtime>(
app_handle: AppHandle<R>,
) {
pub async fn refresh_all_coco_server_info<R: Runtime>(app_handle: AppHandle<R>) {
let servers = get_all_servers();
for server in servers {
let _ = refresh_coco_server_info(app_handle.clone(), server.id.clone()).await;
@@ -334,7 +332,6 @@ pub async fn refresh_coco_server_info<R: Runtime>(
let _ = get_datasources_by_server(&id).await;
Ok(server)
}
Err(e) => Err(format!("Failed to deserialize the response: {:?}", e)),
@@ -407,9 +404,8 @@ pub async fn add_coco_server<R: Runtime>(
// Save the new server to the cache
save_server(&server);
let registry = app_handle.state::<SearchSourceRegistry>();
let source = CocoSearchSource::new(server.clone(), Client::new());
registry.register_source(source).await;
// Register the server to the search source
try_register_server_to_search_source(app_handle.clone(), &server).await;
// Persist the servers to the store
persist_servers(&app_handle)
@@ -459,9 +455,8 @@ pub async fn enable_server<R: Runtime>(app_handle: AppHandle<R>, id: String) ->
server.enabled = true;
save_server(&server);
let registry = app_handle.state::<SearchSourceRegistry>();
let source = CocoSearchSource::new(server.clone(), Client::new());
registry.register_source(source).await;
// Register the server to the search source
try_register_server_to_search_source(app_handle.clone(), &server).await;
persist_servers(&app_handle)
.await
@@ -470,6 +465,16 @@ pub async fn enable_server<R: Runtime>(app_handle: AppHandle<R>, id: String) ->
Ok(())
}
pub async fn try_register_server_to_search_source(
app_handle: AppHandle<impl Runtime>,
server: &Server,
) {
if server.enabled {
let registry = app_handle.state::<SearchSourceRegistry>();
let source = CocoSearchSource::new(server.clone(), Client::new());
registry.register_source(source).await;
}
}
pub async fn mark_server_as_offline(id: &str) {
// println!("server_is_offline: {}", id);
@@ -584,6 +589,7 @@ fn test_trim_endpoint_last_forward_slash() {
version: Version {
number: "".to_string(),
},
minimal_client_version: None,
updated: "".to_string(),
public: false,
available: false,

View File

@@ -113,7 +113,7 @@
"updater": {
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDlDRjNDRUU0NTdBMzdCRTMKUldUamU2Tlg1TTd6bkUwZWM0d2Zjdk0wdXJmendWVlpMMmhKN25EcmprYmIydnJ3dmFUME9QYXkK",
"endpoints": [
"https://api.coco.rs/update/{{target}}/{{arch}}/{{current_version}}"
"https://release.infinilabs.com/coco/app/.latest.json?target={{target}}&arch={{arch}}&current_version={{current_version}}"
]
},
"websocket": {},

View File

@@ -4,27 +4,23 @@ import {
useCallback,
useEffect,
useImperativeHandle,
useMemo,
useRef,
useState,
} from "react";
import { invoke, isTauri } from "@tauri-apps/api/core";
import { useTranslation } from "react-i18next";
import { debounce } from "lodash-es";
import { ChatMessage } from "@/components/ChatMessage";
import type { Chat } from "./types";
import { useChatStore } from "@/stores/chatStore";
import { useWindows } from "@/hooks/useWindows";
import { ChatHeader } from "./ChatHeader";
import { Sidebar } from "@/components/Assistant/Sidebar";
import { useConnectStore } from "@/stores/connectStore";
import { useSearchStore } from "@/stores/searchStore";
import FileList from "@/components/Search/FileList";
import { Greetings } from "./Greetings";
import ConnectPrompt from "./ConnectPrompt";
import { useWindows } from "@/hooks/useWindows";
import useMessageChunkData from "@/hooks/useMessageChunkData";
import useWebSocket from "@/hooks/useWebSocket";
import { useChatActions } from "@/hooks/useChatActions";
import { useMessageHandler } from "@/hooks/useMessageHandler";
import { ChatSidebar } from "./ChatSidebar";
import { ChatHeader } from "./ChatHeader";
import { ChatContent } from "./ChatContent";
import ConnectPrompt from "./ConnectPrompt";
import type { Chat } from "./types";
interface ChatAIProps {
isTransitioned: boolean;
@@ -63,17 +59,13 @@ const ChatAI = memo(
) => {
if (!isTransitioned) return null;
const { t } = useTranslation();
useImperativeHandle(ref, () => ({
init: init,
cancelChat: cancelChat,
cancelChat: () => cancelChat(activeChat),
reconnect: reconnect,
clearChat: clearChat,
}));
const { createWin } = useWindows();
const { curChatEnd, setCurChatEnd, connected, setConnected } =
useChatStore();
@@ -81,23 +73,18 @@ const ChatAI = memo(
const [activeChat, setActiveChat] = useState<Chat>();
const [timedoutShow, setTimedoutShow] = useState(false);
const [IsLogin, setIsLogin] = useState(true);
const messagesEndRef = useRef<HTMLDivElement>(null);
const [isLogin, setIsLogin] = useState(true);
const curIdRef = useRef("");
const [isSidebarOpenChat, setIsSidebarOpenChat] = useState(isSidebarOpen);
const [chats, setChats] = useState<Chat[]>([]);
const sourceDataIds = useSearchStore((state) => state.sourceDataIds);
const uploadFiles = useChatStore((state) => state.uploadFiles);
useEffect(() => {
activeChatProp && setActiveChat(activeChatProp);
}, [activeChatProp]);
const messageTimeoutRef = useRef<NodeJS.Timeout>();
const [Question, setQuestion] = useState<string>("");
const {
@@ -122,385 +109,125 @@ const ChatAI = memo(
response: false,
});
const dealMsg = useCallback(
(msg: string) => {
if (messageTimeoutRef.current) {
clearTimeout(messageTimeoutRef.current);
}
const dealMsgRef = useRef<((msg: string) => void) | null>(null);
if (!msg.includes("PRIVATE")) return;
const { errorShow, setErrorShow, reconnect, updateDealMsg } =
useWebSocket({
connected,
setConnected,
currentService,
dealMsgRef,
});
messageTimeoutRef.current = setTimeout(() => {
if (!curChatEnd) {
console.log("AI response timeout");
setTimedoutShow(true);
cancelChat();
}
}, 60000);
if (msg.includes("assistant finished output")) {
clearTimeout(messageTimeoutRef.current);
console.log("AI finished output");
setCurChatEnd(true);
return;
}
const cleanedData = msg.replace(/^PRIVATE /, "");
try {
const chunkData = JSON.parse(cleanedData);
if (chunkData.reply_to_message !== curIdRef.current) return;
setLoadingStep(() => ({
query_intent: false,
fetch_source: false,
pick_source: false,
deep_read: false,
think: false,
response: false,
[chunkData.chunk_type]: true,
}));
// ['query_intent', 'fetch_source', 'pick_source', 'deep_read', 'think', 'response'];
if (chunkData.chunk_type === "query_intent") {
handlers.deal_query_intent(chunkData);
} else if (chunkData.chunk_type === "fetch_source") {
handlers.deal_fetch_source(chunkData);
} else if (chunkData.chunk_type === "pick_source") {
handlers.deal_pick_source(chunkData);
} else if (chunkData.chunk_type === "deep_read") {
handlers.deal_deep_read(chunkData);
} else if (chunkData.chunk_type === "think") {
handlers.deal_think(chunkData);
} else if (chunkData.chunk_type === "response") {
handlers.deal_response(chunkData);
}
} catch (error) {
console.error("parse error:", error);
}
},
[curChatEnd]
const {
chatClose,
cancelChat,
chatHistory,
createNewChat,
handleSendMessage,
openSessionChat,
getChatHistory,
createChatWindow,
} = useChatActions(
currentService?.id,
setActiveChat,
setCurChatEnd,
setErrorShow,
setTimedoutShow,
clearAllChunkData,
setQuestion,
curIdRef,
isSearchActive,
isDeepThinkActive,
sourceDataIds,
changeInput
);
const { errorShow, setErrorShow, reconnect } = useWebSocket({
connected,
setConnected,
currentService,
dealMsg,
});
const updatedChat = useMemo(() => {
if (!activeChat?._id) return null;
return {
...activeChat,
messages: [...(activeChat.messages || [])],
};
}, [activeChat]);
const simulateAssistantResponse = useCallback(() => {
if (!updatedChat) return;
// console.log("updatedChat:", updatedChat);
setActiveChat(updatedChat);
}, [updatedChat]);
const { dealMsg, messageTimeoutRef } = useMessageHandler(
curIdRef,
setCurChatEnd,
setTimedoutShow,
(chat) => cancelChat(chat || activeChat),
setLoadingStep,
handlers
);
useEffect(() => {
if (curChatEnd) {
simulateAssistantResponse();
if (dealMsg) {
dealMsgRef.current = dealMsg;
updateDealMsg && updateDealMsg(dealMsg);
}
}, [curChatEnd]);
}, [dealMsg, updateDealMsg]);
const [userScrolling, setUserScrolling] = useState(false);
const scrollTimeoutRef = useRef<NodeJS.Timeout>();
const scrollToBottom = useCallback(
debounce(() => {
if (!userScrolling) {
const container = messagesEndRef.current?.parentElement;
if (container) {
container.scrollTo({
top: container.scrollHeight,
behavior: "smooth",
});
}
}
}, 100),
[userScrolling]
);
useEffect(() => {
const container = messagesEndRef.current?.parentElement;
if (!container) return;
const handleScroll = () => {
if (scrollTimeoutRef.current) {
clearTimeout(scrollTimeoutRef.current);
}
const { scrollTop, scrollHeight, clientHeight } = container;
const isAtBottom =
Math.abs(scrollHeight - scrollTop - clientHeight) < 10;
setUserScrolling(!isAtBottom);
if (isAtBottom) {
setUserScrolling(false);
}
scrollTimeoutRef.current = setTimeout(() => {
const {
scrollTop: newScrollTop,
scrollHeight: newScrollHeight,
clientHeight: newClientHeight,
} = container;
const nowAtBottom =
Math.abs(newScrollHeight - newScrollTop - newClientHeight) < 10;
if (nowAtBottom) {
setUserScrolling(false);
}
}, 500);
};
container.addEventListener("scroll", handleScroll);
return () => {
container.removeEventListener("scroll", handleScroll);
if (scrollTimeoutRef.current) {
clearTimeout(scrollTimeoutRef.current);
}
};
}, []);
useEffect(() => {
scrollToBottom();
}, [
activeChat?.messages,
query_intent?.message_chunk,
fetch_source?.message_chunk,
pick_source?.message_chunk,
deep_read?.message_chunk,
think?.message_chunk,
response?.message_chunk,
]);
const clearChat = () => {
const clearChat = useCallback(() => {
console.log("clearChat");
chatClose();
setTimedoutShow(false);
setErrorShow(false);
chatClose(activeChat);
setActiveChat(undefined);
setCurChatEnd(true);
clearChatPage && clearChatPage();
};
}, [
activeChat,
chatClose,
clearChatPage,
setCurChatEnd,
setErrorShow,
setTimedoutShow,
]);
const createNewChat = useCallback(
async (value: string = "") => {
setTimedoutShow(false);
setErrorShow(false);
chatClose();
clearAllChunkData();
setQuestion(value);
try {
console.log("sourceDataIds", sourceDataIds);
let response: any = await invoke("new_chat", {
serverId: currentService?.id,
message: value,
queryParams: {
search: isSearchActive,
deep_thinking: isDeepThinkActive,
datasource: sourceDataIds.join(","),
},
});
console.log("_new", response);
const newChat: Chat = response;
curIdRef.current = response?.payload?.id;
newChat._source = {
message: value,
};
const updatedChat: Chat = {
...newChat,
messages: [newChat],
};
changeInput && changeInput("");
//console.log("updatedChat2", updatedChat);
setActiveChat(updatedChat);
setCurChatEnd(false);
} catch (error) {
setErrorShow(true);
console.error("createNewChat:", error);
const init = useCallback(
(value: string) => {
if (!isLogin) return;
if (!curChatEnd) return;
if (!activeChat?._id) {
createNewChat(value, activeChat);
} else {
handleSendMessage(value, activeChat);
}
},
[currentService?.id, sourceDataIds, isSearchActive, isDeepThinkActive]
[isLogin, curChatEnd, activeChat, createNewChat, handleSendMessage]
);
const init = (value: string) => {
if (!IsLogin) return;
if (!curChatEnd) return;
if (!activeChat?._id) {
createNewChat(value);
} else {
handleSendMessage(value);
}
};
const sendMessage = useCallback(
async (content: string, newChat: Chat) => {
if (!newChat?._id || !content) return;
try {
//console.log("sourceDataIds", sourceDataIds);
let response: any = await invoke("send_message", {
serverId: currentService?.id,
sessionId: newChat?._id,
queryParams: {
search: isSearchActive,
deep_thinking: isDeepThinkActive,
datasource: sourceDataIds.join(","),
},
message: content,
});
response = JSON.parse(response || "");
console.log("_send", response);
curIdRef.current = response[0]?._id;
const updatedChat: Chat = {
...newChat,
messages: [...(newChat?.messages || []), ...(response || [])],
};
changeInput && changeInput("");
//console.log("updatedChat2", updatedChat);
setActiveChat(updatedChat);
setCurChatEnd(false);
} catch (error) {
setErrorShow(true);
console.error("sendMessage:", error);
}
},
[
JSON.stringify(activeChat?.messages),
currentService?.id,
sourceDataIds,
isSearchActive,
isDeepThinkActive,
]
);
const handleSendMessage = useCallback(
async (content: string, newChat?: Chat) => {
newChat = newChat || activeChat;
if (!newChat?._id || !content) return;
setQuestion(content);
await chatHistory(newChat, (chat) => sendMessage(content, chat));
setTimedoutShow(false);
setErrorShow(false);
clearAllChunkData();
},
[activeChat, sendMessage]
);
const chatClose = async () => {
if (!activeChat?._id) return;
try {
let response: any = await invoke("close_session_chat", {
serverId: currentService?.id,
sessionId: activeChat?._id,
});
response = JSON.parse(response || "");
console.log("_close", response);
} catch (error) {
console.error("chatClose:", error);
}
};
const cancelChat = async () => {
setCurChatEnd(true);
if (!activeChat?._id) return;
try {
let response: any = await invoke("cancel_session_chat", {
serverId: currentService?.id,
sessionId: activeChat?._id,
});
response = JSON.parse(response || "");
console.log("_cancel", response);
} catch (error) {
console.error("cancelChat:", error);
}
};
async function openChatAI() {
if (isTauri()) {
createWin &&
createWin({
label: "chat",
title: "Coco Chat",
dragDropEnabled: true,
center: true,
width: 1000,
height: 800,
alwaysOnTop: false,
skipTaskbar: false,
decorations: true,
closable: true,
url: "/ui/chat",
});
}
}
const { createWin } = useWindows();
const openChatAI = useCallback(() => {
createChatWindow(createWin);
}, [createChatWindow, createWin]);
useEffect(() => {
return () => {
if (messageTimeoutRef.current) {
clearTimeout(messageTimeoutRef.current);
}
chatClose();
chatClose(activeChat);
setActiveChat(undefined);
setCurChatEnd(true);
scrollToBottom.cancel();
};
}, []);
}, [chatClose, setCurChatEnd]);
const chatHistory = async (
chat: Chat,
callback?: (chat: Chat) => void
) => {
try {
let response: any = await invoke("session_chat_history", {
serverId: currentService?.id,
sessionId: chat?._id,
from: 0,
size: 20,
});
response = JSON.parse(response || "");
const hits = response?.hits?.hits || [];
const updatedChat: Chat = {
...chat,
messages: hits,
};
console.log("id_history", response, updatedChat);
setActiveChat(updatedChat);
callback && callback(updatedChat);
} catch (error) {
console.error("chatHistory:", error);
}
};
const onSelectChat = useCallback(
async (chat: Chat) => {
setTimedoutShow(false);
setErrorShow(false);
clearAllChunkData();
await cancelChat(activeChat);
await chatClose(activeChat);
const response = await openSessionChat(chat);
if (response) {
chatHistory(response);
}
},
[
clearAllChunkData,
cancelChat,
activeChat,
chatClose,
openSessionChat,
chatHistory,
]
);
const onSelectChat = async (chat: any) => {
chatClose();
clearAllChunkData();
try {
let response: any = await invoke("open_session_chat", {
serverId: currentService?.id,
sessionId: chat?._id,
});
response = JSON.parse(response || "");
console.log("_open", response);
chatHistory(response);
} catch (error) {
console.error("open_session_chat:", error);
}
};
const deleteChat = (chatId: string) => {
const deleteChat = useCallback((chatId: string) => {
setChats((prev) => prev.filter((chat) => chat._id !== chatId));
if (activeChat?._id === chatId) {
const remainingChats = chats.filter((chat) => chat._id !== chatId);
@@ -510,7 +237,7 @@ const ChatAI = memo(
init("");
}
}
};
}, [activeChat, chats, init, setActiveChat]);
const handleOutsideClick = useCallback((e: MouseEvent) => {
const sidebar = document.querySelector("[data-sidebar]");
@@ -534,152 +261,69 @@ const ChatAI = memo(
};
}, [isSidebarOpenChat, handleOutsideClick]);
const getChatHistory = useCallback(async () => {
if (!currentService?.id) return;
try {
let response: any = await invoke("chat_history", {
serverId: currentService?.id,
from: 0,
size: 20,
});
response = JSON.parse(response || "");
console.log("_history", response);
const hits = response?.hits?.hits || [];
setChats(hits);
} catch (error) {
console.error("chat_history:", error);
}
}, [currentService?.id]);
const fetchChatHistory = useCallback(async () => {
const hits = await getChatHistory();
setChats(hits);
}, [getChatHistory]);
const setIsLoginChat = useCallback(
(value: boolean) => {
setIsLogin(value);
value && currentService && !setIsSidebarOpen && getChatHistory();
value && currentService && !setIsSidebarOpen && fetchChatHistory();
!value && setChats([]);
},
[currentService]
[currentService, setIsSidebarOpen, fetchChatHistory]
);
const toggleSidebar = useCallback(() => {
setIsSidebarOpenChat(!isSidebarOpenChat);
setIsSidebarOpen && setIsSidebarOpen(!isSidebarOpenChat);
!isSidebarOpenChat && fetchChatHistory();
}, [isSidebarOpenChat, setIsSidebarOpen, fetchChatHistory]);
return (
<div
data-tauri-drag-region
className={`h-full flex flex-col rounded-xl overflow-hidden`}
>
{setIsSidebarOpen ? null : (
<div
data-sidebar
className={`fixed inset-y-0 left-0 z-50 w-64 transform transition-all duration-300 ease-in-out
${
isSidebarOpenChat
? "translate-x-0"
: "-translate-x-[calc(100%)]"
}
md:relative md:translate-x-0 bg-gray-100 dark:bg-gray-800
border-r border-gray-200 dark:border-gray-700 rounded-tl-xl rounded-bl-xl
overflow-hidden`}
>
<Sidebar
chats={chats}
activeChat={activeChat}
onNewChat={clearChat}
onSelectChat={onSelectChat}
onDeleteChat={deleteChat}
/>
</div>
{!setIsSidebarOpen && (
<ChatSidebar
isSidebarOpen={isSidebarOpenChat}
chats={chats}
activeChat={activeChat}
onNewChat={clearChat}
onSelectChat={onSelectChat}
onDeleteChat={deleteChat}
fetchChatHistory={fetchChatHistory}
/>
)}
<ChatHeader
onCreateNewChat={clearChat}
onOpenChatAI={openChatAI}
setIsSidebarOpen={() => {
setIsSidebarOpenChat(!isSidebarOpenChat);
setIsSidebarOpen && setIsSidebarOpen(!isSidebarOpenChat);
!isSidebarOpenChat && getChatHistory();
}}
setIsSidebarOpen={toggleSidebar}
isSidebarOpen={isSidebarOpenChat}
activeChat={activeChat}
reconnect={reconnect}
isChatPage={isChatPage}
setIsLogin={setIsLoginChat}
/>
{IsLogin ? (
<div className="flex flex-col h-full justify-between overflow-hidden">
<div 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">
<Greetings />
{activeChat?.messages?.map((message, index) => (
<ChatMessage
key={message._id + index}
message={message}
isTyping={false}
onResend={handleSendMessage}
/>
))}
{(query_intent ||
fetch_source ||
pick_source ||
deep_read ||
think ||
response) &&
activeChat?._id ? (
<ChatMessage
key={"current"}
message={{
_id: "current",
_source: {
type: "assistant",
message: "",
question: Question,
},
}}
onResend={handleSendMessage}
isTyping={!curChatEnd}
query_intent={query_intent}
fetch_source={fetch_source}
pick_source={pick_source}
deep_read={deep_read}
think={think}
response={response}
loadingStep={loadingStep}
/>
) : null}
{timedoutShow ? (
<ChatMessage
key={"timedout"}
message={{
_id: "timedout",
_source: {
type: "assistant",
message: t("assistant.chat.timedout"),
question: Question,
},
}}
onResend={handleSendMessage}
isTyping={false}
/>
) : null}
{errorShow ? (
<ChatMessage
key={"error"}
message={{
_id: "error",
_source: {
type: "assistant",
message: t("assistant.chat.error"),
question: Question,
},
}}
onResend={handleSendMessage}
isTyping={false}
/>
) : null}
<div ref={messagesEndRef} />
</div>
{uploadFiles.length > 0 && (
<div className="max-h-[120px] overflow-auto p-2">
<FileList />
</div>
)}
</div>
{isLogin ? (
<ChatContent
activeChat={activeChat}
curChatEnd={curChatEnd}
query_intent={query_intent}
fetch_source={fetch_source}
pick_source={pick_source}
deep_read={deep_read}
think={think}
response={response}
loadingStep={loadingStep}
timedoutShow={timedoutShow}
errorShow={errorShow}
Question={Question}
handleSendMessage={(value) => handleSendMessage(value, activeChat)}
/>
) : (
<ConnectPrompt />
)}

View File

@@ -0,0 +1,151 @@
import { useRef, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { ChatMessage } from "@/components/ChatMessage";
import { Greetings } from "./Greetings";
import FileList from "@/components/Search/FileList";
import { useChatScroll } from "@/hooks/useChatScroll";
import { useChatStore } from "@/stores/chatStore";
import type { Chat, IChunkData } from "./types";
interface ChatContentProps {
activeChat?: Chat;
curChatEnd: boolean;
query_intent?: IChunkData;
fetch_source?: IChunkData;
pick_source?: IChunkData;
deep_read?: IChunkData;
think?: IChunkData;
response?: IChunkData;
loadingStep?: Record<string, boolean>;
timedoutShow: boolean;
errorShow: boolean;
Question: string;
handleSendMessage: (content: string, newChat?: Chat) => void;
}
export const ChatContent = ({
activeChat,
curChatEnd,
query_intent,
fetch_source,
pick_source,
deep_read,
think,
response,
loadingStep,
timedoutShow,
errorShow,
Question,
handleSendMessage,
}: ChatContentProps) => {
const { t } = useTranslation();
const uploadFiles = useChatStore((state) => state.uploadFiles);
const messagesEndRef = useRef<HTMLDivElement>(null);
const { scrollToBottom } = useChatScroll(messagesEndRef);
useEffect(() => {
scrollToBottom();
}, [
activeChat?.messages,
query_intent?.message_chunk,
fetch_source?.message_chunk,
pick_source?.message_chunk,
deep_read?.message_chunk,
think?.message_chunk,
response?.message_chunk,
curChatEnd,
]);
useEffect(() => {
return () => {
scrollToBottom.cancel();
};
}, [scrollToBottom]);
return (
<div className="flex flex-col h-full justify-between overflow-hidden">
<div 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">
<Greetings />
{activeChat?.messages?.map((message, index) => (
<ChatMessage
key={message._id + index}
message={message}
isTyping={false}
onResend={handleSendMessage}
/>
))}
{(!curChatEnd ||
query_intent ||
fetch_source ||
pick_source ||
deep_read ||
think ||
response) &&
activeChat?._id ? (
<ChatMessage
key={"current"}
message={{
_id: "current",
_source: {
type: "assistant",
message: "",
question: Question,
},
}}
onResend={handleSendMessage}
isTyping={!curChatEnd}
query_intent={query_intent}
fetch_source={fetch_source}
pick_source={pick_source}
deep_read={deep_read}
think={think}
response={response}
loadingStep={loadingStep}
/>
) : null}
{timedoutShow ? (
<ChatMessage
key={"timedout"}
message={{
_id: "timedout",
_source: {
type: "assistant",
message: t("assistant.chat.timedout"),
question: Question,
},
}}
onResend={handleSendMessage}
isTyping={false}
/>
) : null}
{errorShow ? (
<ChatMessage
key={"error"}
message={{
_id: "error",
_source: {
type: "assistant",
message: t("assistant.chat.error"),
question: Question,
},
}}
onResend={handleSendMessage}
isTyping={false}
/>
) : null}
<div ref={messagesEndRef} />
</div>
{uploadFiles.length > 0 && (
<div className="max-h-[120px] overflow-auto p-2">
<FileList />
</div>
)}
</div>
);
};

View File

@@ -0,0 +1,44 @@
import React from "react";
import { Sidebar } from "@/components/Assistant/Sidebar";
import type { Chat } from "./types";
interface ChatSidebarProps {
isSidebarOpen: boolean;
chats: Chat[];
activeChat?: Chat;
onNewChat: () => void;
onSelectChat: (chat: any) => void;
onDeleteChat: (chatId: string) => void;
fetchChatHistory: () => void;
}
export const ChatSidebar: React.FC<ChatSidebarProps> = ({
isSidebarOpen,
chats,
activeChat,
onNewChat,
onSelectChat,
onDeleteChat,
fetchChatHistory,
}) => {
return (
<div
data-sidebar
className={`fixed inset-y-0 left-0 z-50 w-64 transform transition-all duration-300 ease-in-out
${isSidebarOpen ? "translate-x-0" : "-translate-x-[calc(100%)]"}
md:relative md:translate-x-0 bg-gray-100 dark:bg-gray-800
border-r border-gray-200 dark:border-gray-700 rounded-tl-xl rounded-bl-xl
overflow-hidden`}
>
<Sidebar
chats={chats}
activeChat={activeChat}
onNewChat={onNewChat}
onSelectChat={onSelectChat}
onDeleteChat={onDeleteChat}
fetchChatHistory={fetchChatHistory}
/>
</div>
);
};

View File

@@ -1,5 +1,6 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { MessageSquare, Plus } from "lucide-react";
import { MessageSquare, Plus, RefreshCw } from "lucide-react";
import type { Chat } from "./types";
@@ -10,6 +11,7 @@ interface SidebarProps {
onSelectChat: (chat: Chat) => void;
onDeleteChat: (chatId: string) => void;
className?: string;
fetchChatHistory: () => void;
}
export function Sidebar({
@@ -18,19 +20,37 @@ export function Sidebar({
onNewChat,
onSelectChat,
className = "",
fetchChatHistory,
}: SidebarProps) {
const { t } = useTranslation();
const [isRefreshing, setIsRefreshing] = useState(false);
return (
<div className={`h-full flex flex-col ${className}`}>
<div className="p-4">
<div className="flex justify-between gap-1 p-4">
<button
onClick={onNewChat}
className={`w-full flex items-center gap-3 px-4 py-3 text-sm font-medium rounded-xl transition-all border border-[#E6E6E6] dark:border-[#272626] text-gray-700 hover:bg-gray-50/80 active:bg-gray-100/80 dark:text-white dark:hover:bg-gray-600/50 dark:active:bg-gray-500/50`}
className={`flex items-center gap-3 px-4 py-3 text-sm font-medium rounded-xl transition-all border border-[#E6E6E6] dark:border-[#272626] text-gray-700 hover:bg-gray-50/80 active:bg-gray-100/80 dark:text-white dark:hover:bg-gray-600/50 dark:active:bg-gray-500/50`}
>
<Plus className={`h-4 w-4 text-[#0072FF] dark:text-[#0072FF]`} />
{t("assistant.sidebar.newChat")}
</button>
<button
onClick={async () => {
setIsRefreshing(true);
fetchChatHistory();
setTimeout(() => setIsRefreshing(false), 1000);
}}
className="p-1 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-500 dark:text-gray-400"
disabled={isRefreshing}
>
<RefreshCw
className={`h-4 w-4 text-[#0287FF] transition-transform duration-1000 ${
isRefreshing ? "animate-spin" : ""
}`}
/>
</button>
</div>
<div className="flex-1 overflow-y-auto px-3 pb-3 space-y-2 custom-scrollbar">
{chats.map((chat) => (

View File

@@ -9,7 +9,9 @@ import RehypeHighlight from "rehype-highlight";
import mermaid from "mermaid";
import { useDebouncedCallback } from "use-debounce";
import { copyToClipboard, useWindowSize } from "@/utils";
import { copyToClipboard,
// useWindowSize
} from "@/utils";
import "./markdown.css";
import "./highlight.css";
@@ -67,9 +69,9 @@ function PreCode(props: { children?: any }) {
const ref = useRef<HTMLPreElement>(null);
// const previewRef = useRef<HTMLPreviewHander>(null);
const [mermaidCode, setMermaidCode] = useState("");
const [htmlCode, setHtmlCode] = useState("");
const { height } = useWindowSize();
console.log(htmlCode, height);
// const [htmlCode, setHtmlCode] = useState("");
// const { height } = useWindowSize();
// console.log(htmlCode, height);
const renderArtifacts = useDebouncedCallback(() => {
if (!ref.current) return;
@@ -77,17 +79,17 @@ function PreCode(props: { children?: any }) {
if (mermaidDom) {
setMermaidCode((mermaidDom as HTMLElement).innerText);
}
const htmlDom = ref.current.querySelector("code.language-html");
const refText = ref.current.querySelector("code")?.innerText;
if (htmlDom) {
setHtmlCode((htmlDom as HTMLElement).innerText);
} else if (refText?.startsWith("<!DOCTYPE")) {
setHtmlCode(refText);
}
// const htmlDom = ref.current.querySelector("code.language-html");
// const refText = ref.current.querySelector("code")?.innerText;
// if (htmlDom) {
// setHtmlCode((htmlDom as HTMLElement).innerText);
// } else if (refText?.startsWith("<!DOCTYPE")) {
// setHtmlCode(refText);
// }
}, 600);
const enableArtifacts = true;
console.log(enableArtifacts);
// const enableArtifacts = true;
// console.log(enableArtifacts);
//Wrap the paragraph for plain-text
useEffect(() => {

View File

@@ -36,7 +36,7 @@ export const PickSource = ({
useEffect(() => {
if (!ChunkData?.message_chunk) return;
if (loading) {
if (!loading) {
try {
const cleanContent = ChunkData.message_chunk.replace(/^"|"$/g, "");
const allMatches = cleanContent.match(/<JSON>([\s\S]*?)<\/JSON>/g);
@@ -44,7 +44,7 @@ export const PickSource = ({
if (allMatches) {
for (let i = allMatches.length - 1; i >= 0; i--) {
try {
const jsonString = allMatches[i].replace(/<JSON>|<\/JSON>/g, "");
const jsonString = allMatches[i].replace(/<JSON>|<\/JSON>|<think>|<\/think>/g, "");
const data = JSON.parse(jsonString.trim());
if (

View File

@@ -42,7 +42,7 @@ export const QueryIntent = ({
useEffect(() => {
if (!ChunkData?.message_chunk) return;
if (loading) {
if (!loading) {
const cleanContent = ChunkData.message_chunk.replace(/^"|"$/g, "");
const allMatches = cleanContent.match(/<JSON>([\s\S]*?)<\/JSON>/g);
if (allMatches) {
@@ -108,7 +108,7 @@ export const QueryIntent = ({
<div className="flex flex-wrap gap-1">
{Data?.keyword?.map((keyword, index) => (
<span
key={index}
key={keyword + index}
className="text-[#333333] dark:text-[#D8D8D8]"
>
{keyword}
@@ -144,8 +144,8 @@ export const QueryIntent = ({
- {t("assistant.message.steps.relatedQuestions")}
</span>
<div className="flex-1 flex flex-col text-[#333333] dark:text-[#D8D8D8]">
{Data?.query?.map((question) => (
<span key={question}>- {question}</span>
{Data?.query?.map((question, qIndex) => (
<span key={question + qIndex}>- {question}</span>
))}
</div>
</div>

View File

@@ -0,0 +1,33 @@
import { useState } from "react";
import { CopyButton } from "@/components/Common/CopyButton";
interface UserMessageProps {
messageContent: string;
}
export const UserMessage = ({ messageContent }: UserMessageProps) => {
const [showCopyButton, setShowCopyButton] = useState(false);
return (
<div
className="flex gap-1 items-center"
onMouseEnter={() => setShowCopyButton(true)}
onMouseLeave={() => setShowCopyButton(false)}
>
{showCopyButton && <CopyButton textToCopy={messageContent} />}
<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"
onDoubleClick={(e) => {
const selection = window.getSelection();
const range = document.createRange();
range.selectNodeContents(e.currentTarget);
selection?.removeAllRanges();
selection?.addRange(range);
}}
>
{messageContent}
</div>
</div>
);
};

View File

@@ -11,6 +11,7 @@ import { Think } from "./Think";
import { MessageActions } from "./MessageActions";
import Markdown from "./Markdown";
import { SuggestionList } from "./SuggestionList";
import { UserMessage } from "./UserMessage";
interface ChatMessageProps {
message: Message;
@@ -55,11 +56,7 @@ export const ChatMessage = memo(function ChatMessage({
const renderContent = () => {
if (!isAssistant) {
return (
<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]">
{messageContent}
</div>
);
return <UserMessage messageContent={messageContent} />;
}
return (

View File

@@ -235,7 +235,6 @@
padding: 0;
margin: 24px 0;
background-color: var(--color-border-default);
border: 0;
}
.markdown-body input {

View File

@@ -28,6 +28,7 @@ import { useAppStore } from "@/stores/appStore";
import { useConnectStore } from "@/stores/connectStore";
import bannerImg from "@/assets/images/coco-cloud-banner.jpeg";
import SettingsToggle from "@/components/Settings/SettingsToggle";
import Tooltip from "@/components/Common/Tooltip";
export default function Cloud() {
const { t } = useTranslation();
@@ -312,19 +313,22 @@ export default function Cloud() {
});
};
const enable_coco_server = useCallback(async (enabled: boolean) => {
try {
const command = enabled ? "enable_server" : "disable_server";
const enable_coco_server = useCallback(
async (enabled: boolean) => {
try {
const command = enabled ? "enable_server" : "disable_server";
await invoke(command, { id: currentService?.id });
await invoke(command, { id: currentService?.id });
setCurrentService({ ...currentService, enabled });
setCurrentService({ ...currentService, enabled });
await fetchServers(false);
} catch (error) {
setError(error);
}
}, [currentService?.id]);
await fetchServers(false);
} catch (error) {
setError(error);
}
},
[currentService?.id]
);
return (
<div className="flex bg-gray-50 dark:bg-gray-900">
@@ -346,9 +350,11 @@ export default function Cloud() {
</div>
<div className="flex items-center justify-between mb-4">
<div className="flex items-center space-x-3">
<div className="flex items-center text-gray-900 dark:text-white font-medium">
{currentService?.name}
</div>
<Tooltip content={currentService?.endpoint}>
<div className="flex items-center text-gray-900 dark:text-white font-medium cursor-pointer">
{currentService?.name}
</div>
</Tooltip>
</div>
<div className="flex items-center gap-2">
<SettingsToggle

View File

@@ -0,0 +1,36 @@
import { useState } from "react";
import { Copy, Check } from "lucide-react";
interface CopyButtonProps {
textToCopy: string;
}
export const CopyButton = ({ textToCopy }: CopyButtonProps) => {
const [copied, setCopied] = useState(false);
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(textToCopy);
setCopied(true);
const timerID = setTimeout(() => {
setCopied(false);
clearTimeout(timerID);
}, 2000);
} catch (err) {
console.error("copy error:", err);
}
};
return (
<button
className={`p-1 bg-gray-200 dark:bg-gray-700 rounded`}
onClick={handleCopy}
>
{copied ? (
<Check className="w-4 h-4 text-[#38C200] dark:text-[#38C200]" />
) : (
<Copy className="w-4 h-4 text-gray-600 dark:text-gray-300" />
)}
</button>
);
};

View File

@@ -47,6 +47,10 @@ const ContextMenu = () => {
shortcut: "enter",
clickEvent: () => {
OpenURLWithBrowser(selectedSearchContent?.url);
setVisibleContextMenu(false);
invoke("hide_coco");
},
},
{
@@ -56,6 +60,8 @@ const ContextMenu = () => {
shortcut: isMac ? "meta.l" : "ctrl.l",
clickEvent: () => {
copyToClipboard(selectedSearchContent?.url);
setVisibleContextMenu(false);
},
},
];
@@ -108,7 +114,7 @@ const ContextMenu = () => {
const item = menus.find((item) => item.shortcut === key);
handleClick(item?.clickEvent);
item?.clickEvent();
}
);
@@ -118,14 +124,6 @@ const ContextMenu = () => {
event.stopImmediatePropagation();
});
const handleClick = (clickEvent?: () => void) => {
clickEvent?.();
setVisibleContextMenu(false);
invoke("hide_coco");
};
return (
<>
{visibleContextMenu && (
@@ -165,7 +163,7 @@ const ContextMenu = () => {
onMouseEnter={() => {
state.activeMenuIndex = index;
}}
onClick={() => handleClick(clickEvent)}
onClick={clickEvent}
>
<div className="flex items-center gap-2 text-black/80 dark:text-white/80">
{cloneElement(icon, { className: "size-4" })}

View File

@@ -1,7 +1,6 @@
import React, { useState, useRef, useEffect, useCallback } from "react";
import { useInfiniteScroll } from "ahooks";
import { isTauri, invoke } from "@tauri-apps/api/core";
import { open } from "@tauri-apps/plugin-shell";
import { invoke } from "@tauri-apps/api/core";
import { useTranslation } from "react-i18next";
import { FixedSizeList } from "react-window";
@@ -10,6 +9,7 @@ import { SearchHeader } from "./SearchHeader";
import noDataImg from "@/assets/coconut-tree.png";
import { metaOrCtrlKey } from "@/utils/keyboardUtils";
import SearchListItem from "./SearchListItem";
import { OpenURLWithBrowser } from "@/utils/index";
interface DocumentListProps {
onSelectDocument: (id: string) => void;
@@ -111,18 +111,6 @@ export const DocumentList: React.FC<DocumentListProps> = ({
setIsKeyboardMode(false);
}, [isChatMode, input]);
const handleOpenURL = async (url: string) => {
if (!url) return;
try {
if (isTauri()) {
await open(url);
// console.log("URL opened in default browser");
}
} catch (error) {
console.error("Failed to open URL:", error);
}
};
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
if (!data?.list?.length) return;
@@ -158,7 +146,7 @@ export const DocumentList: React.FC<DocumentListProps> = ({
if (e.key === "Enter" && selectedItem !== null) {
const item = data?.list?.[selectedItem];
if (item?.url) {
handleOpenURL(item?.url);
OpenURLWithBrowser(item?.url);
}
}
},
@@ -211,7 +199,7 @@ export const DocumentList: React.FC<DocumentListProps> = ({
onMouseEnter={() => onMouseEnter(index, item)}
onItemClick={() => {
if (item?.url) {
handleOpenURL(item?.url);
OpenURLWithBrowser(item?.url);
}
}}
showListRight={viewMode === "list"}
@@ -219,7 +207,7 @@ export const DocumentList: React.FC<DocumentListProps> = ({
</div>
);
},
[data, selectedItem, viewMode, onMouseEnter, handleOpenURL]
[data, selectedItem, viewMode, onMouseEnter, OpenURLWithBrowser]
);
return (

View File

@@ -14,7 +14,6 @@ import { OpenURLWithBrowser } from "@/utils/index";
type ISearchData = Record<string, any[]>;
interface DropdownListProps {
selected: (item: any) => void;
suggests: any[];
SearchData: ISearchData;
IsError: boolean;
@@ -23,7 +22,6 @@ interface DropdownListProps {
}
function DropdownList({
selected,
suggests,
SearchData,
IsError,
@@ -110,8 +108,6 @@ function DropdownList({
const item = globalItemIndexMap[selectedItem];
if (item?.url) {
OpenURLWithBrowser(item?.url);
} else {
selected(item);
}
}
@@ -120,12 +116,10 @@ function DropdownList({
const item = globalItemIndexMap[parseInt(e.key, 10)];
if (item?.url) {
OpenURLWithBrowser(item?.url);
} else {
selected(item);
}
}
},
[suggests, selectedItem, showIndex, selected, globalItemIndexMap]
[suggests, selectedItem, showIndex, globalItemIndexMap]
);
const handleKeyUp = useCallback((e: KeyboardEvent) => {
@@ -158,7 +152,6 @@ function DropdownList({
function goToTwoPage(item: any) {
setSourceData(item);
selected && selected(item);
}
return (
@@ -219,8 +212,6 @@ function DropdownList({
onItemClick={() => {
if (item?.url) {
OpenURLWithBrowser(item?.url);
} else {
selected(item);
}
}}
goToTwoPage={goToTwoPage}

View File

@@ -10,10 +10,10 @@ import { useAppStore } from "@/stores/appStore";
import { isMac } from "@/utils/platform";
import PinOffIcon from "@/icons/PinOff";
import PinIcon from "@/icons/Pin";
import { useUpdateStore } from "@/stores/updateStore";
import clsx from "clsx";
interface FooterProps {
isChat: boolean;
name?: string;
}
export default function Footer({}: FooterProps) {
@@ -22,6 +22,8 @@ export default function Footer({}: FooterProps) {
const isPinned = useAppStore((state) => state.isPinned);
const setIsPinned = useAppStore((state) => state.setIsPinned);
const setVisible = useUpdateStore((state) => state.setVisible);
const updateInfo = useUpdateStore((state) => state.updateInfo);
function openSetting() {
emit("open_settings", "");
@@ -55,16 +57,26 @@ export default function Footer({}: FooterProps) {
alt={t("search.footer.logoAlt")}
/>
)}
<span className="text-xs text-gray-500 dark:text-gray-400">
{sourceData?.source?.name ||
<div className="relative text-xs text-gray-500 dark:text-gray-400">
{updateInfo?.available ? (
<div className="cursor-pointer" onClick={() => setVisible(true)}>
<span>{t("search.footer.updateAvailable")}</span>
<span className="absolute top-0 -right-2 size-1.5 bg-[#FF3434] rounded-full"></span>
</div>
) : (
sourceData?.source?.name ||
t("search.footer.version", {
version: process.env.VERSION || "v1.0.0",
})}
</span>
})
)}
</div>
<button
onClick={togglePin}
className={`${isPinned ? "text-blue-500" : ""}`}
className={clsx({
"text-blue-500": isPinned,
"pl-2": updateInfo?.available,
})}
>
{isPinned ? <PinIcon /> : <PinOffIcon />}
</button>

View File

@@ -213,11 +213,7 @@ export default function ChatInput({
return (
<div
className={`w-full relative ${
isChatPage
? "bg-inputbox_bg_light dark:bg-inputbox_bg_dark bg-cover rounded-xl border border-[#E6E6E6] dark:border-[#272626]"
: ""
}`}
className={`w-full relative`}
>
<div
className={`p-2 flex items-center dark:text-[#D8D8D8] bg-[#ededed] dark:bg-[#202126] rounded transition-all relative `}
@@ -281,23 +277,13 @@ export default function ChatInput({
) : null}
</div>
{/* {isChatMode ? (
<button
className={`p-1 hover:bg-gray-50 dark:hover:bg-gray-700 rounded-full transition-colors ${
isListening ? "bg-blue-100 dark:bg-blue-900" : ""
}`}
type="button"
onClick={() => {}}
>
<Mic
className={`w-4 h-4 ${
isListening
? "text-blue-500 animate-pulse"
: "text-[#999] dark:text-[#999]"
}`}
/>
</button>
) : null} */}
{/* {isChatMode && (
<SpeechToText
onChange={(transcript) => {
changeInput(inputValue + transcript);
}}
/>
)} */}
{isChatMode && curChatEnd ? (
<button
@@ -396,7 +382,14 @@ export default function ChatInput({
/>
</div>
) : (
<div className="w-28 flex gap-2 relative"></div>
<div data-tauri-drag-region className="w-28 flex gap-2 relative">
{/* <SpeechToText
Icon={AudioLines}
onChange={(transcript) => {
changeInput(inputValue + transcript);
}}
/> */}
</div>
)}
{isChatPage ? null : (

View File

@@ -26,7 +26,6 @@ function Search({ isChatMode, input }: SearchProps) {
const [suggests, setSuggests] = useState<any[]>([]);
const [SearchData, setSearchData] = useState<any>({});
const [isSearchComplete, setIsSearchComplete] = useState(false);
const [selectedItem, setSelectedItem] = useState<any>();
const mainWindowRef = useRef<HTMLDivElement>(null);
@@ -87,7 +86,6 @@ function Search({ isChatMode, input }: SearchProps) {
IsError={IsError}
isSearchComplete={isSearchComplete}
isChatMode={isChatMode}
selected={(item) => setSelectedItem(item)}
/>
)
) : (
@@ -119,7 +117,7 @@ function Search({ isChatMode, input }: SearchProps) {
</div>
)}
<Footer isChat={false} name={selectedItem?.source?.name} />
<Footer />
<ContextMenu />
</div>

View File

@@ -126,7 +126,7 @@ export default function SearchPopover({
{dataSourceList?.length > 0 && (
<Popover>
<PopoverButton className={clsx("flex items-center")}>
<PopoverButton as="span" className={clsx("flex items-center")}>
<ChevronDownIcon
className={clsx("size-5", [
isSearchActive

View File

@@ -0,0 +1,97 @@
import { useEventListener, useReactive } from "ahooks";
import clsx from "clsx";
import { LucideIcon, Mic } from "lucide-react";
import { FC, useEffect } from "react";
interface SpeechToTextProps {
Icon?: LucideIcon;
onChange?: (transcript: string) => void;
}
let recognition: SpeechRecognition | null = null;
const SpeechToText: FC<SpeechToTextProps> = (props) => {
const { Icon = Mic, onChange } = props;
const state = useReactive({
speaking: false,
});
useEffect(() => {
return destroyRecognition;
}, []);
useEventListener("focusin", (event) => {
const { target } = event;
const isInputElement =
target instanceof HTMLInputElement ||
target instanceof HTMLTextAreaElement;
if (state.speaking && isInputElement) {
target.blur();
}
});
const handleSpeak = () => {
if (state.speaking) {
return destroyRecognition();
}
const SpeechRecognition =
window.SpeechRecognition || window.webkitSpeechRecognition;
recognition = new SpeechRecognition();
recognition.continuous = true;
recognition.interimResults = true;
recognition.lang = "zh-CN";
recognition.onresult = (event) => {
const transcript = [...event.results]
.map((result) => result[0].transcript)
.join("");
onChange?.(transcript);
};
recognition.onerror = destroyRecognition;
recognition.onend = destroyRecognition;
recognition.start();
state.speaking = true;
};
const destroyRecognition = () => {
if (recognition) {
recognition.abort();
recognition.onresult = null;
recognition.onerror = null;
recognition.onend = null;
recognition = null;
}
state.speaking = false;
};
return (
<div
className={clsx(
"p-1 hover:bg-gray-50 dark:hover:bg-gray-700 rounded-full transition cursor-pointer",
{
"bg-blue-100 dark:bg-blue-900": state.speaking,
}
)}
>
<Icon
className={clsx("size-4 text-[#999] dark:text-[#999]", {
"text-blue-500 animate-pulse": state.speaking,
})}
onClick={handleSpeak}
/>
</div>
);
};
export default SpeechToText;

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,181 @@
import { Button, Dialog, DialogPanel } from "@headlessui/react";
import { useTranslation } from "react-i18next";
import lightIcon from "./imgs/light-icon.png";
import darkIcon from "./imgs/dark-icon.png";
import { useThemeStore } from "@/stores/themeStore";
import { noop } from "lodash-es";
import { LoaderCircle, X } from "lucide-react";
import { useUpdateStore } from "@/stores/updateStore";
import { useInterval, useReactive } from "ahooks";
import { check } from "@tauri-apps/plugin-updater";
import { useCallback, useMemo } from "react";
import { relaunch } from "@tauri-apps/plugin-process";
import clsx from "clsx";
import { open } from "@tauri-apps/plugin-shell";
interface State {
loading?: boolean;
total?: number;
download: number;
}
const UpdateApp = () => {
const { t } = useTranslation();
const isDark = useThemeStore((state) => state.isDark);
const visible = useUpdateStore((state) => state.visible);
const setVisible = useUpdateStore((state) => state.setVisible);
const skipVersion = useUpdateStore((state) => state.skipVersion);
const setSkipVersion = useUpdateStore((state) => state.setSkipVersion);
const isOptional = useUpdateStore((state) => state.isOptional);
const updateInfo = useUpdateStore((state) => state.updateInfo);
const setUpdateInfo = useUpdateStore((state) => state.setUpdateInfo);
const state = useReactive<State>({ download: 0 });
useInterval(() => checkUpdate(), 1000 * 60 * 60 * 24, {
immediate: true,
});
const checkUpdate = useCallback(async () => {
const update = await check();
if (update?.available) {
setUpdateInfo(update);
if (skipVersion === update.version) return;
setVisible(true);
}
}, [skipVersion]);
const cursorClassName = useMemo(() => {
return state.loading ? "cursor-not-allowed" : "cursor-pointer";
}, [state.loading]);
const percent = useMemo(() => {
const { total, download } = state;
if (!total) return 0;
return ((download / total) * 100).toFixed(2);
}, [state.total, state.download]);
const handleDownload = async () => {
if (state.loading) return;
state.loading = true;
await updateInfo?.downloadAndInstall((progress) => {
switch (progress.event) {
case "Started":
state.total = progress.data.contentLength;
break;
case "Progress":
state.download += progress.data.chunkLength;
break;
}
});
state.loading = false;
relaunch();
};
const handleCancel = () => {
if (state.loading) return;
setVisible(false);
};
const handleSkip = () => {
if (state.loading) return;
setSkipVersion(updateInfo?.version);
setVisible(false);
};
return (
<Dialog
open={visible}
as="div"
className="relative z-10 focus:outline-none"
onClose={noop}
>
<div className="fixed inset-0 z-10 w-screen overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4">
<DialogPanel
transition
className="relative w-[340px] py-8 flex flex-col items-center bg-white shadow-md border border-[#EDEDED] rounded-lg dark:bg-[#333] dark:border-black/20"
>
<X
className={clsx(
"absolute size-5 text-[#999] top-3 right-3 dark:text-[#D8D8D8]",
cursorClassName,
{
hidden: !isOptional,
}
)}
onClick={handleCancel}
/>
<img src={isDark ? darkIcon : lightIcon} className="h-6" />
<div className="text-[#333] text-sm leading-5 py-2 dark:text-[#D8D8D8]">
{isOptional ? (
t("update.optional_description")
) : (
<div className="leading-5 text-center">
<p>{t("update.force_description1")}</p>
<p>{t("update.force_description2")}</p>
</div>
)}
</div>
<div
className="text-xs text-[#0072FF] cursor-pointer"
onClick={() => {
open(
"https://docs.infinilabs.com/coco-app/main/docs/release-notes"
);
}}
>
v{updateInfo?.version} {t("update.releaseNotes")}
</div>
<Button
className={clsx(
"mb-3 mt-6 bg-[#0072FF] text-white text-sm px-[14px] py-[8px] rounded-lg",
cursorClassName,
{
"opacity-50": state.loading,
}
)}
onClick={handleDownload}
>
{state.loading ? (
<div className="flex justify-center items-center gap-2">
<LoaderCircle className="animate-spin size-5" />
{percent}%
</div>
) : (
t("update.button.download")
)}
</Button>
<div
className={clsx("text-xs text-[#999]", cursorClassName, {
hidden: !isOptional,
})}
onClick={handleSkip}
>
{t("update.skip_version")}
</div>
</DialogPanel>
</div>
</div>
</Dialog>
);
};
export default UpdateApp;

View File

@@ -1,33 +1,227 @@
import { useCallback } from "react";
import { invoke } from "@tauri-apps/api/core";
import { invoke, isTauri } from "@tauri-apps/api/core";
import { IServer } from "@/stores/appStore";
import type { Chat } from "@/components/Assistant/types";
export default function useChatActions(currentService: IServer, activeChat?: Chat) {
const chatClose = useCallback(async () => {
export function useChatActions(
currentServiceId: string | undefined,
setActiveChat: (chat: Chat | undefined) => void,
setCurChatEnd: (value: boolean) => void,
setErrorShow: (value: boolean) => void,
setTimedoutShow: (value: boolean) => void,
clearAllChunkData: () => void,
setQuestion: (value: string) => void,
curIdRef: React.MutableRefObject<string>,
isSearchActive?: boolean,
isDeepThinkActive?: boolean,
sourceDataIds?: string[],
changeInput?: (val: string) => void,
) {
const chatClose = useCallback(async (activeChat?: Chat) => {
if (!activeChat?._id) return;
try {
await invoke("close_session_chat", {
serverId: currentService?.id,
let response: any = await invoke("close_session_chat", {
serverId: currentServiceId,
sessionId: activeChat?._id,
});
response = JSON.parse(response || "");
console.log("_close", response);
} catch (error) {
console.error("Failed to close chat:", error);
console.error("chatClose:", error);
}
}, [currentService?.id, activeChat?._id]);
}, [currentServiceId]);
const cancelChat = useCallback(async () => {
const cancelChat = useCallback(async (activeChat?: Chat) => {
setCurChatEnd(true);
if (!activeChat?._id) return;
try {
await invoke("cancel_session_chat", {
serverId: currentService?.id,
let response: any = await invoke("cancel_session_chat", {
serverId: currentServiceId,
sessionId: activeChat?._id,
});
response = JSON.parse(response || "");
console.log("_cancel", response);
} catch (error) {
console.error("Failed to cancel chat:", error);
console.error("cancelChat:", error);
}
}, [currentService?.id, activeChat?._id]);
}, [currentServiceId, setCurChatEnd]);
return { chatClose, cancelChat };
const chatHistory = useCallback(async (
chat: Chat,
callback?: (chat: Chat) => void
) => {
try {
let response: any = await invoke("session_chat_history", {
serverId: currentServiceId,
sessionId: chat?._id,
from: 0,
size: 20,
});
response = JSON.parse(response || "");
const hits = response?.hits?.hits || [];
const updatedChat: Chat = {
...chat,
messages: hits,
};
console.log("id_history", response, updatedChat);
setActiveChat(updatedChat);
callback && callback(updatedChat);
} catch (error) {
console.error("chatHistory:", error);
}
}, [currentServiceId, setActiveChat]);
const createNewChat = useCallback(
async (value: string = "", activeChat?: Chat) => {
setTimedoutShow(false);
setErrorShow(false);
chatClose(activeChat);
clearAllChunkData();
setQuestion(value);
try {
console.log("sourceDataIds", sourceDataIds);
let response: any = await invoke("new_chat", {
serverId: currentServiceId,
message: value,
queryParams: {
search: isSearchActive,
deep_thinking: isDeepThinkActive,
datasource: sourceDataIds?.join(",") || "",
},
});
console.log("_new", response);
const newChat: Chat = response;
curIdRef.current = response?.payload?.id;
newChat._source = {
message: value,
};
const updatedChat: Chat = {
...newChat,
messages: [newChat],
};
changeInput && changeInput("");
setActiveChat(updatedChat);
setCurChatEnd(false);
} catch (error) {
setErrorShow(true);
console.error("createNewChat:", error);
}
},
[currentServiceId, sourceDataIds, isSearchActive, isDeepThinkActive, curIdRef]
);
const sendMessage = useCallback(
async (content: string, newChat: Chat) => {
if (!newChat?._id || !content) return;
clearAllChunkData();
try {
let response: any = await invoke("send_message", {
serverId: currentServiceId,
sessionId: newChat?._id,
queryParams: {
search: isSearchActive,
deep_thinking: isDeepThinkActive,
datasource: sourceDataIds?.join(",") || "",
},
message: content,
});
response = JSON.parse(response || "");
console.log("_send", response);
curIdRef.current = response[0]?._id;
const updatedChat: Chat = {
...newChat,
messages: [...(newChat?.messages || []), ...(response || [])],
};
changeInput && changeInput("");
setActiveChat(updatedChat);
setCurChatEnd(false);
} catch (error) {
setErrorShow(true);
console.error("sendMessage:", error);
}
},
[currentServiceId, sourceDataIds, isSearchActive, isDeepThinkActive, curIdRef, setActiveChat, setCurChatEnd, setErrorShow, changeInput]
);
const handleSendMessage = useCallback(
async (content: string, activeChat?: Chat) => {
if (!activeChat?._id || !content) return;
setQuestion(content);
setTimedoutShow(false);
setErrorShow(false);
await chatHistory(activeChat, (chat) => sendMessage(content, chat));
},
[chatHistory, sendMessage, setQuestion, setTimedoutShow, setErrorShow, clearAllChunkData]
);
const openSessionChat = useCallback(async (chat: Chat) => {
try {
let response: any = await invoke("open_session_chat", {
serverId: currentServiceId,
sessionId: chat?._id,
});
response = JSON.parse(response || "");
console.log("_open", response);
return response;
} catch (error) {
console.error("open_session_chat:", error);
return null;
}
}, [currentServiceId]);
const getChatHistory = useCallback(async () => {
if (!currentServiceId) return [];
try {
let response: any = await invoke("chat_history", {
serverId: currentServiceId,
from: 0,
size: 20,
});
response = JSON.parse(response || "");
console.log("_history", response);
const hits = response?.hits?.hits || [];
return hits;
} catch (error) {
console.error("chat_history:", error);
return [];
}
}, [currentServiceId]);
const createChatWindow = useCallback(async (createWin: any) => {
if (isTauri()) {
createWin && createWin({
label: "chat",
title: "Coco Chat",
dragDropEnabled: true,
center: true,
width: 1000,
height: 800,
minWidth: 1000,
minHeight: 800,
alwaysOnTop: false,
skipTaskbar: false,
decorations: true,
closable: true,
url: "/ui/chat",
});
}
}, []);
return {
chatClose,
cancelChat,
chatHistory,
createNewChat,
sendMessage,
handleSendMessage,
openSessionChat,
getChatHistory,
createChatWindow
};
}

View File

@@ -0,0 +1,69 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { debounce } from "lodash-es";
export function useChatScroll(messagesEndRef: React.RefObject<HTMLDivElement>) {
const [userScrolling, setUserScrolling] = useState(false);
const scrollTimeoutRef = useRef<NodeJS.Timeout>();
const lastScrollHeightRef = useRef<number>(0);
const isNearBottom = (container: HTMLElement) => {
const { scrollTop, scrollHeight, clientHeight } = container;
return Math.abs(scrollHeight - scrollTop - clientHeight) < 150;
};
const scrollToBottom = useCallback(
debounce(() => {
const container = messagesEndRef.current?.parentElement;
if (!container) return;
const contentChanged = lastScrollHeightRef.current !== container.scrollHeight;
lastScrollHeightRef.current = container.scrollHeight;
if (!userScrolling || (contentChanged && isNearBottom(container))) {
container.scrollTo({
top: container.scrollHeight,
behavior: "smooth",
});
}
}, 50),
[userScrolling, messagesEndRef]
);
useEffect(() => {
const container = messagesEndRef.current?.parentElement;
if (!container) return;
lastScrollHeightRef.current = container.scrollHeight;
const handleScroll = () => {
if (scrollTimeoutRef.current) {
clearTimeout(scrollTimeoutRef.current);
}
const near = isNearBottom(container);
if (!near) {
setUserScrolling(true);
}
scrollTimeoutRef.current = setTimeout(() => {
if (isNearBottom(container)) {
setUserScrolling(false);
}
}, 300);
};
container.addEventListener("scroll", handleScroll);
return () => {
container.removeEventListener("scroll", handleScroll);
if (scrollTimeoutRef.current) {
clearTimeout(scrollTimeoutRef.current);
}
};
}, [messagesEndRef]);
return {
userScrolling,
scrollToBottom
};
}

View File

@@ -0,0 +1,83 @@
import { useCallback, useRef } from "react";
import type { IChunkData, Chat } from "@/components/Assistant/types";
export function useMessageHandler(
curIdRef: React.MutableRefObject<string>,
setCurChatEnd: (value: boolean) => void,
setTimedoutShow: (value: boolean) => void,
onCancel: (chat?: Chat) => void,
setLoadingStep: (value: Record<string, boolean> | ((prev: Record<string, boolean>) => Record<string, boolean>)) => void,
handlers: {
deal_query_intent: (data: IChunkData) => void;
deal_fetch_source: (data: IChunkData) => void;
deal_pick_source: (data: IChunkData) => void;
deal_deep_read: (data: IChunkData) => void;
deal_think: (data: IChunkData) => void;
deal_response: (data: IChunkData) => void;
}
) {
const messageTimeoutRef = useRef<NodeJS.Timeout>();
const dealMsg = useCallback(
(msg: string) => {
if (messageTimeoutRef.current) {
clearTimeout(messageTimeoutRef.current);
}
if (!msg.includes("PRIVATE")) return;
messageTimeoutRef.current = setTimeout(() => {
console.log("AI response timeout");
setTimedoutShow(true);
onCancel();
}, 120000);
const cleanedData = msg.replace(/^PRIVATE /, "");
try {
const chunkData = JSON.parse(cleanedData);
if (chunkData.reply_to_message !== curIdRef.current) return;
setLoadingStep(() => ({
query_intent: false,
fetch_source: false,
pick_source: false,
deep_read: false,
think: false,
response: false,
[chunkData.chunk_type]: true,
}));
if (chunkData.chunk_type === "query_intent") {
handlers.deal_query_intent(chunkData);
} else if (chunkData.chunk_type === "fetch_source") {
handlers.deal_fetch_source(chunkData);
} else if (chunkData.chunk_type === "pick_source") {
handlers.deal_pick_source(chunkData);
} else if (chunkData.chunk_type === "deep_read") {
handlers.deal_deep_read(chunkData);
} else if (chunkData.chunk_type === "think") {
handlers.deal_think(chunkData);
} else if (chunkData.chunk_type === "response") {
handlers.deal_response(chunkData);
} else if (chunkData.chunk_type === "reply_end") {
if (messageTimeoutRef.current) {
clearTimeout(messageTimeoutRef.current);
}
setCurChatEnd(true);
console.log("AI finished output");
return;
}
} catch (error) {
console.error("parse error:", error);
}
},
[onCancel, setCurChatEnd, setTimedoutShow, curIdRef.current]
);
return {
dealMsg,
messageTimeoutRef
};
}

View File

@@ -8,14 +8,14 @@ interface WebSocketProps {
connected: boolean;
setConnected: (connected: boolean) => void;
currentService: IServer | null;
dealMsg: (msg: string) => void;
dealMsgRef: React.MutableRefObject<((msg: string) => void) | null>;
}
export default function useWebSocket({
connected,
setConnected,
currentService,
dealMsg
dealMsgRef,
}: WebSocketProps) {
const [errorShow, setErrorShow] = useState(false);
@@ -32,7 +32,13 @@ export default function useWebSocket({
}
}, [currentService]);
const updateDealMsg = useCallback((newDealMsg: (msg: string) => void) => {
dealMsgRef.current = newDealMsg;
}, [dealMsgRef]);
useEffect(() => {
if (!currentService?.id) return;
let unlisten_error = null;
let unlisten_message = null;
@@ -52,7 +58,8 @@ export default function useWebSocket({
});
unlisten_message = listen("ws-message", (event) => {
dealMsg(String(event.payload));
const msg = event.payload as string;
dealMsgRef.current && dealMsgRef.current(msg);
});
}
@@ -60,7 +67,7 @@ export default function useWebSocket({
unlisten_error?.then((fn: any) => fn());
unlisten_message?.then((fn: any) => fn());
};
}, [connected, dealMsg]);
}, [connected, dealMsgRef]);
return { errorShow, setErrorShow, reconnect };
return { errorShow, setErrorShow, reconnect, updateDealMsg };
}

View File

@@ -9,7 +9,9 @@ const defaultWindowConfig = {
title: "",
url: "",
width: 1000,
height: 640,
height: 800,
minWidth: 1000,
minHeight: 800,
center: true,
resizable: true,
maximized: false,

View File

@@ -81,6 +81,7 @@
"footer": {
"logoAlt": "Coco Logo",
"version": "{{version}}",
"updateAvailable": "Update available",
"select": "Select",
"open": "Open"
},
@@ -206,5 +207,16 @@
"showCoco": "Show Coco",
"settings": "Settings...",
"quitCoco": "Quit Coco"
},
"update": {
"title": "New update available for Coco AI.",
"optional_description": "New update available for Coco AI.",
"force_description1": "Coco AI update required.",
"force_description2": "Please install the latest version to continue.",
"releaseNotes": "Release Notes",
"button": {
"download": "Download"
},
"skip_version": "Skip this version"
}
}

View File

@@ -81,6 +81,7 @@
"footer": {
"logoAlt": "Coco 图标",
"version": "{{version}}",
"updateAvailable": "有可用更新",
"select": "选择",
"open": "打开"
},
@@ -206,5 +207,15 @@
"showCoco": "显示 Coco",
"settings": "偏好设置",
"quitCoco": "退出 Coco"
},
"update": {
"optional_description": "Coco AI 有新的可用更新。",
"force_description1": "Coco Al 需要更新。",
"force_description2": "请安装最新版本后继续使用。",
"releaseNotes": "更新日志",
"button": {
"download": "下载"
},
"skip_version": "跳过此版本"
}
}

View File

@@ -149,6 +149,7 @@ export default function Chat({}: ChatProps) {
}}
onSelectChat={onSelectChat}
onDeleteChat={deleteChat}
fetchChatHistory={getChatHistory}
/>
</div>
) : null}
@@ -167,10 +168,11 @@ export default function Chat({}: ChatProps) {
isSidebarOpen={isSidebarOpen}
clearChatPage={clearChat}
isChatPage={isChatPage}
changeInput={setInput}
/>
{/* Input area */}
<div className={`border-t p-4 border-gray-200 dark:border-gray-800`}>
<div className={`border-t p-4 pb-0 border-gray-200 dark:border-gray-800`}>
<InputBox
isChatMode={true}
inputValue={input}

View File

@@ -10,6 +10,7 @@ import ChatAI, { ChatAIRef } from "@/components/Assistant/Chat";
import { useAppStore } from "@/stores/appStore";
import { useAuthStore } from "@/stores/authStore";
import { isLinux, isWin } from "@/utils/platform";
import UpdateApp from "@/components/UpdateApp";
export default function DesktopApp() {
const initializeListeners = useAppStore((state) => state.initializeListeners);
@@ -163,6 +164,8 @@ export default function DesktopApp() {
/>
) : null}
</div>
<UpdateApp />
</div>
);
}

View File

@@ -21,6 +21,9 @@ export interface IServer {
status: string;
};
assistantCount?: number;
minimal_client_version?: {
number: number;
};
}
export type IAppStore = {

View File

@@ -1,6 +1,10 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { produce } from 'immer'
import { listen, emit } from "@tauri-apps/api/event";
const CONNECTOR_CHANGE_EVENT = "connector_data_change";
const DATASOURCE_CHANGE_EVENT = "datasourceData_change";
type keyArrayObject = {
[key: string]: any[];
@@ -20,18 +24,18 @@ export type IConnectStore = {
export const useConnectStore = create<IConnectStore>()(
persist(
(set) => ({
serverList: [],
setServerList: (serverList: []) => {
console.log("set serverList:",serverList)
serverList: [],
setServerList: (serverList: []) => {
console.log("set serverList:", serverList)
set(produce((draft) => {
draft.serverList = serverList;
draft.serverList = serverList;
}))
},
currentService: "default_coco_server",
setCurrentService: (server: any) => {
console.log("set default server:",server)
console.log("set default server:", server)
set(produce((draft) => {
draft.currentService = server;
draft.currentService = server;
}))
},
connector_data: {},
@@ -41,6 +45,9 @@ export const useConnectStore = create<IConnectStore>()(
draft.connector_data[key] = connector_data
})
);
await emit(CONNECTOR_CHANGE_EVENT, {
connector_data,
});
},
datasourceData: {},
setDatasourceData: async (datasourceData: any[], key: string) => {
@@ -49,6 +56,19 @@ export const useConnectStore = create<IConnectStore>()(
draft.datasourceData[key] = datasourceData
})
);
await emit(DATASOURCE_CHANGE_EVENT, {
datasourceData,
});
},
initializeListeners: () => {
listen(CONNECTOR_CHANGE_EVENT, (event: any) => {
const { connector_data } = event.payload;
set({ connector_data });
});
listen(DATASOURCE_CHANGE_EVENT, (event: any) => {
const { datasourceData } = event.payload;
set({ datasourceData });
});
},
}),
{
@@ -60,4 +80,4 @@ export const useConnectStore = create<IConnectStore>()(
}),
}
)
);
);

41
src/stores/updateStore.ts Normal file
View File

@@ -0,0 +1,41 @@
import { Update } from "@tauri-apps/plugin-updater";
import { create } from "zustand";
import { persist } from "zustand/middleware";
export type IUpdateStore = {
visible: boolean;
setVisible: (visible: boolean) => void;
skipVersion?: string;
setSkipVersion: (skipVersion?: string) => void;
isOptional: boolean;
setIsOptional: (isOptional: boolean) => void;
updateInfo?: Update;
setUpdateInfo: (updateInfo?: Update) => void;
};
export const useUpdateStore = create<IUpdateStore>()(
persist(
(set) => ({
visible: false,
setVisible: (visible: boolean) => {
return set({ visible });
},
setSkipVersion: (skipVersion?: string) => {
return set({ skipVersion });
},
isOptional: true,
setIsOptional: (isOptional: boolean) => {
return set({ isOptional });
},
setUpdateInfo: (updateInfo?: Update) => {
return set({ updateInfo });
},
}),
{
name: "update-store",
partialize: (state) => ({
skipVersion: state.skipVersion,
}),
}
)
);

View File

@@ -1,5 +1,5 @@
import { useEffect, useState } from "react";
import { isTauri } from "@tauri-apps/api/core";
import { invoke, isTauri } from "@tauri-apps/api/core";
import { open } from "@tauri-apps/plugin-shell";
// 1
@@ -56,8 +56,8 @@ export function useWindowSize() {
export const IsTauri = () => {
return Boolean(
typeof window !== "undefined" &&
window !== undefined &&
(window as any).__TAURI_INTERNALS__ !== undefined
window !== undefined &&
(window as any).__TAURI_INTERNALS__ !== undefined
);
};
@@ -66,6 +66,7 @@ export const OpenURLWithBrowser = async (url: string) => {
if (isTauri()) {
try {
await open(url);
await invoke("hide_coco");
console.log("URL opened in default browser");
} catch (error) {
console.error("Failed to open URL:", error);