mirror of
https://github.com/infinilabs/coco-app.git
synced 2025-12-21 22:09:23 +01:00
Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ac81566c6 | ||
|
|
b004670dec | ||
|
|
a426e33e6b | ||
|
|
bb7dd6bf7c | ||
|
|
37c5f2de24 | ||
|
|
ab6c25fe96 | ||
|
|
1fb464df09 | ||
|
|
65aa75043f | ||
|
|
79dcc7b4ec | ||
|
|
3d29cfe235 | ||
|
|
aea3a7ba98 | ||
|
|
190dfc6ecd | ||
|
|
316a7940d6 | ||
|
|
acfc1bb32d | ||
|
|
c4d178dc2d | ||
|
|
6333c697d5 | ||
|
|
810541494f | ||
|
|
e45dc2acbe | ||
|
|
2d1ccb9744 | ||
|
|
406f3b31e9 | ||
|
|
f51dd81014 | ||
|
|
3b38cbfb6c | ||
|
|
a4483ba277 | ||
|
|
bf46979b80 | ||
|
|
070f171ad4 | ||
|
|
3180704a0d | ||
|
|
b3f68697ce | ||
|
|
69d2b4b834 | ||
|
|
6837286061 | ||
|
|
a431ead22a | ||
|
|
7ec41dfe80 | ||
|
|
06053e9fd9 | ||
|
|
70b048fba3 | ||
|
|
45083f829b | ||
|
|
e4f6fb8e98 | ||
|
|
ee182b22da | ||
|
|
a37e22c227 | ||
|
|
d75ab1018d | ||
|
|
40ad066e69 | ||
|
|
a2a5a9f8fe | ||
|
|
5fd9339e56 | ||
|
|
a8a9208b1f |
123
.github/workflows/release.yml
vendored
123
.github/workflows/release.yml
vendored
@@ -9,10 +9,16 @@ on:
|
||||
jobs:
|
||||
create-release:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
outputs:
|
||||
APP_VERSION: ${{ steps.get-version.outputs.APP_VERSION }}
|
||||
RELEASE_BODY: ${{ steps.get-changelog.outputs.RELEASE_BODY }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set output
|
||||
id: vars
|
||||
run: echo "tag=${GITHUB_REF#refs/*/}" >> $GITHUB_OUTPUT
|
||||
@@ -22,11 +28,28 @@ jobs:
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Get build version
|
||||
shell: bash
|
||||
id: get-version
|
||||
run: |
|
||||
PACKAGE_VERSION=$(jq -r '.version' package.json)
|
||||
CARGO_VERSION=$(grep -m 1 '^version =' src-tauri/Cargo.toml | sed -E 's/.*"([^"]+)".*/\1/')
|
||||
if [ "$PACKAGE_VERSION" != "$CARGO_VERSION" ]; then
|
||||
echo "::error::Version mismatch!"
|
||||
else
|
||||
echo "Version match: $PACKAGE_VERSION"
|
||||
fi
|
||||
echo "APP_VERSION=$PACKAGE_VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Generate changelog
|
||||
id: create_release
|
||||
run: npx changelogithub --draft --name ${{ steps.vars.outputs.tag }}
|
||||
id: get-changelog
|
||||
run: |
|
||||
CHANGELOG_BODY=$(npx changelogithub --draft --name ${{ steps.vars.outputs.tag }})
|
||||
echo "RELEASE_BODY<<EOF" >> $GITHUB_OUTPUT
|
||||
echo "$CHANGELOG_BODY" >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
build-app:
|
||||
needs: create-release
|
||||
@@ -52,11 +75,24 @@ jobs:
|
||||
target: "x86_64-unknown-linux-gnu"
|
||||
- platform: "ubuntu-22.04-arm"
|
||||
target: "aarch64-unknown-linux-gnu"
|
||||
env:
|
||||
APP_VERSION: ${{ needs.create-release.outputs.APP_VERSION }}
|
||||
RELEASE_BODY: ${{ needs.create-release.outputs.RELEASE_BODY }}
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Checkout dependency repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: 'infinilabs/pizza'
|
||||
ssh-key: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
submodules: recursive
|
||||
ref: main
|
||||
path: pizza
|
||||
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
@@ -65,17 +101,31 @@ jobs:
|
||||
with:
|
||||
version: latest
|
||||
|
||||
- name: Install rust target
|
||||
run: rustup target add ${{ matrix.target }}
|
||||
|
||||
- name: Install dependencies (ubuntu only)
|
||||
if: startsWith(matrix.platform, 'ubuntu-22.04')
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf xdg-utils
|
||||
|
||||
- name: Install Rust stable
|
||||
run: rustup toolchain install stable
|
||||
- name: Add Rust build target at ${{ matrix.platform}} for ${{ matrix.target }}
|
||||
working-directory: src-tauri
|
||||
shell: bash
|
||||
run: |
|
||||
rustup target add ${{ matrix.target }} || true
|
||||
|
||||
- name: Add pizza engine as a dependency
|
||||
working-directory: src-tauri
|
||||
shell: bash
|
||||
run: |
|
||||
BUILD_ARGS="--target ${{ matrix.target }}"
|
||||
if [[ "${{matrix.target }}" != "i686-pc-windows-msvc" ]]; then
|
||||
echo "Adding pizza engine as a dependency for ${{matrix.platform }}-${{matrix.target }}"
|
||||
( cargo add --path ../pizza/lib/engine --features query_string_parser,persistence )
|
||||
BUILD_ARGS+=" --features use_pizza_engine"
|
||||
else
|
||||
echo "Skipping pizza engine dependency for ${{matrix.platform }}-${{matrix.target }}"
|
||||
fi
|
||||
echo "BUILD_ARGS=${BUILD_ARGS}" >> $GITHUB_ENV
|
||||
|
||||
- name: Rust cache
|
||||
uses: swatinem/rust-cache@v2
|
||||
@@ -90,33 +140,9 @@ jobs:
|
||||
|
||||
- name: Install app dependencies and build web
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Set up SSH agent for private repository clone
|
||||
if: matrix.target != 'i686-pc-windows-msvc'
|
||||
uses: webfactory/ssh-agent@v0.9.0
|
||||
with:
|
||||
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
|
||||
- name: Add Git server to known hosts
|
||||
if: matrix.platform != 'windows-latest'
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
ssh-keyscan github.com >> ~/.ssh/known_hosts
|
||||
chmod 600 ~/.ssh/known_hosts
|
||||
|
||||
- name: Pizza engine features setup
|
||||
run: |
|
||||
if [[ ${{ matrix.target }} == "i686-pc-windows-msvc" ]]; then
|
||||
rustup target add i686-pc-windows-msvc --toolchain stable
|
||||
else
|
||||
make add-dep-pizza-engine-linux
|
||||
rustup target add ${{ matrix.target}} --toolchain nightly-2025-02-28
|
||||
fi
|
||||
|
||||
|
||||
- name: Build the app with ${{ matrix.platform }}
|
||||
|
||||
- name: Build the coco at ${{ matrix.platform}} for ${{ matrix.target }} @ ${{ env.APP_VERSION }}
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
if: matrix.target != 'i686-pc-windows-msvc'
|
||||
env:
|
||||
CI: false
|
||||
PLATFORM: ${{ matrix.platform }}
|
||||
@@ -131,31 +157,8 @@ jobs:
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
with:
|
||||
tagName: ${{ github.ref_name }}
|
||||
releaseName: Coco ${{ needs.create-release.outputs.APP_VERSION }}
|
||||
releaseBody: ""
|
||||
releaseName: Coco ${{ env.APP_VERSION }}
|
||||
releaseBody: "${{ env.RELEASE_BODY }}"
|
||||
releaseDraft: true
|
||||
prerelease: false
|
||||
args: --target ${{ matrix.target }} --features use_pizza_engine
|
||||
|
||||
- name: Build the app with ${{ matrix.platform }} (windows i686 only)
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
if: matrix.target == 'i686-pc-windows-msvc'
|
||||
env:
|
||||
CI: false
|
||||
PLATFORM: ${{ matrix.platform }}
|
||||
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ""
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ""
|
||||
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
with:
|
||||
tagName: ${{ github.ref_name }}
|
||||
releaseName: Coco ${{ needs.create-release.outputs.APP_VERSION }}
|
||||
releaseBody: ""
|
||||
releaseDraft: true
|
||||
prerelease: false
|
||||
args: --target ${{ matrix.target }}
|
||||
args: ${{ env.BUILD_ARGS }}
|
||||
|
||||
@@ -10,56 +10,41 @@ Information about release notes of Coco Server is provided here.
|
||||
## Latest (In development)
|
||||
|
||||
### ❌ Breaking changes
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
### 🐛 Bug fix
|
||||
|
||||
### ✈️ Improvements
|
||||
|
||||
## 0.5.2 (2025-06-13)
|
||||
## 0.6.0 (2025-06-29)
|
||||
|
||||
### ❌ Breaking changes
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
- feat: ai overview support is enabled with shortcut #597
|
||||
- feat: add key monitoring during reset #615
|
||||
- feat: calculator extension add description #623
|
||||
- feat: support right-click actions after text selection #624
|
||||
- feat: add ai overview minimum number of search results configuration #625
|
||||
- feat: add internationalized translations of AI-related extensions #632
|
||||
- feat: context menu support for secondary pages #680
|
||||
- feat: support `Tab` and `Enter` for delete dialog buttons #700
|
||||
- feat: add check for updates #701
|
||||
- feat: impl extension store #699
|
||||
- feat: support back navigation via delete key #717
|
||||
|
||||
### 🐛 Bug fix
|
||||
|
||||
- fix: fixed issue with incorrect login status #600
|
||||
- fix: new chat assistant id not found #603
|
||||
- fix: resolve regex error on older macOS versions #605
|
||||
- fix: fix chat log update and sorting issues #612
|
||||
- fix: resolved an issue where number keys were not working on the web #616
|
||||
- fix: do not panic when the datasource specified does not exist #618
|
||||
- fix: fixed modifier keys not working with continue chat #619
|
||||
- fix: invalid DSL error if input contains multiple lines #620
|
||||
- fix: fix ai overview hidden height before message #622
|
||||
- fix: tab key hides window in chat mode #641
|
||||
- fix: arrow keys still navigated search when menu opened with Cmd+K #642
|
||||
- fix: input lost when reopening dialog after search #644
|
||||
- fix: web page unmount event #645
|
||||
- fix: fix the problem of local path not opening #650
|
||||
- fix: number keys not following settings #661
|
||||
- fix: fix problem with up and down key indexing #676
|
||||
- fix: arrow inserting escape sequences #683
|
||||
- fix: quick ai state synchronous #693
|
||||
- fix: toggle extension should register/unregister hotkey #691
|
||||
- fix: take coco server back on refresh #696
|
||||
- fix: some input fields couldn’t accept spaces #709
|
||||
- fix: context menu search not working #713
|
||||
- fix: open extension store display #724
|
||||
|
||||
### ✈️ Improvements
|
||||
|
||||
- chore: initialize current assistant from history #606
|
||||
- chore: add onContextMenu event #629
|
||||
- chore: more logs for the setup process #634
|
||||
- chore: copy supports http protocol #639
|
||||
- chore: add special character filtering #668
|
||||
- refactor: use author/ext_id as extension unique identifier #643
|
||||
- refactor: refactoring search api #679
|
||||
- chore: continue to chat page display #690
|
||||
- chore: improve server list selection with enter key #692
|
||||
- chore: add message for latest version check #703
|
||||
- chore: log command execution results #718
|
||||
- chore: adjust styles and add button reindex #719
|
||||
|
||||
## 0.5.1 (2025-05-31)
|
||||
## 0.5.0 (2025-06-13)
|
||||
|
||||
### ❌ Breaking changes
|
||||
|
||||
@@ -80,6 +65,13 @@ Information about release notes of Coco Server is provided here.
|
||||
- feat: dynamic log level via env var COCO_LOG #535
|
||||
- feat: add quick AI access to search mode #556
|
||||
- feat: rerank search results #561
|
||||
- feat: ai overview support is enabled with shortcut #597
|
||||
- feat: add key monitoring during reset #615
|
||||
- feat: calculator extension add description #623
|
||||
- feat: support right-click actions after text selection #624
|
||||
- feat: add ai overview minimum number of search results configuration #625
|
||||
- feat: add internationalized translations of AI-related extensions #632
|
||||
- feat: context menu support for secondary pages #680
|
||||
|
||||
### 🐛 Bug fix
|
||||
|
||||
@@ -101,6 +93,23 @@ Information about release notes of Coco Server is provided here.
|
||||
- fix: independent chat window has no data #554
|
||||
- fix: resolved navigation error on continue chat action #558
|
||||
- fix: make extension search source respect parameter datasource #576
|
||||
- fix: fixed issue with incorrect login status #600
|
||||
- fix: new chat assistant id not found #603
|
||||
- fix: resolve regex error on older macOS versions #605
|
||||
- fix: fix chat log update and sorting issues #612
|
||||
- fix: resolved an issue where number keys were not working on the web #616
|
||||
- fix: do not panic when the datasource specified does not exist #618
|
||||
- fix: fixed modifier keys not working with continue chat #619
|
||||
- fix: invalid DSL error if input contains multiple lines #620
|
||||
- fix: fix ai overview hidden height before message #622
|
||||
- fix: tab key hides window in chat mode #641
|
||||
- fix: arrow keys still navigated search when menu opened with Cmd+K #642
|
||||
- fix: input lost when reopening dialog after search #644
|
||||
- fix: web page unmount event #645
|
||||
- fix: fix the problem of local path not opening #650
|
||||
- fix: number keys not following settings #661
|
||||
- fix: fix problem with up and down key indexing #676
|
||||
- fix: arrow inserting escape sequences #683
|
||||
|
||||
### ✈️ Improvements
|
||||
|
||||
@@ -135,6 +144,12 @@ Information about release notes of Coco Server is provided here.
|
||||
- chore: mark unavailable server to offline on refresh info #569
|
||||
- chore: only show available servers in chat #570
|
||||
- refactor: search result related components #571
|
||||
- chore: initialize current assistant from history #606
|
||||
- chore: add onContextMenu event #629
|
||||
- chore: more logs for the setup process #634
|
||||
- chore: copy supports http protocol #639
|
||||
- refactor: use author/ext_id as extension unique identifier #643
|
||||
- chore: add special character filtering #668
|
||||
|
||||
## 0.4.0 (2025-04-27)
|
||||
|
||||
@@ -286,4 +301,4 @@ Information about release notes of Coco Server is provided here.
|
||||
|
||||
### Bug fix
|
||||
|
||||
### Improvements
|
||||
### Improvements
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "coco",
|
||||
"private": true,
|
||||
"version": "0.5.0",
|
||||
"version": "0.6.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -60,6 +60,7 @@
|
||||
"remark-breaks": "^4.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"remark-math": "^6.0.0",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tauri-plugin-fs-pro-api": "^2.4.0",
|
||||
"tauri-plugin-macos-permissions-api": "^2.3.0",
|
||||
"tauri-plugin-screenshots-api": "^2.2.0",
|
||||
@@ -94,4 +95,4 @@
|
||||
"vite": "^5.4.19"
|
||||
},
|
||||
"packageManager": "pnpm@10.11.0+sha512.6540583f41cc5f628eb3d9773ecee802f4f9ef9923cc45b69890fb47991d4b092964694ec3a4f738a420c918a333062c8b925d312f42e4f0c263eb603551f977"
|
||||
}
|
||||
}
|
||||
8
pnpm-lock.yaml
generated
8
pnpm-lock.yaml
generated
@@ -134,6 +134,9 @@ importers:
|
||||
remark-math:
|
||||
specifier: ^6.0.0
|
||||
version: 6.0.0
|
||||
tailwind-merge:
|
||||
specifier: ^3.3.1
|
||||
version: 3.3.1
|
||||
tauri-plugin-fs-pro-api:
|
||||
specifier: ^2.4.0
|
||||
version: 2.4.0
|
||||
@@ -3447,6 +3450,9 @@ packages:
|
||||
tabbable@6.2.0:
|
||||
resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==}
|
||||
|
||||
tailwind-merge@3.3.1:
|
||||
resolution: {integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==}
|
||||
|
||||
tailwindcss@3.4.17:
|
||||
resolution: {integrity: sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
@@ -7248,6 +7254,8 @@ snapshots:
|
||||
|
||||
tabbable@6.2.0: {}
|
||||
|
||||
tailwind-merge@3.3.1: {}
|
||||
|
||||
tailwindcss@3.4.17:
|
||||
dependencies:
|
||||
'@alloc/quick-lru': 5.2.0
|
||||
|
||||
225
src-tauri/Cargo.lock
generated
225
src-tauri/Cargo.lock
generated
@@ -17,6 +17,17 @@ version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
|
||||
|
||||
[[package]]
|
||||
name = "aes"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cipher",
|
||||
"cpufeatures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.7.8"
|
||||
@@ -518,6 +529,26 @@ dependencies = [
|
||||
"piper",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "borrowme"
|
||||
version = "0.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc75de882fa904cab1f2efe7184a871121d12eacebd181cbd2a15ff7af84c519"
|
||||
dependencies = [
|
||||
"borrowme-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "borrowme-macros"
|
||||
version = "0.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b00f4d9b344db5782c18429dac7a9af047d522f1d9ba1617d715aaffbdc040d0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.101",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "borsh"
|
||||
version = "1.5.7"
|
||||
@@ -634,6 +665,25 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bzip2"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47"
|
||||
dependencies = [
|
||||
"bzip2-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bzip2-sys"
|
||||
version = "0.1.13+1.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cairo-rs"
|
||||
version = "0.18.5"
|
||||
@@ -794,6 +844,16 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cipher"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
|
||||
dependencies = [
|
||||
"crypto-common",
|
||||
"inout",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "3.2.25"
|
||||
@@ -821,12 +881,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "coco"
|
||||
version = "0.5.0"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"applications",
|
||||
"async-recursion",
|
||||
"async-trait",
|
||||
"base64 0.13.1",
|
||||
"borrowme",
|
||||
"chinese-number",
|
||||
"chrono",
|
||||
"derive_more 2.0.1",
|
||||
@@ -884,6 +946,7 @@ dependencies = [
|
||||
"tungstenite 0.24.0",
|
||||
"url",
|
||||
"walkdir",
|
||||
"zip 4.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -991,6 +1054,12 @@ dependencies = [
|
||||
"tiny-keccak",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "constant_time_eq"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.4.0"
|
||||
@@ -1263,6 +1332,12 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deflate64"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b"
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.4.0"
|
||||
@@ -1326,6 +1401,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"crypto-common",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1768,6 +1844,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"libz-rs-sys",
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
@@ -2505,6 +2582,15 @@ version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "hmac"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
|
||||
dependencies = [
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hostname"
|
||||
version = "0.3.1"
|
||||
@@ -2948,6 +3034,15 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inout"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "interpolate_name"
|
||||
version = "0.2.4"
|
||||
@@ -3213,6 +3308,26 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "liblzma"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "66352d7a8ac12d4877b6e6ea5a9b7650ee094257dc40889955bea5bc5b08c1d0"
|
||||
dependencies = [
|
||||
"liblzma-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "liblzma-sys"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "01b9596486f6d60c3bbe644c0e1be1aa6ccc472ad630fe8927b456973d7cb736"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libredox"
|
||||
version = "0.1.3"
|
||||
@@ -3224,6 +3339,15 @@ dependencies = [
|
||||
"redox_syscall",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libz-rs-sys"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "172a788537a2221661b480fee8dc5f96c580eb34fa88764d3205dc356c7e4221"
|
||||
dependencies = [
|
||||
"zlib-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linked-hash-map"
|
||||
version = "0.5.6"
|
||||
@@ -4271,6 +4395,16 @@ version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
|
||||
|
||||
[[package]]
|
||||
name = "pbkdf2"
|
||||
version = "0.12.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
|
||||
dependencies = [
|
||||
"digest",
|
||||
"hmac",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.1"
|
||||
@@ -5439,6 +5573,7 @@ version = "1.0.140"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
|
||||
dependencies = [
|
||||
"indexmap 2.9.0",
|
||||
"itoa 1.0.15",
|
||||
"memchr",
|
||||
"ryu",
|
||||
@@ -6424,7 +6559,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"url",
|
||||
"windows-sys 0.59.0",
|
||||
"zip",
|
||||
"zip 2.6.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8449,6 +8584,20 @@ name = "zeroize"
|
||||
version = "1.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
|
||||
dependencies = [
|
||||
"zeroize_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize_derive"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.101",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerotrie"
|
||||
@@ -8496,6 +8645,78 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "4.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "153a6fff49d264c4babdcfa6b4d534747f520e56e8f0f384f3b808c4b64cc1fd"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"arbitrary",
|
||||
"bzip2",
|
||||
"constant_time_eq",
|
||||
"crc32fast",
|
||||
"deflate64",
|
||||
"flate2",
|
||||
"getrandom 0.3.2",
|
||||
"hmac",
|
||||
"indexmap 2.9.0",
|
||||
"liblzma",
|
||||
"memchr",
|
||||
"pbkdf2",
|
||||
"sha1",
|
||||
"time",
|
||||
"zeroize",
|
||||
"zopfli",
|
||||
"zstd",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zlib-rs"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "626bd9fa9734751fc50d6060752170984d7053f5a39061f524cda68023d4db8a"
|
||||
|
||||
[[package]]
|
||||
name = "zopfli"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"crc32fast",
|
||||
"log",
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd"
|
||||
version = "0.13.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
|
||||
dependencies = [
|
||||
"zstd-safe",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd-safe"
|
||||
version = "7.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d"
|
||||
dependencies = [
|
||||
"zstd-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd-sys"
|
||||
version = "2.0.15+zstd.1.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zune-core"
|
||||
version = "0.4.12"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "coco"
|
||||
version = "0.5.0"
|
||||
version = "0.6.0"
|
||||
description = "Search, connect, collaborate – all in one place."
|
||||
authors = ["INFINI Labs"]
|
||||
edition = "2021"
|
||||
@@ -49,7 +49,7 @@ tauri-plugin-shell = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
# Need `arbitrary_precision` feature to support storing u128
|
||||
# see: https://docs.rs/serde_json/latest/serde_json/struct.Number.html#method.from_u128
|
||||
serde_json = { version = "1", features = ["arbitrary_precision"] }
|
||||
serde_json = { version = "1", features = ["arbitrary_precision", "preserve_order"] }
|
||||
tauri-plugin-http = "2"
|
||||
tauri-plugin-websocket = "2"
|
||||
tauri-plugin-deep-link = "2.0.0"
|
||||
@@ -83,7 +83,6 @@ walkdir = "2"
|
||||
log = "0.4"
|
||||
strsim = "0.10"
|
||||
futures-util = "0.3.31"
|
||||
url = "2.5.2"
|
||||
http = "1.1.0"
|
||||
tungstenite = "0.24.0"
|
||||
tokio-util = "0.7.14"
|
||||
@@ -98,7 +97,11 @@ derive_more = { version = "2.0.1", features = ["display"] }
|
||||
anyhow = "1.0.98"
|
||||
function_name = "0.3.0"
|
||||
regex = "1.11.1"
|
||||
borrowme = "0.0.15"
|
||||
tauri-plugin-opener = "2"
|
||||
async-recursion = "1.1.1"
|
||||
zip = "4.0.0"
|
||||
url = "2.5.2"
|
||||
|
||||
[target."cfg(target_os = \"macos\")".dependencies]
|
||||
tauri-nspanel = { git = "https://github.com/ahkohd/tauri-nspanel", branch = "v2" }
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"id": "AIOverview",
|
||||
"title": "AI Overview",
|
||||
"description": "...",
|
||||
"icon": "font_a-AIOverview",
|
||||
"type": "ai_extension",
|
||||
"enabled": true
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"id": "Applications",
|
||||
"platforms": ["macos", "linux", "windows"],
|
||||
"title": "Applications",
|
||||
"description": "...",
|
||||
"icon": "font_Application",
|
||||
"type": "group",
|
||||
"enabled": true
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"id": "Calculator",
|
||||
"title": "Calculator",
|
||||
"platforms": ["macos", "linux", "windows"],
|
||||
"description": "...",
|
||||
"icon": "font_Calculator",
|
||||
"type": "calculator",
|
||||
"enabled": true
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"id": "QuickAIAccess",
|
||||
"title": "Quick AI Access",
|
||||
"description": "...",
|
||||
"icon": "font_a-QuickAIAccess",
|
||||
"type": "ai_extension",
|
||||
"enabled": true
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Capability for the main window",
|
||||
"windows": ["main", "chat", "settings"],
|
||||
"windows": ["main", "chat", "settings", "check"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"core:event:allow-emit",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::common::assistant::ChatRequestMessage;
|
||||
use crate::common::http::GetResponse;
|
||||
use crate::common::http::{convert_query_params_to_strings, GetResponse};
|
||||
use crate::common::register::SearchSourceRegistry;
|
||||
use crate::server::http_client::HttpClient;
|
||||
use crate::{common, server::servers::COCO_SERVERS};
|
||||
@@ -20,17 +20,15 @@ pub async fn chat_history<R: Runtime>(
|
||||
size: u32,
|
||||
query: Option<String>,
|
||||
) -> Result<String, String> {
|
||||
let mut query_params: HashMap<String, Value> = HashMap::new();
|
||||
if from > 0 {
|
||||
query_params.insert("from".to_string(), from.into());
|
||||
}
|
||||
if size > 0 {
|
||||
query_params.insert("size".to_string(), size.into());
|
||||
}
|
||||
let mut query_params = Vec::new();
|
||||
|
||||
// Add from/size as number values
|
||||
query_params.push(format!("from={}", from));
|
||||
query_params.push(format!("size={}", size));
|
||||
|
||||
if let Some(query) = query {
|
||||
if !query.is_empty() {
|
||||
query_params.insert("query".to_string(), query.into());
|
||||
query_params.push(format!("query={}", query.to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,13 +50,11 @@ pub async fn session_chat_history<R: Runtime>(
|
||||
from: u32,
|
||||
size: u32,
|
||||
) -> Result<String, String> {
|
||||
let mut query_params: HashMap<String, Value> = HashMap::new();
|
||||
if from > 0 {
|
||||
query_params.insert("from".to_string(), from.into());
|
||||
}
|
||||
if size > 0 {
|
||||
query_params.insert("size".to_string(), size.into());
|
||||
}
|
||||
let mut query_params = Vec::new();
|
||||
|
||||
// Add from/size as number values
|
||||
query_params.push(format!("from={}", from));
|
||||
query_params.push(format!("size={}", size));
|
||||
|
||||
let path = format!("/chat/{}/_history", session_id);
|
||||
|
||||
@@ -75,10 +71,9 @@ pub async fn open_session_chat<R: Runtime>(
|
||||
server_id: String,
|
||||
session_id: String,
|
||||
) -> Result<String, String> {
|
||||
let query_params = HashMap::new();
|
||||
let path = format!("/chat/{}/_open", session_id);
|
||||
|
||||
let response = HttpClient::post(&server_id, path.as_str(), Some(query_params), None)
|
||||
let response = HttpClient::post(&server_id, path.as_str(), None, None)
|
||||
.await
|
||||
.map_err(|e| format!("Error open session: {}", e))?;
|
||||
|
||||
@@ -91,10 +86,9 @@ pub async fn close_session_chat<R: Runtime>(
|
||||
server_id: String,
|
||||
session_id: String,
|
||||
) -> Result<String, String> {
|
||||
let query_params = HashMap::new();
|
||||
let path = format!("/chat/{}/_close", session_id);
|
||||
|
||||
let response = HttpClient::post(&server_id, path.as_str(), Some(query_params), None)
|
||||
let response = HttpClient::post(&server_id, path.as_str(), None, None)
|
||||
.await
|
||||
.map_err(|e| format!("Error close session: {}", e))?;
|
||||
|
||||
@@ -106,10 +100,9 @@ pub async fn cancel_session_chat<R: Runtime>(
|
||||
server_id: String,
|
||||
session_id: String,
|
||||
) -> Result<String, String> {
|
||||
let query_params = HashMap::new();
|
||||
let path = format!("/chat/{}/_cancel", session_id);
|
||||
|
||||
let response = HttpClient::post(&server_id, path.as_str(), Some(query_params), None)
|
||||
let response = HttpClient::post(&server_id, path.as_str(), None, None)
|
||||
.await
|
||||
.map_err(|e| format!("Error cancel session: {}", e))?;
|
||||
|
||||
@@ -140,10 +133,15 @@ pub async fn new_chat<R: Runtime>(
|
||||
let mut headers = HashMap::new();
|
||||
headers.insert("WEBSOCKET-SESSION-ID".to_string(), websocket_id.into());
|
||||
|
||||
let response =
|
||||
HttpClient::advanced_post(&server_id, "/chat/_new", Some(headers), query_params, body)
|
||||
.await
|
||||
.map_err(|e| format!("Error sending message: {}", e))?;
|
||||
let response = HttpClient::advanced_post(
|
||||
&server_id,
|
||||
"/chat/_new",
|
||||
Some(headers),
|
||||
convert_query_params_to_strings(query_params),
|
||||
body,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| format!("Error sending message: {}", e))?;
|
||||
|
||||
let body_text = common::http::get_response_body_text(response).await?;
|
||||
|
||||
@@ -181,12 +179,11 @@ pub async fn send_message<R: Runtime>(
|
||||
&server_id,
|
||||
path.as_str(),
|
||||
Some(headers),
|
||||
query_params,
|
||||
convert_query_params_to_strings(query_params),
|
||||
Some(body),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| format!("Error cancel session: {}", e))?;
|
||||
|
||||
.await
|
||||
.map_err(|e| format!("Error cancel session: {}", e))?;
|
||||
|
||||
common::http::get_response_body_text(response).await
|
||||
}
|
||||
@@ -228,8 +225,8 @@ pub async fn update_session_chat(
|
||||
None,
|
||||
Some(reqwest::Body::from(serde_json::to_string(&body).unwrap())),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| format!("Error updating session: {}", e))?;
|
||||
.await
|
||||
.map_err(|e| format!("Error updating session: {}", e))?;
|
||||
|
||||
Ok(response.status().is_success())
|
||||
}
|
||||
@@ -238,25 +235,9 @@ pub async fn update_session_chat(
|
||||
pub async fn assistant_search<R: Runtime>(
|
||||
_app_handle: AppHandle<R>,
|
||||
server_id: String,
|
||||
from: u32,
|
||||
size: u32,
|
||||
query: Option<HashMap<String, Value>>,
|
||||
query_params: Option<Vec<String>>,
|
||||
) -> Result<Value, String> {
|
||||
let mut body = serde_json::json!({
|
||||
"from": from,
|
||||
"size": size,
|
||||
});
|
||||
|
||||
if let Some(q) = query {
|
||||
body["query"] = serde_json::to_value(q).map_err(|e| e.to_string())?;
|
||||
}
|
||||
|
||||
let response = HttpClient::post(
|
||||
&server_id,
|
||||
"/assistant/_search",
|
||||
None,
|
||||
Some(reqwest::Body::from(body.to_string())),
|
||||
)
|
||||
let response = HttpClient::post(&server_id, "/assistant/_search", query_params, None)
|
||||
.await
|
||||
.map_err(|e| format!("Error searching assistants: {}", e))?;
|
||||
|
||||
@@ -277,8 +258,8 @@ pub async fn assistant_get<R: Runtime>(
|
||||
&format!("/assistant/{}", assistant_id),
|
||||
None, // headers
|
||||
)
|
||||
.await
|
||||
.map_err(|e| format!("Error getting assistant: {}", e))?;
|
||||
.await
|
||||
.map_err(|e| format!("Error getting assistant: {}", e))?;
|
||||
|
||||
response
|
||||
.json::<Value>()
|
||||
@@ -318,7 +299,7 @@ pub async fn assistant_get_multi<R: Runtime>(
|
||||
&path,
|
||||
None, // headers
|
||||
)
|
||||
.await;
|
||||
.await;
|
||||
match res_response {
|
||||
Ok(response) => response
|
||||
.json::<serde_json::Value>()
|
||||
@@ -380,7 +361,8 @@ pub fn remove_icon_fields(json: &str) -> String {
|
||||
} else {
|
||||
"".to_string()
|
||||
}
|
||||
}).to_string()
|
||||
})
|
||||
.to_string()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -407,7 +389,7 @@ pub async fn ask_ai<R: Runtime>(
|
||||
None,
|
||||
Some(reqwest::Body::from(body.to_string())),
|
||||
)
|
||||
.await?;
|
||||
.await?;
|
||||
|
||||
if response.status() == 429 {
|
||||
log::warn!("Rate limit exceeded for assistant: {}", &assistant_id);
|
||||
|
||||
@@ -51,7 +51,9 @@ impl OnOpened {
|
||||
const WHITESPACE: &str = " ";
|
||||
let mut ret = action.exec.clone();
|
||||
ret.push_str(WHITESPACE);
|
||||
ret.push_str(action.args.join(WHITESPACE).as_str());
|
||||
if let Some(ref args) = action.args {
|
||||
ret.push_str(args.join(WHITESPACE).as_str());
|
||||
}
|
||||
|
||||
ret
|
||||
}
|
||||
@@ -80,9 +82,25 @@ pub(crate) async fn open(on_opened: OnOpened) -> Result<(), String> {
|
||||
}
|
||||
OnOpened::Command { action } => {
|
||||
let mut cmd = Command::new(action.exec);
|
||||
cmd.args(action.args);
|
||||
if let Some(args) = action.args {
|
||||
cmd.args(args);
|
||||
}
|
||||
let output = cmd.output().map_err(|e| e.to_string())?;
|
||||
// Sometimes, we wanna see the result in logs even though it doesn't fail.
|
||||
log::debug!(
|
||||
"executing open(Command) result, exit code: [{}], stdout: [{}], stderr: [{}]",
|
||||
output.status,
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
if !output.status.success() {
|
||||
log::warn!(
|
||||
"executing open(Command) failed, exit code: [{}], stdout: [{}], stderr: [{}]",
|
||||
output.status,
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
|
||||
return Err(format!(
|
||||
"Command failed, stderr [{}]",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
|
||||
@@ -2,6 +2,8 @@ use crate::common;
|
||||
use reqwest::Response;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
use tauri_plugin_store::JsonValue;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct GetResponse {
|
||||
@@ -54,3 +56,25 @@ pub async fn get_response_body_text(response: Response) -> Result<String, String
|
||||
Ok(body)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub fn convert_query_params_to_strings(
|
||||
query_params: Option<HashMap<String, JsonValue>>,
|
||||
) -> Option<Vec<String>> {
|
||||
query_params.map(|map| {
|
||||
map.into_iter()
|
||||
.filter_map(|(k, v)| match v {
|
||||
JsonValue::String(s) => Some(format!("{}={}", k, s)),
|
||||
JsonValue::Number(n) => Some(format!("{}={}", k, n)),
|
||||
JsonValue::Bool(b) => Some(format!("{}={}", k, b)),
|
||||
_ => {
|
||||
eprintln!(
|
||||
"Skipping unsupported query value for key '{}': {:?}",
|
||||
k, v
|
||||
);
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
@@ -1,16 +1,17 @@
|
||||
pub mod health;
|
||||
pub mod profile;
|
||||
pub mod server;
|
||||
pub mod auth;
|
||||
pub mod datasource;
|
||||
pub mod connector;
|
||||
pub mod search;
|
||||
pub mod document;
|
||||
pub mod traits;
|
||||
pub mod register;
|
||||
pub mod assistant;
|
||||
pub mod http;
|
||||
pub mod auth;
|
||||
pub mod connector;
|
||||
pub mod datasource;
|
||||
pub mod document;
|
||||
pub mod error;
|
||||
pub mod health;
|
||||
pub mod http;
|
||||
pub mod profile;
|
||||
pub mod register;
|
||||
pub mod search;
|
||||
pub mod server;
|
||||
pub mod traits;
|
||||
|
||||
pub static MAIN_WINDOW_LABEL: &str = "main";
|
||||
pub static SETTINGS_WINDOW_LABEL: &str = "settings";
|
||||
pub static CHECK_WINDOW_LABEL: &str = "check";
|
||||
|
||||
@@ -1 +1,13 @@
|
||||
pub(super) const EXTENSION_ID: &str = "AIOverview";
|
||||
pub(super) const EXTENSION_ID: &str = "AIOverview";
|
||||
|
||||
/// JSON file for this extension.
|
||||
pub(crate) const PLUGIN_JSON_FILE: &str = r#"
|
||||
{
|
||||
"id": "AIOverview",
|
||||
"name": "AI Overview",
|
||||
"description": "...",
|
||||
"icon": "font_a-AIOverview",
|
||||
"type": "ai_extension",
|
||||
"enabled": true
|
||||
}
|
||||
"#;
|
||||
|
||||
@@ -33,3 +33,16 @@ pub struct AppMetadata {
|
||||
modified: u128,
|
||||
last_opened: u128,
|
||||
}
|
||||
|
||||
/// JSON file for this extension.
|
||||
pub(crate) const PLUGIN_JSON_FILE: &str = r#"
|
||||
{
|
||||
"id": "Applications",
|
||||
"platforms": ["macos", "linux", "windows"],
|
||||
"name": "Applications",
|
||||
"description": "Application search",
|
||||
"icon": "font_Application",
|
||||
"type": "group",
|
||||
"enabled": true
|
||||
}
|
||||
"#;
|
||||
@@ -25,6 +25,7 @@ use pizza_engine::store::{DiskStore, DiskStoreSnapshot};
|
||||
use pizza_engine::writer::Writer;
|
||||
use pizza_engine::{doc, Engine, EngineBuilder};
|
||||
use serde_json::Value as Json;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use tauri::{async_runtime, AppHandle, Manager, Runtime};
|
||||
use tauri_plugin_fs_pro::{icon, metadata, name, IconOptions};
|
||||
@@ -47,6 +48,8 @@ const TAURI_STORE_APP_ALIAS: &str = "app_alias";
|
||||
const TAURI_STORE_KEY_SEARCH_PATH: &str = "search_path";
|
||||
const TAURI_STORE_KEY_DISABLED_APP_LIST: &str = "disabled_app_list";
|
||||
|
||||
const INDEX_DIR: &str = "local_application_index";
|
||||
|
||||
/// We use this as:
|
||||
///
|
||||
/// 1. querysource ID
|
||||
@@ -209,6 +212,86 @@ impl SearchSourceState for ApplicationSearchSourceState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Index applications if they have not been indexed (by checking if `app_index_dir` exists).
|
||||
async fn index_applications_if_not_indexed<R: Runtime>(
|
||||
tauri_app_handle: &AppHandle<R>,
|
||||
app_index_dir: &Path,
|
||||
) -> anyhow::Result<ApplicationSearchSourceState> {
|
||||
let index_exists = app_index_dir.exists();
|
||||
|
||||
let mut pizza_engine_builder = EngineBuilder::new();
|
||||
let disk_store = DiskStore::new(&app_index_dir)?;
|
||||
pizza_engine_builder.set_data_store(disk_store);
|
||||
|
||||
let mut schema = Schema::new();
|
||||
let field_app_name = Property::builder(FieldType::Text).build();
|
||||
schema
|
||||
.add_property(FIELD_APP_NAME, field_app_name)
|
||||
.expect("no collision could happen");
|
||||
let property_icon = Property::builder(FieldType::Text).index(false).build();
|
||||
schema
|
||||
.add_property(FIELD_ICON_PATH, property_icon)
|
||||
.expect("no collision could happen");
|
||||
schema
|
||||
.add_property(FIELD_APP_ALIAS, Property::as_text(None))
|
||||
.expect("no collision could happen");
|
||||
schema.freeze();
|
||||
pizza_engine_builder.set_schema(schema);
|
||||
|
||||
let pizza_engine = pizza_engine_builder
|
||||
.build()
|
||||
.unwrap_or_else(|e| panic!("failed to build Pizza engine due to [{}]", e));
|
||||
pizza_engine.start();
|
||||
let mut writer = pizza_engine.acquire_writer();
|
||||
|
||||
if !index_exists {
|
||||
let default_search_path = get_default_search_paths();
|
||||
let apps = list_app_in(default_search_path).map_err(|str| anyhow::anyhow!(str))?;
|
||||
|
||||
for app in apps.iter() {
|
||||
let app_path = get_app_path(app);
|
||||
let app_name = get_app_name(app).await;
|
||||
let app_icon_path = get_app_icon_path(&tauri_app_handle, app)
|
||||
.await
|
||||
.map_err(|str| anyhow::anyhow!(str))?;
|
||||
let app_alias = get_app_alias(&tauri_app_handle, &app_path).unwrap_or(String::new());
|
||||
|
||||
if app_name.is_empty() || app_name.eq(&tauri_app_handle.package_info().name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// You cannot write `app_name.clone()` within the `doc!()` macro, we should fix this.
|
||||
let app_name_clone = app_name.clone();
|
||||
let app_path_clone = app_path.clone();
|
||||
let document = doc!( app_path_clone, {
|
||||
FIELD_APP_NAME => app_name_clone,
|
||||
FIELD_ICON_PATH => app_icon_path,
|
||||
FIELD_APP_ALIAS => app_alias,
|
||||
}
|
||||
);
|
||||
|
||||
// We don't error out because one failure won't break the whole thing
|
||||
if let Err(e) = writer.create_document(document).await {
|
||||
warn!(
|
||||
"failed to index application [app name: '{}', app path: '{}'] due to error [{}]", app_name, app_path, e
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
writer.commit()?;
|
||||
}
|
||||
|
||||
let snapshot = pizza_engine.create_snapshot();
|
||||
let searcher = pizza_engine.acquire_searcher();
|
||||
|
||||
Ok(ApplicationSearchSourceState {
|
||||
searcher,
|
||||
snapshot,
|
||||
engine: pizza_engine,
|
||||
writer,
|
||||
})
|
||||
}
|
||||
|
||||
/// Upon application start, index all the applications found in the `get_default_search_paths()`.
|
||||
struct IndexAllApplicationsTask<R: Runtime> {
|
||||
tauri_app_handle: AppHandle<R>,
|
||||
@@ -228,87 +311,47 @@ impl<R: Runtime> Task for IndexAllApplicationsTask<R> {
|
||||
.path()
|
||||
.app_data_dir()
|
||||
.expect("failed to find the local dir");
|
||||
app_index_dir.push("local_application_index");
|
||||
app_index_dir.push(INDEX_DIR);
|
||||
let app_search_source_state = task_exec_try!(
|
||||
index_applications_if_not_indexed(&self.tauri_app_handle, &app_index_dir).await,
|
||||
callback
|
||||
);
|
||||
*state = Some(Box::new(app_search_source_state));
|
||||
callback.send(Ok(())).expect("rx dropped");
|
||||
}
|
||||
}
|
||||
|
||||
let index_exists = app_index_dir.exists();
|
||||
struct ReindexAllApplicationsTask<R: Runtime> {
|
||||
tauri_app_handle: AppHandle<R>,
|
||||
callback: Option<tokio::sync::oneshot::Sender<Result<(), String>>>,
|
||||
}
|
||||
|
||||
let mut pizza_engine_builder = EngineBuilder::new();
|
||||
let disk_store = task_exec_try!(DiskStore::new(&app_index_dir), callback);
|
||||
pizza_engine_builder.set_data_store(disk_store);
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl<R: Runtime> Task for ReindexAllApplicationsTask<R> {
|
||||
fn search_source_id(&self) -> &'static str {
|
||||
APPLICATION_SEARCH_SOURCE_ID
|
||||
}
|
||||
|
||||
let mut schema = Schema::new();
|
||||
let field_app_name = Property::builder(FieldType::Text).build();
|
||||
schema
|
||||
.add_property(FIELD_APP_NAME, field_app_name)
|
||||
.expect("no collision could happen");
|
||||
let property_icon = Property::builder(FieldType::Text).index(false).build();
|
||||
schema
|
||||
.add_property(FIELD_ICON_PATH, property_icon)
|
||||
.expect("no collision could happen");
|
||||
schema
|
||||
.add_property(FIELD_APP_ALIAS, Property::as_text(None))
|
||||
.expect("no collision could happen");
|
||||
schema.freeze();
|
||||
pizza_engine_builder.set_schema(schema);
|
||||
async fn exec(&mut self, state: &mut Option<Box<dyn SearchSourceState>>) {
|
||||
let callback = self.callback.take().unwrap();
|
||||
|
||||
let pizza_engine = pizza_engine_builder
|
||||
.build()
|
||||
.unwrap_or_else(|e| panic!("failed to build Pizza engine due to [{}]", e));
|
||||
pizza_engine.start();
|
||||
let mut writer = pizza_engine.acquire_writer();
|
||||
// Clear the state
|
||||
*state = None;
|
||||
let mut app_index_dir = self
|
||||
.tauri_app_handle
|
||||
.path()
|
||||
.app_data_dir()
|
||||
.expect("failed to find the local dir");
|
||||
app_index_dir.push(INDEX_DIR);
|
||||
task_exec_try!(tokio::fs::remove_dir_all(&app_index_dir).await, callback);
|
||||
|
||||
if !index_exists {
|
||||
let default_search_path = get_default_search_paths();
|
||||
let apps = task_exec_try!(list_app_in(default_search_path), callback);
|
||||
|
||||
for app in apps.iter() {
|
||||
let app_path = get_app_path(app);
|
||||
let app_name = get_app_name(app).await;
|
||||
let app_icon_path = task_exec_try!(
|
||||
get_app_icon_path(&self.tauri_app_handle, app).await,
|
||||
callback
|
||||
);
|
||||
let app_alias =
|
||||
get_app_alias(&self.tauri_app_handle, &app_path).unwrap_or(String::new());
|
||||
|
||||
if app_name.is_empty() || app_name.eq(&self.tauri_app_handle.package_info().name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// You cannot write `app_name.clone()` within the `doc!()` macro, we should fix this.
|
||||
let app_name_clone = app_name.clone();
|
||||
let app_path_clone = app_path.clone();
|
||||
let document = doc!( app_path_clone, {
|
||||
FIELD_APP_NAME => app_name_clone,
|
||||
FIELD_ICON_PATH => app_icon_path,
|
||||
FIELD_APP_ALIAS => app_alias,
|
||||
}
|
||||
);
|
||||
|
||||
// We don't error out because one failure won't break the whole thing
|
||||
if let Err(e) = writer.create_document(document).await {
|
||||
warn!(
|
||||
"failed to index application [app name: '{}', app path: '{}'] due to error [{}]", app_name, app_path, e
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
task_exec_try!(writer.commit(), callback);
|
||||
}
|
||||
|
||||
let snapshot = pizza_engine.create_snapshot();
|
||||
let searcher = pizza_engine.acquire_searcher();
|
||||
|
||||
let state_to_store = Box::new(ApplicationSearchSourceState {
|
||||
searcher,
|
||||
snapshot,
|
||||
engine: pizza_engine,
|
||||
writer,
|
||||
}) as Box<dyn SearchSourceState>;
|
||||
|
||||
*state = Some(state_to_store);
|
||||
|
||||
callback.send(Ok(())).unwrap();
|
||||
// Then re-index the apps
|
||||
let app_search_source_state = task_exec_try!(
|
||||
index_applications_if_not_indexed(&self.tauri_app_handle, &app_index_dir).await,
|
||||
callback
|
||||
);
|
||||
*state = Some(Box::new(app_search_source_state));
|
||||
callback.send(Ok(())).expect("rx dropped");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -326,6 +369,23 @@ impl<R: Runtime> Task for SearchApplicationsTask<R> {
|
||||
|
||||
async fn exec(&mut self, state: &mut Option<Box<dyn SearchSourceState>>) {
|
||||
let callback = self.callback.take().unwrap();
|
||||
|
||||
let Some(state) = state.as_mut() else {
|
||||
let empty_hits = SearchResult {
|
||||
tracing_id: String::new(),
|
||||
explains: None,
|
||||
total_hits: 0,
|
||||
hits: None,
|
||||
};
|
||||
|
||||
let rx_dropped_error = callback.send(Ok(empty_hits)).is_err();
|
||||
if rx_dropped_error {
|
||||
warn!("failed to send local app search result back because the corresponding channel receiver end has been unexpected dropped, which could happen due to a low query timeout")
|
||||
}
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
let disabled_app_list = get_disabled_app_list(&self.tauri_app_handle);
|
||||
|
||||
// TODO: search via alias, implement this when Pizza engine supports update
|
||||
@@ -344,8 +404,6 @@ impl<R: Runtime> Task for SearchApplicationsTask<R> {
|
||||
"{{ \"query\": {{ \"bool\": {{ \"should\": [ {{ \"match\": {{ \"{FIELD_APP_NAME}\": {:?} }} }}, {{ \"prefix\": {{ \"{FIELD_APP_NAME}\": {:?} }} }} ] }} }} }}", self.query_string, self.query_string);
|
||||
|
||||
let state = state
|
||||
.as_mut()
|
||||
.expect("should be set before")
|
||||
.as_mut_any()
|
||||
.downcast_mut::<ApplicationSearchSourceState>()
|
||||
.unwrap();
|
||||
@@ -428,7 +486,9 @@ impl Task for IndexNewApplicationsTask {
|
||||
pub struct ApplicationSearchSource;
|
||||
|
||||
impl ApplicationSearchSource {
|
||||
pub async fn init<R: Runtime>(app_handle: AppHandle<R>) -> Result<(), String> {
|
||||
pub async fn prepare_index_and_store<R: Runtime>(
|
||||
app_handle: AppHandle<R>,
|
||||
) -> Result<(), String> {
|
||||
let (tx, rx) = tokio::sync::oneshot::channel();
|
||||
let index_applications_task = IndexAllApplicationsTask {
|
||||
tauri_app_handle: app_handle.clone(),
|
||||
@@ -472,8 +532,6 @@ impl ApplicationSearchSource {
|
||||
.set(TAURI_STORE_KEY_SEARCH_PATH, default_search_path);
|
||||
}
|
||||
|
||||
register_app_hotkey_upon_start(app_handle.clone())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -638,14 +696,43 @@ fn app_hotkey_handler<R: Runtime>(
|
||||
}
|
||||
}
|
||||
|
||||
fn register_app_hotkey_upon_start<R: Runtime>(
|
||||
tauri_app_handle: AppHandle<R>,
|
||||
) -> Result<(), String> {
|
||||
/// For all the applications, if it is enabled & has hotkey set, then set it up.
|
||||
pub(crate) fn set_apps_hotkey<R: Runtime>(tauri_app_handle: &AppHandle<R>) -> Result<(), String> {
|
||||
let app_hotkey_store = tauri_app_handle
|
||||
.store(TAURI_STORE_APP_HOTKEY)
|
||||
.unwrap_or_else(|_| panic!("store [{}] not found/loaded", TAURI_STORE_APP_HOTKEY));
|
||||
|
||||
let disabled_app_list = get_disabled_app_list(&tauri_app_handle);
|
||||
|
||||
for (app_path, hotkey) in app_hotkey_store.entries() {
|
||||
if disabled_app_list.contains(&app_path) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let hotkey = match hotkey {
|
||||
Json::String(str) => str,
|
||||
_ => unreachable!("hotkey should be stored in a string"),
|
||||
};
|
||||
|
||||
set_app_hotkey(&tauri_app_handle, &app_path, &hotkey)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// For all the applications, if it is enabled & has hotkey set, then unset it.
|
||||
pub(crate) fn unset_apps_hotkey<R: Runtime>(tauri_app_handle: &AppHandle<R>) -> Result<(), String> {
|
||||
let app_hotkey_store = tauri_app_handle
|
||||
.store(TAURI_STORE_APP_HOTKEY)
|
||||
.unwrap_or_else(|_| panic!("store [{}] not found/loaded", TAURI_STORE_APP_HOTKEY));
|
||||
|
||||
let disabled_app_list = get_disabled_app_list(&tauri_app_handle);
|
||||
|
||||
for (app_path, hotkey) in app_hotkey_store.entries() {
|
||||
if disabled_app_list.contains(&app_path) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let hotkey = match hotkey {
|
||||
Json::String(str) => str,
|
||||
_ => unreachable!("hotkey should be stored in a string"),
|
||||
@@ -653,13 +740,25 @@ fn register_app_hotkey_upon_start<R: Runtime>(
|
||||
|
||||
tauri_app_handle
|
||||
.global_shortcut()
|
||||
.on_shortcut(hotkey.as_str(), app_hotkey_handler(app_path))
|
||||
.unregister(hotkey.as_str())
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set the hotkey but won't persist this settings change.
|
||||
pub(crate) fn set_app_hotkey<R: Runtime>(
|
||||
tauri_app_handle: &AppHandle<R>,
|
||||
app_path: &str,
|
||||
hotkey: &str,
|
||||
) -> Result<(), String> {
|
||||
tauri_app_handle
|
||||
.global_shortcut()
|
||||
.on_shortcut(hotkey, app_hotkey_handler(app_path.into()))
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
pub fn register_app_hotkey<R: Runtime>(
|
||||
tauri_app_handle: &AppHandle<R>,
|
||||
app_path: &str,
|
||||
@@ -671,13 +770,9 @@ pub fn register_app_hotkey<R: Runtime>(
|
||||
let app_hotkey_store = tauri_app_handle
|
||||
.store(TAURI_STORE_APP_HOTKEY)
|
||||
.unwrap_or_else(|_| panic!("store [{}] not found/loaded", TAURI_STORE_APP_HOTKEY));
|
||||
|
||||
app_hotkey_store.set(app_path, hotkey);
|
||||
|
||||
tauri_app_handle
|
||||
.global_shortcut()
|
||||
.on_shortcut(hotkey, app_hotkey_handler(app_path.into()))
|
||||
.map_err(|e| e.to_string())?;
|
||||
set_app_hotkey(tauri_app_handle, app_path, hotkey)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -789,6 +884,21 @@ pub fn disable_app_search<R: Runtime>(
|
||||
|
||||
store.set(TAURI_STORE_KEY_DISABLED_APP_LIST, disabled_app_list);
|
||||
|
||||
let app_hotkey_store = tauri_app_handle
|
||||
.store(TAURI_STORE_APP_HOTKEY)
|
||||
.unwrap_or_else(|_| panic!("store [{}] not found/loaded", TAURI_STORE_APP_HOTKEY));
|
||||
let opt_hokey = app_hotkey_store.get(app_path).map(|json| match json {
|
||||
Json::String(s) => s,
|
||||
_ => panic!("hotkey should be stored in a string"),
|
||||
});
|
||||
|
||||
if let Some(hotkey) = opt_hokey {
|
||||
tauri_app_handle
|
||||
.global_shortcut()
|
||||
.unregister(hotkey.as_str())
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -815,6 +925,18 @@ pub fn enable_app_search<R: Runtime>(
|
||||
disabled_app_list.remove(index);
|
||||
store.set(TAURI_STORE_KEY_DISABLED_APP_LIST, disabled_app_list);
|
||||
|
||||
let app_hotkey_store = tauri_app_handle
|
||||
.store(TAURI_STORE_APP_HOTKEY)
|
||||
.unwrap_or_else(|_| panic!("store [{}] not found/loaded", TAURI_STORE_APP_HOTKEY));
|
||||
let opt_hokey = app_hotkey_store.get(app_path).map(|json| match json {
|
||||
Json::String(s) => s,
|
||||
_ => panic!("hotkey should be stored in a string"),
|
||||
});
|
||||
|
||||
if let Some(hotkey) = opt_hokey {
|
||||
set_app_hotkey(tauri_app_handle, app_path, &hotkey)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
None => Err(format!(
|
||||
@@ -974,21 +1096,25 @@ pub async fn get_app_list<R: Runtime>(
|
||||
|
||||
let app_entry = Extension {
|
||||
id: path,
|
||||
title: name,
|
||||
name,
|
||||
platforms: None,
|
||||
developer: None,
|
||||
// Leave it empty as it won't be used
|
||||
description: String::new(),
|
||||
icon: icon_path,
|
||||
r#type: ExtensionType::Application,
|
||||
action: None,
|
||||
quick_link: None,
|
||||
quicklink: None,
|
||||
commands: None,
|
||||
scripts: None,
|
||||
quick_links: None,
|
||||
quicklinks: None,
|
||||
alias: Some(alias),
|
||||
hotkey,
|
||||
enabled,
|
||||
settings: None,
|
||||
screenshots: None,
|
||||
url: None,
|
||||
version: None,
|
||||
};
|
||||
|
||||
app_entries.push(app_entry);
|
||||
@@ -1037,3 +1163,30 @@ pub async fn get_app_metadata(app_name: String, app_path: String) -> Result<AppM
|
||||
last_opened,
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn reindex_applications<R: Runtime>(
|
||||
tauri_app_handle: AppHandle<R>,
|
||||
) -> Result<(), String> {
|
||||
let (tx, rx) = tokio::sync::oneshot::channel();
|
||||
let reindex_applications_task = ReindexAllApplicationsTask {
|
||||
tauri_app_handle: tauri_app_handle.clone(),
|
||||
callback: Some(tx),
|
||||
};
|
||||
|
||||
RUNTIME_TX
|
||||
.get()
|
||||
.unwrap()
|
||||
.send(Box::new(reindex_applications_task))
|
||||
.unwrap();
|
||||
|
||||
let reindexing_applications_result = rx.await.unwrap();
|
||||
if let Err(ref e) = reindexing_applications_result {
|
||||
error!(
|
||||
"re-indexing local applications failed, app search won't work, error [{}]",
|
||||
e
|
||||
)
|
||||
}
|
||||
|
||||
reindexing_applications_result
|
||||
}
|
||||
|
||||
@@ -12,7 +12,9 @@ pub(crate) const QUERYSOURCE_ID_DATASOURCE_ID_DATASOURCE_NAME: &str = "Applicati
|
||||
pub struct ApplicationSearchSource;
|
||||
|
||||
impl ApplicationSearchSource {
|
||||
pub async fn init<R: Runtime>(_app_handle: AppHandle<R>) -> Result<(), String> {
|
||||
pub async fn prepare_index_and_store<R: Runtime>(
|
||||
_app_handle: AppHandle<R>,
|
||||
) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -117,3 +119,23 @@ pub async fn get_app_metadata<R: Runtime>(
|
||||
) -> Result<AppMetadata, String> {
|
||||
unreachable!("app list should be empty, there is no way this can be invoked")
|
||||
}
|
||||
|
||||
pub(crate) fn set_apps_hotkey<R: Runtime>(_tauri_app_handle: &AppHandle<R>) -> Result<(), String> {
|
||||
// no-op
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn unset_apps_hotkey<R: Runtime>(
|
||||
_tauri_app_handle: &AppHandle<R>,
|
||||
) -> Result<(), String> {
|
||||
// no-op
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn reindex_applications<R: Runtime>(
|
||||
_tauri_app_handle: AppHandle<R>,
|
||||
) -> Result<(), String> {
|
||||
// no-op
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -13,6 +13,19 @@ use std::collections::HashMap;
|
||||
|
||||
pub(crate) const DATA_SOURCE_ID: &str = "Calculator";
|
||||
|
||||
/// JSON file for this extension.
|
||||
pub(crate) const PLUGIN_JSON_FILE: &str = r#"
|
||||
{
|
||||
"id": "Calculator",
|
||||
"name": "Calculator",
|
||||
"platforms": ["macos", "linux", "windows"],
|
||||
"description": "...",
|
||||
"icon": "font_Calculator",
|
||||
"type": "calculator",
|
||||
"enabled": true
|
||||
}
|
||||
"#;
|
||||
|
||||
pub struct CalculatorSource {
|
||||
base_score: f64,
|
||||
}
|
||||
|
||||
@@ -8,37 +8,186 @@ pub mod pizza_engine_runtime;
|
||||
pub mod quick_ai_access;
|
||||
|
||||
use super::Extension;
|
||||
use crate::extension::{alter_extension_json_file, load_extension_from_json_file};
|
||||
use crate::extension::built_in::application::{set_apps_hotkey, unset_apps_hotkey};
|
||||
use crate::extension::{
|
||||
alter_extension_json_file, ExtensionBundleIdBorrowed, PLUGIN_JSON_FILE_NAME,
|
||||
};
|
||||
use crate::{SearchSourceRegistry, GLOBAL_TAURI_APP_HANDLE};
|
||||
use std::path::PathBuf;
|
||||
use anyhow::Context;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::LazyLock;
|
||||
use tauri::path::BaseDirectory;
|
||||
use tauri::Manager;
|
||||
use tauri::{AppHandle, Manager, Runtime};
|
||||
|
||||
pub(crate) static BUILT_IN_EXTENSION_DIRECTORY: LazyLock<PathBuf> = LazyLock::new(|| {
|
||||
let mut resource_dir = GLOBAL_TAURI_APP_HANDLE
|
||||
.get()
|
||||
.expect("global tauri app handle not set")
|
||||
.path()
|
||||
.resolve("assets", BaseDirectory::Resource)
|
||||
.app_data_dir()
|
||||
.expect(
|
||||
"User home directory not found, which should be impossible on desktop environments",
|
||||
);
|
||||
resource_dir.push("extension");
|
||||
resource_dir.push("built_in_extensions");
|
||||
|
||||
resource_dir
|
||||
});
|
||||
|
||||
pub(super) async fn init_built_in_extension(
|
||||
/// Helper function to load the built-in extension specified by `extension_id`, used
|
||||
/// in `list_built_in_extensions()`.
|
||||
///
|
||||
/// For built-in extensions, users are only allowed to edit these fields:
|
||||
///
|
||||
/// 1. alias (if this extension supports alias)
|
||||
/// 2. hotkey (if this extension supports hotkey)
|
||||
/// 3. enabled
|
||||
///
|
||||
/// If
|
||||
///
|
||||
/// 1. The above fields have invalid value
|
||||
/// 2. Other fields are modified
|
||||
///
|
||||
/// we ignore and reset them to the default value.
|
||||
async fn load_built_in_extension(
|
||||
built_in_extensions_dir: &Path,
|
||||
extension_id: &str,
|
||||
default_plugin_json_file: &str,
|
||||
) -> Result<Extension, String> {
|
||||
let mut extension_dir = built_in_extensions_dir.join(extension_id);
|
||||
let mut default_plugin_json = serde_json::from_str::<Extension>(&default_plugin_json_file).unwrap_or_else( |e| {
|
||||
panic!("the default extension {} file of built-in extension [{}] cannot be parsed as a valid [struct Extension], error [{}]", PLUGIN_JSON_FILE_NAME, extension_id, e);
|
||||
});
|
||||
|
||||
if !extension_dir.try_exists().map_err(|e| e.to_string())? {
|
||||
tokio::fs::create_dir_all(extension_dir.as_path())
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
|
||||
let plugin_json_file_path = {
|
||||
extension_dir.push(PLUGIN_JSON_FILE_NAME);
|
||||
extension_dir
|
||||
};
|
||||
|
||||
// If the JSON file does not exist, create a file with the default template and return.
|
||||
if !plugin_json_file_path
|
||||
.try_exists()
|
||||
.map_err(|e| e.to_string())?
|
||||
{
|
||||
tokio::fs::write(plugin_json_file_path, default_plugin_json_file)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
return Ok(default_plugin_json);
|
||||
}
|
||||
|
||||
let plugin_json_file_content = tokio::fs::read_to_string(plugin_json_file_path.as_path())
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
let res_plugin_json = serde_json::from_str::<Extension>(&plugin_json_file_content);
|
||||
let Ok(plugin_json) = res_plugin_json else {
|
||||
log::warn!("user invalidated built-in extension [{}] file, overwriting it with the default template", extension_id);
|
||||
|
||||
// If the JSON file cannot be parsed as `struct Extension`, overwrite it with the default template and return.
|
||||
tokio::fs::write(plugin_json_file_path, default_plugin_json_file)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
return Ok(default_plugin_json);
|
||||
};
|
||||
|
||||
// Users are only allowed to edit the below fields
|
||||
// 1. alias (if this extension supports alias)
|
||||
// 2. hotkey (if this extension supports hotkey)
|
||||
// 3. enabled
|
||||
// so we ignore all other fields.
|
||||
let alias = if default_plugin_json.supports_alias_hotkey() {
|
||||
plugin_json.alias.clone()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let hotkey = if default_plugin_json.supports_alias_hotkey() {
|
||||
plugin_json.hotkey.clone()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let enabled = plugin_json.enabled;
|
||||
|
||||
default_plugin_json.alias = alias;
|
||||
default_plugin_json.hotkey = hotkey;
|
||||
default_plugin_json.enabled = enabled;
|
||||
|
||||
let final_plugin_json_file_content = serde_json::to_string_pretty(&default_plugin_json)
|
||||
.expect("failed to serialize `struct Extension`");
|
||||
tokio::fs::write(plugin_json_file_path, final_plugin_json_file_content)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(default_plugin_json)
|
||||
}
|
||||
|
||||
/// Return the built-in extension list.
|
||||
///
|
||||
/// Will create extension files when they are not found.
|
||||
///
|
||||
/// Users may put extension files in the built-in extension directory, but
|
||||
/// we do not care and will ignore them.
|
||||
///
|
||||
/// We only read alias/hotkey/enabled from the JSON file, we have ensured that if
|
||||
/// alias/hotkey is not supported, then it will be `None`. Besides that, no further
|
||||
/// validation is needed because nothing could go wrong.
|
||||
pub(crate) async fn list_built_in_extensions() -> Result<Vec<Extension>, String> {
|
||||
let dir = BUILT_IN_EXTENSION_DIRECTORY.as_path();
|
||||
|
||||
let mut built_in_extensions = Vec::new();
|
||||
built_in_extensions.push(
|
||||
load_built_in_extension(
|
||||
dir,
|
||||
application::QUERYSOURCE_ID_DATASOURCE_ID_DATASOURCE_NAME,
|
||||
application::PLUGIN_JSON_FILE,
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
built_in_extensions.push(
|
||||
load_built_in_extension(
|
||||
dir,
|
||||
calculator::DATA_SOURCE_ID,
|
||||
calculator::PLUGIN_JSON_FILE,
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
built_in_extensions.push(
|
||||
load_built_in_extension(
|
||||
dir,
|
||||
ai_overview::EXTENSION_ID,
|
||||
ai_overview::PLUGIN_JSON_FILE,
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
built_in_extensions.push(
|
||||
load_built_in_extension(
|
||||
dir,
|
||||
quick_ai_access::EXTENSION_ID,
|
||||
quick_ai_access::PLUGIN_JSON_FILE,
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
|
||||
Ok(built_in_extensions)
|
||||
}
|
||||
|
||||
pub(super) async fn init_built_in_extension<R: Runtime>(
|
||||
tauri_app_handle: &AppHandle<R>,
|
||||
extension: &Extension,
|
||||
search_source_registry: &SearchSourceRegistry,
|
||||
) {
|
||||
) -> Result<(), String> {
|
||||
log::trace!("initializing built-in extensions");
|
||||
|
||||
if extension.id == application::QUERYSOURCE_ID_DATASOURCE_ID_DATASOURCE_NAME {
|
||||
search_source_registry
|
||||
.register_source(application::ApplicationSearchSource)
|
||||
.await;
|
||||
set_apps_hotkey(&tauri_app_handle)?;
|
||||
log::debug!("built-in extension [{}] initialized", extension.id);
|
||||
}
|
||||
|
||||
@@ -49,36 +198,17 @@ pub(super) async fn init_built_in_extension(
|
||||
.await;
|
||||
log::debug!("built-in extension [{}] initialized", extension.id);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn is_extension_built_in(extension_id: &str) -> bool {
|
||||
if extension_id == application::QUERYSOURCE_ID_DATASOURCE_ID_DATASOURCE_NAME {
|
||||
return true;
|
||||
}
|
||||
|
||||
if extension_id.starts_with(&format!(
|
||||
"{}.",
|
||||
application::QUERYSOURCE_ID_DATASOURCE_ID_DATASOURCE_NAME
|
||||
)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if extension_id == calculator::DATA_SOURCE_ID {
|
||||
return true;
|
||||
}
|
||||
|
||||
if extension_id == quick_ai_access::EXTENSION_ID {
|
||||
return true;
|
||||
}
|
||||
|
||||
if extension_id == ai_overview::EXTENSION_ID {
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
pub(crate) fn is_extension_built_in(bundle_id: &ExtensionBundleIdBorrowed<'_>) -> bool {
|
||||
bundle_id.developer.is_none()
|
||||
}
|
||||
|
||||
pub(crate) async fn enable_built_in_extension(extension_id: &str) -> Result<(), String> {
|
||||
pub(crate) async fn enable_built_in_extension(
|
||||
bundle_id: &ExtensionBundleIdBorrowed<'_>,
|
||||
) -> Result<(), String> {
|
||||
let tauri_app_handle = GLOBAL_TAURI_APP_HANDLE
|
||||
.get()
|
||||
.expect("global tauri app handle not set");
|
||||
@@ -89,13 +219,17 @@ pub(crate) async fn enable_built_in_extension(extension_id: &str) -> Result<(),
|
||||
Ok(())
|
||||
};
|
||||
|
||||
if extension_id == application::QUERYSOURCE_ID_DATASOURCE_ID_DATASOURCE_NAME {
|
||||
if bundle_id.extension_id == application::QUERYSOURCE_ID_DATASOURCE_ID_DATASOURCE_NAME
|
||||
&& bundle_id.sub_extension_id.is_none()
|
||||
{
|
||||
search_source_registry_tauri_state
|
||||
.register_source(application::ApplicationSearchSource)
|
||||
.await;
|
||||
set_apps_hotkey(tauri_app_handle)?;
|
||||
|
||||
alter_extension_json_file(
|
||||
&BUILT_IN_EXTENSION_DIRECTORY.as_path(),
|
||||
extension_id,
|
||||
bundle_id,
|
||||
update_extension,
|
||||
)?;
|
||||
|
||||
@@ -103,42 +237,40 @@ pub(crate) async fn enable_built_in_extension(extension_id: &str) -> Result<(),
|
||||
}
|
||||
|
||||
// Check if this is an application
|
||||
let application_prefix = format!(
|
||||
"{}.",
|
||||
application::QUERYSOURCE_ID_DATASOURCE_ID_DATASOURCE_NAME
|
||||
);
|
||||
if extension_id.starts_with(&application_prefix) {
|
||||
let app_path = &extension_id[application_prefix.len()..];
|
||||
if bundle_id.extension_id == application::QUERYSOURCE_ID_DATASOURCE_ID_DATASOURCE_NAME
|
||||
&& bundle_id.sub_extension_id.is_some()
|
||||
{
|
||||
let app_path = bundle_id.sub_extension_id.expect("just checked it is Some");
|
||||
application::enable_app_search(tauri_app_handle, app_path)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if extension_id == calculator::DATA_SOURCE_ID {
|
||||
if bundle_id.extension_id == calculator::DATA_SOURCE_ID {
|
||||
let calculator_search = calculator::CalculatorSource::new(2000f64);
|
||||
search_source_registry_tauri_state
|
||||
.register_source(calculator_search)
|
||||
.await;
|
||||
alter_extension_json_file(
|
||||
&BUILT_IN_EXTENSION_DIRECTORY.as_path(),
|
||||
extension_id,
|
||||
bundle_id,
|
||||
update_extension,
|
||||
)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if extension_id == quick_ai_access::EXTENSION_ID {
|
||||
if bundle_id.extension_id == quick_ai_access::EXTENSION_ID {
|
||||
alter_extension_json_file(
|
||||
&BUILT_IN_EXTENSION_DIRECTORY.as_path(),
|
||||
extension_id,
|
||||
bundle_id,
|
||||
update_extension,
|
||||
)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if extension_id == ai_overview::EXTENSION_ID {
|
||||
if bundle_id.extension_id == ai_overview::EXTENSION_ID {
|
||||
alter_extension_json_file(
|
||||
&BUILT_IN_EXTENSION_DIRECTORY.as_path(),
|
||||
extension_id,
|
||||
bundle_id,
|
||||
update_extension,
|
||||
)?;
|
||||
return Ok(());
|
||||
@@ -147,7 +279,9 @@ pub(crate) async fn enable_built_in_extension(extension_id: &str) -> Result<(),
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn disable_built_in_extension(extension_id: &str) -> Result<(), String> {
|
||||
pub(crate) async fn disable_built_in_extension(
|
||||
bundle_id: &ExtensionBundleIdBorrowed<'_>,
|
||||
) -> Result<(), String> {
|
||||
let tauri_app_handle = GLOBAL_TAURI_APP_HANDLE
|
||||
.get()
|
||||
.expect("global tauri app handle not set");
|
||||
@@ -158,55 +292,57 @@ pub(crate) async fn disable_built_in_extension(extension_id: &str) -> Result<(),
|
||||
Ok(())
|
||||
};
|
||||
|
||||
if extension_id == application::QUERYSOURCE_ID_DATASOURCE_ID_DATASOURCE_NAME {
|
||||
if bundle_id.extension_id == application::QUERYSOURCE_ID_DATASOURCE_ID_DATASOURCE_NAME
|
||||
&& bundle_id.sub_extension_id.is_none()
|
||||
{
|
||||
search_source_registry_tauri_state
|
||||
.remove_source(extension_id)
|
||||
.remove_source(bundle_id.extension_id)
|
||||
.await;
|
||||
unset_apps_hotkey(tauri_app_handle)?;
|
||||
|
||||
alter_extension_json_file(
|
||||
&BUILT_IN_EXTENSION_DIRECTORY.as_path(),
|
||||
extension_id,
|
||||
bundle_id,
|
||||
update_extension,
|
||||
)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Check if this is an application
|
||||
let application_prefix = format!(
|
||||
"{}.",
|
||||
application::QUERYSOURCE_ID_DATASOURCE_ID_DATASOURCE_NAME
|
||||
);
|
||||
if extension_id.starts_with(&application_prefix) {
|
||||
let app_path = &extension_id[application_prefix.len()..];
|
||||
if bundle_id.extension_id == application::QUERYSOURCE_ID_DATASOURCE_ID_DATASOURCE_NAME
|
||||
&& bundle_id.sub_extension_id.is_some()
|
||||
{
|
||||
let app_path = bundle_id.sub_extension_id.expect("just checked it is Some");
|
||||
application::disable_app_search(tauri_app_handle, app_path)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if extension_id == calculator::DATA_SOURCE_ID {
|
||||
if bundle_id.extension_id == calculator::DATA_SOURCE_ID {
|
||||
search_source_registry_tauri_state
|
||||
.remove_source(extension_id)
|
||||
.remove_source(bundle_id.extension_id)
|
||||
.await;
|
||||
alter_extension_json_file(
|
||||
&BUILT_IN_EXTENSION_DIRECTORY.as_path(),
|
||||
extension_id,
|
||||
bundle_id,
|
||||
update_extension,
|
||||
)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if extension_id == quick_ai_access::EXTENSION_ID {
|
||||
if bundle_id.extension_id == quick_ai_access::EXTENSION_ID {
|
||||
alter_extension_json_file(
|
||||
&BUILT_IN_EXTENSION_DIRECTORY.as_path(),
|
||||
extension_id,
|
||||
bundle_id,
|
||||
update_extension,
|
||||
)?;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if extension_id == ai_overview::EXTENSION_ID {
|
||||
if bundle_id.extension_id == ai_overview::EXTENSION_ID {
|
||||
alter_extension_json_file(
|
||||
&BUILT_IN_EXTENSION_DIRECTORY.as_path(),
|
||||
extension_id,
|
||||
bundle_id,
|
||||
update_extension,
|
||||
)?;
|
||||
|
||||
@@ -216,95 +352,131 @@ pub(crate) async fn disable_built_in_extension(extension_id: &str) -> Result<(),
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn set_built_in_extension_alias(extension_id: &str, alias: &str) {
|
||||
pub(crate) fn set_built_in_extension_alias(bundle_id: &ExtensionBundleIdBorrowed<'_>, alias: &str) {
|
||||
let tauri_app_handle = GLOBAL_TAURI_APP_HANDLE
|
||||
.get()
|
||||
.expect("global tauri app handle not set");
|
||||
|
||||
let application_prefix = format!(
|
||||
"{}.",
|
||||
application::QUERYSOURCE_ID_DATASOURCE_ID_DATASOURCE_NAME
|
||||
);
|
||||
if extension_id.starts_with(&application_prefix) {
|
||||
let app_path = &extension_id[application_prefix.len()..];
|
||||
application::set_app_alias(tauri_app_handle, app_path, alias);
|
||||
if bundle_id.extension_id == application::QUERYSOURCE_ID_DATASOURCE_ID_DATASOURCE_NAME {
|
||||
if let Some(app_path) = bundle_id.sub_extension_id {
|
||||
application::set_app_alias(tauri_app_handle, app_path, alias);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn register_built_in_extension_hotkey(
|
||||
extension_id: &str,
|
||||
bundle_id: &ExtensionBundleIdBorrowed<'_>,
|
||||
hotkey: &str,
|
||||
) -> Result<(), String> {
|
||||
let tauri_app_handle = GLOBAL_TAURI_APP_HANDLE
|
||||
.get()
|
||||
.expect("global tauri app handle not set");
|
||||
let application_prefix = format!(
|
||||
"{}.",
|
||||
application::QUERYSOURCE_ID_DATASOURCE_ID_DATASOURCE_NAME
|
||||
);
|
||||
if extension_id.starts_with(&application_prefix) {
|
||||
let app_path = &extension_id[application_prefix.len()..];
|
||||
application::register_app_hotkey(&tauri_app_handle, app_path, hotkey)?;
|
||||
|
||||
if bundle_id.extension_id == application::QUERYSOURCE_ID_DATASOURCE_ID_DATASOURCE_NAME {
|
||||
if let Some(app_path) = bundle_id.sub_extension_id {
|
||||
application::register_app_hotkey(&tauri_app_handle, app_path, hotkey)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn unregister_built_in_extension_hotkey(extension_id: &str) -> Result<(), String> {
|
||||
pub(crate) fn unregister_built_in_extension_hotkey(
|
||||
bundle_id: &ExtensionBundleIdBorrowed<'_>,
|
||||
) -> Result<(), String> {
|
||||
let tauri_app_handle = GLOBAL_TAURI_APP_HANDLE
|
||||
.get()
|
||||
.expect("global tauri app handle not set");
|
||||
let application_prefix = format!(
|
||||
"{}.",
|
||||
application::QUERYSOURCE_ID_DATASOURCE_ID_DATASOURCE_NAME
|
||||
);
|
||||
if extension_id.starts_with(&application_prefix) {
|
||||
let app_path = &extension_id[application_prefix.len()..];
|
||||
application::unregister_app_hotkey(&tauri_app_handle, app_path)?;
|
||||
|
||||
if bundle_id.extension_id == application::QUERYSOURCE_ID_DATASOURCE_ID_DATASOURCE_NAME {
|
||||
if let Some(app_path) = bundle_id.sub_extension_id {
|
||||
application::unregister_app_hotkey(&tauri_app_handle, app_path)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn is_built_in_extension_enabled(extension_id: &str) -> Result<bool, String> {
|
||||
fn split_extension_id(extension_id: &str) -> (&str, Option<&str>) {
|
||||
match extension_id.find('.') {
|
||||
Some(idx) => (&extension_id[..idx], Some(&extension_id[idx + 1..])),
|
||||
None => (extension_id, None),
|
||||
}
|
||||
}
|
||||
|
||||
fn load_extension_from_json_file(
|
||||
extension_directory: &Path,
|
||||
extension_id: &str,
|
||||
) -> Result<Extension, String> {
|
||||
let (parent_extension_id, _opt_sub_extension_id) = split_extension_id(extension_id);
|
||||
let json_file_path = {
|
||||
let mut extension_directory_path = extension_directory.join(parent_extension_id);
|
||||
extension_directory_path.push(PLUGIN_JSON_FILE_NAME);
|
||||
|
||||
extension_directory_path
|
||||
};
|
||||
|
||||
let mut extension = serde_json::from_reader::<_, Extension>(
|
||||
std::fs::File::open(&json_file_path)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"the [{}] file for extension [{}] is missing or broken",
|
||||
PLUGIN_JSON_FILE_NAME, parent_extension_id
|
||||
)
|
||||
})
|
||||
.map_err(|e| e.to_string())?,
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
super::canonicalize_relative_icon_path(extension_directory, &mut extension)?;
|
||||
|
||||
Ok(extension)
|
||||
}
|
||||
|
||||
pub(crate) async fn is_built_in_extension_enabled(
|
||||
bundle_id: &ExtensionBundleIdBorrowed<'_>,
|
||||
) -> Result<bool, String> {
|
||||
let tauri_app_handle = GLOBAL_TAURI_APP_HANDLE
|
||||
.get()
|
||||
.expect("global tauri app handle not set");
|
||||
let search_source_registry_tauri_state = tauri_app_handle.state::<SearchSourceRegistry>();
|
||||
|
||||
if extension_id == application::QUERYSOURCE_ID_DATASOURCE_ID_DATASOURCE_NAME {
|
||||
if bundle_id.extension_id == application::QUERYSOURCE_ID_DATASOURCE_ID_DATASOURCE_NAME
|
||||
&& bundle_id.sub_extension_id.is_none()
|
||||
{
|
||||
return Ok(search_source_registry_tauri_state
|
||||
.get_source(extension_id)
|
||||
.get_source(bundle_id.extension_id)
|
||||
.await
|
||||
.is_some());
|
||||
}
|
||||
|
||||
// Check if this is an application
|
||||
let application_prefix = format!(
|
||||
"{}.",
|
||||
application::QUERYSOURCE_ID_DATASOURCE_ID_DATASOURCE_NAME
|
||||
);
|
||||
if extension_id.starts_with(&application_prefix) {
|
||||
let app_path = &extension_id[application_prefix.len()..];
|
||||
return Ok(application::is_app_search_enabled(app_path));
|
||||
if bundle_id.extension_id == application::QUERYSOURCE_ID_DATASOURCE_ID_DATASOURCE_NAME {
|
||||
if let Some(app_path) = bundle_id.sub_extension_id {
|
||||
return Ok(application::is_app_search_enabled(app_path));
|
||||
}
|
||||
}
|
||||
|
||||
if extension_id == calculator::DATA_SOURCE_ID {
|
||||
if bundle_id.extension_id == calculator::DATA_SOURCE_ID {
|
||||
return Ok(search_source_registry_tauri_state
|
||||
.get_source(extension_id)
|
||||
.get_source(bundle_id.extension_id)
|
||||
.await
|
||||
.is_some());
|
||||
}
|
||||
|
||||
if extension_id == quick_ai_access::EXTENSION_ID {
|
||||
let extension =
|
||||
load_extension_from_json_file(&BUILT_IN_EXTENSION_DIRECTORY.as_path(), extension_id)?;
|
||||
if bundle_id.extension_id == quick_ai_access::EXTENSION_ID {
|
||||
let extension = load_extension_from_json_file(
|
||||
&BUILT_IN_EXTENSION_DIRECTORY.as_path(),
|
||||
bundle_id.extension_id,
|
||||
)?;
|
||||
return Ok(extension.enabled);
|
||||
}
|
||||
|
||||
if extension_id == ai_overview::EXTENSION_ID {
|
||||
let extension =
|
||||
load_extension_from_json_file(&BUILT_IN_EXTENSION_DIRECTORY.as_path(), extension_id)?;
|
||||
if bundle_id.extension_id == ai_overview::EXTENSION_ID {
|
||||
let extension = load_extension_from_json_file(
|
||||
&BUILT_IN_EXTENSION_DIRECTORY.as_path(),
|
||||
bundle_id.extension_id,
|
||||
)?;
|
||||
return Ok(extension.enabled);
|
||||
}
|
||||
|
||||
unreachable!("extension [{}] is not a built-in extension", extension_id)
|
||||
unreachable!("extension [{:?}] is not a built-in extension", bundle_id)
|
||||
}
|
||||
|
||||
@@ -1 +1,12 @@
|
||||
pub(super) const EXTENSION_ID: &str = "QuickAIAccess";
|
||||
|
||||
pub(crate) const PLUGIN_JSON_FILE: &str = r#"
|
||||
{
|
||||
"id": "QuickAIAccess",
|
||||
"name": "Quick AI Access",
|
||||
"description": "...",
|
||||
"icon": "font_a-QuickAIAccess",
|
||||
"type": "ai_extension",
|
||||
"enabled": true
|
||||
}
|
||||
"#;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
345
src-tauri/src/extension/store.rs
Normal file
345
src-tauri/src/extension/store.rs
Normal file
@@ -0,0 +1,345 @@
|
||||
//! Extension store related stuff.
|
||||
|
||||
use super::LOCAL_QUERY_SOURCE_TYPE;
|
||||
use crate::common::document::DataSourceReference;
|
||||
use crate::common::document::Document;
|
||||
use crate::common::error::SearchError;
|
||||
use crate::common::search::QueryResponse;
|
||||
use crate::common::search::QuerySource;
|
||||
use crate::common::search::SearchQuery;
|
||||
use crate::common::traits::SearchSource;
|
||||
use crate::extension::canonicalize_relative_icon_path;
|
||||
use crate::extension::third_party::THIRD_PARTY_EXTENSIONS_DIRECTORY;
|
||||
use crate::extension::Extension;
|
||||
use crate::extension::PLUGIN_JSON_FILE_NAME;
|
||||
use crate::extension::THIRD_PARTY_EXTENSIONS_SEARCH_SOURCE;
|
||||
use crate::server::http_client::HttpClient;
|
||||
use async_trait::async_trait;
|
||||
use reqwest::StatusCode;
|
||||
use serde_json::Map as JsonObject;
|
||||
use serde_json::Value as Json;
|
||||
|
||||
const DATA_SOURCE_ID: &str = "Extension Store";
|
||||
|
||||
pub(crate) struct ExtensionStore;
|
||||
|
||||
#[async_trait]
|
||||
impl SearchSource for ExtensionStore {
|
||||
fn get_type(&self) -> QuerySource {
|
||||
QuerySource {
|
||||
r#type: LOCAL_QUERY_SOURCE_TYPE.into(),
|
||||
name: hostname::get()
|
||||
.unwrap_or(DATA_SOURCE_ID.into())
|
||||
.to_string_lossy()
|
||||
.into(),
|
||||
id: DATA_SOURCE_ID.into(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn search(&self, query: SearchQuery) -> Result<QueryResponse, SearchError> {
|
||||
const SCORE: f64 = 2000.0;
|
||||
|
||||
let Some(query_string) = query.query_strings.get("query") else {
|
||||
return Ok(QueryResponse {
|
||||
source: self.get_type(),
|
||||
hits: Vec::new(),
|
||||
total_hits: 0,
|
||||
});
|
||||
};
|
||||
|
||||
let lowercase_query_string = query_string.to_lowercase();
|
||||
let expected_str = "extension store";
|
||||
|
||||
if expected_str.contains(&lowercase_query_string) {
|
||||
let doc = Document {
|
||||
id: DATA_SOURCE_ID.to_string(),
|
||||
category: Some(DATA_SOURCE_ID.to_string()),
|
||||
title: Some(DATA_SOURCE_ID.to_string()),
|
||||
icon: Some("font_Store".to_string()),
|
||||
source: Some(DataSourceReference {
|
||||
r#type: Some(LOCAL_QUERY_SOURCE_TYPE.into()),
|
||||
name: Some(DATA_SOURCE_ID.into()),
|
||||
id: Some(DATA_SOURCE_ID.into()),
|
||||
icon: Some("font_Store".to_string()),
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
Ok(QueryResponse {
|
||||
source: self.get_type(),
|
||||
hits: vec![(doc, SCORE)],
|
||||
total_hits: 1,
|
||||
})
|
||||
} else {
|
||||
Ok(QueryResponse {
|
||||
source: self.get_type(),
|
||||
hits: Vec::new(),
|
||||
total_hits: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) async fn search_extension(
|
||||
query_params: Option<Vec<String>>,
|
||||
) -> Result<Vec<Json>, String> {
|
||||
let response = HttpClient::get(
|
||||
"default_coco_server",
|
||||
"store/extension/_search",
|
||||
query_params,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to send request: {:?}", e))?;
|
||||
|
||||
// The response of a ES style search request
|
||||
let mut response: JsonObject<String, Json> = response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to parse response: {:?}", e))?;
|
||||
|
||||
let hits_json = response
|
||||
.remove("hits")
|
||||
.expect("the JSON response should contain field [hits]");
|
||||
let mut hits = match hits_json {
|
||||
Json::Object(obj) => obj,
|
||||
_ => panic!(
|
||||
"field [hits] should be a JSON object, but it is not, value: [{}]",
|
||||
hits_json
|
||||
),
|
||||
};
|
||||
|
||||
let Some(hits_hits_json) = hits.remove("hits") else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
|
||||
let hits_hits = match hits_hits_json {
|
||||
Json::Array(arr) => arr,
|
||||
_ => panic!(
|
||||
"field [hits.hits] should be an array, but it is not, value: [{}]",
|
||||
hits_hits_json
|
||||
),
|
||||
};
|
||||
|
||||
let mut extensions = Vec::with_capacity(hits_hits.len());
|
||||
for hit in hits_hits {
|
||||
let mut hit_obj = match hit {
|
||||
Json::Object(obj) => obj,
|
||||
_ => panic!(
|
||||
"each hit in [hits.hits] should be a JSON object, but it is not, value: [{}]",
|
||||
hit
|
||||
),
|
||||
};
|
||||
let source = hit_obj
|
||||
.remove("_source")
|
||||
.expect("each hit should contain field [_source]");
|
||||
|
||||
let mut source_obj = match source {
|
||||
Json::Object(obj) => obj,
|
||||
_ => panic!(
|
||||
"field [_source] should be a JSON object, but it is not, value: [{}]",
|
||||
source
|
||||
),
|
||||
};
|
||||
|
||||
let developer_id = source_obj
|
||||
.get("developer")
|
||||
.and_then(|dev| dev.get("id"))
|
||||
.and_then(|id| id.as_str())
|
||||
.expect("developer.id should exist")
|
||||
.to_string();
|
||||
|
||||
let extension_id = source_obj
|
||||
.get("id")
|
||||
.and_then(|id| id.as_str())
|
||||
.expect("extension id should exist")
|
||||
.to_string();
|
||||
|
||||
let installed = is_extension_installed(developer_id, extension_id).await;
|
||||
source_obj.insert("installed".to_string(), Json::Bool(installed));
|
||||
|
||||
extensions.push(Json::Object(source_obj));
|
||||
}
|
||||
|
||||
Ok(extensions)
|
||||
}
|
||||
|
||||
async fn is_extension_installed(developer: String, extension_id: String) -> bool {
|
||||
THIRD_PARTY_EXTENSIONS_SEARCH_SOURCE
|
||||
.get()
|
||||
.unwrap()
|
||||
.extension_exists(&developer, &extension_id)
|
||||
.await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) async fn install_extension(id: String) -> Result<(), String> {
|
||||
let path = format!("store/extension/{}/_download", id);
|
||||
let response = HttpClient::get("default_coco_server", &path, None)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to download extension: {}", e))?;
|
||||
|
||||
if response.status() == StatusCode::NOT_FOUND {
|
||||
return Err(format!("extension [{}] not found", id));
|
||||
}
|
||||
|
||||
let bytes = response
|
||||
.bytes()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to read response bytes: {}", e))?;
|
||||
|
||||
let cursor = std::io::Cursor::new(bytes);
|
||||
let mut archive =
|
||||
zip::ZipArchive::new(cursor).map_err(|e| format!("Failed to read zip archive: {}", e))?;
|
||||
|
||||
let mut plugin_json = archive.by_name("plugin.json").map_err(|e| e.to_string())?;
|
||||
let mut plugin_json_content = String::new();
|
||||
std::io::Read::read_to_string(&mut plugin_json, &mut plugin_json_content)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let mut extension: Json = serde_json::from_str(&plugin_json_content)
|
||||
.map_err(|e| format!("Failed to parse plugin.json: {}", e))?;
|
||||
|
||||
let mut_ref_to_developer_object: &mut Json = extension
|
||||
.as_object_mut()
|
||||
.expect("plugin.json should be an object")
|
||||
.get_mut("developer")
|
||||
.expect("plugin.json should contain field [developer]");
|
||||
let developer_id = mut_ref_to_developer_object
|
||||
.get("id")
|
||||
.expect("plugin.json should contain [developer.id]")
|
||||
.as_str()
|
||||
.expect("plugin.json field [developer.id] should be a string");
|
||||
*mut_ref_to_developer_object = Json::String(developer_id.into());
|
||||
|
||||
// Set IDs for sub-extensions (commands, quicklinks, scripts)
|
||||
let mut counter = 0;
|
||||
// Set IDs for commands
|
||||
// Helper function to set IDs for array fields
|
||||
fn set_ids_for_field(extension: &mut Json, field_name: &str, counter: &mut i32) {
|
||||
if let Some(field) = extension.as_object_mut().unwrap().get_mut(field_name) {
|
||||
if let Some(array) = field.as_array_mut() {
|
||||
for item in array {
|
||||
if let Some(item_obj) = item.as_object_mut() {
|
||||
if !item_obj.contains_key("id") {
|
||||
item_obj.insert("id".to_string(), Json::String(counter.to_string()));
|
||||
*counter += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set IDs for sub-extensions
|
||||
set_ids_for_field(&mut extension, "commands", &mut counter);
|
||||
set_ids_for_field(&mut extension, "quicklinks", &mut counter);
|
||||
set_ids_for_field(&mut extension, "scripts", &mut counter);
|
||||
|
||||
let mut extension: Extension = serde_json::from_value(extension).unwrap_or_else(|e| {
|
||||
panic!(
|
||||
"cannot parse plugin.json as struct Extension, error [{:?}]",
|
||||
e
|
||||
);
|
||||
});
|
||||
|
||||
drop(plugin_json);
|
||||
|
||||
let developer = extension.developer.clone().unwrap_or_default();
|
||||
let extension_id = extension.id.clone();
|
||||
|
||||
// Extract the zip file
|
||||
let extension_directory = {
|
||||
let mut path = THIRD_PARTY_EXTENSIONS_DIRECTORY.to_path_buf();
|
||||
path.push(developer);
|
||||
path.push(extension_id.as_str());
|
||||
path
|
||||
};
|
||||
|
||||
tokio::fs::create_dir_all(extension_directory.as_path())
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
// Extract all files except plugin.json
|
||||
for i in 0..archive.len() {
|
||||
let mut file = archive.by_index(i).map_err(|e| e.to_string())?;
|
||||
let outpath = match file.enclosed_name() {
|
||||
Some(path) => extension_directory.join(path),
|
||||
None => continue,
|
||||
};
|
||||
|
||||
// Skip the plugin.json file as we'll create it from the extension variable
|
||||
if file.name() == "plugin.json" {
|
||||
continue;
|
||||
}
|
||||
|
||||
if file.name().ends_with('/') {
|
||||
tokio::fs::create_dir_all(&outpath)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
} else {
|
||||
if let Some(p) = outpath.parent() {
|
||||
if !p.exists() {
|
||||
tokio::fs::create_dir_all(p)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
}
|
||||
let mut outfile = tokio::fs::File::create(&outpath)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
let mut content = Vec::new();
|
||||
std::io::Read::read_to_end(&mut file, &mut content).map_err(|e| e.to_string())?;
|
||||
tokio::io::AsyncWriteExt::write_all(&mut outfile, &content)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
}
|
||||
|
||||
// Create plugin.json from the extension variable
|
||||
let plugin_json_path = extension_directory.join(PLUGIN_JSON_FILE_NAME);
|
||||
let extension_json = serde_json::to_string_pretty(&extension).map_err(|e| e.to_string())?;
|
||||
tokio::fs::write(&plugin_json_path, extension_json)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
// Turn it into an absolute path if it is a valid relative path because frontend code need this.
|
||||
canonicalize_relative_icon_path(&extension_directory, &mut extension)?;
|
||||
|
||||
THIRD_PARTY_EXTENSIONS_SEARCH_SOURCE
|
||||
.get()
|
||||
.unwrap()
|
||||
.add_extension(extension)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) async fn uninstall_extension(
|
||||
developer: String,
|
||||
extension_id: String,
|
||||
) -> Result<(), String> {
|
||||
let extension_dir = {
|
||||
let mut path = THIRD_PARTY_EXTENSIONS_DIRECTORY.join(developer.as_str());
|
||||
path.push(extension_id.as_str());
|
||||
|
||||
path
|
||||
};
|
||||
if !extension_dir.try_exists().map_err(|e| e.to_string())? {
|
||||
panic!(
|
||||
"we are uninstalling extension [{}/{}], but there is no such extension files on disk",
|
||||
developer, extension_id
|
||||
)
|
||||
}
|
||||
tokio::fs::remove_dir_all(extension_dir.as_path())
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
THIRD_PARTY_EXTENSIONS_SEARCH_SOURCE
|
||||
.get()
|
||||
.unwrap()
|
||||
.remove_extension(&developer, &extension_id)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,7 @@ mod util;
|
||||
|
||||
use crate::common::register::SearchSourceRegistry;
|
||||
// use crate::common::traits::SearchSource;
|
||||
use crate::common::{MAIN_WINDOW_LABEL, SETTINGS_WINDOW_LABEL};
|
||||
use crate::common::{CHECK_WINDOW_LABEL, MAIN_WINDOW_LABEL, SETTINGS_WINDOW_LABEL};
|
||||
use crate::server::servers::{load_or_insert_default_server, load_servers_token};
|
||||
use autostart::{change_autostart, ensure_autostart_state_consistent};
|
||||
use lazy_static::lazy_static;
|
||||
@@ -19,9 +19,7 @@ use std::sync::Mutex;
|
||||
use std::sync::OnceLock;
|
||||
use tauri::async_runtime::block_on;
|
||||
use tauri::plugin::TauriPlugin;
|
||||
use tauri::{
|
||||
AppHandle, Emitter, Manager, PhysicalPosition, Runtime, WebviewWindow, WindowEvent,
|
||||
};
|
||||
use tauri::{AppHandle, Emitter, Manager, PhysicalPosition, Runtime, WebviewWindow, WindowEvent};
|
||||
use tauri_plugin_autostart::MacosLauncher;
|
||||
|
||||
/// Tauri store name
|
||||
@@ -107,6 +105,8 @@ pub fn run() {
|
||||
show_coco,
|
||||
hide_coco,
|
||||
show_settings,
|
||||
show_check,
|
||||
hide_check,
|
||||
server::servers::get_server_token,
|
||||
server::servers::add_coco_server,
|
||||
server::servers::remove_coco_server,
|
||||
@@ -149,6 +149,7 @@ pub fn run() {
|
||||
extension::built_in::application::get_app_metadata,
|
||||
extension::built_in::application::add_app_search_path,
|
||||
extension::built_in::application::remove_app_search_path,
|
||||
extension::built_in::application::reindex_applications,
|
||||
extension::list_extensions,
|
||||
extension::enable_extension,
|
||||
extension::disable_extension,
|
||||
@@ -156,6 +157,9 @@ pub fn run() {
|
||||
extension::register_extension_hotkey,
|
||||
extension::unregister_extension_hotkey,
|
||||
extension::is_extension_enabled,
|
||||
extension::store::search_extension,
|
||||
extension::store::install_extension,
|
||||
extension::store::uninstall_extension,
|
||||
settings::set_allow_self_signature,
|
||||
settings::get_allow_self_signature,
|
||||
assistant::ask_ai,
|
||||
@@ -211,7 +215,13 @@ pub fn run() {
|
||||
|
||||
let main_window = app.get_webview_window(MAIN_WINDOW_LABEL).unwrap();
|
||||
let settings_window = app.get_webview_window(SETTINGS_WINDOW_LABEL).unwrap();
|
||||
setup::default(app, main_window.clone(), settings_window.clone());
|
||||
let check_window = app.get_webview_window(CHECK_WINDOW_LABEL).unwrap();
|
||||
setup::default(
|
||||
app,
|
||||
main_window.clone(),
|
||||
settings_window.clone(),
|
||||
check_window.clone(),
|
||||
);
|
||||
|
||||
Ok(())
|
||||
})
|
||||
@@ -390,7 +400,8 @@ fn move_window_to_active_monitor<R: Runtime>(window: &WebviewWindow<R>) {
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_app_search_source<R: Runtime>(app_handle: AppHandle<R>) -> Result<(), String> {
|
||||
let (_found_invalid_extensions, extensions) = extension::list_extensions()
|
||||
// We want all the extensions here, so no filter condition specified.
|
||||
let (_found_invalid_extensions, extensions) = extension::list_extensions(None, None, false)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
extension::init_extensions(extensions).await?;
|
||||
@@ -413,6 +424,28 @@ async fn show_settings(app_handle: AppHandle) {
|
||||
window.set_focus().unwrap();
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn show_check(app_handle: AppHandle) {
|
||||
log::debug!("check menu item was clicked");
|
||||
let window = app_handle
|
||||
.get_webview_window(CHECK_WINDOW_LABEL)
|
||||
.expect("we have a check window");
|
||||
|
||||
window.show().unwrap();
|
||||
window.unminimize().unwrap();
|
||||
window.set_focus().unwrap();
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn hide_check(app_handle: AppHandle) {
|
||||
log::debug!("check window was closed");
|
||||
let window = &app_handle
|
||||
.get_webview_window(CHECK_WINDOW_LABEL)
|
||||
.expect("we have a check window");
|
||||
|
||||
window.hide().unwrap();
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn simulate_mouse_click<R: Runtime>(window: WebviewWindow<R>, is_chat_mode: bool) {
|
||||
#[cfg(target_os = "windows")]
|
||||
|
||||
@@ -111,15 +111,15 @@ pub async fn get_attachment(
|
||||
server_id: String,
|
||||
session_id: String,
|
||||
) -> Result<GetAttachmentResponse, String> {
|
||||
let mut query_params = HashMap::new();
|
||||
query_params.insert("session".to_string(), serde_json::Value::String(session_id));
|
||||
let mut query_params = Vec::new();
|
||||
query_params.push(format!("session={}", session_id));
|
||||
|
||||
let response = HttpClient::get(&server_id, "/attachment/_search", Some(query_params))
|
||||
.await
|
||||
.map_err(|e| format!("Request error: {}", e))?;
|
||||
|
||||
let body = get_response_body_text(response).await?;
|
||||
|
||||
|
||||
serde_json::from_str::<GetAttachmentResponse>(&body)
|
||||
.map_err(|e| format!("Failed to parse attachment response: {}", e))
|
||||
}
|
||||
|
||||
@@ -4,18 +4,10 @@ use crate::server::connector::get_connector_by_id;
|
||||
use crate::server::http_client::HttpClient;
|
||||
use crate::server::servers::get_all_servers;
|
||||
use lazy_static::lazy_static;
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use tauri::{AppHandle, Runtime};
|
||||
|
||||
#[derive(serde::Deserialize, Debug)]
|
||||
pub struct GetDatasourcesByServerOptions {
|
||||
pub from: Option<u32>,
|
||||
pub size: Option<u32>,
|
||||
pub query: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref DATASOURCE_CACHE: Arc<RwLock<HashMap<String, HashMap<String, DataSource>>>> =
|
||||
Arc::new(RwLock::new(HashMap::new()));
|
||||
@@ -97,29 +89,12 @@ pub async fn refresh_all_datasources<R: Runtime>(_app_handle: &AppHandle<R>) ->
|
||||
#[tauri::command]
|
||||
pub async fn datasource_search(
|
||||
id: &str,
|
||||
options: Option<GetDatasourcesByServerOptions>,
|
||||
query_params: Option<Vec<String>>, //["query=abc", "filter=er", "filter=efg", "from=0", "size=5"],
|
||||
) -> Result<Vec<DataSource>, String> {
|
||||
let from = options.as_ref().and_then(|opt| opt.from).unwrap_or(0);
|
||||
let size = options.as_ref().and_then(|opt| opt.size).unwrap_or(10000);
|
||||
|
||||
let mut body = serde_json::json!({
|
||||
"from": from,
|
||||
"size": size,
|
||||
});
|
||||
|
||||
if let Some(q) = options.and_then(|get_data_source_options| get_data_source_options.query ) {
|
||||
body["query"] = q;
|
||||
}
|
||||
|
||||
// Perform the async HTTP request outside the cache lock
|
||||
let resp = HttpClient::post(
|
||||
id,
|
||||
"/datasource/_search",
|
||||
None,
|
||||
Some(reqwest::Body::from(body.to_string())),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| format!("Error fetching datasource: {}", e))?;
|
||||
let resp = HttpClient::post(id, "/datasource/_search", query_params, None)
|
||||
.await
|
||||
.map_err(|e| format!("Error fetching datasource: {}", e))?;
|
||||
|
||||
// Parse the search results from the response
|
||||
let datasources: Vec<DataSource> = parse_search_results(resp).await.map_err(|e| {
|
||||
@@ -136,28 +111,12 @@ pub async fn datasource_search(
|
||||
#[tauri::command]
|
||||
pub async fn mcp_server_search(
|
||||
id: &str,
|
||||
from: u32,
|
||||
size: u32,
|
||||
query: Option<HashMap<String, Value>>,
|
||||
query_params: Option<Vec<String>>,
|
||||
) -> Result<Vec<DataSource>, String> {
|
||||
let mut body = serde_json::json!({
|
||||
"from": from,
|
||||
"size": size,
|
||||
});
|
||||
|
||||
if let Some(q) = query {
|
||||
body["query"] = serde_json::to_value(q).map_err(|e| e.to_string())?;
|
||||
}
|
||||
|
||||
// Perform the async HTTP request outside the cache lock
|
||||
let resp = HttpClient::post(
|
||||
id,
|
||||
"/mcp_server/_search",
|
||||
None,
|
||||
Some(reqwest::Body::from(body.to_string())),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| format!("Error fetching datasource: {}", e))?;
|
||||
let resp = HttpClient::post(id, "/mcp_server/_search", query_params, None)
|
||||
.await
|
||||
.map_err(|e| format!("Error fetching datasource: {}", e))?;
|
||||
|
||||
// Parse the search results from the response
|
||||
let mcp_server: Vec<DataSource> = parse_search_results(resp).await.map_err(|e| {
|
||||
|
||||
@@ -4,7 +4,6 @@ use once_cell::sync::Lazy;
|
||||
use reqwest::{Client, Method, RequestBuilder};
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
use tauri_plugin_store::JsonValue;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
pub(crate) fn new_reqwest_http_client(accept_invalid_certs: bool) -> Client {
|
||||
@@ -40,7 +39,7 @@ impl HttpClient {
|
||||
pub async fn send_raw_request(
|
||||
method: Method,
|
||||
url: &str,
|
||||
query_params: Option<HashMap<String, JsonValue>>,
|
||||
query_params: Option<Vec<String>>,
|
||||
headers: Option<HashMap<String, String>>,
|
||||
body: Option<reqwest::Body>,
|
||||
) -> Result<reqwest::Response, String> {
|
||||
@@ -74,7 +73,7 @@ impl HttpClient {
|
||||
method: Method,
|
||||
url: &str,
|
||||
headers: Option<HashMap<String, String>>,
|
||||
query_params: Option<HashMap<String, JsonValue>>, // Add query parameters
|
||||
query_params: Option<Vec<String>>, // Add query parameters
|
||||
body: Option<reqwest::Body>,
|
||||
) -> RequestBuilder {
|
||||
let client = HTTP_CLIENT.lock().await; // Acquire the lock on HTTP_CLIENT
|
||||
@@ -106,23 +105,10 @@ impl HttpClient {
|
||||
request_builder = request_builder.headers(req_headers);
|
||||
}
|
||||
|
||||
if let Some(query) = query_params {
|
||||
// Convert only supported value types into strings
|
||||
let query: HashMap<String, String> = query
|
||||
.into_iter()
|
||||
.filter_map(|(k, v)| {
|
||||
match v {
|
||||
JsonValue::String(s) => Some((k, s)),
|
||||
JsonValue::Number(n) => Some((k, n.to_string())),
|
||||
JsonValue::Bool(b) => Some((k, b.to_string())),
|
||||
_ => {
|
||||
dbg!(
|
||||
"Unsupported query parameter type. Only strings, numbers, and booleans are supported.",k,v,
|
||||
);
|
||||
None
|
||||
} // skip arrays, objects, nulls
|
||||
}
|
||||
})
|
||||
if let Some(params) = query_params {
|
||||
let query: Vec<(&str, &str)> = params
|
||||
.iter()
|
||||
.filter_map(|s| s.split_once('='))
|
||||
.collect();
|
||||
request_builder = request_builder.query(&query);
|
||||
}
|
||||
@@ -135,12 +121,13 @@ impl HttpClient {
|
||||
request_builder
|
||||
}
|
||||
|
||||
|
||||
pub async fn send_request(
|
||||
server_id: &str,
|
||||
method: Method,
|
||||
path: &str,
|
||||
custom_headers: Option<HashMap<String, String>>,
|
||||
query_params: Option<HashMap<String, JsonValue>>,
|
||||
query_params: Option<Vec<String>>,
|
||||
body: Option<reqwest::Body>,
|
||||
) -> Result<reqwest::Response, String> {
|
||||
// Fetch the server using the server_id
|
||||
@@ -182,16 +169,17 @@ impl HttpClient {
|
||||
pub async fn get(
|
||||
server_id: &str,
|
||||
path: &str,
|
||||
query_params: Option<HashMap<String, JsonValue>>, // Add query parameters
|
||||
query_params: Option<Vec<String>>,
|
||||
) -> Result<reqwest::Response, String> {
|
||||
HttpClient::send_request(server_id, Method::GET, path, None, query_params, None).await
|
||||
HttpClient::send_request(server_id, Method::GET, path, None, query_params,
|
||||
None).await
|
||||
}
|
||||
|
||||
// Convenience method for POST requests
|
||||
pub async fn post(
|
||||
server_id: &str,
|
||||
path: &str,
|
||||
query_params: Option<HashMap<String, JsonValue>>, // Add query parameters
|
||||
query_params: Option<Vec<String>>,
|
||||
body: Option<reqwest::Body>,
|
||||
) -> Result<reqwest::Response, String> {
|
||||
HttpClient::send_request(server_id, Method::POST, path, None, query_params, body).await
|
||||
@@ -201,7 +189,7 @@ impl HttpClient {
|
||||
server_id: &str,
|
||||
path: &str,
|
||||
custom_headers: Option<HashMap<String, String>>,
|
||||
query_params: Option<HashMap<String, JsonValue>>, // Add query parameters
|
||||
query_params: Option<Vec<String>>,
|
||||
body: Option<reqwest::Body>,
|
||||
) -> Result<reqwest::Response, String> {
|
||||
HttpClient::send_request(
|
||||
@@ -212,7 +200,7 @@ impl HttpClient {
|
||||
query_params,
|
||||
body,
|
||||
)
|
||||
.await
|
||||
.await
|
||||
}
|
||||
|
||||
// Convenience method for PUT requests
|
||||
@@ -221,7 +209,7 @@ impl HttpClient {
|
||||
server_id: &str,
|
||||
path: &str,
|
||||
custom_headers: Option<HashMap<String, String>>,
|
||||
query_params: Option<HashMap<String, JsonValue>>, // Add query parameters
|
||||
query_params: Option<Vec<String>>,
|
||||
body: Option<reqwest::Body>,
|
||||
) -> Result<reqwest::Response, String> {
|
||||
HttpClient::send_request(
|
||||
@@ -232,7 +220,7 @@ impl HttpClient {
|
||||
query_params,
|
||||
body,
|
||||
)
|
||||
.await
|
||||
.await
|
||||
}
|
||||
|
||||
// Convenience method for DELETE requests
|
||||
@@ -241,7 +229,7 @@ impl HttpClient {
|
||||
server_id: &str,
|
||||
path: &str,
|
||||
custom_headers: Option<HashMap<String, String>>,
|
||||
query_params: Option<HashMap<String, JsonValue>>, // Add query parameters
|
||||
query_params: Option<Vec<String>>,
|
||||
) -> Result<reqwest::Response, String> {
|
||||
HttpClient::send_request(
|
||||
server_id,
|
||||
@@ -251,6 +239,6 @@ impl HttpClient {
|
||||
query_params,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ use async_trait::async_trait;
|
||||
// use futures::stream::StreamExt;
|
||||
use ordered_float::OrderedFloat;
|
||||
use std::collections::HashMap;
|
||||
use tauri_plugin_store::JsonValue;
|
||||
// use std::hash::Hash;
|
||||
|
||||
#[allow(dead_code)]
|
||||
@@ -96,14 +95,18 @@ impl SearchSource for CocoSearchSource {
|
||||
let mut total_hits = 0;
|
||||
let mut hits: Vec<(Document, f64)> = Vec::new();
|
||||
|
||||
let mut query_args: HashMap<String, JsonValue> = HashMap::new();
|
||||
query_args.insert("from".into(), JsonValue::Number(query.from.into()));
|
||||
query_args.insert("size".into(), JsonValue::Number(query.size.into()));
|
||||
let mut query_params = Vec::new();
|
||||
|
||||
// Add from/size as number values
|
||||
query_params.push(format!("from={}", query.from));
|
||||
query_params.push(format!("size={}", query.size));
|
||||
|
||||
// Add query strings
|
||||
for (key, value) in query.query_strings {
|
||||
query_args.insert(key, JsonValue::String(value));
|
||||
query_params.push(format!("{}={}", key, value));
|
||||
}
|
||||
|
||||
let response = HttpClient::get(&self.server.id, &url, Some(query_args))
|
||||
let response = HttpClient::get(&self.server.id, &url, Some(query_params))
|
||||
.await
|
||||
.map_err(|e| SearchError::HttpError(format!("{}", e)))?;
|
||||
|
||||
|
||||
@@ -315,9 +315,7 @@ pub async fn refresh_coco_server_info<R: Runtime>(
|
||||
// Send request to fetch updated server info
|
||||
let response = HttpClient::get(&id, "/provider/_info", None)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
format!("Failed to contact the server: {}", e)
|
||||
});
|
||||
.map_err(|e| format!("Failed to contact the server: {}", e));
|
||||
|
||||
if response.is_err() {
|
||||
let _ = mark_server_as_offline(app_handle, &id).await;
|
||||
@@ -338,6 +336,9 @@ pub async fn refresh_coco_server_info<R: Runtime>(
|
||||
let mut updated_server: Server = serde_json::from_str(&body)
|
||||
.map_err(|e| format!("Failed to deserialize the response: {}", e))?;
|
||||
|
||||
// Mark server as online
|
||||
let _ = mark_server_as_online(app_handle.clone(), &id).await;
|
||||
|
||||
// Restore local state
|
||||
updated_server.id = id.clone();
|
||||
updated_server.builtin = is_builtin;
|
||||
@@ -476,8 +477,25 @@ pub async fn try_register_server_to_search_source(
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn mark_server_as_offline<R: Runtime>(
|
||||
pub async fn mark_server_as_online<R: Runtime>(
|
||||
app_handle: AppHandle<R>, id: &str) -> Result<(), ()> {
|
||||
// println!("server_is_offline: {}", id);
|
||||
let server = get_server_by_id(id);
|
||||
if let Some(mut server) = server {
|
||||
server.available = true;
|
||||
server.health = None;
|
||||
save_server(&server);
|
||||
|
||||
try_register_server_to_search_source(app_handle.clone(), &server).await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn mark_server_as_offline<R: Runtime>(
|
||||
app_handle: AppHandle<R>,
|
||||
id: &str,
|
||||
) -> Result<(), ()> {
|
||||
// println!("server_is_offline: {}", id);
|
||||
let server = get_server_by_id(id);
|
||||
if let Some(mut server) = server {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
use crate::common::http::get_response_body_text;
|
||||
use crate::server::http_client::HttpClient;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value as JsonValue;
|
||||
use std::collections::HashMap;
|
||||
use tauri::command;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
@@ -13,18 +11,18 @@ pub struct TranscriptionResponse {
|
||||
#[command]
|
||||
pub async fn transcription(
|
||||
server_id: String,
|
||||
audio_type: String,
|
||||
audio_content: String,
|
||||
_audio_type: String,
|
||||
_audio_content: String,
|
||||
) -> Result<TranscriptionResponse, String> {
|
||||
let mut query_params = HashMap::new();
|
||||
query_params.insert("type".to_string(), JsonValue::String(audio_type));
|
||||
query_params.insert("content".to_string(), JsonValue::String(audio_content));
|
||||
// let mut query_params = HashMap::new();
|
||||
// query_params.insert("type".to_string(), JsonValue::String(audio_type));
|
||||
// query_params.insert("content".to_string(), JsonValue::String(audio_content));
|
||||
|
||||
// Send the HTTP POST request
|
||||
let response = HttpClient::post(
|
||||
&server_id,
|
||||
"/services/audio/transcription",
|
||||
Some(query_params),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
use tauri::{App, WebviewWindow};
|
||||
|
||||
pub fn platform(_app: &mut App, _main_window: WebviewWindow, _settings_window: WebviewWindow) {}
|
||||
pub fn platform(
|
||||
_app: &mut App,
|
||||
_main_window: WebviewWindow,
|
||||
_settings_window: WebviewWindow,
|
||||
_check_window: WebviewWindow,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -12,7 +12,12 @@ const WINDOW_BLUR_EVENT: &str = "tauri://blur";
|
||||
const WINDOW_MOVED_EVENT: &str = "tauri://move";
|
||||
const WINDOW_RESIZED_EVENT: &str = "tauri://resize";
|
||||
|
||||
pub fn platform(_app: &mut App, main_window: WebviewWindow, _settings_window: WebviewWindow) {
|
||||
pub fn platform(
|
||||
_app: &mut App,
|
||||
main_window: WebviewWindow,
|
||||
_settings_window: WebviewWindow,
|
||||
_check_window: WebviewWindow,
|
||||
) {
|
||||
// Convert ns_window to ns_panel
|
||||
let panel = main_window.to_panel().unwrap();
|
||||
|
||||
|
||||
@@ -18,10 +18,20 @@ pub use windows::*;
|
||||
#[cfg(target_os = "linux")]
|
||||
pub use linux::*;
|
||||
|
||||
pub fn default(app: &mut App, main_window: WebviewWindow, settings_window: WebviewWindow) {
|
||||
pub fn default(
|
||||
app: &mut App,
|
||||
main_window: WebviewWindow,
|
||||
settings_window: WebviewWindow,
|
||||
check_window: WebviewWindow,
|
||||
) {
|
||||
// Development mode automatically opens the console: https://tauri.app/develop/debug
|
||||
#[cfg(debug_assertions)]
|
||||
main_window.open_devtools();
|
||||
|
||||
platform(app, main_window.clone(), settings_window.clone());
|
||||
platform(
|
||||
app,
|
||||
main_window.clone(),
|
||||
settings_window.clone(),
|
||||
check_window.clone(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
use tauri::{App, WebviewWindow};
|
||||
|
||||
pub fn platform(_app: &mut App, _main_window: WebviewWindow, _settings_window: WebviewWindow) {}
|
||||
pub fn platform(
|
||||
_app: &mut App,
|
||||
_main_window: WebviewWindow,
|
||||
_settings_window: WebviewWindow,
|
||||
_check_window: WebviewWindow,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -41,9 +41,9 @@
|
||||
"title": "Coco AI Settings",
|
||||
"url": "/ui/settings",
|
||||
"width": 1000,
|
||||
"minWidth": 1000,
|
||||
"height": 700,
|
||||
"minHeight": 700,
|
||||
"minWidth": 1000,
|
||||
"center": true,
|
||||
"transparent": true,
|
||||
"maximizable": false,
|
||||
@@ -55,6 +55,26 @@
|
||||
"effects": ["sidebar"],
|
||||
"state": "active"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "check",
|
||||
"title": "Coco AI Update",
|
||||
"url": "/ui/check",
|
||||
"width": 340,
|
||||
"minWidth": 340,
|
||||
"height": 260,
|
||||
"minHeight": 260,
|
||||
"center": false,
|
||||
"transparent": true,
|
||||
"maximizable": false,
|
||||
"skipTaskbar": false,
|
||||
"dragDropEnabled": false,
|
||||
"hiddenTitle": true,
|
||||
"visible": false,
|
||||
"windowEffects": {
|
||||
"effects": ["sidebar"],
|
||||
"state": "active"
|
||||
}
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
|
||||
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
@@ -124,12 +124,26 @@ export function get_connectors_by_server(id: string): Promise<Connector[]> {
|
||||
return invokeWithErrorHandler(`get_connectors_by_server`, { id });
|
||||
}
|
||||
|
||||
export function datasource_search(id: string): Promise<DataSource[]> {
|
||||
return invokeWithErrorHandler(`datasource_search`, { id });
|
||||
export function datasource_search({
|
||||
id,
|
||||
queryParams,
|
||||
}: {
|
||||
id: string;
|
||||
//["query=abc", "filter=er", "filter=efg", "from=0", "size=5"]
|
||||
queryParams?: string[];
|
||||
}): Promise<DataSource[]> {
|
||||
return invokeWithErrorHandler(`datasource_search`, { id, queryParams });
|
||||
}
|
||||
|
||||
export function mcp_server_search(id: string): Promise<DataSource[]> {
|
||||
return invokeWithErrorHandler(`mcp_server_search`, { id });
|
||||
export function mcp_server_search({
|
||||
id,
|
||||
queryParams,
|
||||
}: {
|
||||
id: string;
|
||||
//["query=abc", "filter=er", "filter=efg", "from=0", "size=5"]
|
||||
queryParams?: string[];
|
||||
}): Promise<DataSource[]> {
|
||||
return invokeWithErrorHandler(`mcp_server_search`, { id, queryParams });
|
||||
}
|
||||
|
||||
export function connect_to_server(id: string, clientId: string): Promise<void> {
|
||||
@@ -224,7 +238,7 @@ export function new_chat({
|
||||
queryParams,
|
||||
}: {
|
||||
serverId: string;
|
||||
websocketId?: string;
|
||||
websocketId: string;
|
||||
message: string;
|
||||
queryParams?: Record<string, any>;
|
||||
}): Promise<GetResponse> {
|
||||
@@ -244,7 +258,7 @@ export function send_message({
|
||||
queryParams,
|
||||
}: {
|
||||
serverId: string;
|
||||
websocketId?: string;
|
||||
websocketId: string;
|
||||
sessionId: string;
|
||||
message: string;
|
||||
queryParams?: Record<string, any>;
|
||||
@@ -269,15 +283,16 @@ export const update_session_chat = (payload: {
|
||||
serverId: string;
|
||||
sessionId: string;
|
||||
title?: string;
|
||||
context?: {
|
||||
attachments?: string[];
|
||||
};
|
||||
context?: Record<string, any>;
|
||||
}): Promise<boolean> => {
|
||||
return invokeWithErrorHandler<boolean>("update_session_chat", payload);
|
||||
};
|
||||
|
||||
export const assistant_search = (payload: {
|
||||
serverId: string;
|
||||
from: number;
|
||||
size: number;
|
||||
query?: Record<string, any>;
|
||||
}): Promise<boolean> => {
|
||||
return invokeWithErrorHandler<boolean>("assistant_search", payload);
|
||||
};
|
||||
|
||||
@@ -26,4 +26,12 @@ export function show_coco(): Promise<void> {
|
||||
|
||||
export function show_settings(): Promise<void> {
|
||||
return invoke('show_settings');
|
||||
}
|
||||
|
||||
export function show_check(): Promise<void> {
|
||||
return invoke('show_check');
|
||||
}
|
||||
|
||||
export function hide_check(): Promise<void> {
|
||||
return invoke('hide_check');
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { Post } from "@/api/axiosRequest";
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
import { useConnectStore } from "@/stores/connectStore";
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
import { parseSearchQuery, SearchQuery, unrequitable } from "@/utils";
|
||||
|
||||
interface AssistantFetcherProps {
|
||||
debounceKeyword?: string;
|
||||
@@ -16,12 +17,8 @@ export const AssistantFetcher = ({
|
||||
}: AssistantFetcherProps) => {
|
||||
const isTauri = useAppStore((state) => state.isTauri);
|
||||
|
||||
const currentService = useConnectStore((state) => state.currentService);
|
||||
|
||||
const currentAssistant = useConnectStore((state) => state.currentAssistant);
|
||||
const setCurrentAssistant = useConnectStore((state) => {
|
||||
return state.setCurrentAssistant;
|
||||
});
|
||||
const { currentService, currentAssistant, setCurrentAssistant } =
|
||||
useConnectStore();
|
||||
|
||||
const lastServerId = useRef<string | null>(null);
|
||||
|
||||
@@ -29,54 +26,42 @@ export const AssistantFetcher = ({
|
||||
current: number;
|
||||
pageSize: number;
|
||||
serverId?: string;
|
||||
query?: string;
|
||||
}) => {
|
||||
try {
|
||||
if (isTauri && !currentService?.enabled) {
|
||||
if (unrequitable()) {
|
||||
return {
|
||||
total: 0,
|
||||
list: [],
|
||||
};
|
||||
}
|
||||
|
||||
const { pageSize, current, serverId = currentService?.id } = params;
|
||||
const {
|
||||
pageSize,
|
||||
current,
|
||||
serverId = currentService?.id,
|
||||
query,
|
||||
} = params;
|
||||
|
||||
const from = (current - 1) * pageSize;
|
||||
const size = pageSize;
|
||||
|
||||
let response: any;
|
||||
|
||||
const body: Record<string, any> = {
|
||||
serverId,
|
||||
from,
|
||||
size,
|
||||
};
|
||||
|
||||
body.query = {
|
||||
bool: {
|
||||
must: [{ term: { enabled: true } }],
|
||||
const searchQuery: SearchQuery = {
|
||||
from: (current - 1) * pageSize,
|
||||
size: pageSize,
|
||||
query: query ?? debounceKeyword,
|
||||
fuzziness: 5,
|
||||
filters: {
|
||||
enabled: true,
|
||||
id: assistantIDs,
|
||||
},
|
||||
};
|
||||
|
||||
if (debounceKeyword) {
|
||||
body.query.bool.must.push({
|
||||
query_string: {
|
||||
fields: ["combined_fulltext"],
|
||||
query: debounceKeyword,
|
||||
fuzziness: "AUTO",
|
||||
fuzzy_prefix_length: 2,
|
||||
fuzzy_max_expansions: 10,
|
||||
fuzzy_transpositions: true,
|
||||
allow_leading_wildcard: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (assistantIDs.length > 0) {
|
||||
body.query.bool.must.push({
|
||||
terms: {
|
||||
id: assistantIDs.map((id) => id),
|
||||
},
|
||||
});
|
||||
}
|
||||
const queryParams = parseSearchQuery(searchQuery);
|
||||
|
||||
const body: Record<string, any> = {
|
||||
serverId,
|
||||
queryParams,
|
||||
};
|
||||
|
||||
let response: any;
|
||||
|
||||
if (isTauri) {
|
||||
if (!currentService?.id) {
|
||||
|
||||
@@ -18,7 +18,6 @@ import AssistantItem from "./AssistantItem";
|
||||
import Pagination from "@/components/Common/Pagination";
|
||||
import { useSearchStore } from "@/stores/searchStore";
|
||||
import { useChatStore } from "@/stores/chatStore";
|
||||
import { specialCharacterFiltering } from "@/utils"
|
||||
|
||||
interface AssistantListProps {
|
||||
assistantIDs?: string[];
|
||||
@@ -240,8 +239,7 @@ export function AssistantList({ assistantIDs = [] }: AssistantListProps) {
|
||||
placeholder={t("assistant.popover.search")}
|
||||
className="w-full h-8 px-2 bg-transparent border rounded-md dark:border-white/10"
|
||||
onChange={(event) => {
|
||||
const value = specialCharacterFiltering(event.target.value.trim())
|
||||
setKeyword(value);
|
||||
setKeyword(event.target.value);
|
||||
}}
|
||||
/>
|
||||
</VisibleKey>
|
||||
|
||||
@@ -129,16 +129,12 @@ const ChatAI = memo(
|
||||
}, [currentService?.enabled, showChatHistory, connected]);
|
||||
|
||||
useEffect(() => {
|
||||
if (askAiServerId || !askAiSessionId || chats.length === 0) return;
|
||||
if (askAiServerId || !askAiSessionId) return;
|
||||
|
||||
const matched = chats.find((item) => item._id === askAiSessionId);
|
||||
onSelectChat({ _id: askAiSessionId });
|
||||
|
||||
if (matched) {
|
||||
onSelectChat(matched);
|
||||
|
||||
setAskAiSessionId(void 0);
|
||||
}
|
||||
}, [chats, askAiSessionId, askAiServerId]);
|
||||
setAskAiSessionId(void 0);
|
||||
}, [askAiSessionId, askAiServerId]);
|
||||
|
||||
const [Question, setQuestion] = useState<string>("");
|
||||
|
||||
|
||||
@@ -38,12 +38,16 @@ export function ServerList({ reconnect, clearChat }: ServerListProps) {
|
||||
|
||||
const [serverList, setServerList] = useState<IServer[]>([]);
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
const [highlightId, setHighlightId] = useState<string>("");
|
||||
|
||||
const askAiServerId = useSearchStore((state) => {
|
||||
return state.askAiServerId;
|
||||
});
|
||||
const setAskAiServerId = useSearchStore((state) => {
|
||||
return state.setAskAiServerId;
|
||||
});
|
||||
|
||||
const popoverRef = useRef<HTMLDivElement>(null);
|
||||
const serverListButtonRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const fetchServers = useCallback(
|
||||
@@ -146,29 +150,45 @@ export function ServerList({ reconnect, clearChat }: ServerListProps) {
|
||||
}
|
||||
};
|
||||
|
||||
useKeyPress(["uparrow", "downarrow"], (_, key) => {
|
||||
useKeyPress(["uparrow", "downarrow", "enter"], (event, key) => {
|
||||
const isClose = isNil(serverListButtonRef.current?.dataset["open"]);
|
||||
const length = serverList.length;
|
||||
|
||||
if (isClose || length <= 1) return;
|
||||
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
const currentIndex = serverList.findIndex((server) => {
|
||||
return server.id === currentService?.id;
|
||||
return server.id === (highlightId === '' ? currentService?.id : highlightId);
|
||||
});
|
||||
|
||||
let nextIndex = currentIndex;
|
||||
|
||||
if (key === "uparrow") {
|
||||
nextIndex = currentIndex > 0 ? currentIndex - 1 : length - 1;
|
||||
setHighlightId(serverList[nextIndex].id);
|
||||
} else if (key === "downarrow") {
|
||||
nextIndex = currentIndex < serverList.length - 1 ? currentIndex + 1 : 0;
|
||||
setHighlightId(serverList[nextIndex].id);
|
||||
} else if (key === "enter" && currentIndex >= 0) {
|
||||
if (document.activeElement instanceof HTMLTextAreaElement) return;
|
||||
const selectedServer = serverList[currentIndex];
|
||||
if (selectedServer) {
|
||||
switchServer(selectedServer);
|
||||
serverListButtonRef.current?.click();
|
||||
}
|
||||
}
|
||||
|
||||
switchServer(serverList[nextIndex]);
|
||||
}, {
|
||||
target: popoverRef,
|
||||
});
|
||||
|
||||
const handleMouseMove = useCallback(() => {
|
||||
setHighlightId("");
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Popover className="relative">
|
||||
<Popover ref={popoverRef} className="relative">
|
||||
<PopoverButton ref={serverListButtonRef} className="flex items-center">
|
||||
<VisibleKey
|
||||
shortcut={serviceList}
|
||||
@@ -180,7 +200,9 @@ export function ServerList({ reconnect, clearChat }: ServerListProps) {
|
||||
</VisibleKey>
|
||||
</PopoverButton>
|
||||
|
||||
<PopoverPanel className="absolute right-0 z-10 mt-2 min-w-[240px] bg-white dark:bg-[#202126] rounded-lg shadow-lg border border-gray-200 dark:border-gray-700">
|
||||
<PopoverPanel
|
||||
onMouseMove={handleMouseMove}
|
||||
className="absolute right-0 z-10 mt-2 min-w-[240px] bg-white dark:bg-[#202126] rounded-lg shadow-lg border border-gray-200 dark:border-gray-700">
|
||||
<div className="p-3">
|
||||
<div className="flex items-center justify-between mb-3 whitespace-nowrap">
|
||||
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
@@ -202,9 +224,8 @@ export function ServerList({ reconnect, clearChat }: ServerListProps) {
|
||||
>
|
||||
<VisibleKey shortcut="R" onKeyPress={handleRefresh}>
|
||||
<RefreshCw
|
||||
className={`h-4 w-4 text-[#0287FF] transition-transform duration-1000 ${
|
||||
isRefreshing ? "animate-spin" : ""
|
||||
}`}
|
||||
className={`h-4 w-4 text-[#0287FF] transition-transform duration-1000 ${isRefreshing ? "animate-spin" : ""
|
||||
}`}
|
||||
/>
|
||||
</VisibleKey>
|
||||
</button>
|
||||
@@ -216,11 +237,11 @@ export function ServerList({ reconnect, clearChat }: ServerListProps) {
|
||||
<div
|
||||
key={server.id}
|
||||
onClick={() => switchServer(server)}
|
||||
className={`w-full flex items-center justify-between gap-1 p-2 rounded-lg transition-colors whitespace-nowrap ${
|
||||
currentService?.id === server.id
|
||||
className={`w-full flex items-center justify-between gap-1 p-2 rounded-lg transition-colors whitespace-nowrap
|
||||
${currentService?.id === server.id || highlightId === server.id
|
||||
? "bg-gray-100 dark:bg-gray-800"
|
||||
: "hover:bg-gray-50 dark:hover:bg-gray-800/50"
|
||||
}`}
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2 overflow-hidden min-w-0">
|
||||
<img
|
||||
|
||||
@@ -9,6 +9,7 @@ import FontIcon from "@/components/Common/Icons/FontIcon";
|
||||
import logoImg from "@/assets/icon.svg";
|
||||
import { AssistantFetcher } from "./AssistantFetcher";
|
||||
import type { StartPage } from "@/types/chat";
|
||||
import { unrequitable } from "@/utils";
|
||||
|
||||
export interface Response {
|
||||
app_settings?: {
|
||||
@@ -53,6 +54,10 @@ const Splash = ({ assistantIDs = [], startPage }: SplashProps) => {
|
||||
|
||||
let response: any;
|
||||
if (isTauri) {
|
||||
if (unrequitable()) {
|
||||
return setVisibleStartPage(false);
|
||||
}
|
||||
|
||||
response = await platformAdapter.invokeBackend<Response>(
|
||||
"get_system_settings",
|
||||
{
|
||||
|
||||
@@ -10,6 +10,23 @@ interface UserMessageProps {
|
||||
export const UserMessage = ({ messageContent }: UserMessageProps) => {
|
||||
const [showCopyButton, setShowCopyButton] = useState(false);
|
||||
|
||||
const handleDoubleClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
|
||||
const selection = window.getSelection();
|
||||
const range = document.createRange();
|
||||
|
||||
if (e.currentTarget && selection && range) {
|
||||
try {
|
||||
range.selectNodeContents(e.currentTarget);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
} catch (error) {
|
||||
console.error('Selection failed:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex gap-1 items-center justify-end"
|
||||
@@ -25,13 +42,7 @@ export const UserMessage = ({ messageContent }: UserMessageProps) => {
|
||||
</div>
|
||||
<div
|
||||
className="max-w-[85%] overflow-auto text-left px-3 py-2 bg-white dark:bg-[#202126] rounded-xl border border-black/12 dark:border-black/15 font-normal text-sm text-[#333333] dark:text-[#D8D8D8] cursor-pointer user-select-text whitespace-pre-wrap"
|
||||
onDoubleClick={(e) => {
|
||||
const selection = window.getSelection();
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(e.currentTarget);
|
||||
selection?.removeAllRanges();
|
||||
selection?.addRange(range);
|
||||
}}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
>
|
||||
{messageContent}
|
||||
</div>
|
||||
|
||||
@@ -35,7 +35,7 @@ export default function Cloud() {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
console.log("currentService", currentService);
|
||||
// console.log("currentService", currentService);
|
||||
setRefreshLoading(false);
|
||||
setIsConnect(true);
|
||||
}, [JSON.stringify(currentService)]);
|
||||
@@ -54,15 +54,17 @@ export default function Cloud() {
|
||||
return item;
|
||||
});
|
||||
}
|
||||
//console.log("list_coco_servers", res);
|
||||
// console.log("list_coco_servers", res);
|
||||
setServerList(res);
|
||||
|
||||
if (resetSelection && res.length > 0) {
|
||||
const currentServiceExists = res.some(
|
||||
(server: any) => server.id === currentService?.id
|
||||
);
|
||||
const matched = res.find((server: any) => {
|
||||
return server.id === currentService?.id;
|
||||
});
|
||||
|
||||
if (!currentServiceExists) {
|
||||
if (matched) {
|
||||
setCurrentService(matched);
|
||||
} else {
|
||||
setCurrentService(res[res.length - 1]);
|
||||
}
|
||||
}
|
||||
@@ -140,7 +142,7 @@ export default function Cloud() {
|
||||
refreshClick={refreshClick}
|
||||
/>
|
||||
|
||||
{currentService?.profile ? (
|
||||
{currentService?.profile && currentService?.available ? (
|
||||
<DataSourcesList server={currentService?.id} />
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -2,8 +2,6 @@ import React, { useState } from "react";
|
||||
import { ChevronLeft } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { specialCharacterFiltering } from "@/utils/index"
|
||||
|
||||
interface ConnectServiceProps {
|
||||
setIsConnect: (isConnect: boolean) => void;
|
||||
onAddServer: (endpoint: string) => void;
|
||||
@@ -29,9 +27,8 @@ export function Connect({ setIsConnect, onAddServer }: ConnectServiceProps) {
|
||||
};
|
||||
|
||||
const onChangeEndpoint = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = specialCharacterFiltering(e.target.value)
|
||||
setEndpointLink(value)
|
||||
}
|
||||
setEndpointLink(e.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl">
|
||||
|
||||
@@ -25,7 +25,7 @@ export function DataSourcesList({ server }: { server: string }) {
|
||||
.finally(() => {});
|
||||
|
||||
// fetch datasource data
|
||||
datasource_search(server)
|
||||
datasource_search({ id: server })
|
||||
.then((res: any) => {
|
||||
// console.log("datasource_search", res);
|
||||
setDatasourceData(res, server);
|
||||
|
||||
119
src/components/Common/DeleteDialog.tsx
Normal file
119
src/components/Common/DeleteDialog.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
import {
|
||||
Button,
|
||||
ButtonProps,
|
||||
Description,
|
||||
Dialog,
|
||||
DialogPanel,
|
||||
DialogTitle,
|
||||
} from "@headlessui/react";
|
||||
import { FC, KeyboardEvent } from "react";
|
||||
import clsx from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import VisibleKey from "./VisibleKey";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface DeleteDialogProps {
|
||||
isOpen: boolean;
|
||||
title: string;
|
||||
description: string;
|
||||
deleteButtonProps?: ButtonProps;
|
||||
cancelButtonProps?: ButtonProps;
|
||||
reverseButtonPosition?: boolean;
|
||||
setIsOpen: (isOpen: boolean) => void;
|
||||
onCancel: () => void;
|
||||
onDelete: () => void;
|
||||
}
|
||||
|
||||
const DeleteDialog: FC<DeleteDialogProps> = (props) => {
|
||||
const {
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
title,
|
||||
description,
|
||||
deleteButtonProps,
|
||||
cancelButtonProps,
|
||||
reverseButtonPosition,
|
||||
onCancel,
|
||||
onDelete,
|
||||
} = props;
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleEnter = (event: KeyboardEvent, fn: () => void) => {
|
||||
if (event.code !== "Enter") return;
|
||||
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
fn();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
onClose={() => setIsOpen(false)}
|
||||
className="relative z-1000"
|
||||
>
|
||||
<div
|
||||
id="headlessui-popover-panel:delete-history"
|
||||
className="fixed inset-0 flex items-center justify-center w-screen"
|
||||
>
|
||||
<DialogPanel className="flex flex-col justify-between w-[360px] h-[160px] p-3 text-[#333] dark:text-white/90 border border-[#e6e6e6] bg-white dark:bg-[#202126] dark:border-white/10 shadow-[0_4px_10px_rgba(0,0,0,0.2)] rounded-lg dark:shadow-[0_8px_20px_rgba(0,0,0,0.4)]">
|
||||
<div className="flex flex-col gap-3">
|
||||
<DialogTitle className="text-base font-bold">{title}</DialogTitle>
|
||||
<Description className="text-sm">{description}</Description>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={clsx("flex gap-4 self-end", {
|
||||
"flex-row-reverse": reverseButtonPosition,
|
||||
})}
|
||||
>
|
||||
<VisibleKey
|
||||
shortcut="N"
|
||||
shortcutClassName="left-[unset] right-0"
|
||||
onKeyPress={onCancel}
|
||||
>
|
||||
<Button
|
||||
{...cancelButtonProps}
|
||||
autoFocus
|
||||
className={twMerge(
|
||||
"h-8 px-4 text-sm text-[#666666] bg-[#F8F9FA] dark:text-white dark:bg-[#202126] border border-[#E6E6E6] dark:border-white/10 rounded-lg focus:border-black/30 dark:focus:border-white/50 transition",
|
||||
cancelButtonProps?.className as string
|
||||
)}
|
||||
onClick={onCancel}
|
||||
onKeyDown={(event) => {
|
||||
handleEnter(event, onCancel);
|
||||
}}
|
||||
>
|
||||
{t("deleteDialog.button.cancel")}
|
||||
</Button>
|
||||
</VisibleKey>
|
||||
|
||||
<VisibleKey
|
||||
shortcut="Y"
|
||||
shortcutClassName="left-[unset] right-0"
|
||||
onKeyPress={onDelete}
|
||||
>
|
||||
<Button
|
||||
{...deleteButtonProps}
|
||||
className={twMerge(
|
||||
"h-8 px-4 text-sm text-white bg-[#EF4444] rounded-lg border border-[#EF4444] focus:border-black/30 dark:focus:border-white/50 transition",
|
||||
deleteButtonProps?.className as string
|
||||
)}
|
||||
onClick={onDelete}
|
||||
onKeyDown={(event) => {
|
||||
handleEnter(event, onDelete);
|
||||
}}
|
||||
>
|
||||
{t("deleteDialog.button.delete")}
|
||||
</Button>
|
||||
</VisibleKey>
|
||||
</div>
|
||||
</DialogPanel>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteDialog;
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
Button,
|
||||
Description,
|
||||
Dialog,
|
||||
DialogPanel,
|
||||
@@ -8,6 +9,7 @@ import { useTranslation } from "react-i18next";
|
||||
|
||||
import VisibleKey from "@/components/Common/VisibleKey";
|
||||
import { Chat } from "@/types/chat";
|
||||
import { KeyboardEvent } from "react";
|
||||
|
||||
interface DeleteDialogProps {
|
||||
isOpen: boolean;
|
||||
@@ -24,6 +26,15 @@ const DeleteDialog = ({
|
||||
}: DeleteDialogProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleEnter = (event: KeyboardEvent, cb: () => void) => {
|
||||
if (event.code !== "Enter") return;
|
||||
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
cb();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
@@ -56,12 +67,18 @@ const DeleteDialog = ({
|
||||
shortcutClassName="left-[unset] right-0"
|
||||
onKeyPress={() => setIsOpen(false)}
|
||||
>
|
||||
<button
|
||||
className="h-8 px-4 text-sm text-[#666666] bg-[#F8F9FA] dark:text-white dark:bg-[#202126] border border-[#E6E6E6] dark:border-white/10 rounded-lg"
|
||||
<Button
|
||||
autoFocus
|
||||
className="h-8 px-4 text-sm text-[#666666] bg-[#F8F9FA] dark:text-white dark:bg-[#202126] border border-[#E6E6E6] dark:border-white/10 rounded-lg focus:border-black/30 dark:focus:border-white/50 transition"
|
||||
onClick={() => setIsOpen(false)}
|
||||
onKeyDown={(event) => {
|
||||
handleEnter(event, () => {
|
||||
setIsOpen(false);
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("history_list.delete_modal.button.cancel")}
|
||||
</button>
|
||||
</Button>
|
||||
</VisibleKey>
|
||||
|
||||
<VisibleKey
|
||||
@@ -69,12 +86,15 @@ const DeleteDialog = ({
|
||||
shortcutClassName="left-[unset] right-0"
|
||||
onKeyPress={handleRemove}
|
||||
>
|
||||
<button
|
||||
className="h-8 px-4 text-sm text-white bg-[#EF4444] rounded-lg"
|
||||
<Button
|
||||
className="h-8 px-4 text-sm text-white bg-[#EF4444] rounded-lg border border-[#EF4444] focus:border-black/30 dark:focus:border-white/50 transition"
|
||||
onClick={handleRemove}
|
||||
onKeyDown={(event) => {
|
||||
handleEnter(event, handleRemove);
|
||||
}}
|
||||
>
|
||||
{t("history_list.delete_modal.button.delete")}
|
||||
</button>
|
||||
</Button>
|
||||
</VisibleKey>
|
||||
</div>
|
||||
</DialogPanel>
|
||||
|
||||
@@ -113,6 +113,8 @@ const HistoryListContent: FC<HistoryListContentProps> = ({
|
||||
const scrollToElement = useCallback(
|
||||
(elementId: string, isKeyboardNav: boolean) => {
|
||||
if (!listRef.current) return;
|
||||
if (typeof window === 'undefined' || typeof document === 'undefined') return;
|
||||
|
||||
const element = listRef.current.querySelector(`#${elementId}`);
|
||||
if (!element) return;
|
||||
|
||||
@@ -121,7 +123,7 @@ const HistoryListContent: FC<HistoryListContentProps> = ({
|
||||
const isVisible =
|
||||
rect.top >= 0 &&
|
||||
rect.bottom <=
|
||||
(window.innerHeight || document.documentElement.clientHeight);
|
||||
(window.innerHeight || document.documentElement.clientHeight);
|
||||
|
||||
// Only scroll if element is not visible
|
||||
if (!isVisible) {
|
||||
|
||||
@@ -7,7 +7,6 @@ import { Pencil, Trash2 } from "lucide-react";
|
||||
|
||||
import type { Chat } from "@/types/chat";
|
||||
import VisibleKey from "../VisibleKey";
|
||||
import { specialCharacterFiltering } from "@/utils/index"
|
||||
|
||||
interface HistoryListItemProps {
|
||||
item: Chat;
|
||||
@@ -108,14 +107,14 @@ const HistoryListItem: FC<HistoryListItemProps> = ({
|
||||
onKeyDown={(event) => {
|
||||
if (event.key !== "Enter") return;
|
||||
|
||||
const value = specialCharacterFiltering(event.currentTarget.value)
|
||||
const value = event.currentTarget.value;
|
||||
|
||||
onRename(item._id, value);
|
||||
|
||||
setIsEdit(false);
|
||||
}}
|
||||
onBlur={(event) => {
|
||||
const value = specialCharacterFiltering(event.target.value)
|
||||
const value = event.currentTarget.value;
|
||||
|
||||
onRename(item._id, value);
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useTranslation } from "react-i18next";
|
||||
|
||||
import VisibleKey from "../VisibleKey";
|
||||
import { Chat } from "@/types/chat";
|
||||
import { closeHistoryPanel, specialCharacterFiltering } from "@/utils";
|
||||
import { closeHistoryPanel } from "@/utils";
|
||||
import HistoryListContent from "./HistoryListContent";
|
||||
|
||||
interface HistoryListProps {
|
||||
@@ -74,9 +74,7 @@ const HistoryList: FC<HistoryListProps> = (props) => {
|
||||
className="w-full bg-transparent outline-none"
|
||||
placeholder={t("history_list.search.placeholder")}
|
||||
onChange={(event) => {
|
||||
const value = specialCharacterFiltering(event.target.value)
|
||||
|
||||
debouncedSearch(value);
|
||||
debouncedSearch(event.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -29,7 +29,7 @@ const PopoverInput = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
|
||||
}
|
||||
);
|
||||
|
||||
return <Input ref={inputRef} {...props} />;
|
||||
return <Input autoCorrect="off" ref={inputRef} {...props} />;
|
||||
});
|
||||
|
||||
export default PopoverInput;
|
||||
|
||||
96
src/components/Common/PreviewImage.tsx
Normal file
96
src/components/Common/PreviewImage.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import { useBoolean } from "ahooks";
|
||||
import clsx from "clsx";
|
||||
import { CircleChevronLeft, CircleChevronRight } from "lucide-react";
|
||||
import { FC, useState } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
interface PreviewImageProps {
|
||||
urls: string[];
|
||||
classNames?: {
|
||||
container?: string;
|
||||
image?: string;
|
||||
};
|
||||
}
|
||||
|
||||
const PreviewImage: FC<PreviewImageProps> = (props) => {
|
||||
const { urls, classNames } = props;
|
||||
const [open, { setTrue, setFalse }] = useBoolean();
|
||||
const [index, setIndex] = useState(0);
|
||||
|
||||
const handlePrev = () => {
|
||||
const nextIndex = index === 0 ? urls.length - 1 : index - 1;
|
||||
|
||||
setIndex(nextIndex);
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
const nextIndex = index === urls.length - 1 ? 0 : index + 1;
|
||||
|
||||
setIndex(nextIndex);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={twMerge("flex gap-3", classNames?.container)}>
|
||||
{urls.map((url, index) => {
|
||||
return (
|
||||
<img
|
||||
key={url}
|
||||
src={url}
|
||||
className={twMerge("h-[125px] cursor-pointer", classNames?.image)}
|
||||
onClick={() => {
|
||||
setTrue();
|
||||
|
||||
setIndex(index);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={clsx("fixed inset-0 z-2000", {
|
||||
hidden: !open,
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className="absolute inset-0 flex items-center justify-center gap-2 px-2 bg-black/65 rounded-xl"
|
||||
onClick={setFalse}
|
||||
>
|
||||
<CircleChevronLeft
|
||||
className={clsx("size-6 text-white cursor-pointer", {
|
||||
"opacity-50 !cursor-not-allowed": urls.length === 1,
|
||||
})}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
|
||||
handlePrev();
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
className="flex-1"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<img src={urls[index]} className="size-full object-contain" />
|
||||
</div>
|
||||
|
||||
<CircleChevronRight
|
||||
className={clsx("size-6 text-white cursor-pointer", {
|
||||
"opacity-50 !cursor-not-allowed": urls.length === 1,
|
||||
})}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
|
||||
handleNext();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PreviewImage;
|
||||
118
src/components/Common/SearchEmpty.tsx
Normal file
118
src/components/Common/SearchEmpty.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
import { useThemeStore } from "@/stores/themeStore";
|
||||
import { FC } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface SearchEmptyProps {
|
||||
width?: number;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
const SearchEmpty: FC<SearchEmptyProps> = (props) => {
|
||||
const { width = 108, height } = props;
|
||||
const { isDark } = useThemeStore();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox="0 0 110 74"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
>
|
||||
<title>编组 7</title>
|
||||
<g
|
||||
id="插件商店"
|
||||
stroke="none"
|
||||
strokeWidth="1"
|
||||
fill="none"
|
||||
fillRule="evenodd"
|
||||
>
|
||||
<g
|
||||
id="无结果"
|
||||
transform="translate(-285, -238)"
|
||||
stroke={isDark ? "#666" : "#999"}
|
||||
strokeWidth="2"
|
||||
>
|
||||
<g id="编组-7" transform="translate(286.0008, 239)">
|
||||
<path
|
||||
d="M13.3231659,21.5136996 C13.3231659,19.3007352 13.3231659,14.8686653 13.3231659,8.21749008 C13.3231659,3.67909563 17.0122442,0 21.5629529,0 L88.2118384,0 C92.7625471,0 96.4516254,3.67909563 96.4516254,8.21749008 C96.4516254,10.094192 96.4516254,11.5017184 96.4516254,12.4400693 M96.4516254,51.9326386 C96.4516254,53.9261881 96.4516254,57.8761452 96.4516254,63.7825099 C96.4516254,68.3209044 92.7625471,72 88.2118384,72 L21.5629529,72 C17.0122442,72 13.3231659,68.3209044 13.3231659,63.7825099 L13.3231659,60.938714"
|
||||
id="形状"
|
||||
strokeDasharray="7,3"
|
||||
></path>
|
||||
<ellipse
|
||||
id="椭圆形备份"
|
||||
cx="81.1877607"
|
||||
cy="29.2037781"
|
||||
rx="18.4438929"
|
||||
ry="18.182295"
|
||||
></ellipse>
|
||||
<line
|
||||
x1="94.7817074"
|
||||
y1="42.614832"
|
||||
x2="108"
|
||||
y2="55.6552859"
|
||||
id="路径-4备份"
|
||||
strokeLinecap="round"
|
||||
></line>
|
||||
<path
|
||||
d="M10.5844571,27.5074364 C16.6773969,25.9924085 23.1773619,29.6710245 27.386048,36.2620174 C22.1657703,35.1830338 16.8575124,35.21291 11.6484221,36.5081661 C7.41338948,37.5612222 3.51420993,39.3834599 -0.000291581531,41.8525859 C0.923937746,34.6468262 4.82125546,28.9404773 10.5844571,27.5074364 Z"
|
||||
id="形状结合备份-7"
|
||||
strokeLinejoin="round"
|
||||
></path>
|
||||
<path
|
||||
d="M37.8969953,26.1959104 C43.5101629,25.7114352 48.6739265,29.2024386 51.2337447,34.5954621 C47.1674647,33.4803904 42.8983353,33.0573353 38.538847,33.4336049 C34.1798035,33.8098457 30.0503934,34.9576309 26.2420368,36.7514978 C27.813275,31.0027019 32.2840091,26.6803824 37.8969953,26.1959104 Z"
|
||||
id="形状结合备份-8"
|
||||
strokeLinejoin="round"
|
||||
></path>
|
||||
<path
|
||||
d="M31.539458,18.2001402 C36.5615365,15.6541096 42.6600468,16.9613729 47.0591458,21.0046327 C42.8699689,21.4911269 38.7533829,22.6937278 34.8532022,24.6709927 C30.9523581,26.6486264 27.5543743,29.2560064 24.6969024,32.3425899 C23.9951662,26.4249906 26.5167961,20.7465085 31.539458,18.2001402 Z"
|
||||
id="形状结合备份-11"
|
||||
strokeLinejoin="round"
|
||||
transform="translate(35.821, 24.6183) rotate(-12) translate(-35.821, -24.6183)"
|
||||
></path>
|
||||
<path
|
||||
d="M10.5436753,41.4266578 C14.7331796,36.1358502 21.2661914,34.5295217 27.1728604,36.6822627 C23.0525507,39.325329 19.2570765,42.737484 15.9487291,46.9155032 C12.640202,51.0937495 10.0569324,55.7372814 8.18485826,60.662697 C5.65340555,54.2465445 6.3541482,46.7174943 10.5436753,41.4266578 Z"
|
||||
id="形状结合备份-9"
|
||||
strokeLinejoin="round"
|
||||
></path>
|
||||
<path
|
||||
d="M26.9124079,37.8021241 C31.7762268,33.9875103 38.3818206,33.4524199 44.0035888,37.0544707 C49.6980215,40.7030801 52.8264479,47.5991177 52.5343362,54.4887727 C49.1502233,50.435903 45.1867531,46.8795531 40.6898778,43.9982579 C36.1927406,41.116795 31.4857464,39.1178133 26.7239231,37.952533 Z"
|
||||
id="形状结合备份-10"
|
||||
strokeLinejoin="round"
|
||||
></path>
|
||||
<path
|
||||
d="M25.2800001,38.5113791 C28.9578107,48.5878922 28.4367035,59.4047936 23.7166785,70.9620834"
|
||||
id="路径-7备份-2"
|
||||
></path>
|
||||
<path
|
||||
d="M29.8677805,38.5132245 C35.0745191,48.0589279 36.9874556,58.8758293 35.60659,70.9639287"
|
||||
id="路径-7备份-3"
|
||||
></path>
|
||||
<line
|
||||
x1="28.2081316"
|
||||
y1="51.976707"
|
||||
x2="30.2418038"
|
||||
y2="51.976707"
|
||||
id="路径-2"
|
||||
></line>
|
||||
<line
|
||||
x1="28.2081316"
|
||||
y1="57.0471296"
|
||||
x2="31.2586399"
|
||||
y2="57.0471296"
|
||||
id="路径-2备份"
|
||||
></line>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
<span className="text-sm text-[#999]">{t("search.main.noResults")}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchEmpty;
|
||||
@@ -18,6 +18,7 @@ import source_default_img from "@/assets/images/source_default.png";
|
||||
import source_default_dark_img from "@/assets/images/source_default_dark.png";
|
||||
import { useThemeStore } from "@/stores/themeStore";
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
import FontIcon from "../Icons/FontIcon";
|
||||
|
||||
interface FooterProps {
|
||||
setIsPinnedWeb?: (value: boolean) => void;
|
||||
@@ -26,7 +27,13 @@ interface FooterProps {
|
||||
export default function Footer({ setIsPinnedWeb }: FooterProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { sourceData, goAskAi } = useSearchStore();
|
||||
const {
|
||||
sourceData,
|
||||
goAskAi,
|
||||
selectedExtension,
|
||||
visibleExtensionStore,
|
||||
visibleExtensionDetail,
|
||||
} = useSearchStore();
|
||||
|
||||
const isDark = useThemeStore((state) => state.isDark);
|
||||
|
||||
@@ -56,6 +63,63 @@ export default function Footer({ setIsPinnedWeb }: FooterProps) {
|
||||
return platformAdapter.emitEvent("open_settings", "");
|
||||
}, []);
|
||||
|
||||
const renderLeft = () => {
|
||||
if (sourceData?.source?.name) {
|
||||
return (
|
||||
<CommonIcon
|
||||
item={sourceData}
|
||||
renderOrder={["connector_icon", "default_icon"]}
|
||||
itemIcon={sourceData?.source?.icon}
|
||||
defaultIcon={isDark ? source_default_dark_img : source_default_img}
|
||||
className="w-4 h-4"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (visibleExtensionDetail && selectedExtension) {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<img src={selectedExtension.icon} className="size-5" />
|
||||
<span className="text-sm">{selectedExtension.name}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (visibleExtensionStore) {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<FontIcon name="font_Store" className="size-5" />
|
||||
<span className="text-sm">Extension Store</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<img
|
||||
src={logoImg}
|
||||
className="w-4 h-4 cursor-pointer"
|
||||
onClick={openSetting}
|
||||
alt={t("search.footer.logoAlt")}
|
||||
/>
|
||||
|
||||
<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",
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
data-tauri-drag-region={isTauri}
|
||||
@@ -64,40 +128,7 @@ export default function Footer({ setIsPinnedWeb }: FooterProps) {
|
||||
{isTauri ? (
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-center space-x-2">
|
||||
{sourceData?.source?.name ? (
|
||||
<CommonIcon
|
||||
item={sourceData}
|
||||
renderOrder={["connector_icon", "default_icon"]}
|
||||
itemIcon={sourceData?.source?.icon}
|
||||
defaultIcon={
|
||||
isDark ? source_default_dark_img : source_default_img
|
||||
}
|
||||
className="w-4 h-4"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={logoImg}
|
||||
className="w-4 h-4 cursor-pointer"
|
||||
onClick={openSetting}
|
||||
alt={t("search.footer.logoAlt")}
|
||||
/>
|
||||
)}
|
||||
<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",
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
{renderLeft()}
|
||||
|
||||
<button
|
||||
onClick={togglePin}
|
||||
@@ -117,12 +148,23 @@ export default function Footer({ setIsPinnedWeb }: FooterProps) {
|
||||
)}
|
||||
|
||||
<div className={`flex mobile:hidden items-center gap-3`}>
|
||||
<div className="gap-1 flex items-center text-[#666] dark:text-[#666] text-xs">
|
||||
<div
|
||||
className={clsx(
|
||||
"gap-1 flex items-center text-[#666] dark:text-[#666] text-xs",
|
||||
{
|
||||
hidden:
|
||||
(visibleExtensionStore && !selectedExtension) ||
|
||||
visibleExtensionDetail ||
|
||||
selectedExtension?.installed,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<span className="mr-1.5">
|
||||
{goAskAi
|
||||
? t("search.askCocoAi.continueInChat")
|
||||
: (visibleExtensionStore || visibleExtensionDetail) && !selectedExtension?.installed
|
||||
? t("search.footer.install")
|
||||
: t("search.footer.select")}
|
||||
:
|
||||
</span>
|
||||
<kbd className="coco-modal-footer-commands-key pr-1">
|
||||
<div className="flex items-center justify-center min-w-3 h-3">
|
||||
@@ -131,16 +173,32 @@ export default function Footer({ setIsPinnedWeb }: FooterProps) {
|
||||
</kbd>
|
||||
+
|
||||
<kbd className="coco-modal-footer-commands-key pr-1">
|
||||
{goAskAi ? (
|
||||
{goAskAi || selectedExtension ? (
|
||||
<CornerDownLeft className="w-3 h-3" />
|
||||
) : (
|
||||
<ArrowDown01 className="w-3 h-3" />
|
||||
)}
|
||||
</kbd>
|
||||
</div>
|
||||
<div className="flex items-center text-[#666] dark:text-[#666] text-xs">
|
||||
|
||||
<div
|
||||
className={clsx(
|
||||
"flex items-center text-[#666] dark:text-[#666] text-xs",
|
||||
{
|
||||
hidden:
|
||||
(visibleExtensionStore && !selectedExtension) ||
|
||||
(visibleExtensionDetail && selectedExtension?.installed),
|
||||
}
|
||||
)}
|
||||
>
|
||||
<span className="mr-1.5">
|
||||
{goAskAi ? t("search.askCocoAi.copy") : t("search.footer.open")}:{" "}
|
||||
{goAskAi
|
||||
? t("search.askCocoAi.copy")
|
||||
: visibleExtensionDetail && !selectedExtension?.installed
|
||||
? t("search.footer.install")
|
||||
: visibleExtensionStore
|
||||
? t("search.footer.details")
|
||||
: t("search.footer.open")}
|
||||
</span>
|
||||
<kbd className="coco-modal-footer-commands-key pr-1">
|
||||
<CornerDownLeft className="w-3 h-3" />
|
||||
|
||||
@@ -2,9 +2,9 @@ import { useTranslation } from "react-i18next";
|
||||
|
||||
import { isMac } from "@/utils/platform";
|
||||
import { useShortcutsStore } from "@/stores/shortcutsStore";
|
||||
import noDataImg from "@/assets/coconut-tree.png";
|
||||
import clsx from "clsx";
|
||||
import { formatKey } from "@/utils/keyboardUtils";
|
||||
import SearchEmpty from "../SearchEmpty";
|
||||
|
||||
export const NoResults = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -17,10 +17,8 @@ export const NoResults = () => {
|
||||
data-tauri-drag-region
|
||||
className="h-full w-full flex flex-col justify-center items-center"
|
||||
>
|
||||
<img src={noDataImg} alt="no-data" className="w-16 h-16" />
|
||||
<div className="mt-4 text-sm text-[#999] dark:text-[#666]">
|
||||
{t("search.main.noResults")}
|
||||
</div>
|
||||
<SearchEmpty />
|
||||
|
||||
<div
|
||||
className={`flex mobile:hidden mt-10 text-sm text-[#333] dark:text-[#D8D8D8]`}
|
||||
>
|
||||
|
||||
@@ -95,4 +95,4 @@ const Footer = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
export default Footer;
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
} from "ahooks";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { noop } from "lodash-es";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
import { ChatMessage } from "../ChatMessage";
|
||||
import { useSearchStore } from "@/stores/searchStore";
|
||||
@@ -15,7 +16,6 @@ import useMessageChunkData from "@/hooks/useMessageChunkData";
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
import { useExtensionsStore } from "@/stores/extensionsStore";
|
||||
import { useShortcutsStore } from "@/stores/shortcutsStore";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
interface State {
|
||||
serverId?: string;
|
||||
@@ -24,12 +24,9 @@ interface State {
|
||||
}
|
||||
|
||||
const AskAi = () => {
|
||||
const askAiMessage = useSearchStore((state) => state.askAiMessage);
|
||||
const { askAiMessage, setGoAskAi, setSelectedAssistant, setAskAiSessionId, selectedAssistant, setAskAiServerId, setAskAiAssistantId } = useSearchStore();
|
||||
|
||||
const addError = useAppStore((state) => state.addError);
|
||||
const setGoAskAi = useSearchStore((state) => state.setGoAskAi);
|
||||
const setSelectedAssistant = useSearchStore((state) => {
|
||||
return state.setSelectedAssistant;
|
||||
});
|
||||
|
||||
const {
|
||||
data: {
|
||||
@@ -62,23 +59,11 @@ const AskAi = () => {
|
||||
|
||||
const unlisten = useRef<() => void>(noop);
|
||||
const sessionIdRef = useRef<string>("");
|
||||
const setAskAiSessionId = useSearchStore((state) => state.setAskAiSessionId);
|
||||
const quickAiAccessServer = useExtensionsStore((state) => {
|
||||
return state.quickAiAccessServer;
|
||||
});
|
||||
const quickAiAccessAssistant = useExtensionsStore((state) => {
|
||||
return state.quickAiAccessAssistant;
|
||||
});
|
||||
const selectedAssistant = useSearchStore((state) => {
|
||||
return state.selectedAssistant;
|
||||
});
|
||||
const setAskAiServerId = useSearchStore((state) => {
|
||||
return state.setAskAiServerId;
|
||||
});
|
||||
|
||||
const { quickAiAccessServer, quickAiAccessAssistant } = useExtensionsStore();
|
||||
|
||||
const state = useReactive<State>({});
|
||||
const setAskAiAssistantId = useSearchStore((state) => {
|
||||
return state.setAskAiAssistantId;
|
||||
});
|
||||
|
||||
const modifierKey = useShortcutsStore((state) => state.modifierKey);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -190,16 +175,11 @@ const AskAi = () => {
|
||||
|
||||
const { serverId, assistantId } = state;
|
||||
|
||||
await platformAdapter.commands("open_session_chat", {
|
||||
serverId,
|
||||
sessionId: sessionIdRef.current,
|
||||
});
|
||||
|
||||
platformAdapter.emitEvent("toggle-to-chat-mode");
|
||||
|
||||
setAskAiServerId(serverId);
|
||||
setAskAiSessionId(sessionIdRef.current);
|
||||
setAskAiAssistantId(assistantId);
|
||||
|
||||
platformAdapter.emitEvent("toggle-to-chat-mode");
|
||||
},
|
||||
{
|
||||
exactMatch: true,
|
||||
|
||||
@@ -23,12 +23,22 @@ export function useAssistantManager({
|
||||
}: AssistantManagerProps) {
|
||||
const isTauri = useAppStore((state) => state.isTauri);
|
||||
|
||||
const { goAskAi, setGoAskAi, setAskAiMessage, selectedAssistant } =
|
||||
useSearchStore();
|
||||
const {
|
||||
goAskAi,
|
||||
setGoAskAi,
|
||||
setAskAiMessage,
|
||||
selectedAssistant,
|
||||
selectedSearchContent,
|
||||
visibleExtensionStore,
|
||||
setVisibleExtensionStore,
|
||||
setSearchValue,
|
||||
visibleExtensionDetail,
|
||||
setVisibleExtensionDetail,
|
||||
sourceData,
|
||||
setSourceData,
|
||||
} = useSearchStore();
|
||||
|
||||
const quickAiAccessAssistant = useExtensionsStore(
|
||||
(state) => state.quickAiAccessAssistant
|
||||
);
|
||||
const { quickAiAccessAssistant, disabledExtensions } = useExtensionsStore();
|
||||
|
||||
const askAIRef = useRef<Assistant | null>(null);
|
||||
|
||||
@@ -41,6 +51,8 @@ export function useAssistantManager({
|
||||
|
||||
const assistant_get = useCallback(async () => {
|
||||
if (!askAI?.id) return;
|
||||
if (disabledExtensions.includes("QuickAIAccess")) return;
|
||||
|
||||
if (isTauri) {
|
||||
if (!askAI?.querySource?.id) return;
|
||||
const res = await platformAdapter.commands("assistant_get", {
|
||||
@@ -56,17 +68,17 @@ export function useAssistantManager({
|
||||
}
|
||||
setAssistantDetail(res);
|
||||
}
|
||||
}, [askAI]);
|
||||
}, [askAI?.id, askAI?.querySource?.id, disabledExtensions]);
|
||||
|
||||
const handleAskAi = () => {
|
||||
const handleAskAi = useCallback(() => {
|
||||
if (!isTauri) return;
|
||||
|
||||
askAIRef.current = cloneDeep(askAI);
|
||||
if (disabledExtensions.includes("QuickAIAccess")) return;
|
||||
|
||||
askAIRef.current = cloneDeep(askAI);
|
||||
if (!askAIRef.current) return;
|
||||
|
||||
let value = inputValue.trim();
|
||||
|
||||
if (isEmpty(value)) return;
|
||||
|
||||
if (!goAskAi && selectedAssistant) {
|
||||
@@ -76,41 +88,64 @@ export function useAssistantManager({
|
||||
changeInput("");
|
||||
setAskAiMessage(value);
|
||||
setGoAskAi(true);
|
||||
};
|
||||
}, [disabledExtensions, askAI, inputValue, goAskAi, selectedAssistant]);
|
||||
|
||||
const handleKeyDownAutoResizeTextarea = (
|
||||
e: React.KeyboardEvent<HTMLTextAreaElement>
|
||||
) => {
|
||||
const { key, shiftKey } = e;
|
||||
const handleKeyDownAutoResizeTextarea = useCallback(
|
||||
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
const { key, shiftKey, currentTarget } = e;
|
||||
const { value } = currentTarget;
|
||||
|
||||
const { value } = e.currentTarget;
|
||||
if (key === "Backspace" && value === "") {
|
||||
if (goAskAi) {
|
||||
return setGoAskAi(false);
|
||||
}
|
||||
|
||||
if (key === "Backspace" && value === "") {
|
||||
return setGoAskAi(false);
|
||||
}
|
||||
if (visibleExtensionDetail) {
|
||||
return setVisibleExtensionDetail(false);
|
||||
}
|
||||
|
||||
if (key === "Tab" && isTauri) {
|
||||
e.preventDefault();
|
||||
if (visibleExtensionStore) {
|
||||
return setVisibleExtensionStore(false);
|
||||
}
|
||||
|
||||
if (isChatMode) {
|
||||
return;
|
||||
if (sourceData) {
|
||||
return setSourceData(void 0);
|
||||
}
|
||||
}
|
||||
|
||||
assistant_get();
|
||||
if (key === "Tab" && !isChatMode && isTauri) {
|
||||
e.preventDefault();
|
||||
|
||||
return handleAskAi();
|
||||
}
|
||||
if (visibleExtensionStore) return;
|
||||
|
||||
if (key === "Enter" && !shiftKey && !isChatMode && isTauri) {
|
||||
e.preventDefault();
|
||||
if (selectedSearchContent?.id === "Extension Store") {
|
||||
changeInput("");
|
||||
setSearchValue("");
|
||||
return setVisibleExtensionStore(true);
|
||||
}
|
||||
|
||||
if (goAskAi) {
|
||||
assistant_get();
|
||||
return handleAskAi();
|
||||
}
|
||||
|
||||
handleSubmit();
|
||||
}
|
||||
};
|
||||
if (key === "Enter" && !shiftKey && !isChatMode && isTauri) {
|
||||
e.preventDefault();
|
||||
|
||||
goAskAi ? handleAskAi() : handleSubmit();
|
||||
}
|
||||
},
|
||||
[
|
||||
isChatMode,
|
||||
goAskAi,
|
||||
assistant_get,
|
||||
handleAskAi,
|
||||
handleSubmit,
|
||||
selectedSearchContent,
|
||||
visibleExtensionStore,
|
||||
visibleExtensionDetail,
|
||||
sourceData,
|
||||
]
|
||||
);
|
||||
|
||||
return {
|
||||
askAI,
|
||||
|
||||
@@ -9,8 +9,6 @@ import {
|
||||
} from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { specialCharacterFiltering } from "@/utils/index";
|
||||
|
||||
const LINE_HEIGHT = 24; // 1.5rem
|
||||
const MAX_FIRST_LINE_WIDTH = 470; // Width in pixels for first line
|
||||
const MAX_HEIGHT = 240; // 15rem
|
||||
@@ -51,6 +49,8 @@ const AutoResizeTextarea = forwardRef<
|
||||
() => {
|
||||
const textarea = textareaRef.current;
|
||||
if (!textarea) return;
|
||||
if (typeof window === "undefined" || typeof document === "undefined")
|
||||
return;
|
||||
|
||||
// Reset height to auto to get the correct scrollHeight
|
||||
textarea.style.height = "auto";
|
||||
@@ -137,8 +137,7 @@ const AutoResizeTextarea = forwardRef<
|
||||
|
||||
const handleChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const value = specialCharacterFiltering(e.target.value);
|
||||
setInput(value);
|
||||
setInput(e.target.value);
|
||||
},
|
||||
[setInput]
|
||||
);
|
||||
|
||||
@@ -1,66 +1,120 @@
|
||||
import { useClickAway, useCreation, useReactive } from "ahooks";
|
||||
import clsx from "clsx";
|
||||
import { isNil, lowerCase, noop } from "lodash-es";
|
||||
import { Copy, Link, SquareArrowOutUpRight } from "lucide-react";
|
||||
import { cloneElement, FC, useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
Copy,
|
||||
Download,
|
||||
Info,
|
||||
Link,
|
||||
Settings,
|
||||
SquareArrowOutUpRight,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import { cloneElement, useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Input } from "@headlessui/react";
|
||||
|
||||
import { useOSKeyPress } from "@/hooks/useOSKeyPress";
|
||||
import { useSearchStore } from "@/stores/searchStore";
|
||||
import { copyToClipboard, specialCharacterFiltering } from "@/utils";
|
||||
import { copyToClipboard } from "@/utils";
|
||||
import { isMac } from "@/utils/platform";
|
||||
import { CONTEXT_MENU_PANEL_ID } from "@/constants";
|
||||
import { useShortcutsStore } from "@/stores/shortcutsStore";
|
||||
import VisibleKey from "../Common/VisibleKey";
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
import SearchEmpty from "../Common/SearchEmpty";
|
||||
|
||||
interface State {
|
||||
activeMenuIndex: number;
|
||||
}
|
||||
|
||||
interface ContextMenuProps {}
|
||||
|
||||
const ContextMenu: FC<ContextMenuProps> = () => {
|
||||
const ContextMenu = () => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const { t, i18n } = useTranslation();
|
||||
const state = useReactive<State>({
|
||||
activeMenuIndex: 0,
|
||||
});
|
||||
const visibleContextMenu = useSearchStore((state) => {
|
||||
return state.visibleContextMenu;
|
||||
});
|
||||
const setVisibleContextMenu = useSearchStore((state) => {
|
||||
return state.setVisibleContextMenu;
|
||||
});
|
||||
const setOpenPopover = useShortcutsStore((state) => state.setOpenPopover);
|
||||
const selectedSearchContent = useSearchStore((state) => {
|
||||
return state.selectedSearchContent;
|
||||
});
|
||||
const { setOpenPopover } = useShortcutsStore();
|
||||
const [searchMenus, setSearchMenus] = useState<typeof menus>([]);
|
||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||
const {
|
||||
visibleContextMenu,
|
||||
setVisibleContextMenu,
|
||||
selectedSearchContent,
|
||||
selectedExtension,
|
||||
setVisibleExtensionDetail,
|
||||
} = useSearchStore();
|
||||
|
||||
const title = useCreation(() => {
|
||||
if (selectedExtension) {
|
||||
return selectedExtension.name;
|
||||
}
|
||||
|
||||
if (selectedSearchContent?.id === "Calculator") {
|
||||
return t("search.contextMenu.title.calculator");
|
||||
}
|
||||
|
||||
return selectedSearchContent?.title;
|
||||
}, [selectedSearchContent]);
|
||||
}, [selectedSearchContent, selectedExtension]);
|
||||
|
||||
const menus = useCreation(() => {
|
||||
if (isNil(selectedSearchContent)) return [];
|
||||
|
||||
const { url, category, type, payload } = selectedSearchContent;
|
||||
const { query, result } = payload ?? {};
|
||||
|
||||
if (category === "AI Overview") {
|
||||
setSearchMenus([]);
|
||||
if (selectedExtension) {
|
||||
return [
|
||||
{
|
||||
name: t("search.contextMenu.details"),
|
||||
icon: <Info />,
|
||||
keys: isMac ? ["↩︎"] : ["Enter"],
|
||||
shortcut: "enter",
|
||||
clickEvent() {
|
||||
setVisibleExtensionDetail(true);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: t("search.contextMenu.install"),
|
||||
icon: <Download />,
|
||||
keys: isMac ? ["⌘", "↩︎"] : ["Ctrl", "Enter"],
|
||||
shortcut: isMac ? "meta.enter" : "ctrl.enter",
|
||||
hide: selectedExtension.installed,
|
||||
clickEvent() {
|
||||
platformAdapter.emitEvent("install-extension");
|
||||
},
|
||||
},
|
||||
{
|
||||
name: t("search.contextMenu.configureExtension"),
|
||||
icon: <Settings />,
|
||||
keys: isMac ? ["⌘", "/"] : ["Ctrl", "/"],
|
||||
shortcut: isMac ? "meta.forwardslash" : "ctrl.forwardslash",
|
||||
hide: !selectedExtension.installed,
|
||||
clickEvent() {
|
||||
platformAdapter.emitEvent("config-extension", selectedExtension.id);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: t("search.contextMenu.uninstall"),
|
||||
icon: <Trash2 />,
|
||||
keys: isMac ? ["⌘", "X"] : ["Ctrl", "X"],
|
||||
shortcut: isMac ? "meta.x" : "ctrl.x",
|
||||
hide: !selectedExtension.installed,
|
||||
color: "#fa4545",
|
||||
clickEvent() {
|
||||
platformAdapter.emitEvent("uninstall-extension");
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (isNil(selectedSearchContent)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const menus = [
|
||||
const { id, url, category, type, payload } = selectedSearchContent;
|
||||
const { query, result } = payload ?? {};
|
||||
|
||||
if (category === "AI Overview") {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
name: t("search.contextMenu.open"),
|
||||
icon: <SquareArrowOutUpRight />,
|
||||
@@ -76,7 +130,10 @@ const ContextMenu: FC<ContextMenuProps> = () => {
|
||||
icon: <Link />,
|
||||
keys: isMac ? ["⌘", "L"] : ["Ctrl", "L"],
|
||||
shortcut: isMac ? "meta.l" : "ctrl.l",
|
||||
hide: category === "Calculator" || type === "AI Assistant",
|
||||
hide:
|
||||
category === "Calculator" ||
|
||||
type === "AI Assistant" ||
|
||||
id === "Extension Store",
|
||||
clickEvent() {
|
||||
copyToClipboard(url);
|
||||
},
|
||||
@@ -95,7 +152,7 @@ const ContextMenu: FC<ContextMenuProps> = () => {
|
||||
name: t("search.contextMenu.copyUppercaseAnswer"),
|
||||
icon: <Copy />,
|
||||
keys: isMac ? ["⌘", "↩︎"] : ["Ctrl", "Enter"],
|
||||
shortcut: "meta.enter",
|
||||
shortcut: isMac ? "meta.enter" : "ctrl.enter",
|
||||
hide: category !== "Calculator",
|
||||
clickEvent() {
|
||||
copyToClipboard(i18n.language === "zh" ? result.toZh : result.toEn);
|
||||
@@ -105,24 +162,24 @@ const ContextMenu: FC<ContextMenuProps> = () => {
|
||||
name: t("search.contextMenu.copyQuestionAndAnswer"),
|
||||
icon: <Copy />,
|
||||
keys: isMac ? ["⌘", "L"] : ["Ctrl", "L"],
|
||||
shortcut: "meta.l",
|
||||
shortcut: isMac ? "meta.l" : "ctrl+l",
|
||||
hide: category !== "Calculator",
|
||||
clickEvent() {
|
||||
copyToClipboard(`${query.value} = ${result.value}`);
|
||||
},
|
||||
},
|
||||
];
|
||||
}, [selectedSearchContent, selectedExtension]);
|
||||
|
||||
const filterMenus = menus.filter((item) => !item.hide);
|
||||
useEffect(() => {
|
||||
const filterMenus = menus.filter((item) => !item?.hide);
|
||||
|
||||
setSearchMenus(filterMenus);
|
||||
|
||||
return filterMenus;
|
||||
}, [selectedSearchContent]);
|
||||
}, [menus]);
|
||||
|
||||
const shortcuts = useCreation(() => {
|
||||
return menus.map((item) => item.shortcut);
|
||||
}, [menus]);
|
||||
return searchMenus.map((item) => item.shortcut);
|
||||
}, [searchMenus]);
|
||||
|
||||
useEffect(() => {
|
||||
state.activeMenuIndex = 0;
|
||||
@@ -134,8 +191,8 @@ const ContextMenu: FC<ContextMenuProps> = () => {
|
||||
}
|
||||
}, [selectedSearchContent]);
|
||||
|
||||
useOSKeyPress(["meta.k", "ctrl.k"], () => {
|
||||
if (isNil(selectedSearchContent)) return;
|
||||
useOSKeyPress(["meta.k", "ctrl+k"], () => {
|
||||
if (isNil(selectedSearchContent) && isNil(selectedExtension)) return;
|
||||
|
||||
setVisibleContextMenu(!visibleContextMenu);
|
||||
});
|
||||
@@ -148,7 +205,7 @@ const ContextMenu: FC<ContextMenuProps> = () => {
|
||||
if (!visibleContextMenu) return;
|
||||
|
||||
const index = state.activeMenuIndex;
|
||||
const length = menus.length;
|
||||
const length = searchMenus.length;
|
||||
|
||||
switch (key) {
|
||||
case "uparrow":
|
||||
@@ -166,9 +223,9 @@ const ContextMenu: FC<ContextMenuProps> = () => {
|
||||
let matched;
|
||||
|
||||
if (key === "enter") {
|
||||
matched = menus.find((_, index) => index === state.activeMenuIndex);
|
||||
matched = searchMenus.find((_, index) => index === state.activeMenuIndex);
|
||||
} else {
|
||||
matched = menus.find((item) => item.shortcut === key);
|
||||
matched = searchMenus.find((item) => item.shortcut === key);
|
||||
}
|
||||
|
||||
handleClick(matched?.clickEvent);
|
||||
@@ -181,11 +238,13 @@ const ContextMenu: FC<ContextMenuProps> = () => {
|
||||
const handleClick = (click = noop) => {
|
||||
click?.();
|
||||
|
||||
setVisibleContextMenu(false);
|
||||
requestAnimationFrame(() => {
|
||||
setVisibleContextMenu(false);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
searchMenus.length > 0 && (
|
||||
menus.length > 0 && (
|
||||
<>
|
||||
{visibleContextMenu && (
|
||||
<div
|
||||
@@ -210,50 +269,59 @@ const ContextMenu: FC<ContextMenuProps> = () => {
|
||||
>
|
||||
<div className="text-[#999] dark:text-[#666] truncate">{title}</div>
|
||||
|
||||
<ul className="flex flex-col -mx-2 p-0">
|
||||
{searchMenus.map((item, index) => {
|
||||
const { name, icon, keys, clickEvent } = item;
|
||||
{searchMenus.length > 0 ? (
|
||||
<ul className="flex flex-col -mx-2 p-0">
|
||||
{searchMenus.map((item, index) => {
|
||||
const { name, icon, keys, color, clickEvent } = item;
|
||||
|
||||
return (
|
||||
<li
|
||||
key={name}
|
||||
className={clsx(
|
||||
"flex justify-between items-center gap-2 px-2 py-2 rounded-lg cursor-pointer",
|
||||
{
|
||||
"bg-[#EDEDED] dark:bg-[#202126]":
|
||||
index === state.activeMenuIndex,
|
||||
}
|
||||
)}
|
||||
onMouseEnter={() => {
|
||||
state.activeMenuIndex = index;
|
||||
}}
|
||||
onClick={() => handleClick(clickEvent)}
|
||||
>
|
||||
<div className="flex items-center gap-2 text-black/80 dark:text-white/80">
|
||||
{cloneElement(icon, { className: "size-4" })}
|
||||
return (
|
||||
<li
|
||||
key={name}
|
||||
className={clsx(
|
||||
"flex justify-between items-center gap-2 px-2 py-2 rounded-lg cursor-pointer",
|
||||
{
|
||||
"bg-[#EDEDED] dark:bg-[#202126]":
|
||||
index === state.activeMenuIndex,
|
||||
}
|
||||
)}
|
||||
onMouseEnter={() => {
|
||||
state.activeMenuIndex = index;
|
||||
}}
|
||||
onClick={() => handleClick(clickEvent)}
|
||||
>
|
||||
<div className="flex items-center gap-2 text-black/80 dark:text-white/80">
|
||||
{cloneElement(icon, {
|
||||
className: "size-4",
|
||||
style: { color },
|
||||
})}
|
||||
|
||||
<span>{name}</span>
|
||||
</div>
|
||||
<span style={{ color }}>{name}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-[4px] text-black/60 dark:text-white/60">
|
||||
{keys.map((key) => (
|
||||
<kbd
|
||||
key={key}
|
||||
className={clsx(
|
||||
"flex justify-center items-center font-sans h-[20px] min-w-[20px] text-[10px] rounded-md border border-[#EDEDED] dark:border-white/10 bg-white dark:bg-[#202126]",
|
||||
{
|
||||
"px-1": key.length > 1,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{key}
|
||||
</kbd>
|
||||
))}
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
<div className="flex gap-[4px] text-black/60 dark:text-white/60">
|
||||
{keys.map((key) => (
|
||||
<kbd
|
||||
key={key}
|
||||
className={clsx(
|
||||
"flex justify-center items-center font-sans h-[20px] min-w-[20px] text-[10px] rounded-md border border-[#EDEDED] dark:border-white/10 bg-white dark:bg-[#202126]",
|
||||
{
|
||||
"px-1": key.length > 1,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{key}
|
||||
</kbd>
|
||||
))}
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
) : (
|
||||
<div className="py-4">
|
||||
<SearchEmpty width={80} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="-mx-3 p-2 border-t border-[#E6E6E6] dark:border-[#262626]">
|
||||
{visibleContextMenu && (
|
||||
@@ -267,16 +335,19 @@ const ContextMenu: FC<ContextMenuProps> = () => {
|
||||
<Input
|
||||
ref={searchInputRef}
|
||||
autoFocus
|
||||
autoCorrect="off"
|
||||
placeholder={t("search.contextMenu.search")}
|
||||
className="w-full bg-transparent"
|
||||
onChange={(event) => {
|
||||
const value = specialCharacterFiltering(event.target.value);
|
||||
const value = event.target.value.trim();
|
||||
|
||||
const searchMenus = menus.filter((item) => {
|
||||
return lowerCase(item.name).includes(lowerCase(value));
|
||||
});
|
||||
const nextMenus = menus
|
||||
.filter((item) => !item.hide)
|
||||
.filter((item) => {
|
||||
return lowerCase(item.name).includes(lowerCase(value));
|
||||
});
|
||||
|
||||
setSearchMenus(searchMenus);
|
||||
setSearchMenus(nextMenus);
|
||||
}}
|
||||
/>
|
||||
</VisibleKey>
|
||||
|
||||
@@ -4,13 +4,13 @@ import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useSearchStore } from "@/stores/searchStore";
|
||||
import { SearchHeader } from "./SearchHeader";
|
||||
import noDataImg from "@/assets/coconut-tree.png";
|
||||
import { metaOrCtrlKey } from "@/utils/keyboardUtils";
|
||||
import SearchListItem from "./SearchListItem";
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
import { Get } from "@/api/axiosRequest";
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
import { useConnectStore } from "@/stores/connectStore";
|
||||
import SearchEmpty from "../Common/SearchEmpty";
|
||||
|
||||
interface DocumentListProps {
|
||||
onSelectDocument: (id: string) => void;
|
||||
@@ -255,16 +255,9 @@ export const DocumentList: React.FC<DocumentListProps> = ({
|
||||
{!loading && (!data?.list || data.list.length === 0) && (
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className="h-full w-full flex flex-col items-center"
|
||||
className="h-full w-full flex flex-col justify-center items-center"
|
||||
>
|
||||
<img
|
||||
src={noDataImg}
|
||||
alt={t("search.list.noDataAlt")}
|
||||
className="w-16 h-16 mt-24"
|
||||
/>
|
||||
<div className="mt-4 text-sm text-[#999] dark:text-[#666]">
|
||||
{t("search.list.noResults")}
|
||||
</div>
|
||||
<SearchEmpty />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
193
src/components/Search/ExtensionDetail.tsx
Normal file
193
src/components/Search/ExtensionDetail.tsx
Normal file
@@ -0,0 +1,193 @@
|
||||
import { Button } from "@headlessui/react";
|
||||
import dayjs from "dayjs";
|
||||
import {
|
||||
CircleCheck,
|
||||
Download,
|
||||
FolderDown,
|
||||
GitFork,
|
||||
Loader,
|
||||
Trash2,
|
||||
User,
|
||||
} from "lucide-react";
|
||||
import { FC, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useSearchStore } from "@/stores/searchStore";
|
||||
import DeleteDialog from "../Common/DeleteDialog";
|
||||
import PreviewImage from "../Common/PreviewImage";
|
||||
|
||||
interface ExtensionDetailProps {
|
||||
onInstall: () => void;
|
||||
onUninstall: () => void;
|
||||
}
|
||||
|
||||
const ExtensionDetail: FC<ExtensionDetailProps> = (props) => {
|
||||
const { onInstall, onUninstall } = props;
|
||||
const { selectedExtension, installingExtensions } = useSearchStore();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleCancel = () => {
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
onUninstall();
|
||||
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const renderDivider = () => {
|
||||
return <div className="my-4 h-px bg-[#E6E6E6] dark:bg-[#262626]"></div>;
|
||||
};
|
||||
|
||||
return (
|
||||
selectedExtension && (
|
||||
<>
|
||||
<div className="text-sm text-[#333] dark:text-white">
|
||||
<div className="flex justify-between">
|
||||
<div className="flex gap-4">
|
||||
<img src={selectedExtension.icon} className="size-[56px]" />
|
||||
<div className="flex flex-col justify-around">
|
||||
<span>{selectedExtension.name}</span>
|
||||
<div className="flex items-center gap-6 text-[#999]">
|
||||
<div className="flex items-center gap-1">
|
||||
<User className="size-4" />
|
||||
<span>{selectedExtension.developer.name}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<GitFork className="size-4" />
|
||||
<span>v{selectedExtension.version.number}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<FolderDown className="size-4" />
|
||||
<span>{selectedExtension.stats.installs}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt-2">
|
||||
{selectedExtension.installed ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<Trash2
|
||||
className="size-4 text-red-500 cursor-pointer"
|
||||
onClick={() => {
|
||||
setIsOpen(true);
|
||||
}}
|
||||
/>
|
||||
<div className="flex items-center gap-1 h-6 px-2 rounded-full text-[#22C461] bg-[#22C461]/20">
|
||||
<CircleCheck className="size-4" />
|
||||
<span>{t("extensionDetail.hints.installed")}</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
className="flex justify-center items-center w-14 h-6 rounded-full bg-[#007BFF] text-white"
|
||||
onClick={() => {
|
||||
onInstall();
|
||||
}}
|
||||
>
|
||||
{installingExtensions.includes(selectedExtension.id) ? (
|
||||
<Loader className="size-4 animate-spin" />
|
||||
) : (
|
||||
<Download className="size-4" />
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{(selectedExtension.screenshots?.length ?? 0) > 0 && (
|
||||
<PreviewImage
|
||||
urls={selectedExtension.screenshots.map((item) => item.url)}
|
||||
classNames={{
|
||||
container: "pt-4",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{(selectedExtension.screenshots?.length ?? 0) > 0 && renderDivider()}
|
||||
|
||||
<div className="mb-2 text-[#999]">
|
||||
{t("extensionDetail.label.description")}
|
||||
</div>
|
||||
<p>{selectedExtension.description}</p>
|
||||
|
||||
{renderDivider()}
|
||||
|
||||
{(selectedExtension.commands?.length ?? 0) > 0 && (
|
||||
<>
|
||||
<div className="mb-2 text-[#999]">
|
||||
{t("extensionDetail.label.commands")}
|
||||
</div>
|
||||
|
||||
{selectedExtension.commands?.map((item) => {
|
||||
return (
|
||||
<div key={item.name}>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<img src={item.icon} className="size-5" />
|
||||
<span>{item.name}</span>
|
||||
</div>
|
||||
<div className="mb-4 text-[#999]">{item.description}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
|
||||
{(selectedExtension.commands?.length ?? 0) > 0 && renderDivider()}
|
||||
|
||||
{(selectedExtension.tags?.length ?? 0) > 0 && (
|
||||
<>
|
||||
<div className="mb-2 text-[#999]">
|
||||
{t("extensionDetail.label.tags")}
|
||||
</div>
|
||||
<div className="flex gap-2 mb-4">
|
||||
{selectedExtension.tags?.map((item) => {
|
||||
return (
|
||||
<div
|
||||
key={item}
|
||||
className="flex items-center h-6 px-2 rounded text-[#333] bg-[#E6E6E6] dark:text-white dark:bg-[#333]"
|
||||
>
|
||||
{item}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{(selectedExtension.tags?.length ?? 0) > 0 && renderDivider()}
|
||||
|
||||
<div className="mb-2 text-[#999]">
|
||||
{t("extensionDetail.label.lastUpdate")}
|
||||
</div>
|
||||
<p>
|
||||
{dayjs(selectedExtension.updated).format("YYYY-MM-DD HH:mm:ss")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<DeleteDialog
|
||||
reverseButtonPosition
|
||||
isOpen={isOpen}
|
||||
title={`${t("extensionDetail.deleteDialog.title")} ${selectedExtension.name
|
||||
}`}
|
||||
description={t("extensionDetail.deleteDialog.description")}
|
||||
cancelButtonProps={{
|
||||
className:
|
||||
"text-white bg-[#007BFF] border-[#007BFF] dark:bg-[#007BFF] dark:border-[#007BFF]",
|
||||
}}
|
||||
deleteButtonProps={{
|
||||
className:
|
||||
"!text-[#FF4949] bg-[#F8F9FA] dark:text-white dark:bg-[#202126] border-[#E6E6E6] dark:border-white/10",
|
||||
}}
|
||||
setIsOpen={setIsOpen}
|
||||
onCancel={handleCancel}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export default ExtensionDetail;
|
||||
333
src/components/Search/ExtensionStore.tsx
Normal file
333
src/components/Search/ExtensionStore.tsx
Normal file
@@ -0,0 +1,333 @@
|
||||
import { useAsyncEffect, useDebounce, useKeyPress, useUnmount } from "ahooks";
|
||||
import { useEffect, useState } from "react";
|
||||
import { CircleCheck, FolderDown, Loader } from "lucide-react";
|
||||
import clsx from "clsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useSearchStore } from "@/stores/searchStore";
|
||||
import { parseSearchQuery } from "@/utils";
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
import SearchEmpty from "../Common/SearchEmpty";
|
||||
import ExtensionDetail from "./ExtensionDetail";
|
||||
import { useShortcutsStore } from "@/stores/shortcutsStore";
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
import { platform } from "@/utils/platform";
|
||||
|
||||
export interface SearchExtensionItem {
|
||||
id: string;
|
||||
created: string;
|
||||
updated: string;
|
||||
name: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
type: string;
|
||||
category: string;
|
||||
tags?: string[];
|
||||
platforms: string[];
|
||||
developer: {
|
||||
id: string;
|
||||
name: string;
|
||||
avatar: string;
|
||||
twitter_handle?: string;
|
||||
github_handle?: string;
|
||||
location?: string;
|
||||
website?: string;
|
||||
bio?: string;
|
||||
};
|
||||
contributors: {
|
||||
id: string;
|
||||
name: string;
|
||||
avatar: string;
|
||||
}[];
|
||||
url: {
|
||||
code: string;
|
||||
download: string;
|
||||
};
|
||||
version: {
|
||||
number: string;
|
||||
};
|
||||
screenshots: {
|
||||
title?: string;
|
||||
url: string;
|
||||
}[];
|
||||
action: {
|
||||
exec: string;
|
||||
args: string[];
|
||||
};
|
||||
enabled: boolean;
|
||||
stats: {
|
||||
installs: number;
|
||||
views: number;
|
||||
};
|
||||
checksum: string;
|
||||
installed: boolean;
|
||||
commands?: Array<{
|
||||
type: string;
|
||||
name: string;
|
||||
icon: string;
|
||||
description: string;
|
||||
action: {
|
||||
exec: string;
|
||||
args: string[];
|
||||
};
|
||||
}>;
|
||||
}
|
||||
|
||||
const ExtensionStore = () => {
|
||||
const {
|
||||
searchValue,
|
||||
selectedExtension,
|
||||
setSelectedExtension,
|
||||
installingExtensions,
|
||||
setInstallingExtensions,
|
||||
uninstallingExtensions,
|
||||
setUninstallingExtensions,
|
||||
visibleExtensionDetail,
|
||||
setVisibleExtensionDetail,
|
||||
visibleContextMenu,
|
||||
setVisibleContextMenu,
|
||||
} = useSearchStore();
|
||||
const debouncedSearchValue = useDebounce(searchValue);
|
||||
const [list, setList] = useState<SearchExtensionItem[]>([]);
|
||||
const { modifierKey } = useShortcutsStore();
|
||||
const { addError } = useAppStore();
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
const unlisten1 = platformAdapter.listenEvent("install-extension", () => {
|
||||
handleInstall();
|
||||
});
|
||||
|
||||
const unlisten2 = platformAdapter.listenEvent("uninstall-extension", () => {
|
||||
handleUnInstall();
|
||||
});
|
||||
|
||||
return () => {
|
||||
unlisten1.then((fn) => fn());
|
||||
unlisten2.then((fn) => fn());
|
||||
};
|
||||
}, [selectedExtension]);
|
||||
|
||||
useAsyncEffect(async () => {
|
||||
const result = await platformAdapter.invokeBackend<SearchExtensionItem[]>(
|
||||
"search_extension",
|
||||
{
|
||||
queryParams: parseSearchQuery({
|
||||
query: debouncedSearchValue.trim(),
|
||||
filters: {
|
||||
platforms: [platform()],
|
||||
},
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
console.log("search_extension", result);
|
||||
|
||||
setList(result ?? []);
|
||||
|
||||
setSelectedExtension(result?.[0]);
|
||||
}, [debouncedSearchValue]);
|
||||
|
||||
useUnmount(() => {
|
||||
setSelectedExtension(void 0);
|
||||
});
|
||||
|
||||
useKeyPress(
|
||||
"enter",
|
||||
() => {
|
||||
if (visibleContextMenu) return;
|
||||
|
||||
if (visibleExtensionDetail) {
|
||||
return handleInstall();
|
||||
}
|
||||
|
||||
setVisibleExtensionDetail(true);
|
||||
},
|
||||
{ exactMatch: true }
|
||||
);
|
||||
|
||||
useKeyPress(
|
||||
`${modifierKey}.enter`,
|
||||
() => {
|
||||
if (
|
||||
visibleContextMenu ||
|
||||
visibleExtensionDetail ||
|
||||
selectedExtension?.installed
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
handleInstall();
|
||||
},
|
||||
{ exactMatch: true }
|
||||
);
|
||||
|
||||
useKeyPress(["uparrow", "downarrow"], (_, key) => {
|
||||
if (visibleContextMenu || visibleExtensionDetail) return;
|
||||
|
||||
const index = list.findIndex((item) => item.id === selectedExtension?.id);
|
||||
const length = list.length;
|
||||
|
||||
if (length <= 1) return;
|
||||
|
||||
let nextIndex = index;
|
||||
|
||||
if (key === "uparrow") {
|
||||
nextIndex = nextIndex > 0 ? nextIndex - 1 : length - 1;
|
||||
} else {
|
||||
nextIndex = nextIndex < length - 1 ? nextIndex + 1 : 0;
|
||||
}
|
||||
|
||||
setSelectedExtension(list[nextIndex]);
|
||||
});
|
||||
|
||||
const toggleInstall = (installed = true) => {
|
||||
if (!selectedExtension) return;
|
||||
|
||||
const { id } = selectedExtension;
|
||||
|
||||
setList((prev) => {
|
||||
return prev.map((item) => {
|
||||
if (item.id === id) {
|
||||
return { ...item, installed };
|
||||
}
|
||||
|
||||
return item;
|
||||
});
|
||||
});
|
||||
|
||||
if (selectedExtension?.id === id) {
|
||||
setSelectedExtension({
|
||||
...selectedExtension,
|
||||
installed,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleInstall = async () => {
|
||||
if (!selectedExtension) return;
|
||||
|
||||
const { id, name, installed } = selectedExtension;
|
||||
|
||||
try {
|
||||
if (installed || installingExtensions.includes(id)) return;
|
||||
|
||||
setInstallingExtensions(installingExtensions.concat(id));
|
||||
|
||||
await platformAdapter.invokeBackend("install_extension", { id });
|
||||
|
||||
toggleInstall();
|
||||
|
||||
addError(
|
||||
`${name} ${t("extensionStore.hints.installationCompleted")}`,
|
||||
"info"
|
||||
);
|
||||
} catch (error) {
|
||||
addError(String(error), "error");
|
||||
} finally {
|
||||
setInstallingExtensions(
|
||||
installingExtensions.filter((item) => item !== id)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUnInstall = async () => {
|
||||
if (!selectedExtension) return;
|
||||
|
||||
const { id, name, installed, developer } = selectedExtension;
|
||||
|
||||
try {
|
||||
if (!installed || uninstallingExtensions.includes(id)) return;
|
||||
|
||||
setUninstallingExtensions(uninstallingExtensions.concat(id));
|
||||
|
||||
await platformAdapter.invokeBackend("uninstall_extension", {
|
||||
developer: developer.id,
|
||||
extensionId: id,
|
||||
});
|
||||
|
||||
toggleInstall(false);
|
||||
|
||||
addError(
|
||||
`${name} ${t("extensionStore.hints.uninstallationCompleted")}`,
|
||||
"info"
|
||||
);
|
||||
} catch (error) {
|
||||
addError(String(error), "error");
|
||||
} finally {
|
||||
setUninstallingExtensions(
|
||||
uninstallingExtensions.filter((item) => item !== id)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full text-sm p-4 overflow-auto custom-scrollbar">
|
||||
{visibleExtensionDetail ? (
|
||||
<ExtensionDetail
|
||||
onInstall={handleInstall}
|
||||
onUninstall={handleUnInstall}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{list.length > 0 ? (
|
||||
list.map((item) => {
|
||||
const { id, icon, name, description, stats, installed } = item;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={id}
|
||||
className={clsx(
|
||||
"flex justify-between gap-4 h-[40px] px-2 rounded-lg cursor-pointer text-[#333] dark:text-[#d8d8d8] transition",
|
||||
{
|
||||
"bg-black/10 dark:bg-white/15":
|
||||
selectedExtension?.id === id,
|
||||
}
|
||||
)}
|
||||
onMouseOver={() => {
|
||||
setSelectedExtension(item);
|
||||
}}
|
||||
onClick={() => {
|
||||
setVisibleExtensionDetail(true);
|
||||
}}
|
||||
onContextMenu={(event) => {
|
||||
event.preventDefault();
|
||||
|
||||
setVisibleContextMenu(true);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2 overflow-hidden">
|
||||
<img src={icon} className="size-[20px]" />
|
||||
<span className="whitespace-nowrap">{name}</span>
|
||||
<span className="truncate text-[#999]">{description}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
{installed && (
|
||||
<CircleCheck className="size-4 text-green-500" />
|
||||
)}
|
||||
|
||||
{installingExtensions.includes(item.id) && (
|
||||
<Loader className="size-4 text-blue-500 animate-spin" />
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-1 text-[#999]">
|
||||
<FolderDown className="size-4" />
|
||||
<span>{stats.installs}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className="flex justify-center items-center h-full">
|
||||
<SearchEmpty />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExtensionStore;
|
||||
@@ -97,6 +97,8 @@ export default function ChatInput({
|
||||
const textareaRef = useRef<{ reset: () => void; focus: () => void }>(null);
|
||||
|
||||
const { curChatEnd, connected } = useChatStore();
|
||||
const { setSearchValue, visibleExtensionStore, selectedExtension } =
|
||||
useSearchStore();
|
||||
|
||||
useEffect(() => {
|
||||
const handleFocus = () => {
|
||||
@@ -135,6 +137,7 @@ export default function ChatInput({
|
||||
const handleInputChange = useCallback(
|
||||
(value: string) => {
|
||||
changeInput(value);
|
||||
setSearchValue(value);
|
||||
if (!isChatMode) {
|
||||
onSend(value);
|
||||
}
|
||||
@@ -215,15 +218,16 @@ export default function ChatInput({
|
||||
disabledChange={disabledChange}
|
||||
/>
|
||||
|
||||
{!isChatMode && sourceData && (
|
||||
<div
|
||||
className={`absolute ${
|
||||
lineCount === 1 ? "-top-[5px]" : "top-[calc(100%-25px)]"
|
||||
} left-2`}
|
||||
>
|
||||
<VisibleKey shortcut="←" />
|
||||
</div>
|
||||
)}
|
||||
{!isChatMode &&
|
||||
(sourceData || visibleExtensionStore || selectedExtension) && (
|
||||
<div
|
||||
className={`absolute ${
|
||||
lineCount === 1 ? "-top-[5px]" : "top-[calc(100%-25px)]"
|
||||
} left-2`}
|
||||
>
|
||||
<VisibleKey shortcut="←" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/*
|
||||
<div
|
||||
@@ -244,7 +248,8 @@ export default function ChatInput({
|
||||
isTauri &&
|
||||
!goAskAi &&
|
||||
askAI &&
|
||||
!disabledExtensions.includes("QuickAIAccess") && (
|
||||
!disabledExtensions.includes("QuickAIAccess") &&
|
||||
!visibleExtensionStore && (
|
||||
<div className="flex items-center gap-2 text-sm text-[#AEAEAE] dark:text-[#545454] whitespace-nowrap">
|
||||
<span>
|
||||
{t("search.askCocoAi.title", {
|
||||
|
||||
@@ -16,6 +16,7 @@ import { useShortcutsStore } from "@/stores/shortcutsStore";
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
import { useSearchStore } from "@/stores/searchStore";
|
||||
import { useExtensionsStore } from "@/stores/extensionsStore";
|
||||
import { parseSearchQuery, SearchQuery, unrequitable } from "@/utils";
|
||||
// import InputExtra from "./InputExtra";
|
||||
// import AiSummaryIcon from "@/components/Common/Icons/AiSummaryIcon";
|
||||
|
||||
@@ -70,44 +71,32 @@ const InputControls = ({
|
||||
const getDataSourcesByServer = useCallback(
|
||||
async (
|
||||
serverId: string,
|
||||
options?: {
|
||||
from?: number;
|
||||
size?: number;
|
||||
query?: string;
|
||||
}
|
||||
searchQuery: SearchQuery = {}
|
||||
): Promise<DataSource[]> => {
|
||||
searchQuery.from ??= 0;
|
||||
searchQuery.size ??= 1000;
|
||||
|
||||
const body: Record<string, any> = {
|
||||
id: serverId,
|
||||
from: options?.from || 0,
|
||||
size: options?.size || 1000,
|
||||
};
|
||||
|
||||
body.query = {
|
||||
bool: {
|
||||
must: [{ term: { enabled: true } }],
|
||||
},
|
||||
};
|
||||
|
||||
if (options?.query) {
|
||||
body.query.bool.must.push({
|
||||
query_string: {
|
||||
fields: ["combined_fulltext"],
|
||||
query: options?.query,
|
||||
fuzziness: "AUTO",
|
||||
fuzzy_prefix_length: 2,
|
||||
fuzzy_max_expansions: 10,
|
||||
fuzzy_transpositions: true,
|
||||
allow_leading_wildcard: false,
|
||||
queryParams: parseSearchQuery({
|
||||
...searchQuery,
|
||||
fuzziness: 5,
|
||||
filters: {
|
||||
enabled: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
}),
|
||||
};
|
||||
|
||||
let response: any;
|
||||
if (isTauri) {
|
||||
response = await platformAdapter.invokeBackend("datasource_search", {
|
||||
id: serverId,
|
||||
options: body,
|
||||
});
|
||||
if (unrequitable()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
response = await platformAdapter.invokeBackend(
|
||||
"datasource_search",
|
||||
body
|
||||
);
|
||||
} else {
|
||||
body.id = undefined;
|
||||
const [error, res]: any = await Post("/datasource/_search", body);
|
||||
@@ -135,39 +124,28 @@ const InputControls = ({
|
||||
const getMCPByServer = useCallback(
|
||||
async (
|
||||
serverId: string,
|
||||
options?: {
|
||||
from?: number;
|
||||
size?: number;
|
||||
query?: string;
|
||||
}
|
||||
searchQuery: SearchQuery = {}
|
||||
): Promise<DataSource[]> => {
|
||||
searchQuery.from ??= 0;
|
||||
searchQuery.size ??= 1000;
|
||||
|
||||
const body: Record<string, any> = {
|
||||
id: serverId,
|
||||
from: options?.from || 0,
|
||||
size: options?.size || 1000,
|
||||
};
|
||||
body.query = {
|
||||
bool: {
|
||||
must: [{ term: { enabled: true } }],
|
||||
},
|
||||
};
|
||||
|
||||
if (options?.query) {
|
||||
body.query.bool.must.push({
|
||||
query_string: {
|
||||
fields: ["combined_fulltext"],
|
||||
query: options?.query,
|
||||
fuzziness: "AUTO",
|
||||
fuzzy_prefix_length: 2,
|
||||
fuzzy_max_expansions: 10,
|
||||
fuzzy_transpositions: true,
|
||||
allow_leading_wildcard: false,
|
||||
queryParams: parseSearchQuery({
|
||||
...searchQuery,
|
||||
fuzziness: 5,
|
||||
filters: {
|
||||
enabled: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
}),
|
||||
};
|
||||
|
||||
let response: any;
|
||||
if (isTauri) {
|
||||
if (unrequitable()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
response = await platformAdapter.invokeBackend(
|
||||
"mcp_server_search",
|
||||
body
|
||||
@@ -212,6 +190,7 @@ const InputControls = ({
|
||||
return state.aiOverviewAssistant;
|
||||
});
|
||||
const aiOverviewShortcut = useShortcutsStore((state) => state.aiOverview);
|
||||
const { visibleExtensionStore } = useSearchStore();
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -249,16 +228,18 @@ const InputControls = ({
|
||||
onKeyPress={setIsDeepThinkActive}
|
||||
>
|
||||
<Brain
|
||||
className={`size-3 ${isDeepThinkActive
|
||||
? "text-[#0072FF] dark:text-[#0072FF]"
|
||||
: "text-[#333] dark:text-white"
|
||||
}`}
|
||||
className={`size-3 ${
|
||||
isDeepThinkActive
|
||||
? "text-[#0072FF] dark:text-[#0072FF]"
|
||||
: "text-[#333] dark:text-white"
|
||||
}`}
|
||||
/>
|
||||
</VisibleKey>
|
||||
{isDeepThinkActive && (
|
||||
<span
|
||||
className={`${isDeepThinkActive ? "text-[#0072FF]" : "dark:text-white"
|
||||
}`}
|
||||
className={`${
|
||||
isDeepThinkActive ? "text-[#0072FF]" : "dark:text-white"
|
||||
}`}
|
||||
>
|
||||
{t("search.input.deepThink")}
|
||||
</span>
|
||||
@@ -281,8 +262,8 @@ const InputControls = ({
|
||||
/>
|
||||
|
||||
{!(source?.datasource?.enabled && source?.datasource?.visible) &&
|
||||
(source?.type !== "deep_think" || !source?.config?.visible) &&
|
||||
!(source?.mcp_servers?.enabled && source?.mcp_servers?.visible) ? (
|
||||
(source?.type !== "deep_think" || !source?.config?.visible) &&
|
||||
!(source?.mcp_servers?.enabled && source?.mcp_servers?.visible) ? (
|
||||
<div className="px-[9px]">
|
||||
<Copyright />
|
||||
</div>
|
||||
@@ -293,7 +274,8 @@ const InputControls = ({
|
||||
{!disabledExtensions.includes("AIOverview") &&
|
||||
isTauri &&
|
||||
aiOverviewServer &&
|
||||
aiOverviewAssistant && (
|
||||
aiOverviewAssistant &&
|
||||
!visibleExtensionStore && (
|
||||
<div
|
||||
className={clsx(
|
||||
"inline-flex items-center gap-1 px-2 py-1 rounded-full hover:!text-[#881c94] cursor-pointer transition",
|
||||
|
||||
@@ -16,7 +16,7 @@ import { useChatStore } from "@/stores/chatStore";
|
||||
import NoDataImage from "@/components/Common/NoDataImage";
|
||||
import PopoverInput from "@/components/Common/PopoverInput";
|
||||
import Pagination from "@/components/Common/Pagination";
|
||||
import { specialCharacterFiltering } from "@/utils"
|
||||
import { SearchQuery } from "@/utils";
|
||||
|
||||
interface MCPPopoverProps {
|
||||
mcp_servers: any;
|
||||
@@ -24,11 +24,7 @@ interface MCPPopoverProps {
|
||||
setIsMCPActive: () => void;
|
||||
getMCPByServer: (
|
||||
serverId: string,
|
||||
options?: {
|
||||
from?: number;
|
||||
size?: number;
|
||||
query?: string;
|
||||
}
|
||||
searchQuery?: SearchQuery
|
||||
) => Promise<DataSource[]>;
|
||||
}
|
||||
|
||||
@@ -69,12 +65,12 @@ export default function MCPPopover({
|
||||
}
|
||||
const data = res?.length
|
||||
? [
|
||||
{
|
||||
id: "all",
|
||||
name: "search.input.searchPopover.allScope",
|
||||
},
|
||||
...res,
|
||||
]
|
||||
{
|
||||
id: "all",
|
||||
name: "search.input.searchPopover.allScope",
|
||||
},
|
||||
...res,
|
||||
]
|
||||
: [];
|
||||
|
||||
setDataList(data);
|
||||
@@ -166,7 +162,7 @@ export default function MCPPopover({
|
||||
};
|
||||
|
||||
if (!(mcp_servers?.enabled && mcp_servers?.visible)) {
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -181,10 +177,11 @@ export default function MCPPopover({
|
||||
>
|
||||
<VisibleKey shortcut={mcpSearch} onKeyPress={setIsMCPActive}>
|
||||
<Hammer
|
||||
className={`size-3 ${isMCPActive
|
||||
? "text-[#0072FF] dark:text-[#0072FF]"
|
||||
: "text-[#333] dark:text-white"
|
||||
}`}
|
||||
className={`size-3 ${
|
||||
isMCPActive
|
||||
? "text-[#0072FF] dark:text-[#0072FF]"
|
||||
: "text-[#333] dark:text-white"
|
||||
}`}
|
||||
/>
|
||||
</VisibleKey>
|
||||
|
||||
@@ -231,8 +228,9 @@ export default function MCPPopover({
|
||||
>
|
||||
<VisibleKey shortcut="R" onKeyPress={handleRefresh}>
|
||||
<RefreshCw
|
||||
className={`size-3 text-[#0287FF] transition-transform duration-1000 ${isRefreshDataSource ? "animate-spin" : ""
|
||||
}`}
|
||||
className={`size-3 text-[#0287FF] transition-transform duration-1000 ${
|
||||
isRefreshDataSource ? "animate-spin" : ""
|
||||
}`}
|
||||
/>
|
||||
</VisibleKey>
|
||||
</div>
|
||||
@@ -255,8 +253,7 @@ export default function MCPPopover({
|
||||
ref={searchInputRef}
|
||||
className="size-full px-2 rounded-lg border dark:border-white/10 bg-transparent"
|
||||
onChange={(e) => {
|
||||
const value = specialCharacterFiltering(e.target.value.trim())
|
||||
setKeyword(value);
|
||||
setKeyword(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -8,12 +8,22 @@ import { NoResults } from "@/components/Common/UI/NoResults";
|
||||
import Footer from "@/components/Common/UI/Footer";
|
||||
import AskAi from "./AskAi";
|
||||
import { useSearch } from "@/hooks/useSearch";
|
||||
import ExtensionStore from "./ExtensionStore";
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
|
||||
const SearchResultsPanel = memo<{
|
||||
input: string;
|
||||
isChatMode: boolean;
|
||||
}>(({ input, isChatMode }) => {
|
||||
const { sourceData, goAskAi } = useSearchStore();
|
||||
changeInput: (val: string) => void;
|
||||
changeMode?: (isChatMode: boolean) => void;
|
||||
}>(({ input, isChatMode, changeInput, changeMode }) => {
|
||||
const {
|
||||
sourceData,
|
||||
goAskAi,
|
||||
visibleExtensionDetail,
|
||||
setSearchValue,
|
||||
setVisibleExtensionStore,
|
||||
} = useSearchStore();
|
||||
|
||||
const searchState = useSearch();
|
||||
const {
|
||||
@@ -33,7 +43,8 @@ const SearchResultsPanel = memo<{
|
||||
}
|
||||
}, [input, isChatMode, performSearch, sourceData]);
|
||||
|
||||
const { setSelectedAssistant, selectedSearchContent } = useSearchStore();
|
||||
const { setSelectedAssistant, selectedSearchContent, visibleExtensionStore } =
|
||||
useSearchStore();
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedSearchContent?.type === "AI Assistant") {
|
||||
@@ -46,6 +57,26 @@ const SearchResultsPanel = memo<{
|
||||
}
|
||||
}, [selectedSearchContent]);
|
||||
|
||||
useEffect(() => {
|
||||
const unlisten = platformAdapter.listenEvent("open-extension-store", () => {
|
||||
platformAdapter.showWindow();
|
||||
changeMode && changeMode(false);
|
||||
|
||||
if (visibleExtensionStore || visibleExtensionDetail) return;
|
||||
|
||||
changeInput("");
|
||||
setSearchValue("");
|
||||
setVisibleExtensionStore(true);
|
||||
});
|
||||
|
||||
return () => {
|
||||
unlisten.then((fn) => {
|
||||
fn();
|
||||
});
|
||||
};
|
||||
}, [visibleExtensionStore, visibleExtensionDetail]);
|
||||
|
||||
if (visibleExtensionStore) return <ExtensionStore />;
|
||||
if (goAskAi) return <AskAi />;
|
||||
if (suggests.length === 0) return <NoResults />;
|
||||
|
||||
@@ -68,14 +99,26 @@ interface SearchProps {
|
||||
isChatMode: boolean;
|
||||
input: string;
|
||||
setIsPinned?: (value: boolean) => void;
|
||||
changeMode?: (isChatMode: boolean) => void;
|
||||
}
|
||||
|
||||
function Search({ isChatMode, input, setIsPinned }: SearchProps) {
|
||||
function Search({
|
||||
changeInput,
|
||||
isChatMode,
|
||||
input,
|
||||
setIsPinned,
|
||||
changeMode,
|
||||
}: SearchProps) {
|
||||
const mainWindowRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
return (
|
||||
<div ref={mainWindowRef} className={`h-full pb-8 w-full relative`}>
|
||||
<SearchResultsPanel input={input} isChatMode={isChatMode} />
|
||||
<SearchResultsPanel
|
||||
input={input}
|
||||
isChatMode={isChatMode}
|
||||
changeInput={changeInput}
|
||||
changeMode={changeMode}
|
||||
/>
|
||||
|
||||
<Footer setIsPinnedWeb={setIsPinned} />
|
||||
|
||||
|
||||
@@ -14,7 +14,16 @@ export default function SearchIcons({
|
||||
isChatMode,
|
||||
assistant,
|
||||
}: SearchIconsProps) {
|
||||
const { sourceData, setSourceData, goAskAi, setGoAskAi } = useSearchStore();
|
||||
const {
|
||||
sourceData,
|
||||
setSourceData,
|
||||
goAskAi,
|
||||
setGoAskAi,
|
||||
visibleExtensionStore,
|
||||
setVisibleExtensionStore,
|
||||
visibleExtensionDetail,
|
||||
setVisibleExtensionDetail,
|
||||
} = useSearchStore();
|
||||
|
||||
if (isChatMode) {
|
||||
return null;
|
||||
@@ -49,11 +58,21 @@ export default function SearchIcons({
|
||||
);
|
||||
}
|
||||
|
||||
if (sourceData) {
|
||||
if (sourceData || visibleExtensionStore || visibleExtensionDetail) {
|
||||
return (
|
||||
<ArrowBigLeft
|
||||
className="w-4 h-4 text-[#ccc] dark:text-[#d8d8d8] cursor-pointer"
|
||||
onClick={() => setSourceData(undefined)}
|
||||
onClick={() => {
|
||||
if (visibleExtensionDetail) {
|
||||
return setVisibleExtensionDetail(false);
|
||||
}
|
||||
|
||||
if (visibleExtensionStore) {
|
||||
return setVisibleExtensionStore(false);
|
||||
}
|
||||
|
||||
setSourceData(void 0);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ import { useChatStore } from "@/stores/chatStore";
|
||||
import NoDataImage from "@/components/Common/NoDataImage";
|
||||
import PopoverInput from "@/components/Common/PopoverInput";
|
||||
import Pagination from "@/components/Common/Pagination";
|
||||
import { specialCharacterFiltering } from "@/utils"
|
||||
|
||||
interface SearchPopoverProps {
|
||||
datasource: any;
|
||||
@@ -72,12 +71,12 @@ export default function SearchPopover({
|
||||
}
|
||||
const data = res?.length
|
||||
? [
|
||||
{
|
||||
id: "all",
|
||||
name: "search.input.searchPopover.allScope",
|
||||
},
|
||||
...res,
|
||||
]
|
||||
{
|
||||
id: "all",
|
||||
name: "search.input.searchPopover.allScope",
|
||||
},
|
||||
...res,
|
||||
]
|
||||
: [];
|
||||
|
||||
setDataSourceList(data);
|
||||
@@ -169,7 +168,7 @@ export default function SearchPopover({
|
||||
};
|
||||
|
||||
if (!(datasource?.enabled && datasource?.visible)) {
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -184,18 +183,20 @@ export default function SearchPopover({
|
||||
>
|
||||
<VisibleKey shortcut={internetSearch} onKeyPress={setIsSearchActive}>
|
||||
<Globe
|
||||
className={`size-3 ${isSearchActive
|
||||
? "text-[#0072FF] dark:text-[#0072FF]"
|
||||
: "text-[#333] dark:text-white"
|
||||
}`}
|
||||
className={`size-3 ${
|
||||
isSearchActive
|
||||
? "text-[#0072FF] dark:text-[#0072FF]"
|
||||
: "text-[#333] dark:text-white"
|
||||
}`}
|
||||
/>
|
||||
</VisibleKey>
|
||||
|
||||
{isSearchActive && (
|
||||
<>
|
||||
<span
|
||||
className={`${isSearchActive ? "text-[#0072FF]" : "dark:text-white"
|
||||
}`}
|
||||
className={`${
|
||||
isSearchActive ? "text-[#0072FF]" : "dark:text-white"
|
||||
}`}
|
||||
>
|
||||
{t("search.input.search")}
|
||||
</span>
|
||||
@@ -235,8 +236,9 @@ export default function SearchPopover({
|
||||
>
|
||||
<VisibleKey shortcut="R" onKeyPress={handleRefresh}>
|
||||
<RefreshCw
|
||||
className={`size-3 text-[#0287FF] transition-transform duration-1000 ${isRefreshDataSource ? "animate-spin" : ""
|
||||
}`}
|
||||
className={`size-3 text-[#0287FF] transition-transform duration-1000 ${
|
||||
isRefreshDataSource ? "animate-spin" : ""
|
||||
}`}
|
||||
/>
|
||||
</VisibleKey>
|
||||
</div>
|
||||
@@ -259,8 +261,7 @@ export default function SearchPopover({
|
||||
ref={searchInputRef}
|
||||
className="size-full px-2 rounded-lg border dark:border-white/10 bg-transparent"
|
||||
onChange={(e) => {
|
||||
const value = specialCharacterFiltering(e.target.value.trim())
|
||||
setKeyword(value);
|
||||
setKeyword(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -62,12 +62,15 @@ function SearchChat({
|
||||
|
||||
const source = currentAssistant?._source;
|
||||
|
||||
const customInitialState = useMemo(() => ({
|
||||
...initialAppState,
|
||||
isDeepThinkActive: source?.type === "deep_think",
|
||||
isSearchActive: source?.datasource?.enabled_by_default === true,
|
||||
isMCPActive: source?.mcp_servers?.enabled_by_default === true,
|
||||
}), [source]);
|
||||
const customInitialState = useMemo(
|
||||
() => ({
|
||||
...initialAppState,
|
||||
isDeepThinkActive: source?.type === "deep_think",
|
||||
isSearchActive: source?.datasource?.enabled_by_default === true,
|
||||
isMCPActive: source?.mcp_servers?.enabled_by_default === true,
|
||||
}),
|
||||
[source]
|
||||
);
|
||||
|
||||
const [state, dispatch] = useReducer(appReducer, customInitialState);
|
||||
const {
|
||||
@@ -80,9 +83,18 @@ function SearchChat({
|
||||
isTyping,
|
||||
} = state;
|
||||
useEffect(() => {
|
||||
dispatch({ type: "SET_SEARCH_ACTIVE", payload: customInitialState.isSearchActive });
|
||||
dispatch({ type: "SET_DEEP_THINK_ACTIVE", payload: customInitialState.isDeepThinkActive });
|
||||
dispatch({ type: "SET_MCP_ACTIVE", payload: customInitialState.isMCPActive });
|
||||
dispatch({
|
||||
type: "SET_SEARCH_ACTIVE",
|
||||
payload: customInitialState.isSearchActive,
|
||||
});
|
||||
dispatch({
|
||||
type: "SET_DEEP_THINK_ACTIVE",
|
||||
payload: customInitialState.isDeepThinkActive,
|
||||
});
|
||||
dispatch({
|
||||
type: "SET_MCP_ACTIVE",
|
||||
payload: customInitialState.isMCPActive,
|
||||
});
|
||||
}, [customInitialState]);
|
||||
|
||||
const [isWin10, setIsWin10] = useState(false);
|
||||
@@ -264,13 +276,11 @@ function SearchChat({
|
||||
)}
|
||||
style={{ opacity: blurred ? (opacity ?? 30) / 100 : 1 }}
|
||||
>
|
||||
|
||||
<div
|
||||
data-tauri-drag-region={isTauri}
|
||||
className={clsx(
|
||||
"flex-1 w-full overflow-auto",
|
||||
{ "hidden": !isTransitioned }
|
||||
)}
|
||||
className={clsx("flex-1 w-full overflow-auto", {
|
||||
hidden: !isTransitioned,
|
||||
})}
|
||||
>
|
||||
<Suspense fallback={<LoadingFallback />}>
|
||||
<ChatAI
|
||||
@@ -290,8 +300,9 @@ function SearchChat({
|
||||
|
||||
<div
|
||||
data-tauri-drag-region={isTauri}
|
||||
className={`p-2 w-full flex justify-center transition-all duration-500 min-h-[82px] ${isTransitioned ? "border-t" : "border-b"
|
||||
} border-[#E6E6E6] dark:border-[#272626]`}
|
||||
className={`p-2 w-full flex justify-center transition-all duration-500 min-h-[82px] ${
|
||||
isTransitioned ? "border-t" : "border-b"
|
||||
} border-[#E6E6E6] dark:border-[#272626]`}
|
||||
>
|
||||
<InputBox
|
||||
isChatMode={isChatMode}
|
||||
@@ -326,10 +337,9 @@ function SearchChat({
|
||||
|
||||
<div
|
||||
data-tauri-drag-region={isTauri}
|
||||
className={clsx(
|
||||
"flex-1 w-full overflow-auto",
|
||||
{ "hidden": isTransitioned }
|
||||
)}
|
||||
className={clsx("flex-1 w-full overflow-auto", {
|
||||
hidden: isTransitioned,
|
||||
})}
|
||||
>
|
||||
<Suspense fallback={<LoadingFallback />}>
|
||||
<Search
|
||||
@@ -338,6 +348,7 @@ function SearchChat({
|
||||
isChatMode={isChatMode}
|
||||
changeInput={setInput}
|
||||
setIsPinned={setIsPinned}
|
||||
changeMode={changeMode}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FC, MouseEvent, useContext } from "react";
|
||||
import { FC, MouseEvent, useContext, useMemo, useState } from "react";
|
||||
import { Extension, ExtensionId, ExtensionsContext } from "../..";
|
||||
import { useReactive } from "ahooks";
|
||||
import { ChevronRight, LoaderCircle } from "lucide-react";
|
||||
@@ -19,13 +19,15 @@ const Content = () => {
|
||||
return rootState.extensions.map((item) => {
|
||||
const { id } = item;
|
||||
|
||||
return <Item key={id} {...item} level={1} extensionId={id} />;
|
||||
return <Item key={id} {...item} level={1} />;
|
||||
});
|
||||
};
|
||||
|
||||
interface ItemProps extends Extension {
|
||||
level: number;
|
||||
extensionId: ExtensionId;
|
||||
parentId?: ExtensionId;
|
||||
parentDeveloper?: string;
|
||||
parentDisabled?: boolean;
|
||||
}
|
||||
|
||||
interface ItemState {
|
||||
@@ -39,28 +41,42 @@ const subExtensionCommand: Partial<Record<ExtensionId, string>> = {
|
||||
};
|
||||
|
||||
const Item: FC<ItemProps> = (props) => {
|
||||
const { id, icon, title, type, level, extensionId, platforms } = props;
|
||||
const {
|
||||
id,
|
||||
icon,
|
||||
name,
|
||||
type,
|
||||
level,
|
||||
platforms,
|
||||
developer,
|
||||
enabled,
|
||||
parentId,
|
||||
parentDeveloper,
|
||||
parentDisabled,
|
||||
} = props;
|
||||
const { rootState } = useContext(ExtensionsContext);
|
||||
const state = useReactive<ItemState>({
|
||||
loading: false,
|
||||
expanded: false,
|
||||
});
|
||||
const { t } = useTranslation();
|
||||
const disabledExtensions = useExtensionsStore((state) => {
|
||||
return state.disabledExtensions;
|
||||
});
|
||||
const setDisabledExtensions = useExtensionsStore((state) => {
|
||||
return state.setDisabledExtensions;
|
||||
});
|
||||
const { disabledExtensions, setDisabledExtensions } = useExtensionsStore();
|
||||
const [selfDisabled, setSelfDisabled] = useState(!enabled);
|
||||
|
||||
const bundleId = {
|
||||
developer: developer ?? parentDeveloper,
|
||||
extension_id: level === 1 ? id : parentId,
|
||||
sub_extension_id: level === 1 ? void 0 : id,
|
||||
};
|
||||
|
||||
const hasSubExtensions = () => {
|
||||
const { commands, scripts, quick_links } = props;
|
||||
const { commands, scripts, quicklinks } = props;
|
||||
|
||||
if (subExtensionCommand[id]) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isArray(commands) || isArray(scripts) || isArray(quick_links)) {
|
||||
if (isArray(commands) || isArray(scripts) || isArray(quicklinks)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -70,7 +86,7 @@ const Item: FC<ItemProps> = (props) => {
|
||||
const getSubExtensions = async () => {
|
||||
state.loading = true;
|
||||
|
||||
const { commands, scripts, quick_links } = props;
|
||||
const { commands, scripts, quicklinks } = props;
|
||||
|
||||
let subExtensions: Extension[] = [];
|
||||
|
||||
@@ -79,12 +95,12 @@ const Item: FC<ItemProps> = (props) => {
|
||||
if (command) {
|
||||
subExtensions = await platformAdapter.invokeBackend<Extension[]>(command);
|
||||
} else {
|
||||
subExtensions = [commands, scripts, quick_links].filter(isArray).flat();
|
||||
subExtensions = [commands, scripts, quicklinks].filter(isArray).flat();
|
||||
}
|
||||
|
||||
state.loading = false;
|
||||
|
||||
return sortBy(subExtensions, ["title"]);
|
||||
return sortBy(subExtensions, ["name"]);
|
||||
};
|
||||
|
||||
const handleExpand = async (event: MouseEvent) => {
|
||||
@@ -99,46 +115,60 @@ const Item: FC<ItemProps> = (props) => {
|
||||
}
|
||||
};
|
||||
|
||||
const editable = () => {
|
||||
const isDisabled = useMemo(() => {
|
||||
if (level === 1) {
|
||||
return selfDisabled;
|
||||
}
|
||||
|
||||
return parentDisabled || selfDisabled;
|
||||
}, [parentDisabled, selfDisabled]);
|
||||
|
||||
const editable = useMemo(() => {
|
||||
return (
|
||||
type !== "group" &&
|
||||
type !== "calculator" &&
|
||||
type !== "extension" &&
|
||||
type !== "ai_extension"
|
||||
);
|
||||
};
|
||||
}, [type]);
|
||||
|
||||
const renderAlias = () => {
|
||||
const { alias } = props;
|
||||
|
||||
const handleChange = (value: string) => {
|
||||
platformAdapter.invokeBackend("set_extension_alias", {
|
||||
extensionId,
|
||||
bundleId,
|
||||
alias: value,
|
||||
});
|
||||
};
|
||||
|
||||
if (editable()) {
|
||||
return (
|
||||
<div
|
||||
className="-translate-x-2"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<SettingsInput
|
||||
defaultValue={alias}
|
||||
placeholder={t("settings.extensions.hits.addAlias")}
|
||||
className="!w-[90%] !h-6 !border-transparent rounded-[4px]"
|
||||
onChange={(value) => {
|
||||
handleChange(String(value));
|
||||
return (
|
||||
<div
|
||||
className={clsx({
|
||||
"opacity-50 pointer-events-none": isDisabled,
|
||||
})}
|
||||
>
|
||||
{editable ? (
|
||||
<div
|
||||
className="-translate-x-2"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <>--</>;
|
||||
>
|
||||
<SettingsInput
|
||||
defaultValue={alias}
|
||||
placeholder={t("settings.extensions.hits.addAlias")}
|
||||
className="!w-[90%] !h-6 !border-transparent rounded-[4px]"
|
||||
onChange={(value) => {
|
||||
handleChange(String(value));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<>--</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderHotkey = () => {
|
||||
@@ -147,80 +177,100 @@ const Item: FC<ItemProps> = (props) => {
|
||||
const handleChange = (value: string) => {
|
||||
if (value) {
|
||||
platformAdapter.invokeBackend("register_extension_hotkey", {
|
||||
extensionId,
|
||||
bundleId,
|
||||
hotkey: value,
|
||||
});
|
||||
} else {
|
||||
platformAdapter.invokeBackend("unregister_extension_hotkey", {
|
||||
extensionId,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (editable()) {
|
||||
return (
|
||||
<div
|
||||
className="-translate-x-2"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<Shortcut
|
||||
value={hotkey}
|
||||
placeholder={t("settings.extensions.hits.recordHotkey")}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <>--</>;
|
||||
};
|
||||
|
||||
const renderSwitch = () => {
|
||||
const { enabled } = props;
|
||||
|
||||
const handleChange = (value: boolean) => {
|
||||
if (value) {
|
||||
setDisabledExtensions(
|
||||
disabledExtensions.filter((item) => item !== extensionId)
|
||||
);
|
||||
|
||||
platformAdapter.invokeBackend("enable_extension", {
|
||||
extensionId,
|
||||
});
|
||||
} else {
|
||||
setDisabledExtensions([...disabledExtensions, extensionId]);
|
||||
|
||||
platformAdapter.invokeBackend("disable_extension", {
|
||||
extensionId,
|
||||
bundleId,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex items-center justify-end"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
className={clsx({
|
||||
"opacity-50 pointer-events-none": isDisabled,
|
||||
})}
|
||||
>
|
||||
{editable ? (
|
||||
<div
|
||||
className="-translate-x-2"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<Shortcut
|
||||
value={hotkey}
|
||||
placeholder={t("settings.extensions.hits.recordHotkey")}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<>--</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderSwitch = () => {
|
||||
const handleChange = (value: boolean) => {
|
||||
if (value) {
|
||||
setDisabledExtensions(disabledExtensions.filter((item) => item !== id));
|
||||
|
||||
platformAdapter.invokeBackend("enable_extension", {
|
||||
bundleId,
|
||||
});
|
||||
} else {
|
||||
setDisabledExtensions([...disabledExtensions, id]);
|
||||
|
||||
platformAdapter.invokeBackend("disable_extension", {
|
||||
bundleId,
|
||||
});
|
||||
}
|
||||
|
||||
setSelfDisabled(!value);
|
||||
|
||||
if (level === 1) {
|
||||
const matched = rootState.extensions.find((item) => {
|
||||
return item.id === id;
|
||||
});
|
||||
|
||||
if (matched) {
|
||||
matched.enabled = value;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx("flex items-center justify-end", {
|
||||
"opacity-50 pointer-events-none": parentDisabled,
|
||||
})}
|
||||
>
|
||||
<SettingsToggle
|
||||
label={id}
|
||||
defaultChecked={enabled}
|
||||
className="scale-75"
|
||||
onChange={handleChange}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderType = () => {
|
||||
if (type === "ai_extension") {
|
||||
return "AI Extension";
|
||||
}
|
||||
|
||||
return startCase(type);
|
||||
return (
|
||||
<div
|
||||
className={clsx({
|
||||
"opacity-50 pointer-events-none": isDisabled,
|
||||
})}
|
||||
>
|
||||
{type === "ai_extension" ? "AI Extension" : startCase(type)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderContent = () => {
|
||||
@@ -267,7 +317,11 @@ const Item: FC<ItemProps> = (props) => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="size-4">
|
||||
<div
|
||||
className={clsx("size-4", {
|
||||
"opacity-50 pointer-events-none": isDisabled,
|
||||
})}
|
||||
>
|
||||
{icon.startsWith("font_") ? (
|
||||
<FontIcon name={icon} className="size-full" />
|
||||
) : (
|
||||
@@ -278,7 +332,13 @@ const Item: FC<ItemProps> = (props) => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="truncate">{title}</div>
|
||||
<div
|
||||
className={clsx("truncate", {
|
||||
"opacity-50 pointer-events-none": isDisabled,
|
||||
})}
|
||||
>
|
||||
{name}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-4/6 flex items-center text-[#999]">
|
||||
@@ -297,7 +357,9 @@ const Item: FC<ItemProps> = (props) => {
|
||||
key={item.id}
|
||||
{...item}
|
||||
level={level + 1}
|
||||
extensionId={`${id}.${item.id}`}
|
||||
parentId={id}
|
||||
parentDeveloper={developer}
|
||||
parentDisabled={!enabled}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -24,13 +24,13 @@ const App = () => {
|
||||
useAsyncEffect(async () => {
|
||||
if (!rootState.activeExtension) return;
|
||||
|
||||
const { id, title } = rootState.activeExtension;
|
||||
const { id, name } = rootState.activeExtension;
|
||||
|
||||
const appMetadata = await platformAdapter.invokeBackend<Metadata>(
|
||||
"get_app_metadata",
|
||||
{
|
||||
appPath: id,
|
||||
appName: title,
|
||||
appName: name,
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
import { Button } from "@headlessui/react";
|
||||
import { useMount } from "ahooks";
|
||||
import { castArray } from "lodash-es";
|
||||
@@ -7,6 +5,9 @@ import { Folder, SquareArrowOutUpRight, X } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
|
||||
const Applications = () => {
|
||||
const { t } = useTranslation();
|
||||
const addError = useAppStore((state) => state.addError);
|
||||
@@ -73,6 +74,10 @@ const Applications = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleReindex = () => {
|
||||
platformAdapter.invokeBackend("reindex_applications");
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="text-[#999]">
|
||||
@@ -119,6 +124,23 @@ const Applications = () => {
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
|
||||
<div className="text-[#999] mt-4">
|
||||
<p className="font-bold mb-2">
|
||||
{t("settings.extensions.application.details.rebuildIndex")}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
{t("settings.extensions.application.details.rebuildIndexDescription")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
className="w-full h-8 my-4 text-[#0087FF] border border-[#EEF0F3] hover:!border-[#0087FF] dark:border-gray-700 rounded-md transition"
|
||||
onClick={handleReindex}
|
||||
>
|
||||
{t("settings.extensions.application.details.reindex")}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { AssistantFetcher } from "@/components/Assistant/AssistantFetcher";
|
||||
import FontIcon from "@/components/Common/Icons/FontIcon";
|
||||
import SettingsSelectPro from "@/components/Settings/SettingsSelectPro";
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
@@ -25,6 +24,7 @@ const SharedAi: FC<SharedAiProps> = (props) => {
|
||||
const addError = useAppStore((state) => state.addError);
|
||||
const { fetchAssistant } = AssistantFetcher({});
|
||||
const { t } = useTranslation();
|
||||
const [assistantSearchValue, setAssistantSearchValue] = useState("");
|
||||
|
||||
useMount(async () => {
|
||||
try {
|
||||
@@ -34,8 +34,8 @@ const SharedAi: FC<SharedAiProps> = (props) => {
|
||||
|
||||
if (isArray(data)) {
|
||||
const enabledServers = data.filter(
|
||||
(s) => s.enabled && s.available && (s.public || s.profile)
|
||||
);
|
||||
(s) => s.enabled && s.available && (s.public || s.profile)
|
||||
);
|
||||
|
||||
setServerList(enabledServers);
|
||||
|
||||
@@ -64,6 +64,7 @@ const SharedAi: FC<SharedAiProps> = (props) => {
|
||||
current: 1,
|
||||
pageSize: 1000,
|
||||
serverId: server.id,
|
||||
query: assistantSearchValue,
|
||||
});
|
||||
|
||||
const list = data.list.map((item: any) => item._source);
|
||||
@@ -84,7 +85,7 @@ const SharedAi: FC<SharedAiProps> = (props) => {
|
||||
} catch (error) {
|
||||
addError(String(error));
|
||||
}
|
||||
}, [server]);
|
||||
}, [server, assistantSearchValue]);
|
||||
|
||||
const selectList = useMemo(() => {
|
||||
return [
|
||||
@@ -95,6 +96,7 @@ const SharedAi: FC<SharedAiProps> = (props) => {
|
||||
value: server?.id,
|
||||
icon: server?.provider?.icon,
|
||||
data: serverList,
|
||||
searchable: false,
|
||||
onChange: (value: string) => {
|
||||
const matched = serverList.find((item) => item.id === value);
|
||||
|
||||
@@ -108,11 +110,15 @@ const SharedAi: FC<SharedAiProps> = (props) => {
|
||||
value: assistant?.id,
|
||||
icon: assistant?.icon,
|
||||
data: assistantList,
|
||||
searchable: true,
|
||||
onChange: (value: string) => {
|
||||
const matched = assistantList.find((item) => item.id === value);
|
||||
|
||||
setAssistant(matched);
|
||||
},
|
||||
onSearch: (value: string) => {
|
||||
setAssistantSearchValue(value);
|
||||
},
|
||||
},
|
||||
];
|
||||
}, [serverList, assistantList, server, assistant]);
|
||||
@@ -136,28 +142,19 @@ const SharedAi: FC<SharedAiProps> = (props) => {
|
||||
</div>
|
||||
|
||||
{selectList.map((item) => {
|
||||
const { label, value, icon, data, onChange } = item;
|
||||
const { label, value, data, searchable, onChange, onSearch } = item;
|
||||
|
||||
return (
|
||||
<div key={label} className="mt-4">
|
||||
<div className="mb-2 text-[#666] dark:text-white/70">{label}</div>
|
||||
|
||||
<div className="flex items-center gap-1 px-3 py-1 border dark:border-gray-700 rounded-md focus-within:!border-[#0087FF] hover:!border-[#0087FF] transition">
|
||||
{icon?.startsWith("font_") ? (
|
||||
<FontIcon name={icon} className="size-5" />
|
||||
) : (
|
||||
<img src={icon} className="size-5" />
|
||||
)}
|
||||
|
||||
<SettingsSelectPro
|
||||
data={data}
|
||||
value={value}
|
||||
rootClassName="flex-1 border-0"
|
||||
onChange={(event) => {
|
||||
onChange(event.currentTarget.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<SettingsSelectPro
|
||||
value={value}
|
||||
options={data}
|
||||
searchable={searchable}
|
||||
onChange={onChange}
|
||||
onSearch={onSearch}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -26,7 +26,7 @@ const Details = () => {
|
||||
const renderContent = () => {
|
||||
if (!rootState.activeExtension) return;
|
||||
|
||||
const { id, type } = rootState.activeExtension;
|
||||
const { id, type, description } = rootState.activeExtension;
|
||||
|
||||
if (id === "Applications") {
|
||||
return <Applications />;
|
||||
@@ -56,12 +56,14 @@ const Details = () => {
|
||||
if (id === "Calculator") {
|
||||
return <Calculator />;
|
||||
}
|
||||
|
||||
return <div className="text-[#999]">{description}</div>;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex-1 h-full overflow-auto">
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
||||
{rootState.activeExtension?.title}
|
||||
{rootState.activeExtension?.name}
|
||||
</h2>
|
||||
|
||||
<div className="pr-4 pb-4 text-sm">{renderContent()}</div>
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import { createContext, useEffect } from "react";
|
||||
import { useMount, useReactive } from "ahooks";
|
||||
import { useReactive } from "ahooks";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { LiteralUnion } from "type-fest";
|
||||
import { cloneDeep, sortBy } from "lodash-es";
|
||||
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
import Content from "./components/Content";
|
||||
import Details from "./components/Details";
|
||||
import { cloneDeep, sortBy } from "lodash-es";
|
||||
import { useExtensionsStore } from "@/stores/extensionsStore";
|
||||
import { Button } from "@headlessui/react";
|
||||
import { Plus } from "lucide-react";
|
||||
import SettingsInput from "../SettingsInput";
|
||||
import clsx from "clsx";
|
||||
|
||||
export type ExtensionId = LiteralUnion<
|
||||
"Applications" | "Calculator" | "QuickAIAccess" | "AIOverview",
|
||||
@@ -19,7 +23,7 @@ type ExtensionType =
|
||||
| "extension"
|
||||
| "application"
|
||||
| "script"
|
||||
| "quick_link"
|
||||
| "quicklink"
|
||||
| "setting"
|
||||
| "calculator"
|
||||
| "command"
|
||||
@@ -40,27 +44,38 @@ export interface Extension {
|
||||
id: ExtensionId;
|
||||
type: ExtensionType;
|
||||
icon: string;
|
||||
title: string;
|
||||
name: string;
|
||||
description: string;
|
||||
alias?: string;
|
||||
hotkey?: string;
|
||||
enabled: boolean;
|
||||
platforms?: ExtensionPlatform[];
|
||||
action: ExtensionAction;
|
||||
quick_link: ExtensionQuickLink;
|
||||
quicklink: ExtensionQuickLink;
|
||||
commands?: Extension[];
|
||||
scripts?: Extension[];
|
||||
quick_links?: Extension[];
|
||||
quicklinks?: Extension[];
|
||||
settings: Record<string, unknown>;
|
||||
developer?: string;
|
||||
}
|
||||
|
||||
type Category = LiteralUnion<
|
||||
"All" | "Commands" | "Scripts" | "Apps" | "QuickLinks",
|
||||
string
|
||||
>;
|
||||
|
||||
interface State {
|
||||
extensions: Extension[];
|
||||
activeExtension?: Extension;
|
||||
categories: Category[];
|
||||
currentCategory: Category;
|
||||
searchValue?: string;
|
||||
}
|
||||
|
||||
const INITIAL_STATE: State = {
|
||||
extensions: [],
|
||||
categories: ["All", "Commands", "Scripts", "Apps", "QuickLinks"],
|
||||
currentCategory: "All",
|
||||
};
|
||||
|
||||
export const ExtensionsContext = createContext<{ rootState: State }>({
|
||||
@@ -70,16 +85,11 @@ export const ExtensionsContext = createContext<{ rootState: State }>({
|
||||
export const Extensions = () => {
|
||||
const { t } = useTranslation();
|
||||
const state = useReactive<State>(cloneDeep(INITIAL_STATE));
|
||||
const { configId, setConfigId } = useExtensionsStore();
|
||||
|
||||
useMount(async () => {
|
||||
const result = await platformAdapter.invokeBackend<[boolean, Extension[]]>(
|
||||
"list_extensions"
|
||||
);
|
||||
|
||||
const extensions = result[1];
|
||||
|
||||
state.extensions = sortBy(extensions, ["title"]);
|
||||
});
|
||||
useEffect(() => {
|
||||
getExtensions();
|
||||
}, [state.searchValue, state.currentCategory, configId]);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = useExtensionsStore.subscribe((state) => {
|
||||
@@ -91,19 +101,105 @@ export const Extensions = () => {
|
||||
};
|
||||
});
|
||||
|
||||
const getExtensions = async () => {
|
||||
const result = await platformAdapter.invokeBackend<[boolean, Extension[]]>(
|
||||
"list_extensions",
|
||||
{
|
||||
query: state.searchValue,
|
||||
extensionType: getExtensionType(),
|
||||
listEnabled: false,
|
||||
}
|
||||
);
|
||||
|
||||
const extensions = result[1];
|
||||
|
||||
state.extensions = sortBy(extensions, ["name"]);
|
||||
|
||||
if (configId) {
|
||||
const matched = extensions.find((item) => item.id === configId);
|
||||
|
||||
if (!matched) return;
|
||||
|
||||
state.activeExtension = matched;
|
||||
|
||||
setConfigId(void 0);
|
||||
}
|
||||
};
|
||||
|
||||
const getExtensionType = (): ExtensionType | undefined => {
|
||||
switch (state.currentCategory) {
|
||||
case "All":
|
||||
return void 0;
|
||||
case "Commands":
|
||||
return "command";
|
||||
case "Scripts":
|
||||
return "script";
|
||||
case "Apps":
|
||||
return "application";
|
||||
case "QuickLinks":
|
||||
return "quicklink";
|
||||
default:
|
||||
return void 0;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ExtensionsContext.Provider
|
||||
value={{
|
||||
rootState: state,
|
||||
}}
|
||||
>
|
||||
<div className="flex h-[calc(100vh-128px)] -mx-6 gap-4">
|
||||
<div className="flex h-[calc(100vh-128px)] -mx-6 gap-4 text-sm">
|
||||
<div className="w-2/3 h-full px-4 border-r dark:border-gray-700 overflow-auto">
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
||||
{t("settings.extensions.title")}
|
||||
</h2>
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{t("settings.extensions.title")}
|
||||
</h2>
|
||||
|
||||
<div>
|
||||
<Button
|
||||
className="flex items-center justify-center size-6 border rounded-md dark:border-gray-700 hover:!border-[#0096FB] transition"
|
||||
onClick={() => {
|
||||
platformAdapter.emitEvent("open-extension-store");
|
||||
}}
|
||||
>
|
||||
<Plus className="size-4 text-[#0096FB]" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between gap-6 my-4">
|
||||
<div className="flex h-8 border dark:border-gray-700">
|
||||
{state.categories.map((item) => {
|
||||
return (
|
||||
<div
|
||||
key={item}
|
||||
className={clsx(
|
||||
"flex items-center h-full px-4 cursor-pointer",
|
||||
{
|
||||
"bg-[#F0F6FE] dark:bg-gray-700":
|
||||
item === state.currentCategory,
|
||||
}
|
||||
)}
|
||||
onClick={() => {
|
||||
state.currentCategory = item;
|
||||
}}
|
||||
>
|
||||
{item}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<SettingsInput
|
||||
className="flex-1"
|
||||
placeholder="Search"
|
||||
value={state.searchValue}
|
||||
onChange={(value) => {
|
||||
state.searchValue = String(value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<>
|
||||
<div className="flex">
|
||||
<div className="flex-1">{t("settings.extensions.list.name")}</div>
|
||||
|
||||
@@ -124,7 +220,7 @@ export const Extensions = () => {
|
||||
</div>
|
||||
|
||||
<Content />
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
|
||||
<Details />
|
||||
|
||||
@@ -3,10 +3,8 @@ import clsx from "clsx";
|
||||
import { isNumber } from "lodash-es";
|
||||
import { FC, FocusEvent } from "react";
|
||||
|
||||
import { specialCharacterFiltering } from "@/utils"
|
||||
|
||||
interface SettingsInputProps extends Omit<InputProps, "onChange"> {
|
||||
onChange: (value?: string | number) => void;
|
||||
onChange?: (value?: string | number) => void;
|
||||
}
|
||||
|
||||
const SettingsInput: FC<SettingsInputProps> = (props) => {
|
||||
@@ -40,8 +38,7 @@ const SettingsInput: FC<SettingsInputProps> = (props) => {
|
||||
)}
|
||||
onBlur={handleBlur}
|
||||
onChange={(event) => {
|
||||
const value = specialCharacterFiltering(event.target.value)
|
||||
onChange?.(value);
|
||||
onChange?.(event.target.value);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,60 +1,109 @@
|
||||
import { Select, SelectProps } from "@headlessui/react";
|
||||
import { useBoolean, useClickAway, useDebounce } from "ahooks";
|
||||
import clsx from "clsx";
|
||||
import { isArray } from "lodash-es";
|
||||
import { ChevronDownIcon } from "lucide-react";
|
||||
import { FC } from "react";
|
||||
import { FC, useEffect, useMemo, useRef, useState } from "react";
|
||||
import SettingsInput from "./SettingsInput";
|
||||
import NoDataImage from "../Common/NoDataImage";
|
||||
|
||||
interface SettingsSelectProProps extends SelectProps {
|
||||
data?: any[];
|
||||
interface SettingsSelectProProps {
|
||||
value: any;
|
||||
placeholder?: string;
|
||||
options?: any[];
|
||||
labelField?: string;
|
||||
valueField?: string;
|
||||
rootClassName?: string;
|
||||
selectClassName?: string;
|
||||
searchable?: boolean;
|
||||
onChange?: (value: string) => void;
|
||||
onSearch?: (value: string) => void;
|
||||
}
|
||||
|
||||
const SettingsSelectPro: FC<SettingsSelectProProps> = (props) => {
|
||||
const {
|
||||
data,
|
||||
value,
|
||||
placeholder = "Select",
|
||||
options,
|
||||
labelField = "name",
|
||||
valueField = "id",
|
||||
rootClassName,
|
||||
selectClassName,
|
||||
children,
|
||||
...rest
|
||||
searchable,
|
||||
onChange,
|
||||
onSearch,
|
||||
} = props;
|
||||
|
||||
const renderOptions = () => {
|
||||
if (isArray(data)) {
|
||||
return data.map((item) => {
|
||||
return (
|
||||
<option key={item?.[valueField]} value={item?.[valueField]}>
|
||||
{item?.[labelField]}
|
||||
</option>
|
||||
);
|
||||
});
|
||||
}
|
||||
const [open, { toggle, setFalse }] = useBoolean();
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [searchValue, setSearchValue] = useState("");
|
||||
const debouncedSearchValue = useDebounce(searchValue, { wait: 500 });
|
||||
|
||||
return children;
|
||||
};
|
||||
useClickAway(setFalse, containerRef);
|
||||
|
||||
useEffect(() => {
|
||||
onSearch?.(debouncedSearchValue);
|
||||
}, [debouncedSearchValue]);
|
||||
|
||||
const option = useMemo(() => {
|
||||
return options?.find((item) => {
|
||||
return item?.[valueField] === value;
|
||||
});
|
||||
}, [options, value]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"relative flex items-center h-8 px-2 border rounded-md",
|
||||
rootClassName
|
||||
)}
|
||||
>
|
||||
<Select
|
||||
{...rest}
|
||||
<div ref={containerRef} className="relative">
|
||||
<div
|
||||
className="flex items-center h-8 px-3 truncate rounded-md border dark:bg-[#1F2937] bg-white dark:border-[#374151]"
|
||||
onClick={toggle}
|
||||
>
|
||||
{option?.[labelField] ?? (
|
||||
<div className="opacity-50">{placeholder}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={clsx(
|
||||
"appearance-none size-full pr-4 bg-transparent",
|
||||
selectClassName
|
||||
"absolute z-100 top-10 left-0 right-0 rounded-md py-2 border dark:border-[#374151] bg-white dark:bg-[#1F2937] shadow-[0_5px_15px_rgba(0,0,0,0.2)] dark:shadow-[0_5px_10px_rgba(0,0,0,0.3)]",
|
||||
{
|
||||
hidden: !open,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{renderOptions()}
|
||||
</Select>
|
||||
{searchable && (
|
||||
<div className="px-2 mb-2">
|
||||
<SettingsInput
|
||||
autoFocus
|
||||
value={searchValue}
|
||||
className="w-full"
|
||||
onChange={(value) => {
|
||||
setSearchValue(String(value));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ChevronDownIcon className="absolute size-4 right-2 pointer-events-none" />
|
||||
{options && options.length > 0 ? (
|
||||
<div className="flex flex-col gap-1 max-h-80 px-2 overflow-auto custom-scrollbar">
|
||||
{options?.map((item, index) => {
|
||||
return (
|
||||
<div
|
||||
key={item?.[valueField] ?? index}
|
||||
className={clsx(
|
||||
"h-8 leading-8 px-2 rounded-md hover:bg-[#EDEDED] hover:dark:bg-[#374151] transition cursor-pointer",
|
||||
{
|
||||
"bg-[#EDEDED] dark:bg-[#374151]":
|
||||
value === item?.[valueField],
|
||||
}
|
||||
)}
|
||||
onClick={() => {
|
||||
onChange?.(item?.[valueField]);
|
||||
}}
|
||||
>
|
||||
<span className="block truncate">{item?.[labelField]}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-center py-4">
|
||||
<NoDataImage />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useCallback, useMemo, useEffect } from "react";
|
||||
import { Button, Dialog, DialogPanel } from "@headlessui/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { noop } from "lodash-es";
|
||||
@@ -6,12 +6,15 @@ import { LoaderCircle, X } from "lucide-react";
|
||||
import { useInterval, useReactive } from "ahooks";
|
||||
import clsx from "clsx";
|
||||
|
||||
import lightIcon from "./imgs/light-icon.png";
|
||||
import darkIcon from "./imgs/dark-icon.png";
|
||||
import lightIcon from "@/assets/images/UpdateApp/light-icon.png";
|
||||
import darkIcon from "@/assets/images/UpdateApp/dark-icon.png";
|
||||
import { useThemeStore } from "@/stores/themeStore";
|
||||
import { useUpdateStore } from "@/stores/updateStore";
|
||||
import { OpenURLWithBrowser } from "@/utils/index";
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
import { useAppearanceStore } from "@/stores/appearanceStore";
|
||||
import { hide_check } from "@/commands";
|
||||
|
||||
interface State {
|
||||
loading?: boolean;
|
||||
@@ -20,21 +23,31 @@ interface State {
|
||||
}
|
||||
|
||||
interface UpdateAppProps {
|
||||
checkUpdate: () => Promise<any>;
|
||||
relaunchApp: () => Promise<void>;
|
||||
isCheckPage?: boolean;
|
||||
}
|
||||
|
||||
const UpdateApp = ({ checkUpdate, relaunchApp }: UpdateAppProps) => {
|
||||
const UpdateApp = ({ isCheckPage }: UpdateAppProps) => {
|
||||
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 { visible, setVisible, skipVersion, setSkipVersion, isOptional, updateInfo, setUpdateInfo } = useUpdateStore();
|
||||
const addError = useAppStore((state) => state.addError);
|
||||
const snapshotUpdate = useAppearanceStore((state) => state.snapshotUpdate);
|
||||
|
||||
const checkUpdate = useCallback(async () => {
|
||||
return platformAdapter.checkUpdate();
|
||||
}, []);
|
||||
|
||||
const relaunchApp = useCallback(async () => {
|
||||
return platformAdapter.relaunchApp();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!snapshotUpdate) return;
|
||||
|
||||
checkUpdate().catch((error) => {
|
||||
addError("Update failed:" + error, "error");
|
||||
});
|
||||
}, [snapshotUpdate]);
|
||||
|
||||
const state = useReactive<State>({ download: 0 });
|
||||
|
||||
@@ -102,7 +115,7 @@ const UpdateApp = ({ checkUpdate, relaunchApp }: UpdateAppProps) => {
|
||||
|
||||
setSkipVersion(updateInfo?.version);
|
||||
|
||||
setVisible(false);
|
||||
isCheckPage ? hide_check() : setVisible(false);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -113,78 +126,75 @@ const UpdateApp = ({ checkUpdate, relaunchApp }: UpdateAppProps) => {
|
||||
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">
|
||||
<div className={clsx("flex min-h-full items-center justify-center", !isCheckPage && "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"
|
||||
className={`relative w-[340px] py-8 flex flex-col items-center bg-white shadow-md border border-[#EDEDED] dark:bg-[#333] dark:border-black/20 ${isCheckPage ? "" : "rounded-lg"}`}
|
||||
>
|
||||
<X
|
||||
className={clsx(
|
||||
"absolute size-5 text-[#999] top-3 right-3 dark:text-[#D8D8D8]",
|
||||
cursorClassName,
|
||||
{
|
||||
hidden: !isOptional,
|
||||
}
|
||||
)}
|
||||
onClick={handleCancel}
|
||||
/>
|
||||
{!isCheckPage && isOptional && (
|
||||
<X
|
||||
className={clsx("absolute size-5 top-3 right-3 text-[#999] dark:text-[#D8D8D8]", cursorClassName)}
|
||||
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 className="text-[#333] text-sm leading-5 py-2 dark:text-[#D8D8D8] text-center">
|
||||
{updateInfo?.available ? (
|
||||
isOptional ? t("update.optional_description") : (
|
||||
<>
|
||||
<p>{t("update.force_description1")}</p>
|
||||
<p>{t("update.force_description2")}</p>
|
||||
</>
|
||||
)
|
||||
) : t("update.date")}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="text-xs text-[#0072FF] cursor-pointer"
|
||||
onClick={() => {
|
||||
OpenURLWithBrowser(
|
||||
"https://docs.infinilabs.com/coco-app/main/docs/release-notes"
|
||||
);
|
||||
}}
|
||||
>
|
||||
v{updateInfo?.version} {t("update.releaseNotes")}
|
||||
</div>
|
||||
{updateInfo?.available ? (
|
||||
<div
|
||||
className="text-xs text-[#0072FF] cursor-pointer"
|
||||
onClick={() =>
|
||||
OpenURLWithBrowser("https://docs.infinilabs.com/coco-app/main/docs/release-notes")
|
||||
}
|
||||
>
|
||||
v{updateInfo.version} {t("update.releaseNotes")}
|
||||
</div>
|
||||
) : (
|
||||
<div className={clsx("text-xs text-[#999]", cursorClassName)}>
|
||||
{t("update.latest", { replace: [updateInfo?.version] })}
|
||||
</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,
|
||||
}
|
||||
state.loading && "opacity-50"
|
||||
)}
|
||||
onClick={handleDownload}
|
||||
onClick={updateInfo?.available ? handleDownload : handleSkip}
|
||||
>
|
||||
{state.loading ? (
|
||||
<div className="flex justify-center items-center gap-2">
|
||||
<LoaderCircle className="animate-spin size-5" />
|
||||
{percent}%
|
||||
</div>
|
||||
) : (
|
||||
t("update.button.download")
|
||||
)}
|
||||
) : updateInfo?.available ? t("update.button.install") : t("update.button.ok")}
|
||||
</Button>
|
||||
|
||||
<div
|
||||
className={clsx("text-xs text-[#999]", cursorClassName, {
|
||||
hidden: !isOptional,
|
||||
})}
|
||||
onClick={handleSkip}
|
||||
>
|
||||
{t("update.skip_version")}
|
||||
</div>
|
||||
{updateInfo?.available && isOptional && (
|
||||
<div
|
||||
className={clsx("text-xs text-[#999]", cursorClassName)}
|
||||
onClick={handleSkip}
|
||||
>
|
||||
{t("update.skip_version")}
|
||||
</div>
|
||||
)}
|
||||
</DialogPanel>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useConnectStore } from "@/stores/connectStore";
|
||||
import { useChatStore } from "@/stores/chatStore";
|
||||
import { useSearchStore } from "@/stores/searchStore";
|
||||
import { useAuthStore } from "@/stores/authStore";
|
||||
import { unrequitable } from "@/utils";
|
||||
|
||||
export function useChatActions(
|
||||
setActiveChat: (chat: Chat | undefined) => void,
|
||||
@@ -324,7 +325,7 @@ export function useChatActions(
|
||||
const getChatHistory = useCallback(async () => {
|
||||
let response: any;
|
||||
if (isTauri) {
|
||||
if (!currentService?.id || !isCurrentLogin || !currentService?.enabled) {
|
||||
if (unrequitable()) {
|
||||
return setChats([]);
|
||||
}
|
||||
|
||||
@@ -347,11 +348,17 @@ export function useChatActions(
|
||||
console.log("_history", response);
|
||||
const hits = response?.hits?.hits || [];
|
||||
setChats(hits);
|
||||
}, [currentService?.id, keyword, isTauri, currentService?.enabled, isCurrentLogin]);
|
||||
}, [
|
||||
currentService?.id,
|
||||
keyword,
|
||||
isTauri,
|
||||
currentService?.enabled,
|
||||
isCurrentLogin,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (showChatHistory && connected) {
|
||||
getChatHistory()
|
||||
getChatHistory();
|
||||
}
|
||||
}, [showChatHistory, connected, getChatHistory, currentService?.id]);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useEffect } from "react";
|
||||
|
||||
import { isMetaOrCtrlKey } from "@/utils/keyboardUtils";
|
||||
import { useSearchStore } from "@/stores/searchStore";
|
||||
import { useShortcutsStore } from "@/stores/shortcutsStore";
|
||||
|
||||
interface KeyboardHandlersProps {
|
||||
isChatMode: boolean;
|
||||
@@ -14,15 +14,38 @@ export function useKeyboardHandlers({
|
||||
handleSubmit,
|
||||
curChatEnd,
|
||||
}: KeyboardHandlersProps) {
|
||||
const { setSourceData } = useSearchStore();
|
||||
const {
|
||||
setSourceData,
|
||||
visibleExtensionStore,
|
||||
setVisibleExtensionStore,
|
||||
visibleExtensionDetail,
|
||||
setVisibleExtensionDetail,
|
||||
} = useSearchStore();
|
||||
const { modifierKey } = useShortcutsStore();
|
||||
|
||||
const getModifierKeyPressed = (event: KeyboardEvent) => {
|
||||
const metaKeyPressed = event.metaKey && modifierKey === "meta";
|
||||
const ctrlKeyPressed = event.ctrlKey && modifierKey === "ctrl";
|
||||
const altKeyPressed = event.altKey && modifierKey === "alt";
|
||||
|
||||
return metaKeyPressed || ctrlKeyPressed || altKeyPressed;
|
||||
};
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
// Handle ArrowLeft with meta key
|
||||
if (e.code === "ArrowLeft" && isMetaOrCtrlKey(e)) {
|
||||
if (e.code === "ArrowLeft" && getModifierKeyPressed(e)) {
|
||||
e.preventDefault();
|
||||
setSourceData(undefined);
|
||||
return;
|
||||
|
||||
if (visibleExtensionDetail) {
|
||||
return setVisibleExtensionDetail(false);
|
||||
}
|
||||
|
||||
if (visibleExtensionStore) {
|
||||
return setVisibleExtensionStore(false);
|
||||
}
|
||||
|
||||
return setSourceData(void 0);
|
||||
}
|
||||
|
||||
// Handle Enter without meta key requirement
|
||||
@@ -31,7 +54,14 @@ export function useKeyboardHandlers({
|
||||
curChatEnd && handleSubmit();
|
||||
}
|
||||
},
|
||||
[isChatMode, handleSubmit, setSourceData, curChatEnd]
|
||||
[
|
||||
isChatMode,
|
||||
handleSubmit,
|
||||
setSourceData,
|
||||
curChatEnd,
|
||||
modifierKey,
|
||||
visibleExtensionDetail,
|
||||
]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -45,6 +75,7 @@ export function useKeyboardHandlers({
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
setSourceData(undefined);
|
||||
setVisibleExtensionStore(false);
|
||||
};
|
||||
}, []);
|
||||
}
|
||||
|
||||
@@ -30,5 +30,5 @@ export const useIconfontScript = () => {
|
||||
// Coco Server Icons
|
||||
useScript("https://at.alicdn.com/t/c/font_4878526_cykw3et0ezd.js");
|
||||
// Coco App Icons
|
||||
useScript("https://at.alicdn.com/t/c/font_4934333_j1t3b1xyxkk.js");
|
||||
useScript("https://at.alicdn.com/t/c/font_4934333_80wr9yn2eup.js");
|
||||
};
|
||||
|
||||
@@ -193,6 +193,7 @@ export const useSyncStore = () => {
|
||||
setQueryTimeout(querySourceTimeout);
|
||||
}
|
||||
setAllowSelfSignature(allowSelfSignature);
|
||||
|
||||
setCurrentService(currentService);
|
||||
}),
|
||||
|
||||
|
||||
@@ -7,7 +7,9 @@ import { exit } from "@tauri-apps/plugin-process";
|
||||
|
||||
import { isMac } from "@/utils/platform";
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
import { show_coco, show_settings } from "@/commands";
|
||||
import { useUpdateStore } from "@/stores/updateStore";
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
import { show_coco, show_settings, show_check } from "@/commands";
|
||||
|
||||
const TRAY_ID = "COCO_TRAY";
|
||||
|
||||
@@ -62,6 +64,17 @@ export const useTray = () => {
|
||||
show_settings()
|
||||
},
|
||||
}),
|
||||
MenuItem.new({
|
||||
text: t("tray.checkUpdate"),
|
||||
action: async () => {
|
||||
const update = await platformAdapter.checkUpdate();
|
||||
if (update) {
|
||||
useUpdateStore.getState().setUpdateInfo(update);
|
||||
useUpdateStore.getState().setVisible(true);
|
||||
}
|
||||
show_check();
|
||||
},
|
||||
}),
|
||||
PredefinedMenuItem.new({ item: "Separator" }),
|
||||
MenuItem.new({
|
||||
text: t("tray.quitCoco"),
|
||||
|
||||
@@ -219,7 +219,10 @@
|
||||
},
|
||||
"details": {
|
||||
"searchScope": "Search Scope",
|
||||
"rebuildIndex": "Rebuild Index",
|
||||
"reindex": "Rebuild Index",
|
||||
"searchScopeDescription": "Directories added here will be searched for applications and preference panes.",
|
||||
"rebuildIndexDescription": "Rebuild the index to search the latest application list.",
|
||||
"name": "Name",
|
||||
"where": "Where",
|
||||
"type": "Type",
|
||||
@@ -301,7 +304,10 @@
|
||||
"updateAvailable": "Update available",
|
||||
"select": "Select",
|
||||
"open": "Open",
|
||||
"powered": "Powered by Coco AI"
|
||||
"powered": "Powered by Coco AI",
|
||||
"install": "Install",
|
||||
"details": "Details",
|
||||
"uninstall": "Uninstall"
|
||||
},
|
||||
"input": {
|
||||
"searchPlaceholder": "Search whatever you want ...",
|
||||
@@ -340,7 +346,11 @@
|
||||
"title": {
|
||||
"calculator": "Calculator"
|
||||
},
|
||||
"search": "Search Operation"
|
||||
"search": "Search Operation",
|
||||
"details": "Details",
|
||||
"install": "Install",
|
||||
"uninstall": "Uninstall",
|
||||
"configureExtension": "Configure Extension"
|
||||
},
|
||||
"askCocoAi": {
|
||||
"title": "{{0}} {{1}}",
|
||||
@@ -455,7 +465,8 @@
|
||||
"tray": {
|
||||
"showCoco": "Show Coco",
|
||||
"settings": "Settings...",
|
||||
"quitCoco": "Quit Coco"
|
||||
"quitCoco": "Quit Coco",
|
||||
"checkUpdate": "Check for Updates"
|
||||
},
|
||||
"update": {
|
||||
"title": "New update available for Coco AI.",
|
||||
@@ -464,9 +475,12 @@
|
||||
"force_description2": "Please install the latest version to continue.",
|
||||
"releaseNotes": "Release Notes",
|
||||
"button": {
|
||||
"download": "Download"
|
||||
"install": "Install",
|
||||
"ok": "Ok"
|
||||
},
|
||||
"skip_version": "Skip this version"
|
||||
"skip_version": "Skip this version",
|
||||
"date": "You're up to date",
|
||||
"latest": "\"{{0}}\" is the latest version"
|
||||
},
|
||||
"error": {
|
||||
"message": "Sorry, there is an error in your Coco App. Please contact the administrator."
|
||||
@@ -502,5 +516,32 @@
|
||||
"divide": "Divide",
|
||||
"remainder": "Remainder",
|
||||
"expression": "Expression"
|
||||
},
|
||||
"extensionStore": {
|
||||
"hints": {
|
||||
"installationCompleted": "installation completed",
|
||||
"uninstallationCompleted": "uninstallation completed"
|
||||
}
|
||||
},
|
||||
"extensionDetail": {
|
||||
"label": {
|
||||
"description": "Description",
|
||||
"commands": "Commands",
|
||||
"tags": "Tags",
|
||||
"lastUpdate": "Last Update"
|
||||
},
|
||||
"hints": {
|
||||
"installed": "Installed"
|
||||
},
|
||||
"deleteDialog": {
|
||||
"title": "Uninstall",
|
||||
"description": "This will remove all the data and commands associated with this extension."
|
||||
}
|
||||
},
|
||||
"deleteDialog": {
|
||||
"button": {
|
||||
"cancel": "Cancel",
|
||||
"delete": "Delete"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,7 +219,10 @@
|
||||
},
|
||||
"details": {
|
||||
"searchScope": "搜索范围",
|
||||
"rebuildIndex": "重建索引",
|
||||
"reindex": "重建索引",
|
||||
"searchScopeDescription": "在此添加的目录将用于搜索应用程序。",
|
||||
"rebuildIndexDescription": "重建索引以搜索最新的 App 列表。",
|
||||
"name": "名称",
|
||||
"where": "来源",
|
||||
"type": "类型",
|
||||
@@ -301,7 +304,10 @@
|
||||
"updateAvailable": "有可用更新",
|
||||
"select": "选择",
|
||||
"open": "打开",
|
||||
"powered": "由 Coco AI 提供支持"
|
||||
"powered": "由 Coco AI 提供支持",
|
||||
"install": "安装",
|
||||
"details": "详情",
|
||||
"uninstall": "卸载"
|
||||
},
|
||||
"input": {
|
||||
"searchPlaceholder": "搜索任何内容...",
|
||||
@@ -340,7 +346,11 @@
|
||||
"title": {
|
||||
"calculator": "计算器"
|
||||
},
|
||||
"search": "搜索操作"
|
||||
"search": "搜索操作",
|
||||
"details": "详情",
|
||||
"install": "安装",
|
||||
"uninstall": "卸载",
|
||||
"configureExtension": "配置扩展"
|
||||
},
|
||||
"askCocoAi": {
|
||||
"title": "{{0}}{{1}}",
|
||||
@@ -455,7 +465,8 @@
|
||||
"tray": {
|
||||
"showCoco": "显示 Coco",
|
||||
"settings": "偏好设置",
|
||||
"quitCoco": "退出 Coco"
|
||||
"quitCoco": "退出 Coco",
|
||||
"checkUpdate": "检查更新"
|
||||
},
|
||||
"update": {
|
||||
"optional_description": "Coco AI 有新的可用更新。",
|
||||
@@ -463,9 +474,12 @@
|
||||
"force_description2": "请安装最新版本后继续使用。",
|
||||
"releaseNotes": "更新日志",
|
||||
"button": {
|
||||
"download": "下载"
|
||||
"install": "安装",
|
||||
"ok": "好"
|
||||
},
|
||||
"skip_version": "跳过此版本"
|
||||
"skip_version": "跳过此版本",
|
||||
"date": "已是最新版本",
|
||||
"latest": "当前版本:\"{{0}}\""
|
||||
},
|
||||
"error": {
|
||||
"message": "抱歉,Coco 应用出现了错误。请联系管理员。"
|
||||
@@ -501,5 +515,32 @@
|
||||
"divide": "相除",
|
||||
"remainder": "求余",
|
||||
"expression": "表达式"
|
||||
},
|
||||
"extensionStore": {
|
||||
"hints": {
|
||||
"installationCompleted": "安装成功",
|
||||
"uninstallationCompleted": "卸载成功"
|
||||
}
|
||||
},
|
||||
"extensionDetail": {
|
||||
"label": {
|
||||
"description": "描述",
|
||||
"commands": "命令",
|
||||
"tags": "标签",
|
||||
"lastUpdate": "最后更新时间"
|
||||
},
|
||||
"hints": {
|
||||
"installed": "已安装"
|
||||
},
|
||||
"deleteDialog": {
|
||||
"title": "卸载",
|
||||
"description": "这将删除与该扩展相关的所有数据和命令。"
|
||||
}
|
||||
},
|
||||
"deleteDialog": {
|
||||
"button": {
|
||||
"cancel": "取消",
|
||||
"delete": "删除"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@ import HistoryList from "@/components/Common/HistoryList";
|
||||
import { useSyncStore } from "@/hooks/useSyncStore";
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
import { useChatStore } from "@/stores/chatStore";
|
||||
import { unrequitable } from "@/utils";
|
||||
|
||||
interface ChatProps {}
|
||||
|
||||
@@ -72,7 +73,7 @@ export default function Chat({}: ChatProps) {
|
||||
|
||||
const getChatHistory = async () => {
|
||||
try {
|
||||
if (!currentService.enabled) {
|
||||
if (unrequitable()) {
|
||||
return setChats([]);
|
||||
}
|
||||
|
||||
|
||||
20
src/pages/check/index.tsx
Normal file
20
src/pages/check/index.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { useUpdateStore } from "@/stores/updateStore";
|
||||
import UpdateApp from "@/components/UpdateApp";
|
||||
|
||||
const CheckApp = () => {
|
||||
const setVisible = useUpdateStore((state) => state.setVisible);
|
||||
|
||||
useEffect(() => {
|
||||
setVisible(true)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<UpdateApp isCheckPage={true} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CheckApp;
|
||||
@@ -1,14 +1,11 @@
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { useEffect } from "react";
|
||||
|
||||
import SearchChat from "@/components/SearchChat";
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
import { useSyncStore } from "@/hooks/useSyncStore";
|
||||
import UpdateApp from "@/components/UpdateApp";
|
||||
import { useAppearanceStore } from "@/stores/appearanceStore";
|
||||
|
||||
function MainApp() {
|
||||
const addError = useAppStore((state) => state.addError);
|
||||
|
||||
const setIsTauri = useAppStore((state) => state.setIsTauri);
|
||||
useEffect(() => {
|
||||
@@ -17,28 +14,10 @@ function MainApp() {
|
||||
|
||||
useSyncStore();
|
||||
|
||||
const snapshotUpdate = useAppearanceStore((state) => state.snapshotUpdate);
|
||||
|
||||
const checkUpdate = useCallback(async () => {
|
||||
return platformAdapter.checkUpdate();
|
||||
}, []);
|
||||
|
||||
const relaunchApp = useCallback(async () => {
|
||||
return platformAdapter.relaunchApp();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!snapshotUpdate) return;
|
||||
|
||||
checkUpdate().catch((error) => {
|
||||
addError("Update failed:" + error, "error");
|
||||
});
|
||||
}, [snapshotUpdate]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SearchChat isTauri={true} hasModules={["search", "chat"]} />
|
||||
<UpdateApp checkUpdate={checkUpdate} relaunchApp={relaunchApp} />
|
||||
<UpdateApp />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import Extensions from "@/components/Settings/Extensions";
|
||||
import { useConnectStore } from "@/stores/connectStore";
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
import { useExtensionsStore } from "@/stores/extensionsStore";
|
||||
|
||||
const tabIndexMap: { [key: string]: number } = {
|
||||
general: 0,
|
||||
@@ -26,6 +27,7 @@ const tabIndexMap: { [key: string]: number } = {
|
||||
|
||||
function SettingsPage() {
|
||||
const { t } = useTranslation();
|
||||
const { setConfigId } = useExtensionsStore();
|
||||
|
||||
useTray();
|
||||
|
||||
@@ -56,10 +58,20 @@ function SettingsPage() {
|
||||
platformAdapter.emitEvent("change-app-store", state);
|
||||
});
|
||||
|
||||
const unlisten2 = platformAdapter.listenEvent(
|
||||
"config-extension",
|
||||
({ payload }) => {
|
||||
platformAdapter.showWindow();
|
||||
setDefaultIndex(1);
|
||||
setConfigId(payload);
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
unlisten.then((fn) => fn());
|
||||
unsubscribeConnect();
|
||||
unsubscribeAppStore();
|
||||
unlisten.then((fn) => fn());
|
||||
unlisten2.then((fn) => fn());
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import DesktopApp from "@/pages/main/index";
|
||||
import SettingsPage from "@/pages/settings/index";
|
||||
import ChatAI from "@/pages/chat/index";
|
||||
import WebPage from "@/pages/web/index";
|
||||
import CheckPage from "@/pages/check/index";
|
||||
|
||||
const routerOptions = {
|
||||
basename: "/",
|
||||
@@ -25,6 +26,7 @@ export const router = createBrowserRouter(
|
||||
{ path: "/ui", element: <DesktopApp /> },
|
||||
{ path: "/ui/settings", element: <SettingsPage /> },
|
||||
{ path: "/ui/chat", element: <ChatAI /> },
|
||||
{ path: "/ui/check", element: <CheckPage /> },
|
||||
{ path: "/web", element: <WebPage /> },
|
||||
],
|
||||
},
|
||||
|
||||
@@ -117,7 +117,10 @@ export default function Layout() {
|
||||
|
||||
useMount(async () => {
|
||||
const result = await platformAdapter.invokeBackend<[boolean, Extension[]]>(
|
||||
"list_extensions"
|
||||
"list_extensions",
|
||||
{
|
||||
listEnabled: false,
|
||||
}
|
||||
);
|
||||
|
||||
if (!isArray(result)) return;
|
||||
|
||||
@@ -19,6 +19,8 @@ export type IExtensionsStore = {
|
||||
setAiOverviewDelay: (aiOverviewDelay: number) => void;
|
||||
aiOverviewMinQuantity: number;
|
||||
setAiOverviewMinQuantity: (aiOverviewMinQuantity: number) => void;
|
||||
configId?: string;
|
||||
setConfigId: (configId?: string) => void;
|
||||
};
|
||||
|
||||
export const useExtensionsStore = create<IExtensionsStore>()(
|
||||
@@ -53,6 +55,9 @@ export const useExtensionsStore = create<IExtensionsStore>()(
|
||||
setAiOverviewMinQuantity(aiOverviewMinQuantity) {
|
||||
return set({ aiOverviewMinQuantity });
|
||||
},
|
||||
setConfigId(configId) {
|
||||
return set({ configId });
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: "extensions-store",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { SearchExtensionItem } from "@/components/Search/ExtensionStore";
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
|
||||
@@ -28,6 +29,18 @@ export type ISearchStore = {
|
||||
setEnabledAiOverview: (enabledAiOverview: boolean) => void;
|
||||
askAiAssistantId?: string;
|
||||
setAskAiAssistantId: (askAiAssistantId?: string) => void;
|
||||
visibleExtensionStore: boolean;
|
||||
setVisibleExtensionStore: (visibleExtensionStore: boolean) => void;
|
||||
searchValue: string;
|
||||
setSearchValue: (searchValue: string) => void;
|
||||
selectedExtension?: SearchExtensionItem;
|
||||
setSelectedExtension: (selectedExtension?: SearchExtensionItem) => void;
|
||||
installingExtensions: string[];
|
||||
setInstallingExtensions: (installingExtensions: string[]) => void;
|
||||
uninstallingExtensions: string[];
|
||||
setUninstallingExtensions: (uninstallingExtensions: string[]) => void;
|
||||
visibleExtensionDetail: boolean;
|
||||
setVisibleExtensionDetail: (visibleExtensionDetail: boolean) => void;
|
||||
};
|
||||
|
||||
export const useSearchStore = create<ISearchStore>()(
|
||||
@@ -70,6 +83,29 @@ export const useSearchStore = create<ISearchStore>()(
|
||||
setAskAiAssistantId: (askAiAssistantId) => {
|
||||
return set({ askAiAssistantId });
|
||||
},
|
||||
visibleExtensionStore: false,
|
||||
setVisibleExtensionStore: (visibleExtensionStore) => {
|
||||
return set({ visibleExtensionStore });
|
||||
},
|
||||
searchValue: "",
|
||||
setSearchValue: (searchValue) => {
|
||||
return set({ searchValue });
|
||||
},
|
||||
setSelectedExtension(selectedExtension) {
|
||||
return set({ selectedExtension });
|
||||
},
|
||||
installingExtensions: [],
|
||||
setInstallingExtensions: (installingExtensions) => {
|
||||
return set({ installingExtensions });
|
||||
},
|
||||
uninstallingExtensions: [],
|
||||
setUninstallingExtensions: (uninstallingExtensions) => {
|
||||
return set({ uninstallingExtensions });
|
||||
},
|
||||
visibleExtensionDetail: false,
|
||||
setVisibleExtensionDetail: (visibleExtensionDetail) => {
|
||||
return set({ visibleExtensionDetail });
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: "search-store",
|
||||
|
||||
@@ -44,6 +44,10 @@ export interface EventPayloads {
|
||||
"quick-ai-access-client-id": any;
|
||||
"ai-overview-client-id": any;
|
||||
"change-app-store": IAppStore;
|
||||
"open-extension-store": void;
|
||||
"install-extension": void;
|
||||
"uninstall-extension": void;
|
||||
"config-extension": string;
|
||||
}
|
||||
|
||||
// Window operation interface
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user