From e15baef8f9edda29f37e147e0b6ff5bd601388ee Mon Sep 17 00:00:00 2001 From: BiggerRain <15911122312@163.COM> Date: Mon, 7 Apr 2025 11:19:09 +0800 Subject: [PATCH] refactor: web components (#331) * refactor: web components * chore: web component * chore: web * chore: web * docs: update notes --- .vscode/settings.json | 1 + docs/content.en/docs/release-notes/_index.md | 2 + package.json | 6 +- pnpm-lock.yaml | 169 ++++ src/api/axiosRequest.ts | 93 +++ src/api/tools.ts | 73 ++ src/assets/images/logo-dark.png | Bin 0 -> 3501 bytes src/assets/images/logo-light.png | Bin 0 -> 3487 bytes src/components/Assistant/Chat.tsx | 107 ++- src/components/Assistant/ChatHeader.tsx | 137 ++-- src/components/Assistant/ChatSidebar.tsx | 13 +- src/components/Assistant/SessionFile.tsx | 24 +- src/components/ChatMessage/PrevSuggestion.tsx | 65 ++ src/components/Common/Copyright/index.tsx | 24 + src/components/Common/Icons/ItemIcon.tsx | 7 +- src/components/Common/Icons/TypeIcon.tsx | 13 + .../{Search => Common/UI}/Footer.tsx | 84 +- .../{Search => Common/UI}/NoResults.tsx | 5 +- .../UI/SettingsFooter.tsx} | 87 ++- src/components/Search/AutoResizeTextarea.tsx | 9 +- src/components/Search/ContextMenu.tsx | 4 +- src/components/Search/InputBox.tsx | 112 +-- src/components/Search/Search.tsx | 12 +- src/components/Search/SearchListItem.tsx | 4 +- src/components/Search/SearchPopover.tsx | 198 +++-- src/components/SearchChat/index.tsx | 420 ++++++++++ .../Advanced/components/Shortcuts/index.tsx | 7 +- src/components/UpdateApp/index.tsx | 2 +- src/hooks/useChatActions.ts | 229 ++++-- src/hooks/useEscape.ts | 10 +- src/hooks/useFindConnectorIcon.ts | 11 +- src/hooks/useMessageHandler.ts | 1 + src/hooks/useWebSocket.ts | 144 +++- src/hooks/useWindows.ts | 67 +- src/locales/en/translation.json | 5 +- src/locales/zh/translation.json | 5 +- src/pages/main/index.tsx | 21 +- src/pages/settings/index.tsx | 2 +- src/pages/web/README.md | 100 +++ src/pages/web/SearchChat.tsx | 311 -------- src/pages/web/index.tsx | 124 ++- src/stores/appStore.ts | 5 + src/stores/connectStore.ts | 11 +- src/stores/updateStore.ts | 7 +- src/utils/index.ts | 7 +- src/utils/platformAdapter.ts | 433 +---------- src/utils/tauriAdapter.ts | 188 +++++ src/utils/webAdapter.ts | 161 ++++ src/web.css | 729 ++++++++++++++++++ tailwind.config.js | 3 +- tsup.config.ts | 83 +- vite.config.ts | 5 + 52 files changed, 3117 insertions(+), 1223 deletions(-) create mode 100644 src/api/axiosRequest.ts create mode 100644 src/api/tools.ts create mode 100644 src/assets/images/logo-dark.png create mode 100644 src/assets/images/logo-light.png create mode 100644 src/components/ChatMessage/PrevSuggestion.tsx create mode 100644 src/components/Common/Copyright/index.tsx rename src/components/{Search => Common/UI}/Footer.tsx (62%) rename src/components/{Search => Common/UI}/NoResults.tsx (89%) rename src/components/{Footer.tsx => Common/UI/SettingsFooter.tsx} (54%) create mode 100644 src/components/SearchChat/index.tsx create mode 100644 src/pages/web/README.md delete mode 100644 src/pages/web/SearchChat.tsx create mode 100644 src/utils/tauriAdapter.ts create mode 100644 src/utils/webAdapter.ts create mode 100644 src/web.css diff --git a/.vscode/settings.json b/.vscode/settings.json index 2ca66c02..68071ce3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -57,6 +57,7 @@ "uuidv", "VITE", "walkdir", + "wavesurfer", "webviews", "xzvf", "yuque", diff --git a/docs/content.en/docs/release-notes/_index.md b/docs/content.en/docs/release-notes/_index.md index 27f59135..5abb4c82 100644 --- a/docs/content.en/docs/release-notes/_index.md +++ b/docs/content.en/docs/release-notes/_index.md @@ -19,6 +19,8 @@ Information about release notes of Coco Server is provided here. ### Improvements +- refactor: web components #331 + ## 0.3.0 (2025-03-31) ### Breaking changes diff --git a/package.json b/package.json index 4f9013cd..3457369b 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,8 @@ "scripts": { "dev": "vite", "build": "tsc && vite build", - "build:web": "tsc && tsup", - "publish:web": "cd dist/search-chat && npm publish", + "build:web": "cross-env BUILD_TARGET=web tsc && cross-env BUILD_TARGET=web tsup --format esm", + "publish:web": "cd out/search-chat && npm publish", "publish:web:beta": "cd dist/search-chat && npm publish --tag beta", "publish:web:alpha": "cd dist/search-chat && npm publish --tag alpha", "publish:web:rc": "cd dist/search-chat && npm publish --tag rc", @@ -34,6 +34,7 @@ "@tauri-apps/plugin-window": "2.0.0-alpha.1", "@wavesurfer/react": "^1.0.9", "ahooks": "^3.8.4", + "axios": "^1.8.4", "clsx": "^2.1.1", "dayjs": "^1.11.13", "dotenv": "^16.4.7", @@ -76,6 +77,7 @@ "@types/react-window": "^1.8.8", "@vitejs/plugin-react": "^4.3.4", "autoprefixer": "^10.4.21", + "cross-env": "^7.0.3", "immer": "^10.1.1", "postcss": "^8.5.3", "release-it": "^18.1.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b5e5deff..2fb73c99 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -56,6 +56,9 @@ importers: ahooks: specifier: ^3.8.4 version: 3.8.4(react@18.3.1) + axios: + specifier: ^1.8.4 + version: 1.8.4 clsx: specifier: ^2.1.1 version: 2.1.1 @@ -177,6 +180,9 @@ importers: autoprefixer: specifier: ^10.4.21 version: 10.4.21(postcss@8.5.3) + cross-env: + specifier: ^7.0.3 + version: 7.0.3 immer: specifier: ^10.1.1 version: 10.1.1 @@ -1418,6 +1424,9 @@ packages: async-retry@1.3.3: resolution: {integrity: sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==} + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + atomically@2.0.3: resolution: {integrity: sha512-kU6FmrwZ3Lx7/7y3hPS5QnbJfaohcIul5fGqf7ok+4KklIEk9tJ0C2IQPdacSbVUWv6zVHXEBWoWd6NrVMT7Cw==} @@ -1428,6 +1437,9 @@ packages: peerDependencies: postcss: ^8.1.0 + axios@1.8.4: + resolution: {integrity: sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==} + bail@2.0.2: resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} @@ -1478,6 +1490,10 @@ packages: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -1565,6 +1581,10 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + comma-separated-tokens@2.0.3: resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} @@ -1618,6 +1638,11 @@ packages: typescript: optional: true + cross-env@7.0.3: + resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} + engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} + hasBin: true + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -1828,6 +1853,10 @@ packages: delaunator@5.0.1: resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==} + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -1852,6 +1881,10 @@ packages: resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} engines: {node: '>=12'} + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} @@ -1878,6 +1911,22 @@ packages: error-ex@1.3.2: resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + esbuild@0.21.5: resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} engines: {node: '>=12'} @@ -1969,10 +2018,23 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + follow-redirects@1.15.9: + resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + foreground-child@3.3.1: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} + form-data@4.0.2: + resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==} + engines: {node: '>= 6'} + fraction.js@4.3.7: resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} @@ -1995,6 +2057,14 @@ packages: resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==} engines: {node: '>=18'} + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + get-stream@8.0.1: resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} engines: {node: '>=16'} @@ -2048,6 +2118,10 @@ packages: resolution: {integrity: sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==} engines: {node: '>=18'} + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + graceful-fs@4.2.10: resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==} @@ -2057,6 +2131,14 @@ packages: hachure-fill@0.5.2: resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==} + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} @@ -2433,6 +2515,10 @@ packages: engines: {node: '>= 18'} hasBin: true + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + mdast-util-find-and-replace@3.0.2: resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} @@ -4653,6 +4739,8 @@ snapshots: dependencies: retry: 0.13.1 + asynckit@0.4.0: {} + atomically@2.0.3: dependencies: stubborn-fs: 1.2.5 @@ -4668,6 +4756,14 @@ snapshots: postcss: 8.5.3 postcss-value-parser: 4.2.0 + axios@1.8.4: + dependencies: + follow-redirects: 1.15.9 + form-data: 4.0.2 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + bail@2.0.2: {} balanced-match@1.0.2: {} @@ -4720,6 +4816,11 @@ snapshots: cac@6.7.14: {} + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + callsites@3.1.0: {} camelcase-css@2.0.1: {} @@ -4794,6 +4895,10 @@ snapshots: color-name@1.1.4: {} + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + comma-separated-tokens@2.0.3: {} commander@4.1.1: {} @@ -4841,6 +4946,10 @@ snapshots: optionalDependencies: typescript: 5.8.2 + cross-env@7.0.3: + dependencies: + cross-spawn: 7.0.6 + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -5068,6 +5177,8 @@ snapshots: dependencies: robust-predicates: 3.0.2 + delayed-stream@1.0.0: {} + dequal@2.0.3: {} devlop@1.1.0: @@ -5088,6 +5199,12 @@ snapshots: dotenv@16.4.7: {} + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + eastasianwidth@0.2.0: {} electron-to-chromium@1.5.123: {} @@ -5106,6 +5223,21 @@ snapshots: dependencies: is-arrayish: 0.2.1 + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + esbuild@0.21.5: optionalDependencies: '@esbuild/aix-ppc64': 0.21.5 @@ -5247,11 +5379,20 @@ snapshots: dependencies: to-regex-range: 5.0.1 + follow-redirects@1.15.9: {} + foreground-child@3.3.1: dependencies: cross-spawn: 7.0.6 signal-exit: 4.1.0 + form-data@4.0.2: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + mime-types: 2.1.35 + fraction.js@4.3.7: {} fs.realpath@1.0.0: {} @@ -5265,6 +5406,24 @@ snapshots: get-east-asian-width@1.3.0: {} + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + get-stream@8.0.1: {} get-stream@9.0.1: @@ -5336,12 +5495,20 @@ snapshots: slash: 5.1.0 unicorn-magic: 0.1.0 + gopd@1.2.0: {} + graceful-fs@4.2.10: {} graceful-fs@4.2.11: {} hachure-fill@0.5.2: {} + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + hasown@2.0.2: dependencies: function-bind: 1.1.2 @@ -5707,6 +5874,8 @@ snapshots: marked@15.0.7: {} + math-intrinsics@1.1.0: {} + mdast-util-find-and-replace@3.0.2: dependencies: '@types/mdast': 4.0.4 diff --git a/src/api/axiosRequest.ts b/src/api/axiosRequest.ts new file mode 100644 index 00000000..3df445f1 --- /dev/null +++ b/src/api/axiosRequest.ts @@ -0,0 +1,93 @@ +import axios from "axios"; + +import { + handleChangeRequestHeader, + handleConfigureAuth, + // handleAuthError, + // handleGeneralError, + handleNetworkError, +} from "./tools"; + +type Fn = (data: FcResponse) => unknown; + +interface IAnyObj { + [index: string]: unknown; +} + +interface FcResponse { + errno: string; + errmsg: string; + data: T; +} + +axios.interceptors.request.use((config) => { + config = handleChangeRequestHeader(config); + config = handleConfigureAuth(config); + // console.log("config", config); + return config; +}); + +axios.interceptors.response.use( + (response) => { + if (response.status !== 200) return Promise.reject(response.data); + // handleAuthError(response.data.errno); + // handleGeneralError(response.data.errno, response.data.errmsg); + return response; + }, + (err) => { + handleNetworkError(err?.response?.status); + return Promise.reject(err?.response); + } +); + +export const Get = ( + url: string, + params: IAnyObj = {}, + clearFn?: Fn +): Promise<[any, FcResponse | undefined]> => + new Promise((resolve) => { + const appStore = JSON.parse(localStorage.getItem("app-store") || "{}"); + // console.log("baseURL", appStore.state?.endpoint_http) + + let baseURL = appStore.state?.endpoint_http; + axios + .get(baseURL + url, { params }) + .then((result) => { + let res: FcResponse; + if (clearFn !== undefined) { + res = clearFn(result?.data) as unknown as FcResponse; + } else { + res = result?.data as FcResponse; + } + resolve([null, res as FcResponse]); + }) + .catch((err) => { + resolve([err, undefined]); + }); + }); + +export const Post = ( + url: string, + data: IAnyObj, + params: IAnyObj = {}, + headers: IAnyObj = {} +): Promise<[any, FcResponse | undefined]> => { + return new Promise((resolve) => { + const appStore = JSON.parse(localStorage.getItem("app-store") || "{}"); + // console.log("baseURL", appStore.state?.endpoint_http) + + let baseURL = appStore.state?.endpoint_http; + + axios + .post(baseURL + url, data, { + params, + headers, + } as any) + .then((result) => { + resolve([null, result.data as FcResponse]); + }) + .catch((err) => { + resolve([err, undefined]); + }); + }); +}; diff --git a/src/api/tools.ts b/src/api/tools.ts new file mode 100644 index 00000000..1935ba77 --- /dev/null +++ b/src/api/tools.ts @@ -0,0 +1,73 @@ +export const handleChangeRequestHeader = (config: any) => { + config["xxxx"] = "xxx"; + return config; +}; + +export const handleConfigureAuth = (config: any) => { + // config.headers["X-API-TOKEN"] = localStorage.getItem("token") || ""; + + const headersStr = localStorage.getItem("headers") || "{}"; + const headers = JSON.parse(headersStr); + // console.log("headers:", headers); + + config.headers = { + ...config.headers, + ...headers, + } + // console.log("config.headers", config.headers) + return config; +}; + +export const handleNetworkError = (errStatus?: number): void => { + const networkErrMap: any = { + "400": "Bad Request", // token invalid + "401": "Unauthorized, please login again", + "403": "Access Denied", + "404": "Resource Not Found", + "405": "Method Not Allowed", + "408": "Request Timeout", + "500": "Internal Server Error", + "501": "Not Implemented", + "502": "Bad Gateway", + "503": "Service Unavailable", + "504": "Gateway Timeout", + "505": "HTTP Version Not Supported", + }; + if (errStatus) { + console.error(networkErrMap[errStatus] ?? `Other Connection Error --${errStatus}`); + return; + } + + console.error("Unable to connect to server!"); +}; + +export const handleAuthError = (errno: string): boolean => { + const authErrMap: any = { + "10031": "Login expired, please login again", // token invalid + "10032": "Session timeout, please login again", // token expired + "10033": "Account not bound to role, please contact administrator", + "10034": "User not registered, please contact administrator", + "10035": "Unable to get third-party platform user with code", + "10036": "Account not linked to employee, please contact administrator", + "10037": "Account is invalid", + "10038": "Account not found", + }; + + if (authErrMap.hasOwnProperty(errno)) { + console.error(authErrMap[errno]); + // Authorization error, logout account + // logout(); + return false; + } + + return true; +}; + +export const handleGeneralError = (errno: string, errmsg: string): boolean => { + if (errno !== "0") { + console.error(errmsg); + return false; + } + + return true; +}; \ No newline at end of file diff --git a/src/assets/images/logo-dark.png b/src/assets/images/logo-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..d68914ad1ae58797b501e5e1a5bedc9b459a01bc GIT binary patch literal 3501 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 b(9wSYN$?uZT)Y^~00000NkvXXu0mjf=k>nL literal 0 HcmV?d00001 diff --git a/src/assets/images/logo-light.png b/src/assets/images/logo-light.png new file mode 100644 index 0000000000000000000000000000000000000000..c7e9ef43e196270bebb774bf9b23c75841754ec2 GIT binary patch literal 3487 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 N002ovPDHLkV1hWcpy&Vq literal 0 HcmV?d00001 diff --git a/src/components/Assistant/Chat.tsx b/src/components/Assistant/Chat.tsx index 6d497c95..6eedccf5 100644 --- a/src/components/Assistant/Chat.tsx +++ b/src/components/Assistant/Chat.tsx @@ -21,6 +21,7 @@ import { ChatHeader } from "./ChatHeader"; import { ChatContent } from "./ChatContent"; import ConnectPrompt from "./ConnectPrompt"; import type { Chat } from "./types"; +import PrevSuggestion from "@/components/ChatMessage/PrevSuggestion"; interface ChatAIProps { isTransitioned: boolean; @@ -33,6 +34,7 @@ interface ChatAIProps { clearChatPage?: () => void; isChatPage?: boolean; getFileUrl: (path: string) => string; + showChatHistory?: boolean; } export interface ChatAIRef { @@ -56,6 +58,7 @@ const ChatAI = memo( clearChatPage, isChatPage = false, getFileUrl, + showChatHistory = true, }, ref ) => { @@ -89,7 +92,9 @@ const ChatAI = memo( const [Question, setQuestion] = useState(""); - const [websocketSessionId, setWebsocketSessionId] = useState(''); + const [showPrevSuggestion, setShowPrevSuggestion] = useState(true); + + const [websocketSessionId, setWebsocketSessionId] = useState(""); const onWebsocketSessionId = useCallback((sessionId: string) => { setWebsocketSessionId(sessionId); @@ -119,16 +124,21 @@ const ChatAI = memo( const dealMsgRef = useRef<((msg: string) => void) | null>(null); - const clientId = isChatPage ? "standalone" : "popup" - const { errorShow, setErrorShow, reconnect, disconnectWS, updateDealMsg } = - useWebSocket({ - clientId, - connected, - setConnected, - currentService, - dealMsgRef, - onWebsocketSessionId, - }); + const clientId = isChatPage ? "standalone" : "popup"; + const { + errorShow, + setErrorShow, + reconnect, + disconnectWS, + updateDealMsg, + } = useWebSocket({ + clientId, + connected, + setConnected, + currentService, + dealMsgRef, + onWebsocketSessionId, + }); const { chatClose, @@ -161,7 +171,7 @@ const ChatAI = memo( setTimedoutShow, (chat) => cancelChat(chat || activeChat), setLoadingStep, - handlers, + handlers ); useEffect(() => { @@ -189,16 +199,28 @@ const ChatAI = memo( ]); const init = useCallback( - (value: string) => { - if (!isLogin) return; - if (!curChatEnd) return; - if (!activeChat?._id) { - createNewChat(value, activeChat, websocketSessionId); - } else { - handleSendMessage(value, activeChat, websocketSessionId); + async (value: string) => { + try { + console.log("init", isLogin, curChatEnd, activeChat?._id); + if (!isLogin || !curChatEnd) return; + setShowPrevSuggestion(false); + if (!activeChat?._id) { + await createNewChat(value, activeChat, websocketSessionId); + } else { + await handleSendMessage(value, activeChat, websocketSessionId); + } + } catch (error) { + console.error('Failed to initialize chat:', error); } }, - [isLogin, curChatEnd, activeChat, createNewChat, handleSendMessage, websocketSessionId] + [ + isLogin, + curChatEnd, + activeChat, + createNewChat, + handleSendMessage, + websocketSessionId, + ] ); const { createWin } = useWindows(); @@ -207,14 +229,17 @@ const ChatAI = memo( }, [createChatWindow, createWin]); useEffect(() => { + setCurChatEnd(true); return () => { if (messageTimeoutRef.current) { clearTimeout(messageTimeoutRef.current); } - chatClose(activeChat); - setActiveChat(undefined); - setCurChatEnd(true); - disconnectWS(); + Promise.resolve().then(() => { + chatClose(activeChat); + setActiveChat(undefined); + setCurChatEnd(true); + disconnectWS(); + }); }; }, [chatClose, setCurChatEnd]); @@ -240,17 +265,20 @@ const ChatAI = memo( ] ); - const deleteChat = useCallback((chatId: string) => { - setChats((prev) => prev.filter((chat) => chat._id !== chatId)); - if (activeChat?._id === chatId) { - const remainingChats = chats.filter((chat) => chat._id !== chatId); - if (remainingChats.length > 0) { - setActiveChat(remainingChats[0]); - } else { - init(""); + const deleteChat = useCallback( + (chatId: string) => { + setChats((prev) => prev.filter((chat) => chat._id !== chatId)); + if (activeChat?._id === chatId) { + const remainingChats = chats.filter((chat) => chat._id !== chatId); + if (remainingChats.length > 0) { + setActiveChat(remainingChats[0]); + } else { + init(""); + } } - } - }, [activeChat, chats, init, setActiveChat]); + }, + [activeChat, chats, init, setActiveChat] + ); const handleOutsideClick = useCallback((e: MouseEvent) => { const sidebar = document.querySelector("[data-sidebar]"); @@ -297,9 +325,9 @@ const ChatAI = memo( return (
- {!setIsSidebarOpen && ( + {showChatHistory && !setIsSidebarOpen && ( {isLogin ? ( handleSendMessage(value, activeChat)} + handleSendMessage={(value) => + handleSendMessage(value, activeChat) + } getFileUrl={getFileUrl} /> ) : ( )} + + {showPrevSuggestion ? : null}
); } diff --git a/src/components/Assistant/ChatHeader.tsx b/src/components/Assistant/ChatHeader.tsx index cf1f00ad..cbfe68a6 100644 --- a/src/components/Assistant/ChatHeader.tsx +++ b/src/components/Assistant/ChatHeader.tsx @@ -29,6 +29,8 @@ import { useChatStore } from "@/stores/chatStore"; import type { Chat } from "./types"; import { useConnectStore } from "@/stores/connectStore"; import platformAdapter from "@/utils/platformAdapter"; +import { list_coco_servers } from "@/commands"; + interface ChatHeaderProps { onCreateNewChat: () => void; onOpenChatAI: () => void; @@ -38,6 +40,7 @@ interface ChatHeaderProps { reconnect: (server?: IServer) => void; setIsLogin: (isLogin: boolean) => void; isChatPage?: boolean; + showChatHistory?: boolean; } export function ChatHeader({ @@ -48,6 +51,7 @@ export function ChatHeader({ reconnect, setIsLogin, isChatPage = false, + showChatHistory, }: ChatHeaderProps) { const { t } = useTranslation(); @@ -63,34 +67,39 @@ export function ChatHeader({ const currentService = useConnectStore((state) => state.currentService); const setCurrentService = useConnectStore((state) => state.setCurrentService); - const fetchServers = useCallback(async (resetSelection: boolean) => { - platformAdapter.invokeBackend("list_coco_servers") - .then((res: any) => { - const enabledServers = (res as IServer[]).filter( - (server) => server.enabled !== false - ); - //console.log("list_coco_servers", enabledServers); - setServerList(enabledServers); + const isTauri = useAppStore((state) => state.isTauri); - if (resetSelection && enabledServers.length > 0) { - const currentServiceExists = enabledServers.find( - (server) => server.id === currentService?.id + const fetchServers = useCallback( + async (resetSelection: boolean) => { + list_coco_servers() + .then((res: any) => { + const enabledServers = (res as IServer[]).filter( + (server) => server.enabled !== false ); + //console.log("list_coco_servers", enabledServers); + setServerList(enabledServers); - if (currentServiceExists) { - switchServer(currentServiceExists); - } else { - switchServer(enabledServers[enabledServers.length - 1]); + if (resetSelection && enabledServers.length > 0) { + const currentServiceExists = enabledServers.find( + (server) => server.id === currentService?.id + ); + + if (currentServiceExists) { + switchServer(currentServiceExists); + } else { + switchServer(enabledServers[enabledServers.length - 1]); + } } - } - }) - .catch((err: any) => { - console.error(err); - }); - }, [currentService?.id]); + }) + .catch((err: any) => { + console.error(err); + }); + }, + [currentService?.id] + ); useEffect(() => { - fetchServers(true); + isTauri && fetchServers(true); const unlisten = platformAdapter.listenEvent("login_or_logout", (event) => { console.log("Login or Logout:", currentService, event); @@ -103,8 +112,6 @@ export function ChatHeader({ }; }, []); - - const switchServer = async (server: IServer) => { if (!server) return; try { @@ -119,7 +126,7 @@ export function ChatHeader({ return; } setIsLogin(true); - // The Rust backend will automatically disconnect, + // The Rust backend will automatically disconnect, // so we don't need to handle disconnection on the frontend // src-tauri/src/server/websocket.rs reconnect && reconnect(server); @@ -149,52 +156,60 @@ export function ChatHeader({ data-tauri-drag-region >
- + {isTauri && ( + + )} - + {t("assistant.message.logo")} Coco AI - + {showChatHistory && isTauri ? ( + + ) : null} - - - - - + {showChatHistory && isTauri ? ( + + + + + + ) : null} - + {showChatHistory && isTauri ? ( + + ) : null}
@@ -205,7 +220,7 @@ export function ChatHeader({
-
+ {isTauri ?
)} -
+
:
} ); } diff --git a/src/components/Assistant/ChatSidebar.tsx b/src/components/Assistant/ChatSidebar.tsx index 775bc58f..eba958dc 100644 --- a/src/components/Assistant/ChatSidebar.tsx +++ b/src/components/Assistant/ChatSidebar.tsx @@ -25,11 +25,14 @@ export const ChatSidebar: React.FC = ({ return (
{ const { sessionId } = props; const { t } = useTranslation(); + + const isTauri = useAppStore((state) => state.isTauri); const currentService = useConnectStore((state) => state.currentService); const [uploadedFiles, setUploadedFiles] = useState([]); const [visible, setVisible] = useState(false); @@ -32,14 +36,20 @@ const SessionFile = (props: SessionFileProps) => { }, [sessionId]); const getUploadedFiles = async () => { - const response = await get_attachment({ serverId, sessionId }); + if (isTauri) { + const response = await get_attachment({ serverId, sessionId }); - setUploadedFiles(response.hits.hits); + setUploadedFiles(response.hits.hits); + } else { + } }; const handleDelete = async (id: string) => { - const result = await delete_attachment({ serverId, id }); - + let result; + if (isTauri) { + result = await delete_attachment({ serverId, id }); + } else { + } if (!result) return; getUploadedFiles(); diff --git a/src/components/ChatMessage/PrevSuggestion.tsx b/src/components/ChatMessage/PrevSuggestion.tsx new file mode 100644 index 00000000..857abea3 --- /dev/null +++ b/src/components/ChatMessage/PrevSuggestion.tsx @@ -0,0 +1,65 @@ +import { MoveRight } from "lucide-react"; +import { FC, useEffect, useState } from "react"; + +import { Get } from "@/api/axiosRequest"; +import { useAppStore } from "@/stores/appStore"; + +interface PrevSuggestionProps { + sendMessage: (message: string) => void; +} + +const PrevSuggestion: FC = (props) => { + const { sendMessage } = props; + + const isTauri = useAppStore((state) => state.isTauri); + + const headersStr = localStorage.getItem("headers") || "{}"; + const headers = JSON.parse(headersStr); + const id = headers["APP-INTEGRATION-ID"] || "cvkm9hmhpcemufsg3vug"; + console.log("id", id); + + const [list, setList] = useState([]); + + useEffect(() => { + if (!isTauri) getList(); + }, [id]); + + const getList = async () => { + if (!id) return; + + const url = `/integration/${id}/chat/_suggest`; + + const [error, res] = await Get(`/integration/${id}/chat/_suggest`); + + if (error) { + console.error(url, error); + return setList([]); + } + + console.log("res", res); + + setList(Array.isArray(res) ? res : []); + }; + + console.log("id", id); + + return ( +
    + {list.map((item) => { + return ( +
  • sendMessage(item)} + > + {item} + + +
  • + ); + })} +
+ ); +}; + +export default PrevSuggestion; diff --git a/src/components/Common/Copyright/index.tsx b/src/components/Common/Copyright/index.tsx new file mode 100644 index 00000000..3b989daa --- /dev/null +++ b/src/components/Common/Copyright/index.tsx @@ -0,0 +1,24 @@ +import { useThemeStore } from "@/stores/themeStore"; +import logoLight from "@/assets/images/logo-light.png"; +import logoDark from "@/assets/images/logo-dark.png"; + +const Copyright = () => { + const isDark = useThemeStore((state) => state.isDark); + + const renderLogo = () => { + return ( +
+ Logo + + ); + }; + + return ( +
+ Powered by + {renderLogo()} +
+ ); +}; + +export default Copyright; diff --git a/src/components/Common/Icons/ItemIcon.tsx b/src/components/Common/Icons/ItemIcon.tsx index 9b34bfaa..b0ce90a6 100644 --- a/src/components/Common/Icons/ItemIcon.tsx +++ b/src/components/Common/Icons/ItemIcon.tsx @@ -1,4 +1,5 @@ import { File } from "lucide-react"; +import React from 'react'; import IconWrapper from "./IconWrapper"; import ThemedIcon from "./ThemedIcon"; @@ -11,7 +12,7 @@ interface ItemIconProps { onClick?: React.MouseEventHandler; } -function ItemIcon({ +const ItemIcon = React.memo(function ItemIcon({ item, className = "w-5 h-5 flex-shrink-0", onClick = () => {}, @@ -34,7 +35,7 @@ function ItemIcon({ let selectedIcon = icons[item?.icon]; if (!selectedIcon) { - selectedIcon=item?.icon + selectedIcon = item?.icon; } if (!selectedIcon) { @@ -65,6 +66,6 @@ function ItemIcon({ ); } -} +}) export default ItemIcon; diff --git a/src/components/Common/Icons/TypeIcon.tsx b/src/components/Common/Icons/TypeIcon.tsx index 295e7bae..4d72d410 100644 --- a/src/components/Common/Icons/TypeIcon.tsx +++ b/src/components/Common/Icons/TypeIcon.tsx @@ -20,6 +20,19 @@ function TypeIcon({ const endpoint_http = useAppStore((state) => state.endpoint_http); const connectorSource = useFindConnectorIcon(item); + if (item?.source?.icon) { + if ( + item?.source?.icon.startsWith("http://") || + item?.source?.icon.startsWith("https://") + ) { + return ( + + icon + + ); + } + } + // If the icon is a valid base64-encoded image const isBase64 = connectorSource?.icon?.startsWith("data:image/"); if (isBase64) { diff --git a/src/components/Search/Footer.tsx b/src/components/Common/UI/Footer.tsx similarity index 62% rename from src/components/Search/Footer.tsx rename to src/components/Common/UI/Footer.tsx index 82a5420d..6c84cec8 100644 --- a/src/components/Search/Footer.tsx +++ b/src/components/Common/UI/Footer.tsx @@ -2,21 +2,24 @@ import { ArrowDown01, Command, CornerDownLeft } from "lucide-react"; import { useTranslation } from "react-i18next"; import clsx from "clsx"; -import logoImg from "@/assets/icon.svg"; -import { useSearchStore } from "@/stores/searchStore"; import TypeIcon from "@/components/Common/Icons/TypeIcon"; -import { useAppStore } from "@/stores/appStore"; +import Copyright from "@/components/Common/Copyright"; import { isMac } from "@/utils/platform"; import PinOffIcon from "@/icons/PinOff"; import PinIcon from "@/icons/Pin"; +import logoImg from "@/assets/icon.svg"; +import { useAppStore } from "@/stores/appStore"; +import { useSearchStore } from "@/stores/searchStore"; import { useUpdateStore } from "@/stores/updateStore"; interface FooterProps { + isTauri: boolean; openSetting: () => void; setWindowAlwaysOnTop: (isPinned: boolean) => Promise; } export default function Footer({ + isTauri, openSetting, setWindowAlwaysOnTop, }: FooterProps) { @@ -41,46 +44,53 @@ export default function Footer({ return (
-
-
- {sourceData?.source?.name ? ( - - ) : ( - {t("search.footer.logoAlt")} - )} -
- {updateInfo?.available ? ( -
setVisible(true)}> - {t("search.footer.updateAvailable")} - -
+ {isTauri ? ( +
+
+ {sourceData?.source?.name ? ( + ) : ( - sourceData?.source?.name || - t("search.footer.version", { - version: process.env.VERSION || "v1.0.0", - }) + {t("search.footer.logoAlt")} )} -
+
+ {updateInfo?.available ? ( +
setVisible(true)} + > + {t("search.footer.updateAvailable")} + +
+ ) : ( + sourceData?.source?.name || + t("search.footer.version", { + version: process.env.VERSION || "v1.0.0", + }) + )} +
- + +
-
+ ) : ( + + )}
diff --git a/src/components/Search/NoResults.tsx b/src/components/Common/UI/NoResults.tsx similarity index 89% rename from src/components/Search/NoResults.tsx rename to src/components/Common/UI/NoResults.tsx index dbc1f431..a6e37b66 100644 --- a/src/components/Search/NoResults.tsx +++ b/src/components/Common/UI/NoResults.tsx @@ -2,11 +2,14 @@ import { Command } from "lucide-react"; import { useTranslation } from "react-i18next"; import { isMac } from "@/utils/platform"; +import { useShortcutsStore } from "@/stores/shortcutsStore"; import noDataImg from "@/assets/coconut-tree.png"; export const NoResults = () => { const { t } = useTranslation(); + const modeSwitch = useShortcutsStore((state) => state.modeSwitch); + return (
{ )} - T + {modeSwitch}
diff --git a/src/components/Footer.tsx b/src/components/Common/UI/SettingsFooter.tsx similarity index 54% rename from src/components/Footer.tsx rename to src/components/Common/UI/SettingsFooter.tsx index 03e63cb2..f7d5eca9 100644 --- a/src/components/Footer.tsx +++ b/src/components/Common/UI/SettingsFooter.tsx @@ -1,49 +1,49 @@ -import {Menu, MenuButton,} from "@headlessui/react"; +import { Menu, MenuButton } from "@headlessui/react"; +import { OctagonAlert, X } from "lucide-react"; // import { Settings, LogOut, User, ChevronUp, Home } from "lucide-react"; // import { Link } from "react-router-dom"; -import logoImg from "../assets/icon.svg"; -import {useAppStore} from "@/stores/appStore"; -import {OctagonAlert, X} from 'lucide-react'; + +import logoImg from "@/assets/icon.svg"; +import { useAppStore } from "@/stores/appStore"; const Footer = () => { + const error = useAppStore((state) => state.error); + const setError = useAppStore((state) => state.setError); - const error = useAppStore((state) => state.error); - const setError = useAppStore((state) => state.setError); + return ( +
+ {/* Move the warning message outside the border */} + {error && ( +
+
+ + + {error} + + setError("")} + size={32} + color="gray" + /> +
+
+ )} - return ( -
- {/* Move the warning message outside the border */} - {error && ( -
-
- - {error} - setError("")} - size={32} - color="gray" - /> -
-
- )} - -
- - - - +
+ + + + Coco - {/* */} - + {/* */} + - {/* + {/*
{({ active }) => ( @@ -104,21 +104,20 @@ const Footer = () => {
*/} -
+
- -
+
Version {process.env.VERSION || "v1.0.0"} - {/*
+ {/*
*/} -
-
- ); +
+
+ ); }; export default Footer; diff --git a/src/components/Search/AutoResizeTextarea.tsx b/src/components/Search/AutoResizeTextarea.tsx index 5a2410b5..30ed165f 100644 --- a/src/components/Search/AutoResizeTextarea.tsx +++ b/src/components/Search/AutoResizeTextarea.tsx @@ -6,13 +6,14 @@ interface AutoResizeTextareaProps { setInput: (value: string) => void; handleKeyDown?: (e: React.KeyboardEvent) => void; connected: boolean; + chatPlaceholder?: string; } // Forward ref to allow parent to interact with this component const AutoResizeTextarea = forwardRef< { reset: () => void; focus: () => void }, AutoResizeTextareaProps ->(({ input, setInput, handleKeyDown, connected }, ref) => { +>(({ input, setInput, handleKeyDown, connected, chatPlaceholder }, ref) => { const { t } = useTranslation(); const textareaRef = useRef(null); @@ -34,8 +35,10 @@ const AutoResizeTextarea = forwardRef< autoCapitalize="none" spellCheck="false" className="text-base flex-1 outline-none min-w-[200px] text-[#333] dark:text-[#d8d8d8] placeholder-text-xs placeholder-[#999] dark:placeholder-gray-500 bg-transparent" - placeholder={connected ? t('search.textarea.placeholder') : ""} - aria-label={t('search.textarea.ariaLabel')} + placeholder={ + connected ? chatPlaceholder || t("search.textarea.placeholder") : "" + } + aria-label={t("search.textarea.ariaLabel")} value={input} onChange={(e) => setInput(e.target.value)} onKeyDown={(e) => handleKeyDown?.(e)} diff --git a/src/components/Search/ContextMenu.tsx b/src/components/Search/ContextMenu.tsx index 0a9e92b6..323ecadc 100644 --- a/src/components/Search/ContextMenu.tsx +++ b/src/components/Search/ContextMenu.tsx @@ -20,7 +20,7 @@ interface State { } interface ContextMenuProps { - hideCoco: () => Promise; + hideCoco?: () => void; } const ContextMenu = ({ hideCoco }: ContextMenuProps) => { @@ -54,7 +54,7 @@ const ContextMenu = ({ hideCoco }: ContextMenuProps) => { setVisibleContextMenu(false); - hideCoco(); + hideCoco && hideCoco(); }, }, { diff --git a/src/components/Search/InputBox.tsx b/src/components/Search/InputBox.tsx index 02944f29..d2cb7f81 100644 --- a/src/components/Search/InputBox.tsx +++ b/src/components/Search/InputBox.tsx @@ -2,6 +2,7 @@ import { ArrowBigLeft, Search, Send, Brain } from "lucide-react"; import { useCallback, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import clsx from "clsx"; +import { useKeyPress } from "ahooks"; import ChatSwitch from "@/components/Common/ChatSwitch"; import AutoResizeTextarea from "./AutoResizeTextarea"; @@ -12,12 +13,11 @@ import { useSearchStore } from "@/stores/searchStore"; import { metaOrCtrlKey } from "@/utils/keyboardUtils"; import SearchPopover from "./SearchPopover"; // import AudioRecording from "../AudioRecording"; -import { hide_coco } from "@/commands"; import { DataSource } from "@/types/commands"; // import InputExtra from "./InputExtra"; // import { useConnectStore } from "@/stores/connectStore"; import { useShortcutsStore } from "@/stores/shortcutsStore"; -import { useKeyPress } from "ahooks"; +import Copyright from "@/components/Common/Copyright"; interface ChatInputProps { onSend: (message: string) => void; @@ -46,6 +46,11 @@ interface ChatInputProps { }) => Promise; getFileMetadata: (path: string) => Promise; getFileIcon: (path: string, size: number) => Promise; + hideCoco?: () => void; + hasFeature?: string[]; + hasModules?: string[]; + searchPlaceholder?: string; + chatPlaceholder?: string; } export default function ChatInput({ @@ -64,16 +69,12 @@ export default function ChatInput({ isChatPage = false, getDataSourcesByServer, setupWindowFocusListener, -}: // checkScreenPermission, -// requestScreenPermission, -// getScreenMonitors, -// getScreenWindows, -// captureMonitorScreenshot, -// captureWindowScreenshot, -// openFileDialog, -// getFileMetadata, -// getFileIcon, -ChatInputProps) { + hasFeature = ["think", "search", "think_icon", "search_icon"], + hideCoco, + hasModules = [], + searchPlaceholder, + chatPlaceholder, +}: ChatInputProps) { const { t } = useTranslation(); const showTooltip = useAppStore( @@ -156,6 +157,7 @@ ChatInputProps) { const handleSubmit = useCallback(() => { const trimmedValue = inputValue.trim(); + console.log("handleSubmit", trimmedValue, disabled); if (trimmedValue && !disabled) { changeInput(""); onSend(trimmedValue); @@ -168,7 +170,7 @@ ChatInputProps) { if (inputValue) { changeInput(""); } else if (!isPinned) { - hide_coco(); + hideCoco && hideCoco(); } }, [inputValue, isPinned]); @@ -295,12 +297,17 @@ ChatInputProps) { changeInput(value); }} connected={connected} - handleKeyDown={(e) => { + handleKeyDown={(e: React.KeyboardEvent) => { if (e.key === "Enter") { + if (e.nativeEvent.isComposing) { + return; + } + console.log("handleKeyDown", e.nativeEvent.isComposing); e.preventDefault(); handleSubmit(); } }} + chatPlaceholder={chatPlaceholder} /> ) : ( { onSend(e.target.value); @@ -390,7 +399,7 @@ ChatInputProps) {
{t("search.input.connectionError")}
{ reconnect(); setReconnectCountdown(10); @@ -424,38 +433,47 @@ ChatInputProps) { /> )} */} - + )} + onClick={DeepThinkClick} + > + + {isDeepThinkActive && ( + + {t("search.input.deepThink")} + + )} + + )} - + {hasFeature.includes("search") && ( + + )} + {!hasFeature.includes("search") && !hasFeature.includes("think") ? ( +
+ +
+ ) : null}
) : (
)} - {isChatPage ? null : ( + {isChatPage || hasModules?.length !== 2 ? null : (
{showTooltip && modifierKeyPressed ? (
void; isChatMode: boolean; input: string; @@ -18,12 +18,13 @@ interface SearchProps { size: number, queryStrings: any ) => Promise; - hideCoco: () => Promise; + hideCoco?: () => void; openSetting: () => void; setWindowAlwaysOnTop: (isPinned: boolean) => Promise; } function Search({ + isTauri, isChatMode, input, querySearch, @@ -81,7 +82,7 @@ function Search({ return (
{/* Search Results Panel */} {suggests.length > 0 ? ( @@ -105,6 +106,7 @@ function Search({ )}
diff --git a/src/components/Search/SearchListItem.tsx b/src/components/Search/SearchListItem.tsx index 6ace3301..37a49823 100644 --- a/src/components/Search/SearchListItem.tsx +++ b/src/components/Search/SearchListItem.tsx @@ -16,7 +16,7 @@ interface SearchListItemProps { showListRight?: boolean; } -const SearchListItem: React.FC = ({ +const SearchListItem: React.FC = React.memo(({ item, isSelected, currentIndex, @@ -68,6 +68,6 @@ const SearchListItem: React.FC = ({ ) : null}
); -}; +}); export default SearchListItem; diff --git a/src/components/Search/SearchPopover.tsx b/src/components/Search/SearchPopover.tsx index e14cc364..53f7f329 100644 --- a/src/components/Search/SearchPopover.tsx +++ b/src/components/Search/SearchPopover.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useCallback, useMemo } from "react"; +import { useState, useEffect, useCallback, useMemo, useRef } from "react"; import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react"; import { ChevronDownIcon, RefreshCw, Layers, Globe } from "lucide-react"; import clsx from "clsx"; @@ -8,7 +8,7 @@ import TypeIcon from "@/components/Common/Icons/TypeIcon"; import { useConnectStore } from "@/stores/connectStore"; import { useSearchStore } from "@/stores/searchStore"; import { DataSource } from "@/types/commands"; -import Checkbox from "../Common/Checkbox"; +import Checkbox from "@/components/Common/Checkbox"; interface SearchPopoverProps { isSearchActive: boolean; @@ -30,18 +30,26 @@ export default function SearchPopover({ const currentService = useConnectStore((state) => state.currentService); + const [showDataSource, setShowDataSource] = useState(false); + const getDataSourceList = useCallback(async () => { try { const res: DataSource[] = await getDataSourcesByServer( currentService?.id ); - const data = [ - { - id: "all", - name: "search.input.searchPopover.allScope", - }, - ...(res || []), - ]; + if (res?.length === 0) { + setDataSourceList([]); + return; + } + const data = res?.length + ? [ + { + id: "all", + name: "search.input.searchPopover.allScope", + }, + ...res, + ] + : []; setDataSourceList(data); } catch (err) { setDataSourceList([]); @@ -49,6 +57,29 @@ export default function SearchPopover({ } }, [currentService?.id]); + const popoverRef = useRef(null); + const buttonRef = useRef(null); + + useEffect(() => { + if (!showDataSource) return; + + const handleClickOutside = (event: MouseEvent) => { + if ( + popoverRef.current && + !popoverRef.current.contains(event.target as Node) && + buttonRef.current && + !buttonRef.current.contains(event.target as Node) + ) { + setShowDataSource(false); + } + }; + + document.addEventListener("mousedown", handleClickOutside); + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, [showDataSource]); + useEffect(() => { if (dataSourceList.length > 0) { onSelectDataSource("all", true, true); @@ -111,8 +142,16 @@ export default function SearchPopover({ {dataSourceList?.length > 0 && ( - - + + { + e.stopPropagation(); + setShowDataSource((prev) => !prev); + }} + > - -
{ - e.stopPropagation(); - }} + {showDataSource ? ( + -
- {t("search.input.searchPopover.title")} +
{ + e.stopPropagation(); + }} + > +
+ {t("search.input.searchPopover.title")} -
{ - setIsRefreshDataSource(true); +
{ + setIsRefreshDataSource(true); - getDataSourceList(); + getDataSourceList(); - setTimeout(() => { - setIsRefreshDataSource(false); - }, 1000); - }} - className="size-[24px] flex justify-center items-center rounded-lg border border-black/10 dark:border-white/10 cursor-pointer" - > - + setTimeout(() => { + setIsRefreshDataSource(false); + }, 1000); + }} + className="size-[24px] flex justify-center items-center rounded-lg border border-black/10 dark:border-white/10 cursor-pointer" + > + +
+
    + {dataSourceList?.map((item, index) => { + const { id, name } = item; + + const isAll = index === 0; + + return ( +
  • +
    + {isAll ? ( + + ) : ( + + )} + + {isAll && name ? t(name) : name} +
    + +
    + + onSelectDataSource(id, value, isAll) + } + /> +
    +
  • + ); + })} +
-
    - {dataSourceList?.map((item, index) => { - const { id, name } = item; - - const isAll = index === 0; - - return ( -
  • -
    - {isAll ? ( - - ) : ( - - )} - - {isAll && name ? t(name) : name} -
    - -
    - - onSelectDataSource(id, value, isAll) - } - /> -
    -
  • - ); - })} -
-
- + + ) : null} )} diff --git a/src/components/SearchChat/index.tsx b/src/components/SearchChat/index.tsx new file mode 100644 index 00000000..5f09b199 --- /dev/null +++ b/src/components/SearchChat/index.tsx @@ -0,0 +1,420 @@ +import { + useEffect, + useRef, + useCallback, + useReducer, + Suspense, + memo, +} from "react"; +import clsx from "clsx"; + +import Search from "@/components/Search/Search"; +import InputBox from "@/components/Search/InputBox"; +import ChatAI, { ChatAIRef } from "@/components/Assistant/Chat"; +import UpdateApp from "@/components/UpdateApp"; +import { isLinux, isWin } from "@/utils/platform"; +import { appReducer, initialAppState } from "@/reducers/appReducer"; +import { useWindowEvents } from "@/hooks/useWindowEvents"; +import { useAppStore } from "@/stores/appStore"; +import { useAuthStore } from "@/stores/authStore"; +import platformAdapter from "@/utils/platformAdapter"; +import { useStartupStore } from "@/stores/startupStore"; +import { DataSource } from "@/types/commands"; +import { useThemeStore } from "@/stores/themeStore"; +import { Get } from "@/api/axiosRequest"; + +interface SearchChatProps { + isTauri?: boolean; + hasModules?: string[]; + defaultModule?: "search" | "chat"; + + hasFeature?: string[]; + showChatHistory?: boolean; + + theme?: "auto" | "light" | "dark"; + searchPlaceholder?: string; + chatPlaceholder?: string; + + hideCoco?: () => void; + setIsPinned?: (value: boolean) => void; + querySearch: (input: string) => Promise; + queryDocuments: ( + from: number, + size: number, + queryStrings: any + ) => Promise; +} + +function SearchChat({ + isTauri = true, + hasModules = ["search", "chat"], + defaultModule = "search", + hasFeature = ["think", "search", "think_active", "search_active"], + theme, + hideCoco, + querySearch, + queryDocuments, + searchPlaceholder, + chatPlaceholder, + showChatHistory, + setIsPinned, +}: SearchChatProps) { + const customInitialState = { + ...initialAppState, + isDeepThinkActive: hasFeature.includes("think_active"), + isSearchActive: hasFeature.includes("search_active"), + }; + + const [state, dispatch] = useReducer(appReducer, customInitialState); + const { + isChatMode, + input, + isTransitioned, + isSearchActive, + isDeepThinkActive, + isTyping, + } = state; + + useWindowEvents(); + + const initializeListeners = useAppStore((state) => state.initializeListeners); + const initializeListeners_auth = useAuthStore( + (state) => state.initializeListeners + ); + + const setTheme = useThemeStore((state) => state.setTheme); + + useEffect(() => { + let mounted = true; + + const init = async () => { + if (!mounted) return; + await initializeListeners(); + await initializeListeners_auth(); + await platformAdapter.invokeBackend("get_app_search_source"); + if (theme && mounted) { + setTheme(theme); + } + }; + + init(); + + return () => { + mounted = false; + }; + }, []); + + const chatAIRef = useRef(null); + + const changeMode = useCallback(async (value: boolean) => { + dispatch({ type: "SET_CHAT_MODE", payload: value }); + }, []); + + const handleSendMessage = useCallback( + async (value: string) => { + dispatch({ type: "SET_INPUT", payload: value }); + if (isChatMode) { + chatAIRef.current?.init(value); + } + }, + [isChatMode] + ); + + const cancelChat = useCallback(() => { + chatAIRef.current?.cancelChat(); + }, []); + + const reconnect = useCallback(() => { + chatAIRef.current?.reconnect(); + }, []); + + const setInput = useCallback((value: string) => { + dispatch({ type: "SET_INPUT", payload: value }); + }, []); + + const toggleSearchActive = useCallback(() => { + dispatch({ type: "TOGGLE_SEARCH_ACTIVE" }); + }, []); + + const toggleDeepThinkActive = useCallback(() => { + dispatch({ type: "TOGGLE_DEEP_THINK_ACTIVE" }); + }, []); + + const LoadingFallback = () => ( +
loading...
+ ); + + const getFileUrl = useCallback((path: string) => { + return platformAdapter.convertFileSrc(path); + }, []); + + const openSetting = useCallback(() => { + return platformAdapter.emitEvent("open_settings", ""); + }, []); + + const setWindowAlwaysOnTop = useCallback(async (isPinned: boolean) => { + setIsPinned && setIsPinned(isPinned); + return platformAdapter.setAlwaysOnTop(isPinned); + }, []); + + const getDataSourcesByServer = useCallback( + async (serverId: string): Promise => { + if (isTauri) { + return platformAdapter.invokeBackend("get_datasources_by_server", { + id: serverId, + }); + } else { + const [error, response]: any = await Get("/datasource/_search"); + if (error) { + console.error("_search", error); + return []; + } + const res = response?.hits?.hits?.map((item: any) => { + return { + ...item, + id: item._source.id, + name: item._source.name, + }; + }); + return res || []; + } + }, + [] + ); + + const setupWindowFocusListener = useCallback(async (callback: () => void) => { + return platformAdapter.listenEvent("tauri://focus", callback); + }, []); + + const checkScreenPermission = useCallback(async () => { + return platformAdapter.checkScreenRecordingPermission(); + }, []); + + const requestScreenPermission = useCallback(() => { + return platformAdapter.requestScreenRecordingPermission(); + }, []); + + const getScreenMonitors = useCallback(async () => { + return platformAdapter.getScreenshotableMonitors(); + }, []); + + const getScreenWindows = useCallback(async () => { + return platformAdapter.getScreenshotableWindows(); + }, []); + + const captureMonitorScreenshot = useCallback(async (id: number) => { + return platformAdapter.captureMonitorScreenshot(id); + }, []); + + const captureWindowScreenshot = useCallback(async (id: number) => { + return platformAdapter.captureWindowScreenshot(id); + }, []); + + const openFileDialog = useCallback(async (options: { multiple: boolean }) => { + return platformAdapter.openFileDialog(options); + }, []); + + const getFileMetadata = useCallback(async (path: string) => { + return platformAdapter.getFileMetadata(path); + }, []); + + const getFileIcon = useCallback(async (path: string, size: number) => { + return platformAdapter.getFileIcon(path, size); + }, []); + + const checkUpdate = useCallback(async () => { + return platformAdapter.checkUpdate(); + }, []); + + const relaunchApp = useCallback(async () => { + return platformAdapter.relaunchApp(); + }, []); + + const defaultStartupWindow = useStartupStore((state) => { + return state.defaultStartupWindow; + }); + const setDefaultStartupWindow = useStartupStore((state) => { + return state.setDefaultStartupWindow; + }); + + const showCocoListenRef = useRef<(() => void) | undefined>(); + + useEffect(() => { + if (hasModules?.length === 1 && hasModules?.includes("chat")) { + changeMode(true); + } else { + changeMode(defaultModule === "chat"); + } + + let unlistenChangeStartupStore: (() => void) | undefined; + + const setupListener = async () => { + try { + unlistenChangeStartupStore = await platformAdapter.listenEvent( + "change-startup-store", + ({ payload }) => { + if ( + payload && + typeof payload === "object" && + "defaultStartupWindow" in payload + ) { + const startupWindow = payload.defaultStartupWindow; + if ( + startupWindow === "searchMode" || + startupWindow === "chatMode" + ) { + setDefaultStartupWindow(startupWindow); + } + } + } + ); + } catch (error) { + console.error("Error setting up change-startup-store listener:", error); + } + }; + + setupListener(); + + return () => { + if (unlistenChangeStartupStore) { + unlistenChangeStartupStore(); + } + }; + }, []); + + useEffect(() => { + const setupShowCocoListener = async () => { + if (showCocoListenRef.current) { + showCocoListenRef.current(); + showCocoListenRef.current = undefined; + } + + try { + const unlisten = await platformAdapter.listenEvent("show-coco", () => { + changeMode(defaultStartupWindow === "chatMode"); + }); + + showCocoListenRef.current = unlisten; + } catch (error) { + console.error("Error setting up show-coco listener:", error); + } + }; + + setupShowCocoListener(); + + return () => { + if (showCocoListenRef.current) { + showCocoListenRef.current(); + showCocoListenRef.current = undefined; + } + }; + }, [defaultStartupWindow, changeMode]); + + return ( +
+
+ +
+ +
+ }> + + +
+ +
+ {isTransitioned && isChatMode ? ( + }> + + + ) : null} +
+ + +
+ ); +} + +export default memo(SearchChat); diff --git a/src/components/Settings/Advanced/components/Shortcuts/index.tsx b/src/components/Settings/Advanced/components/Shortcuts/index.tsx index d7b6274e..f441f6fc 100644 --- a/src/components/Settings/Advanced/components/Shortcuts/index.tsx +++ b/src/components/Settings/Advanced/components/Shortcuts/index.tsx @@ -1,11 +1,12 @@ -import { ModifierKey, useShortcutsStore } from "@/stores/shortcutsStore"; import { useTranslation } from "react-i18next"; -import { formatKey } from "@/utils/keyboardUtils"; -import SettingsItem from "@/components/Settings/SettingsItem"; import { Command } from "lucide-react"; import { ChangeEvent, useEffect } from "react"; import { emit } from "@tauri-apps/api/event"; + +import { formatKey } from "@/utils/keyboardUtils"; +import SettingsItem from "@/components/Settings/SettingsItem"; import { isMac } from "@/utils/platform"; +import { ModifierKey, useShortcutsStore } from "@/stores/shortcutsStore"; export const modifierKeys: ModifierKey[] = isMac ? ["meta", "ctrl"] diff --git a/src/components/UpdateApp/index.tsx b/src/components/UpdateApp/index.tsx index 1f9b38b0..a2925981 100644 --- a/src/components/UpdateApp/index.tsx +++ b/src/components/UpdateApp/index.tsx @@ -69,7 +69,7 @@ const UpdateApp = ({ checkUpdate, relaunchApp }: UpdateAppProps) => { state.loading = true; - await updateInfo?.downloadAndInstall((progress) => { + await updateInfo?.downloadAndInstall((progress: any) => { switch (progress.event) { case "Started": state.total = progress.data.contentLength; diff --git a/src/hooks/useChatActions.ts b/src/hooks/useChatActions.ts index 35f99b2f..9912a9f0 100644 --- a/src/hooks/useChatActions.ts +++ b/src/hooks/useChatActions.ts @@ -1,8 +1,9 @@ import { useCallback } from "react"; -import { isTauri } from "@tauri-apps/api/core"; import type { Chat } from "@/components/Assistant/types"; import { close_session_chat, cancel_session_chat, session_chat_history, new_chat, send_message, open_session_chat, chat_history } from "@/commands" +import { useAppStore } from "@/stores/appStore"; +import { Get, Post } from "@/api/axiosRequest"; export function useChatActions( currentServiceId: string | undefined, @@ -19,14 +20,27 @@ export function useChatActions( changeInput?: (val: string) => void, websocketSessionId?: string, ) { + const isTauri = useAppStore((state) => state.isTauri); + const chatClose = useCallback(async (activeChat?: Chat) => { - if (!activeChat?._id || !currentServiceId) return; + if (!activeChat?._id) return; try { - let response: any = await close_session_chat({ - serverId: currentServiceId, - sessionId: activeChat?._id, - }); - response = JSON.parse(response || ""); + let response: any + if (isTauri) { + if (!currentServiceId) return; + response = await close_session_chat({ + serverId: currentServiceId, + sessionId: activeChat?._id, + }); + response = JSON.parse(response || ""); + } else { + const [error, res] = await Post(`/chat/${activeChat?._id}/_close`, {}) + if (error) { + console.error('_close', error); + return + } + response = res + } console.log("_close", response); } catch (error) { console.error("chatClose:", error); @@ -35,13 +49,24 @@ export function useChatActions( const cancelChat = useCallback(async (activeChat?: Chat) => { setCurChatEnd(true); - if (!activeChat?._id || !currentServiceId) return; + if (!activeChat?._id) return; try { - let response: any = await cancel_session_chat({ - serverId: currentServiceId, - sessionId: activeChat?._id, - }); - response = JSON.parse(response || ""); + let response: any + if (isTauri) { + if (!currentServiceId) return; + response = await cancel_session_chat({ + serverId: currentServiceId, + sessionId: activeChat?._id, + }); + response = JSON.parse(response || ""); + } else { + const [error, res] = await Post(`/chat/${activeChat?._id}/_cancel`, {}) + if (error) { + console.error('_cancel', error); + return + } + response = res + } console.log("_cancel", response); } catch (error) { console.error("cancelChat:", error); @@ -52,15 +77,29 @@ export function useChatActions( chat: Chat, callback?: (chat: Chat) => void ) => { - if (!chat?._id || !currentServiceId) return; + if (!chat?._id) return; try { - let response: any = await session_chat_history({ - serverId: currentServiceId, - sessionId: chat?._id, - from: 0, - size: 20, - }); - response = JSON.parse(response || ""); + let response: any + if (isTauri) { + if (!currentServiceId) return; + response = await session_chat_history({ + serverId: currentServiceId, + sessionId: chat?._id, + from: 0, + size: 20, + }); + response = JSON.parse(response || ""); + } else { + const [error, res] = await Get(`/chat/${chat?._id}/_history`, { + from: 0, + size: 20, + }) + if (error) { + console.error('_cancel', error); + return + } + response = res + } const hits = response?.hits?.hits || []; const updatedChat: Chat = { ...chat, @@ -76,29 +115,50 @@ export function useChatActions( const createNewChat = useCallback( async (value: string = "", activeChat?: Chat, id?: string) => { - setTimedoutShow(false); - setErrorShow(false); - chatClose(activeChat); - clearAllChunkData(); - setQuestion(value); - if (!currentServiceId) return; try { - if (!(websocketSessionId || id)){ + setTimedoutShow(false); + setErrorShow(false); + await chatClose(activeChat); + clearAllChunkData(); + setQuestion(value); + if (!(websocketSessionId || id)) { setErrorShow(true); console.error("websocketSessionId", websocketSessionId, id); return; } console.log("sourceDataIds", sourceDataIds, websocketSessionId, id); - let response: any = await new_chat({ - serverId: currentServiceId, - websocketId: websocketSessionId || id, - message: value, - queryParams: { + let response: any + if (isTauri) { + if (!currentServiceId) return; + response = await new_chat({ + serverId: currentServiceId, + websocketId: websocketSessionId || id, + message: value, + queryParams: { + search: isSearchActive, + deep_thinking: isDeepThinkActive, + datasource: sourceDataIds?.join(",") || "", + }, + }); + + } else { + console.log('websocketSessionId', websocketSessionId, id) + const [error, res] = await Post('/chat/_new', { + message: value, + }, { search: isSearchActive, deep_thinking: isDeepThinkActive, datasource: sourceDataIds?.join(",") || "", - }, - }); + }, { + "WEBSOCKET-SESSION-ID": websocketSessionId || id, + }) + if (error) { + setErrorShow(true); + console.error('_new', error); + return + } + response = res + } console.log("_new", response); const newChat: Chat = response; curIdRef.current = response?.payload?.id; @@ -124,27 +184,49 @@ export function useChatActions( const sendMessage = useCallback( async (content: string, newChat: Chat, id?: string) => { - if (!newChat?._id || !currentServiceId || !content) return; - + if (!newChat?._id || !content) return; + clearAllChunkData(); try { - if (!(websocketSessionId || id)){ + if (!(websocketSessionId || id)) { setErrorShow(true); console.error("websocketSessionId", websocketSessionId, id); return; } - let response: any = await send_message({ - serverId: currentServiceId, - websocketId: websocketSessionId || id, - sessionId: newChat?._id, - queryParams: { + let response: any + if (isTauri) { + if (!currentServiceId) return; + response = await send_message({ + serverId: currentServiceId, + websocketId: websocketSessionId || id, + sessionId: newChat?._id, + queryParams: { + search: isSearchActive, + deep_thinking: isDeepThinkActive, + datasource: sourceDataIds?.join(",") || "", + }, + message: content, + }); + response = JSON.parse(response || ""); + } else { + console.log('websocketSessionId', websocketSessionId, id) + const [error, res] = await Post(`/chat/${newChat?._id}/_send`, { + message: content + }, { search: isSearchActive, deep_thinking: isDeepThinkActive, datasource: sourceDataIds?.join(",") || "", - }, - message: content, - }); - response = JSON.parse(response || ""); + }, { + "WEBSOCKET-SESSION-ID": websocketSessionId || id, + }) + + if (error) { + setErrorShow(true); + console.error('_cancel', error); + return + } + response = res + } console.log("_send", response); curIdRef.current = response[0]?._id; @@ -178,13 +260,25 @@ export function useChatActions( ); const openSessionChat = useCallback(async (chat: Chat) => { - if (!chat?._id || !currentServiceId) return; + if (!chat?._id) return; try { - let response: any = await open_session_chat({ - serverId: currentServiceId, - sessionId: chat?._id, - }); - response = JSON.parse(response || ""); + let response: any + if (isTauri) { + if (!currentServiceId) return; + response = await open_session_chat({ + serverId: currentServiceId, + sessionId: chat?._id, + }); + response = JSON.parse(response || ""); + } else { + const [error, res] = await Post(`/chat/${chat?._id}/_open`, {}) + if (error) { + console.error('_open', error); + return null + } + response = res + } + console.log("_open", response); return response; } catch (error) { @@ -194,14 +288,27 @@ export function useChatActions( }, [currentServiceId]); const getChatHistory = useCallback(async () => { - if (!currentServiceId) return []; try { - let response: any = await chat_history({ - serverId: currentServiceId, - from: 0, - size: 20, - }); - response = JSON.parse(response || ""); + let response: any + if (isTauri) { + if (!currentServiceId) return []; + response = await chat_history({ + serverId: currentServiceId, + from: 0, + size: 20, + }); + response = JSON.parse(response || ""); + } else { + const [error, res] = await Get(`/chat/_history`, { + from: 0, + size: 20, + }) + if (error) { + console.error('_history', error); + return [] + } + response = res + } console.log("_history", response); const hits = response?.hits?.hits || []; return hits; @@ -212,7 +319,7 @@ export function useChatActions( }, [currentServiceId]); const createChatWindow = useCallback(async (createWin: any) => { - if (isTauri()) { + if (isTauri) { createWin && createWin({ label: "chat", title: "Coco Chat", diff --git a/src/hooks/useEscape.ts b/src/hooks/useEscape.ts index d60685da..418a49a4 100644 --- a/src/hooks/useEscape.ts +++ b/src/hooks/useEscape.ts @@ -1,8 +1,6 @@ import { useEffect } from "react"; -import { isTauri } from "@tauri-apps/api/core"; -import { listen } from "@tauri-apps/api/event"; -import { hide_coco } from "@/commands" +import platformAdapter from "@/utils/platformAdapter"; const useEscape = () => { const handleEscape = async (event: KeyboardEvent) => { @@ -12,16 +10,14 @@ const useEscape = () => { event.preventDefault(); // Hide the Tauri app window when 'Esc' is pressed - await hide_coco() + await platformAdapter.invokeBackend("hide_coco"); console.log("App window hidden successfully."); } }; useEffect(() => { - if (!isTauri()) return; - - const unlisten = listen("tauri://focus", () => { + const unlisten = platformAdapter.listenEvent("tauri://focus", () => { // Add event listener for keydown window.addEventListener("keydown", handleEscape); }); diff --git a/src/hooks/useFindConnectorIcon.ts b/src/hooks/useFindConnectorIcon.ts index 1268593c..eee38391 100644 --- a/src/hooks/useFindConnectorIcon.ts +++ b/src/hooks/useFindConnectorIcon.ts @@ -1,19 +1,26 @@ import { useConnectStore } from "@/stores/connectStore"; +import { useAppStore } from "@/stores/appStore"; export function useFindConnectorIcon(item: any) { const connector_data = useConnectStore((state) => state.connector_data); const datasourceData = useConnectStore((state) => state.datasourceData); const currentService = useConnectStore((state) => state.currentService); + const isTauri = useAppStore((state) => state.isTauri); + + let currentServiceId = currentService?.id; + if (!isTauri) { + currentServiceId = "web" + } const id = item?.source?.id || ""; - const result_source = datasourceData[currentService?.id]?.find( + const result_source = datasourceData[currentServiceId]?.find( (data: any) => data.id === id ); const connector_id = result_source?.connector?.id; - const result_connector = connector_data[currentService?.id]?.find( + const result_connector = connector_data[currentServiceId]?.find( (data: any) => data.id === connector_id ); diff --git a/src/hooks/useMessageHandler.ts b/src/hooks/useMessageHandler.ts index 5ebdd7b7..e2d3c384 100644 --- a/src/hooks/useMessageHandler.ts +++ b/src/hooks/useMessageHandler.ts @@ -76,6 +76,7 @@ export function useMessageHandler( return; } } catch (error) { + setCurChatEnd(true); console.error("parse error:", error); } }, diff --git a/src/hooks/useWebSocket.ts b/src/hooks/useWebSocket.ts index c9b1c960..5dc8faf1 100644 --- a/src/hooks/useWebSocket.ts +++ b/src/hooks/useWebSocket.ts @@ -1,8 +1,16 @@ import { useState, useEffect, useCallback, useRef } from "react"; -import { listen } from "@tauri-apps/api/event"; +import { useWebSocket as useWebSocketAHook } from "ahooks"; -import { IServer } from "@/stores/appStore"; -import { connect_to_server, disconnect } from "@/commands" +import { useAppStore, IServer } from "@/stores/appStore"; +import { connect_to_server, disconnect as disconnectCommand } from "@/commands" +import platformAdapter from "@/utils/platformAdapter"; + +enum ReadyState { + Connecting = 0, + Open = 1, + Closing = 2, + Closed = 3, +} interface WebSocketProps { clientId: string; @@ -21,41 +29,119 @@ export default function useWebSocket({ dealMsgRef, onWebsocketSessionId, }: WebSocketProps) { + const isTauri = useAppStore((state) => state.isTauri); + const endpoint_websocket = useAppStore((state) => state.endpoint_websocket); + + const websocketIdRef = useRef(""); + const messageQueue = useRef([]); + const processingRef = useRef(false); + + const { readyState, connect, disconnect } = useWebSocketAHook( + // "wss://coco.infini.cloud/ws", + // "ws://localhost:9000/ws", + isTauri ? "" : endpoint_websocket, + { + manual: true, + reconnectLimit: 3, + reconnectInterval: 3000, + onMessage: (event) => { + const msg = event.data as string; + messageQueue.current.push(msg); + processQueue(); + }, + } + ); + useEffect(() => { + if (!isTauri) { + connect(); + } + }, [isTauri, connect]); + + const processMessage = useCallback( + (msg: string) => { + try { + if (msg.includes("websocket-session-id")) { + const sessionId = msg.split(":")[1].trim(); + websocketIdRef.current = sessionId; + setConnected(true); + onWebsocketSessionId?.(sessionId); + } else { + dealMsgRef.current?.(msg); + } + } catch (error) { + console.error("处理消息出错:", error, msg); + } + }, + [onWebsocketSessionId] + ); + + const processQueue = useCallback(() => { + if (processingRef.current || messageQueue.current.length === 0) return; + + processingRef.current = true; + while (messageQueue.current.length > 0) { + const msg = messageQueue.current.shift(); + if (msg) { + console.log("处理消息:", msg.substring(0, 100)); + processMessage(msg); + } + } + processingRef.current = false; + }, [processMessage]); + + useEffect(() => { + if (readyState !== ReadyState.Open) { + setConnected(false); + } + }, [readyState]); + const [errorShow, setErrorShow] = useState(false); // 1. WebSocket connects when loading or switching services // src/components/Assistant/ChatHeader.tsx // 2. If not connected or disconnected, input box has a connect button, clicking it will connect to WebSocket // src/components/Search/InputBox.tsx - const reconnect = useCallback(async (server?: IServer) => { - const targetServer = server || currentService; - console.log("reconnect_targetServer", targetServer?.id); - if (!targetServer?.id) return; - try { - console.log("reconnect", targetServer.id, clientId); - await connect_to_server(targetServer.id, clientId); - } catch (error) { - setConnected(false); - console.error("Failed to connect:", error); - } - }, [currentService]); + const reconnect = useCallback( + async (server?: IServer) => { + if (isTauri) { + const targetServer = server || currentService; + if (!targetServer?.id) return; + try { + // console.log("reconnect", targetServer.id); + await connect_to_server(targetServer.id, clientId); + } catch (error) { + setConnected(false); + console.error("Failed to connect:", error); + } + } else { + connect(); + } + }, + [currentService] + ); + const disconnectWS = async () => { if (!connected) return; - try { - console.log("disconnect"); - await disconnect(clientId); - setConnected(false); - } catch (error) { - console.error("Failed to disconnect:", error); + if (isTauri) { + try { + console.log("disconnect"); + await disconnectCommand(clientId); + setConnected(false); + } catch (error) { + console.error("Failed to disconnect:", error); + } + } else { + disconnect(); } }; - const updateDealMsg = useCallback((newDealMsg: (msg: string) => void) => { - dealMsgRef.current = newDealMsg; - }, [dealMsgRef]); - - const websocketIdRef = useRef('') + const updateDealMsg = useCallback( + (newDealMsg: (msg: string) => void) => { + dealMsgRef.current = newDealMsg; + }, + [dealMsgRef] + ); useEffect(() => { if (!currentService?.id) return; @@ -64,7 +150,9 @@ export default function useWebSocket({ let unlisten_message = null; setErrorShow(false); - unlisten_error = listen(`ws-error-${clientId}`, (event) => { + + if (!isTauri) return; + unlisten_error = platformAdapter.listenEvent(`ws-error-${clientId}`, (event) => { // { // "error": { // "reason": "invalid login" @@ -76,7 +164,7 @@ export default function useWebSocket({ setErrorShow(true); }); - unlisten_message = listen(`ws-message-${clientId}`, (event) => { + unlisten_message = platformAdapter.listenEvent(`ws-message-${clientId}`, (event) => { const msg = event.payload as string; console.log(`ws-message-${clientId}`, msg); if (msg.includes("websocket-session-id")) { diff --git a/src/hooks/useWindows.ts b/src/hooks/useWindows.ts index f3e2fa75..167d1243 100644 --- a/src/hooks/useWindows.ts +++ b/src/hooks/useWindows.ts @@ -1,8 +1,6 @@ -import { useEffect, useCallback } from "react"; -import { getAllWindows, getCurrentWindow } from "@tauri-apps/api/window"; -import { WebviewWindow } from "@tauri-apps/api/webviewWindow"; -import { listen } from "@tauri-apps/api/event"; -import { isTauri } from "@tauri-apps/api/core"; +import { useState, useEffect, useCallback } from "react"; + +import platformAdapter from "@/utils/platformAdapter"; const defaultWindowConfig = { label: "", @@ -23,8 +21,20 @@ const defaultWindowConfig = { }; export const useWindows = () => { - if (!isTauri()) return {} - const appWindow = getCurrentWindow(); + const [appWindow, setAppWindow] = useState(null); + + useEffect(() => { + const fetchWindow = async () => { + try { + const window = await platformAdapter.getCurrentWindow(); + setAppWindow(window); + } catch (error) { + console.error("Failed to get current window:", error); + } + }; + + fetchWindow(); + }, []); const createWin = useCallback(async (options: any) => { const args = { ...defaultWindowConfig, ...options }; @@ -39,23 +49,26 @@ export const useWindows = () => { return; } - const win = new WebviewWindow(args.label, args); + const win = await platformAdapter.createWebviewWindow(args.label, args); - win.once("tauri://created", async () => { - console.log("tauri://created"); - // if (args.label.includes("main")) { - // - // } + if(win) { + win.once("tauri://created", async () => { + console.log("tauri://created"); + // if (args.label.includes("main")) { + // + // } - if (args.maximized && args.resizable) { - console.log("is-maximized"); - await win.maximize(); - } - }); + if (args.maximized && args.resizable) { + console.log("is-maximized"); + await win.maximize(); + } + }); + + win.once("tauri://error", (error: any) => { + console.error("error:", error); + }); + } - win.once("tauri://error", (error) => { - console.error("error:", error); - }); }, []); const closeWin = useCallback(async (label: string) => { @@ -75,24 +88,24 @@ export const useWindows = () => { }, []); const getWin = useCallback(async (label: string) => { - return WebviewWindow.getByLabel(label); + return platformAdapter.getWindowByLabel(label); }, []); const getAllWin = useCallback(async () => { - return getAllWindows(); + return platformAdapter.getAllWindows(); }, []); const listenEvents = useCallback(() => { let unlistenHandlers: { (): void; (): void; (): void; (): void; }[] = []; const setupListeners = async () => { - const winCreateHandler = await listen("win-create", (event) => { + const winCreateHandler = await platformAdapter.listenWindowEvent("win-create", (event) => { console.log(event); createWin(event.payload); }); unlistenHandlers.push(winCreateHandler); - const winShowHandler = await listen("win-show", async () => { + const winShowHandler = await platformAdapter.listenWindowEvent("win-show", async () => { if (!appWindow || !appWindow.label.includes("main")) return; await appWindow.show(); await appWindow.unminimize(); @@ -100,13 +113,13 @@ export const useWindows = () => { }); unlistenHandlers.push(winShowHandler); - const winHideHandler = await listen("win-hide", async () => { + const winHideHandler = await platformAdapter.listenWindowEvent("win-hide", async () => { if (!appWindow || !appWindow.label.includes("main")) return; await appWindow.hide(); }); unlistenHandlers.push(winHideHandler); - const winCloseHandler = await listen("win-close", async () => { + const winCloseHandler = await platformAdapter.listenWindowEvent("win-close", async () => { await appWindow.close(); }); unlistenHandlers.push(winCloseHandler); diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index fe2923f2..e3ef6a18 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -147,12 +147,13 @@ "version": "{{version}}", "updateAvailable": "Update available", "select": "Select", - "open": "Open" + "open": "Open", + "powered": "Powered by Coco AI" }, "input": { "searchPlaceholder": "Search whatever you want ...", "connectionError": "Unable to connect to the server", - "reconnect": "Reconnect", + "reconnect": "Click here to reconnect.", "connecting": "Connecting", "deepThink": "Deep Think", "search": "Search", diff --git a/src/locales/zh/translation.json b/src/locales/zh/translation.json index 047bfdf2..63b77fbe 100644 --- a/src/locales/zh/translation.json +++ b/src/locales/zh/translation.json @@ -147,12 +147,13 @@ "version": "{{version}}", "updateAvailable": "有可用更新", "select": "选择", - "open": "打开" + "open": "打开", + "powered": "由 Coco AI 提供支持" }, "input": { "searchPlaceholder": "搜索任何内容...", "connectionError": "无法连接到服务器", - "reconnect": "重新连接", + "reconnect": "点此重连", "connecting": "连接中", "deepThink": "深度思考", "search": "联网搜索", diff --git a/src/pages/main/index.tsx b/src/pages/main/index.tsx index 83402a68..eaacfae2 100644 --- a/src/pages/main/index.tsx +++ b/src/pages/main/index.tsx @@ -1,13 +1,17 @@ import { useCallback, useEffect } from "react"; +import { useKeyPress } from "ahooks"; -import SearchChat from "@/pages/web/SearchChat"; +import SearchChat from "@/components/SearchChat"; import platformAdapter from "@/utils/platformAdapter"; import { useShortcutsStore } from "@/stores/shortcutsStore"; import { useStartupStore } from "@/stores/startupStore"; -import { useKeyPress } from "ahooks"; import { modifierKeys } from "@/components/Settings/Advanced/components/Shortcuts"; +import { useAppStore } from "@/stores/appStore"; function MainApp() { + const setIsTauri = useAppStore((state) => state.setIsTauri); + setIsTauri(true); + const querySearch = useCallback(async (input: string) => { try { const response: any = await platformAdapter.invokeBackend( @@ -44,6 +48,11 @@ function MainApp() { }, [] ); + + const hideCoco = useCallback(() => { + return platformAdapter.hideWindow(); + }, []); + const modifierKey = useShortcutsStore((state) => { return state.modifierKey; }); @@ -129,7 +138,13 @@ function MainApp() { ); return ( - + ); } diff --git a/src/pages/settings/index.tsx b/src/pages/settings/index.tsx index c8c215f2..e8fa03c5 100644 --- a/src/pages/settings/index.tsx +++ b/src/pages/settings/index.tsx @@ -8,7 +8,7 @@ import SettingsPanel from "@/components/Settings/SettingsPanel"; import GeneralSettings from "@/components/Settings/GeneralSettings"; import AboutView from "@/components/Settings/AboutView"; import Cloud from "@/components/Cloud/Cloud.tsx"; -import Footer from "@/components/Footer"; +import Footer from "@/components/Common/UI/SettingsFooter"; import { useTray } from "@/hooks/useTray"; import Advanced from "@/components/Settings/Advanced"; diff --git a/src/pages/web/README.md b/src/pages/web/README.md new file mode 100644 index 00000000..3dc4379f --- /dev/null +++ b/src/pages/web/README.md @@ -0,0 +1,100 @@ +# SearchChat Web Component API + +## Props + +### `serverUrl` +- **类型**: `string` +- **可选**: 是 +- **默认值**: `""` +- **描述**: 设置服务器地址 + +### `headers` +- **类型**: `Record` +- **可选**: 是 +- **默认值**: `{}` +- **描述**: 请求头配置 + +### `width` +- **类型**: `number` +- **可选**: 是 +- **默认值**: `680` +- **描述**: 组件容器的宽度,单位为像素 + +### `height` +- **类型**: `number` +- **可选**: 是 +- **默认值**: `590` +- **描述**: 组件容器的高度,单位为像素 + +### `hasModules` +- **类型**: `string[]` +- **可选**: 是 +- **默认值**: `['search', 'chat']` +- **描述**: 启用的功能模块列表,目前支持 'search' 和 'chat' 模块 + +### `hasFeature` +- **类型**: `string[]` +- **可选**: 是 +- **默认值**: `['think', 'search', 'think_active', 'search_active']` +- **描述**: 启用的特性列表,支持 'think'、'search'、'think_active'、'search_active' 特性。其中 'think_active' 表示默认开启深度思考,'search_active' 表示默认开启搜索 + +### `hideCoco` +- **类型**: `() => void` +- **可选**: 是 +- **默认值**: `() => {}` +- **描述**: 隐藏搜索窗口的回调函数 + +### `theme` +- **类型**: `"auto" | "light" | "dark"` +- **可选**: 是 +- **默认值**: `"dark"` +- **描述**: 主题设置,支持自动(跟随系统)、亮色和暗色三种模式 + +### `searchPlaceholder` +- **类型**: `string` +- **可选**: 是 +- **默认值**: `""` +- **描述**: 搜索框的占位文本 + +### `chatPlaceholder` +- **类型**: `string` +- **可选**: 是 +- **默认值**: `""` +- **描述**: 聊天输入框的占位文本 + +### `showChatHistory` +- **类型**: `boolean` +- **可选**: 是 +- **默认值**: `true` +- **描述**: 是否显示聊天历史记录 + +### `setIsPinned` +- **类型**: `(value: boolean) => void` +- **可选**: 是 +- **默认值**: `undefined` +- **描述**: 设置窗口置顶状态的回调函数 + +## 使用示例 + +```tsx +import SearchChat from 'search-chat'; + +function App() { + return ( + console.log('hide')} + theme="dark" + searchPlaceholder="" + chatPlaceholder="" + showChatHistory={true} + setIsPinned={(isPinned) => console.log('isPinned:', isPinned)} + /> + ); +} +``` \ No newline at end of file diff --git a/src/pages/web/SearchChat.tsx b/src/pages/web/SearchChat.tsx deleted file mode 100644 index 4e54bd39..00000000 --- a/src/pages/web/SearchChat.tsx +++ /dev/null @@ -1,311 +0,0 @@ -import { - useEffect, - useRef, - useCallback, - useReducer, - Suspense, - memo, -} from "react"; -import clsx from "clsx"; - -import ErrorBoundary from "@/components/Common/ErrorBoundary"; -import Search from "@/components/Search/Search"; -import InputBox from "@/components/Search/InputBox"; -import ChatAI, { ChatAIRef } from "@/components/Assistant/Chat"; -import UpdateApp from "@/components/UpdateApp"; -import { isLinux, isWin } from "@/utils/platform"; -import { appReducer, initialAppState } from "@/reducers/appReducer"; -import { useWindowEvents } from "@/hooks/useWindowEvents"; -import { useAppStore } from "@/stores/appStore"; -import { useAuthStore } from "@/stores/authStore"; -import platformAdapter from "@/utils/platformAdapter"; -import { useStartupStore } from "@/stores/startupStore"; -import { DataSource } from "@/types/commands"; - -interface SearchChatProps { - querySearch: (input: string) => Promise; - queryDocuments: ( - from: number, - size: number, - queryStrings: any - ) => Promise; -} - -function SearchChat({ querySearch, queryDocuments }: SearchChatProps) { - const [state, dispatch] = useReducer(appReducer, initialAppState); - const { - isChatMode, - input, - isTransitioned, - isSearchActive, - isDeepThinkActive, - isTyping, - } = state; - - useWindowEvents(); - - const initializeListeners = useAppStore((state) => state.initializeListeners); - const initializeListeners_auth = useAuthStore( - (state) => state.initializeListeners - ); - - useEffect(() => { - initializeListeners(); - initializeListeners_auth(); - platformAdapter.invokeBackend("get_app_search_source"); - }, []); - - const chatAIRef = useRef(null); - - const changeMode = useCallback(async (value: boolean) => { - dispatch({ type: "SET_CHAT_MODE", payload: value }); - localStorage.setItem("coco-chat-mode", String(value)); - }, []); - - const handleSendMessage = useCallback( - async (value: string) => { - dispatch({ type: "SET_INPUT", payload: value }); - if (isChatMode) { - chatAIRef.current?.init(value); - } - }, - [isChatMode] - ); - - const cancelChat = useCallback(() => { - chatAIRef.current?.cancelChat(); - }, []); - - const reconnect = useCallback(() => { - chatAIRef.current?.reconnect(); - }, []); - - const setInput = useCallback((value: string) => { - dispatch({ type: "SET_INPUT", payload: value }); - }, []); - - const toggleSearchActive = useCallback(() => { - dispatch({ type: "TOGGLE_SEARCH_ACTIVE" }); - }, []); - - const toggleDeepThinkActive = useCallback(() => { - dispatch({ type: "TOGGLE_DEEP_THINK_ACTIVE" }); - }, []); - - const LoadingFallback = () => ( -
loading...
- ); - - const hideCoco = useCallback(() => { - return platformAdapter.hideWindow(); - }, []); - - const getFileUrl = useCallback((path: string) => { - return platformAdapter.convertFileSrc(path); - }, []); - - const openSetting = useCallback(() => { - return platformAdapter.emitEvent("open_settings", ""); - }, []); - - const setWindowAlwaysOnTop = useCallback(async (isPinned: boolean) => { - return platformAdapter.setAlwaysOnTop(isPinned); - }, []); - - const getDataSourcesByServer = useCallback( - async (serverId: string): Promise => { - return platformAdapter.invokeBackend("get_datasources_by_server", { - id: serverId, - }); - }, - [] - ); - - const setupWindowFocusListener = useCallback(async (callback: () => void) => { - return platformAdapter.listenEvent("tauri://focus", callback); - }, []); - - const checkScreenPermission = useCallback(async () => { - return platformAdapter.checkScreenRecordingPermission(); - }, []); - - const requestScreenPermission = useCallback(() => { - return platformAdapter.requestScreenRecordingPermission(); - }, []); - - const getScreenMonitors = useCallback(async () => { - return platformAdapter.getScreenshotableMonitors(); - }, []); - - const getScreenWindows = useCallback(async () => { - return platformAdapter.getScreenshotableWindows(); - }, []); - - const captureMonitorScreenshot = useCallback(async (id: number) => { - return platformAdapter.captureMonitorScreenshot(id); - }, []); - - const captureWindowScreenshot = useCallback(async (id: number) => { - return platformAdapter.captureWindowScreenshot(id); - }, []); - - const openFileDialog = useCallback(async (options: { multiple: boolean }) => { - return platformAdapter.openFileDialog(options); - }, []); - - const getFileMetadata = useCallback(async (path: string) => { - return platformAdapter.getFileMetadata(path); - }, []); - - const getFileIcon = useCallback(async (path: string, size: number) => { - return platformAdapter.getFileIcon(path, size); - }, []); - - const checkUpdate = useCallback(async () => { - return platformAdapter.checkUpdate(); - }, []); - - const relaunchApp = useCallback(async () => { - return platformAdapter.relaunchApp(); - }, []); - - const defaultStartupWindow = useStartupStore((state) => { - return state.defaultStartupWindow; - }); - - const showCocoListenRef = useRef<(() => void) | undefined>(); - - useEffect(() => { - const setupShowCocoListener = async () => { - if (showCocoListenRef.current) { - showCocoListenRef.current(); - showCocoListenRef.current = undefined; - } - - try { - const unlisten = await platformAdapter.listenEvent("show-coco", () => { - const chatMode = localStorage.getItem("coco-chat-mode"); - changeMode( - chatMode ? chatMode === "true" : defaultStartupWindow === "chatMode" - ); - }); - - showCocoListenRef.current = unlisten; - } catch (error) { - console.error("Error setting up show-coco listener:", error); - } - }; - - setupShowCocoListener(); - - return () => { - if (showCocoListenRef.current) { - showCocoListenRef.current(); - showCocoListenRef.current = undefined; - } - }; - }, [defaultStartupWindow, changeMode]); - - return ( - -
-
- -
- -
- }> - - -
- -
- {isTransitioned && isChatMode ? ( - }> - - - ) : null} -
- - -
-
- ); -} - -export default memo(SearchChat); diff --git a/src/pages/web/index.tsx b/src/pages/web/index.tsx index 8fe8eb72..8ad71ee8 100644 --- a/src/pages/web/index.tsx +++ b/src/pages/web/index.tsx @@ -1,22 +1,136 @@ -import { useCallback } from "react"; +import { useEffect, useCallback } from "react"; -import SearchChat from "./SearchChat"; +import SearchChat from "@/components/SearchChat"; +import { useAppStore } from "@/stores/appStore"; +import { Get } from "@/api/axiosRequest"; +import { useShortcutsStore } from "@/stores/shortcutsStore"; + +import "@/i18n"; +import "@/web.css"; + +interface WebAppProps { + headers?: Record; + serverUrl?: string; + width?: number; + height?: number; + hasModules?: string[]; + defaultModule?: "search" | "chat"; + hasFeature?: string[]; + hideCoco?: () => void; + theme?: "auto" | "light" | "dark"; + searchPlaceholder?: string; + chatPlaceholder?: string; + showChatHistory?: boolean; + setIsPinned?: (value: boolean) => void; +} + +function WebApp({ + width = 680, + height = 590, + headers = { + "X-API-TOKEN": "cvkm9hmhpcemufsg3vtgxbns7jqioo6uq5btira638fzclrkstbvc0hoe2kd86vnhrnf2e3izpoy4phrmv79", + "APP-INTEGRATION-ID": "cvkm9hmhpcemufsg3vug", + }, + // token = "cva1j5ehpcenic3ir7k0h8fb8qtv35iwtywze248oscrej8yoivhb5b1hyovp24xejjk27jy9ddt69ewfi3n", // https://coco.infini.cloud + // token = "cv97ieo2sdbbru4vtha094eyxuzxdj6pvp9fbdzxb66dff0djy4rsjyju6yymypxe42lg2h7jl6ohdksecth", // http://localhost:9000 + // token = "cv5djeb9om602jdvtnmg6kc1muyn2vcadr6te48j9t9pvt59ewrnwj7fwvxrw3va84j2a0lb5y8194fbr3jd", // http://43.153.113.88:9000 + serverUrl = "http://localhost:9000", + hideCoco = () => {}, + hasModules = ["search", "chat"], + defaultModule = "search", + hasFeature = ['think_active', 'search_active'], + theme="light", + searchPlaceholder = "", + chatPlaceholder = "", + showChatHistory = true, + setIsPinned, +}: WebAppProps) { + const setIsTauri = useAppStore((state) => state.setIsTauri); + const setEndpoint = useAppStore((state) => state.setEndpoint); + const setModeSwitch = useShortcutsStore((state) => state.setModeSwitch); + + useEffect(() => { + setIsTauri(false); + setEndpoint(serverUrl); + setModeSwitch("S"); + localStorage.setItem("headers", JSON.stringify(headers||{})); + }, []); + + const query_coco_fusion = useCallback(async (url: string) => { + try { + const [error, response]: any = await Get(url); + + if (error) { + console.error("_search", error); + return { hits: [], total: 0 }; + } + + console.log("_suggest", url, response); + const hits = + response?.hits?.hits?.map((hit: any) => ({ + document: { + ...hit._source, + }, + score: hit._score || 0, + source: hit._source.source || null, + })) || []; + const total = response?.hits?.total?.value || 0; + + console.log("_suggest2", url, total, hits); + + return { + hits: hits, + total_hits: total, + }; + } catch (error) { + console.error("query_coco_fusion error:", error); + throw error; + } + }, []); -function WebApp() { const querySearch = useCallback(async (input: string) => { console.log(input); + return await query_coco_fusion(`/query/_search?query=${input}`); }, []); const queryDocuments = useCallback( async (from: number, size: number, queryStrings: any) => { console.log(from, size, queryStrings); + try { + let url = `/query/_search?query=${queryStrings.query}&datasource=${queryStrings.datasource}&from=${from}&size=${size}`; + if (queryStrings?.rich_categories) { + url = `/query/_search?query=${queryStrings.query}&rich_category=${queryStrings.rich_category}&from=${from}&size=${size}`; + } + return await query_coco_fusion(url); + } catch (error) { + console.error("query_coco_fusion error:", error); + throw error; + } }, [] ); return ( -
- +
+
); } diff --git a/src/stores/appStore.ts b/src/stores/appStore.ts index 5febad56..f51da659 100644 --- a/src/stores/appStore.ts +++ b/src/stores/appStore.ts @@ -52,6 +52,9 @@ export type IAppStore = { showCocoShortcuts: string[]; setShowCocoShortcuts: (showCocoShortcuts: string[]) => void; + isTauri: boolean; + setIsTauri: (isTauri: boolean) => void; + visible: boolean; withVisibility: (fn: () => Promise) => Promise; }; @@ -107,6 +110,8 @@ export const useAppStore = create()( return set({ showCocoShortcuts }); }, + isTauri: true, + setIsTauri: (isTauri: boolean) => set({ isTauri }), visible: false, withVisibility: async (fn: () => Promise) => { set({ visible: true }); diff --git a/src/stores/connectStore.ts b/src/stores/connectStore.ts index 01429570..e6714f8e 100644 --- a/src/stores/connectStore.ts +++ b/src/stores/connectStore.ts @@ -1,7 +1,8 @@ import { create } from "zustand"; import { persist } from "zustand/middleware"; import { produce } from "immer"; -import { listen, emit } from "@tauri-apps/api/event"; + +import platformAdapter from "@/utils/platformAdapter"; const CONNECTOR_CHANGE_EVENT = "connector_data_change"; const DATASOURCE_CHANGE_EVENT = "datasourceData_change"; @@ -53,7 +54,7 @@ export const useConnectStore = create()( draft.connector_data[key] = connector_data; }) ); - await emit(CONNECTOR_CHANGE_EVENT, { + await platformAdapter.emitEvent(CONNECTOR_CHANGE_EVENT, { connector_data, }); }, @@ -64,16 +65,16 @@ export const useConnectStore = create()( draft.datasourceData[key] = datasourceData; }) ); - await emit(DATASOURCE_CHANGE_EVENT, { + await platformAdapter.emitEvent(DATASOURCE_CHANGE_EVENT, { datasourceData, }); }, initializeListeners: () => { - listen(CONNECTOR_CHANGE_EVENT, (event: any) => { + platformAdapter.listenEvent(CONNECTOR_CHANGE_EVENT, (event: any) => { const { connector_data } = event.payload; set({ connector_data }); }); - listen(DATASOURCE_CHANGE_EVENT, (event: any) => { + platformAdapter.listenEvent(DATASOURCE_CHANGE_EVENT, (event: any) => { const { datasourceData } = event.payload; set({ datasourceData }); }); diff --git a/src/stores/updateStore.ts b/src/stores/updateStore.ts index 48fa3f04..f7998ddf 100644 --- a/src/stores/updateStore.ts +++ b/src/stores/updateStore.ts @@ -1,4 +1,3 @@ -import { Update } from "@tauri-apps/plugin-updater"; import { create } from "zustand"; import { persist } from "zustand/middleware"; @@ -9,8 +8,8 @@ export type IUpdateStore = { setSkipVersion: (skipVersion?: string) => void; isOptional: boolean; setIsOptional: (isOptional: boolean) => void; - updateInfo?: Update; - setUpdateInfo: (updateInfo?: Update) => void; + updateInfo?: any; + setUpdateInfo: (updateInfo?: any) => void; }; export const useUpdateStore = create()( @@ -27,7 +26,7 @@ export const useUpdateStore = create()( setIsOptional: (isOptional: boolean) => { return set({ isOptional }); }, - setUpdateInfo: (updateInfo?: Update) => { + setUpdateInfo: (updateInfo?: any) => { return set({ updateInfo }); }, }), diff --git a/src/utils/index.ts b/src/utils/index.ts index bf12cecc..22239b07 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,8 +1,8 @@ import { useEffect, useState } from "react"; -import { isTauri } from "@tauri-apps/api/core"; -import { open } from "@tauri-apps/plugin-shell"; import { hide_coco } from "@/commands" +import { useAppStore } from "@/stores/appStore"; +import platformAdapter from "./platformAdapter"; // 1 export async function copyToClipboard(text: string) { @@ -65,10 +65,11 @@ export const IsTauri = () => { export const OpenURLWithBrowser = async (url: string) => { if (!url) return; - if (isTauri()) { + if (IsTauri()) { try { await open(url); await hide_coco(); + await platformAdapter.invokeBackend("hide_coco"); console.log("URL opened in default browser"); } catch (error) { console.error("Failed to open URL:", error); diff --git a/src/utils/platformAdapter.ts b/src/utils/platformAdapter.ts index ce4b8217..43719dcc 100644 --- a/src/utils/platformAdapter.ts +++ b/src/utils/platformAdapter.ts @@ -1,7 +1,6 @@ -import { useState } from "react"; -import { isTauri } from "@tauri-apps/api/core"; -import { convertFileSrc as tauriConvertFileSrc } from "@tauri-apps/api/core"; -import type { OpenDialogOptions } from "@tauri-apps/plugin-dialog"; +import { createWebAdapter } from './webAdapter'; +// import { createTauriAdapter } from './tauriAdapter'; + import { IShortcutsStore } from "@/stores/shortcutsStore"; import { IStartupStore } from "@/stores/startupStore"; @@ -29,8 +28,19 @@ export interface EventPayloads { open_settings: string | ""; tab_index: string | ""; login_or_logout: any; + 'show-coco': void; + 'connector_data_change': void; + 'datasourceData_change': void; + 'ws-error': void; + 'ws-message': void; + [key: `ws-error-${string}`]: { + error: { + reason: string; + }; + status: number; + }; + [key: `ws-message-${string}`]: string; "change-startup-store": IStartupStore - "show-coco": void; "change-shortcuts-store": IShortcutsStore; } @@ -40,7 +50,6 @@ export interface PlatformAdapter { setWindowSize: (width: number, height: number) => Promise; hideWindow: () => Promise; showWindow: () => Promise; - isPlatformTauri: () => boolean; convertFileSrc: (path: string) => string; emitEvent: (event: string, payload?: any) => Promise; listenEvent: ( @@ -54,9 +63,9 @@ export interface PlatformAdapter { getScreenshotableWindows: () => Promise; captureMonitorScreenshot: (id: number) => Promise; captureWindowScreenshot: (id: number) => Promise; - openFileDialog: ( - options: OpenDialogOptions - ) => Promise; + openFileDialog: (options: { + multiple: boolean; + }) => Promise; getFileMetadata: (path: string) => Promise; getFileIcon: (path: string, size: number) => Promise; checkUpdate: () => Promise; @@ -72,406 +81,18 @@ export interface PlatformAdapter { show: () => Promise; setFocus: () => Promise; center: () => Promise; + close: () => Promise; } | null>; createWindow: (label: string, options: any) => Promise; + getAllWindows: () => Promise; + getCurrentWindow: () => Promise; + createWebviewWindow: (label: string, options: any) => Promise; + listenWindowEvent: (event: string, callback: (event: any) => void) => Promise<() => void>; + isTauri: () => boolean; + openExternal: (url: string) => Promise; } -// Create Tauri adapter functions -export const createTauriAdapter = (): PlatformAdapter => { - return { - async invokeBackend(command: string, args?: any): Promise { - if (isTauri()) { - const { invoke } = await import("@tauri-apps/api/core"); - return invoke(command, args); - } - return null; - }, - - async setWindowSize(width: number, height: number): Promise { - if (isTauri()) { - const { getCurrentWebviewWindow } = await import( - "@tauri-apps/api/webviewWindow" - ); - const { LogicalSize } = await import("@tauri-apps/api/dpi"); - const window = await getCurrentWebviewWindow(); - if (window) { - await window.setSize(new LogicalSize(width, height)); - } - } - }, - - async hideWindow(): Promise { - if (isTauri()) { - const { invoke } = await import("@tauri-apps/api/core"); - await invoke("hide_coco"); - } - }, - - async showWindow(): Promise { - if (isTauri()) { - const { getCurrentWebviewWindow } = await import( - "@tauri-apps/api/webviewWindow" - ); - const window = await getCurrentWebviewWindow(); - if (window) { - await window.show(); - await window.unminimize(); - await window.setFocus(); - } - } - }, - - isPlatformTauri(): boolean { - return isTauri(); - }, - - convertFileSrc(path: string): string { - if (isTauri()) { - return tauriConvertFileSrc(path); - } - return path; - }, - - async emitEvent(event: string, payload?: any) { - if (isTauri()) { - const { emit } = await import("@tauri-apps/api/event"); - return emit(event, payload); - } - }, - - async listenEvent( - event: K, - callback: (event: { payload: EventPayloads[K] }) => void - ) { - if (isTauri()) { - const { listen } = await import("@tauri-apps/api/event"); - return listen(event, callback); - } - return () => {}; - }, - - async setAlwaysOnTop(isPinned: boolean) { - if (isTauri()) { - const { getCurrentWindow } = await import("@tauri-apps/api/window"); - const window = getCurrentWindow(); - return window.setAlwaysOnTop(isPinned); - } - }, - - async checkScreenRecordingPermission() { - if (isTauri()) { - const { checkScreenRecordingPermission } = await import( - "tauri-plugin-macos-permissions-api" - ); - return checkScreenRecordingPermission(); - } - return false; - }, - - async requestScreenRecordingPermission() { - if (isTauri()) { - const { requestScreenRecordingPermission } = await import( - "tauri-plugin-macos-permissions-api" - ); - return requestScreenRecordingPermission(); - } - }, - - async getScreenshotableMonitors() { - if (isTauri()) { - const { getScreenshotableMonitors } = await import( - "tauri-plugin-screenshots-api" - ); - return getScreenshotableMonitors(); - } - return []; - }, - - async getScreenshotableWindows() { - if (isTauri()) { - const { getScreenshotableWindows } = await import( - "tauri-plugin-screenshots-api" - ); - return getScreenshotableWindows(); - } - return []; - }, - - async captureMonitorScreenshot(id: number) { - if (isTauri()) { - const { getMonitorScreenshot } = await import( - "tauri-plugin-screenshots-api" - ); - return getMonitorScreenshot(id); - } - return ""; - }, - - async captureWindowScreenshot(id: number) { - if (isTauri()) { - const { getWindowScreenshot } = await import( - "tauri-plugin-screenshots-api" - ); - return getWindowScreenshot(id); - } - return ""; - }, - - async openFileDialog(options: OpenDialogOptions) { - if (isTauri()) { - const { open } = await import("@tauri-apps/plugin-dialog"); - return open(options); - } - return null; - }, - - async getFileMetadata(path: string) { - if (isTauri()) { - const { metadata } = await import("tauri-plugin-fs-pro-api"); - return metadata(path); - } - return null; - }, - - async getFileIcon(path: string, size: number) { - if (isTauri()) { - const { icon } = await import("tauri-plugin-fs-pro-api"); - return icon(path, size); - } - return ""; - }, - - async checkUpdate() { - if (isTauri()) { - const { check } = await import("@tauri-apps/plugin-updater"); - return check(); - } - return null; - }, - - async relaunchApp() { - if (isTauri()) { - const { relaunch } = await import("@tauri-apps/plugin-process"); - return relaunch(); - } - }, - - async listenThemeChanged(callback) { - if (isTauri()) { - const { listen } = await import("@tauri-apps/api/event"); - return listen("theme-changed", ({ payload }) => { - callback(payload); - }); - } - return () => {}; - }, - - async getWebviewWindow() { - if (isTauri()) { - const { getCurrentWebviewWindow } = await import( - "@tauri-apps/api/webviewWindow" - ); - return getCurrentWebviewWindow(); - } - return null; - }, - - async setWindowTheme(theme) { - const window = await this.getWebviewWindow(); - if (window) { - return window.setTheme(theme); - } - }, - - async getWindowTheme() { - const window = await this.getWebviewWindow(); - if (window) { - return window.theme(); - } - return "light"; - }, - - async onThemeChanged(callback) { - const window = await this.getWebviewWindow(); - if (window) { - window.onThemeChanged(callback); - } - }, - - async getWindowByLabel(label: string) { - if (isTauri()) { - const { WebviewWindow } = await import("@tauri-apps/api/webviewWindow"); - const window = await WebviewWindow.getByLabel(label); - return window; - } - return null; - }, - - async createWindow(label: string, options: any) { - if (isTauri()) { - const { WebviewWindow } = await import("@tauri-apps/api/webviewWindow"); - new WebviewWindow(label, options); - } - }, - }; -}; - -// Create Web adapter functions -export const createWebAdapter = (): PlatformAdapter => { - return { - async invokeBackend(command: string, args?: any): Promise { - console.log(`Web mode simulated backend call: ${command}`, args); - // Implement web environment simulation logic or API calls here - return null; - }, - - async setWindowSize(width: number, height: number): Promise { - console.log(`Web mode simulated window resize: ${width}x${height}`); - // No actual operation needed in web environment - }, - - async hideWindow(): Promise { - console.log("Web mode simulated window hide"); - // No actual operation needed in web environment - }, - - async showWindow(): Promise { - console.log("Web mode simulated window show"); - // No actual operation needed in web environment - }, - - isPlatformTauri(): boolean { - return false; - }, - - convertFileSrc(path: string): string { - return path; - }, - - async emitEvent(event: string, payload?: any): Promise { - console.log("Web mode simulated event emit", event, payload); - }, - - async listenEvent( - event: K, - _callback: (event: { payload: EventPayloads[K] }) => void - ): Promise<() => void> { - console.log("Web mode simulated event listen", event); - return () => {}; - }, - - async setAlwaysOnTop(isPinned: boolean): Promise { - console.log("Web mode simulated set always on top", isPinned); - }, - - async checkScreenRecordingPermission(): Promise { - console.log("Web mode simulated check screen recording permission"); - return false; - }, - - requestScreenRecordingPermission(): void { - console.log("Web mode simulated request screen recording permission"); - }, - - async getScreenshotableMonitors(): Promise { - console.log("Web mode simulated get screenshotable monitors"); - return []; - }, - - async getScreenshotableWindows(): Promise { - console.log("Web mode simulated get screenshotable windows"); - return []; - }, - - async captureMonitorScreenshot(id: number): Promise { - console.log("Web mode simulated capture monitor screenshot", id); - return ""; - }, - - async captureWindowScreenshot(id: number): Promise { - console.log("Web mode simulated capture window screenshot", id); - return ""; - }, - - async openFileDialog(options: OpenDialogOptions): Promise { - console.log("Web mode simulated open file dialog", options); - return null; - }, - - async getFileMetadata(path: string): Promise { - console.log("Web mode simulated get file metadata", path); - return null; - }, - - async getFileIcon(path: string, size: number): Promise { - console.log("Web mode simulated get file icon", path, size); - return ""; - }, - - async checkUpdate(): Promise { - console.log("Web mode simulated check update"); - return null; - }, - - async relaunchApp(): Promise { - console.log("Web mode simulated relaunch app"); - }, - - async listenThemeChanged() { - console.log("Web mode simulated theme change listener"); - return () => {}; - }, - - async getWebviewWindow() { - console.log("Web mode simulated get webview window"); - return null; - }, - - async setWindowTheme(theme) { - console.log("Web mode simulated set window theme:", theme); - }, - - async getWindowTheme() { - console.log("Web mode simulated get window theme"); - return "light"; - }, - - async onThemeChanged(callback) { - console.log("Web mode simulated on theme changed", callback); - }, - - async getWindowByLabel(label: string) { - console.log("Web mode simulated get window by label:", label); - return null; - }, - - async createWindow(label: string, options: any) { - console.log("Web mode simulated create window:", label, options); - }, - }; -}; - -// Create platform adapter based on environment -export const createPlatformAdapter = (): PlatformAdapter => { - try { - if (isTauri()) { - return createTauriAdapter(); - } else { - return createWebAdapter(); - } - } catch (e) { - return createWebAdapter(); - } -}; - // Default adapter instance -const platformAdapter = createPlatformAdapter(); +const platformAdapter: PlatformAdapter = typeof window !== 'undefined' ? createWebAdapter() : {} as PlatformAdapter; -export default platformAdapter; - -// Custom hook for using platform adapter -export const usePlatformAdapter = () => { - const [adapter] = useState(platformAdapter); - - return adapter; -}; +export default platformAdapter; \ No newline at end of file diff --git a/src/utils/tauriAdapter.ts b/src/utils/tauriAdapter.ts new file mode 100644 index 00000000..2997683b --- /dev/null +++ b/src/utils/tauriAdapter.ts @@ -0,0 +1,188 @@ +import type { PlatformAdapter, EventPayloads } from './platformAdapter'; + +import type { OpenDialogOptions } from '@tauri-apps/plugin-dialog'; + +// Create Tauri adapter functions +export const createTauriAdapter = (): PlatformAdapter => { + return { + async invokeBackend(command: string, args?: any): Promise { + const { invoke } = await import("@tauri-apps/api/core"); + return invoke(command, args); + }, + + async setWindowSize(width: number, height: number): Promise { + const { getCurrentWebviewWindow } = await import("@tauri-apps/api/webviewWindow"); + const { LogicalSize } = await import("@tauri-apps/api/dpi"); + const window = await getCurrentWebviewWindow(); + if (window) { + await window.setSize(new LogicalSize(width, height)); + } + }, + + async hideWindow(): Promise { + const { invoke } = await import("@tauri-apps/api/core"); + await invoke("hide_coco"); + }, + + async showWindow(): Promise { + const { getCurrentWebviewWindow } = await import("@tauri-apps/api/webviewWindow"); + const window = await getCurrentWebviewWindow(); + if (window) { + await window.show(); + } + }, + + convertFileSrc(path: string): string { + const { convertFileSrc } = require("@tauri-apps/api/core"); + return convertFileSrc(path); + }, + + async emitEvent(event: string, payload?: any) { + const { emit } = await import("@tauri-apps/api/event"); + return emit(event, payload); + }, + + async listenEvent( + event: K, + callback: (event: { payload: EventPayloads[K] }) => void + ) { + const { listen } = await import("@tauri-apps/api/event"); + return listen(event, callback); + }, + + async setAlwaysOnTop(isPinned: boolean) { + const { getCurrentWindow } = await import("@tauri-apps/api/window"); + const window = getCurrentWindow(); + return window.setAlwaysOnTop(isPinned); + }, + + async checkScreenRecordingPermission() { + const { checkScreenRecordingPermission } = await import("tauri-plugin-macos-permissions-api"); + return checkScreenRecordingPermission(); + }, + + async requestScreenRecordingPermission() { + const { requestScreenRecordingPermission } = await import("tauri-plugin-macos-permissions-api"); + return requestScreenRecordingPermission(); + }, + + async getScreenshotableMonitors() { + const { getScreenshotableMonitors } = await import("tauri-plugin-screenshots-api"); + return getScreenshotableMonitors(); + }, + + async getScreenshotableWindows() { + const { getScreenshotableWindows } = await import("tauri-plugin-screenshots-api"); + return getScreenshotableWindows(); + }, + + async captureMonitorScreenshot(id: number) { + const { getMonitorScreenshot } = await import("tauri-plugin-screenshots-api"); + return getMonitorScreenshot(id); + }, + + async captureWindowScreenshot(id: number) { + const { getWindowScreenshot } = await import("tauri-plugin-screenshots-api"); + return getWindowScreenshot(id); + }, + + async openFileDialog(options: OpenDialogOptions) { + const { open } = await import("@tauri-apps/plugin-dialog"); + return open(options); + }, + + async getFileMetadata(path: string) { + const { metadata } = await import("tauri-plugin-fs-pro-api"); + return metadata(path); + }, + + async getFileIcon(path: string, size: number) { + const { icon } = await import("tauri-plugin-fs-pro-api"); + return icon(path, size); + }, + + async checkUpdate() { + const { check } = await import("@tauri-apps/plugin-updater"); + return check(); + }, + + async relaunchApp() { + const { relaunch } = await import("@tauri-apps/plugin-process"); + return relaunch(); + }, + + async listenThemeChanged(callback) { + const { listen } = await import("@tauri-apps/api/event"); + return listen("theme-changed", ({ payload }) => { + callback(payload); + }); + }, + + async getWebviewWindow() { + const { getCurrentWebviewWindow } = await import("@tauri-apps/api/webviewWindow"); + return getCurrentWebviewWindow(); + }, + + async setWindowTheme(theme) { + const window = await this.getWebviewWindow(); + if (window) { + return window.setTheme(theme); + } + }, + + async getWindowTheme() { + const window = await this.getWebviewWindow(); + if (window) { + return window.theme(); + } + return 'light'; + }, + + async onThemeChanged(callback) { + const window = await this.getWebviewWindow(); + if (window) { + window.onThemeChanged(callback); + } + }, + + async getWindowByLabel(label: string) { + const { WebviewWindow } = await import("@tauri-apps/api/webviewWindow"); + const window = await WebviewWindow.getByLabel(label); + return window; + }, + + async createWindow(label: string, options: any) { + const { WebviewWindow } = await import("@tauri-apps/api/webviewWindow"); + new WebviewWindow(label, options); + }, + + async getAllWindows(): Promise { + const { getAllWindows } = await import("@tauri-apps/api/window"); + return getAllWindows(); + }, + + async getCurrentWindow(): Promise { + const { getCurrentWindow } = await import("@tauri-apps/api/window"); + return getCurrentWindow(); + }, + + async createWebviewWindow(label: string, options: any): Promise { + const { WebviewWindow } = await import("@tauri-apps/api/webviewWindow"); + return new WebviewWindow(label, options); + }, + + async listenWindowEvent(event: string, callback: (event: any) => void): Promise<() => void> { + const { listen } = await import("@tauri-apps/api/event"); + return listen(event, callback); + }, + + isTauri(): boolean { + return true; + }, + + async openExternal(url: string): Promise { + const { open } = await import("@tauri-apps/plugin-shell"); + return open(url); + }, + }; +}; \ No newline at end of file diff --git a/src/utils/webAdapter.ts b/src/utils/webAdapter.ts new file mode 100644 index 00000000..72a7b164 --- /dev/null +++ b/src/utils/webAdapter.ts @@ -0,0 +1,161 @@ +import type { PlatformAdapter, EventPayloads } from './platformAdapter'; + +// Create Web adapter functions +export const createWebAdapter = (): PlatformAdapter => { + return { + async invokeBackend(command: string, args?: any): Promise { + console.log(`Web mode simulated backend call: ${command}`, args); + // Implement web environment simulation logic or API calls here + return null; + }, + + async setWindowSize(width: number, height: number): Promise { + console.log(`Web mode simulated window resize: ${width}x${height}`); + // No actual operation needed in web environment + }, + + async hideWindow(): Promise { + console.log("Web mode simulated window hide"); + // No actual operation needed in web environment + }, + + async showWindow(): Promise { + console.log("Web mode simulated window show"); + // No actual operation needed in web environment + }, + + convertFileSrc(path: string): string { + return path; + }, + + async emitEvent(event: string, payload?: any): Promise { + console.log("Web mode simulated event emit", event, payload); + }, + + async listenEvent( + event: K, + _callback: (event: { payload: EventPayloads[K] }) => void + ): Promise<() => void> { + console.log("Web mode simulated event listen", event); + return () => { }; + }, + + async setAlwaysOnTop(isPinned: boolean): Promise { + console.log("Web mode simulated set always on top", isPinned); + }, + + async checkScreenRecordingPermission(): Promise { + console.log("Web mode simulated check screen recording permission"); + return false; + }, + + requestScreenRecordingPermission(): void { + console.log("Web mode simulated request screen recording permission"); + }, + + async getScreenshotableMonitors(): Promise { + console.log("Web mode simulated get screenshotable monitors"); + return []; + }, + + async getScreenshotableWindows(): Promise { + console.log("Web mode simulated get screenshotable windows"); + return []; + }, + + async captureMonitorScreenshot(id: number): Promise { + console.log("Web mode simulated capture monitor screenshot", id); + return ""; + }, + + async captureWindowScreenshot(id: number): Promise { + console.log("Web mode simulated capture window screenshot", id); + return ""; + }, + + async openFileDialog(options: { multiple: boolean }): Promise { + console.log("Web mode simulated open file dialog", options); + return null; + }, + + async getFileMetadata(path: string): Promise { + console.log("Web mode simulated get file metadata", path); + return null; + }, + + async getFileIcon(path: string, size: number): Promise { + console.log("Web mode simulated get file icon", path, size); + return ""; + }, + + async checkUpdate(): Promise { + console.log("Web mode simulated check update"); + return null; + }, + + async relaunchApp(): Promise { + console.log("Web mode simulated relaunch app"); + }, + + async listenThemeChanged() { + console.log("Web mode simulated theme change listener"); + return () => { }; + }, + + async getWebviewWindow() { + console.log("Web mode simulated get webview window"); + return null; + }, + + async setWindowTheme(theme) { + console.log("Web mode simulated set window theme:", theme); + }, + + async getWindowTheme() { + console.log("Web mode simulated get window theme"); + return 'light'; + }, + + async onThemeChanged(callback) { + console.log("Web mode simulated on theme changed", callback); + }, + + async getWindowByLabel(label: string) { + console.log("Web mode simulated get window by label:", label); + return null; + }, + + async createWindow(label: string, options: any) { + console.log("Web mode simulated create window:", label, options); + }, + + async getAllWindows(): Promise { + console.log("Web mode simulated get all windows"); + return []; + }, + + async getCurrentWindow(): Promise { + console.log("Web mode simulated get current window"); + return null; + }, + + async createWebviewWindow(label: string, options: any): Promise { + console.log("Web mode simulated create webview window:", label, options); + return null; + }, + + async listenWindowEvent(event: string, _callback: (event: any) => void): Promise<() => void> { + console.log("Web mode simulated listen window event:", event); + return () => {}; + }, + + isTauri(): boolean { + return false; + }, + + async openExternal(url: string): Promise { + console.log(`Web mode opening URL: ${url}`); + window.open(url, '_blank'); + }, + }; +}; \ No newline at end of file diff --git a/src/web.css b/src/web.css new file mode 100644 index 00000000..47fd8e9b --- /dev/null +++ b/src/web.css @@ -0,0 +1,729 @@ +/* @tailwind base; */ +@tailwind components; +@tailwind utilities; + +/* Base variables */ +:host, +:root, +.searchbox-container, +.coco-container { + --spacing-base: 12px; + --modal-width: 560px; + --modal-height: 600px; + --searchbox-height: 56px; + --hit-height: 56px; + --footer-height: 44px; + --icon-stroke-width: 1.4; + --background: #ffffff; + --foreground: #09090b; + --border: #e3e3e7; + --coco-primary-color: rgb(149, 5, 153); +} + +/* Light theme */ +.light.coco-container{ + --coco-primary-color: rgb(149, 5, 153); + --coco-text-color: rgb(28, 30, 33); + --coco-muted-color: rgb(150, 159, 175); + --coco-modal-container-background: rgba(101, 108, 133, 0.8); + --coco-modal-background: rgb(245, 246, 247); + --coco-modal-shadow: inset 1px 1px 0 0 rgba(255, 255, 255, 0.5), + 0 3px 8px 0 rgba(85, 90, 100, 1); + --coco-searchbox-background: rgb(235, 237, 240); + --coco-searchbox-focus-background: #fff; + --coco-hit-color: rgb(68, 73, 80); + --coco-hit-active-color: #fff; + --coco-hit-background: #fff; + --coco-hit-shadow: 0 1px 3px 0 rgb(212, 217, 225); + --coco-key-gradient: linear-gradient( + -225deg, + rgb(213, 219, 228) 0%, + rgb(248, 248, 248) 100% + ); + --coco-key-shadow: inset 0 -2px 0 0 rgb(205, 205, 230), inset 0 0 1px 1px #fff, + 0 1px 2px 1px rgba(30, 35, 90, 0.4); + --coco-footer-background: #fff; + --coco-footer-shadow: 0 -1px 0 0 rgb(224, 227, 232), + 0 -3px 6px 0 rgba(69, 98, 155, 0.12); + --coco-icon-color: rgb(21, 21, 21); +} + +/* Dark theme */ +.dark.coco-container { + --coco-primary-color: rgb(149, 5, 153); + --background: #09090b; + --foreground: #f9f9f9; + --border: #27272a; + --coco-text-color: rgb(245, 246, 247); + --coco-modal-container-background: rgba(9, 10, 17, 0.8); + --coco-modal-background: rgb(21, 23, 42); + --coco-modal-shadow: inset 1px 1px 0 0 rgb(44, 46, 64), + 0 3px 8px 0 rgb(0, 3, 9); + --coco-searchbox-background: rgb(9, 10, 17); + --coco-searchbox-focus-background: #000; + --coco-hit-color: rgb(190, 195, 201); + --coco-hit-shadow: none; + --coco-hit-background: rgb(9, 10, 17); + --coco-key-gradient: linear-gradient( + -26.5deg, + rgb(86, 88, 114) 0%, + rgb(49, 53, 91) 100% + ); + --coco-key-shadow: inset 0 -2px 0 0 rgb(40, 45, 85), + inset 0 0 1px 1px rgb(81, 87, 125), 0 2px 2px 0 rgba(3, 4, 9, 0.3); + --coco-footer-background: rgb(30, 33, 54); + --coco-footer-shadow: inset 0 1px 0 0 rgba(73, 76, 106, 0.5), + 0 -4px 8px 0 rgba(0, 0, 0, 0.2); + --coco-muted-color: rgb(127, 132, 151); + --coco-icon-color: rgb(255, 255, 255); +} + +@media (prefers-color-scheme: dark) { + .dark.coco-container { + --coco-primary-color: rgb(149, 5, 153); + --background: #09090b; + --foreground: #f9f9f9; + --border: #27272a; + --coco-text-color: rgb(245, 246, 247); + --coco-modal-container-background: rgba(9, 10, 17, 0.8); + --coco-modal-background: rgb(21, 23, 42); + --coco-modal-shadow: inset 1px 1px 0 0 rgb(44, 46, 64), + 0 3px 8px 0 rgb(0, 3, 9); + --coco-searchbox-background: rgb(9, 10, 17); + --coco-searchbox-focus-background: #000; + --coco-hit-color: rgb(190, 195, 201); + --coco-hit-shadow: none; + --coco-hit-background: rgb(9, 10, 17); + --coco-key-gradient: linear-gradient( + -26.5deg, + rgb(86, 88, 114) 0%, + rgb(49, 53, 91) 100% + ); + --coco-key-shadow: inset 0 -2px 0 0 rgb(40, 45, 85), + inset 0 0 1px 1px rgb(81, 87, 125), 0 2px 2px 0 rgba(3, 4, 9, 0.3); + --coco-footer-background: rgb(30, 33, 54); + --coco-footer-shadow: inset 0 1px 0 0 rgba(73, 76, 106, 0.5), + 0 -4px 8px 0 rgba(0, 0, 0, 0.2); + --coco-muted-color: rgb(127, 132, 151); + --coco-icon-color: rgb(255, 255, 255); + } +} + +/* html, +:host { + line-height: 1.5; + -webkit-text-size-adjust: 100%; + -moz-tab-size: 4; + -o-tab-size: 4; + tab-size: 4; + font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", + "Segoe UI Emoji", Segoe UI Symbol, "Noto Color Emoji"; + font-feature-settings: normal; + font-variation-settings: normal; + -webkit-tap-highlight-color: transparent; +} */ + +/* html { + height: 100%; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + overscroll-behavior: none; +} + +body, +#root { + height: 100%; + --tw-text-opacity: 1; + color: rgb(17 24 39 / var(--tw-text-opacity, 1)); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} */ + +/* body { + margin: 0; + line-height: inherit; +} */ + +.dark body, +.dark #root, +.dark.coco-container, +.dark.coco-container { + --tw-text-opacity: 1; + color: rgb(243 244 246 / var(--tw-text-opacity, 1)); +} + +.input-body { + overflow: hidden; + border-radius: 0.75rem; +} + +.\!container { + width: 100% !important; +} +.container { + width: 100%; +} +@media (min-width: 640px) { + .\!container { + max-width: 640px !important; + } + .container { + max-width: 640px; + } +} +@media (min-width: 768px) { + .\!container { + max-width: 768px !important; + } + .container { + max-width: 768px; + } +} +@media (min-width: 1024px) { + .\!container { + max-width: 1024px !important; + } + .container { + max-width: 1024px; + } +} +@media (min-width: 1280px) { + .\!container { + max-width: 1280px !important; + } + .container { + max-width: 1280px; + } +} +@media (min-width: 1536px) { + .\!container { + max-width: 1536px !important; + } + .container { + max-width: 1536px; + } +} + +.coco-container { + line-height: 1.5; + -webkit-text-size-adjust: 100%; + -moz-tab-size: 4; + -o-tab-size: 4; + tab-size: 4; + font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", + "Segoe UI Emoji", Segoe UI Symbol, "Noto Color Emoji"; + font-feature-settings: normal; + font-variation-settings: normal; + -webkit-tap-highlight-color: transparent; + + *, + :before, + :after { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-gradient-from-position: ; + --tw-gradient-via-position: ; + --tw-gradient-to-position: ; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; + --tw-contain-size: ; + --tw-contain-layout: ; + --tw-contain-paint: ; + --tw-contain-style: ; + } + ::backdrop { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-gradient-from-position: ; + --tw-gradient-via-position: ; + --tw-gradient-to-position: ; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; + --tw-contain-size: ; + --tw-contain-layout: ; + --tw-contain-paint: ; + --tw-contain-style: ; + } + *, + :before, + :after { + box-sizing: border-box; + border-width: 0; + border-style: solid; + border-color: #e5e7eb; + } + :before, + :after { + --tw-content: ""; + } + + hr { + height: 0; + color: inherit; + border-top-width: 1px; + } + abbr:where([title]) { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; + } + h1, + h2, + h3, + h4, + h5, + h6 { + font-size: inherit; + font-weight: inherit; + } + a { + color: inherit; + text-decoration: inherit; + } + b, + strong { + font-weight: bolder; + } + code, + kbd, + samp, + pre { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, + Liberation Mono, Courier New, monospace; + font-feature-settings: normal; + font-variation-settings: normal; + font-size: 1em; + } + small { + font-size: 80%; + } + sub, + sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; + } + sub { + bottom: -0.25em; + } + sup { + top: -0.5em; + } + table { + text-indent: 0; + border-color: inherit; + border-collapse: collapse; + } + button, + input, + optgroup, + select, + textarea { + font-family: inherit; + font-feature-settings: inherit; + font-variation-settings: inherit; + font-size: 100%; + font-weight: inherit; + line-height: inherit; + letter-spacing: inherit; + color: inherit; + margin: 0; + padding: 0; + } + button, + select { + text-transform: none; + } + button, + input:where([type="button"]), + input:where([type="reset"]), + input:where([type="submit"]) { + -webkit-appearance: button; + background-color: transparent; + background-image: none; + } + :-moz-focusring { + outline: auto; + } + :-moz-ui-invalid { + box-shadow: none; + } + progress { + vertical-align: baseline; + } + ::-webkit-inner-spin-button, + ::-webkit-outer-spin-button { + height: auto; + } + [type="search"] { + -webkit-appearance: textfield; + outline-offset: -2px; + } + ::-webkit-search-decoration { + -webkit-appearance: none; + } + ::-webkit-file-upload-button { + -webkit-appearance: button; + font: inherit; + } + summary { + display: list-item; + } + blockquote, + dl, + dd, + h1, + h2, + h3, + h4, + h5, + h6, + hr, + figure, + p, + pre { + margin: 0; + } + fieldset { + margin: 0; + padding: 0; + } + legend { + padding: 0; + } + ol, + ul, + menu { + list-style: none; + margin: 0; + padding: 0; + } + dialog { + padding: 0; + } + textarea { + resize: vertical; + } + input::-moz-placeholder, + textarea::-moz-placeholder { + opacity: 1; + color: #9ca3af; + } + input::placeholder, + textarea::placeholder { + opacity: 1; + color: #9ca3af; + } + button, + [role="button"] { + cursor: pointer; + } + :disabled { + cursor: default; + } + img, + svg, + video, + canvas, + audio, + iframe, + embed, + object { + display: block; + vertical-align: middle; + } + img, + video { + max-width: 100%; + height: auto; + } + [hidden]:where(:not([hidden="until-found"])) { + display: none; + } + * { + box-sizing: border-box; + border-color: var(--border); + } + .settings-input { + display: block; + width: 100%; + border-radius: 0.375rem; + --tw-border-opacity: 1; + border-color: rgb(209 213 219 / var(--tw-border-opacity, 1)); + --tw-bg-opacity: 1; + background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1)); + --tw-text-opacity: 1; + color: rgb(17 24 39 / var(--tw-text-opacity, 1)); + --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); + --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), + var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); + transition-property: color, background-color, border-color, + text-decoration-color, fill, stroke; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 0.2s; + } + .settings-input:focus { + --tw-border-opacity: 1; + border-color: rgb(59 130 246 / var(--tw-border-opacity, 1)); + --tw-ring-opacity: 1; + --tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity, 1)); + } + .settings-input:is([data-theme="dark"] *) { + --tw-border-opacity: 1; + border-color: rgb(75 85 99 / var(--tw-border-opacity, 1)); + --tw-bg-opacity: 1; + background-color: rgb(55 65 81 / var(--tw-bg-opacity, 1)); + --tw-text-opacity: 1; + color: rgb(243 244 246 / var(--tw-text-opacity, 1)); + } + .settings-select { + border-radius: 0.375rem; + --tw-border-opacity: 1; + border-color: rgb(209 213 219 / var(--tw-border-opacity, 1)); + --tw-bg-opacity: 1; + background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1)); + font-size: 0.875rem; + line-height: 1.25rem; + --tw-text-opacity: 1; + color: rgb(17 24 39 / var(--tw-text-opacity, 1)); + --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); + --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), + var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); + transition-property: color, background-color, border-color, + text-decoration-color, fill, stroke; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 0.2s; + } + .settings-select:focus { + --tw-border-opacity: 1; + border-color: rgb(59 130 246 / var(--tw-border-opacity, 1)); + --tw-ring-opacity: 1; + --tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity, 1)); + } + .settings-select:is([data-theme="dark"] *) { + --tw-border-opacity: 1; + border-color: rgb(75 85 99 / var(--tw-border-opacity, 1)); + --tw-bg-opacity: 1; + background-color: rgb(55 65 81 / var(--tw-bg-opacity, 1)); + --tw-text-opacity: 1; + color: rgb(243 244 246 / var(--tw-text-opacity, 1)); + } +} + +/* Component styles */ +@layer components { + .settings-input { + @apply block w-full rounded-md border-gray-300 dark:border-gray-600 + bg-white dark:bg-gray-700 + text-gray-900 dark:text-gray-100 + shadow-sm focus:border-blue-500 focus:ring-blue-500 + transition-colors duration-200; + } + + .settings-select { + @apply text-sm rounded-md border-gray-300 dark:border-gray-600 + bg-white dark:bg-gray-700 + text-gray-900 dark:text-gray-100 + shadow-sm focus:border-blue-500 focus:ring-blue-500 + transition-colors duration-200; + } +} + +/* Utility styles */ +@layer utilities { + /* Scrollbar styles */ + .custom-scrollbar { + scrollbar-width: thin; + scrollbar-color: #cbd5e1 transparent; + } + + .custom-scrollbar::-webkit-scrollbar { + width: 6px; + } + + .custom-scrollbar::-webkit-scrollbar-track { + background: transparent; + } + + .custom-scrollbar::-webkit-scrollbar-thumb { + background-color: #cbd5e1; + border-radius: 3px; + } + + .dark .custom-scrollbar { + scrollbar-color: #475569 transparent; + } + + .dark .custom-scrollbar::-webkit-scrollbar-thumb { + background-color: #475569; + } + + /* Background styles */ + .bg-100 { + background-size: 100% 100%; + } + + /* Error page styles */ + #error-page { + display: flex; + justify-content: center; + align-items: center; + height: 100%; + background: linear-gradient(to right, #f79c42, #f2d600); + font-family: "Arial", sans-serif; + color: #fff; + text-align: center; + padding: 0 20px; + } + + .error-content { + background-color: rgba(0, 0, 0, 0.6); + padding: 40px; + border-radius: 8px; + box-shadow: 0px 10px 20px rgba(0, 0, 0, 0.2); + max-width: 500px; + width: 100%; + margin: 0 20px; + } + + .error-title { + font-size: 60px; + font-weight: bold; + margin-bottom: 20px; + color: #f2d600; + } + + .error-message { + font-size: 18px; + margin-bottom: 20px; + font-weight: 300; + color: #fff; + } + + .error-details { + font-size: 16px; + color: #ffcc00; + font-style: italic; + } + + .error-content button { + background-color: #f2d600; + border: none; + padding: 10px 20px; + font-size: 16px; + color: #333; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.3s ease; + } + + .error-content button:hover { + background-color: #f79c42; + } + + /* coco styles */ + .coco-modal-footer-commands-key { + display: flex; + align-items: center; + justify-content: center; + border-radius: 4px; + border: 0; + padding: 2px; + background: var(--coco-key-gradient); + box-shadow: var(--coco-key-shadow); + color: var(--coco-muted-color); + } + + /* User selection styles */ + .user-select { + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + } +} diff --git a/tailwind.config.js b/tailwind.config.js index dd095c49..c36862e1 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,6 +1,7 @@ /** @type {import('tailwindcss').Config} */ export default { - content: ["./index.html", "./src/**/*.{html,js,jsx,ts,tsx}"], + content: ["./index.html", "./src/**/*.{html,js,jsx,ts,tsx}", "./src/**/*.css"], + important: '.coco-container', theme: { extend: { backgroundColor: { diff --git a/tsup.config.ts b/tsup.config.ts index 84952b5f..ce029c7b 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -3,18 +3,22 @@ import { writeFileSync, readFileSync } from 'fs'; import { join, resolve } from 'path'; export default defineConfig({ - entry: ['src/pages/web/SearchChat.tsx'], - format: ['esm', 'cjs'], + entry: ['src/pages/web/index.tsx'], + format: ['esm'], dts: true, - splitting: false, - sourcemap: true, + splitting: true, + sourcemap: false, clean: true, + treeshake: true, + minify: true, + env: { + BUILD_TARGET: 'web', + NODE_ENV: 'production', + }, external: [ 'react', 'react-dom', ], - treeshake: true, - minify: true, esbuildOptions(options) { options.bundle = true; options.platform = 'browser'; @@ -28,6 +32,22 @@ export default defineConfig({ options.alias = { '@': resolve(__dirname, './src') } + options.external = [ + '@tauri-apps/api', + '@tauri-apps/plugin-*', + 'tauri-plugin-*', + ]; + options.treeShaking = true; + options.define = { + 'process.env.BUILD_TARGET': '"web"', + 'process.env.NODE_ENV': '"production"', + 'process.env.DEBUG': 'false', + 'process.env.IS_DEV': 'false', + }; + options.pure = ['console.log']; + options.target = 'es2020'; + options.legalComments = 'none'; + options.ignoreAnnotations = false; }, esbuildPlugins: [ { @@ -38,36 +58,65 @@ export default defineConfig({ }, }, ], - outDir: 'dist/search-chat', + outDir: 'out/search-chat', + async onSuccess() { const projectPackageJson = JSON.parse( readFileSync(join(__dirname, 'package.json'), 'utf-8') ); const packageJson = { - name: "search-chat", - version: "1.0.0", - main: "SearchChat.cjs", - module: "SearchChat.js", - types: "SearchChat.d.ts", + name: "@infinilabs/search-chat", + version: "1.0.10", + main: "index.js", + module: "index.js", + type: "module", + types: "index.d.ts", dependencies: projectPackageJson.dependencies, peerDependencies: { "react": "^18.0.0", "react-dom": "^18.0.0" - } + }, + "sideEffects": [ + "*.css", + "*.scss" + ], + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + } }; + + const noNeedDeps = [ + "@wavesurfer/react", + "dotenv", + "uuid", + "wavesurfer.js", + ] - const tauriDeps = Object.keys(packageJson.dependencies).filter(dep => - dep.includes('@tauri-apps') || - dep.includes('tauri-plugin') + const tauriDeps = Object.keys(packageJson.dependencies).filter(dep => + dep.includes('@tauri-apps') || + dep.includes('tauri-plugin') || + noNeedDeps.includes(dep) ); tauriDeps.forEach(dep => { delete packageJson.dependencies[dep]; }); writeFileSync( - join(__dirname, 'dist/search-chat/package.json'), + join(__dirname, 'out/search-chat/package.json'), JSON.stringify(packageJson, null, 2) ); + + try { + const readmePath = join(__dirname, 'src/pages/web/README.md'); + const readmeContent = readFileSync(readmePath, 'utf-8'); + writeFileSync( + join(__dirname, 'out/search-chat/README.md'), + readmeContent + ); + } catch (error) { + console.error('Failed to copy README.md:', error); + } } }); \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts index 58c617c6..deca7a97 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -56,6 +56,11 @@ export default defineConfig(async () => ({ changeOrigin: true, secure: false, }, + "/integration": { + target: process.env.COCO_SERVER_URL, + changeOrigin: true, + secure: false, + }, }, }, build: {