Compare commits
135 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 | ||
|
|
8c9a2ff441 | ||
|
|
2251b0af95 | ||
|
|
560a12ab93 | ||
|
|
2ff66c0b91 | ||
|
|
ef4a184233 | ||
|
|
8422bc03e7 | ||
|
|
370113129c | ||
|
|
cb758ef452 | ||
|
|
12b9b4bb81 | ||
|
|
562db19f16 | ||
|
|
dc5cd9aecb | ||
|
|
0b018cd24f | ||
|
|
2ed22d3d7c | ||
|
|
4ce9561eb7 | ||
|
|
3aeb39b3af | ||
|
|
27e99d4629 | ||
|
|
df70276a54 | ||
|
|
6553a8f5d3 | ||
|
|
4ebbc9ec6e | ||
|
|
4208633556 | ||
|
|
fc43fbe798 | ||
|
|
b5bb9105d4 | ||
|
|
b6ebd6e5f8 | ||
|
|
22216491b6 | ||
|
|
44ca66259c | ||
|
|
be3cae36e2 | ||
|
|
35ea30626f | ||
|
|
4bcae5cffb | ||
|
|
76458db8ab | ||
|
|
5b41e190d3 | ||
|
|
43ac9a054c | ||
|
|
ac485a32cc | ||
|
|
e10908a095 | ||
|
|
78b8908ac8 | ||
|
|
3c54cb84a8 | ||
|
|
8ed808c591 | ||
|
|
7a2dde7448 | ||
|
|
65451fc63e | ||
|
|
5d108a46d3 | ||
|
|
f9567c2d46 | ||
|
|
da917e6012 | ||
|
|
335a906674 | ||
|
|
a50a636d59 | ||
|
|
2dd3f776e6 | ||
|
|
40f6aa0ccd | ||
|
|
4da9e024e0 | ||
|
|
c20bba51f5 | ||
|
|
0a62a2095b | ||
|
|
5677995185 | ||
|
|
ec4e5e7d1d | ||
|
|
1df5265b1a | ||
|
|
fb8a4684dc | ||
|
|
0b609e570d | ||
|
|
f91f6bdc17 | ||
|
|
57590f3b57 | ||
|
|
c18f9ea154 | ||
|
|
441875d9b4 | ||
|
|
eddf9075bb | ||
|
|
9eac8f8a8e | ||
|
|
515260c43f | ||
|
|
118de0e80b | ||
|
|
19ce896fdc | ||
|
|
4a41ea5d8b | ||
|
|
880e1206ce | ||
|
|
1e6d9f9550 | ||
|
|
ff0faf425f | ||
|
|
1fbf5d6552 | ||
|
|
db41e817c3 | ||
|
|
1296755bc5 | ||
|
|
d410f20864 | ||
|
|
61d0a3b79a | ||
|
|
b24319b649 | ||
|
|
3c0fb24548 | ||
|
|
2fcbed0381 | ||
|
|
7444347e0c | ||
|
|
725ce042de | ||
|
|
3b67de5387 | ||
|
|
9b53a026ff | ||
|
|
9ea7dbf3aa | ||
|
|
55622911ac | ||
|
|
92f78ad08c | ||
|
|
f690dbaab2 | ||
|
|
210efe763d | ||
|
|
f23498afa0 | ||
|
|
a80a5d928f | ||
|
|
b733bb5516 | ||
|
|
5046754534 | ||
|
|
f557f7e780 | ||
|
|
18feb2d690 | ||
|
|
af59f2fe9f | ||
|
|
5e1bb54d5e | ||
|
|
33fa516aad | ||
|
|
d2c1cf513d |
117
.github/workflows/release.yml
vendored
@@ -9,10 +9,16 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
create-release:
|
create-release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
outputs:
|
||||||
|
APP_VERSION: ${{ steps.get-version.outputs.APP_VERSION }}
|
||||||
|
RELEASE_BODY: ${{ steps.get-changelog.outputs.RELEASE_BODY }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Set output
|
- name: Set output
|
||||||
id: vars
|
id: vars
|
||||||
run: echo "tag=${GITHUB_REF#refs/*/}" >> $GITHUB_OUTPUT
|
run: echo "tag=${GITHUB_REF#refs/*/}" >> $GITHUB_OUTPUT
|
||||||
@@ -22,11 +28,28 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: 20
|
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
|
- name: Generate changelog
|
||||||
id: create_release
|
id: get-changelog
|
||||||
run: npx changelogithub --draft --name ${{ steps.vars.outputs.tag }}
|
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:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
build-app:
|
build-app:
|
||||||
needs: create-release
|
needs: create-release
|
||||||
@@ -52,11 +75,24 @@ jobs:
|
|||||||
target: "x86_64-unknown-linux-gnu"
|
target: "x86_64-unknown-linux-gnu"
|
||||||
- platform: "ubuntu-22.04-arm"
|
- platform: "ubuntu-22.04-arm"
|
||||||
target: "aarch64-unknown-linux-gnu"
|
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 }}
|
runs-on: ${{ matrix.platform }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
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
|
- name: Setup node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
@@ -65,17 +101,31 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
|
|
||||||
- name: Install rust target
|
|
||||||
run: rustup target add ${{ matrix.target }}
|
|
||||||
|
|
||||||
- name: Install dependencies (ubuntu only)
|
- name: Install dependencies (ubuntu only)
|
||||||
if: startsWith(matrix.platform, 'ubuntu-22.04')
|
if: startsWith(matrix.platform, 'ubuntu-22.04')
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf xdg-utils
|
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf xdg-utils
|
||||||
|
|
||||||
- name: Install Rust stable
|
- name: Add Rust build target at ${{ matrix.platform}} for ${{ matrix.target }}
|
||||||
run: rustup toolchain install stable
|
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
|
- name: Rust cache
|
||||||
uses: swatinem/rust-cache@v2
|
uses: swatinem/rust-cache@v2
|
||||||
@@ -91,28 +141,8 @@ jobs:
|
|||||||
- name: Install app dependencies and build web
|
- name: Install app dependencies and build web
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: Set up SSH agent for private repository clone
|
- name: Build the coco at ${{ matrix.platform}} for ${{ matrix.target }} @ ${{ env.APP_VERSION }}
|
||||||
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
|
|
||||||
if: matrix.target != 'i686-pc-windows-msvc'
|
|
||||||
run: |
|
|
||||||
make add-dep-pizza-engine
|
|
||||||
rustup target add ${{ matrix.target}} --toolchain nightly-2025-02-28
|
|
||||||
|
|
||||||
- name: Build the app with ${{ matrix.platform }}
|
|
||||||
uses: tauri-apps/tauri-action@v0
|
uses: tauri-apps/tauri-action@v0
|
||||||
if: matrix.target != 'i686-pc-windows-msvc'
|
|
||||||
env:
|
env:
|
||||||
CI: false
|
CI: false
|
||||||
PLATFORM: ${{ matrix.platform }}
|
PLATFORM: ${{ matrix.platform }}
|
||||||
@@ -127,31 +157,8 @@ jobs:
|
|||||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||||
with:
|
with:
|
||||||
tagName: ${{ github.ref_name }}
|
tagName: ${{ github.ref_name }}
|
||||||
releaseName: Coco ${{ needs.create-release.outputs.APP_VERSION }}
|
releaseName: Coco ${{ env.APP_VERSION }}
|
||||||
releaseBody: ""
|
releaseBody: "${{ env.RELEASE_BODY }}"
|
||||||
releaseDraft: true
|
releaseDraft: true
|
||||||
prerelease: false
|
prerelease: false
|
||||||
args: --target ${{ matrix.target }} --features use_pizza_engine
|
args: ${{ env.BUILD_ARGS }}
|
||||||
|
|
||||||
- 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 }}
|
|
||||||
|
|||||||
@@ -1,21 +1,35 @@
|
|||||||
---
|
---
|
||||||
weight: 10
|
weight: 10
|
||||||
title: "Mac OS"
|
title: "macOS"
|
||||||
asciinema: true
|
asciinema: true
|
||||||
---
|
---
|
||||||
|
|
||||||
# Mac OS
|
# macOS
|
||||||
|
|
||||||
## Download Coco AI
|
## Download Coco AI
|
||||||
|
|
||||||
Goto [https://coco.rs/](https://coco.rs/)
|
Go to [coco.rs](https://coco.rs/) and download the package of your architecture:
|
||||||
|
|
||||||
{{% load-img "/img/download-mac-app.png" "" %}}
|
{{% load-img "/img/macos/mac-download-app.png" "" %}}
|
||||||
|
|
||||||
|
It should be placed in your "Downloads" folder:
|
||||||
|
|
||||||
|
{{% load-img "/img/macos/mac-zip-file.png" "" %}}
|
||||||
|
|
||||||
## Unzip DMG file
|
## Unzip DMG file
|
||||||
|
|
||||||
{{% load-img "/img/unzip-dmg-file.png" "" %}}
|
Unzip the file:
|
||||||
|
|
||||||
|
{{% load-img "/img/macos/mac-unzip-zip-file.png" "" %}}
|
||||||
|
|
||||||
|
You will get a `dmg` file:
|
||||||
|
|
||||||
|
{{% load-img "/img/macos/mac-dmg.png" "" %}}
|
||||||
|
|
||||||
## Drag to Application Folder
|
## Drag to Application Folder
|
||||||
|
|
||||||
{{% load-img "/img/drag-to-application-folder.png" "" %}}
|
Double click the `dmg` file, a window will pop up. Then drag the "Coco-AI" app to
|
||||||
|
your "Applications" folder:
|
||||||
|
|
||||||
|
{{% load-img "/img/macos/drag-to-app-folder.png" "" %}}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,9 @@ asciinema: true
|
|||||||
[if_x11]: https://unix.stackexchange.com/q/202891/498440
|
[if_x11]: https://unix.stackexchange.com/q/202891/498440
|
||||||
|
|
||||||
|
|
||||||
## Goto [https://coco.rs/](https://coco.rs/)
|
## Go to the download page
|
||||||
|
|
||||||
|
Download page: [link](https://coco.rs/#install)
|
||||||
|
|
||||||
## Download the package
|
## Download the package
|
||||||
|
|
||||||
|
|||||||
@@ -8,12 +8,43 @@ title: "Release Notes"
|
|||||||
Information about release notes of Coco Server is provided here.
|
Information about release notes of Coco Server is provided here.
|
||||||
|
|
||||||
## Latest (In development)
|
## Latest (In development)
|
||||||
|
|
||||||
### ❌ Breaking changes
|
### ❌ Breaking changes
|
||||||
### 🚀 Features
|
### 🚀 Features
|
||||||
### 🐛 Bug fix
|
### 🐛 Bug fix
|
||||||
### ✈️ Improvements
|
### ✈️ Improvements
|
||||||
|
|
||||||
## 0.5.1 (2025-05-31)
|
## 0.6.0 (2025-06-29)
|
||||||
|
|
||||||
|
### ❌ Breaking changes
|
||||||
|
|
||||||
|
### 🚀 Features
|
||||||
|
|
||||||
|
- 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: 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
|
||||||
|
|
||||||
|
- 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.0 (2025-06-13)
|
||||||
|
|
||||||
### ❌ Breaking changes
|
### ❌ Breaking changes
|
||||||
|
|
||||||
@@ -34,6 +65,13 @@ Information about release notes of Coco Server is provided here.
|
|||||||
- feat: dynamic log level via env var COCO_LOG #535
|
- feat: dynamic log level via env var COCO_LOG #535
|
||||||
- feat: add quick AI access to search mode #556
|
- feat: add quick AI access to search mode #556
|
||||||
- feat: rerank search results #561
|
- 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
|
### 🐛 Bug fix
|
||||||
|
|
||||||
@@ -55,6 +93,23 @@ Information about release notes of Coco Server is provided here.
|
|||||||
- fix: independent chat window has no data #554
|
- fix: independent chat window has no data #554
|
||||||
- fix: resolved navigation error on continue chat action #558
|
- fix: resolved navigation error on continue chat action #558
|
||||||
- fix: make extension search source respect parameter datasource #576
|
- 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
|
### ✈️ Improvements
|
||||||
|
|
||||||
@@ -89,6 +144,12 @@ Information about release notes of Coco Server is provided here.
|
|||||||
- chore: mark unavailable server to offline on refresh info #569
|
- chore: mark unavailable server to offline on refresh info #569
|
||||||
- chore: only show available servers in chat #570
|
- chore: only show available servers in chat #570
|
||||||
- refactor: search result related components #571
|
- 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)
|
## 0.4.0 (2025-04-27)
|
||||||
|
|
||||||
|
|||||||
BIN
docs/static/img/download-mac-app.png
vendored
|
Before Width: | Height: | Size: 155 KiB |
BIN
docs/static/img/drag-to-application-folder.png
vendored
|
Before Width: | Height: | Size: 69 KiB |
BIN
docs/static/img/macos/drag-to-app-folder.png
vendored
Normal file
|
After Width: | Height: | Size: 239 KiB |
BIN
docs/static/img/macos/mac-dmg.png
vendored
Normal file
|
After Width: | Height: | Size: 586 KiB |
BIN
docs/static/img/macos/mac-download-app.png
vendored
Normal file
|
After Width: | Height: | Size: 299 KiB |
BIN
docs/static/img/macos/mac-unzip-zip-file.png
vendored
Normal file
|
After Width: | Height: | Size: 650 KiB |
BIN
docs/static/img/macos/mac-zip-file.png
vendored
Normal file
|
After Width: | Height: | Size: 441 KiB |
BIN
docs/static/img/unzip-dmg-file.png
vendored
|
Before Width: | Height: | Size: 121 KiB |
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "coco",
|
"name": "coco",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.5.2",
|
"version": "0.6.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
@@ -27,6 +27,7 @@
|
|||||||
"@tauri-apps/plugin-global-shortcut": "~2.0.0",
|
"@tauri-apps/plugin-global-shortcut": "~2.0.0",
|
||||||
"@tauri-apps/plugin-http": "~2.0.2",
|
"@tauri-apps/plugin-http": "~2.0.2",
|
||||||
"@tauri-apps/plugin-log": "~2.4.0",
|
"@tauri-apps/plugin-log": "~2.4.0",
|
||||||
|
"@tauri-apps/plugin-opener": "^2.2.7",
|
||||||
"@tauri-apps/plugin-os": "^2.2.1",
|
"@tauri-apps/plugin-os": "^2.2.1",
|
||||||
"@tauri-apps/plugin-process": "^2.2.1",
|
"@tauri-apps/plugin-process": "^2.2.1",
|
||||||
"@tauri-apps/plugin-shell": "^2.2.1",
|
"@tauri-apps/plugin-shell": "^2.2.1",
|
||||||
@@ -44,6 +45,7 @@
|
|||||||
"i18next-browser-languagedetector": "^8.1.0",
|
"i18next-browser-languagedetector": "^8.1.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"lucide-react": "^0.461.0",
|
"lucide-react": "^0.461.0",
|
||||||
|
"mdast-util-gfm-autolink-literal": "2.0.0",
|
||||||
"mermaid": "^11.6.0",
|
"mermaid": "^11.6.0",
|
||||||
"nanoid": "^5.1.5",
|
"nanoid": "^5.1.5",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
@@ -58,6 +60,7 @@
|
|||||||
"remark-breaks": "^4.0.0",
|
"remark-breaks": "^4.0.0",
|
||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
"remark-math": "^6.0.0",
|
"remark-math": "^6.0.0",
|
||||||
|
"tailwind-merge": "^3.3.1",
|
||||||
"tauri-plugin-fs-pro-api": "^2.4.0",
|
"tauri-plugin-fs-pro-api": "^2.4.0",
|
||||||
"tauri-plugin-macos-permissions-api": "^2.3.0",
|
"tauri-plugin-macos-permissions-api": "^2.3.0",
|
||||||
"tauri-plugin-screenshots-api": "^2.2.0",
|
"tauri-plugin-screenshots-api": "^2.2.0",
|
||||||
|
|||||||
83
pnpm-lock.yaml
generated
@@ -35,6 +35,9 @@ importers:
|
|||||||
'@tauri-apps/plugin-log':
|
'@tauri-apps/plugin-log':
|
||||||
specifier: ~2.4.0
|
specifier: ~2.4.0
|
||||||
version: 2.4.0
|
version: 2.4.0
|
||||||
|
'@tauri-apps/plugin-opener':
|
||||||
|
specifier: ^2.2.7
|
||||||
|
version: 2.2.7
|
||||||
'@tauri-apps/plugin-os':
|
'@tauri-apps/plugin-os':
|
||||||
specifier: ^2.2.1
|
specifier: ^2.2.1
|
||||||
version: 2.2.1
|
version: 2.2.1
|
||||||
@@ -86,6 +89,9 @@ importers:
|
|||||||
lucide-react:
|
lucide-react:
|
||||||
specifier: ^0.461.0
|
specifier: ^0.461.0
|
||||||
version: 0.461.0(react@18.3.1)
|
version: 0.461.0(react@18.3.1)
|
||||||
|
mdast-util-gfm-autolink-literal:
|
||||||
|
specifier: 2.0.0
|
||||||
|
version: 2.0.0
|
||||||
mermaid:
|
mermaid:
|
||||||
specifier: ^11.6.0
|
specifier: ^11.6.0
|
||||||
version: 11.6.0
|
version: 11.6.0
|
||||||
@@ -128,6 +134,9 @@ importers:
|
|||||||
remark-math:
|
remark-math:
|
||||||
specifier: ^6.0.0
|
specifier: ^6.0.0
|
||||||
version: 6.0.0
|
version: 6.0.0
|
||||||
|
tailwind-merge:
|
||||||
|
specifier: ^3.3.1
|
||||||
|
version: 3.3.1
|
||||||
tauri-plugin-fs-pro-api:
|
tauri-plugin-fs-pro-api:
|
||||||
specifier: ^2.4.0
|
specifier: ^2.4.0
|
||||||
version: 2.4.0
|
version: 2.4.0
|
||||||
@@ -185,7 +194,7 @@ importers:
|
|||||||
version: 1.8.8
|
version: 1.8.8
|
||||||
'@vitejs/plugin-react':
|
'@vitejs/plugin-react':
|
||||||
specifier: ^4.4.1
|
specifier: ^4.4.1
|
||||||
version: 4.4.1(vite@5.4.19(@types/node@22.15.17)(sass@1.87.0))
|
version: 4.4.1(vite@5.4.19(@types/node@22.15.17)(sass@1.87.0)(terser@5.40.0))
|
||||||
autoprefixer:
|
autoprefixer:
|
||||||
specifier: ^10.4.21
|
specifier: ^10.4.21
|
||||||
version: 10.4.21(postcss@8.5.3)
|
version: 10.4.21(postcss@8.5.3)
|
||||||
@@ -218,7 +227,7 @@ importers:
|
|||||||
version: 5.8.3
|
version: 5.8.3
|
||||||
vite:
|
vite:
|
||||||
specifier: ^5.4.19
|
specifier: ^5.4.19
|
||||||
version: 5.4.19(@types/node@22.15.17)(sass@1.87.0)
|
version: 5.4.19(@types/node@22.15.17)(sass@1.87.0)(terser@5.40.0)
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@@ -816,6 +825,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==}
|
resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==}
|
||||||
engines: {node: '>=6.0.0'}
|
engines: {node: '>=6.0.0'}
|
||||||
|
|
||||||
|
'@jridgewell/source-map@0.3.6':
|
||||||
|
resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==}
|
||||||
|
|
||||||
'@jridgewell/sourcemap-codec@1.5.0':
|
'@jridgewell/sourcemap-codec@1.5.0':
|
||||||
resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==}
|
resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==}
|
||||||
|
|
||||||
@@ -1259,6 +1271,9 @@ packages:
|
|||||||
'@tauri-apps/plugin-log@2.4.0':
|
'@tauri-apps/plugin-log@2.4.0':
|
||||||
resolution: {integrity: sha512-j7yrDtLNmayCBOO2esl3aZv9jSXy2an8MDLry3Ys9ZXerwUg35n1Y2uD8HoCR+8Ng/EUgx215+qOUfJasjYrHw==}
|
resolution: {integrity: sha512-j7yrDtLNmayCBOO2esl3aZv9jSXy2an8MDLry3Ys9ZXerwUg35n1Y2uD8HoCR+8Ng/EUgx215+qOUfJasjYrHw==}
|
||||||
|
|
||||||
|
'@tauri-apps/plugin-opener@2.2.7':
|
||||||
|
resolution: {integrity: sha512-uduEyvOdjpPOEeDRrhwlCspG/f9EQalHumWBtLBnp3fRp++fKGLqDOyUhSIn7PzX45b/rKep//ZQSAQoIxobLA==}
|
||||||
|
|
||||||
'@tauri-apps/plugin-os@2.2.1':
|
'@tauri-apps/plugin-os@2.2.1':
|
||||||
resolution: {integrity: sha512-cNYpNri2CCc6BaNeB6G/mOtLvg8dFyFQyCUdf2y0K8PIAKGEWdEcu8DECkydU2B+oj4OJihDPD2de5K6cbVl9A==}
|
resolution: {integrity: sha512-cNYpNri2CCc6BaNeB6G/mOtLvg8dFyFQyCUdf2y0K8PIAKGEWdEcu8DECkydU2B+oj4OJihDPD2de5K6cbVl9A==}
|
||||||
|
|
||||||
@@ -1586,6 +1601,9 @@ packages:
|
|||||||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
buffer-from@1.1.2:
|
||||||
|
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
||||||
|
|
||||||
bundle-name@4.1.0:
|
bundle-name@4.1.0:
|
||||||
resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==}
|
resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -1698,6 +1716,9 @@ packages:
|
|||||||
comma-separated-tokens@2.0.3:
|
comma-separated-tokens@2.0.3:
|
||||||
resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
|
resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
|
||||||
|
|
||||||
|
commander@2.20.3:
|
||||||
|
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
|
||||||
|
|
||||||
commander@4.1.1:
|
commander@4.1.1:
|
||||||
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
|
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
@@ -2643,8 +2664,8 @@ packages:
|
|||||||
mdast-util-from-markdown@2.0.2:
|
mdast-util-from-markdown@2.0.2:
|
||||||
resolution: {integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==}
|
resolution: {integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==}
|
||||||
|
|
||||||
mdast-util-gfm-autolink-literal@2.0.1:
|
mdast-util-gfm-autolink-literal@2.0.0:
|
||||||
resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==}
|
resolution: {integrity: sha512-FyzMsduZZHSc3i0Px3PQcBT4WJY/X/RCtEJKuybiC6sjPqLv7h1yqAkmILZtuxMSsUyaLUWNp71+vQH2zqp5cg==}
|
||||||
|
|
||||||
mdast-util-gfm-footnote@2.1.0:
|
mdast-util-gfm-footnote@2.1.0:
|
||||||
resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==}
|
resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==}
|
||||||
@@ -3349,6 +3370,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
source-map-support@0.5.21:
|
||||||
|
resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==}
|
||||||
|
|
||||||
source-map@0.6.1:
|
source-map@0.6.1:
|
||||||
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
|
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -3426,6 +3450,9 @@ packages:
|
|||||||
tabbable@6.2.0:
|
tabbable@6.2.0:
|
||||||
resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==}
|
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:
|
tailwindcss@3.4.17:
|
||||||
resolution: {integrity: sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==}
|
resolution: {integrity: sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==}
|
||||||
engines: {node: '>=14.0.0'}
|
engines: {node: '>=14.0.0'}
|
||||||
@@ -3443,6 +3470,11 @@ packages:
|
|||||||
tauri-plugin-windows-version-api@2.0.0:
|
tauri-plugin-windows-version-api@2.0.0:
|
||||||
resolution: {integrity: sha512-tty5n4ASYbXpnsD5ws2iTcTTpDCrSbzRTVp5Bo3UTpYGqlN1gBn2Zk8s3oO4w7VIM5WtJhDM9Jr/UgoTk7tFJQ==}
|
resolution: {integrity: sha512-tty5n4ASYbXpnsD5ws2iTcTTpDCrSbzRTVp5Bo3UTpYGqlN1gBn2Zk8s3oO4w7VIM5WtJhDM9Jr/UgoTk7tFJQ==}
|
||||||
|
|
||||||
|
terser@5.40.0:
|
||||||
|
resolution: {integrity: sha512-cfeKl/jjwSR5ar7d0FGmave9hFGJT8obyo0z+CrQOylLDbk7X81nPU6vq9VORa5jU30SkDnT2FXjLbR8HLP+xA==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
thenify-all@1.6.0:
|
thenify-all@1.6.0:
|
||||||
resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
|
resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
|
||||||
engines: {node: '>=0.8'}
|
engines: {node: '>=0.8'}
|
||||||
@@ -4263,6 +4295,12 @@ snapshots:
|
|||||||
|
|
||||||
'@jridgewell/set-array@1.2.1': {}
|
'@jridgewell/set-array@1.2.1': {}
|
||||||
|
|
||||||
|
'@jridgewell/source-map@0.3.6':
|
||||||
|
dependencies:
|
||||||
|
'@jridgewell/gen-mapping': 0.3.8
|
||||||
|
'@jridgewell/trace-mapping': 0.3.25
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@jridgewell/sourcemap-codec@1.5.0': {}
|
'@jridgewell/sourcemap-codec@1.5.0': {}
|
||||||
|
|
||||||
'@jridgewell/trace-mapping@0.3.25':
|
'@jridgewell/trace-mapping@0.3.25':
|
||||||
@@ -4640,6 +4678,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@tauri-apps/api': 2.5.0
|
'@tauri-apps/api': 2.5.0
|
||||||
|
|
||||||
|
'@tauri-apps/plugin-opener@2.2.7':
|
||||||
|
dependencies:
|
||||||
|
'@tauri-apps/api': 2.5.0
|
||||||
|
|
||||||
'@tauri-apps/plugin-os@2.2.1':
|
'@tauri-apps/plugin-os@2.2.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tauri-apps/api': 2.5.0
|
'@tauri-apps/api': 2.5.0
|
||||||
@@ -4881,14 +4923,14 @@ snapshots:
|
|||||||
|
|
||||||
'@ungap/structured-clone@1.3.0': {}
|
'@ungap/structured-clone@1.3.0': {}
|
||||||
|
|
||||||
'@vitejs/plugin-react@4.4.1(vite@5.4.19(@types/node@22.15.17)(sass@1.87.0))':
|
'@vitejs/plugin-react@4.4.1(vite@5.4.19(@types/node@22.15.17)(sass@1.87.0)(terser@5.40.0))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.27.1
|
'@babel/core': 7.27.1
|
||||||
'@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.27.1)
|
'@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.27.1)
|
||||||
'@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.27.1)
|
'@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.27.1)
|
||||||
'@types/babel__core': 7.20.5
|
'@types/babel__core': 7.20.5
|
||||||
react-refresh: 0.17.0
|
react-refresh: 0.17.0
|
||||||
vite: 5.4.19(@types/node@22.15.17)(sass@1.87.0)
|
vite: 5.4.19(@types/node@22.15.17)(sass@1.87.0)(terser@5.40.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -5017,6 +5059,9 @@ snapshots:
|
|||||||
node-releases: 2.0.19
|
node-releases: 2.0.19
|
||||||
update-browserslist-db: 1.1.3(browserslist@4.24.5)
|
update-browserslist-db: 1.1.3(browserslist@4.24.5)
|
||||||
|
|
||||||
|
buffer-from@1.1.2:
|
||||||
|
optional: true
|
||||||
|
|
||||||
bundle-name@4.1.0:
|
bundle-name@4.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
run-applescript: 7.0.0
|
run-applescript: 7.0.0
|
||||||
@@ -5113,6 +5158,9 @@ snapshots:
|
|||||||
|
|
||||||
comma-separated-tokens@2.0.3: {}
|
comma-separated-tokens@2.0.3: {}
|
||||||
|
|
||||||
|
commander@2.20.3:
|
||||||
|
optional: true
|
||||||
|
|
||||||
commander@4.1.1: {}
|
commander@4.1.1: {}
|
||||||
|
|
||||||
commander@7.2.0: {}
|
commander@7.2.0: {}
|
||||||
@@ -6117,7 +6165,7 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
mdast-util-gfm-autolink-literal@2.0.1:
|
mdast-util-gfm-autolink-literal@2.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/mdast': 4.0.4
|
'@types/mdast': 4.0.4
|
||||||
ccount: 2.0.1
|
ccount: 2.0.1
|
||||||
@@ -6165,7 +6213,7 @@ snapshots:
|
|||||||
mdast-util-gfm@3.1.0:
|
mdast-util-gfm@3.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
mdast-util-from-markdown: 2.0.2
|
mdast-util-from-markdown: 2.0.2
|
||||||
mdast-util-gfm-autolink-literal: 2.0.1
|
mdast-util-gfm-autolink-literal: 2.0.0
|
||||||
mdast-util-gfm-footnote: 2.1.0
|
mdast-util-gfm-footnote: 2.1.0
|
||||||
mdast-util-gfm-strikethrough: 2.0.0
|
mdast-util-gfm-strikethrough: 2.0.0
|
||||||
mdast-util-gfm-table: 2.0.0
|
mdast-util-gfm-table: 2.0.0
|
||||||
@@ -7124,6 +7172,12 @@ snapshots:
|
|||||||
|
|
||||||
source-map-js@1.2.1: {}
|
source-map-js@1.2.1: {}
|
||||||
|
|
||||||
|
source-map-support@0.5.21:
|
||||||
|
dependencies:
|
||||||
|
buffer-from: 1.1.2
|
||||||
|
source-map: 0.6.1
|
||||||
|
optional: true
|
||||||
|
|
||||||
source-map@0.6.1:
|
source-map@0.6.1:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -7200,6 +7254,8 @@ snapshots:
|
|||||||
|
|
||||||
tabbable@6.2.0: {}
|
tabbable@6.2.0: {}
|
||||||
|
|
||||||
|
tailwind-merge@3.3.1: {}
|
||||||
|
|
||||||
tailwindcss@3.4.17:
|
tailwindcss@3.4.17:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@alloc/quick-lru': 5.2.0
|
'@alloc/quick-lru': 5.2.0
|
||||||
@@ -7243,6 +7299,14 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@tauri-apps/api': 2.5.0
|
'@tauri-apps/api': 2.5.0
|
||||||
|
|
||||||
|
terser@5.40.0:
|
||||||
|
dependencies:
|
||||||
|
'@jridgewell/source-map': 0.3.6
|
||||||
|
acorn: 8.14.1
|
||||||
|
commander: 2.20.3
|
||||||
|
source-map-support: 0.5.21
|
||||||
|
optional: true
|
||||||
|
|
||||||
thenify-all@1.6.0:
|
thenify-all@1.6.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
thenify: 3.3.1
|
thenify: 3.3.1
|
||||||
@@ -7429,7 +7493,7 @@ snapshots:
|
|||||||
'@types/unist': 3.0.3
|
'@types/unist': 3.0.3
|
||||||
vfile-message: 4.0.2
|
vfile-message: 4.0.2
|
||||||
|
|
||||||
vite@5.4.19(@types/node@22.15.17)(sass@1.87.0):
|
vite@5.4.19(@types/node@22.15.17)(sass@1.87.0)(terser@5.40.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.21.5
|
esbuild: 0.21.5
|
||||||
postcss: 8.5.3
|
postcss: 8.5.3
|
||||||
@@ -7438,6 +7502,7 @@ snapshots:
|
|||||||
'@types/node': 22.15.17
|
'@types/node': 22.15.17
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
sass: 1.87.0
|
sass: 1.87.0
|
||||||
|
terser: 5.40.0
|
||||||
|
|
||||||
void-elements@3.1.0: {}
|
void-elements@3.1.0: {}
|
||||||
|
|
||||||
|
|||||||
1
scripts/devWeb.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
(() => {})();
|
||||||
248
src-tauri/Cargo.lock
generated
@@ -17,6 +17,17 @@ version = "2.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
|
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]]
|
[[package]]
|
||||||
name = "ahash"
|
name = "ahash"
|
||||||
version = "0.7.8"
|
version = "0.7.8"
|
||||||
@@ -518,6 +529,26 @@ dependencies = [
|
|||||||
"piper",
|
"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]]
|
[[package]]
|
||||||
name = "borsh"
|
name = "borsh"
|
||||||
version = "1.5.7"
|
version = "1.5.7"
|
||||||
@@ -634,6 +665,25 @@ dependencies = [
|
|||||||
"serde",
|
"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]]
|
[[package]]
|
||||||
name = "cairo-rs"
|
name = "cairo-rs"
|
||||||
version = "0.18.5"
|
version = "0.18.5"
|
||||||
@@ -794,6 +844,16 @@ dependencies = [
|
|||||||
"windows-link",
|
"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]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "3.2.25"
|
version = "3.2.25"
|
||||||
@@ -821,12 +881,14 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "coco"
|
name = "coco"
|
||||||
version = "0.5.2"
|
version = "0.6.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"applications",
|
"applications",
|
||||||
|
"async-recursion",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"base64 0.13.1",
|
"base64 0.13.1",
|
||||||
|
"borrowme",
|
||||||
"chinese-number",
|
"chinese-number",
|
||||||
"chrono",
|
"chrono",
|
||||||
"derive_more 2.0.1",
|
"derive_more 2.0.1",
|
||||||
@@ -866,6 +928,7 @@ dependencies = [
|
|||||||
"tauri-plugin-http",
|
"tauri-plugin-http",
|
||||||
"tauri-plugin-log",
|
"tauri-plugin-log",
|
||||||
"tauri-plugin-macos-permissions",
|
"tauri-plugin-macos-permissions",
|
||||||
|
"tauri-plugin-opener",
|
||||||
"tauri-plugin-os",
|
"tauri-plugin-os",
|
||||||
"tauri-plugin-process",
|
"tauri-plugin-process",
|
||||||
"tauri-plugin-screenshots",
|
"tauri-plugin-screenshots",
|
||||||
@@ -883,6 +946,7 @@ dependencies = [
|
|||||||
"tungstenite 0.24.0",
|
"tungstenite 0.24.0",
|
||||||
"url",
|
"url",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
|
"zip 4.0.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -990,6 +1054,12 @@ dependencies = [
|
|||||||
"tiny-keccak",
|
"tiny-keccak",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "constant_time_eq"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "convert_case"
|
name = "convert_case"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
@@ -1262,6 +1332,12 @@ dependencies = [
|
|||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "deflate64"
|
||||||
|
version = "0.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "deranged"
|
name = "deranged"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
@@ -1325,6 +1401,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"block-buffer",
|
"block-buffer",
|
||||||
"crypto-common",
|
"crypto-common",
|
||||||
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1767,6 +1844,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece"
|
checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crc32fast",
|
"crc32fast",
|
||||||
|
"libz-rs-sys",
|
||||||
"miniz_oxide",
|
"miniz_oxide",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2504,6 +2582,15 @@ version = "0.4.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hmac"
|
||||||
|
version = "0.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
|
||||||
|
dependencies = [
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hostname"
|
name = "hostname"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
@@ -2947,6 +3034,15 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "inout"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "interpolate_name"
|
name = "interpolate_name"
|
||||||
version = "0.2.4"
|
version = "0.2.4"
|
||||||
@@ -3212,6 +3308,26 @@ dependencies = [
|
|||||||
"winapi",
|
"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]]
|
[[package]]
|
||||||
name = "libredox"
|
name = "libredox"
|
||||||
version = "0.1.3"
|
version = "0.1.3"
|
||||||
@@ -3223,6 +3339,15 @@ dependencies = [
|
|||||||
"redox_syscall",
|
"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]]
|
[[package]]
|
||||||
name = "linked-hash-map"
|
name = "linked-hash-map"
|
||||||
version = "0.5.6"
|
version = "0.5.6"
|
||||||
@@ -4270,6 +4395,16 @@ version = "0.2.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
|
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]]
|
[[package]]
|
||||||
name = "percent-encoding"
|
name = "percent-encoding"
|
||||||
version = "2.3.1"
|
version = "2.3.1"
|
||||||
@@ -5438,6 +5573,7 @@ version = "1.0.140"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
|
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"indexmap 2.9.0",
|
||||||
"itoa 1.0.15",
|
"itoa 1.0.15",
|
||||||
"memchr",
|
"memchr",
|
||||||
"ryu",
|
"ryu",
|
||||||
@@ -6279,6 +6415,28 @@ dependencies = [
|
|||||||
"thiserror 2.0.12",
|
"thiserror 2.0.12",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-plugin-opener"
|
||||||
|
version = "2.2.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "66644b71a31ec1a8a52c4a16575edd28cf763c87cf4a7da24c884122b5c77097"
|
||||||
|
dependencies = [
|
||||||
|
"dunce",
|
||||||
|
"glob",
|
||||||
|
"objc2-app-kit 0.3.1",
|
||||||
|
"objc2-foundation 0.3.1",
|
||||||
|
"open",
|
||||||
|
"schemars",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tauri",
|
||||||
|
"tauri-plugin",
|
||||||
|
"thiserror 2.0.12",
|
||||||
|
"url",
|
||||||
|
"windows 0.61.1",
|
||||||
|
"zbus",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-os"
|
name = "tauri-plugin-os"
|
||||||
version = "2.2.1"
|
version = "2.2.1"
|
||||||
@@ -6401,7 +6559,7 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
"url",
|
"url",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
"zip",
|
"zip 2.6.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -8426,6 +8584,20 @@ name = "zeroize"
|
|||||||
version = "1.8.1"
|
version = "1.8.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
|
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]]
|
[[package]]
|
||||||
name = "zerotrie"
|
name = "zerotrie"
|
||||||
@@ -8473,6 +8645,78 @@ dependencies = [
|
|||||||
"memchr",
|
"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]]
|
[[package]]
|
||||||
name = "zune-core"
|
name = "zune-core"
|
||||||
version = "0.4.12"
|
version = "0.4.12"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "coco"
|
name = "coco"
|
||||||
version = "0.5.2"
|
version = "0.6.0"
|
||||||
description = "Search, connect, collaborate – all in one place."
|
description = "Search, connect, collaborate – all in one place."
|
||||||
authors = ["INFINI Labs"]
|
authors = ["INFINI Labs"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
@@ -44,12 +44,12 @@ use_pizza_engine = []
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
pizza-common = { git = "https://github.com/infinilabs/pizza-common", branch = "main" }
|
pizza-common = { git = "https://github.com/infinilabs/pizza-common", branch = "main" }
|
||||||
|
|
||||||
tauri = { version = "2", features = ["protocol-asset", "macos-private-api", "tray-icon", "image-ico", "image-png", "unstable"] }
|
tauri = { version = "2", features = ["protocol-asset", "macos-private-api", "tray-icon", "image-ico", "image-png"] }
|
||||||
tauri-plugin-shell = "2"
|
tauri-plugin-shell = "2"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
# Need `arbitrary_precision` feature to support storing u128
|
# Need `arbitrary_precision` feature to support storing u128
|
||||||
# see: https://docs.rs/serde_json/latest/serde_json/struct.Number.html#method.from_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-http = "2"
|
||||||
tauri-plugin-websocket = "2"
|
tauri-plugin-websocket = "2"
|
||||||
tauri-plugin-deep-link = "2.0.0"
|
tauri-plugin-deep-link = "2.0.0"
|
||||||
@@ -83,7 +83,6 @@ walkdir = "2"
|
|||||||
log = "0.4"
|
log = "0.4"
|
||||||
strsim = "0.10"
|
strsim = "0.10"
|
||||||
futures-util = "0.3.31"
|
futures-util = "0.3.31"
|
||||||
url = "2.5.2"
|
|
||||||
http = "1.1.0"
|
http = "1.1.0"
|
||||||
tungstenite = "0.24.0"
|
tungstenite = "0.24.0"
|
||||||
tokio-util = "0.7.14"
|
tokio-util = "0.7.14"
|
||||||
@@ -98,6 +97,11 @@ derive_more = { version = "2.0.1", features = ["display"] }
|
|||||||
anyhow = "1.0.98"
|
anyhow = "1.0.98"
|
||||||
function_name = "0.3.0"
|
function_name = "0.3.0"
|
||||||
regex = "1.11.1"
|
regex = "1.11.1"
|
||||||
|
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]
|
[target."cfg(target_os = \"macos\")".dependencies]
|
||||||
tauri-nspanel = { git = "https://github.com/ahkohd/tauri-nspanel", branch = "v2" }
|
tauri-nspanel = { git = "https://github.com/ahkohd/tauri-nspanel", branch = "v2" }
|
||||||
|
|||||||
@@ -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",
|
"$schema": "../gen/schemas/desktop-schema.json",
|
||||||
"identifier": "default",
|
"identifier": "default",
|
||||||
"description": "Capability for the main window",
|
"description": "Capability for the main window",
|
||||||
"windows": ["main", "chat", "settings"],
|
"windows": ["main", "chat", "settings", "check"],
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"core:default",
|
"core:default",
|
||||||
"core:event:allow-emit",
|
"core:event:allow-emit",
|
||||||
@@ -71,6 +71,7 @@
|
|||||||
"process:default",
|
"process:default",
|
||||||
"updater:default",
|
"updater:default",
|
||||||
"windows-version:default",
|
"windows-version:default",
|
||||||
"log:default"
|
"log:default",
|
||||||
|
"opener:default"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::common::assistant::ChatRequestMessage;
|
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::common::register::SearchSourceRegistry;
|
||||||
use crate::server::http_client::HttpClient;
|
use crate::server::http_client::HttpClient;
|
||||||
use crate::{common, server::servers::COCO_SERVERS};
|
use crate::{common, server::servers::COCO_SERVERS};
|
||||||
@@ -20,17 +20,15 @@ pub async fn chat_history<R: Runtime>(
|
|||||||
size: u32,
|
size: u32,
|
||||||
query: Option<String>,
|
query: Option<String>,
|
||||||
) -> Result<String, String> {
|
) -> Result<String, String> {
|
||||||
let mut query_params: HashMap<String, Value> = HashMap::new();
|
let mut query_params = Vec::new();
|
||||||
if from > 0 {
|
|
||||||
query_params.insert("from".to_string(), from.into());
|
// Add from/size as number values
|
||||||
}
|
query_params.push(format!("from={}", from));
|
||||||
if size > 0 {
|
query_params.push(format!("size={}", size));
|
||||||
query_params.insert("size".to_string(), size.into());
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(query) = query {
|
if let Some(query) = query {
|
||||||
if !query.is_empty() {
|
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,
|
from: u32,
|
||||||
size: u32,
|
size: u32,
|
||||||
) -> Result<String, String> {
|
) -> Result<String, String> {
|
||||||
let mut query_params: HashMap<String, Value> = HashMap::new();
|
let mut query_params = Vec::new();
|
||||||
if from > 0 {
|
|
||||||
query_params.insert("from".to_string(), from.into());
|
// Add from/size as number values
|
||||||
}
|
query_params.push(format!("from={}", from));
|
||||||
if size > 0 {
|
query_params.push(format!("size={}", size));
|
||||||
query_params.insert("size".to_string(), size.into());
|
|
||||||
}
|
|
||||||
|
|
||||||
let path = format!("/chat/{}/_history", session_id);
|
let path = format!("/chat/{}/_history", session_id);
|
||||||
|
|
||||||
@@ -75,10 +71,9 @@ pub async fn open_session_chat<R: Runtime>(
|
|||||||
server_id: String,
|
server_id: String,
|
||||||
session_id: String,
|
session_id: String,
|
||||||
) -> Result<String, String> {
|
) -> Result<String, String> {
|
||||||
let query_params = HashMap::new();
|
|
||||||
let path = format!("/chat/{}/_open", session_id);
|
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
|
.await
|
||||||
.map_err(|e| format!("Error open session: {}", e))?;
|
.map_err(|e| format!("Error open session: {}", e))?;
|
||||||
|
|
||||||
@@ -91,10 +86,9 @@ pub async fn close_session_chat<R: Runtime>(
|
|||||||
server_id: String,
|
server_id: String,
|
||||||
session_id: String,
|
session_id: String,
|
||||||
) -> Result<String, String> {
|
) -> Result<String, String> {
|
||||||
let query_params = HashMap::new();
|
|
||||||
let path = format!("/chat/{}/_close", session_id);
|
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
|
.await
|
||||||
.map_err(|e| format!("Error close session: {}", e))?;
|
.map_err(|e| format!("Error close session: {}", e))?;
|
||||||
|
|
||||||
@@ -106,10 +100,9 @@ pub async fn cancel_session_chat<R: Runtime>(
|
|||||||
server_id: String,
|
server_id: String,
|
||||||
session_id: String,
|
session_id: String,
|
||||||
) -> Result<String, String> {
|
) -> Result<String, String> {
|
||||||
let query_params = HashMap::new();
|
|
||||||
let path = format!("/chat/{}/_cancel", session_id);
|
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
|
.await
|
||||||
.map_err(|e| format!("Error cancel session: {}", e))?;
|
.map_err(|e| format!("Error cancel session: {}", e))?;
|
||||||
|
|
||||||
@@ -140,8 +133,13 @@ pub async fn new_chat<R: Runtime>(
|
|||||||
let mut headers = HashMap::new();
|
let mut headers = HashMap::new();
|
||||||
headers.insert("WEBSOCKET-SESSION-ID".to_string(), websocket_id.into());
|
headers.insert("WEBSOCKET-SESSION-ID".to_string(), websocket_id.into());
|
||||||
|
|
||||||
let response =
|
let response = HttpClient::advanced_post(
|
||||||
HttpClient::advanced_post(&server_id, "/chat/_new", Some(headers), query_params, body)
|
&server_id,
|
||||||
|
"/chat/_new",
|
||||||
|
Some(headers),
|
||||||
|
convert_query_params_to_strings(query_params),
|
||||||
|
body,
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("Error sending message: {}", e))?;
|
.map_err(|e| format!("Error sending message: {}", e))?;
|
||||||
|
|
||||||
@@ -181,13 +179,12 @@ pub async fn send_message<R: Runtime>(
|
|||||||
&server_id,
|
&server_id,
|
||||||
path.as_str(),
|
path.as_str(),
|
||||||
Some(headers),
|
Some(headers),
|
||||||
query_params,
|
convert_query_params_to_strings(query_params),
|
||||||
Some(body),
|
Some(body),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("Error cancel session: {}", e))?;
|
.map_err(|e| format!("Error cancel session: {}", e))?;
|
||||||
|
|
||||||
|
|
||||||
common::http::get_response_body_text(response).await
|
common::http::get_response_body_text(response).await
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,25 +235,9 @@ pub async fn update_session_chat(
|
|||||||
pub async fn assistant_search<R: Runtime>(
|
pub async fn assistant_search<R: Runtime>(
|
||||||
_app_handle: AppHandle<R>,
|
_app_handle: AppHandle<R>,
|
||||||
server_id: String,
|
server_id: String,
|
||||||
from: u32,
|
query_params: Option<Vec<String>>,
|
||||||
size: u32,
|
|
||||||
query: Option<HashMap<String, Value>>,
|
|
||||||
) -> Result<Value, String> {
|
) -> Result<Value, String> {
|
||||||
let mut body = serde_json::json!({
|
let response = HttpClient::post(&server_id, "/assistant/_search", query_params, None)
|
||||||
"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())),
|
|
||||||
)
|
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("Error searching assistants: {}", e))?;
|
.map_err(|e| format!("Error searching assistants: {}", e))?;
|
||||||
|
|
||||||
@@ -380,7 +361,8 @@ pub fn remove_icon_fields(json: &str) -> String {
|
|||||||
} else {
|
} else {
|
||||||
"".to_string()
|
"".to_string()
|
||||||
}
|
}
|
||||||
}).to_string()
|
})
|
||||||
|
.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
|||||||
@@ -3,38 +3,43 @@ use std::{fs::create_dir, io::Read};
|
|||||||
use tauri::{Manager, Runtime};
|
use tauri::{Manager, Runtime};
|
||||||
use tauri_plugin_autostart::ManagerExt;
|
use tauri_plugin_autostart::ManagerExt;
|
||||||
|
|
||||||
// Start or stop according to configuration
|
/// If the state reported from the OS and the state stored by us differ, our state is
|
||||||
pub fn enable_autostart(app: &mut tauri::App) {
|
/// prioritized and seen as the correct one. Update the OS state to make them consistent.
|
||||||
use tauri_plugin_autostart::MacosLauncher;
|
pub fn ensure_autostart_state_consistent(app: &mut tauri::App) -> Result<(), String> {
|
||||||
use tauri_plugin_autostart::ManagerExt;
|
|
||||||
|
|
||||||
app.handle()
|
|
||||||
.plugin(tauri_plugin_autostart::init(
|
|
||||||
MacosLauncher::AppleScript,
|
|
||||||
None,
|
|
||||||
))
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let autostart_manager = app.autolaunch();
|
let autostart_manager = app.autolaunch();
|
||||||
|
|
||||||
// close autostart
|
let os_state = autostart_manager.is_enabled().map_err(|e| e.to_string())?;
|
||||||
// autostart_manager.disable().unwrap();
|
let coco_stored_state = current_autostart(app.app_handle()).map_err(|e| e.to_string())?;
|
||||||
// return;
|
|
||||||
|
|
||||||
match (
|
if os_state != coco_stored_state {
|
||||||
autostart_manager.is_enabled(),
|
log::warn!(
|
||||||
current_autostart(app.app_handle()),
|
"autostart inconsistent states, OS state [{}], Coco state [{}], config file could be deleted or corrupted",
|
||||||
) {
|
os_state,
|
||||||
(Ok(false), Ok(true)) => match autostart_manager.enable() {
|
coco_stored_state
|
||||||
Ok(_) => println!("Autostart enabled successfully."),
|
);
|
||||||
Err(err) => eprintln!("Failed to enable autostart: {}", err),
|
log::info!("trying to correct the inconsistent states");
|
||||||
},
|
|
||||||
(Ok(true), Ok(false)) => match autostart_manager.disable() {
|
let result = if coco_stored_state {
|
||||||
Ok(_) => println!("Autostart disable successfully."),
|
autostart_manager.enable()
|
||||||
Err(err) => eprintln!("Failed to disable autostart: {}", err),
|
} else {
|
||||||
},
|
autostart_manager.disable()
|
||||||
_ => (),
|
};
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(_) => {
|
||||||
|
log::info!("inconsistent autostart states fixed");
|
||||||
}
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!(
|
||||||
|
"failed to fix inconsistent autostart state due to error [{}]",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
return Err(e.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn current_autostart<R: Runtime>(app: &tauri::AppHandle<R>) -> Result<bool, String> {
|
fn current_autostart<R: Runtime>(app: &tauri::AppHandle<R>) -> Result<bool, String> {
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use crate::hide_coco;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct RichLabel {
|
pub struct RichLabel {
|
||||||
pub label: Option<String>,
|
pub label: Option<String>,
|
||||||
@@ -53,7 +51,9 @@ impl OnOpened {
|
|||||||
const WHITESPACE: &str = " ";
|
const WHITESPACE: &str = " ";
|
||||||
let mut ret = action.exec.clone();
|
let mut ret = action.exec.clone();
|
||||||
ret.push_str(WHITESPACE);
|
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
|
ret
|
||||||
}
|
}
|
||||||
@@ -82,9 +82,25 @@ pub(crate) async fn open(on_opened: OnOpened) -> Result<(), String> {
|
|||||||
}
|
}
|
||||||
OnOpened::Command { action } => {
|
OnOpened::Command { action } => {
|
||||||
let mut cmd = Command::new(action.exec);
|
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())?;
|
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() {
|
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!(
|
return Err(format!(
|
||||||
"Command failed, stderr [{}]",
|
"Command failed, stderr [{}]",
|
||||||
String::from_utf8_lossy(&output.stderr)
|
String::from_utf8_lossy(&output.stderr)
|
||||||
@@ -93,7 +109,6 @@ pub(crate) async fn open(on_opened: OnOpened) -> Result<(), String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hide_coco(global_tauri_app_handle.clone()).await;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ use crate::common;
|
|||||||
use reqwest::Response;
|
use reqwest::Response;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use tauri_plugin_store::JsonValue;
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct GetResponse {
|
pub struct GetResponse {
|
||||||
@@ -54,3 +56,25 @@ pub async fn get_response_body_text(response: Response) -> Result<String, String
|
|||||||
Ok(body)
|
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 assistant;
|
||||||
pub mod http;
|
pub mod auth;
|
||||||
|
pub mod connector;
|
||||||
|
pub mod datasource;
|
||||||
|
pub mod document;
|
||||||
pub mod error;
|
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 MAIN_WINDOW_LABEL: &str = "main";
|
||||||
pub static SETTINGS_WINDOW_LABEL: &str = "settings";
|
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,
|
modified: u128,
|
||||||
last_opened: 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::writer::Writer;
|
||||||
use pizza_engine::{doc, Engine, EngineBuilder};
|
use pizza_engine::{doc, Engine, EngineBuilder};
|
||||||
use serde_json::Value as Json;
|
use serde_json::Value as Json;
|
||||||
|
use std::path::Path;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use tauri::{async_runtime, AppHandle, Manager, Runtime};
|
use tauri::{async_runtime, AppHandle, Manager, Runtime};
|
||||||
use tauri_plugin_fs_pro::{icon, metadata, name, IconOptions};
|
use tauri_plugin_fs_pro::{icon, metadata, name, IconOptions};
|
||||||
@@ -47,6 +48,7 @@ const TAURI_STORE_APP_ALIAS: &str = "app_alias";
|
|||||||
const TAURI_STORE_KEY_SEARCH_PATH: &str = "search_path";
|
const TAURI_STORE_KEY_SEARCH_PATH: &str = "search_path";
|
||||||
const TAURI_STORE_KEY_DISABLED_APP_LIST: &str = "disabled_app_list";
|
const TAURI_STORE_KEY_DISABLED_APP_LIST: &str = "disabled_app_list";
|
||||||
|
|
||||||
|
const INDEX_DIR: &str = "local_application_index";
|
||||||
|
|
||||||
/// We use this as:
|
/// We use this as:
|
||||||
///
|
///
|
||||||
@@ -210,31 +212,15 @@ impl SearchSourceState for ApplicationSearchSourceState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Upon application start, index all the applications found in the `get_default_search_paths()`.
|
/// Index applications if they have not been indexed (by checking if `app_index_dir` exists).
|
||||||
struct IndexAllApplicationsTask<R: Runtime> {
|
async fn index_applications_if_not_indexed<R: Runtime>(
|
||||||
tauri_app_handle: AppHandle<R>,
|
tauri_app_handle: &AppHandle<R>,
|
||||||
callback: Option<tokio::sync::oneshot::Sender<Result<(), String>>>,
|
app_index_dir: &Path,
|
||||||
}
|
) -> anyhow::Result<ApplicationSearchSourceState> {
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
|
||||||
impl<R: Runtime> Task for IndexAllApplicationsTask<R> {
|
|
||||||
fn search_source_id(&self) -> &'static str {
|
|
||||||
APPLICATION_SEARCH_SOURCE_ID
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn exec(&mut self, state: &mut Option<Box<dyn SearchSourceState>>) {
|
|
||||||
let callback = self.callback.take().unwrap();
|
|
||||||
let mut app_index_dir = self
|
|
||||||
.tauri_app_handle
|
|
||||||
.path()
|
|
||||||
.app_data_dir()
|
|
||||||
.expect("failed to find the local dir");
|
|
||||||
app_index_dir.push("local_application_index");
|
|
||||||
|
|
||||||
let index_exists = app_index_dir.exists();
|
let index_exists = app_index_dir.exists();
|
||||||
|
|
||||||
let mut pizza_engine_builder = EngineBuilder::new();
|
let mut pizza_engine_builder = EngineBuilder::new();
|
||||||
let disk_store = task_exec_try!(DiskStore::new(&app_index_dir), callback);
|
let disk_store = DiskStore::new(&app_index_dir)?;
|
||||||
pizza_engine_builder.set_data_store(disk_store);
|
pizza_engine_builder.set_data_store(disk_store);
|
||||||
|
|
||||||
let mut schema = Schema::new();
|
let mut schema = Schema::new();
|
||||||
@@ -260,19 +246,17 @@ impl<R: Runtime> Task for IndexAllApplicationsTask<R> {
|
|||||||
|
|
||||||
if !index_exists {
|
if !index_exists {
|
||||||
let default_search_path = get_default_search_paths();
|
let default_search_path = get_default_search_paths();
|
||||||
let apps = task_exec_try!(list_app_in(default_search_path), callback);
|
let apps = list_app_in(default_search_path).map_err(|str| anyhow::anyhow!(str))?;
|
||||||
|
|
||||||
for app in apps.iter() {
|
for app in apps.iter() {
|
||||||
let app_path = get_app_path(app);
|
let app_path = get_app_path(app);
|
||||||
let app_name = get_app_name(app).await;
|
let app_name = get_app_name(app).await;
|
||||||
let app_icon_path = task_exec_try!(
|
let app_icon_path = get_app_icon_path(&tauri_app_handle, app)
|
||||||
get_app_icon_path(&self.tauri_app_handle, app).await,
|
.await
|
||||||
callback
|
.map_err(|str| anyhow::anyhow!(str))?;
|
||||||
);
|
let app_alias = get_app_alias(&tauri_app_handle, &app_path).unwrap_or(String::new());
|
||||||
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) {
|
if app_name.is_empty() || app_name.eq(&tauri_app_handle.package_info().name) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -294,22 +278,80 @@ impl<R: Runtime> Task for IndexAllApplicationsTask<R> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
task_exec_try!(writer.commit(), callback);
|
writer.commit()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let snapshot = pizza_engine.create_snapshot();
|
let snapshot = pizza_engine.create_snapshot();
|
||||||
let searcher = pizza_engine.acquire_searcher();
|
let searcher = pizza_engine.acquire_searcher();
|
||||||
|
|
||||||
let state_to_store = Box::new(ApplicationSearchSourceState {
|
Ok(ApplicationSearchSourceState {
|
||||||
searcher,
|
searcher,
|
||||||
snapshot,
|
snapshot,
|
||||||
engine: pizza_engine,
|
engine: pizza_engine,
|
||||||
writer,
|
writer,
|
||||||
}) as Box<dyn SearchSourceState>;
|
})
|
||||||
|
}
|
||||||
|
|
||||||
*state = Some(state_to_store);
|
/// Upon application start, index all the applications found in the `get_default_search_paths()`.
|
||||||
|
struct IndexAllApplicationsTask<R: Runtime> {
|
||||||
|
tauri_app_handle: AppHandle<R>,
|
||||||
|
callback: Option<tokio::sync::oneshot::Sender<Result<(), String>>>,
|
||||||
|
}
|
||||||
|
|
||||||
callback.send(Ok(())).unwrap();
|
#[async_trait::async_trait(?Send)]
|
||||||
|
impl<R: Runtime> Task for IndexAllApplicationsTask<R> {
|
||||||
|
fn search_source_id(&self) -> &'static str {
|
||||||
|
APPLICATION_SEARCH_SOURCE_ID
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn exec(&mut self, state: &mut Option<Box<dyn SearchSourceState>>) {
|
||||||
|
let callback = self.callback.take().unwrap();
|
||||||
|
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);
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ReindexAllApplicationsTask<R: Runtime> {
|
||||||
|
tauri_app_handle: AppHandle<R>,
|
||||||
|
callback: Option<tokio::sync::oneshot::Sender<Result<(), String>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait(?Send)]
|
||||||
|
impl<R: Runtime> Task for ReindexAllApplicationsTask<R> {
|
||||||
|
fn search_source_id(&self) -> &'static str {
|
||||||
|
APPLICATION_SEARCH_SOURCE_ID
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn exec(&mut self, state: &mut Option<Box<dyn SearchSourceState>>) {
|
||||||
|
let callback = self.callback.take().unwrap();
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
// 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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -327,15 +369,41 @@ impl<R: Runtime> Task for SearchApplicationsTask<R> {
|
|||||||
|
|
||||||
async fn exec(&mut self, state: &mut Option<Box<dyn SearchSourceState>>) {
|
async fn exec(&mut self, state: &mut Option<Box<dyn SearchSourceState>>) {
|
||||||
let callback = self.callback.take().unwrap();
|
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);
|
let disabled_app_list = get_disabled_app_list(&self.tauri_app_handle);
|
||||||
|
|
||||||
// TODO: search via alias, implement this when Pizza engine supports update
|
// TODO: search via alias, implement this when Pizza engine supports update
|
||||||
|
//
|
||||||
|
// NOTE: we use the Debug impl rather than Display for `self.query_string` as String's Debug
|
||||||
|
// impl won't interrupt escape characters. So for input like:
|
||||||
|
//
|
||||||
|
// ```text
|
||||||
|
// Google
|
||||||
|
// Chrome
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// It will be passed to Pizza like "Google\nChrome". Using Display impl would result
|
||||||
|
// in an invalid query DSL and serde will complain.
|
||||||
let dsl = format!(
|
let dsl = format!(
|
||||||
"{{ \"query\": {{ \"bool\": {{ \"should\": [ {{ \"match\": {{ \"{FIELD_APP_NAME}\": \"{}\" }} }}, {{ \"prefix\": {{ \"{FIELD_APP_NAME}\": \"{}\" }} }} ] }} }} }}", self.query_string, self.query_string);
|
"{{ \"query\": {{ \"bool\": {{ \"should\": [ {{ \"match\": {{ \"{FIELD_APP_NAME}\": {:?} }} }}, {{ \"prefix\": {{ \"{FIELD_APP_NAME}\": {:?} }} }} ] }} }} }}", self.query_string, self.query_string);
|
||||||
|
|
||||||
let state = state
|
let state = state
|
||||||
.as_mut()
|
|
||||||
.expect("should be set before")
|
|
||||||
.as_mut_any()
|
.as_mut_any()
|
||||||
.downcast_mut::<ApplicationSearchSourceState>()
|
.downcast_mut::<ApplicationSearchSourceState>()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -418,7 +486,9 @@ impl Task for IndexNewApplicationsTask {
|
|||||||
pub struct ApplicationSearchSource;
|
pub struct ApplicationSearchSource;
|
||||||
|
|
||||||
impl 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 (tx, rx) = tokio::sync::oneshot::channel();
|
||||||
let index_applications_task = IndexAllApplicationsTask {
|
let index_applications_task = IndexAllApplicationsTask {
|
||||||
tauri_app_handle: app_handle.clone(),
|
tauri_app_handle: app_handle.clone(),
|
||||||
@@ -462,8 +532,6 @@ impl ApplicationSearchSource {
|
|||||||
.set(TAURI_STORE_KEY_SEARCH_PATH, default_search_path);
|
.set(TAURI_STORE_KEY_SEARCH_PATH, default_search_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
register_app_hotkey_upon_start(app_handle.clone())?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -628,14 +696,43 @@ fn app_hotkey_handler<R: Runtime>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn register_app_hotkey_upon_start<R: Runtime>(
|
/// For all the applications, if it is enabled & has hotkey set, then set it up.
|
||||||
tauri_app_handle: AppHandle<R>,
|
pub(crate) fn set_apps_hotkey<R: Runtime>(tauri_app_handle: &AppHandle<R>) -> Result<(), String> {
|
||||||
) -> Result<(), String> {
|
|
||||||
let app_hotkey_store = tauri_app_handle
|
let app_hotkey_store = tauri_app_handle
|
||||||
.store(TAURI_STORE_APP_HOTKEY)
|
.store(TAURI_STORE_APP_HOTKEY)
|
||||||
.unwrap_or_else(|_| panic!("store [{}] not found/loaded", 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() {
|
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 {
|
let hotkey = match hotkey {
|
||||||
Json::String(str) => str,
|
Json::String(str) => str,
|
||||||
_ => unreachable!("hotkey should be stored in a string"),
|
_ => unreachable!("hotkey should be stored in a string"),
|
||||||
@@ -643,13 +740,25 @@ fn register_app_hotkey_upon_start<R: Runtime>(
|
|||||||
|
|
||||||
tauri_app_handle
|
tauri_app_handle
|
||||||
.global_shortcut()
|
.global_shortcut()
|
||||||
.on_shortcut(hotkey.as_str(), app_hotkey_handler(app_path))
|
.unregister(hotkey.as_str())
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
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>(
|
pub fn register_app_hotkey<R: Runtime>(
|
||||||
tauri_app_handle: &AppHandle<R>,
|
tauri_app_handle: &AppHandle<R>,
|
||||||
app_path: &str,
|
app_path: &str,
|
||||||
@@ -661,13 +770,9 @@ pub fn register_app_hotkey<R: Runtime>(
|
|||||||
let app_hotkey_store = tauri_app_handle
|
let app_hotkey_store = tauri_app_handle
|
||||||
.store(TAURI_STORE_APP_HOTKEY)
|
.store(TAURI_STORE_APP_HOTKEY)
|
||||||
.unwrap_or_else(|_| panic!("store [{}] not found/loaded", TAURI_STORE_APP_HOTKEY));
|
.unwrap_or_else(|_| panic!("store [{}] not found/loaded", TAURI_STORE_APP_HOTKEY));
|
||||||
|
|
||||||
app_hotkey_store.set(app_path, hotkey);
|
app_hotkey_store.set(app_path, hotkey);
|
||||||
|
|
||||||
tauri_app_handle
|
set_app_hotkey(tauri_app_handle, app_path, hotkey)?;
|
||||||
.global_shortcut()
|
|
||||||
.on_shortcut(hotkey, app_hotkey_handler(app_path.into()))
|
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -779,6 +884,21 @@ pub fn disable_app_search<R: Runtime>(
|
|||||||
|
|
||||||
store.set(TAURI_STORE_KEY_DISABLED_APP_LIST, disabled_app_list);
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -805,6 +925,18 @@ pub fn enable_app_search<R: Runtime>(
|
|||||||
disabled_app_list.remove(index);
|
disabled_app_list.remove(index);
|
||||||
store.set(TAURI_STORE_KEY_DISABLED_APP_LIST, disabled_app_list);
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
None => Err(format!(
|
None => Err(format!(
|
||||||
@@ -964,21 +1096,25 @@ pub async fn get_app_list<R: Runtime>(
|
|||||||
|
|
||||||
let app_entry = Extension {
|
let app_entry = Extension {
|
||||||
id: path,
|
id: path,
|
||||||
title: name,
|
name,
|
||||||
platforms: None,
|
platforms: None,
|
||||||
|
developer: None,
|
||||||
// Leave it empty as it won't be used
|
// Leave it empty as it won't be used
|
||||||
description: String::new(),
|
description: String::new(),
|
||||||
icon: icon_path,
|
icon: icon_path,
|
||||||
r#type: ExtensionType::Application,
|
r#type: ExtensionType::Application,
|
||||||
action: None,
|
action: None,
|
||||||
quick_link: None,
|
quicklink: None,
|
||||||
commands: None,
|
commands: None,
|
||||||
scripts: None,
|
scripts: None,
|
||||||
quick_links: None,
|
quicklinks: None,
|
||||||
alias: Some(alias),
|
alias: Some(alias),
|
||||||
hotkey,
|
hotkey,
|
||||||
enabled,
|
enabled,
|
||||||
settings: None,
|
settings: None,
|
||||||
|
screenshots: None,
|
||||||
|
url: None,
|
||||||
|
version: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
app_entries.push(app_entry);
|
app_entries.push(app_entry);
|
||||||
@@ -1027,3 +1163,30 @@ pub async fn get_app_metadata(app_name: String, app_path: String) -> Result<AppM
|
|||||||
last_opened,
|
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;
|
pub struct ApplicationSearchSource;
|
||||||
|
|
||||||
impl 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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -117,3 +119,23 @@ pub async fn get_app_metadata<R: Runtime>(
|
|||||||
) -> Result<AppMetadata, String> {
|
) -> Result<AppMetadata, String> {
|
||||||
unreachable!("app list should be empty, there is no way this can be invoked")
|
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";
|
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 {
|
pub struct CalculatorSource {
|
||||||
base_score: f64,
|
base_score: f64,
|
||||||
}
|
}
|
||||||
@@ -23,7 +36,7 @@ impl CalculatorSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_query(query: String) -> Value {
|
fn parse_query(query: &str) -> Value {
|
||||||
let mut query_json = serde_json::Map::new();
|
let mut query_json = serde_json::Map::new();
|
||||||
|
|
||||||
let operators = ["+", "-", "*", "/", "%"];
|
let operators = ["+", "-", "*", "/", "%"];
|
||||||
@@ -48,7 +61,7 @@ fn parse_query(query: String) -> Value {
|
|||||||
query_json.insert("type".to_string(), Value::String("expression".to_string()));
|
query_json.insert("type".to_string(), Value::String("expression".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
query_json.insert("value".to_string(), Value::String(query));
|
query_json.insert("value".to_string(), Value::String(query.to_string()));
|
||||||
|
|
||||||
Value::Object(query_json)
|
Value::Object(query_json)
|
||||||
}
|
}
|
||||||
@@ -128,11 +141,17 @@ impl SearchSource for CalculatorSource {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
match meval::eval_str(query_string) {
|
let query_string_clone = query_string.to_string();
|
||||||
|
let query_source = self.get_type();
|
||||||
|
let base_score = self.base_score;
|
||||||
|
let closure = move || -> QueryResponse {
|
||||||
|
let res_num = meval::eval_str(&query_string_clone);
|
||||||
|
|
||||||
|
match res_num {
|
||||||
Ok(num) => {
|
Ok(num) => {
|
||||||
let mut payload: HashMap<String, Value> = HashMap::new();
|
let mut payload: HashMap<String, Value> = HashMap::new();
|
||||||
|
|
||||||
let payload_query = parse_query(query_string.into());
|
let payload_query = parse_query(&query_string_clone);
|
||||||
let payload_result = parse_result(num);
|
let payload_result = parse_result(num);
|
||||||
|
|
||||||
payload.insert("query".to_string(), payload_query);
|
payload.insert("query".to_string(), payload_query);
|
||||||
@@ -151,19 +170,27 @@ impl SearchSource for CalculatorSource {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
return Ok(QueryResponse {
|
QueryResponse {
|
||||||
source: self.get_type(),
|
source: query_source,
|
||||||
hits: vec![(doc, self.base_score)],
|
hits: vec![(doc, base_score)],
|
||||||
total_hits: 1,
|
total_hits: 1,
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
return Ok(QueryResponse {
|
QueryResponse {
|
||||||
source: self.get_type(),
|
source: query_source,
|
||||||
hits: Vec::new(),
|
hits: Vec::new(),
|
||||||
total_hits: 0,
|
total_hits: 0,
|
||||||
});
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let spawn_result = tokio::task::spawn_blocking(closure).await;
|
||||||
|
|
||||||
|
match spawn_result {
|
||||||
|
Ok(response) => Ok(response),
|
||||||
|
Err(e) => std::panic::resume_unwind(e.into_panic()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,37 +8,186 @@ pub mod pizza_engine_runtime;
|
|||||||
pub mod quick_ai_access;
|
pub mod quick_ai_access;
|
||||||
|
|
||||||
use super::Extension;
|
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 crate::{SearchSourceRegistry, GLOBAL_TAURI_APP_HANDLE};
|
||||||
use std::path::PathBuf;
|
use anyhow::Context;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
use tauri::path::BaseDirectory;
|
use tauri::{AppHandle, Manager, Runtime};
|
||||||
use tauri::Manager;
|
|
||||||
|
|
||||||
pub(crate) static BUILT_IN_EXTENSION_DIRECTORY: LazyLock<PathBuf> = LazyLock::new(|| {
|
pub(crate) static BUILT_IN_EXTENSION_DIRECTORY: LazyLock<PathBuf> = LazyLock::new(|| {
|
||||||
let mut resource_dir = GLOBAL_TAURI_APP_HANDLE
|
let mut resource_dir = GLOBAL_TAURI_APP_HANDLE
|
||||||
.get()
|
.get()
|
||||||
.expect("global tauri app handle not set")
|
.expect("global tauri app handle not set")
|
||||||
.path()
|
.path()
|
||||||
.resolve("assets", BaseDirectory::Resource)
|
.app_data_dir()
|
||||||
.expect(
|
.expect(
|
||||||
"User home directory not found, which should be impossible on desktop environments",
|
"User home directory not found, which should be impossible on desktop environments",
|
||||||
);
|
);
|
||||||
resource_dir.push("extension");
|
resource_dir.push("built_in_extensions");
|
||||||
|
|
||||||
resource_dir
|
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,
|
extension: &Extension,
|
||||||
search_source_registry: &SearchSourceRegistry,
|
search_source_registry: &SearchSourceRegistry,
|
||||||
) {
|
) -> Result<(), String> {
|
||||||
log::trace!("initializing built-in extensions");
|
log::trace!("initializing built-in extensions");
|
||||||
|
|
||||||
if extension.id == application::QUERYSOURCE_ID_DATASOURCE_ID_DATASOURCE_NAME {
|
if extension.id == application::QUERYSOURCE_ID_DATASOURCE_ID_DATASOURCE_NAME {
|
||||||
search_source_registry
|
search_source_registry
|
||||||
.register_source(application::ApplicationSearchSource)
|
.register_source(application::ApplicationSearchSource)
|
||||||
.await;
|
.await;
|
||||||
|
set_apps_hotkey(&tauri_app_handle)?;
|
||||||
log::debug!("built-in extension [{}] initialized", extension.id);
|
log::debug!("built-in extension [{}] initialized", extension.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,36 +198,17 @@ pub(super) async fn init_built_in_extension(
|
|||||||
.await;
|
.await;
|
||||||
log::debug!("built-in extension [{}] initialized", extension.id);
|
log::debug!("built-in extension [{}] initialized", extension.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn is_extension_built_in(extension_id: &str) -> bool {
|
pub(crate) fn is_extension_built_in(bundle_id: &ExtensionBundleIdBorrowed<'_>) -> bool {
|
||||||
if extension_id == application::QUERYSOURCE_ID_DATASOURCE_ID_DATASOURCE_NAME {
|
bundle_id.developer.is_none()
|
||||||
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) 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
|
let tauri_app_handle = GLOBAL_TAURI_APP_HANDLE
|
||||||
.get()
|
.get()
|
||||||
.expect("global tauri app handle not set");
|
.expect("global tauri app handle not set");
|
||||||
@@ -89,13 +219,17 @@ pub(crate) async fn enable_built_in_extension(extension_id: &str) -> Result<(),
|
|||||||
Ok(())
|
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
|
search_source_registry_tauri_state
|
||||||
.register_source(application::ApplicationSearchSource)
|
.register_source(application::ApplicationSearchSource)
|
||||||
.await;
|
.await;
|
||||||
|
set_apps_hotkey(tauri_app_handle)?;
|
||||||
|
|
||||||
alter_extension_json_file(
|
alter_extension_json_file(
|
||||||
&BUILT_IN_EXTENSION_DIRECTORY.as_path(),
|
&BUILT_IN_EXTENSION_DIRECTORY.as_path(),
|
||||||
extension_id,
|
bundle_id,
|
||||||
update_extension,
|
update_extension,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
@@ -103,42 +237,40 @@ pub(crate) async fn enable_built_in_extension(extension_id: &str) -> Result<(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if this is an application
|
// Check if this is an application
|
||||||
let application_prefix = format!(
|
if bundle_id.extension_id == application::QUERYSOURCE_ID_DATASOURCE_ID_DATASOURCE_NAME
|
||||||
"{}.",
|
&& bundle_id.sub_extension_id.is_some()
|
||||||
application::QUERYSOURCE_ID_DATASOURCE_ID_DATASOURCE_NAME
|
{
|
||||||
);
|
let app_path = bundle_id.sub_extension_id.expect("just checked it is Some");
|
||||||
if extension_id.starts_with(&application_prefix) {
|
|
||||||
let app_path = &extension_id[application_prefix.len()..];
|
|
||||||
application::enable_app_search(tauri_app_handle, app_path)?;
|
application::enable_app_search(tauri_app_handle, app_path)?;
|
||||||
return Ok(());
|
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);
|
let calculator_search = calculator::CalculatorSource::new(2000f64);
|
||||||
search_source_registry_tauri_state
|
search_source_registry_tauri_state
|
||||||
.register_source(calculator_search)
|
.register_source(calculator_search)
|
||||||
.await;
|
.await;
|
||||||
alter_extension_json_file(
|
alter_extension_json_file(
|
||||||
&BUILT_IN_EXTENSION_DIRECTORY.as_path(),
|
&BUILT_IN_EXTENSION_DIRECTORY.as_path(),
|
||||||
extension_id,
|
bundle_id,
|
||||||
update_extension,
|
update_extension,
|
||||||
)?;
|
)?;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
if extension_id == quick_ai_access::EXTENSION_ID {
|
if bundle_id.extension_id == quick_ai_access::EXTENSION_ID {
|
||||||
alter_extension_json_file(
|
alter_extension_json_file(
|
||||||
&BUILT_IN_EXTENSION_DIRECTORY.as_path(),
|
&BUILT_IN_EXTENSION_DIRECTORY.as_path(),
|
||||||
extension_id,
|
bundle_id,
|
||||||
update_extension,
|
update_extension,
|
||||||
)?;
|
)?;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
if extension_id == ai_overview::EXTENSION_ID {
|
if bundle_id.extension_id == ai_overview::EXTENSION_ID {
|
||||||
alter_extension_json_file(
|
alter_extension_json_file(
|
||||||
&BUILT_IN_EXTENSION_DIRECTORY.as_path(),
|
&BUILT_IN_EXTENSION_DIRECTORY.as_path(),
|
||||||
extension_id,
|
bundle_id,
|
||||||
update_extension,
|
update_extension,
|
||||||
)?;
|
)?;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
@@ -147,7 +279,9 @@ pub(crate) async fn enable_built_in_extension(extension_id: &str) -> Result<(),
|
|||||||
Ok(())
|
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
|
let tauri_app_handle = GLOBAL_TAURI_APP_HANDLE
|
||||||
.get()
|
.get()
|
||||||
.expect("global tauri app handle not set");
|
.expect("global tauri app handle not set");
|
||||||
@@ -158,55 +292,57 @@ pub(crate) async fn disable_built_in_extension(extension_id: &str) -> Result<(),
|
|||||||
Ok(())
|
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
|
search_source_registry_tauri_state
|
||||||
.remove_source(extension_id)
|
.remove_source(bundle_id.extension_id)
|
||||||
.await;
|
.await;
|
||||||
|
unset_apps_hotkey(tauri_app_handle)?;
|
||||||
|
|
||||||
alter_extension_json_file(
|
alter_extension_json_file(
|
||||||
&BUILT_IN_EXTENSION_DIRECTORY.as_path(),
|
&BUILT_IN_EXTENSION_DIRECTORY.as_path(),
|
||||||
extension_id,
|
bundle_id,
|
||||||
update_extension,
|
update_extension,
|
||||||
)?;
|
)?;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this is an application
|
// Check if this is an application
|
||||||
let application_prefix = format!(
|
if bundle_id.extension_id == application::QUERYSOURCE_ID_DATASOURCE_ID_DATASOURCE_NAME
|
||||||
"{}.",
|
&& bundle_id.sub_extension_id.is_some()
|
||||||
application::QUERYSOURCE_ID_DATASOURCE_ID_DATASOURCE_NAME
|
{
|
||||||
);
|
let app_path = bundle_id.sub_extension_id.expect("just checked it is Some");
|
||||||
if extension_id.starts_with(&application_prefix) {
|
|
||||||
let app_path = &extension_id[application_prefix.len()..];
|
|
||||||
application::disable_app_search(tauri_app_handle, app_path)?;
|
application::disable_app_search(tauri_app_handle, app_path)?;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
if extension_id == calculator::DATA_SOURCE_ID {
|
if bundle_id.extension_id == calculator::DATA_SOURCE_ID {
|
||||||
search_source_registry_tauri_state
|
search_source_registry_tauri_state
|
||||||
.remove_source(extension_id)
|
.remove_source(bundle_id.extension_id)
|
||||||
.await;
|
.await;
|
||||||
alter_extension_json_file(
|
alter_extension_json_file(
|
||||||
&BUILT_IN_EXTENSION_DIRECTORY.as_path(),
|
&BUILT_IN_EXTENSION_DIRECTORY.as_path(),
|
||||||
extension_id,
|
bundle_id,
|
||||||
update_extension,
|
update_extension,
|
||||||
)?;
|
)?;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
if extension_id == quick_ai_access::EXTENSION_ID {
|
if bundle_id.extension_id == quick_ai_access::EXTENSION_ID {
|
||||||
alter_extension_json_file(
|
alter_extension_json_file(
|
||||||
&BUILT_IN_EXTENSION_DIRECTORY.as_path(),
|
&BUILT_IN_EXTENSION_DIRECTORY.as_path(),
|
||||||
extension_id,
|
bundle_id,
|
||||||
update_extension,
|
update_extension,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
if extension_id == ai_overview::EXTENSION_ID {
|
if bundle_id.extension_id == ai_overview::EXTENSION_ID {
|
||||||
alter_extension_json_file(
|
alter_extension_json_file(
|
||||||
&BUILT_IN_EXTENSION_DIRECTORY.as_path(),
|
&BUILT_IN_EXTENSION_DIRECTORY.as_path(),
|
||||||
extension_id,
|
bundle_id,
|
||||||
update_extension,
|
update_extension,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
@@ -216,95 +352,131 @@ pub(crate) async fn disable_built_in_extension(extension_id: &str) -> Result<(),
|
|||||||
Ok(())
|
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
|
let tauri_app_handle = GLOBAL_TAURI_APP_HANDLE
|
||||||
.get()
|
.get()
|
||||||
.expect("global tauri app handle not set");
|
.expect("global tauri app handle not set");
|
||||||
|
|
||||||
let application_prefix = format!(
|
if bundle_id.extension_id == application::QUERYSOURCE_ID_DATASOURCE_ID_DATASOURCE_NAME {
|
||||||
"{}.",
|
if let Some(app_path) = bundle_id.sub_extension_id {
|
||||||
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);
|
application::set_app_alias(tauri_app_handle, app_path, alias);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn register_built_in_extension_hotkey(
|
pub(crate) fn register_built_in_extension_hotkey(
|
||||||
extension_id: &str,
|
bundle_id: &ExtensionBundleIdBorrowed<'_>,
|
||||||
hotkey: &str,
|
hotkey: &str,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let tauri_app_handle = GLOBAL_TAURI_APP_HANDLE
|
let tauri_app_handle = GLOBAL_TAURI_APP_HANDLE
|
||||||
.get()
|
.get()
|
||||||
.expect("global tauri app handle not set");
|
.expect("global tauri app handle not set");
|
||||||
let application_prefix = format!(
|
|
||||||
"{}.",
|
if bundle_id.extension_id == application::QUERYSOURCE_ID_DATASOURCE_ID_DATASOURCE_NAME {
|
||||||
application::QUERYSOURCE_ID_DATASOURCE_ID_DATASOURCE_NAME
|
if let Some(app_path) = bundle_id.sub_extension_id {
|
||||||
);
|
|
||||||
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)?;
|
application::register_app_hotkey(&tauri_app_handle, app_path, hotkey)?;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
Ok(())
|
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
|
let tauri_app_handle = GLOBAL_TAURI_APP_HANDLE
|
||||||
.get()
|
.get()
|
||||||
.expect("global tauri app handle not set");
|
.expect("global tauri app handle not set");
|
||||||
let application_prefix = format!(
|
|
||||||
"{}.",
|
if bundle_id.extension_id == application::QUERYSOURCE_ID_DATASOURCE_ID_DATASOURCE_NAME {
|
||||||
application::QUERYSOURCE_ID_DATASOURCE_ID_DATASOURCE_NAME
|
if let Some(app_path) = bundle_id.sub_extension_id {
|
||||||
);
|
|
||||||
if extension_id.starts_with(&application_prefix) {
|
|
||||||
let app_path = &extension_id[application_prefix.len()..];
|
|
||||||
application::unregister_app_hotkey(&tauri_app_handle, app_path)?;
|
application::unregister_app_hotkey(&tauri_app_handle, app_path)?;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
Ok(())
|
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
|
let tauri_app_handle = GLOBAL_TAURI_APP_HANDLE
|
||||||
.get()
|
.get()
|
||||||
.expect("global tauri app handle not set");
|
.expect("global tauri app handle not set");
|
||||||
let search_source_registry_tauri_state = tauri_app_handle.state::<SearchSourceRegistry>();
|
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
|
return Ok(search_source_registry_tauri_state
|
||||||
.get_source(extension_id)
|
.get_source(bundle_id.extension_id)
|
||||||
.await
|
.await
|
||||||
.is_some());
|
.is_some());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this is an application
|
// Check if this is an application
|
||||||
let application_prefix = format!(
|
if bundle_id.extension_id == application::QUERYSOURCE_ID_DATASOURCE_ID_DATASOURCE_NAME {
|
||||||
"{}.",
|
if let Some(app_path) = bundle_id.sub_extension_id {
|
||||||
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));
|
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
|
return Ok(search_source_registry_tauri_state
|
||||||
.get_source(extension_id)
|
.get_source(bundle_id.extension_id)
|
||||||
.await
|
.await
|
||||||
.is_some());
|
.is_some());
|
||||||
}
|
}
|
||||||
|
|
||||||
if extension_id == quick_ai_access::EXTENSION_ID {
|
if bundle_id.extension_id == quick_ai_access::EXTENSION_ID {
|
||||||
let extension =
|
let extension = load_extension_from_json_file(
|
||||||
load_extension_from_json_file(&BUILT_IN_EXTENSION_DIRECTORY.as_path(), extension_id)?;
|
&BUILT_IN_EXTENSION_DIRECTORY.as_path(),
|
||||||
|
bundle_id.extension_id,
|
||||||
|
)?;
|
||||||
return Ok(extension.enabled);
|
return Ok(extension.enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
if extension_id == ai_overview::EXTENSION_ID {
|
if bundle_id.extension_id == ai_overview::EXTENSION_ID {
|
||||||
let extension =
|
let extension = load_extension_from_json_file(
|
||||||
load_extension_from_json_file(&BUILT_IN_EXTENSION_DIRECTORY.as_path(), extension_id)?;
|
&BUILT_IN_EXTENSION_DIRECTORY.as_path(),
|
||||||
|
bundle_id.extension_id,
|
||||||
|
)?;
|
||||||
return Ok(extension.enabled);
|
return Ok(extension.enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
unreachable!("extension [{}] is not a built-in extension", extension_id)
|
unreachable!("extension [{:?}] is not a built-in extension", bundle_id)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,18 +27,32 @@ pub(crate) trait Task: Send + Sync {
|
|||||||
pub(crate) static RUNTIME_TX: OnceLock<tokio::sync::mpsc::UnboundedSender<Box<dyn Task>>> =
|
pub(crate) static RUNTIME_TX: OnceLock<tokio::sync::mpsc::UnboundedSender<Box<dyn Task>>> =
|
||||||
OnceLock::new();
|
OnceLock::new();
|
||||||
|
|
||||||
pub(crate) fn start_pizza_engine_runtime() {
|
/// This function blocks until the runtime thread is ready for accepting tasks.
|
||||||
std::thread::spawn(|| {
|
pub(crate) async fn start_pizza_engine_runtime() {
|
||||||
|
const THREAD_NAME: &str = "Pizza engine runtime thread";
|
||||||
|
|
||||||
|
log::trace!("starting Pizza engine runtime");
|
||||||
|
let (engine_start_signal_tx, engine_start_signal_rx) = tokio::sync::oneshot::channel();
|
||||||
|
|
||||||
|
std::thread::Builder::new()
|
||||||
|
.name(THREAD_NAME.into())
|
||||||
|
.spawn(move || {
|
||||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||||
|
|
||||||
let main = async {
|
let main = async {
|
||||||
let mut states: HashMap<String, Option<Box<dyn SearchSourceState>>> = HashMap::new();
|
let mut states: HashMap<String, Option<Box<dyn SearchSourceState>>> =
|
||||||
|
HashMap::new();
|
||||||
|
|
||||||
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
|
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
|
||||||
RUNTIME_TX.set(tx).unwrap();
|
RUNTIME_TX.set(tx).unwrap();
|
||||||
|
|
||||||
|
engine_start_signal_tx
|
||||||
|
.send(())
|
||||||
|
.expect("engine_start_signal_rx dropped");
|
||||||
|
|
||||||
while let Some(mut task) = rx.recv().await {
|
while let Some(mut task) = rx.recv().await {
|
||||||
let opt_search_source_state = match states.entry(task.search_source_id().into()) {
|
let opt_search_source_state = match states.entry(task.search_source_id().into())
|
||||||
|
{
|
||||||
Entry::Occupied(o) => o.into_mut(),
|
Entry::Occupied(o) => o.into_mut(),
|
||||||
Entry::Vacant(v) => v.insert(None),
|
Entry::Vacant(v) => v.insert(None),
|
||||||
};
|
};
|
||||||
@@ -47,5 +61,16 @@ pub(crate) fn start_pizza_engine_runtime() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
rt.block_on(main);
|
rt.block_on(main);
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|e| {
|
||||||
|
panic!(
|
||||||
|
"failed to start thread [{}] due to error [{}]",
|
||||||
|
THREAD_NAME, e
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
engine_start_signal_rx
|
||||||
|
.await
|
||||||
|
.expect("engine_start_signal_tx dropped, the runtime thread could be dead");
|
||||||
|
log::trace!("Pizza engine runtime started");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1,12 @@
|
|||||||
pub(super) const EXTENSION_ID: &str = "QuickAIAccess";
|
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
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
|||||||
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(())
|
||||||
|
}
|
||||||
@@ -11,19 +11,15 @@ mod util;
|
|||||||
|
|
||||||
use crate::common::register::SearchSourceRegistry;
|
use crate::common::register::SearchSourceRegistry;
|
||||||
// use crate::common::traits::SearchSource;
|
// use crate::common::traits::SearchSource;
|
||||||
use crate::common::{MAIN_WINDOW_LABEL, SETTINGS_WINDOW_LABEL};
|
use crate::common::{CHECK_WINDOW_LABEL, MAIN_WINDOW_LABEL, SETTINGS_WINDOW_LABEL};
|
||||||
use crate::server::servers::{load_or_insert_default_server, load_servers_token};
|
use crate::server::servers::{load_or_insert_default_server, load_servers_token};
|
||||||
use autostart::{change_autostart, enable_autostart};
|
use autostart::{change_autostart, ensure_autostart_state_consistent};
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
use tauri::async_runtime::block_on;
|
use tauri::async_runtime::block_on;
|
||||||
use tauri::plugin::TauriPlugin;
|
use tauri::plugin::TauriPlugin;
|
||||||
#[cfg(target_os = "macos")]
|
use tauri::{AppHandle, Emitter, Manager, PhysicalPosition, Runtime, WebviewWindow, WindowEvent};
|
||||||
use tauri::ActivationPolicy;
|
|
||||||
use tauri::{
|
|
||||||
AppHandle, Emitter, Manager, PhysicalPosition, Runtime, WebviewWindow, Window, WindowEvent,
|
|
||||||
};
|
|
||||||
use tauri_plugin_autostart::MacosLauncher;
|
use tauri_plugin_autostart::MacosLauncher;
|
||||||
|
|
||||||
/// Tauri store name
|
/// Tauri store name
|
||||||
@@ -64,6 +60,8 @@ pub fn run() {
|
|||||||
let ctx = tauri::generate_context!();
|
let ctx = tauri::generate_context!();
|
||||||
|
|
||||||
let mut app_builder = tauri::Builder::default();
|
let mut app_builder = tauri::Builder::default();
|
||||||
|
// Set up logger first
|
||||||
|
app_builder = app_builder.plugin(set_up_tauri_logger());
|
||||||
|
|
||||||
#[cfg(desktop)]
|
#[cfg(desktop)]
|
||||||
{
|
{
|
||||||
@@ -77,7 +75,7 @@ pub fn run() {
|
|||||||
.plugin(tauri_plugin_http::init())
|
.plugin(tauri_plugin_http::init())
|
||||||
.plugin(tauri_plugin_shell::init())
|
.plugin(tauri_plugin_shell::init())
|
||||||
.plugin(tauri_plugin_autostart::init(
|
.plugin(tauri_plugin_autostart::init(
|
||||||
MacosLauncher::AppleScript,
|
MacosLauncher::LaunchAgent,
|
||||||
None,
|
None,
|
||||||
))
|
))
|
||||||
.plugin(tauri_plugin_deep_link::init())
|
.plugin(tauri_plugin_deep_link::init())
|
||||||
@@ -89,7 +87,7 @@ pub fn run() {
|
|||||||
.plugin(tauri_plugin_process::init())
|
.plugin(tauri_plugin_process::init())
|
||||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||||
.plugin(tauri_plugin_windows_version::init())
|
.plugin(tauri_plugin_windows_version::init())
|
||||||
.plugin(set_up_tauri_logger());
|
.plugin(tauri_plugin_opener::init());
|
||||||
|
|
||||||
// Conditional compilation for macOS
|
// Conditional compilation for macOS
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
@@ -107,6 +105,8 @@ pub fn run() {
|
|||||||
show_coco,
|
show_coco,
|
||||||
hide_coco,
|
hide_coco,
|
||||||
show_settings,
|
show_settings,
|
||||||
|
show_check,
|
||||||
|
hide_check,
|
||||||
server::servers::get_server_token,
|
server::servers::get_server_token,
|
||||||
server::servers::add_coco_server,
|
server::servers::add_coco_server,
|
||||||
server::servers::remove_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::get_app_metadata,
|
||||||
extension::built_in::application::add_app_search_path,
|
extension::built_in::application::add_app_search_path,
|
||||||
extension::built_in::application::remove_app_search_path,
|
extension::built_in::application::remove_app_search_path,
|
||||||
|
extension::built_in::application::reindex_applications,
|
||||||
extension::list_extensions,
|
extension::list_extensions,
|
||||||
extension::enable_extension,
|
extension::enable_extension,
|
||||||
extension::disable_extension,
|
extension::disable_extension,
|
||||||
@@ -156,16 +157,27 @@ pub fn run() {
|
|||||||
extension::register_extension_hotkey,
|
extension::register_extension_hotkey,
|
||||||
extension::unregister_extension_hotkey,
|
extension::unregister_extension_hotkey,
|
||||||
extension::is_extension_enabled,
|
extension::is_extension_enabled,
|
||||||
|
extension::store::search_extension,
|
||||||
|
extension::store::install_extension,
|
||||||
|
extension::store::uninstall_extension,
|
||||||
settings::set_allow_self_signature,
|
settings::set_allow_self_signature,
|
||||||
settings::get_allow_self_signature,
|
settings::get_allow_self_signature,
|
||||||
assistant::ask_ai,
|
assistant::ask_ai,
|
||||||
crate::common::document::open,
|
crate::common::document::open,
|
||||||
])
|
])
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
log::trace!("hiding Dock icon on macOS");
|
||||||
|
app.set_activation_policy(tauri::ActivationPolicy::Accessory);
|
||||||
|
log::trace!("Dock icon should be hidden now");
|
||||||
|
}
|
||||||
|
|
||||||
let app_handle = app.handle().clone();
|
let app_handle = app.handle().clone();
|
||||||
GLOBAL_TAURI_APP_HANDLE
|
GLOBAL_TAURI_APP_HANDLE
|
||||||
.set(app_handle.clone())
|
.set(app_handle.clone())
|
||||||
.expect("variable already initialized");
|
.expect("variable already initialized");
|
||||||
|
log::trace!("global Tauri app handle set");
|
||||||
|
|
||||||
let registry = SearchSourceRegistry::default();
|
let registry = SearchSourceRegistry::default();
|
||||||
|
|
||||||
@@ -178,10 +190,7 @@ pub fn run() {
|
|||||||
|
|
||||||
shortcut::enable_shortcut(app);
|
shortcut::enable_shortcut(app);
|
||||||
|
|
||||||
enable_autostart(app);
|
ensure_autostart_state_consistent(app)?;
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
app.set_activation_policy(ActivationPolicy::Accessory);
|
|
||||||
|
|
||||||
// app.listen("theme-changed", move |event| {
|
// app.listen("theme-changed", move |event| {
|
||||||
// if let Ok(payload) = serde_json::from_str::<ThemeChangedPayload>(event.payload()) {
|
// if let Ok(payload) = serde_json::from_str::<ThemeChangedPayload>(event.payload()) {
|
||||||
@@ -206,7 +215,13 @@ pub fn run() {
|
|||||||
|
|
||||||
let main_window = app.get_webview_window(MAIN_WINDOW_LABEL).unwrap();
|
let main_window = app.get_webview_window(MAIN_WINDOW_LABEL).unwrap();
|
||||||
let settings_window = app.get_webview_window(SETTINGS_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(())
|
Ok(())
|
||||||
})
|
})
|
||||||
@@ -261,12 +276,12 @@ pub async fn init<R: Runtime>(app_handle: &AppHandle<R>) {
|
|||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
extension::built_in::pizza_engine_runtime::start_pizza_engine_runtime();
|
extension::built_in::pizza_engine_runtime::start_pizza_engine_runtime().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn show_coco<R: Runtime>(app_handle: AppHandle<R>) {
|
async fn show_coco<R: Runtime>(app_handle: AppHandle<R>) {
|
||||||
if let Some(window) = app_handle.get_window(MAIN_WINDOW_LABEL) {
|
if let Some(window) = app_handle.get_webview_window(MAIN_WINDOW_LABEL) {
|
||||||
move_window_to_active_monitor(&window);
|
move_window_to_active_monitor(&window);
|
||||||
|
|
||||||
let _ = window.show();
|
let _ = window.show();
|
||||||
@@ -279,7 +294,7 @@ async fn show_coco<R: Runtime>(app_handle: AppHandle<R>) {
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn hide_coco<R: Runtime>(app: AppHandle<R>) {
|
async fn hide_coco<R: Runtime>(app: AppHandle<R>) {
|
||||||
if let Some(window) = app.get_window(MAIN_WINDOW_LABEL) {
|
if let Some(window) = app.get_webview_window(MAIN_WINDOW_LABEL) {
|
||||||
if let Err(err) = window.hide() {
|
if let Err(err) = window.hide() {
|
||||||
log::error!("Failed to hide the window: {}", err);
|
log::error!("Failed to hide the window: {}", err);
|
||||||
} else {
|
} else {
|
||||||
@@ -290,7 +305,7 @@ async fn hide_coco<R: Runtime>(app: AppHandle<R>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn move_window_to_active_monitor<R: Runtime>(window: &Window<R>) {
|
fn move_window_to_active_monitor<R: Runtime>(window: &WebviewWindow<R>) {
|
||||||
//dbg!("Moving window to active monitor");
|
//dbg!("Moving window to active monitor");
|
||||||
// Try to get the available monitors, handle failure gracefully
|
// Try to get the available monitors, handle failure gracefully
|
||||||
let available_monitors = match window.available_monitors() {
|
let available_monitors = match window.available_monitors() {
|
||||||
@@ -383,41 +398,10 @@ fn move_window_to_active_monitor<R: Runtime>(window: &Window<R>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
fn open_settings(app: &tauri::AppHandle) {
|
|
||||||
use tauri::webview::WebviewBuilder;
|
|
||||||
log::debug!("settings menu item was clicked");
|
|
||||||
let window = app.get_webview_window("settings");
|
|
||||||
if let Some(window) = window {
|
|
||||||
let _ = window.show();
|
|
||||||
let _ = window.unminimize();
|
|
||||||
let _ = window.set_focus();
|
|
||||||
} else {
|
|
||||||
let window = tauri::window::WindowBuilder::new(app, "settings")
|
|
||||||
.title("Settings Window")
|
|
||||||
.fullscreen(false)
|
|
||||||
.resizable(false)
|
|
||||||
.minimizable(false)
|
|
||||||
.maximizable(false)
|
|
||||||
.inner_size(800.0, 600.0)
|
|
||||||
.build()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let webview_builder =
|
|
||||||
WebviewBuilder::new("settings", tauri::WebviewUrl::App("/ui/settings".into()));
|
|
||||||
let _webview = window
|
|
||||||
.add_child(
|
|
||||||
webview_builder,
|
|
||||||
tauri::LogicalPosition::new(0, 0),
|
|
||||||
window.inner_size().unwrap(),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn get_app_search_source<R: Runtime>(app_handle: AppHandle<R>) -> Result<(), String> {
|
async fn get_app_search_source<R: Runtime>(app_handle: AppHandle<R>) -> Result<(), String> {
|
||||||
let (_found_invalid_extensions, extensions) = extension::list_extensions()
|
// We want all the extensions here, so no filter condition specified.
|
||||||
|
let (_found_invalid_extensions, extensions) = extension::list_extensions(None, None, false)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
extension::init_extensions(extensions).await?;
|
extension::init_extensions(extensions).await?;
|
||||||
@@ -430,7 +414,36 @@ async fn get_app_search_source<R: Runtime>(app_handle: AppHandle<R>) -> Result<(
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn show_settings(app_handle: AppHandle) {
|
async fn show_settings(app_handle: AppHandle) {
|
||||||
open_settings(&app_handle);
|
log::debug!("settings menu item was clicked");
|
||||||
|
let window = app_handle
|
||||||
|
.get_webview_window(SETTINGS_WINDOW_LABEL)
|
||||||
|
.expect("we have a settings window");
|
||||||
|
|
||||||
|
window.show().unwrap();
|
||||||
|
window.unminimize().unwrap();
|
||||||
|
window.set_focus().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
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]
|
#[tauri::command]
|
||||||
@@ -585,6 +598,12 @@ fn set_up_tauri_logger() -> TauriPlugin<tauri::Wry> {
|
|||||||
builder
|
builder
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// When running the built binary, set `COCO_LOG` to `coco_lib=trace` to capture all logs
|
||||||
|
// that come from Coco in the log file, which helps with debugging.
|
||||||
|
if !tauri::is_dev() {
|
||||||
|
std::env::set_var("COCO_LOG", "coco_lib=trace");
|
||||||
|
}
|
||||||
|
|
||||||
let mut builder = tauri_plugin_log::Builder::new();
|
let mut builder = tauri_plugin_log::Builder::new();
|
||||||
builder = builder.format(|out, message, record| {
|
builder = builder.format(|out, message, record| {
|
||||||
let now = chrono::Local::now().format("%m-%d %H:%M:%S");
|
let now = chrono::Local::now().format("%m-%d %H:%M:%S");
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ fn same_type_futures(
|
|||||||
timeout_duration: Duration,
|
timeout_duration: Duration,
|
||||||
search_query: SearchQuery,
|
search_query: SearchQuery,
|
||||||
) -> impl Future<
|
) -> impl Future<
|
||||||
Output=(
|
Output = (
|
||||||
QuerySource,
|
QuerySource,
|
||||||
Result<Result<QueryResponse, SearchError>, Elapsed>,
|
Result<Result<QueryResponse, SearchError>, Elapsed>,
|
||||||
),
|
),
|
||||||
@@ -92,11 +92,30 @@ pub async fn query_coco_fusion<R: Runtime>(
|
|||||||
query_source_id
|
query_source_id
|
||||||
);
|
);
|
||||||
|
|
||||||
let query_source_trait_object_index = sources_list
|
let opt_query_source_trait_object_index = sources_list
|
||||||
.iter()
|
.iter()
|
||||||
.position(|query_source| &query_source.get_type().id == query_source_id).unwrap_or_else(|| {
|
.position(|query_source| &query_source.get_type().id == query_source_id);
|
||||||
panic!("frontend code invoked {}() with parameter [querysource={}], but we do not have this query source, the states are inconsistent! Available query sources {:?}", function_name!(), query_source_id, sources_list.iter().map(|qs| qs.get_type().id).collect::<Vec<_>>());
|
|
||||||
|
let Some(query_source_trait_object_index) = opt_query_source_trait_object_index else {
|
||||||
|
// It is possible (an edge case) that the frontend invokes `query_coco_fusion()` with a
|
||||||
|
// datasource that does not exist in the source list:
|
||||||
|
//
|
||||||
|
// 1. Search applications
|
||||||
|
// 2. Navigate to the application sub page
|
||||||
|
// 3. Disable the application extension in settings
|
||||||
|
// 4. hide the search window
|
||||||
|
// 5. Re-open the search window and search for something
|
||||||
|
//
|
||||||
|
// The application search source is not in the source list because the extension
|
||||||
|
// has been disabled, but the last search is indeed invoked with parameter
|
||||||
|
// `datasource=application`.
|
||||||
|
return Ok(MultiSourceQueryResponse {
|
||||||
|
failed: Vec::new(),
|
||||||
|
hits: Vec::new(),
|
||||||
|
total_hits: 0,
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
let query_source_trait_object = sources_list.remove(query_source_trait_object_index);
|
let query_source_trait_object = sources_list.remove(query_source_trait_object_index);
|
||||||
let query_source = query_source_trait_object.get_type();
|
let query_source = query_source_trait_object.get_type();
|
||||||
|
|
||||||
|
|||||||
@@ -111,8 +111,8 @@ pub async fn get_attachment(
|
|||||||
server_id: String,
|
server_id: String,
|
||||||
session_id: String,
|
session_id: String,
|
||||||
) -> Result<GetAttachmentResponse, String> {
|
) -> Result<GetAttachmentResponse, String> {
|
||||||
let mut query_params = HashMap::new();
|
let mut query_params = Vec::new();
|
||||||
query_params.insert("session".to_string(), serde_json::Value::String(session_id));
|
query_params.push(format!("session={}", session_id));
|
||||||
|
|
||||||
let response = HttpClient::get(&server_id, "/attachment/_search", Some(query_params))
|
let response = HttpClient::get(&server_id, "/attachment/_search", Some(query_params))
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -4,18 +4,10 @@ use crate::server::connector::get_connector_by_id;
|
|||||||
use crate::server::http_client::HttpClient;
|
use crate::server::http_client::HttpClient;
|
||||||
use crate::server::servers::get_all_servers;
|
use crate::server::servers::get_all_servers;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use serde_json::Value;
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
use tauri::{AppHandle, Runtime};
|
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! {
|
lazy_static! {
|
||||||
static ref DATASOURCE_CACHE: Arc<RwLock<HashMap<String, HashMap<String, DataSource>>>> =
|
static ref DATASOURCE_CACHE: Arc<RwLock<HashMap<String, HashMap<String, DataSource>>>> =
|
||||||
Arc::new(RwLock::new(HashMap::new()));
|
Arc::new(RwLock::new(HashMap::new()));
|
||||||
@@ -97,27 +89,10 @@ pub async fn refresh_all_datasources<R: Runtime>(_app_handle: &AppHandle<R>) ->
|
|||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn datasource_search(
|
pub async fn datasource_search(
|
||||||
id: &str,
|
id: &str,
|
||||||
options: Option<GetDatasourcesByServerOptions>,
|
query_params: Option<Vec<String>>, //["query=abc", "filter=er", "filter=efg", "from=0", "size=5"],
|
||||||
) -> Result<Vec<DataSource>, String> {
|
) -> 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
|
// Perform the async HTTP request outside the cache lock
|
||||||
let resp = HttpClient::post(
|
let resp = HttpClient::post(id, "/datasource/_search", query_params, None)
|
||||||
id,
|
|
||||||
"/datasource/_search",
|
|
||||||
None,
|
|
||||||
Some(reqwest::Body::from(body.to_string())),
|
|
||||||
)
|
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("Error fetching datasource: {}", e))?;
|
.map_err(|e| format!("Error fetching datasource: {}", e))?;
|
||||||
|
|
||||||
@@ -136,26 +111,10 @@ pub async fn datasource_search(
|
|||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn mcp_server_search(
|
pub async fn mcp_server_search(
|
||||||
id: &str,
|
id: &str,
|
||||||
from: u32,
|
query_params: Option<Vec<String>>,
|
||||||
size: u32,
|
|
||||||
query: Option<HashMap<String, Value>>,
|
|
||||||
) -> Result<Vec<DataSource>, 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
|
// Perform the async HTTP request outside the cache lock
|
||||||
let resp = HttpClient::post(
|
let resp = HttpClient::post(id, "/mcp_server/_search", query_params, None)
|
||||||
id,
|
|
||||||
"/mcp_server/_search",
|
|
||||||
None,
|
|
||||||
Some(reqwest::Body::from(body.to_string())),
|
|
||||||
)
|
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("Error fetching datasource: {}", e))?;
|
.map_err(|e| format!("Error fetching datasource: {}", e))?;
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ use once_cell::sync::Lazy;
|
|||||||
use reqwest::{Client, Method, RequestBuilder};
|
use reqwest::{Client, Method, RequestBuilder};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tauri_plugin_store::JsonValue;
|
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
pub(crate) fn new_reqwest_http_client(accept_invalid_certs: bool) -> Client {
|
pub(crate) fn new_reqwest_http_client(accept_invalid_certs: bool) -> Client {
|
||||||
@@ -40,7 +39,7 @@ impl HttpClient {
|
|||||||
pub async fn send_raw_request(
|
pub async fn send_raw_request(
|
||||||
method: Method,
|
method: Method,
|
||||||
url: &str,
|
url: &str,
|
||||||
query_params: Option<HashMap<String, JsonValue>>,
|
query_params: Option<Vec<String>>,
|
||||||
headers: Option<HashMap<String, String>>,
|
headers: Option<HashMap<String, String>>,
|
||||||
body: Option<reqwest::Body>,
|
body: Option<reqwest::Body>,
|
||||||
) -> Result<reqwest::Response, String> {
|
) -> Result<reqwest::Response, String> {
|
||||||
@@ -74,7 +73,7 @@ impl HttpClient {
|
|||||||
method: Method,
|
method: Method,
|
||||||
url: &str,
|
url: &str,
|
||||||
headers: Option<HashMap<String, String>>,
|
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>,
|
body: Option<reqwest::Body>,
|
||||||
) -> RequestBuilder {
|
) -> RequestBuilder {
|
||||||
let client = HTTP_CLIENT.lock().await; // Acquire the lock on HTTP_CLIENT
|
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);
|
request_builder = request_builder.headers(req_headers);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(query) = query_params {
|
if let Some(params) = query_params {
|
||||||
// Convert only supported value types into strings
|
let query: Vec<(&str, &str)> = params
|
||||||
let query: HashMap<String, String> = query
|
.iter()
|
||||||
.into_iter()
|
.filter_map(|s| s.split_once('='))
|
||||||
.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
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect();
|
.collect();
|
||||||
request_builder = request_builder.query(&query);
|
request_builder = request_builder.query(&query);
|
||||||
}
|
}
|
||||||
@@ -135,12 +121,13 @@ impl HttpClient {
|
|||||||
request_builder
|
request_builder
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pub async fn send_request(
|
pub async fn send_request(
|
||||||
server_id: &str,
|
server_id: &str,
|
||||||
method: Method,
|
method: Method,
|
||||||
path: &str,
|
path: &str,
|
||||||
custom_headers: Option<HashMap<String, String>>,
|
custom_headers: Option<HashMap<String, String>>,
|
||||||
query_params: Option<HashMap<String, JsonValue>>,
|
query_params: Option<Vec<String>>,
|
||||||
body: Option<reqwest::Body>,
|
body: Option<reqwest::Body>,
|
||||||
) -> Result<reqwest::Response, String> {
|
) -> Result<reqwest::Response, String> {
|
||||||
// Fetch the server using the server_id
|
// Fetch the server using the server_id
|
||||||
@@ -182,16 +169,17 @@ impl HttpClient {
|
|||||||
pub async fn get(
|
pub async fn get(
|
||||||
server_id: &str,
|
server_id: &str,
|
||||||
path: &str,
|
path: &str,
|
||||||
query_params: Option<HashMap<String, JsonValue>>, // Add query parameters
|
query_params: Option<Vec<String>>,
|
||||||
) -> Result<reqwest::Response, 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
|
// Convenience method for POST requests
|
||||||
pub async fn post(
|
pub async fn post(
|
||||||
server_id: &str,
|
server_id: &str,
|
||||||
path: &str,
|
path: &str,
|
||||||
query_params: Option<HashMap<String, JsonValue>>, // Add query parameters
|
query_params: Option<Vec<String>>,
|
||||||
body: Option<reqwest::Body>,
|
body: Option<reqwest::Body>,
|
||||||
) -> Result<reqwest::Response, String> {
|
) -> Result<reqwest::Response, String> {
|
||||||
HttpClient::send_request(server_id, Method::POST, path, None, query_params, body).await
|
HttpClient::send_request(server_id, Method::POST, path, None, query_params, body).await
|
||||||
@@ -201,7 +189,7 @@ impl HttpClient {
|
|||||||
server_id: &str,
|
server_id: &str,
|
||||||
path: &str,
|
path: &str,
|
||||||
custom_headers: Option<HashMap<String, String>>,
|
custom_headers: Option<HashMap<String, String>>,
|
||||||
query_params: Option<HashMap<String, JsonValue>>, // Add query parameters
|
query_params: Option<Vec<String>>,
|
||||||
body: Option<reqwest::Body>,
|
body: Option<reqwest::Body>,
|
||||||
) -> Result<reqwest::Response, String> {
|
) -> Result<reqwest::Response, String> {
|
||||||
HttpClient::send_request(
|
HttpClient::send_request(
|
||||||
@@ -221,7 +209,7 @@ impl HttpClient {
|
|||||||
server_id: &str,
|
server_id: &str,
|
||||||
path: &str,
|
path: &str,
|
||||||
custom_headers: Option<HashMap<String, String>>,
|
custom_headers: Option<HashMap<String, String>>,
|
||||||
query_params: Option<HashMap<String, JsonValue>>, // Add query parameters
|
query_params: Option<Vec<String>>,
|
||||||
body: Option<reqwest::Body>,
|
body: Option<reqwest::Body>,
|
||||||
) -> Result<reqwest::Response, String> {
|
) -> Result<reqwest::Response, String> {
|
||||||
HttpClient::send_request(
|
HttpClient::send_request(
|
||||||
@@ -241,7 +229,7 @@ impl HttpClient {
|
|||||||
server_id: &str,
|
server_id: &str,
|
||||||
path: &str,
|
path: &str,
|
||||||
custom_headers: Option<HashMap<String, String>>,
|
custom_headers: Option<HashMap<String, String>>,
|
||||||
query_params: Option<HashMap<String, JsonValue>>, // Add query parameters
|
query_params: Option<Vec<String>>,
|
||||||
) -> Result<reqwest::Response, String> {
|
) -> Result<reqwest::Response, String> {
|
||||||
HttpClient::send_request(
|
HttpClient::send_request(
|
||||||
server_id,
|
server_id,
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ use async_trait::async_trait;
|
|||||||
// use futures::stream::StreamExt;
|
// use futures::stream::StreamExt;
|
||||||
use ordered_float::OrderedFloat;
|
use ordered_float::OrderedFloat;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use tauri_plugin_store::JsonValue;
|
|
||||||
// use std::hash::Hash;
|
// use std::hash::Hash;
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
@@ -96,14 +95,18 @@ impl SearchSource for CocoSearchSource {
|
|||||||
let mut total_hits = 0;
|
let mut total_hits = 0;
|
||||||
let mut hits: Vec<(Document, f64)> = Vec::new();
|
let mut hits: Vec<(Document, f64)> = Vec::new();
|
||||||
|
|
||||||
let mut query_args: HashMap<String, JsonValue> = HashMap::new();
|
let mut query_params = Vec::new();
|
||||||
query_args.insert("from".into(), JsonValue::Number(query.from.into()));
|
|
||||||
query_args.insert("size".into(), JsonValue::Number(query.size.into()));
|
// 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 {
|
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
|
.await
|
||||||
.map_err(|e| SearchError::HttpError(format!("{}", e)))?;
|
.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
|
// Send request to fetch updated server info
|
||||||
let response = HttpClient::get(&id, "/provider/_info", None)
|
let response = HttpClient::get(&id, "/provider/_info", None)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| format!("Failed to contact the server: {}", e));
|
||||||
format!("Failed to contact the server: {}", e)
|
|
||||||
});
|
|
||||||
|
|
||||||
if response.is_err() {
|
if response.is_err() {
|
||||||
let _ = mark_server_as_offline(app_handle, &id).await;
|
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)
|
let mut updated_server: Server = serde_json::from_str(&body)
|
||||||
.map_err(|e| format!("Failed to deserialize the response: {}", e))?;
|
.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
|
// Restore local state
|
||||||
updated_server.id = id.clone();
|
updated_server.id = id.clone();
|
||||||
updated_server.builtin = is_builtin;
|
updated_server.builtin = is_builtin;
|
||||||
@@ -476,8 +477,25 @@ pub async fn try_register_server_to_search_source(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[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<(), ()> {
|
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);
|
// println!("server_is_offline: {}", id);
|
||||||
let server = get_server_by_id(id);
|
let server = get_server_by_id(id);
|
||||||
if let Some(mut server) = server {
|
if let Some(mut server) = server {
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
use crate::common::http::get_response_body_text;
|
use crate::common::http::get_response_body_text;
|
||||||
use crate::server::http_client::HttpClient;
|
use crate::server::http_client::HttpClient;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value as JsonValue;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use tauri::command;
|
use tauri::command;
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
@@ -13,18 +11,18 @@ pub struct TranscriptionResponse {
|
|||||||
#[command]
|
#[command]
|
||||||
pub async fn transcription(
|
pub async fn transcription(
|
||||||
server_id: String,
|
server_id: String,
|
||||||
audio_type: String,
|
_audio_type: String,
|
||||||
audio_content: String,
|
_audio_content: String,
|
||||||
) -> Result<TranscriptionResponse, String> {
|
) -> Result<TranscriptionResponse, String> {
|
||||||
let mut query_params = HashMap::new();
|
// let mut query_params = HashMap::new();
|
||||||
query_params.insert("type".to_string(), JsonValue::String(audio_type));
|
// query_params.insert("type".to_string(), JsonValue::String(audio_type));
|
||||||
query_params.insert("content".to_string(), JsonValue::String(audio_content));
|
// query_params.insert("content".to_string(), JsonValue::String(audio_content));
|
||||||
|
|
||||||
// Send the HTTP POST request
|
// Send the HTTP POST request
|
||||||
let response = HttpClient::post(
|
let response = HttpClient::post(
|
||||||
&server_id,
|
&server_id,
|
||||||
"/services/audio/transcription",
|
"/services/audio/transcription",
|
||||||
Some(query_params),
|
None,
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
use tauri::{App, WebviewWindow};
|
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,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
//credits to: https://github.com/ayangweb/ayangweb-EcoPaste/blob/169323dbe6365ffe4abb64d867439ed2ea84c6d1/src-tauri/src/core/setup/mac.rs
|
//credits to: https://github.com/ayangweb/ayangweb-EcoPaste/blob/169323dbe6365ffe4abb64d867439ed2ea84c6d1/src-tauri/src/core/setup/mac.rs
|
||||||
use tauri::{ActivationPolicy, App, Emitter, EventTarget, WebviewWindow};
|
use tauri::{App, Emitter, EventTarget, WebviewWindow};
|
||||||
use tauri_nspanel::{cocoa::appkit::NSWindowCollectionBehavior, panel_delegate, WebviewWindowExt};
|
use tauri_nspanel::{cocoa::appkit::NSWindowCollectionBehavior, panel_delegate, WebviewWindowExt};
|
||||||
|
|
||||||
use crate::common::MAIN_WINDOW_LABEL;
|
use crate::common::MAIN_WINDOW_LABEL;
|
||||||
@@ -12,9 +12,12 @@ const WINDOW_BLUR_EVENT: &str = "tauri://blur";
|
|||||||
const WINDOW_MOVED_EVENT: &str = "tauri://move";
|
const WINDOW_MOVED_EVENT: &str = "tauri://move";
|
||||||
const WINDOW_RESIZED_EVENT: &str = "tauri://resize";
|
const WINDOW_RESIZED_EVENT: &str = "tauri://resize";
|
||||||
|
|
||||||
pub fn platform(app: &mut App, main_window: WebviewWindow, _settings_window: WebviewWindow) {
|
pub fn platform(
|
||||||
app.set_activation_policy(ActivationPolicy::Accessory);
|
_app: &mut App,
|
||||||
|
main_window: WebviewWindow,
|
||||||
|
_settings_window: WebviewWindow,
|
||||||
|
_check_window: WebviewWindow,
|
||||||
|
) {
|
||||||
// Convert ns_window to ns_panel
|
// Convert ns_window to ns_panel
|
||||||
let panel = main_window.to_panel().unwrap();
|
let panel = main_window.to_panel().unwrap();
|
||||||
|
|
||||||
|
|||||||
@@ -18,10 +18,20 @@ pub use windows::*;
|
|||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
pub use 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
|
// Development mode automatically opens the console: https://tauri.app/develop/debug
|
||||||
#[cfg(all(dev, debug_assertions))]
|
#[cfg(debug_assertions)]
|
||||||
main_window.open_devtools();
|
main_window.open_devtools();
|
||||||
|
|
||||||
platform(app, main_window.clone(), settings_window.clone());
|
platform(
|
||||||
|
app,
|
||||||
|
main_window.clone(),
|
||||||
|
settings_window.clone(),
|
||||||
|
check_window.clone(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
use tauri::{App, WebviewWindow};
|
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,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ const DEFAULT_SHORTCUT: &str = "ctrl+shift+space";
|
|||||||
|
|
||||||
/// Set up the shortcut upon app start.
|
/// Set up the shortcut upon app start.
|
||||||
pub fn enable_shortcut(app: &App) {
|
pub fn enable_shortcut(app: &App) {
|
||||||
|
log::trace!("setting up Coco hotkey");
|
||||||
let store = app
|
let store = app
|
||||||
.store(COCO_TAURI_STORE)
|
.store(COCO_TAURI_STORE)
|
||||||
.expect("creating a store should not fail");
|
.expect("creating a store should not fail");
|
||||||
@@ -43,6 +44,7 @@ pub fn enable_shortcut(app: &App) {
|
|||||||
.expect("default shortcut should never be invalid");
|
.expect("default shortcut should never be invalid");
|
||||||
_register_shortcut_upon_start(app, default_shortcut);
|
_register_shortcut_upon_start(app, default_shortcut);
|
||||||
}
|
}
|
||||||
|
log::trace!("Coco hotkey has been set");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the stored shortcut as a string, same as [`_get_shortcut()`], except that
|
/// Get the stored shortcut as a string, same as [`_get_shortcut()`], except that
|
||||||
@@ -97,7 +99,7 @@ fn _register_shortcut<R: Runtime>(app: &AppHandle<R>, shortcut: Shortcut) {
|
|||||||
.on_shortcut(shortcut, move |app, scut, event| {
|
.on_shortcut(shortcut, move |app, scut, event| {
|
||||||
if scut == &shortcut {
|
if scut == &shortcut {
|
||||||
dbg!("shortcut pressed");
|
dbg!("shortcut pressed");
|
||||||
let main_window = app.get_window(MAIN_WINDOW_LABEL).unwrap();
|
let main_window = app.get_webview_window(MAIN_WINDOW_LABEL).unwrap();
|
||||||
if let ShortcutState::Pressed = event.state() {
|
if let ShortcutState::Pressed = event.state() {
|
||||||
let app_handle = app.clone();
|
let app_handle = app.clone();
|
||||||
if main_window.is_visible().unwrap() {
|
if main_window.is_visible().unwrap() {
|
||||||
@@ -126,7 +128,7 @@ fn _register_shortcut_upon_start(app: &App, shortcut: Shortcut) {
|
|||||||
tauri_plugin_global_shortcut::Builder::new()
|
tauri_plugin_global_shortcut::Builder::new()
|
||||||
.with_handler(move |app, scut, event| {
|
.with_handler(move |app, scut, event| {
|
||||||
if scut == &shortcut {
|
if scut == &shortcut {
|
||||||
let window = app.get_window(MAIN_WINDOW_LABEL).unwrap();
|
let window = app.get_webview_window(MAIN_WINDOW_LABEL).unwrap();
|
||||||
if let ShortcutState::Pressed = event.state() {
|
if let ShortcutState::Pressed = event.state() {
|
||||||
let app_handle = app.clone();
|
let app_handle = app.clone();
|
||||||
|
|
||||||
|
|||||||
@@ -41,9 +41,9 @@
|
|||||||
"title": "Coco AI Settings",
|
"title": "Coco AI Settings",
|
||||||
"url": "/ui/settings",
|
"url": "/ui/settings",
|
||||||
"width": 1000,
|
"width": 1000,
|
||||||
|
"minWidth": 1000,
|
||||||
"height": 700,
|
"height": 700,
|
||||||
"minHeight": 700,
|
"minHeight": 700,
|
||||||
"minWidth": 1000,
|
|
||||||
"center": true,
|
"center": true,
|
||||||
"transparent": true,
|
"transparent": true,
|
||||||
"maximizable": false,
|
"maximizable": false,
|
||||||
@@ -55,6 +55,26 @@
|
|||||||
"effects": ["sidebar"],
|
"effects": ["sidebar"],
|
||||||
"state": "active"
|
"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": {
|
"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 |
@@ -29,6 +29,7 @@ const WHITELIST_SERVERS = [
|
|||||||
"refresh_coco_server_info",
|
"refresh_coco_server_info",
|
||||||
"handle_sso_callback",
|
"handle_sso_callback",
|
||||||
"query_coco_fusion",
|
"query_coco_fusion",
|
||||||
|
"open_session_chat", // TODO: quick ai access is a configured service, even if the current service is not logged in, it should not affect the configured service.
|
||||||
];
|
];
|
||||||
|
|
||||||
async function invokeWithErrorHandler<T>(
|
async function invokeWithErrorHandler<T>(
|
||||||
@@ -50,7 +51,7 @@ async function invokeWithErrorHandler<T>(
|
|||||||
const failedResult = result as any;
|
const failedResult = result as any;
|
||||||
if (failedResult.failed?.length > 0 && failedResult?.hits?.length == 0) {
|
if (failedResult.failed?.length > 0 && failedResult?.hits?.length == 0) {
|
||||||
failedResult.failed.forEach((error: any) => {
|
failedResult.failed.forEach((error: any) => {
|
||||||
addError(error.error, 'error');
|
addError(error.error, "error");
|
||||||
// console.error(error.error);
|
// console.error(error.error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -123,12 +124,26 @@ export function get_connectors_by_server(id: string): Promise<Connector[]> {
|
|||||||
return invokeWithErrorHandler(`get_connectors_by_server`, { id });
|
return invokeWithErrorHandler(`get_connectors_by_server`, { id });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function datasource_search(id: string): Promise<DataSource[]> {
|
export function datasource_search({
|
||||||
return invokeWithErrorHandler(`datasource_search`, { id });
|
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[]> {
|
export function mcp_server_search({
|
||||||
return invokeWithErrorHandler(`mcp_server_search`, { id });
|
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> {
|
export function connect_to_server(id: string, clientId: string): Promise<void> {
|
||||||
@@ -223,7 +238,7 @@ export function new_chat({
|
|||||||
queryParams,
|
queryParams,
|
||||||
}: {
|
}: {
|
||||||
serverId: string;
|
serverId: string;
|
||||||
websocketId?: string;
|
websocketId: string;
|
||||||
message: string;
|
message: string;
|
||||||
queryParams?: Record<string, any>;
|
queryParams?: Record<string, any>;
|
||||||
}): Promise<GetResponse> {
|
}): Promise<GetResponse> {
|
||||||
@@ -243,7 +258,7 @@ export function send_message({
|
|||||||
queryParams,
|
queryParams,
|
||||||
}: {
|
}: {
|
||||||
serverId: string;
|
serverId: string;
|
||||||
websocketId?: string;
|
websocketId: string;
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
message: string;
|
message: string;
|
||||||
queryParams?: Record<string, any>;
|
queryParams?: Record<string, any>;
|
||||||
@@ -268,15 +283,16 @@ export const update_session_chat = (payload: {
|
|||||||
serverId: string;
|
serverId: string;
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
context?: {
|
context?: Record<string, any>;
|
||||||
attachments?: string[];
|
|
||||||
};
|
|
||||||
}): Promise<boolean> => {
|
}): Promise<boolean> => {
|
||||||
return invokeWithErrorHandler<boolean>("update_session_chat", payload);
|
return invokeWithErrorHandler<boolean>("update_session_chat", payload);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const assistant_search = (payload: {
|
export const assistant_search = (payload: {
|
||||||
serverId: string;
|
serverId: string;
|
||||||
|
from: number;
|
||||||
|
size: number;
|
||||||
|
query?: Record<string, any>;
|
||||||
}): Promise<boolean> => {
|
}): Promise<boolean> => {
|
||||||
return invokeWithErrorHandler<boolean>("assistant_search", payload);
|
return invokeWithErrorHandler<boolean>("assistant_search", payload);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -27,3 +27,11 @@ export function show_coco(): Promise<void> {
|
|||||||
export function show_settings(): Promise<void> {
|
export function show_settings(): Promise<void> {
|
||||||
return invoke('show_settings');
|
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 platformAdapter from "@/utils/platformAdapter";
|
||||||
import { useConnectStore } from "@/stores/connectStore";
|
import { useConnectStore } from "@/stores/connectStore";
|
||||||
import { useAppStore } from "@/stores/appStore";
|
import { useAppStore } from "@/stores/appStore";
|
||||||
|
import { parseSearchQuery, SearchQuery, unrequitable } from "@/utils";
|
||||||
|
|
||||||
interface AssistantFetcherProps {
|
interface AssistantFetcherProps {
|
||||||
debounceKeyword?: string;
|
debounceKeyword?: string;
|
||||||
@@ -16,12 +17,8 @@ export const AssistantFetcher = ({
|
|||||||
}: AssistantFetcherProps) => {
|
}: AssistantFetcherProps) => {
|
||||||
const isTauri = useAppStore((state) => state.isTauri);
|
const isTauri = useAppStore((state) => state.isTauri);
|
||||||
|
|
||||||
const currentService = useConnectStore((state) => state.currentService);
|
const { currentService, currentAssistant, setCurrentAssistant } =
|
||||||
|
useConnectStore();
|
||||||
const currentAssistant = useConnectStore((state) => state.currentAssistant);
|
|
||||||
const setCurrentAssistant = useConnectStore((state) => {
|
|
||||||
return state.setCurrentAssistant;
|
|
||||||
});
|
|
||||||
|
|
||||||
const lastServerId = useRef<string | null>(null);
|
const lastServerId = useRef<string | null>(null);
|
||||||
|
|
||||||
@@ -29,47 +26,42 @@ export const AssistantFetcher = ({
|
|||||||
current: number;
|
current: number;
|
||||||
pageSize: number;
|
pageSize: number;
|
||||||
serverId?: string;
|
serverId?: string;
|
||||||
|
query?: string;
|
||||||
}) => {
|
}) => {
|
||||||
try {
|
try {
|
||||||
const { pageSize, current, serverId = currentService?.id } = params;
|
if (unrequitable()) {
|
||||||
|
return {
|
||||||
|
total: 0,
|
||||||
|
list: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const from = (current - 1) * pageSize;
|
const {
|
||||||
const size = pageSize;
|
pageSize,
|
||||||
|
current,
|
||||||
|
serverId = currentService?.id,
|
||||||
|
query,
|
||||||
|
} = params;
|
||||||
|
|
||||||
let response: any;
|
const searchQuery: SearchQuery = {
|
||||||
|
from: (current - 1) * pageSize,
|
||||||
|
size: pageSize,
|
||||||
|
query: query ?? debounceKeyword,
|
||||||
|
fuzziness: 5,
|
||||||
|
filters: {
|
||||||
|
enabled: true,
|
||||||
|
id: assistantIDs,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const queryParams = parseSearchQuery(searchQuery);
|
||||||
|
|
||||||
const body: Record<string, any> = {
|
const body: Record<string, any> = {
|
||||||
serverId,
|
serverId,
|
||||||
from,
|
queryParams,
|
||||||
size,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
body.query = {
|
let response: any;
|
||||||
bool: {
|
|
||||||
must: [{ term: { enabled: true } }],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
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),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isTauri) {
|
if (isTauri) {
|
||||||
if (!currentService?.id) {
|
if (!currentService?.id) {
|
||||||
@@ -78,6 +70,7 @@ export const AssistantFetcher = ({
|
|||||||
|
|
||||||
response = await platformAdapter.commands("assistant_search", body);
|
response = await platformAdapter.commands("assistant_search", body);
|
||||||
} else {
|
} else {
|
||||||
|
body.serverId = undefined;
|
||||||
const [error, res] = await Post(`/assistant/_search`, body);
|
const [error, res] = await Post(`/assistant/_search`, body);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { AssistantFetcher } from "./AssistantFetcher";
|
|||||||
import AssistantItem from "./AssistantItem";
|
import AssistantItem from "./AssistantItem";
|
||||||
import Pagination from "@/components/Common/Pagination";
|
import Pagination from "@/components/Common/Pagination";
|
||||||
import { useSearchStore } from "@/stores/searchStore";
|
import { useSearchStore } from "@/stores/searchStore";
|
||||||
|
import { useChatStore } from "@/stores/chatStore";
|
||||||
|
|
||||||
interface AssistantListProps {
|
interface AssistantListProps {
|
||||||
assistantIDs?: string[];
|
assistantIDs?: string[];
|
||||||
@@ -43,17 +44,40 @@ export function AssistantList({ assistantIDs = [] }: AssistantListProps) {
|
|||||||
return state.setAskAiAssistantId;
|
return state.setAskAiAssistantId;
|
||||||
});
|
});
|
||||||
const assistantList = useConnectStore((state) => state.assistantList);
|
const assistantList = useConnectStore((state) => state.assistantList);
|
||||||
|
const connected = useChatStore((state) => {
|
||||||
|
return state.connected;
|
||||||
|
});
|
||||||
|
|
||||||
const { fetchAssistant } = AssistantFetcher({
|
const { fetchAssistant } = AssistantFetcher({
|
||||||
debounceKeyword,
|
debounceKeyword,
|
||||||
assistantIDs,
|
assistantIDs,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { pagination, runAsync } = usePagination(fetchAssistant, {
|
const getAssistants = (params: { current: number; pageSize: number }) => {
|
||||||
|
if (!connected) {
|
||||||
|
return Promise.resolve({
|
||||||
|
total: 0,
|
||||||
|
list: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetchAssistant(params);
|
||||||
|
};
|
||||||
|
|
||||||
|
const { pagination, runAsync } = usePagination(getAssistants, {
|
||||||
defaultPageSize: 5,
|
defaultPageSize: 5,
|
||||||
refreshDeps: [currentService?.id, debounceKeyword],
|
refreshDeps: [
|
||||||
|
currentService?.id,
|
||||||
|
debounceKeyword,
|
||||||
|
currentService?.enabled,
|
||||||
|
connected,
|
||||||
|
],
|
||||||
onSuccess(data) {
|
onSuccess(data) {
|
||||||
setAssistants(data.list);
|
setAssistants(data.list);
|
||||||
|
|
||||||
|
if (data.list.length === 0) {
|
||||||
|
setCurrentAssistant(void 0);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -215,7 +239,7 @@ export function AssistantList({ assistantIDs = [] }: AssistantListProps) {
|
|||||||
placeholder={t("assistant.popover.search")}
|
placeholder={t("assistant.popover.search")}
|
||||||
className="w-full h-8 px-2 bg-transparent border rounded-md dark:border-white/10"
|
className="w-full h-8 px-2 bg-transparent border rounded-md dark:border-white/10"
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
setKeyword(event.target.value.trim());
|
setKeyword(event.target.value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</VisibleKey>
|
</VisibleKey>
|
||||||
|
|||||||
@@ -80,7 +80,12 @@ const ChatAI = memo(
|
|||||||
const { curChatEnd, setCurChatEnd, connected, setConnected } =
|
const { curChatEnd, setCurChatEnd, connected, setConnected } =
|
||||||
useChatStore();
|
useChatStore();
|
||||||
|
|
||||||
|
const isTauri = useAppStore((state) => state.isTauri);
|
||||||
|
|
||||||
const isCurrentLogin = useAuthStore((state) => state.isCurrentLogin);
|
const isCurrentLogin = useAuthStore((state) => state.isCurrentLogin);
|
||||||
|
const setIsCurrentLogin = useAuthStore((state) => {
|
||||||
|
return state.setIsCurrentLogin;
|
||||||
|
});
|
||||||
|
|
||||||
const visibleStartPage = useConnectStore((state) => {
|
const visibleStartPage = useConnectStore((state) => {
|
||||||
return state.visibleStartPage;
|
return state.visibleStartPage;
|
||||||
@@ -102,22 +107,34 @@ const ChatAI = memo(
|
|||||||
const askAiServerId = useSearchStore((state) => {
|
const askAiServerId = useSearchStore((state) => {
|
||||||
return state.askAiServerId;
|
return state.askAiServerId;
|
||||||
});
|
});
|
||||||
|
const currentService = useConnectStore((state) => {
|
||||||
|
return state.currentService;
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
activeChatProp && setActiveChat(activeChatProp);
|
activeChatProp && setActiveChat(activeChatProp);
|
||||||
}, [activeChatProp]);
|
}, [activeChatProp]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (askAiServerId || !askAiSessionId || chats.length === 0) return;
|
if (!isTauri) return;
|
||||||
|
|
||||||
const matched = chats.find((item) => item._id === askAiSessionId);
|
if (!currentService?.enabled) {
|
||||||
|
setActiveChat(void 0);
|
||||||
|
setIsCurrentLogin(false);
|
||||||
|
}
|
||||||
|
|
||||||
if (matched) {
|
if (showChatHistory && connected) {
|
||||||
onSelectChat(matched);
|
getChatHistory();
|
||||||
|
}
|
||||||
|
}, [currentService?.enabled, showChatHistory, connected]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (askAiServerId || !askAiSessionId) return;
|
||||||
|
|
||||||
|
onSelectChat({ _id: askAiSessionId });
|
||||||
|
|
||||||
setAskAiSessionId(void 0);
|
setAskAiSessionId(void 0);
|
||||||
}
|
}, [askAiSessionId, askAiServerId]);
|
||||||
}, [chats, askAiSessionId, askAiServerId]);
|
|
||||||
|
|
||||||
const [Question, setQuestion] = useState<string>("");
|
const [Question, setQuestion] = useState<string>("");
|
||||||
|
|
||||||
@@ -318,6 +335,7 @@ const ChatAI = memo(
|
|||||||
(chatId: string, title: string) => {
|
(chatId: string, title: string) => {
|
||||||
setChats((prev) => {
|
setChats((prev) => {
|
||||||
const chatIndex = prev.findIndex((chat) => chat._id === chatId);
|
const chatIndex = prev.findIndex((chat) => chat._id === chatId);
|
||||||
|
|
||||||
if (chatIndex === -1) return prev;
|
if (chatIndex === -1) return prev;
|
||||||
|
|
||||||
const modifiedChat = {
|
const modifiedChat = {
|
||||||
@@ -326,8 +344,8 @@ const ChatAI = memo(
|
|||||||
};
|
};
|
||||||
|
|
||||||
const result = [...prev];
|
const result = [...prev];
|
||||||
result.splice(chatIndex, 1);
|
result.splice(chatIndex, 1, modifiedChat);
|
||||||
return [modifiedChat, ...result];
|
return result;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (activeChat?._id === chatId) {
|
if (activeChat?._id === chatId) {
|
||||||
@@ -372,7 +390,8 @@ const ChatAI = memo(
|
|||||||
assistantIDs={assistantIDs}
|
assistantIDs={assistantIDs}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{isCurrentLogin ? (<>
|
{isCurrentLogin ? (
|
||||||
|
<>
|
||||||
<ChatContent
|
<ChatContent
|
||||||
activeChat={activeChat}
|
activeChat={activeChat}
|
||||||
curChatEnd={curChatEnd}
|
curChatEnd={curChatEnd}
|
||||||
@@ -391,10 +410,8 @@ const ChatAI = memo(
|
|||||||
}
|
}
|
||||||
getFileUrl={getFileUrl}
|
getFileUrl={getFileUrl}
|
||||||
/>
|
/>
|
||||||
<Splash assistantIDs={assistantIDs} startPage={startPage}/>
|
<Splash assistantIDs={assistantIDs} startPage={startPage} />
|
||||||
</>
|
</>
|
||||||
|
|
||||||
|
|
||||||
) : (
|
) : (
|
||||||
<ConnectPrompt />
|
<ConnectPrompt />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ export const ChatContent = ({
|
|||||||
const { scrollToBottom } = useChatScroll(messagesEndRef);
|
const { scrollToBottom } = useChatScroll(messagesEndRef);
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
const [isAtBottom, setIsAtBottom] = useState(true);
|
const [isAtBottom, setIsAtBottom] = useState(true);
|
||||||
|
const visibleStartPage = useConnectStore((state) => state.visibleStartPage);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsAtBottom(true);
|
setIsAtBottom(true);
|
||||||
@@ -100,7 +101,8 @@ export const ChatContent = ({
|
|||||||
className="flex-1 w-full overflow-x-hidden overflow-y-auto border-t border-[rgba(0,0,0,0.1)] dark:border-[rgba(255,255,255,0.15)] custom-scrollbar relative"
|
className="flex-1 w-full overflow-x-hidden overflow-y-auto border-t border-[rgba(0,0,0,0.1)] dark:border-[rgba(255,255,255,0.15)] custom-scrollbar relative"
|
||||||
onScroll={handleScroll}
|
onScroll={handleScroll}
|
||||||
>
|
>
|
||||||
{(!activeChat || activeChat?.messages?.length === 0) && <Greetings />}
|
{(!activeChat || activeChat?.messages?.length === 0) &&
|
||||||
|
!visibleStartPage && <Greetings />}
|
||||||
|
|
||||||
{activeChat?.messages?.map((message, index) => (
|
{activeChat?.messages?.map((message, index) => (
|
||||||
<ChatMessage
|
<ChatMessage
|
||||||
|
|||||||
@@ -39,8 +39,8 @@ export const ChatSidebar: React.FC<ChatSidebarProps> = ({
|
|||||||
>
|
>
|
||||||
{isSidebarOpen && (
|
{isSidebarOpen && (
|
||||||
<HistoryList
|
<HistoryList
|
||||||
id={HISTORY_PANEL_ID}
|
historyPanelId={HISTORY_PANEL_ID}
|
||||||
list={chats}
|
chats={chats}
|
||||||
active={activeChat}
|
active={activeChat}
|
||||||
onSearch={onSearch}
|
onSearch={onSearch}
|
||||||
onRefresh={fetchChatHistory}
|
onRefresh={fetchChatHistory}
|
||||||
|
|||||||
@@ -38,12 +38,16 @@ export function ServerList({ reconnect, clearChat }: ServerListProps) {
|
|||||||
|
|
||||||
const [serverList, setServerList] = useState<IServer[]>([]);
|
const [serverList, setServerList] = useState<IServer[]>([]);
|
||||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
|
const [highlightId, setHighlightId] = useState<string>("");
|
||||||
|
|
||||||
const askAiServerId = useSearchStore((state) => {
|
const askAiServerId = useSearchStore((state) => {
|
||||||
return state.askAiServerId;
|
return state.askAiServerId;
|
||||||
});
|
});
|
||||||
const setAskAiServerId = useSearchStore((state) => {
|
const setAskAiServerId = useSearchStore((state) => {
|
||||||
return state.setAskAiServerId;
|
return state.setAskAiServerId;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const popoverRef = useRef<HTMLDivElement>(null);
|
||||||
const serverListButtonRef = useRef<HTMLButtonElement>(null);
|
const serverListButtonRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
const fetchServers = useCallback(
|
const fetchServers = useCallback(
|
||||||
@@ -76,6 +80,10 @@ export function ServerList({ reconnect, clearChat }: ServerListProps) {
|
|||||||
[currentService?.id]
|
[currentService?.id]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchServers(true);
|
||||||
|
}, [currentService?.enabled]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!askAiServerId || serverList.length === 0) return;
|
if (!askAiServerId || serverList.length === 0) return;
|
||||||
|
|
||||||
@@ -142,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 isClose = isNil(serverListButtonRef.current?.dataset["open"]);
|
||||||
const length = serverList.length;
|
const length = serverList.length;
|
||||||
|
|
||||||
if (isClose || length <= 1) return;
|
if (isClose || length <= 1) return;
|
||||||
|
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
const currentIndex = serverList.findIndex((server) => {
|
const currentIndex = serverList.findIndex((server) => {
|
||||||
return server.id === currentService?.id;
|
return server.id === (highlightId === '' ? currentService?.id : highlightId);
|
||||||
});
|
});
|
||||||
|
|
||||||
let nextIndex = currentIndex;
|
let nextIndex = currentIndex;
|
||||||
|
|
||||||
if (key === "uparrow") {
|
if (key === "uparrow") {
|
||||||
nextIndex = currentIndex > 0 ? currentIndex - 1 : length - 1;
|
nextIndex = currentIndex > 0 ? currentIndex - 1 : length - 1;
|
||||||
|
setHighlightId(serverList[nextIndex].id);
|
||||||
} else if (key === "downarrow") {
|
} else if (key === "downarrow") {
|
||||||
nextIndex = currentIndex < serverList.length - 1 ? currentIndex + 1 : 0;
|
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 (
|
return (
|
||||||
<Popover className="relative">
|
<Popover ref={popoverRef} className="relative">
|
||||||
<PopoverButton ref={serverListButtonRef} className="flex items-center">
|
<PopoverButton ref={serverListButtonRef} className="flex items-center">
|
||||||
<VisibleKey
|
<VisibleKey
|
||||||
shortcut={serviceList}
|
shortcut={serviceList}
|
||||||
@@ -176,11 +200,13 @@ export function ServerList({ reconnect, clearChat }: ServerListProps) {
|
|||||||
</VisibleKey>
|
</VisibleKey>
|
||||||
</PopoverButton>
|
</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="p-3">
|
||||||
<div className="flex items-center justify-between mb-3 whitespace-nowrap">
|
<div className="flex items-center justify-between mb-3 whitespace-nowrap">
|
||||||
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||||
Servers
|
{t("assistant.chat.servers")}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
@@ -198,8 +224,7 @@ export function ServerList({ reconnect, clearChat }: ServerListProps) {
|
|||||||
>
|
>
|
||||||
<VisibleKey shortcut="R" onKeyPress={handleRefresh}>
|
<VisibleKey shortcut="R" onKeyPress={handleRefresh}>
|
||||||
<RefreshCw
|
<RefreshCw
|
||||||
className={`h-4 w-4 text-[#0287FF] transition-transform duration-1000 ${
|
className={`h-4 w-4 text-[#0287FF] transition-transform duration-1000 ${isRefreshing ? "animate-spin" : ""
|
||||||
isRefreshing ? "animate-spin" : ""
|
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
</VisibleKey>
|
</VisibleKey>
|
||||||
@@ -212,8 +237,8 @@ export function ServerList({ reconnect, clearChat }: ServerListProps) {
|
|||||||
<div
|
<div
|
||||||
key={server.id}
|
key={server.id}
|
||||||
onClick={() => switchServer(server)}
|
onClick={() => switchServer(server)}
|
||||||
className={`w-full flex items-center justify-between gap-1 p-2 rounded-lg transition-colors whitespace-nowrap ${
|
className={`w-full flex items-center justify-between gap-1 p-2 rounded-lg transition-colors whitespace-nowrap
|
||||||
currentService?.id === server.id
|
${currentService?.id === server.id || highlightId === server.id
|
||||||
? "bg-gray-100 dark:bg-gray-800"
|
? "bg-gray-100 dark:bg-gray-800"
|
||||||
: "hover:bg-gray-50 dark:hover:bg-gray-800/50"
|
: "hover:bg-gray-50 dark:hover:bg-gray-800/50"
|
||||||
}`}
|
}`}
|
||||||
@@ -233,7 +258,8 @@ export function ServerList({ reconnect, clearChat }: ServerListProps) {
|
|||||||
{server.name}
|
{server.name}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-500 dark:text-gray-400 truncate max-w-[200px]">
|
<div className="text-xs text-gray-500 dark:text-gray-400 truncate max-w-[200px]">
|
||||||
AI Assistant: {server.stats?.assistant_count || 1}
|
{t("assistant.chat.aiAssistant")}:{" "}
|
||||||
|
{server.stats?.assistant_count || 1}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import FontIcon from "@/components/Common/Icons/FontIcon";
|
|||||||
import logoImg from "@/assets/icon.svg";
|
import logoImg from "@/assets/icon.svg";
|
||||||
import { AssistantFetcher } from "./AssistantFetcher";
|
import { AssistantFetcher } from "./AssistantFetcher";
|
||||||
import type { StartPage } from "@/types/chat";
|
import type { StartPage } from "@/types/chat";
|
||||||
|
import { unrequitable } from "@/utils";
|
||||||
|
|
||||||
export interface Response {
|
export interface Response {
|
||||||
app_settings?: {
|
app_settings?: {
|
||||||
@@ -53,6 +54,10 @@ const Splash = ({ assistantIDs = [], startPage }: SplashProps) => {
|
|||||||
|
|
||||||
let response: any;
|
let response: any;
|
||||||
if (isTauri) {
|
if (isTauri) {
|
||||||
|
if (unrequitable()) {
|
||||||
|
return setVisibleStartPage(false);
|
||||||
|
}
|
||||||
|
|
||||||
response = await platformAdapter.invokeBackend<Response>(
|
response = await platformAdapter.invokeBackend<Response>(
|
||||||
"get_system_settings",
|
"get_system_settings",
|
||||||
{
|
{
|
||||||
@@ -72,6 +77,12 @@ const Splash = ({ assistantIDs = [], startPage }: SplashProps) => {
|
|||||||
fetchData();
|
fetchData();
|
||||||
}, [currentService?.id]);
|
}, [currentService?.id]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentService?.enabled) return;
|
||||||
|
|
||||||
|
isTauri && setVisibleStartPage(false);
|
||||||
|
}, [currentService?.enabled]);
|
||||||
|
|
||||||
const settingsAssistantList = useMemo(() => {
|
const settingsAssistantList = useMemo(() => {
|
||||||
return assistantList.filter((item) => {
|
return assistantList.filter((item) => {
|
||||||
return settings?.display_assistants?.includes(item?._source?.id);
|
return settings?.display_assistants?.includes(item?._source?.id);
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { COPY_BUTTON_ID } from "@/constants";
|
import { useState } from "react";
|
||||||
import { useSearchStore } from "@/stores/searchStore";
|
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {
|
import {
|
||||||
Check,
|
Check,
|
||||||
@@ -9,7 +8,8 @@ import {
|
|||||||
Volume2,
|
Volume2,
|
||||||
RotateCcw,
|
RotateCcw,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useState } from "react";
|
|
||||||
|
import { copyToClipboard } from "@/utils";
|
||||||
|
|
||||||
interface MessageActionsProps {
|
interface MessageActionsProps {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -17,6 +17,7 @@ interface MessageActionsProps {
|
|||||||
question?: string;
|
question?: string;
|
||||||
actionClassName?: string;
|
actionClassName?: string;
|
||||||
actionIconSize?: number;
|
actionIconSize?: number;
|
||||||
|
copyButtonId?: string;
|
||||||
onResend?: () => void;
|
onResend?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,6 +29,7 @@ export const MessageActions = ({
|
|||||||
question,
|
question,
|
||||||
actionClassName,
|
actionClassName,
|
||||||
actionIconSize,
|
actionIconSize,
|
||||||
|
copyButtonId,
|
||||||
onResend,
|
onResend,
|
||||||
}: MessageActionsProps) => {
|
}: MessageActionsProps) => {
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
@@ -40,7 +42,7 @@ export const MessageActions = ({
|
|||||||
|
|
||||||
const handleCopy = async () => {
|
const handleCopy = async () => {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(content);
|
await copyToClipboard(content);
|
||||||
setCopied(true);
|
setCopied(true);
|
||||||
const timerID = setTimeout(() => {
|
const timerID = setTimeout(() => {
|
||||||
setCopied(false);
|
setCopied(false);
|
||||||
@@ -91,13 +93,12 @@ export const MessageActions = ({
|
|||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const goAskAi = useSearchStore((state) => state.goAskAi);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx("flex items-center gap-1 mt-2", actionClassName)}>
|
<div className={clsx("flex items-center gap-1 mt-2", actionClassName)}>
|
||||||
{!isRefreshOnly && (
|
{!isRefreshOnly && (
|
||||||
<button
|
<button
|
||||||
id={goAskAi ? COPY_BUTTON_ID : ""}
|
id={copyButtonId}
|
||||||
onClick={handleCopy}
|
onClick={handleCopy}
|
||||||
className="p-1 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg transition-colors"
|
className="p-1 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg transition-colors"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -10,9 +10,26 @@ interface UserMessageProps {
|
|||||||
export const UserMessage = ({ messageContent }: UserMessageProps) => {
|
export const UserMessage = ({ messageContent }: UserMessageProps) => {
|
||||||
const [showCopyButton, setShowCopyButton] = useState(false);
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
className="flex gap-1 items-center"
|
className="flex gap-1 items-center justify-end"
|
||||||
onMouseEnter={() => setShowCopyButton(true)}
|
onMouseEnter={() => setShowCopyButton(true)}
|
||||||
onMouseLeave={() => setShowCopyButton(false)}
|
onMouseLeave={() => setShowCopyButton(false)}
|
||||||
>
|
>
|
||||||
@@ -24,14 +41,8 @@ export const UserMessage = ({ messageContent }: UserMessageProps) => {
|
|||||||
<CopyButton textToCopy={messageContent} />
|
<CopyButton textToCopy={messageContent} />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="px-3 py-2 bg-white dark:bg-[#202126] rounded-xl border border-black/12 dark:border-black/15 font-normal text-sm text-[#333333] dark:text-[#D8D8D8] cursor-pointer select-none"
|
className="max-w-[85%] overflow-auto text-left px-3 py-2 bg-white dark:bg-[#202126] rounded-xl border border-black/12 dark:border-black/15 font-normal text-sm text-[#333333] dark:text-[#D8D8D8] cursor-pointer user-select-text whitespace-pre-wrap"
|
||||||
onDoubleClick={(e) => {
|
onDoubleClick={handleDoubleClick}
|
||||||
const selection = window.getSelection();
|
|
||||||
const range = document.createRange();
|
|
||||||
range.selectNodeContents(e.currentTarget);
|
|
||||||
selection?.removeAllRanges();
|
|
||||||
selection?.addRange(range);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{messageContent}
|
{messageContent}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ interface ChatMessageProps {
|
|||||||
rootClassName?: string;
|
rootClassName?: string;
|
||||||
actionClassName?: string;
|
actionClassName?: string;
|
||||||
actionIconSize?: number;
|
actionIconSize?: number;
|
||||||
|
copyButtonId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ChatMessage = memo(function ChatMessage({
|
export const ChatMessage = memo(function ChatMessage({
|
||||||
@@ -51,6 +52,7 @@ export const ChatMessage = memo(function ChatMessage({
|
|||||||
rootClassName,
|
rootClassName,
|
||||||
actionClassName,
|
actionClassName,
|
||||||
actionIconSize,
|
actionIconSize,
|
||||||
|
copyButtonId,
|
||||||
}: ChatMessageProps) {
|
}: ChatMessageProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -65,16 +67,17 @@ export const ChatMessage = memo(function ChatMessage({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (assistant_item) {
|
if (assistant_item) {
|
||||||
setAssistant(assistant_item);
|
setAssistant(assistant_item);
|
||||||
} else {
|
return;
|
||||||
let target = currentAssistant;
|
}
|
||||||
|
|
||||||
if (isAssistant && assistant_id && Array.isArray(assistantList)) {
|
if (isAssistant && assistant_id && Array.isArray(assistantList)) {
|
||||||
const found = assistantList.find((item) => item._id === assistant_id);
|
setAssistant(
|
||||||
if (found) {
|
assistantList.find((item) => item._id === assistant_id) ?? {}
|
||||||
target = found;
|
);
|
||||||
}
|
return;
|
||||||
}
|
|
||||||
setAssistant(target);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setAssistant(currentAssistant);
|
||||||
}, [
|
}, [
|
||||||
isAssistant,
|
isAssistant,
|
||||||
assistant_item,
|
assistant_item,
|
||||||
@@ -91,7 +94,6 @@ export const ChatMessage = memo(function ChatMessage({
|
|||||||
isTyping === false && (messageContent || response?.message_chunk);
|
isTyping === false && (messageContent || response?.message_chunk);
|
||||||
|
|
||||||
const [suggestion, setSuggestion] = useState<string[]>([]);
|
const [suggestion, setSuggestion] = useState<string[]>([]);
|
||||||
const visibleStartPage = useConnectStore((state) => state.visibleStartPage);
|
|
||||||
|
|
||||||
const getSuggestion = (suggestion: string[]) => {
|
const getSuggestion = (suggestion: string[]) => {
|
||||||
setSuggestion(suggestion);
|
setSuggestion(suggestion);
|
||||||
@@ -152,6 +154,7 @@ export const ChatMessage = memo(function ChatMessage({
|
|||||||
question={question}
|
question={question}
|
||||||
actionClassName={actionClassName}
|
actionClassName={actionClassName}
|
||||||
actionIconSize={actionIconSize}
|
actionIconSize={actionIconSize}
|
||||||
|
copyButtonId={copyButtonId}
|
||||||
onResend={() => {
|
onResend={() => {
|
||||||
onResend && onResend(question);
|
onResend && onResend(question);
|
||||||
}}
|
}}
|
||||||
@@ -172,9 +175,6 @@ export const ChatMessage = memo(function ChatMessage({
|
|||||||
className={clsx(
|
className={clsx(
|
||||||
"py-8 flex",
|
"py-8 flex",
|
||||||
[isAssistant ? "justify-start" : "justify-end"],
|
[isAssistant ? "justify-start" : "justify-end"],
|
||||||
{
|
|
||||||
hidden: visibleStartPage,
|
|
||||||
},
|
|
||||||
rootClassName
|
rootClassName
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export default function Cloud() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log("currentService", currentService);
|
// console.log("currentService", currentService);
|
||||||
setRefreshLoading(false);
|
setRefreshLoading(false);
|
||||||
setIsConnect(true);
|
setIsConnect(true);
|
||||||
}, [JSON.stringify(currentService)]);
|
}, [JSON.stringify(currentService)]);
|
||||||
@@ -54,15 +54,17 @@ export default function Cloud() {
|
|||||||
return item;
|
return item;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
//console.log("list_coco_servers", res);
|
// console.log("list_coco_servers", res);
|
||||||
setServerList(res);
|
setServerList(res);
|
||||||
|
|
||||||
if (resetSelection && res.length > 0) {
|
if (resetSelection && res.length > 0) {
|
||||||
const currentServiceExists = res.some(
|
const matched = res.find((server: any) => {
|
||||||
(server: any) => server.id === currentService?.id
|
return server.id === currentService?.id;
|
||||||
);
|
});
|
||||||
|
|
||||||
if (!currentServiceExists) {
|
if (matched) {
|
||||||
|
setCurrentService(matched);
|
||||||
|
} else {
|
||||||
setCurrentService(res[res.length - 1]);
|
setCurrentService(res[res.length - 1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -140,7 +142,7 @@ export default function Cloud() {
|
|||||||
refreshClick={refreshClick}
|
refreshClick={refreshClick}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{currentService?.profile ? (
|
{currentService?.profile && currentService?.available ? (
|
||||||
<DataSourcesList server={currentService?.id} />
|
<DataSourcesList server={currentService?.id} />
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ export function Connect({ setIsConnect, onAddServer }: ConnectServiceProps) {
|
|||||||
setIsConnect(true);
|
setIsConnect(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onChangeEndpoint = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setEndpointLink(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-4xl">
|
<div className="max-w-4xl">
|
||||||
<div className="flex items-center gap-2 mb-8">
|
<div className="flex items-center gap-2 mb-8">
|
||||||
@@ -60,7 +64,7 @@ export function Connect({ setIsConnect, onAddServer }: ConnectServiceProps) {
|
|||||||
id="endpoint"
|
id="endpoint"
|
||||||
value={endpointLink}
|
value={endpointLink}
|
||||||
placeholder={t("cloud.connect.serverPlaceholder")}
|
placeholder={t("cloud.connect.serverPlaceholder")}
|
||||||
onChange={(e) => setEndpointLink(e.target.value)}
|
onChange={onChangeEndpoint}
|
||||||
className="text-[#101010] dark:text-white flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-800"
|
className="text-[#101010] dark:text-white flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-800"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -4,10 +4,7 @@ import { RefreshCcw } from "lucide-react";
|
|||||||
|
|
||||||
import { DataSourceItem } from "./DataSourceItem";
|
import { DataSourceItem } from "./DataSourceItem";
|
||||||
import { useConnectStore } from "@/stores/connectStore";
|
import { useConnectStore } from "@/stores/connectStore";
|
||||||
import {
|
import { get_connectors_by_server, datasource_search } from "@/commands";
|
||||||
get_connectors_by_server,
|
|
||||||
datasource_search,
|
|
||||||
} from "@/commands";
|
|
||||||
|
|
||||||
export function DataSourcesList({ server }: { server: string }) {
|
export function DataSourcesList({ server }: { server: string }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -17,8 +14,9 @@ export function DataSourcesList({ server }: { server: string }) {
|
|||||||
const setDatasourceData = useConnectStore((state) => state.setDatasourceData);
|
const setDatasourceData = useConnectStore((state) => state.setDatasourceData);
|
||||||
const setConnectorData = useConnectStore((state) => state.setConnectorData);
|
const setConnectorData = useConnectStore((state) => state.setConnectorData);
|
||||||
|
|
||||||
function initServerAppData({ server }: { server: string }) {
|
function initServerAppData() {
|
||||||
//fetch datasource data
|
setRefreshLoading(true);
|
||||||
|
// fetch connectors data
|
||||||
get_connectors_by_server(server)
|
get_connectors_by_server(server)
|
||||||
.then((res: any) => {
|
.then((res: any) => {
|
||||||
// console.log("get_connectors_by_server", res);
|
// console.log("get_connectors_by_server", res);
|
||||||
@@ -26,31 +24,20 @@ export function DataSourcesList({ server }: { server: string }) {
|
|||||||
})
|
})
|
||||||
.finally(() => {});
|
.finally(() => {});
|
||||||
|
|
||||||
//fetch datasource data
|
// fetch datasource data
|
||||||
datasource_search(server)
|
datasource_search({ id: server })
|
||||||
.then((res: any) => {
|
.then((res: any) => {
|
||||||
// console.log("datasource_search", res);
|
// console.log("datasource_search", res);
|
||||||
setDatasourceData(res, server);
|
setDatasourceData(res, server);
|
||||||
})
|
})
|
||||||
.finally(() => {});
|
.finally(() => {
|
||||||
}
|
|
||||||
|
|
||||||
async function getDatasourceData() {
|
|
||||||
setRefreshLoading(true);
|
|
||||||
try {
|
|
||||||
initServerAppData({ server });
|
|
||||||
} finally {
|
|
||||||
setRefreshLoading(false);
|
setRefreshLoading(false);
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getDatasourceData();
|
initServerAppData();
|
||||||
}, []);
|
}, [server]);
|
||||||
|
|
||||||
// const handleToggle = (id: string, enabled: boolean) => {
|
|
||||||
// console.log("handleToggle", id, enabled);
|
|
||||||
// };
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@@ -58,10 +45,12 @@ export function DataSourcesList({ server }: { server: string }) {
|
|||||||
{t("cloud.dataSource.title")}
|
{t("cloud.dataSource.title")}
|
||||||
<button
|
<button
|
||||||
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 rounded-[6px] bg-white dark:bg-gray-800 border border-[rgba(228,229,239,1)] dark:border-gray-700"
|
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 rounded-[6px] bg-white dark:bg-gray-800 border border-[rgba(228,229,239,1)] dark:border-gray-700"
|
||||||
onClick={() => getDatasourceData()}
|
onClick={() => initServerAppData()}
|
||||||
>
|
>
|
||||||
<RefreshCcw
|
<RefreshCcw
|
||||||
className={`w-3.5 h-3.5 ${refreshLoading ? "animate-spin" : ""}`}
|
className={`w-3.5 h-3.5 transition-transform duration-1000 ${
|
||||||
|
refreshLoading ? "animate-spin" : ""
|
||||||
|
}`}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</h2>
|
</h2>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { memo, useCallback, useEffect, useState } from "react";
|
import { FC, memo, useCallback, useEffect, useState } from "react";
|
||||||
import { Copy } from "lucide-react";
|
import { Copy } from "lucide-react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
@@ -14,6 +14,7 @@ import { OpenURLWithBrowser } from "@/utils";
|
|||||||
import { useConnectStore } from "@/stores/connectStore";
|
import { useConnectStore } from "@/stores/connectStore";
|
||||||
import { useAppStore } from "@/stores/appStore";
|
import { useAppStore } from "@/stores/appStore";
|
||||||
import { logout_coco_server, handle_sso_callback } from "@/commands";
|
import { logout_coco_server, handle_sso_callback } from "@/commands";
|
||||||
|
import { copyToClipboard } from "@/utils";
|
||||||
|
|
||||||
interface ServiceAuthProps {
|
interface ServiceAuthProps {
|
||||||
setRefreshLoading: (loading: boolean) => void;
|
setRefreshLoading: (loading: boolean) => void;
|
||||||
@@ -30,7 +31,9 @@ const ServiceAuth = memo(
|
|||||||
const addError = useAppStore((state) => state.addError);
|
const addError = useAppStore((state) => state.addError);
|
||||||
|
|
||||||
const currentService = useConnectStore((state) => state.currentService);
|
const currentService = useConnectStore((state) => state.currentService);
|
||||||
const setCurrentService = useConnectStore((state) => state.setCurrentService);
|
const setCurrentService = useConnectStore(
|
||||||
|
(state) => state.setCurrentService
|
||||||
|
);
|
||||||
const serverList = useConnectStore((state) => state.serverList);
|
const serverList = useConnectStore((state) => state.serverList);
|
||||||
const setServerList = useConnectStore((state) => state.setServerList);
|
const setServerList = useConnectStore((state) => state.setServerList);
|
||||||
|
|
||||||
@@ -63,7 +66,7 @@ const ServiceAuth = memo(
|
|||||||
emit("login_or_logout", false);
|
emit("login_or_logout", false);
|
||||||
// update server profile
|
// update server profile
|
||||||
setCurrentService({ ...currentService, profile: null });
|
setCurrentService({ ...currentService, profile: null });
|
||||||
const updatedServerList = serverList.map(server =>
|
const updatedServerList = serverList.map((server) =>
|
||||||
server.id === id ? { ...server, profile: null } : server
|
server.id === id ? { ...server, profile: null } : server
|
||||||
);
|
);
|
||||||
console.log("updatedServerList", updatedServerList);
|
console.log("updatedServerList", updatedServerList);
|
||||||
@@ -130,7 +133,6 @@ const ServiceAuth = memo(
|
|||||||
|
|
||||||
// Fetch the initial deep link intent
|
// Fetch the initial deep link intent
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLoading(false);
|
|
||||||
// Function to handle pasted URL
|
// Function to handle pasted URL
|
||||||
const handlePaste = (event: any) => {
|
const handlePaste = (event: any) => {
|
||||||
const pastedText = event.clipboardData.getData("text").trim();
|
const pastedText = event.clipboardData.getData("text").trim();
|
||||||
@@ -172,6 +174,10 @@ const ServiceAuth = memo(
|
|||||||
};
|
};
|
||||||
}, [ssoRequestID]);
|
}, [ssoRequestID]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLoading(false);
|
||||||
|
}, [currentService]);
|
||||||
|
|
||||||
if (!currentService?.auth_provider?.sso?.url) {
|
if (!currentService?.auth_provider?.sso?.url) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -197,7 +203,7 @@ const ServiceAuth = memo(
|
|||||||
<LoadingState
|
<LoadingState
|
||||||
onCancel={() => setLoading(false)}
|
onCancel={() => setLoading(false)}
|
||||||
onCopy={() => {
|
onCopy={() => {
|
||||||
navigator.clipboard.writeText(
|
copyToClipboard(
|
||||||
`${currentService?.auth_provider?.sso?.url}/?provider=${currentService?.id}&product=coco&request_id=${ssoRequestID}`
|
`${currentService?.auth_provider?.sso?.url}/?provider=${currentService?.id}&product=coco&request_id=${ssoRequestID}`
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
@@ -222,8 +228,14 @@ const ServiceAuth = memo(
|
|||||||
|
|
||||||
export default ServiceAuth;
|
export default ServiceAuth;
|
||||||
|
|
||||||
const LoginButton = memo(({ LoginClick }: { LoginClick: () => void }) => {
|
interface LoginButtonProps {
|
||||||
|
LoginClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LoginButton: FC<LoginButtonProps> = memo((props) => {
|
||||||
|
const { LoginClick } = props;
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className="px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors mb-3"
|
className="px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors mb-3"
|
||||||
@@ -235,9 +247,15 @@ const LoginButton = memo(({ LoginClick }: { LoginClick: () => void }) => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const LoadingState = memo(
|
interface LoadingStateProps {
|
||||||
({ onCancel, onCopy }: { onCancel: () => void; onCopy: () => void }) => {
|
onCancel: () => void;
|
||||||
|
onCopy: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LoadingState: FC<LoadingStateProps> = memo((props) => {
|
||||||
|
const { onCancel, onCopy } = props;
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<button
|
<button
|
||||||
@@ -257,5 +275,4 @@ const LoadingState = memo(
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { User, LogOut } from "lucide-react";
|
import { User, LogOut } from "lucide-react";
|
||||||
|
|
||||||
import { UserProfile as UserInfo } from "@/types/server";
|
import { UserProfile as UserInfo } from "@/types/server";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
interface UserProfileProps {
|
interface UserProfileProps {
|
||||||
server: string; //server's id
|
server: string; //server's id
|
||||||
@@ -14,12 +15,21 @@ export function UserProfile({ server, userInfo, onLogout }: UserProfileProps) {
|
|||||||
console.log("Logout", server);
|
console.log("Logout", server);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const [imageLoadError, setImageLoadError] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<div className="w-12 h-12 bg-gray-100 dark:bg-gray-700 rounded-full flex items-center justify-center">
|
<div className="w-12 h-12 bg-gray-100 dark:bg-gray-700 rounded-full flex items-center justify-center">
|
||||||
{userInfo?.avatar ? (
|
{userInfo?.avatar && !imageLoadError ? (
|
||||||
<img src={userInfo?.avatar} alt="" className="w-6 h-6" />
|
<img
|
||||||
|
src={userInfo?.avatar}
|
||||||
|
alt=""
|
||||||
|
className="w-6 h-6"
|
||||||
|
onError={() => {
|
||||||
|
setImageLoadError(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<User className="w-6 h-6 text-gray-500 dark:text-gray-400" />
|
<User className="w-6 h-6 text-gray-500 dark:text-gray-400" />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Copy, Check } from "lucide-react";
|
import { Copy, Check } from "lucide-react";
|
||||||
|
|
||||||
|
import { copyToClipboard } from "@/utils";
|
||||||
|
|
||||||
interface CopyButtonProps {
|
interface CopyButtonProps {
|
||||||
textToCopy: string;
|
textToCopy: string;
|
||||||
}
|
}
|
||||||
@@ -10,7 +12,7 @@ export const CopyButton = ({ textToCopy }: CopyButtonProps) => {
|
|||||||
|
|
||||||
const handleCopy = async () => {
|
const handleCopy = async () => {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(textToCopy);
|
await copyToClipboard(textToCopy);
|
||||||
setCopied(true);
|
setCopied(true);
|
||||||
const timerID = setTimeout(() => {
|
const timerID = setTimeout(() => {
|
||||||
setCopied(false);
|
setCopied(false);
|
||||||
|
|||||||
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;
|
||||||
106
src/components/Common/HistoryList/DeleteDialog.tsx
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Description,
|
||||||
|
Dialog,
|
||||||
|
DialogPanel,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@headlessui/react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import VisibleKey from "@/components/Common/VisibleKey";
|
||||||
|
import { Chat } from "@/types/chat";
|
||||||
|
import { KeyboardEvent } from "react";
|
||||||
|
|
||||||
|
interface DeleteDialogProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
active?: Chat;
|
||||||
|
setIsOpen: (isOpen: boolean) => void;
|
||||||
|
handleRemove: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DeleteDialog = ({
|
||||||
|
isOpen,
|
||||||
|
active,
|
||||||
|
setIsOpen,
|
||||||
|
handleRemove,
|
||||||
|
}: DeleteDialogProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const handleEnter = (event: KeyboardEvent, cb: () => void) => {
|
||||||
|
if (event.code !== "Enter") return;
|
||||||
|
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
cb();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open={isOpen}
|
||||||
|
onClose={() => setIsOpen(false)}
|
||||||
|
className="relative z-1000"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
id="headlessui-popover-panel:delete-history"
|
||||||
|
className="fixed inset-0 flex items-center justify-center w-screen"
|
||||||
|
>
|
||||||
|
<DialogPanel className="flex flex-col justify-between w-[360px] h-[160px] p-3 text-[#333] dark:text-white/90 border border-[#e6e6e6] bg-white dark:bg-[#202126] dark:border-white/10 shadow-xl rounded-lg">
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<DialogTitle className="text-base font-bold">
|
||||||
|
{t("history_list.delete_modal.title")}
|
||||||
|
</DialogTitle>
|
||||||
|
<Description className="text-sm">
|
||||||
|
{t("history_list.delete_modal.description", {
|
||||||
|
replace: [
|
||||||
|
active?._source?.title ||
|
||||||
|
active?._source?.message ||
|
||||||
|
active?._id,
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
</Description>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-4 self-end">
|
||||||
|
<VisibleKey
|
||||||
|
shortcut="N"
|
||||||
|
shortcutClassName="left-[unset] right-0"
|
||||||
|
onKeyPress={() => setIsOpen(false)}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
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>
|
||||||
|
</VisibleKey>
|
||||||
|
|
||||||
|
<VisibleKey
|
||||||
|
shortcut="Y"
|
||||||
|
shortcutClassName="left-[unset] right-0"
|
||||||
|
onKeyPress={handleRemove}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
className="h-8 px-4 text-sm text-white bg-[#EF4444] rounded-lg 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>
|
||||||
|
</VisibleKey>
|
||||||
|
</div>
|
||||||
|
</DialogPanel>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DeleteDialog;
|
||||||
248
src/components/Common/HistoryList/HistoryListContent.tsx
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
import { FC, useCallback, useState, useEffect, useRef, useMemo } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useKeyPress } from "ahooks";
|
||||||
|
import { debounce, groupBy, isNil } from "lodash-es";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import isSameOrAfter from "dayjs/plugin/isSameOrAfter";
|
||||||
|
|
||||||
|
import type { Chat } from "@/types/chat";
|
||||||
|
import NoDataImage from "../NoDataImage";
|
||||||
|
import DeleteDialog from "./DeleteDialog";
|
||||||
|
import HistoryListItem from "./HistoryListItem";
|
||||||
|
|
||||||
|
dayjs.extend(isSameOrAfter);
|
||||||
|
|
||||||
|
interface HistoryListContentProps {
|
||||||
|
chats: Chat[];
|
||||||
|
active?: Chat;
|
||||||
|
onSelect: (chat: Chat) => void;
|
||||||
|
onRename: (chatId: string, title: string) => void;
|
||||||
|
onRemove: (chatId: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const HistoryListContent: FC<HistoryListContentProps> = ({
|
||||||
|
chats,
|
||||||
|
active,
|
||||||
|
onSelect,
|
||||||
|
onRename,
|
||||||
|
onRemove,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const listRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [highlightId, setHighlightId] = useState<string>("");
|
||||||
|
const [highlightItem, setHighlightItem] = useState<Chat>({} as Chat);
|
||||||
|
|
||||||
|
const sortedList = useMemo(() => {
|
||||||
|
if (isNil(chats)) return {};
|
||||||
|
|
||||||
|
const now = dayjs();
|
||||||
|
|
||||||
|
return groupBy(chats, (chat) => {
|
||||||
|
const date = dayjs(chat._source?.created);
|
||||||
|
|
||||||
|
if (date.isSame(now, "day")) {
|
||||||
|
return "history_list.date.today";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (date.isSame(now.subtract(1, "day"), "day")) {
|
||||||
|
return "history_list.date.yesterday";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (date.isSameOrAfter(now.subtract(7, "day"), "day")) {
|
||||||
|
return "history_list.date.last7Days";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (date.isSameOrAfter(now.subtract(30, "day"), "day")) {
|
||||||
|
return "history_list.date.last30Days";
|
||||||
|
}
|
||||||
|
|
||||||
|
return date.format("YYYY-MM");
|
||||||
|
});
|
||||||
|
}, [chats]);
|
||||||
|
|
||||||
|
// Flatten sorted list for navigation while keeping original structure for display
|
||||||
|
const flattenedChats = useMemo(() => {
|
||||||
|
return Object.values(sortedList).flat();
|
||||||
|
}, [sortedList]);
|
||||||
|
|
||||||
|
useKeyPress(["uparrow", "downarrow", "enter"], (_, key) => {
|
||||||
|
const currentIndex = flattenedChats.findIndex(
|
||||||
|
(chat) => chat._id === highlightId
|
||||||
|
);
|
||||||
|
const length = flattenedChats.length;
|
||||||
|
|
||||||
|
if (length === 0) return;
|
||||||
|
|
||||||
|
let nextIndex = currentIndex;
|
||||||
|
|
||||||
|
switch (key) {
|
||||||
|
case "uparrow":
|
||||||
|
nextIndex = currentIndex <= 0 ? length - 1 : currentIndex - 1;
|
||||||
|
setHighlightId(flattenedChats[nextIndex]._id);
|
||||||
|
break;
|
||||||
|
case "downarrow":
|
||||||
|
nextIndex = currentIndex >= length - 1 ? 0 : currentIndex + 1;
|
||||||
|
setHighlightId(flattenedChats[nextIndex]._id);
|
||||||
|
break;
|
||||||
|
case "enter":
|
||||||
|
if (document.activeElement instanceof HTMLTextAreaElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (currentIndex >= 0) {
|
||||||
|
onSelect(flattenedChats[currentIndex]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleRemove = () => {
|
||||||
|
if (!highlightId) return;
|
||||||
|
|
||||||
|
onRemove(highlightId);
|
||||||
|
|
||||||
|
setIsOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add ref for observer
|
||||||
|
const observerRef = useRef<IntersectionObserver | null>(null);
|
||||||
|
|
||||||
|
// Separate scroll handlers for keyboard and mouse
|
||||||
|
const scrollToElement = useCallback(
|
||||||
|
(elementId: string, isKeyboardNav: boolean) => {
|
||||||
|
if (!listRef.current) return;
|
||||||
|
if (typeof window === 'undefined' || typeof document === 'undefined') return;
|
||||||
|
|
||||||
|
const element = listRef.current.querySelector(`#${elementId}`);
|
||||||
|
if (!element) return;
|
||||||
|
|
||||||
|
// Check if element is in viewport
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
const isVisible =
|
||||||
|
rect.top >= 0 &&
|
||||||
|
rect.bottom <=
|
||||||
|
(window.innerHeight || document.documentElement.clientHeight);
|
||||||
|
|
||||||
|
// Only scroll if element is not visible
|
||||||
|
if (!isVisible) {
|
||||||
|
element.scrollIntoView({
|
||||||
|
behavior: isKeyboardNav ? "smooth" : "auto",
|
||||||
|
block: isKeyboardNav ? "nearest" : "center",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Debounced scroll for mouse hover
|
||||||
|
const debouncedMouseScroll = useCallback(
|
||||||
|
debounce((elementId: string) => scrollToElement(elementId, false), 150),
|
||||||
|
[scrollToElement]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Immediate scroll for keyboard navigation
|
||||||
|
const keyboardScroll = useCallback(
|
||||||
|
(elementId: string) => {
|
||||||
|
scrollToElement(elementId, true);
|
||||||
|
},
|
||||||
|
[scrollToElement]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Setup intersection observer
|
||||||
|
useEffect(() => {
|
||||||
|
if (!listRef.current) return;
|
||||||
|
|
||||||
|
observerRef.current = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
entries.forEach((entry) => {
|
||||||
|
if (!entry.isIntersecting && entry.target.id === highlightId) {
|
||||||
|
scrollToElement(highlightId, false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ threshold: 0.5 }
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
observerRef.current?.disconnect();
|
||||||
|
};
|
||||||
|
}, [scrollToElement]);
|
||||||
|
|
||||||
|
// Handle highlight changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (!highlightId) return;
|
||||||
|
|
||||||
|
// Clear previous observations
|
||||||
|
observerRef.current?.disconnect();
|
||||||
|
|
||||||
|
const element = listRef.current?.querySelector(`#${highlightId}`);
|
||||||
|
if (element) {
|
||||||
|
observerRef.current?.observe(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isKeyboardNav = document.activeElement?.tagName !== "LI";
|
||||||
|
if (isKeyboardNav) {
|
||||||
|
keyboardScroll(highlightId);
|
||||||
|
} else {
|
||||||
|
debouncedMouseScroll(highlightId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
debouncedMouseScroll.cancel();
|
||||||
|
};
|
||||||
|
}, [highlightId, keyboardScroll, debouncedMouseScroll]);
|
||||||
|
|
||||||
|
const handleDelete = useCallback(() => {
|
||||||
|
if (!highlightId) return;
|
||||||
|
const currentIndex = flattenedChats.findIndex(
|
||||||
|
(chat) => chat._id === highlightId
|
||||||
|
);
|
||||||
|
setHighlightItem(flattenedChats[currentIndex]);
|
||||||
|
setIsOpen(true);
|
||||||
|
}, [highlightId]);
|
||||||
|
|
||||||
|
if (chats.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center flex-1 pt-8">
|
||||||
|
<NoDataImage />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div ref={listRef} className="py-4">
|
||||||
|
{Object.entries(sortedList).map(([label, list]) => (
|
||||||
|
<div key={label}>
|
||||||
|
<span className="text-xs text-[#999] px-3">{t(label)}</span>
|
||||||
|
<ul className="p-0">
|
||||||
|
{list.map((item) => (
|
||||||
|
<HistoryListItem
|
||||||
|
key={item._id}
|
||||||
|
item={item}
|
||||||
|
active={active}
|
||||||
|
onSelect={onSelect}
|
||||||
|
onRename={onRename}
|
||||||
|
onMouseEnter={() => setHighlightId(item._id)}
|
||||||
|
highlightId={highlightId}
|
||||||
|
handleDelete={handleDelete}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DeleteDialog
|
||||||
|
isOpen={isOpen}
|
||||||
|
active={active || highlightItem}
|
||||||
|
setIsOpen={setIsOpen}
|
||||||
|
handleRemove={handleRemove}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HistoryListContent;
|
||||||
194
src/components/Common/HistoryList/HistoryListItem.tsx
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
import { FC, useRef, useCallback, useState } from "react";
|
||||||
|
import { Input, Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
|
||||||
|
import { Ellipsis } from "lucide-react";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Pencil, Trash2 } from "lucide-react";
|
||||||
|
|
||||||
|
import type { Chat } from "@/types/chat";
|
||||||
|
import VisibleKey from "../VisibleKey";
|
||||||
|
|
||||||
|
interface HistoryListItemProps {
|
||||||
|
item: Chat;
|
||||||
|
active?: Chat;
|
||||||
|
onSelect: (chat: Chat) => void;
|
||||||
|
onRename: (chatId: string, title: string) => void;
|
||||||
|
onMouseEnter: () => void;
|
||||||
|
handleDelete: () => void;
|
||||||
|
highlightId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const HistoryListItem: FC<HistoryListItemProps> = ({
|
||||||
|
item,
|
||||||
|
active,
|
||||||
|
onSelect,
|
||||||
|
onRename,
|
||||||
|
onMouseEnter,
|
||||||
|
highlightId,
|
||||||
|
handleDelete,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const moreButtonRef = useRef<HTMLButtonElement>(null);
|
||||||
|
const { _id, _source } = item;
|
||||||
|
const title = _source?.title ?? _id;
|
||||||
|
const isActive = item._id === active?._id || item._id === highlightId;
|
||||||
|
|
||||||
|
const [isEdit, setIsEdit] = useState(false);
|
||||||
|
|
||||||
|
const onContextMenu = useCallback(
|
||||||
|
(e: React.MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
moreButtonRef.current?.click();
|
||||||
|
},
|
||||||
|
[moreButtonRef.current]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleRename = useCallback(() => {
|
||||||
|
if (highlightId) {
|
||||||
|
setIsEdit(true);
|
||||||
|
}
|
||||||
|
}, [highlightId]);
|
||||||
|
|
||||||
|
const menuItems = [
|
||||||
|
// {
|
||||||
|
// label: "history_list.menu.share",
|
||||||
|
// icon: Share2,
|
||||||
|
// onClick: () => {},
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
label: "history_list.menu.rename",
|
||||||
|
icon: Pencil,
|
||||||
|
shortcut: "R",
|
||||||
|
onClick: handleRename,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "history_list.menu.delete",
|
||||||
|
icon: Trash2,
|
||||||
|
shortcut: "D",
|
||||||
|
iconColor: "#FF2018",
|
||||||
|
onClick: handleDelete,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
key={_id}
|
||||||
|
id={_id}
|
||||||
|
className={clsx(
|
||||||
|
"flex items-center mt-1 h-10 rounded-lg cursor-pointer hover:bg-[#EDEDED] dark:hover:bg-[#353F4D] transition",
|
||||||
|
{
|
||||||
|
"!bg-[#E5E7EB] dark:!bg-[#2B3444]": isActive,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
if (!isActive) {
|
||||||
|
setIsEdit(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
onSelect(item);
|
||||||
|
}}
|
||||||
|
onMouseEnter={onMouseEnter}
|
||||||
|
onContextMenu={onContextMenu}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={clsx("w-1 h-6 rounded-sm bg-[#0072FF]", {
|
||||||
|
"opacity-0": item._id !== active?._id,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex-1 flex items-center justify-between gap-2 px-2 overflow-hidden">
|
||||||
|
{isEdit && isActive ? (
|
||||||
|
<Input
|
||||||
|
autoFocus
|
||||||
|
defaultValue={title}
|
||||||
|
className="flex-1 -mx-px outline-none bg-transparent border border-[#0061FF] rounded-[4px]"
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key !== "Enter") return;
|
||||||
|
|
||||||
|
const value = event.currentTarget.value;
|
||||||
|
|
||||||
|
onRename(item._id, value);
|
||||||
|
|
||||||
|
setIsEdit(false);
|
||||||
|
}}
|
||||||
|
onBlur={(event) => {
|
||||||
|
const value = event.currentTarget.value;
|
||||||
|
|
||||||
|
onRename(item._id, value);
|
||||||
|
|
||||||
|
setIsEdit(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span className="truncate">{title}</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{isActive && !isEdit && (
|
||||||
|
<VisibleKey
|
||||||
|
shortcut="↑↓"
|
||||||
|
rootClassName="w-6"
|
||||||
|
shortcutClassName="w-6"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Popover>
|
||||||
|
{isActive && !isEdit && (
|
||||||
|
<PopoverButton ref={moreButtonRef} className="flex gap-2">
|
||||||
|
<VisibleKey
|
||||||
|
shortcut="O"
|
||||||
|
onKeyPress={() => {
|
||||||
|
moreButtonRef.current?.click();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Ellipsis className="size-4 text-[#979797]" />
|
||||||
|
</VisibleKey>
|
||||||
|
</PopoverButton>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<PopoverPanel
|
||||||
|
anchor="bottom"
|
||||||
|
className="flex flex-col rounded-lg shadow-md z-100 bg-white dark:bg-[#202126] p-1 border border-black/2 dark:border-white/10"
|
||||||
|
onClick={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{menuItems.map((menuItem) => {
|
||||||
|
const {
|
||||||
|
label,
|
||||||
|
icon: Icon,
|
||||||
|
shortcut,
|
||||||
|
iconColor,
|
||||||
|
onClick,
|
||||||
|
} = menuItem;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={label}
|
||||||
|
className="flex items-center gap-2 px-3 py-2 text-sm rounded-md hover:bg-[#EDEDED] dark:hover:bg-[#2B2C31] transition"
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<VisibleKey shortcut={shortcut} onKeyPress={onClick}>
|
||||||
|
<Icon
|
||||||
|
className="size-4"
|
||||||
|
style={{
|
||||||
|
color: iconColor,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</VisibleKey>
|
||||||
|
|
||||||
|
<span>{t(label)}</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</PopoverPanel>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HistoryListItem;
|
||||||
@@ -1,39 +1,18 @@
|
|||||||
import { useKeyPress } from "ahooks";
|
import { Input } from "@headlessui/react";
|
||||||
import {
|
import { debounce } from "lodash-es";
|
||||||
Description,
|
import { FC, useMemo, useRef, useState } from "react";
|
||||||
Dialog,
|
|
||||||
DialogPanel,
|
|
||||||
DialogTitle,
|
|
||||||
Input,
|
|
||||||
Popover,
|
|
||||||
PopoverButton,
|
|
||||||
PopoverPanel,
|
|
||||||
} from "@headlessui/react";
|
|
||||||
import { debounce, groupBy, isNil } from "lodash-es";
|
|
||||||
import { FC, useEffect, useMemo, useRef, useState } from "react";
|
|
||||||
import dayjs from "dayjs";
|
|
||||||
import isSameOrAfter from "dayjs/plugin/isSameOrAfter";
|
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {
|
import { PanelLeftClose, RefreshCcw, Search } from "lucide-react";
|
||||||
Ellipsis,
|
|
||||||
PanelLeftClose,
|
|
||||||
Pencil,
|
|
||||||
RefreshCcw,
|
|
||||||
Search,
|
|
||||||
Trash2,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import VisibleKey from "../VisibleKey";
|
import VisibleKey from "../VisibleKey";
|
||||||
import { Chat } from "@/types/chat";
|
import { Chat } from "@/types/chat";
|
||||||
import NoDataImage from "../NoDataImage";
|
|
||||||
import { closeHistoryPanel } from "@/utils";
|
import { closeHistoryPanel } from "@/utils";
|
||||||
|
import HistoryListContent from "./HistoryListContent";
|
||||||
dayjs.extend(isSameOrAfter);
|
|
||||||
|
|
||||||
interface HistoryListProps {
|
interface HistoryListProps {
|
||||||
id?: string;
|
historyPanelId?: string;
|
||||||
list: Chat[];
|
chats: Chat[];
|
||||||
active?: Chat;
|
active?: Chat;
|
||||||
onSearch: (keyword: string) => void;
|
onSearch: (keyword: string) => void;
|
||||||
onRefresh: () => void;
|
onRefresh: () => void;
|
||||||
@@ -44,8 +23,8 @@ interface HistoryListProps {
|
|||||||
|
|
||||||
const HistoryList: FC<HistoryListProps> = (props) => {
|
const HistoryList: FC<HistoryListProps> = (props) => {
|
||||||
const {
|
const {
|
||||||
id,
|
historyPanelId,
|
||||||
list,
|
chats,
|
||||||
active,
|
active,
|
||||||
onSearch,
|
onSearch,
|
||||||
onRefresh,
|
onRefresh,
|
||||||
@@ -54,104 +33,13 @@ const HistoryList: FC<HistoryListProps> = (props) => {
|
|||||||
onRemove,
|
onRemove,
|
||||||
} = props;
|
} = props;
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [isEdit, setIsEdit] = useState(false);
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
const listRef = useRef<HTMLDivElement>(null);
|
|
||||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||||
const moreButtonRef = useRef<HTMLButtonElement>(null);
|
|
||||||
const [isRefresh, setIsRefresh] = useState(false);
|
const [isRefresh, setIsRefresh] = useState(false);
|
||||||
|
|
||||||
const sortedList = useMemo(() => {
|
|
||||||
if (isNil(list)) return {};
|
|
||||||
|
|
||||||
const now = dayjs();
|
|
||||||
|
|
||||||
return groupBy(list, (chat) => {
|
|
||||||
const date = dayjs(chat._source?.updated);
|
|
||||||
|
|
||||||
if (date.isSame(now, "day")) {
|
|
||||||
return "history_list.date.today";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (date.isSame(now.subtract(1, "day"), "day")) {
|
|
||||||
return "history_list.date.yesterday";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (date.isSameOrAfter(now.subtract(7, "day"), "day")) {
|
|
||||||
return "history_list.date.last7Days";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (date.isSameOrAfter(now.subtract(30, "day"), "day")) {
|
|
||||||
return "history_list.date.last30Days";
|
|
||||||
}
|
|
||||||
|
|
||||||
return date.format("YYYY-MM");
|
|
||||||
});
|
|
||||||
}, [list]);
|
|
||||||
|
|
||||||
const menuItems = [
|
|
||||||
// {
|
|
||||||
// label: "history_list.menu.share",
|
|
||||||
// icon: Share2,
|
|
||||||
// onClick: () => {},
|
|
||||||
// },
|
|
||||||
{
|
|
||||||
label: "history_list.menu.rename",
|
|
||||||
icon: Pencil,
|
|
||||||
shortcut: "R",
|
|
||||||
onClick: () => {
|
|
||||||
setIsEdit(true);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "history_list.menu.delete",
|
|
||||||
icon: Trash2,
|
|
||||||
shortcut: "D",
|
|
||||||
iconColor: "#FF2018",
|
|
||||||
onClick: () => {
|
|
||||||
setIsOpen(true);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const debouncedSearch = useMemo(() => {
|
const debouncedSearch = useMemo(() => {
|
||||||
return debounce((value: string) => onSearch(value), 300);
|
return debounce((value: string) => onSearch(value), 300);
|
||||||
}, [onSearch]);
|
}, [onSearch]);
|
||||||
|
|
||||||
useKeyPress(["uparrow", "downarrow"], (_, key) => {
|
|
||||||
const index = list.findIndex((item) => item._id === active?._id);
|
|
||||||
const length = list.length;
|
|
||||||
|
|
||||||
let nextIndex = index;
|
|
||||||
|
|
||||||
switch (key) {
|
|
||||||
case "uparrow":
|
|
||||||
nextIndex = index === 0 ? length - 1 : index - 1;
|
|
||||||
break;
|
|
||||||
case "downarrow":
|
|
||||||
nextIndex = index === length - 1 ? 0 : index + 1;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
onSelect(list[nextIndex]);
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!active?._id || !listRef.current) return;
|
|
||||||
|
|
||||||
const activeEl = listRef.current.querySelector(`#${active._id}`);
|
|
||||||
|
|
||||||
activeEl?.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
||||||
}, [active?._id]);
|
|
||||||
|
|
||||||
const handleRemove = () => {
|
|
||||||
if (!active?._id) return;
|
|
||||||
|
|
||||||
onRemove(active._id);
|
|
||||||
|
|
||||||
setIsOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRefresh = async () => {
|
const handleRefresh = async () => {
|
||||||
setIsRefresh(true);
|
setIsRefresh(true);
|
||||||
|
|
||||||
@@ -164,8 +52,7 @@ const HistoryList: FC<HistoryListProps> = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={listRef}
|
id={historyPanelId}
|
||||||
id={id}
|
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"flex flex-col h-screen text-sm bg-[#F3F4F6] dark:bg-[#1F2937]"
|
"flex flex-col h-screen text-sm bg-[#F3F4F6] dark:bg-[#1F2937]"
|
||||||
)}
|
)}
|
||||||
@@ -207,210 +94,16 @@ const HistoryList: FC<HistoryListProps> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 px-2 overflow-auto custom-scrollbar">
|
<div className="flex-1 px-2 overflow-auto custom-scrollbar">
|
||||||
{list.length > 0 ? (
|
<HistoryListContent
|
||||||
<>
|
chats={chats}
|
||||||
<div className="mt-6">
|
active={active}
|
||||||
{Object.entries(sortedList).map(([label, list]) => {
|
onSelect={onSelect}
|
||||||
return (
|
onRename={onRename}
|
||||||
<div key={label}>
|
onRemove={onRemove}
|
||||||
<span className="text-xs text-[#999] px-3">{t(label)}</span>
|
|
||||||
|
|
||||||
<ul className="p-0">
|
|
||||||
{list.map((item) => {
|
|
||||||
const { _id, _source } = item;
|
|
||||||
|
|
||||||
const isActive = _id === active?._id;
|
|
||||||
const title = _source?.title ?? _id;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<li
|
|
||||||
key={_id}
|
|
||||||
id={_id}
|
|
||||||
className={clsx(
|
|
||||||
"flex items-center mt-1 h-10 rounded-lg cursor-pointer hover:bg-[#EDEDED] dark:hover:bg-[#353F4D] transition",
|
|
||||||
{
|
|
||||||
"!bg-[#E5E7EB] dark:!bg-[#2B3444]": isActive,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
onClick={() => {
|
|
||||||
if (!isActive) {
|
|
||||||
setIsEdit(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
onSelect(item);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={clsx(
|
|
||||||
"w-1 h-6 rounded-sm bg-[#0072FF]",
|
|
||||||
{
|
|
||||||
"opacity-0": _id !== active?._id,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex-1 flex items-center justify-between gap-2 px-2 overflow-hidden">
|
|
||||||
{isEdit && isActive ? (
|
|
||||||
<Input
|
|
||||||
autoFocus
|
|
||||||
defaultValue={title}
|
|
||||||
className="flex-1 -mx-px outline-none bg-transparent border border-[#0061FF] rounded-[4px]"
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
if (event.key !== "Enter") return;
|
|
||||||
|
|
||||||
onRename(
|
|
||||||
item._id,
|
|
||||||
event.currentTarget.value
|
|
||||||
);
|
|
||||||
|
|
||||||
setIsEdit(false);
|
|
||||||
}}
|
|
||||||
onBlur={(event) => {
|
|
||||||
onRename(item._id, event.target.value);
|
|
||||||
|
|
||||||
setIsEdit(false);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<span className="truncate">{title}</span>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{isActive && !isEdit && (
|
|
||||||
<VisibleKey
|
|
||||||
shortcut="↑↓"
|
|
||||||
rootClassName="w-6"
|
|
||||||
shortcutClassName="w-6"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Popover>
|
|
||||||
{isActive && !isEdit && (
|
|
||||||
<PopoverButton
|
|
||||||
ref={moreButtonRef}
|
|
||||||
className="flex gap-2"
|
|
||||||
>
|
|
||||||
<VisibleKey
|
|
||||||
shortcut="O"
|
|
||||||
onKeyPress={() => {
|
|
||||||
moreButtonRef.current?.click();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Ellipsis className="size-4 text-[#979797]" />
|
|
||||||
</VisibleKey>
|
|
||||||
</PopoverButton>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<PopoverPanel
|
|
||||||
anchor="bottom"
|
|
||||||
className="flex flex-col rounded-lg shadow-md z-100 bg-white dark:bg-[#202126] p-1 border border-black/2 dark:border-white/10"
|
|
||||||
onClick={(event) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{menuItems.map((menuItem) => {
|
|
||||||
const {
|
|
||||||
label,
|
|
||||||
icon: Icon,
|
|
||||||
shortcut,
|
|
||||||
iconColor,
|
|
||||||
onClick,
|
|
||||||
} = menuItem;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={label}
|
|
||||||
className="flex items-center gap-2 px-3 py-2 text-sm rounded-md hover:bg-[#EDEDED] dark:hover:bg-[#2B2C31] transition"
|
|
||||||
onClick={onClick}
|
|
||||||
>
|
|
||||||
<VisibleKey
|
|
||||||
shortcut={shortcut}
|
|
||||||
onKeyPress={onClick}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
className="size-4"
|
|
||||||
style={{
|
|
||||||
color: iconColor,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</VisibleKey>
|
|
||||||
|
|
||||||
<span>{t(label)}</span>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</PopoverPanel>
|
|
||||||
</Popover>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Dialog
|
|
||||||
open={isOpen}
|
|
||||||
onClose={() => setIsOpen(false)}
|
|
||||||
className="relative z-1000"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
id="headlessui-popover-panel:delete-history"
|
|
||||||
className="fixed inset-0 flex items-center justify-center w-screen"
|
|
||||||
>
|
|
||||||
<DialogPanel className="flex flex-col justify-between w-[360px] h-[160px] p-3 text-[#333] dark:text-white/90 border border-[#e6e6e6] bg-white dark:bg-[#202126] dark:border-white/10 shadow-xl rounded-lg">
|
|
||||||
<div className="flex flex-col gap-3">
|
|
||||||
<DialogTitle className="text-base font-bold">
|
|
||||||
{t("history_list.delete_modal.title")}
|
|
||||||
</DialogTitle>
|
|
||||||
<Description className="text-sm">
|
|
||||||
{t("history_list.delete_modal.description", {
|
|
||||||
replace: [active?._source?.title || active?._source?.message || active?._id],
|
|
||||||
})}
|
|
||||||
</Description>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex gap-4 self-end">
|
|
||||||
<VisibleKey
|
|
||||||
shortcut="N"
|
|
||||||
shortcutClassName="left-[unset] right-0"
|
|
||||||
onKeyPress={() => setIsOpen(false)}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
className="h-8 px-4 text-sm text-[#666666] bg-[#F8F9FA] dark:text-white dark:bg-[#202126] border border-[#E6E6E6] dark:border-white/10 rounded-lg"
|
|
||||||
onClick={() => setIsOpen(false)}
|
|
||||||
>
|
|
||||||
{t("history_list.delete_modal.button.cancel")}
|
|
||||||
</button>
|
|
||||||
</VisibleKey>
|
|
||||||
|
|
||||||
<VisibleKey
|
|
||||||
shortcut="Y"
|
|
||||||
shortcutClassName="left-[unset] right-0"
|
|
||||||
onKeyPress={handleRemove}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
className="h-8 px-4 text-sm text-white bg-[#EF4444] rounded-lg"
|
|
||||||
onClick={handleRemove}
|
|
||||||
>
|
|
||||||
{t("history_list.delete_modal.button.delete")}
|
|
||||||
</button>
|
|
||||||
</VisibleKey>
|
|
||||||
</div>
|
|
||||||
</DialogPanel>
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<div className="flex items-center justify-center flex-1 pt-8">
|
|
||||||
<NoDataImage />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{historyPanelId && (
|
||||||
<div className="flex justify-end p-2 border-t dark:border-[#343D4D]">
|
<div className="flex justify-end p-2 border-t dark:border-[#343D4D]">
|
||||||
<VisibleKey shortcut="Esc" shortcutClassName="w-7">
|
<VisibleKey shortcut="Esc" shortcutClassName="w-7">
|
||||||
<PanelLeftClose
|
<PanelLeftClose
|
||||||
@@ -419,6 +112,7 @@ const HistoryList: FC<HistoryListProps> = (props) => {
|
|||||||
/>
|
/>
|
||||||
</VisibleKey>
|
</VisibleKey>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { isEmpty } from "lodash-es";
|
import { isNil, isEmpty } from "lodash-es";
|
||||||
import { useAsyncEffect } from "ahooks";
|
import { useAsyncEffect } from "ahooks";
|
||||||
import { Box } from "lucide-react";
|
import { Box } from "lucide-react";
|
||||||
|
|
||||||
@@ -27,9 +27,6 @@ function CommonIcon({
|
|||||||
const connectorSource = useFindConnectorIcon(item);
|
const connectorSource = useFindConnectorIcon(item);
|
||||||
|
|
||||||
const [isAbsolute, setIsAbsolute] = useState<boolean>();
|
const [isAbsolute, setIsAbsolute] = useState<boolean>();
|
||||||
const [defaultIconState, setDefaultIconState] = useState<
|
|
||||||
React.FC | string | undefined
|
|
||||||
>(defaultIcon);
|
|
||||||
|
|
||||||
useAsyncEffect(async () => {
|
useAsyncEffect(async () => {
|
||||||
if (isEmpty(item)) return;
|
if (isEmpty(item)) return;
|
||||||
@@ -39,7 +36,6 @@ function CommonIcon({
|
|||||||
omitSize: true,
|
omitSize: true,
|
||||||
});
|
});
|
||||||
setIsAbsolute(Boolean(isAbsolute));
|
setIsAbsolute(Boolean(isAbsolute));
|
||||||
setDefaultIconState(defaultIcon || Box);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setIsAbsolute(false);
|
setIsAbsolute(false);
|
||||||
}
|
}
|
||||||
@@ -47,6 +43,8 @@ function CommonIcon({
|
|||||||
|
|
||||||
// Handle regular icon types
|
// Handle regular icon types
|
||||||
const renderIconByType = (renderType: string) => {
|
const renderIconByType = (renderType: string) => {
|
||||||
|
if (isNil(isAbsolute)) return null;
|
||||||
|
|
||||||
switch (renderType) {
|
switch (renderType) {
|
||||||
case "special_icon": {
|
case "special_icon": {
|
||||||
if (item.id === "Calculator") {
|
if (item.id === "Calculator") {
|
||||||
@@ -95,7 +93,7 @@ function CommonIcon({
|
|||||||
case "default_icon":
|
case "default_icon":
|
||||||
return (
|
return (
|
||||||
<UniversalIcon
|
<UniversalIcon
|
||||||
defaultIcon={defaultIconState}
|
defaultIcon={defaultIcon || Box}
|
||||||
className={className}
|
className={className}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useAppStore } from "@/stores/appStore";
|
import { useAppStore } from "@/stores/appStore";
|
||||||
import logoImg from "@/assets/icon.svg";
|
import logoImg from "@/assets/icon.svg";
|
||||||
|
import { FC } from "react";
|
||||||
|
|
||||||
interface FontIconProps {
|
interface FontIconProps {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -7,7 +8,7 @@ interface FontIconProps {
|
|||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FontIcon = ({ name, className, style, ...rest }: FontIconProps) => {
|
const FontIcon: FC<FontIconProps> = ({ name, className, style, ...rest }) => {
|
||||||
const isTauri = useAppStore((state) => state.isTauri);
|
const isTauri = useAppStore((state) => state.isTauri);
|
||||||
|
|
||||||
if (isTauri) {
|
if (isTauri) {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { POPOVER_PANEL_SELECTOR } from "@/constants";
|
|
||||||
import { Input, InputProps } from "@headlessui/react";
|
import { Input, InputProps } from "@headlessui/react";
|
||||||
import { useKeyPress } from "ahooks";
|
import { useKeyPress } from "ahooks";
|
||||||
import { forwardRef, useImperativeHandle, useRef } from "react";
|
import { forwardRef, useImperativeHandle, useRef } from "react";
|
||||||
|
|
||||||
|
import { POPOVER_PANEL_SELECTOR } from "@/constants";
|
||||||
|
|
||||||
const PopoverInput = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
|
const PopoverInput = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
@@ -28,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;
|
export default PopoverInput;
|
||||||
|
|||||||
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
@@ -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 source_default_dark_img from "@/assets/images/source_default_dark.png";
|
||||||
import { useThemeStore } from "@/stores/themeStore";
|
import { useThemeStore } from "@/stores/themeStore";
|
||||||
import platformAdapter from "@/utils/platformAdapter";
|
import platformAdapter from "@/utils/platformAdapter";
|
||||||
|
import FontIcon from "../Icons/FontIcon";
|
||||||
|
|
||||||
interface FooterProps {
|
interface FooterProps {
|
||||||
setIsPinnedWeb?: (value: boolean) => void;
|
setIsPinnedWeb?: (value: boolean) => void;
|
||||||
@@ -26,7 +27,13 @@ interface FooterProps {
|
|||||||
export default function Footer({ setIsPinnedWeb }: FooterProps) {
|
export default function Footer({ setIsPinnedWeb }: FooterProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { sourceData, goAskAi } = useSearchStore();
|
const {
|
||||||
|
sourceData,
|
||||||
|
goAskAi,
|
||||||
|
selectedExtension,
|
||||||
|
visibleExtensionStore,
|
||||||
|
visibleExtensionDetail,
|
||||||
|
} = useSearchStore();
|
||||||
|
|
||||||
const isDark = useThemeStore((state) => state.isDark);
|
const isDark = useThemeStore((state) => state.isDark);
|
||||||
|
|
||||||
@@ -56,38 +63,49 @@ export default function Footer({ setIsPinnedWeb }: FooterProps) {
|
|||||||
return platformAdapter.emitEvent("open_settings", "");
|
return platformAdapter.emitEvent("open_settings", "");
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const renderLeft = () => {
|
||||||
|
if (sourceData?.source?.name) {
|
||||||
return (
|
return (
|
||||||
<div
|
|
||||||
data-tauri-drag-region={isTauri}
|
|
||||||
className="px-4 z-999 mx-[1px] h-8 absolute bottom-0 left-0 right-0 border-t border-gray-200 dark:border-gray-700 flex items-center justify-between rounded-md rounded-t-none overflow-hidden"
|
|
||||||
>
|
|
||||||
{isTauri ? (
|
|
||||||
<div className="flex items-center">
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
{sourceData?.source?.name ? (
|
|
||||||
<CommonIcon
|
<CommonIcon
|
||||||
item={sourceData}
|
item={sourceData}
|
||||||
renderOrder={["connector_icon", "default_icon"]}
|
renderOrder={["connector_icon", "default_icon"]}
|
||||||
itemIcon={sourceData?.source?.icon}
|
itemIcon={sourceData?.source?.icon}
|
||||||
defaultIcon={
|
defaultIcon={isDark ? source_default_dark_img : source_default_img}
|
||||||
isDark ? source_default_dark_img : source_default_img
|
|
||||||
}
|
|
||||||
className="w-4 h-4"
|
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
|
<img
|
||||||
src={logoImg}
|
src={logoImg}
|
||||||
className="w-4 h-4 cursor-pointer"
|
className="w-4 h-4 cursor-pointer"
|
||||||
onClick={openSetting}
|
onClick={openSetting}
|
||||||
alt={t("search.footer.logoAlt")}
|
alt={t("search.footer.logoAlt")}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
<div className="relative text-xs text-gray-500 dark:text-gray-400">
|
<div className="relative text-xs text-gray-500 dark:text-gray-400">
|
||||||
{updateInfo?.available ? (
|
{updateInfo?.available ? (
|
||||||
<div
|
<div className="cursor-pointer" onClick={() => setVisible(true)}>
|
||||||
className="cursor-pointer"
|
|
||||||
onClick={() => setVisible(true)}
|
|
||||||
>
|
|
||||||
<span>{t("search.footer.updateAvailable")}</span>
|
<span>{t("search.footer.updateAvailable")}</span>
|
||||||
<span className="absolute top-0 -right-2 size-1.5 bg-[#FF3434] rounded-full"></span>
|
<span className="absolute top-0 -right-2 size-1.5 bg-[#FF3434] rounded-full"></span>
|
||||||
</div>
|
</div>
|
||||||
@@ -98,6 +116,19 @@ export default function Footer({ setIsPinnedWeb }: FooterProps) {
|
|||||||
})
|
})
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-tauri-drag-region={isTauri}
|
||||||
|
className="px-4 z-999 mx-[1px] h-8 absolute bottom-0 left-0 right-0 border-t border-gray-200 dark:border-gray-700 flex items-center justify-between rounded-md rounded-t-none overflow-hidden"
|
||||||
|
>
|
||||||
|
{isTauri ? (
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
{renderLeft()}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={togglePin}
|
onClick={togglePin}
|
||||||
@@ -117,12 +148,23 @@ export default function Footer({ setIsPinnedWeb }: FooterProps) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={`flex mobile:hidden items-center gap-3`}>
|
<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">
|
<span className="mr-1.5">
|
||||||
{goAskAi
|
{goAskAi
|
||||||
? t("search.askCocoAi.continueInChat")
|
? t("search.askCocoAi.continueInChat")
|
||||||
|
: (visibleExtensionStore || visibleExtensionDetail) && !selectedExtension?.installed
|
||||||
|
? t("search.footer.install")
|
||||||
: t("search.footer.select")}
|
: t("search.footer.select")}
|
||||||
:
|
|
||||||
</span>
|
</span>
|
||||||
<kbd className="coco-modal-footer-commands-key pr-1">
|
<kbd className="coco-modal-footer-commands-key pr-1">
|
||||||
<div className="flex items-center justify-center min-w-3 h-3">
|
<div className="flex items-center justify-center min-w-3 h-3">
|
||||||
@@ -131,16 +173,32 @@ export default function Footer({ setIsPinnedWeb }: FooterProps) {
|
|||||||
</kbd>
|
</kbd>
|
||||||
+
|
+
|
||||||
<kbd className="coco-modal-footer-commands-key pr-1">
|
<kbd className="coco-modal-footer-commands-key pr-1">
|
||||||
{goAskAi ? (
|
{goAskAi || selectedExtension ? (
|
||||||
<CornerDownLeft className="w-3 h-3" />
|
<CornerDownLeft className="w-3 h-3" />
|
||||||
) : (
|
) : (
|
||||||
<ArrowDown01 className="w-3 h-3" />
|
<ArrowDown01 className="w-3 h-3" />
|
||||||
)}
|
)}
|
||||||
</kbd>
|
</kbd>
|
||||||
</div>
|
</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">
|
<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>
|
</span>
|
||||||
<kbd className="coco-modal-footer-commands-key pr-1">
|
<kbd className="coco-modal-footer-commands-key pr-1">
|
||||||
<CornerDownLeft className="w-3 h-3" />
|
<CornerDownLeft className="w-3 h-3" />
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import { useTranslation } from "react-i18next";
|
|||||||
|
|
||||||
import { isMac } from "@/utils/platform";
|
import { isMac } from "@/utils/platform";
|
||||||
import { useShortcutsStore } from "@/stores/shortcutsStore";
|
import { useShortcutsStore } from "@/stores/shortcutsStore";
|
||||||
import noDataImg from "@/assets/coconut-tree.png";
|
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { formatKey } from "@/utils/keyboardUtils";
|
import { formatKey } from "@/utils/keyboardUtils";
|
||||||
|
import SearchEmpty from "../SearchEmpty";
|
||||||
|
|
||||||
export const NoResults = () => {
|
export const NoResults = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -17,10 +17,8 @@ export const NoResults = () => {
|
|||||||
data-tauri-drag-region
|
data-tauri-drag-region
|
||||||
className="h-full w-full flex flex-col justify-center items-center"
|
className="h-full w-full flex flex-col justify-center items-center"
|
||||||
>
|
>
|
||||||
<img src={noDataImg} alt="no-data" className="w-16 h-16" />
|
<SearchEmpty />
|
||||||
<div className="mt-4 text-sm text-[#999] dark:text-[#666]">
|
|
||||||
{t("search.main.noResults")}
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
className={`flex mobile:hidden mt-10 text-sm text-[#333] dark:text-[#D8D8D8]`}
|
className={`flex mobile:hidden mt-10 text-sm text-[#333] dark:text-[#D8D8D8]`}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { last } from "lodash-es";
|
|||||||
|
|
||||||
import { POPOVER_PANEL_SELECTOR } from "@/constants";
|
import { POPOVER_PANEL_SELECTOR } from "@/constants";
|
||||||
import { useShortcutsStore } from "@/stores/shortcutsStore";
|
import { useShortcutsStore } from "@/stores/shortcutsStore";
|
||||||
|
import { useAppStore } from "@/stores/appStore";
|
||||||
|
|
||||||
interface VisibleKeyProps extends HTMLAttributes<HTMLDivElement> {
|
interface VisibleKeyProps extends HTMLAttributes<HTMLDivElement> {
|
||||||
shortcut: string;
|
shortcut: string;
|
||||||
@@ -23,6 +24,8 @@ const VisibleKey: FC<VisibleKeyProps> = (props) => {
|
|||||||
...rest
|
...rest
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
const showTooltip = useAppStore((state) => state.showTooltip);
|
||||||
|
|
||||||
const modifierKey = useShortcutsStore((state) => {
|
const modifierKey = useShortcutsStore((state) => {
|
||||||
return state.modifierKey;
|
return state.modifierKey;
|
||||||
});
|
});
|
||||||
@@ -82,11 +85,11 @@ const VisibleKey: FC<VisibleKeyProps> = (props) => {
|
|||||||
<div
|
<div
|
||||||
{...rest}
|
{...rest}
|
||||||
ref={childrenRef}
|
ref={childrenRef}
|
||||||
className={clsx(rootClassName, "relative inline-block")}
|
className={clsx(rootClassName, "relative inline-block leading-[100%]")}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
||||||
{visibleShortcut ? (
|
{showTooltip && visibleShortcut ? (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"size-4 flex items-center justify-center font-normal text-xs text-[#333] leading-[14px] bg-[#ccc] dark:bg-[#6B6B6B] rounded-md shadow-[-6px_0px_6px_2px_#fff] dark:shadow-[-6px_0px_6px_2px_#000] absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2",
|
"size-4 flex items-center justify-center font-normal text-xs text-[#333] leading-[14px] bg-[#ccc] dark:bg-[#6B6B6B] rounded-md shadow-[-6px_0px_6px_2px_#fff] dark:shadow-[-6px_0px_6px_2px_#000] absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ChevronUp, Sparkles } from "lucide-react";
|
import { Sparkles, X } from "lucide-react";
|
||||||
import { FC, useState } from "react";
|
import { FC, useState } from "react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { useStreamChat } from "@/hooks/useStreamChat";
|
import { useStreamChat } from "@/hooks/useStreamChat";
|
||||||
@@ -18,7 +18,6 @@ const AiOverview: FC<AiSummaryProps> = (props) => {
|
|||||||
return state.aiOverviewAssistant;
|
return state.aiOverviewAssistant;
|
||||||
});
|
});
|
||||||
|
|
||||||
const [expand, setExpand] = useState(true);
|
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
|
|
||||||
const { isTyping, chunkData, loadingStep } = useStreamChat({
|
const { isTyping, chunkData, loadingStep } = useStreamChat({
|
||||||
@@ -30,21 +29,22 @@ const AiOverview: FC<AiSummaryProps> = (props) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div className={clsx({ "p-2": visible })}>
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"flex flex-col gap-2 relative max-h-[210px] px-4 py-3 rounded-[4px] text-[#333] dark:text-[#D8D8D8] bg-white dark:bg-[#141414] shadow-[0_4px_8px_rgba(0,0,0,0.1)] dark:shadow-[0_4px_20px_rgba(255,255,255,0.2)]",
|
"flex flex-col gap-2 relative max-h-[210px] px-4 py-3 rounded-[4px] text-[#333] dark:text-[#D8D8D8] bg-white dark:bg-[#141414] shadow-[0_4px_8px_rgba(0,0,0,0.1)] dark:shadow-[0_4px_20px_rgba(255,255,255,0.2)]",
|
||||||
{
|
{
|
||||||
"hidden -m-2": !visible,
|
hidden: !visible,
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="absolute top-2 right-2 flex items-center justify-center size-[20px] border rounded-md cursor-pointer dark:border-[#282828]"
|
className="absolute top-2 right-2 flex items-center justify-center size-[20px] border rounded-md cursor-pointer dark:border-[#282828]"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setExpand(!expand);
|
setVisible(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ChevronUp className="size-4" />
|
<X className="size-4" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex item-center gap-1">
|
<div className="flex item-center gap-1">
|
||||||
@@ -52,11 +52,7 @@ const AiOverview: FC<AiSummaryProps> = (props) => {
|
|||||||
<span className="text-xs font-semibold">AI Overview</span>
|
<span className="text-xs font-semibold">AI Overview</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div className="flex-1 overflow-auto text-sm hide-scrollbar">
|
||||||
className={clsx("flex-1 overflow-auto text-sm hide-scrollbar", {
|
|
||||||
hidden: !expand,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<div className="-ml-11 -mr-4 user-select-text">
|
<div className="-ml-11 -mr-4 user-select-text">
|
||||||
<ChatMessage
|
<ChatMessage
|
||||||
key="current"
|
key="current"
|
||||||
@@ -81,10 +77,11 @@ const AiOverview: FC<AiSummaryProps> = (props) => {
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
className={clsx("min-h-[20px]", {
|
className={clsx("min-h-[20px]", {
|
||||||
hidden: !expand || isTyping,
|
hidden: isTyping,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -7,28 +7,26 @@ import {
|
|||||||
} from "ahooks";
|
} from "ahooks";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { noop } from "lodash-es";
|
import { noop } from "lodash-es";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
|
||||||
import { ChatMessage } from "../ChatMessage";
|
import { ChatMessage } from "../ChatMessage";
|
||||||
import { COPY_BUTTON_ID } from "@/constants";
|
|
||||||
import { useSearchStore } from "@/stores/searchStore";
|
import { useSearchStore } from "@/stores/searchStore";
|
||||||
import platformAdapter from "@/utils/platformAdapter";
|
import platformAdapter from "@/utils/platformAdapter";
|
||||||
import useMessageChunkData from "@/hooks/useMessageChunkData";
|
import useMessageChunkData from "@/hooks/useMessageChunkData";
|
||||||
import { useAppStore } from "@/stores/appStore";
|
import { useAppStore } from "@/stores/appStore";
|
||||||
import { isMac } from "@/utils/platform";
|
|
||||||
import { useExtensionsStore } from "@/stores/extensionsStore";
|
import { useExtensionsStore } from "@/stores/extensionsStore";
|
||||||
|
import { useShortcutsStore } from "@/stores/shortcutsStore";
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
serverId?: string;
|
serverId?: string;
|
||||||
assistantId?: string;
|
assistantId?: string;
|
||||||
|
copyButtonId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AskAi = () => {
|
const AskAi = () => {
|
||||||
const askAiMessage = useSearchStore((state) => state.askAiMessage);
|
const { askAiMessage, setGoAskAi, setSelectedAssistant, setAskAiSessionId, selectedAssistant, setAskAiServerId, setAskAiAssistantId } = useSearchStore();
|
||||||
|
|
||||||
const addError = useAppStore((state) => state.addError);
|
const addError = useAppStore((state) => state.addError);
|
||||||
const setGoAskAi = useSearchStore((state) => state.setGoAskAi);
|
|
||||||
const setSelectedAssistant = useSearchStore((state) => {
|
|
||||||
return state.setSelectedAssistant;
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: {
|
data: {
|
||||||
@@ -61,23 +59,12 @@ const AskAi = () => {
|
|||||||
|
|
||||||
const unlisten = useRef<() => void>(noop);
|
const unlisten = useRef<() => void>(noop);
|
||||||
const sessionIdRef = useRef<string>("");
|
const sessionIdRef = useRef<string>("");
|
||||||
const setAskAiSessionId = useSearchStore((state) => state.setAskAiSessionId);
|
|
||||||
const quickAiAccessServer = useExtensionsStore((state) => {
|
const { quickAiAccessServer, quickAiAccessAssistant } = useExtensionsStore();
|
||||||
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 state = useReactive<State>({});
|
const state = useReactive<State>({});
|
||||||
const setAskAiAssistantId = useSearchStore((state) => {
|
|
||||||
return state.setAskAiAssistantId;
|
const modifierKey = useShortcutsStore((state) => state.modifierKey);
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (state.serverId) return;
|
if (state.serverId) return;
|
||||||
@@ -116,11 +103,6 @@ const AskAi = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the chunk data does not contain a message_chunk, we ignore it
|
|
||||||
if (!chunkData.message_chunk) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsTyping(true);
|
setIsTyping(true);
|
||||||
|
|
||||||
setLoadingStep(() => ({
|
setLoadingStep(() => ({
|
||||||
@@ -172,6 +154,8 @@ const AskAi = () => {
|
|||||||
|
|
||||||
const { serverId, assistantId } = state;
|
const { serverId, assistantId } = state;
|
||||||
|
|
||||||
|
state.copyButtonId = nanoid();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await platformAdapter.invokeBackend("ask_ai", {
|
await platformAdapter.invokeBackend("ask_ai", {
|
||||||
message: askAiMessage,
|
message: askAiMessage,
|
||||||
@@ -184,30 +168,37 @@ const AskAi = () => {
|
|||||||
}
|
}
|
||||||
}, [askAiMessage]);
|
}, [askAiMessage]);
|
||||||
|
|
||||||
useKeyPress("enter", async (event) => {
|
useKeyPress(
|
||||||
const { metaKey, ctrlKey } = event;
|
`${modifierKey}.enter`,
|
||||||
|
async () => {
|
||||||
if (isTyping) return;
|
if (isTyping) return;
|
||||||
|
|
||||||
const { serverId, assistantId } = state;
|
const { serverId, assistantId } = state;
|
||||||
|
|
||||||
if ((isMac && metaKey) || (!isMac && ctrlKey)) {
|
|
||||||
await platformAdapter.commands("open_session_chat", {
|
|
||||||
serverId,
|
|
||||||
sessionId: sessionIdRef.current,
|
|
||||||
});
|
|
||||||
|
|
||||||
platformAdapter.emitEvent("toggle-to-chat-mode");
|
|
||||||
|
|
||||||
setAskAiServerId(serverId);
|
setAskAiServerId(serverId);
|
||||||
setAskAiSessionId(sessionIdRef.current);
|
setAskAiSessionId(sessionIdRef.current);
|
||||||
return setAskAiAssistantId(assistantId);
|
setAskAiAssistantId(assistantId);
|
||||||
|
|
||||||
|
platformAdapter.emitEvent("toggle-to-chat-mode");
|
||||||
|
},
|
||||||
|
{
|
||||||
|
exactMatch: true,
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const copyButton = document.getElementById(COPY_BUTTON_ID);
|
useKeyPress(
|
||||||
|
"enter",
|
||||||
|
() => {
|
||||||
|
if (isTyping || !state.copyButtonId) return;
|
||||||
|
|
||||||
copyButton?.click();
|
const copyButton = document.getElementById(state.copyButtonId);
|
||||||
});
|
|
||||||
|
copyButton?.click?.();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
exactMatch: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
askAiMessage && (
|
askAiMessage && (
|
||||||
@@ -238,6 +229,7 @@ const AskAi = () => {
|
|||||||
think={think}
|
think={think}
|
||||||
response={response}
|
response={response}
|
||||||
loadingStep={loadingStep}
|
loadingStep={loadingStep}
|
||||||
|
copyButtonId={state.copyButtonId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,23 +10,35 @@ import { useAppStore } from "@/stores/appStore";
|
|||||||
|
|
||||||
interface AssistantManagerProps {
|
interface AssistantManagerProps {
|
||||||
isChatMode: boolean;
|
isChatMode: boolean;
|
||||||
|
inputValue: string;
|
||||||
handleSubmit: () => void;
|
handleSubmit: () => void;
|
||||||
changeInput: (value: string) => void;
|
changeInput: (value: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useAssistantManager({
|
export function useAssistantManager({
|
||||||
isChatMode,
|
isChatMode,
|
||||||
|
inputValue,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
changeInput,
|
changeInput,
|
||||||
}: AssistantManagerProps) {
|
}: AssistantManagerProps) {
|
||||||
const isTauri = useAppStore((state) => state.isTauri);
|
const isTauri = useAppStore((state) => state.isTauri);
|
||||||
|
|
||||||
const { goAskAi, setGoAskAi, setAskAiMessage, selectedAssistant } =
|
const {
|
||||||
useSearchStore();
|
goAskAi,
|
||||||
|
setGoAskAi,
|
||||||
|
setAskAiMessage,
|
||||||
|
selectedAssistant,
|
||||||
|
selectedSearchContent,
|
||||||
|
visibleExtensionStore,
|
||||||
|
setVisibleExtensionStore,
|
||||||
|
setSearchValue,
|
||||||
|
visibleExtensionDetail,
|
||||||
|
setVisibleExtensionDetail,
|
||||||
|
sourceData,
|
||||||
|
setSourceData,
|
||||||
|
} = useSearchStore();
|
||||||
|
|
||||||
const quickAiAccessAssistant = useExtensionsStore(
|
const { quickAiAccessAssistant, disabledExtensions } = useExtensionsStore();
|
||||||
(state) => state.quickAiAccessAssistant
|
|
||||||
);
|
|
||||||
|
|
||||||
const askAIRef = useRef<Assistant | null>(null);
|
const askAIRef = useRef<Assistant | null>(null);
|
||||||
|
|
||||||
@@ -39,6 +51,8 @@ export function useAssistantManager({
|
|||||||
|
|
||||||
const assistant_get = useCallback(async () => {
|
const assistant_get = useCallback(async () => {
|
||||||
if (!askAI?.id) return;
|
if (!askAI?.id) return;
|
||||||
|
if (disabledExtensions.includes("QuickAIAccess")) return;
|
||||||
|
|
||||||
if (isTauri) {
|
if (isTauri) {
|
||||||
if (!askAI?.querySource?.id) return;
|
if (!askAI?.querySource?.id) return;
|
||||||
const res = await platformAdapter.commands("assistant_get", {
|
const res = await platformAdapter.commands("assistant_get", {
|
||||||
@@ -54,52 +68,84 @@ export function useAssistantManager({
|
|||||||
}
|
}
|
||||||
setAssistantDetail(res);
|
setAssistantDetail(res);
|
||||||
}
|
}
|
||||||
}, [askAI]);
|
}, [askAI?.id, askAI?.querySource?.id, disabledExtensions]);
|
||||||
|
|
||||||
const handleAskAi = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
const handleAskAi = useCallback(() => {
|
||||||
if (!isTauri) return;
|
if (!isTauri) return;
|
||||||
|
|
||||||
askAIRef.current = cloneDeep(askAI);
|
if (disabledExtensions.includes("QuickAIAccess")) return;
|
||||||
|
|
||||||
|
askAIRef.current = cloneDeep(askAI);
|
||||||
if (!askAIRef.current) return;
|
if (!askAIRef.current) return;
|
||||||
|
|
||||||
event.preventDefault();
|
let value = inputValue.trim();
|
||||||
|
if (isEmpty(value)) return;
|
||||||
|
|
||||||
const { value } = event.currentTarget;
|
if (!goAskAi && selectedAssistant) {
|
||||||
|
value = "";
|
||||||
if (!selectedAssistant && isEmpty(value)) return;
|
}
|
||||||
|
|
||||||
changeInput("");
|
changeInput("");
|
||||||
setAskAiMessage(!goAskAi && selectedAssistant ? "" : value);
|
setAskAiMessage(value);
|
||||||
setGoAskAi(true);
|
setGoAskAi(true);
|
||||||
};
|
}, [disabledExtensions, askAI, inputValue, goAskAi, selectedAssistant]);
|
||||||
|
|
||||||
const handleKeyDownAutoResizeTextarea = (
|
const handleKeyDownAutoResizeTextarea = useCallback(
|
||||||
e: React.KeyboardEvent<HTMLTextAreaElement>
|
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
) => {
|
const { key, shiftKey, currentTarget } = e;
|
||||||
const { key, shiftKey } = e;
|
const { value } = currentTarget;
|
||||||
|
|
||||||
const { value } = e.currentTarget;
|
|
||||||
|
|
||||||
if (key === "Backspace" && value === "") {
|
if (key === "Backspace" && value === "") {
|
||||||
|
if (goAskAi) {
|
||||||
return setGoAskAi(false);
|
return setGoAskAi(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key === "Tab" && !isChatMode && isTauri) {
|
if (visibleExtensionDetail) {
|
||||||
assistant_get();
|
return setVisibleExtensionDetail(false);
|
||||||
|
}
|
||||||
|
|
||||||
return handleAskAi(e);
|
if (visibleExtensionStore) {
|
||||||
|
return setVisibleExtensionStore(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sourceData) {
|
||||||
|
return setSourceData(void 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === "Tab" && !isChatMode && isTauri) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (visibleExtensionStore) return;
|
||||||
|
|
||||||
|
if (selectedSearchContent?.id === "Extension Store") {
|
||||||
|
changeInput("");
|
||||||
|
setSearchValue("");
|
||||||
|
return setVisibleExtensionStore(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
assistant_get();
|
||||||
|
return handleAskAi();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key === "Enter" && !shiftKey && !isChatMode && isTauri) {
|
if (key === "Enter" && !shiftKey && !isChatMode && isTauri) {
|
||||||
if (goAskAi) {
|
|
||||||
return handleAskAi(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleSubmit();
|
|
||||||
|
goAskAi ? handleAskAi() : handleSubmit();
|
||||||
}
|
}
|
||||||
};
|
},
|
||||||
|
[
|
||||||
|
isChatMode,
|
||||||
|
goAskAi,
|
||||||
|
assistant_get,
|
||||||
|
handleAskAi,
|
||||||
|
handleSubmit,
|
||||||
|
selectedSearchContent,
|
||||||
|
visibleExtensionStore,
|
||||||
|
visibleExtensionDetail,
|
||||||
|
sourceData,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
askAI,
|
askAI,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
forwardRef,
|
forwardRef,
|
||||||
KeyboardEvent,
|
KeyboardEvent,
|
||||||
useEffect,
|
useEffect,
|
||||||
|
useCallback,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
@@ -13,6 +14,7 @@ const MAX_FIRST_LINE_WIDTH = 470; // Width in pixels for first line
|
|||||||
const MAX_HEIGHT = 240; // 15rem
|
const MAX_HEIGHT = 240; // 15rem
|
||||||
|
|
||||||
interface AutoResizeTextareaProps {
|
interface AutoResizeTextareaProps {
|
||||||
|
isChatMode: boolean;
|
||||||
input: string;
|
input: string;
|
||||||
setInput: (value: string) => void;
|
setInput: (value: string) => void;
|
||||||
handleKeyDown?: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void;
|
handleKeyDown?: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void;
|
||||||
@@ -28,6 +30,7 @@ const AutoResizeTextarea = forwardRef<
|
|||||||
>(
|
>(
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
|
isChatMode,
|
||||||
input,
|
input,
|
||||||
setInput,
|
setInput,
|
||||||
handleKeyDown,
|
handleKeyDown,
|
||||||
@@ -46,6 +49,8 @@ const AutoResizeTextarea = forwardRef<
|
|||||||
() => {
|
() => {
|
||||||
const textarea = textareaRef.current;
|
const textarea = textareaRef.current;
|
||||||
if (!textarea) return;
|
if (!textarea) return;
|
||||||
|
if (typeof window === "undefined" || typeof document === "undefined")
|
||||||
|
return;
|
||||||
|
|
||||||
// Reset height to auto to get the correct scrollHeight
|
// Reset height to auto to get the correct scrollHeight
|
||||||
textarea.style.height = "auto";
|
textarea.style.height = "auto";
|
||||||
@@ -130,9 +135,17 @@ const AutoResizeTextarea = forwardRef<
|
|||||||
handleKeyDown?.(event);
|
handleKeyDown?.(event);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleChange = useCallback(
|
||||||
|
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
setInput(e.target.value);
|
||||||
|
},
|
||||||
|
[setInput]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<textarea
|
<textarea
|
||||||
ref={textareaRef}
|
ref={textareaRef}
|
||||||
|
id={isChatMode ? "chat-textarea" : "search-textarea"}
|
||||||
autoFocus
|
autoFocus
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
@@ -141,7 +154,7 @@ const AutoResizeTextarea = forwardRef<
|
|||||||
placeholder={chatPlaceholder || t("search.textarea.placeholder")}
|
placeholder={chatPlaceholder || t("search.textarea.placeholder")}
|
||||||
aria-label={t("search.textarea.ariaLabel")}
|
aria-label={t("search.textarea.ariaLabel")}
|
||||||
value={input}
|
value={input}
|
||||||
onChange={(e) => setInput(e.target.value)}
|
onChange={handleChange}
|
||||||
onKeyDown={handleKeyPress}
|
onKeyDown={handleKeyPress}
|
||||||
onCompositionStart={setTrue}
|
onCompositionStart={setTrue}
|
||||||
onCompositionEnd={() => {
|
onCompositionEnd={() => {
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ const ChatIcons: React.FC<ChatIconsProps> = ({
|
|||||||
<button
|
<button
|
||||||
className={`ml-1 p-1 ${
|
className={`ml-1 p-1 ${
|
||||||
inputValue ? "bg-[#0072FF]" : "bg-[#E4E5F0] dark:bg-[rgb(84,84,84)]"
|
inputValue ? "bg-[#0072FF]" : "bg-[#E4E5F0] dark:bg-[rgb(84,84,84)]"
|
||||||
} rounded-full transition-colors`}
|
} rounded-full transition-colors h-6`}
|
||||||
type="submit"
|
type="submit"
|
||||||
onClick={() => onSend(inputValue.trim())}
|
onClick={() => onSend(inputValue.trim())}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,68 +1,120 @@
|
|||||||
import { useClickAway, useCreation, useReactive } from "ahooks";
|
import { useClickAway, useCreation, useReactive } from "ahooks";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { isNil, lowerCase, noop } from "lodash-es";
|
import { isNil, lowerCase, noop } from "lodash-es";
|
||||||
import { Copy, Link, SquareArrowOutUpRight } from "lucide-react";
|
import {
|
||||||
import { cloneElement, FC, useEffect, useRef, useState } from "react";
|
Copy,
|
||||||
|
Download,
|
||||||
|
Info,
|
||||||
|
Link,
|
||||||
|
Settings,
|
||||||
|
SquareArrowOutUpRight,
|
||||||
|
Trash2,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { cloneElement, useEffect, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Input } from "@headlessui/react";
|
||||||
|
|
||||||
import { useOSKeyPress } from "@/hooks/useOSKeyPress";
|
import { useOSKeyPress } from "@/hooks/useOSKeyPress";
|
||||||
import { useSearchStore } from "@/stores/searchStore";
|
import { useSearchStore } from "@/stores/searchStore";
|
||||||
import { copyToClipboard, OpenURLWithBrowser } from "@/utils";
|
import { copyToClipboard } from "@/utils";
|
||||||
import { isMac } from "@/utils/platform";
|
import { isMac } from "@/utils/platform";
|
||||||
import { CONTEXT_MENU_PANEL_ID } from "@/constants";
|
import { CONTEXT_MENU_PANEL_ID } from "@/constants";
|
||||||
import { useShortcutsStore } from "@/stores/shortcutsStore";
|
import { useShortcutsStore } from "@/stores/shortcutsStore";
|
||||||
import { Input } from "@headlessui/react";
|
|
||||||
import VisibleKey from "../Common/VisibleKey";
|
import VisibleKey from "../Common/VisibleKey";
|
||||||
import platformAdapter from "@/utils/platformAdapter";
|
import platformAdapter from "@/utils/platformAdapter";
|
||||||
|
import SearchEmpty from "../Common/SearchEmpty";
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
activeMenuIndex: number;
|
activeMenuIndex: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ContextMenuProps {
|
const ContextMenu = () => {
|
||||||
hideCoco?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ContextMenu: FC<ContextMenuProps> = () => {
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const { t, i18n } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
const state = useReactive<State>({
|
const state = useReactive<State>({
|
||||||
activeMenuIndex: 0,
|
activeMenuIndex: 0,
|
||||||
});
|
});
|
||||||
const visibleContextMenu = useSearchStore((state) => {
|
const { setOpenPopover } = useShortcutsStore();
|
||||||
return state.visibleContextMenu;
|
|
||||||
});
|
|
||||||
const setVisibleContextMenu = useSearchStore((state) => {
|
|
||||||
return state.setVisibleContextMenu;
|
|
||||||
});
|
|
||||||
const setOpenPopover = useShortcutsStore((state) => state.setOpenPopover);
|
|
||||||
const selectedSearchContent = useSearchStore((state) => {
|
|
||||||
return state.selectedSearchContent;
|
|
||||||
});
|
|
||||||
const [searchMenus, setSearchMenus] = useState<typeof menus>([]);
|
const [searchMenus, setSearchMenus] = useState<typeof menus>([]);
|
||||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const {
|
||||||
|
visibleContextMenu,
|
||||||
|
setVisibleContextMenu,
|
||||||
|
selectedSearchContent,
|
||||||
|
selectedExtension,
|
||||||
|
setVisibleExtensionDetail,
|
||||||
|
} = useSearchStore();
|
||||||
|
|
||||||
const title = useCreation(() => {
|
const title = useCreation(() => {
|
||||||
|
if (selectedExtension) {
|
||||||
|
return selectedExtension.name;
|
||||||
|
}
|
||||||
|
|
||||||
if (selectedSearchContent?.id === "Calculator") {
|
if (selectedSearchContent?.id === "Calculator") {
|
||||||
return t("search.contextMenu.title.calculator");
|
return t("search.contextMenu.title.calculator");
|
||||||
}
|
}
|
||||||
|
|
||||||
return selectedSearchContent?.title;
|
return selectedSearchContent?.title;
|
||||||
}, [selectedSearchContent]);
|
}, [selectedSearchContent, selectedExtension]);
|
||||||
|
|
||||||
const menus = useCreation(() => {
|
const menus = useCreation(() => {
|
||||||
if (isNil(selectedSearchContent)) return [];
|
if (selectedExtension) {
|
||||||
|
return [
|
||||||
const { url, category, payload, on_opened } = selectedSearchContent;
|
{
|
||||||
const { query, result } = payload ?? {};
|
name: t("search.contextMenu.details"),
|
||||||
|
icon: <Info />,
|
||||||
if (category === "AI Overview") {
|
keys: isMac ? ["↩︎"] : ["Enter"],
|
||||||
setSearchMenus([]);
|
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 [];
|
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"),
|
name: t("search.contextMenu.open"),
|
||||||
icon: <SquareArrowOutUpRight />,
|
icon: <SquareArrowOutUpRight />,
|
||||||
@@ -70,15 +122,7 @@ const ContextMenu: FC<ContextMenuProps> = () => {
|
|||||||
shortcut: "enter",
|
shortcut: "enter",
|
||||||
hide: category === "Calculator",
|
hide: category === "Calculator",
|
||||||
clickEvent: () => {
|
clickEvent: () => {
|
||||||
if (on_opened) {
|
platformAdapter.openSearchItem(selectedSearchContent as any);
|
||||||
return platformAdapter.invokeBackend("open", {
|
|
||||||
onOpened: on_opened,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (url) {
|
|
||||||
OpenURLWithBrowser(url);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -86,7 +130,10 @@ const ContextMenu: FC<ContextMenuProps> = () => {
|
|||||||
icon: <Link />,
|
icon: <Link />,
|
||||||
keys: isMac ? ["⌘", "L"] : ["Ctrl", "L"],
|
keys: isMac ? ["⌘", "L"] : ["Ctrl", "L"],
|
||||||
shortcut: isMac ? "meta.l" : "ctrl.l",
|
shortcut: isMac ? "meta.l" : "ctrl.l",
|
||||||
hide: category === "Calculator",
|
hide:
|
||||||
|
category === "Calculator" ||
|
||||||
|
type === "AI Assistant" ||
|
||||||
|
id === "Extension Store",
|
||||||
clickEvent() {
|
clickEvent() {
|
||||||
copyToClipboard(url);
|
copyToClipboard(url);
|
||||||
},
|
},
|
||||||
@@ -105,7 +152,7 @@ const ContextMenu: FC<ContextMenuProps> = () => {
|
|||||||
name: t("search.contextMenu.copyUppercaseAnswer"),
|
name: t("search.contextMenu.copyUppercaseAnswer"),
|
||||||
icon: <Copy />,
|
icon: <Copy />,
|
||||||
keys: isMac ? ["⌘", "↩︎"] : ["Ctrl", "Enter"],
|
keys: isMac ? ["⌘", "↩︎"] : ["Ctrl", "Enter"],
|
||||||
shortcut: "meta.enter",
|
shortcut: isMac ? "meta.enter" : "ctrl.enter",
|
||||||
hide: category !== "Calculator",
|
hide: category !== "Calculator",
|
||||||
clickEvent() {
|
clickEvent() {
|
||||||
copyToClipboard(i18n.language === "zh" ? result.toZh : result.toEn);
|
copyToClipboard(i18n.language === "zh" ? result.toZh : result.toEn);
|
||||||
@@ -115,24 +162,24 @@ const ContextMenu: FC<ContextMenuProps> = () => {
|
|||||||
name: t("search.contextMenu.copyQuestionAndAnswer"),
|
name: t("search.contextMenu.copyQuestionAndAnswer"),
|
||||||
icon: <Copy />,
|
icon: <Copy />,
|
||||||
keys: isMac ? ["⌘", "L"] : ["Ctrl", "L"],
|
keys: isMac ? ["⌘", "L"] : ["Ctrl", "L"],
|
||||||
shortcut: "meta.l",
|
shortcut: isMac ? "meta.l" : "ctrl+l",
|
||||||
hide: category !== "Calculator",
|
hide: category !== "Calculator",
|
||||||
clickEvent() {
|
clickEvent() {
|
||||||
copyToClipboard(`${query.value} = ${result.value}`);
|
copyToClipboard(`${query.value} = ${result.value}`);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
}, [selectedSearchContent, selectedExtension]);
|
||||||
|
|
||||||
const filterMenus = menus.filter((item) => !item.hide);
|
useEffect(() => {
|
||||||
|
const filterMenus = menus.filter((item) => !item?.hide);
|
||||||
|
|
||||||
setSearchMenus(filterMenus);
|
setSearchMenus(filterMenus);
|
||||||
|
}, [menus]);
|
||||||
return filterMenus;
|
|
||||||
}, [selectedSearchContent]);
|
|
||||||
|
|
||||||
const shortcuts = useCreation(() => {
|
const shortcuts = useCreation(() => {
|
||||||
return menus.map((item) => item.shortcut);
|
return searchMenus.map((item) => item.shortcut);
|
||||||
}, [menus]);
|
}, [searchMenus]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
state.activeMenuIndex = 0;
|
state.activeMenuIndex = 0;
|
||||||
@@ -144,8 +191,8 @@ const ContextMenu: FC<ContextMenuProps> = () => {
|
|||||||
}
|
}
|
||||||
}, [selectedSearchContent]);
|
}, [selectedSearchContent]);
|
||||||
|
|
||||||
useOSKeyPress(["meta.k", "ctrl.k"], () => {
|
useOSKeyPress(["meta.k", "ctrl+k"], () => {
|
||||||
if (isNil(selectedSearchContent)) return;
|
if (isNil(selectedSearchContent) && isNil(selectedExtension)) return;
|
||||||
|
|
||||||
setVisibleContextMenu(!visibleContextMenu);
|
setVisibleContextMenu(!visibleContextMenu);
|
||||||
});
|
});
|
||||||
@@ -158,7 +205,7 @@ const ContextMenu: FC<ContextMenuProps> = () => {
|
|||||||
if (!visibleContextMenu) return;
|
if (!visibleContextMenu) return;
|
||||||
|
|
||||||
const index = state.activeMenuIndex;
|
const index = state.activeMenuIndex;
|
||||||
const length = menus.length;
|
const length = searchMenus.length;
|
||||||
|
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case "uparrow":
|
case "uparrow":
|
||||||
@@ -176,9 +223,9 @@ const ContextMenu: FC<ContextMenuProps> = () => {
|
|||||||
let matched;
|
let matched;
|
||||||
|
|
||||||
if (key === "enter") {
|
if (key === "enter") {
|
||||||
matched = menus.find((_, index) => index === state.activeMenuIndex);
|
matched = searchMenus.find((_, index) => index === state.activeMenuIndex);
|
||||||
} else {
|
} else {
|
||||||
matched = menus.find((item) => item.shortcut === key);
|
matched = searchMenus.find((item) => item.shortcut === key);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleClick(matched?.clickEvent);
|
handleClick(matched?.clickEvent);
|
||||||
@@ -191,11 +238,13 @@ const ContextMenu: FC<ContextMenuProps> = () => {
|
|||||||
const handleClick = (click = noop) => {
|
const handleClick = (click = noop) => {
|
||||||
click?.();
|
click?.();
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
setVisibleContextMenu(false);
|
setVisibleContextMenu(false);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
searchMenus.length > 0 && (
|
menus.length > 0 && (
|
||||||
<>
|
<>
|
||||||
{visibleContextMenu && (
|
{visibleContextMenu && (
|
||||||
<div
|
<div
|
||||||
@@ -220,9 +269,10 @@ const ContextMenu: FC<ContextMenuProps> = () => {
|
|||||||
>
|
>
|
||||||
<div className="text-[#999] dark:text-[#666] truncate">{title}</div>
|
<div className="text-[#999] dark:text-[#666] truncate">{title}</div>
|
||||||
|
|
||||||
|
{searchMenus.length > 0 ? (
|
||||||
<ul className="flex flex-col -mx-2 p-0">
|
<ul className="flex flex-col -mx-2 p-0">
|
||||||
{searchMenus.map((item, index) => {
|
{searchMenus.map((item, index) => {
|
||||||
const { name, icon, keys, clickEvent } = item;
|
const { name, icon, keys, color, clickEvent } = item;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
@@ -240,9 +290,12 @@ const ContextMenu: FC<ContextMenuProps> = () => {
|
|||||||
onClick={() => handleClick(clickEvent)}
|
onClick={() => handleClick(clickEvent)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-black/80 dark:text-white/80">
|
<div className="flex items-center gap-2 text-black/80 dark:text-white/80">
|
||||||
{cloneElement(icon, { className: "size-4" })}
|
{cloneElement(icon, {
|
||||||
|
className: "size-4",
|
||||||
|
style: { color },
|
||||||
|
})}
|
||||||
|
|
||||||
<span>{name}</span>
|
<span style={{ color }}>{name}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-[4px] text-black/60 dark:text-white/60">
|
<div className="flex gap-[4px] text-black/60 dark:text-white/60">
|
||||||
@@ -264,6 +317,11 @@ const ContextMenu: FC<ContextMenuProps> = () => {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
|
) : (
|
||||||
|
<div className="py-4">
|
||||||
|
<SearchEmpty width={80} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="-mx-3 p-2 border-t border-[#E6E6E6] dark:border-[#262626]">
|
<div className="-mx-3 p-2 border-t border-[#E6E6E6] dark:border-[#262626]">
|
||||||
{visibleContextMenu && (
|
{visibleContextMenu && (
|
||||||
@@ -277,16 +335,19 @@ const ContextMenu: FC<ContextMenuProps> = () => {
|
|||||||
<Input
|
<Input
|
||||||
ref={searchInputRef}
|
ref={searchInputRef}
|
||||||
autoFocus
|
autoFocus
|
||||||
|
autoCorrect="off"
|
||||||
placeholder={t("search.contextMenu.search")}
|
placeholder={t("search.contextMenu.search")}
|
||||||
className="w-full bg-transparent"
|
className="w-full bg-transparent"
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
const value = event.target.value;
|
const value = event.target.value.trim();
|
||||||
|
|
||||||
const searchMenus = menus.filter((item) => {
|
const nextMenus = menus
|
||||||
|
.filter((item) => !item.hide)
|
||||||
|
.filter((item) => {
|
||||||
return lowerCase(item.name).includes(lowerCase(value));
|
return lowerCase(item.name).includes(lowerCase(value));
|
||||||
});
|
});
|
||||||
|
|
||||||
setSearchMenus(searchMenus);
|
setSearchMenus(nextMenus);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</VisibleKey>
|
</VisibleKey>
|
||||||
|
|||||||
@@ -4,14 +4,13 @@ import { useTranslation } from "react-i18next";
|
|||||||
|
|
||||||
import { useSearchStore } from "@/stores/searchStore";
|
import { useSearchStore } from "@/stores/searchStore";
|
||||||
import { SearchHeader } from "./SearchHeader";
|
import { SearchHeader } from "./SearchHeader";
|
||||||
import noDataImg from "@/assets/coconut-tree.png";
|
|
||||||
import { metaOrCtrlKey } from "@/utils/keyboardUtils";
|
import { metaOrCtrlKey } from "@/utils/keyboardUtils";
|
||||||
import SearchListItem from "./SearchListItem";
|
import SearchListItem from "./SearchListItem";
|
||||||
import platformAdapter from "@/utils/platformAdapter";
|
import platformAdapter from "@/utils/platformAdapter";
|
||||||
import { Get } from "@/api/axiosRequest";
|
import { Get } from "@/api/axiosRequest";
|
||||||
import { useAppStore } from "@/stores/appStore";
|
import { useAppStore } from "@/stores/appStore";
|
||||||
import { useConnectStore } from "@/stores/connectStore";
|
import { useConnectStore } from "@/stores/connectStore";
|
||||||
import { OpenURLWithBrowser } from "@/utils";
|
import SearchEmpty from "../Common/SearchEmpty";
|
||||||
|
|
||||||
interface DocumentListProps {
|
interface DocumentListProps {
|
||||||
onSelectDocument: (id: string) => void;
|
onSelectDocument: (id: string) => void;
|
||||||
@@ -141,11 +140,15 @@ export const DocumentList: React.FC<DocumentListProps> = ({
|
|||||||
setIsKeyboardMode(false);
|
setIsKeyboardMode(false);
|
||||||
}, [isChatMode, input]);
|
}, [isChatMode, input]);
|
||||||
|
|
||||||
|
const { visibleContextMenu } = useSearchStore();
|
||||||
|
|
||||||
const handleKeyDown = useCallback(
|
const handleKeyDown = useCallback(
|
||||||
(e: KeyboardEvent) => {
|
(e: KeyboardEvent) => {
|
||||||
if (!data?.list?.length) return;
|
if (!data?.list?.length) return;
|
||||||
|
|
||||||
const handleArrowKeys = () => {
|
const handleArrowKeys = () => {
|
||||||
|
if (visibleContextMenu) return;
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setIsKeyboardMode(true);
|
setIsKeyboardMode(true);
|
||||||
|
|
||||||
@@ -170,15 +173,8 @@ export const DocumentList: React.FC<DocumentListProps> = ({
|
|||||||
const handleEnter = () => {
|
const handleEnter = () => {
|
||||||
if (selectedItem === null) return;
|
if (selectedItem === null) return;
|
||||||
const item = data.list[selectedItem]?.document;
|
const item = data.list[selectedItem]?.document;
|
||||||
if (item?.on_opened) {
|
|
||||||
return platformAdapter.invokeBackend("open", {
|
|
||||||
onOpened: item.on_opened,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item?.url) {
|
platformAdapter.openSearchItem(item);
|
||||||
OpenURLWithBrowser(item.url);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
switch (e.key) {
|
switch (e.key) {
|
||||||
@@ -237,20 +233,12 @@ export const DocumentList: React.FC<DocumentListProps> = ({
|
|||||||
<SearchListItem
|
<SearchListItem
|
||||||
key={hit.document.id + index}
|
key={hit.document.id + index}
|
||||||
itemRef={(el) => (itemRefs.current[index] = el)}
|
itemRef={(el) => (itemRefs.current[index] = el)}
|
||||||
item={hit.document}
|
item={{ ...hit.document, querySource: hit.source }}
|
||||||
isSelected={selectedItem === index}
|
isSelected={selectedItem === index}
|
||||||
currentIndex={index}
|
currentIndex={index}
|
||||||
onMouseEnter={() => onMouseEnter(index, hit.document)}
|
onMouseEnter={() => onMouseEnter(index, hit.document)}
|
||||||
onItemClick={() => {
|
onItemClick={() => {
|
||||||
if (hit.document?.on_opened) {
|
platformAdapter.openSearchItem(hit.document);
|
||||||
return platformAdapter.invokeBackend("open", {
|
|
||||||
onOpened: hit.document.on_opened,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hit.document?.url) {
|
|
||||||
OpenURLWithBrowser(hit.document.url);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
showListRight={viewMode === "list"}
|
showListRight={viewMode === "list"}
|
||||||
/>
|
/>
|
||||||
@@ -267,16 +255,9 @@ export const DocumentList: React.FC<DocumentListProps> = ({
|
|||||||
{!loading && (!data?.list || data.list.length === 0) && (
|
{!loading && (!data?.list || data.list.length === 0) && (
|
||||||
<div
|
<div
|
||||||
data-tauri-drag-region
|
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
|
<SearchEmpty />
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
MouseEvent,
|
MouseEvent,
|
||||||
useMemo,
|
useMemo,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { useDebounceFn, useUnmount } from "ahooks";
|
import { useDebounceFn } from "ahooks";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { useSearchStore } from "@/stores/searchStore";
|
import { useSearchStore } from "@/stores/searchStore";
|
||||||
@@ -16,7 +16,6 @@ import { useKeyboardNavigation } from "@/hooks/useKeyboardNavigation";
|
|||||||
import { SearchSource } from "./SearchSource";
|
import { SearchSource } from "./SearchSource";
|
||||||
import DropdownListItem from "./DropdownListItem";
|
import DropdownListItem from "./DropdownListItem";
|
||||||
import platformAdapter from "@/utils/platformAdapter";
|
import platformAdapter from "@/utils/platformAdapter";
|
||||||
import { OpenURLWithBrowser } from "@/utils";
|
|
||||||
|
|
||||||
type ISearchData = Record<string, QueryHits[]>;
|
type ISearchData = Record<string, QueryHits[]>;
|
||||||
|
|
||||||
@@ -44,12 +43,8 @@ function DropdownList({
|
|||||||
const [selectedName, setSelectedName] = useState<string>("");
|
const [selectedName, setSelectedName] = useState<string>("");
|
||||||
const [showIndex, setShowIndex] = useState<boolean>(false);
|
const [showIndex, setShowIndex] = useState<boolean>(false);
|
||||||
|
|
||||||
const {
|
const { setSourceData, setSelectedSearchContent, setVisibleContextMenu } =
|
||||||
setSourceData,
|
useSearchStore();
|
||||||
setSelectedSearchContent,
|
|
||||||
setSelectedAssistant,
|
|
||||||
setVisibleContextMenu,
|
|
||||||
} = useSearchStore();
|
|
||||||
|
|
||||||
const showSource = useMemo(
|
const showSource = useMemo(
|
||||||
() => Object.keys(searchData).length < 5,
|
() => Object.keys(searchData).length < 5,
|
||||||
@@ -82,15 +77,7 @@ function DropdownList({
|
|||||||
setSelectedSearchContent(item);
|
setSelectedSearchContent(item);
|
||||||
},
|
},
|
||||||
onItemClick: (item: SearchDocument) => {
|
onItemClick: (item: SearchDocument) => {
|
||||||
if (item?.on_opened) {
|
platformAdapter.openSearchItem(item);
|
||||||
return platformAdapter.invokeBackend("open", {
|
|
||||||
onOpened: item.on_opened,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item?.url) {
|
|
||||||
OpenURLWithBrowser(item.url);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
goToTwoPage: (item: SearchDocument) => {
|
goToTwoPage: (item: SearchDocument) => {
|
||||||
setSourceData(item);
|
setSourceData(item);
|
||||||
@@ -98,11 +85,6 @@ function DropdownList({
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useUnmount(() => {
|
|
||||||
setSelectedIndex(null);
|
|
||||||
setSelectedSearchContent(undefined);
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedIndex === null) {
|
if (selectedIndex === null) {
|
||||||
setSelectedSearchContent(undefined);
|
setSelectedSearchContent(undefined);
|
||||||
@@ -111,14 +93,6 @@ function DropdownList({
|
|||||||
|
|
||||||
const item = globalItemIndexMap[selectedIndex];
|
const item = globalItemIndexMap[selectedIndex];
|
||||||
setSelectedSearchContent(item);
|
setSelectedSearchContent(item);
|
||||||
if (item?.source?.id === "assistant") {
|
|
||||||
setSelectedAssistant({
|
|
||||||
...item,
|
|
||||||
name: item.title,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setSelectedAssistant(undefined);
|
|
||||||
}
|
|
||||||
}, [selectedIndex]);
|
}, [selectedIndex]);
|
||||||
|
|
||||||
// Scroll selected item into view
|
// Scroll selected item into view
|
||||||
@@ -150,6 +124,13 @@ function DropdownList({
|
|||||||
initializeSelection();
|
initializeSelection();
|
||||||
}, [searchData]);
|
}, [searchData]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
setSelectedIndex(null);
|
||||||
|
setSelectedSearchContent(undefined);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Keyboard navigation
|
// Keyboard navigation
|
||||||
useKeyboardNavigation({
|
useKeyboardNavigation({
|
||||||
suggests,
|
suggests,
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ const DropdownListItem = memo(
|
|||||||
id={`search-item-${currentIndex}`}
|
id={`search-item-${currentIndex}`}
|
||||||
className={clsx("p-2 transition rounded-lg", {
|
className={clsx("p-2 transition rounded-lg", {
|
||||||
"bg-[#EDEDED] dark:bg-[#202126]": isSelected,
|
"bg-[#EDEDED] dark:bg-[#202126]": isSelected,
|
||||||
|
"!p-0": isAiOverview,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{isCalculator && <Calculator item={item} isSelected={isSelected} />}
|
{isCalculator && <Calculator item={item} isSelected={isSelected} />}
|
||||||
|
|||||||
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
@@ -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;
|
||||||
@@ -46,7 +46,6 @@ interface ChatInputProps {
|
|||||||
}) => Promise<string | string[] | null>;
|
}) => Promise<string | string[] | null>;
|
||||||
getFileMetadata: (path: string) => Promise<any>;
|
getFileMetadata: (path: string) => Promise<any>;
|
||||||
getFileIcon: (path: string, size: number) => Promise<string>;
|
getFileIcon: (path: string, size: number) => Promise<string>;
|
||||||
hideCoco?: () => void;
|
|
||||||
hasModules?: string[];
|
hasModules?: string[];
|
||||||
searchPlaceholder?: string;
|
searchPlaceholder?: string;
|
||||||
chatPlaceholder?: string;
|
chatPlaceholder?: string;
|
||||||
@@ -78,7 +77,6 @@ export default function ChatInput({
|
|||||||
const currentAssistant = useConnectStore((state) => state.currentAssistant);
|
const currentAssistant = useConnectStore((state) => state.currentAssistant);
|
||||||
// const sessionId = useConnectStore((state) => state.currentSessionId);
|
// const sessionId = useConnectStore((state) => state.currentSessionId);
|
||||||
|
|
||||||
const showTooltip = useAppStore((state) => state.showTooltip);
|
|
||||||
const setBlurred = useAppStore((state) => state.setBlurred);
|
const setBlurred = useAppStore((state) => state.setBlurred);
|
||||||
const isTauri = useAppStore((state) => state.isTauri);
|
const isTauri = useAppStore((state) => state.isTauri);
|
||||||
|
|
||||||
@@ -86,6 +84,9 @@ export default function ChatInput({
|
|||||||
|
|
||||||
const { modifierKey, returnToInput, setModifierKeyPressed } =
|
const { modifierKey, returnToInput, setModifierKeyPressed } =
|
||||||
useShortcutsStore();
|
useShortcutsStore();
|
||||||
|
const language = useAppStore((state) => {
|
||||||
|
return state.language;
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
@@ -96,6 +97,8 @@ export default function ChatInput({
|
|||||||
const textareaRef = useRef<{ reset: () => void; focus: () => void }>(null);
|
const textareaRef = useRef<{ reset: () => void; focus: () => void }>(null);
|
||||||
|
|
||||||
const { curChatEnd, connected } = useChatStore();
|
const { curChatEnd, connected } = useChatStore();
|
||||||
|
const { setSearchValue, visibleExtensionStore, selectedExtension } =
|
||||||
|
useSearchStore();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleFocus = () => {
|
const handleFocus = () => {
|
||||||
@@ -126,7 +129,6 @@ export default function ChatInput({
|
|||||||
useKeyboardHandlers({
|
useKeyboardHandlers({
|
||||||
isChatMode,
|
isChatMode,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
disabledChange,
|
|
||||||
curChatEnd,
|
curChatEnd,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -135,6 +137,7 @@ export default function ChatInput({
|
|||||||
const handleInputChange = useCallback(
|
const handleInputChange = useCallback(
|
||||||
(value: string) => {
|
(value: string) => {
|
||||||
changeInput(value);
|
changeInput(value);
|
||||||
|
setSearchValue(value);
|
||||||
if (!isChatMode) {
|
if (!isChatMode) {
|
||||||
onSend(value);
|
onSend(value);
|
||||||
}
|
}
|
||||||
@@ -159,6 +162,7 @@ export default function ChatInput({
|
|||||||
const { askAI, askAIRef, assistantDetail, handleKeyDownAutoResizeTextarea } =
|
const { askAI, askAIRef, assistantDetail, handleKeyDownAutoResizeTextarea } =
|
||||||
useAssistantManager({
|
useAssistantManager({
|
||||||
isChatMode,
|
isChatMode,
|
||||||
|
inputValue,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
changeInput,
|
changeInput,
|
||||||
});
|
});
|
||||||
@@ -183,6 +187,18 @@ export default function ChatInput({
|
|||||||
return state.disabledExtensions;
|
return state.disabledExtensions;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const akiAiTooltipPrefix = useMemo(() => {
|
||||||
|
if (language === "zh") {
|
||||||
|
if (/^[a-zA-Z]/.test(askAI?.name)) {
|
||||||
|
return "问 ";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "问";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Ask";
|
||||||
|
}, [language, askAI]);
|
||||||
|
|
||||||
const renderSearchIcon = () => (
|
const renderSearchIcon = () => (
|
||||||
<SearchIcons
|
<SearchIcons
|
||||||
lineCount={lineCount}
|
lineCount={lineCount}
|
||||||
@@ -202,7 +218,8 @@ export default function ChatInput({
|
|||||||
disabledChange={disabledChange}
|
disabledChange={disabledChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{showTooltip && !isChatMode && sourceData && (
|
{!isChatMode &&
|
||||||
|
(sourceData || visibleExtensionStore || selectedExtension) && (
|
||||||
<div
|
<div
|
||||||
className={`absolute ${
|
className={`absolute ${
|
||||||
lineCount === 1 ? "-top-[5px]" : "top-[calc(100%-25px)]"
|
lineCount === 1 ? "-top-[5px]" : "top-[calc(100%-25px)]"
|
||||||
@@ -212,7 +229,7 @@ export default function ChatInput({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* {showTooltip && (
|
{/*
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
`absolute ${
|
`absolute ${
|
||||||
@@ -225,17 +242,18 @@ export default function ChatInput({
|
|||||||
>
|
>
|
||||||
<VisibleKey shortcut={returnToInput} />
|
<VisibleKey shortcut={returnToInput} />
|
||||||
</div>
|
</div>
|
||||||
)} */}
|
*/}
|
||||||
|
|
||||||
{!isChatMode &&
|
{!isChatMode &&
|
||||||
isTauri &&
|
isTauri &&
|
||||||
!goAskAi &&
|
!goAskAi &&
|
||||||
askAI &&
|
askAI &&
|
||||||
!disabledExtensions.includes("QuickAIAccess") && (
|
!disabledExtensions.includes("QuickAIAccess") &&
|
||||||
|
!visibleExtensionStore && (
|
||||||
<div className="flex items-center gap-2 text-sm text-[#AEAEAE] dark:text-[#545454] whitespace-nowrap">
|
<div className="flex items-center gap-2 text-sm text-[#AEAEAE] dark:text-[#545454] whitespace-nowrap">
|
||||||
<span>
|
<span>
|
||||||
{t("search.askCocoAi.title", {
|
{t("search.askCocoAi.title", {
|
||||||
replace: [askAI.name],
|
replace: [akiAiTooltipPrefix, askAI.name],
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex items-center justify-center w-8 h-[20px] text-xs rounded-md border border-black/10 dark:border-[#545454]">
|
<div className="flex items-center justify-center w-8 h-[20px] text-xs rounded-md border border-black/10 dark:border-[#545454]">
|
||||||
@@ -251,7 +269,7 @@ export default function ChatInput({
|
|||||||
}}
|
}}
|
||||||
/> */}
|
/> */}
|
||||||
|
|
||||||
{showTooltip && isChatMode && (
|
{isChatMode && curChatEnd && (
|
||||||
<div
|
<div
|
||||||
className={`absolute ${
|
className={`absolute ${
|
||||||
lineCount === 1 ? "-top-[5px]" : "top-[calc(100%-30px)]"
|
lineCount === 1 ? "-top-[5px]" : "top-[calc(100%-30px)]"
|
||||||
@@ -275,6 +293,7 @@ export default function ChatInput({
|
|||||||
>
|
>
|
||||||
<AutoResizeTextarea
|
<AutoResizeTextarea
|
||||||
ref={textareaRef}
|
ref={textareaRef}
|
||||||
|
isChatMode={isChatMode}
|
||||||
input={inputValue}
|
input={inputValue}
|
||||||
setInput={handleInputChange}
|
setInput={handleInputChange}
|
||||||
handleKeyDown={handleKeyDownAutoResizeTextarea}
|
handleKeyDown={handleKeyDownAutoResizeTextarea}
|
||||||
@@ -329,7 +348,6 @@ export default function ChatInput({
|
|||||||
setIsDeepThinkActive={setIsDeepThinkActive}
|
setIsDeepThinkActive={setIsDeepThinkActive}
|
||||||
isMCPActive={isMCPActive}
|
isMCPActive={isMCPActive}
|
||||||
setIsMCPActive={setIsMCPActive}
|
setIsMCPActive={setIsMCPActive}
|
||||||
showTooltip={showTooltip}
|
|
||||||
changeMode={changeMode}
|
changeMode={changeMode}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { useShortcutsStore } from "@/stores/shortcutsStore";
|
|||||||
import { useAppStore } from "@/stores/appStore";
|
import { useAppStore } from "@/stores/appStore";
|
||||||
import { useSearchStore } from "@/stores/searchStore";
|
import { useSearchStore } from "@/stores/searchStore";
|
||||||
import { useExtensionsStore } from "@/stores/extensionsStore";
|
import { useExtensionsStore } from "@/stores/extensionsStore";
|
||||||
|
import { parseSearchQuery, SearchQuery, unrequitable } from "@/utils";
|
||||||
// import InputExtra from "./InputExtra";
|
// import InputExtra from "./InputExtra";
|
||||||
// import AiSummaryIcon from "@/components/Common/Icons/AiSummaryIcon";
|
// import AiSummaryIcon from "@/components/Common/Icons/AiSummaryIcon";
|
||||||
|
|
||||||
@@ -31,7 +32,6 @@ interface InputControlsProps {
|
|||||||
hasModules?: string[];
|
hasModules?: string[];
|
||||||
searchPlaceholder?: string;
|
searchPlaceholder?: string;
|
||||||
chatPlaceholder?: string;
|
chatPlaceholder?: string;
|
||||||
showTooltip?: boolean;
|
|
||||||
changeMode?: (isChatMode: boolean) => void;
|
changeMode?: (isChatMode: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,7 +45,6 @@ const InputControls = ({
|
|||||||
setIsMCPActive,
|
setIsMCPActive,
|
||||||
isChatPage,
|
isChatPage,
|
||||||
hasModules,
|
hasModules,
|
||||||
showTooltip,
|
|
||||||
changeMode,
|
changeMode,
|
||||||
}: InputControlsProps) => {
|
}: InputControlsProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -72,53 +71,34 @@ const InputControls = ({
|
|||||||
const getDataSourcesByServer = useCallback(
|
const getDataSourcesByServer = useCallback(
|
||||||
async (
|
async (
|
||||||
serverId: string,
|
serverId: string,
|
||||||
options?: {
|
searchQuery: SearchQuery = {}
|
||||||
from?: number;
|
|
||||||
size?: number;
|
|
||||||
query?: string;
|
|
||||||
}
|
|
||||||
): Promise<DataSource[]> => {
|
): Promise<DataSource[]> => {
|
||||||
if (
|
searchQuery.from ??= 0;
|
||||||
!(
|
searchQuery.size ??= 1000;
|
||||||
assistantConfig.datasourceEnabled && assistantConfig.datasourceVisible
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const body: Record<string, any> = {
|
const body: Record<string, any> = {
|
||||||
id: serverId,
|
id: serverId,
|
||||||
from: options?.from || 0,
|
queryParams: parseSearchQuery({
|
||||||
size: options?.size || 1000,
|
...searchQuery,
|
||||||
};
|
fuzziness: 5,
|
||||||
|
filters: {
|
||||||
body.query = {
|
enabled: true,
|
||||||
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,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let response: any;
|
let response: any;
|
||||||
if (isTauri) {
|
if (isTauri) {
|
||||||
response = await platformAdapter.invokeBackend("datasource_search", {
|
if (unrequitable()) {
|
||||||
id: serverId,
|
return [];
|
||||||
options: body,
|
}
|
||||||
});
|
|
||||||
|
response = await platformAdapter.invokeBackend(
|
||||||
|
"datasource_search",
|
||||||
|
body
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
|
body.id = undefined;
|
||||||
const [error, res]: any = await Post("/datasource/_search", body);
|
const [error, res]: any = await Post("/datasource/_search", body);
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error("_search", error);
|
console.error("_search", error);
|
||||||
@@ -144,47 +124,34 @@ const InputControls = ({
|
|||||||
const getMCPByServer = useCallback(
|
const getMCPByServer = useCallback(
|
||||||
async (
|
async (
|
||||||
serverId: string,
|
serverId: string,
|
||||||
options?: {
|
searchQuery: SearchQuery = {}
|
||||||
from?: number;
|
|
||||||
size?: number;
|
|
||||||
query?: string;
|
|
||||||
}
|
|
||||||
): Promise<DataSource[]> => {
|
): Promise<DataSource[]> => {
|
||||||
if (!(assistantConfig.mcpEnabled && assistantConfig.mcpVisible)) {
|
searchQuery.from ??= 0;
|
||||||
return [];
|
searchQuery.size ??= 1000;
|
||||||
}
|
|
||||||
const body: Record<string, any> = {
|
const body: Record<string, any> = {
|
||||||
id: serverId,
|
id: serverId,
|
||||||
from: options?.from || 0,
|
queryParams: parseSearchQuery({
|
||||||
size: options?.size || 1000,
|
...searchQuery,
|
||||||
};
|
fuzziness: 5,
|
||||||
body.query = {
|
filters: {
|
||||||
bool: {
|
enabled: true,
|
||||||
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,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let response: any;
|
let response: any;
|
||||||
if (isTauri) {
|
if (isTauri) {
|
||||||
|
if (unrequitable()) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
response = await platformAdapter.invokeBackend(
|
response = await platformAdapter.invokeBackend(
|
||||||
"mcp_server_search",
|
"mcp_server_search",
|
||||||
body
|
body
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
body.id = undefined;
|
||||||
const [error, res]: any = await Post("/mcp_server/_search", body);
|
const [error, res]: any = await Post("/mcp_server/_search", body);
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error("_search", error);
|
console.error("_search", error);
|
||||||
@@ -222,6 +189,8 @@ const InputControls = ({
|
|||||||
const aiOverviewAssistant = useExtensionsStore((state) => {
|
const aiOverviewAssistant = useExtensionsStore((state) => {
|
||||||
return state.aiOverviewAssistant;
|
return state.aiOverviewAssistant;
|
||||||
});
|
});
|
||||||
|
const aiOverviewShortcut = useShortcutsStore((state) => state.aiOverview);
|
||||||
|
const { visibleExtensionStore } = useSearchStore();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -278,21 +247,19 @@ const InputControls = ({
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{source?.datasource?.enabled && source?.datasource?.visible && (
|
|
||||||
<SearchPopover
|
<SearchPopover
|
||||||
|
datasource={source?.datasource}
|
||||||
isSearchActive={isSearchActive}
|
isSearchActive={isSearchActive}
|
||||||
setIsSearchActive={setIsSearchActive}
|
setIsSearchActive={setIsSearchActive}
|
||||||
getDataSourcesByServer={getDataSourcesByServer}
|
getDataSourcesByServer={getDataSourcesByServer}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
|
|
||||||
{source?.mcp_servers?.enabled && source?.mcp_servers?.visible && (
|
|
||||||
<MCPPopover
|
<MCPPopover
|
||||||
|
mcp_servers={source?.mcp_servers}
|
||||||
isMCPActive={isMCPActive}
|
isMCPActive={isMCPActive}
|
||||||
setIsMCPActive={setIsMCPActive}
|
setIsMCPActive={setIsMCPActive}
|
||||||
getMCPByServer={getMCPByServer}
|
getMCPByServer={getMCPByServer}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
|
|
||||||
{!(source?.datasource?.enabled && source?.datasource?.visible) &&
|
{!(source?.datasource?.enabled && source?.datasource?.visible) &&
|
||||||
(source?.type !== "deep_think" || !source?.config?.visible) &&
|
(source?.type !== "deep_think" || !source?.config?.visible) &&
|
||||||
@@ -307,7 +274,8 @@ const InputControls = ({
|
|||||||
{!disabledExtensions.includes("AIOverview") &&
|
{!disabledExtensions.includes("AIOverview") &&
|
||||||
isTauri &&
|
isTauri &&
|
||||||
aiOverviewServer &&
|
aiOverviewServer &&
|
||||||
aiOverviewAssistant && (
|
aiOverviewAssistant &&
|
||||||
|
!visibleExtensionStore && (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"inline-flex items-center gap-1 px-2 py-1 rounded-full hover:!text-[#881c94] cursor-pointer transition",
|
"inline-flex items-center gap-1 px-2 py-1 rounded-full hover:!text-[#881c94] cursor-pointer transition",
|
||||||
@@ -323,8 +291,16 @@ const InputControls = ({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setEnabledAiOverview(!enabledAiOverview);
|
setEnabledAiOverview(!enabledAiOverview);
|
||||||
}}
|
}}
|
||||||
|
>
|
||||||
|
<VisibleKey
|
||||||
|
shortcut={aiOverviewShortcut}
|
||||||
|
onKeyPress={() => {
|
||||||
|
setEnabledAiOverview(!enabledAiOverview);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Sparkles className="size-4" />
|
<Sparkles className="size-4" />
|
||||||
|
</VisibleKey>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
className={clsx("text-xs", { hidden: !enabledAiOverview })}
|
className={clsx("text-xs", { hidden: !enabledAiOverview })}
|
||||||
>
|
>
|
||||||
@@ -337,7 +313,6 @@ const InputControls = ({
|
|||||||
|
|
||||||
{isChatPage || hasModules?.length !== 2 ? null : (
|
{isChatPage || hasModules?.length !== 2 ? null : (
|
||||||
<div className="relative w-16 flex justify-end items-center">
|
<div className="relative w-16 flex justify-end items-center">
|
||||||
{showTooltip && (
|
|
||||||
<div className="absolute right-[52px] -top-2 z-10">
|
<div className="absolute right-[52px] -top-2 z-10">
|
||||||
<VisibleKey
|
<VisibleKey
|
||||||
shortcut={modeSwitch}
|
shortcut={modeSwitch}
|
||||||
@@ -346,7 +321,6 @@ const InputControls = ({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
<ChatSwitch
|
<ChatSwitch
|
||||||
isChatMode={isChatMode}
|
isChatMode={isChatMode}
|
||||||
|
|||||||