refactor: web components (#331)

* refactor: web components

* chore: web component

* chore: web

* chore: web

* docs: update notes
This commit is contained in:
BiggerRain
2025-04-07 11:19:09 +08:00
committed by GitHub
parent 7225635f08
commit e15baef8f9
52 changed files with 3117 additions and 1223 deletions

View File

@@ -57,6 +57,7 @@
"uuidv", "uuidv",
"VITE", "VITE",
"walkdir", "walkdir",
"wavesurfer",
"webviews", "webviews",
"xzvf", "xzvf",
"yuque", "yuque",

View File

@@ -19,6 +19,8 @@ Information about release notes of Coco Server is provided here.
### Improvements ### Improvements
- refactor: web components #331
## 0.3.0 (2025-03-31) ## 0.3.0 (2025-03-31)
### Breaking changes ### Breaking changes

View File

@@ -6,8 +6,8 @@
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "tsc && vite build", "build": "tsc && vite build",
"build:web": "tsc && tsup", "build:web": "cross-env BUILD_TARGET=web tsc && cross-env BUILD_TARGET=web tsup --format esm",
"publish:web": "cd dist/search-chat && npm publish", "publish:web": "cd out/search-chat && npm publish",
"publish:web:beta": "cd dist/search-chat && npm publish --tag beta", "publish:web:beta": "cd dist/search-chat && npm publish --tag beta",
"publish:web:alpha": "cd dist/search-chat && npm publish --tag alpha", "publish:web:alpha": "cd dist/search-chat && npm publish --tag alpha",
"publish:web:rc": "cd dist/search-chat && npm publish --tag rc", "publish:web:rc": "cd dist/search-chat && npm publish --tag rc",
@@ -34,6 +34,7 @@
"@tauri-apps/plugin-window": "2.0.0-alpha.1", "@tauri-apps/plugin-window": "2.0.0-alpha.1",
"@wavesurfer/react": "^1.0.9", "@wavesurfer/react": "^1.0.9",
"ahooks": "^3.8.4", "ahooks": "^3.8.4",
"axios": "^1.8.4",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
@@ -76,6 +77,7 @@
"@types/react-window": "^1.8.8", "@types/react-window": "^1.8.8",
"@vitejs/plugin-react": "^4.3.4", "@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.4.21", "autoprefixer": "^10.4.21",
"cross-env": "^7.0.3",
"immer": "^10.1.1", "immer": "^10.1.1",
"postcss": "^8.5.3", "postcss": "^8.5.3",
"release-it": "^18.1.2", "release-it": "^18.1.2",

169
pnpm-lock.yaml generated
View File

@@ -56,6 +56,9 @@ importers:
ahooks: ahooks:
specifier: ^3.8.4 specifier: ^3.8.4
version: 3.8.4(react@18.3.1) version: 3.8.4(react@18.3.1)
axios:
specifier: ^1.8.4
version: 1.8.4
clsx: clsx:
specifier: ^2.1.1 specifier: ^2.1.1
version: 2.1.1 version: 2.1.1
@@ -177,6 +180,9 @@ importers:
autoprefixer: autoprefixer:
specifier: ^10.4.21 specifier: ^10.4.21
version: 10.4.21(postcss@8.5.3) version: 10.4.21(postcss@8.5.3)
cross-env:
specifier: ^7.0.3
version: 7.0.3
immer: immer:
specifier: ^10.1.1 specifier: ^10.1.1
version: 10.1.1 version: 10.1.1
@@ -1418,6 +1424,9 @@ packages:
async-retry@1.3.3: async-retry@1.3.3:
resolution: {integrity: sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==} resolution: {integrity: sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==}
asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
atomically@2.0.3: atomically@2.0.3:
resolution: {integrity: sha512-kU6FmrwZ3Lx7/7y3hPS5QnbJfaohcIul5fGqf7ok+4KklIEk9tJ0C2IQPdacSbVUWv6zVHXEBWoWd6NrVMT7Cw==} resolution: {integrity: sha512-kU6FmrwZ3Lx7/7y3hPS5QnbJfaohcIul5fGqf7ok+4KklIEk9tJ0C2IQPdacSbVUWv6zVHXEBWoWd6NrVMT7Cw==}
@@ -1428,6 +1437,9 @@ packages:
peerDependencies: peerDependencies:
postcss: ^8.1.0 postcss: ^8.1.0
axios@1.8.4:
resolution: {integrity: sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==}
bail@2.0.2: bail@2.0.2:
resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==}
@@ -1478,6 +1490,10 @@ packages:
resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
engines: {node: '>=8'} 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: callsites@3.1.0:
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
engines: {node: '>=6'} engines: {node: '>=6'}
@@ -1565,6 +1581,10 @@ packages:
color-name@1.1.4: color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 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: comma-separated-tokens@2.0.3:
resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
@@ -1618,6 +1638,11 @@ packages:
typescript: typescript:
optional: true 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: cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
@@ -1828,6 +1853,10 @@ packages:
delaunator@5.0.1: delaunator@5.0.1:
resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==} resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==}
delayed-stream@1.0.0:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'}
dequal@2.0.3: dequal@2.0.3:
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
engines: {node: '>=6'} engines: {node: '>=6'}
@@ -1852,6 +1881,10 @@ packages:
resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==}
engines: {node: '>=12'} engines: {node: '>=12'}
dunder-proto@1.0.1:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
eastasianwidth@0.2.0: eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
@@ -1878,6 +1911,22 @@ packages:
error-ex@1.3.2: error-ex@1.3.2:
resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} 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: esbuild@0.21.5:
resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==}
engines: {node: '>=12'} engines: {node: '>=12'}
@@ -1969,10 +2018,23 @@ packages:
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
engines: {node: '>=8'} 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: foreground-child@3.3.1:
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
engines: {node: '>=14'} engines: {node: '>=14'}
form-data@4.0.2:
resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==}
engines: {node: '>= 6'}
fraction.js@4.3.7: fraction.js@4.3.7:
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
@@ -1995,6 +2057,14 @@ packages:
resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==} resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==}
engines: {node: '>=18'} 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: get-stream@8.0.1:
resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==}
engines: {node: '>=16'} engines: {node: '>=16'}
@@ -2048,6 +2118,10 @@ packages:
resolution: {integrity: sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==} resolution: {integrity: sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==}
engines: {node: '>=18'} engines: {node: '>=18'}
gopd@1.2.0:
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
engines: {node: '>= 0.4'}
graceful-fs@4.2.10: graceful-fs@4.2.10:
resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==} resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==}
@@ -2057,6 +2131,14 @@ packages:
hachure-fill@0.5.2: hachure-fill@0.5.2:
resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==} 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: hasown@2.0.2:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -2433,6 +2515,10 @@ packages:
engines: {node: '>= 18'} engines: {node: '>= 18'}
hasBin: true 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: mdast-util-find-and-replace@3.0.2:
resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==}
@@ -4653,6 +4739,8 @@ snapshots:
dependencies: dependencies:
retry: 0.13.1 retry: 0.13.1
asynckit@0.4.0: {}
atomically@2.0.3: atomically@2.0.3:
dependencies: dependencies:
stubborn-fs: 1.2.5 stubborn-fs: 1.2.5
@@ -4668,6 +4756,14 @@ snapshots:
postcss: 8.5.3 postcss: 8.5.3
postcss-value-parser: 4.2.0 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: {} bail@2.0.2: {}
balanced-match@1.0.2: {} balanced-match@1.0.2: {}
@@ -4720,6 +4816,11 @@ snapshots:
cac@6.7.14: {} 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: {} callsites@3.1.0: {}
camelcase-css@2.0.1: {} camelcase-css@2.0.1: {}
@@ -4794,6 +4895,10 @@ snapshots:
color-name@1.1.4: {} color-name@1.1.4: {}
combined-stream@1.0.8:
dependencies:
delayed-stream: 1.0.0
comma-separated-tokens@2.0.3: {} comma-separated-tokens@2.0.3: {}
commander@4.1.1: {} commander@4.1.1: {}
@@ -4841,6 +4946,10 @@ snapshots:
optionalDependencies: optionalDependencies:
typescript: 5.8.2 typescript: 5.8.2
cross-env@7.0.3:
dependencies:
cross-spawn: 7.0.6
cross-spawn@7.0.6: cross-spawn@7.0.6:
dependencies: dependencies:
path-key: 3.1.1 path-key: 3.1.1
@@ -5068,6 +5177,8 @@ snapshots:
dependencies: dependencies:
robust-predicates: 3.0.2 robust-predicates: 3.0.2
delayed-stream@1.0.0: {}
dequal@2.0.3: {} dequal@2.0.3: {}
devlop@1.1.0: devlop@1.1.0:
@@ -5088,6 +5199,12 @@ snapshots:
dotenv@16.4.7: {} 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: {} eastasianwidth@0.2.0: {}
electron-to-chromium@1.5.123: {} electron-to-chromium@1.5.123: {}
@@ -5106,6 +5223,21 @@ snapshots:
dependencies: dependencies:
is-arrayish: 0.2.1 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: esbuild@0.21.5:
optionalDependencies: optionalDependencies:
'@esbuild/aix-ppc64': 0.21.5 '@esbuild/aix-ppc64': 0.21.5
@@ -5247,11 +5379,20 @@ snapshots:
dependencies: dependencies:
to-regex-range: 5.0.1 to-regex-range: 5.0.1
follow-redirects@1.15.9: {}
foreground-child@3.3.1: foreground-child@3.3.1:
dependencies: dependencies:
cross-spawn: 7.0.6 cross-spawn: 7.0.6
signal-exit: 4.1.0 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: {} fraction.js@4.3.7: {}
fs.realpath@1.0.0: {} fs.realpath@1.0.0: {}
@@ -5265,6 +5406,24 @@ snapshots:
get-east-asian-width@1.3.0: {} 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@8.0.1: {}
get-stream@9.0.1: get-stream@9.0.1:
@@ -5336,12 +5495,20 @@ snapshots:
slash: 5.1.0 slash: 5.1.0
unicorn-magic: 0.1.0 unicorn-magic: 0.1.0
gopd@1.2.0: {}
graceful-fs@4.2.10: {} graceful-fs@4.2.10: {}
graceful-fs@4.2.11: {} graceful-fs@4.2.11: {}
hachure-fill@0.5.2: {} 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: hasown@2.0.2:
dependencies: dependencies:
function-bind: 1.1.2 function-bind: 1.1.2
@@ -5707,6 +5874,8 @@ snapshots:
marked@15.0.7: {} marked@15.0.7: {}
math-intrinsics@1.1.0: {}
mdast-util-find-and-replace@3.0.2: mdast-util-find-and-replace@3.0.2:
dependencies: dependencies:
'@types/mdast': 4.0.4 '@types/mdast': 4.0.4

93
src/api/axiosRequest.ts Normal file
View File

@@ -0,0 +1,93 @@
import axios from "axios";
import {
handleChangeRequestHeader,
handleConfigureAuth,
// handleAuthError,
// handleGeneralError,
handleNetworkError,
} from "./tools";
type Fn = (data: FcResponse<any>) => unknown;
interface IAnyObj {
[index: string]: unknown;
}
interface FcResponse<T> {
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 = <T>(
url: string,
params: IAnyObj = {},
clearFn?: Fn
): Promise<[any, FcResponse<T> | 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<T>;
if (clearFn !== undefined) {
res = clearFn(result?.data) as unknown as FcResponse<T>;
} else {
res = result?.data as FcResponse<T>;
}
resolve([null, res as FcResponse<T>]);
})
.catch((err) => {
resolve([err, undefined]);
});
});
export const Post = <T>(
url: string,
data: IAnyObj,
params: IAnyObj = {},
headers: IAnyObj = {}
): Promise<[any, FcResponse<T> | 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<T>]);
})
.catch((err) => {
resolve([err, undefined]);
});
});
};

73
src/api/tools.ts Normal file
View File

@@ -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;
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -21,6 +21,7 @@ import { ChatHeader } from "./ChatHeader";
import { ChatContent } from "./ChatContent"; import { ChatContent } from "./ChatContent";
import ConnectPrompt from "./ConnectPrompt"; import ConnectPrompt from "./ConnectPrompt";
import type { Chat } from "./types"; import type { Chat } from "./types";
import PrevSuggestion from "@/components/ChatMessage/PrevSuggestion";
interface ChatAIProps { interface ChatAIProps {
isTransitioned: boolean; isTransitioned: boolean;
@@ -33,6 +34,7 @@ interface ChatAIProps {
clearChatPage?: () => void; clearChatPage?: () => void;
isChatPage?: boolean; isChatPage?: boolean;
getFileUrl: (path: string) => string; getFileUrl: (path: string) => string;
showChatHistory?: boolean;
} }
export interface ChatAIRef { export interface ChatAIRef {
@@ -56,6 +58,7 @@ const ChatAI = memo(
clearChatPage, clearChatPage,
isChatPage = false, isChatPage = false,
getFileUrl, getFileUrl,
showChatHistory = true,
}, },
ref ref
) => { ) => {
@@ -89,7 +92,9 @@ const ChatAI = memo(
const [Question, setQuestion] = useState<string>(""); const [Question, setQuestion] = useState<string>("");
const [websocketSessionId, setWebsocketSessionId] = useState(''); const [showPrevSuggestion, setShowPrevSuggestion] = useState(true);
const [websocketSessionId, setWebsocketSessionId] = useState("");
const onWebsocketSessionId = useCallback((sessionId: string) => { const onWebsocketSessionId = useCallback((sessionId: string) => {
setWebsocketSessionId(sessionId); setWebsocketSessionId(sessionId);
@@ -119,9 +124,14 @@ const ChatAI = memo(
const dealMsgRef = useRef<((msg: string) => void) | null>(null); const dealMsgRef = useRef<((msg: string) => void) | null>(null);
const clientId = isChatPage ? "standalone" : "popup" const clientId = isChatPage ? "standalone" : "popup";
const { errorShow, setErrorShow, reconnect, disconnectWS, updateDealMsg } = const {
useWebSocket({ errorShow,
setErrorShow,
reconnect,
disconnectWS,
updateDealMsg,
} = useWebSocket({
clientId, clientId,
connected, connected,
setConnected, setConnected,
@@ -161,7 +171,7 @@ const ChatAI = memo(
setTimedoutShow, setTimedoutShow,
(chat) => cancelChat(chat || activeChat), (chat) => cancelChat(chat || activeChat),
setLoadingStep, setLoadingStep,
handlers, handlers
); );
useEffect(() => { useEffect(() => {
@@ -189,16 +199,28 @@ const ChatAI = memo(
]); ]);
const init = useCallback( const init = useCallback(
(value: string) => { async (value: string) => {
if (!isLogin) return; try {
if (!curChatEnd) return; console.log("init", isLogin, curChatEnd, activeChat?._id);
if (!isLogin || !curChatEnd) return;
setShowPrevSuggestion(false);
if (!activeChat?._id) { if (!activeChat?._id) {
createNewChat(value, activeChat, websocketSessionId); await createNewChat(value, activeChat, websocketSessionId);
} else { } else {
handleSendMessage(value, activeChat, websocketSessionId); 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(); const { createWin } = useWindows();
@@ -207,14 +229,17 @@ const ChatAI = memo(
}, [createChatWindow, createWin]); }, [createChatWindow, createWin]);
useEffect(() => { useEffect(() => {
setCurChatEnd(true);
return () => { return () => {
if (messageTimeoutRef.current) { if (messageTimeoutRef.current) {
clearTimeout(messageTimeoutRef.current); clearTimeout(messageTimeoutRef.current);
} }
Promise.resolve().then(() => {
chatClose(activeChat); chatClose(activeChat);
setActiveChat(undefined); setActiveChat(undefined);
setCurChatEnd(true); setCurChatEnd(true);
disconnectWS(); disconnectWS();
});
}; };
}, [chatClose, setCurChatEnd]); }, [chatClose, setCurChatEnd]);
@@ -240,7 +265,8 @@ const ChatAI = memo(
] ]
); );
const deleteChat = useCallback((chatId: string) => { const deleteChat = useCallback(
(chatId: string) => {
setChats((prev) => prev.filter((chat) => chat._id !== chatId)); setChats((prev) => prev.filter((chat) => chat._id !== chatId));
if (activeChat?._id === chatId) { if (activeChat?._id === chatId) {
const remainingChats = chats.filter((chat) => chat._id !== chatId); const remainingChats = chats.filter((chat) => chat._id !== chatId);
@@ -250,7 +276,9 @@ const ChatAI = memo(
init(""); init("");
} }
} }
}, [activeChat, chats, init, setActiveChat]); },
[activeChat, chats, init, setActiveChat]
);
const handleOutsideClick = useCallback((e: MouseEvent) => { const handleOutsideClick = useCallback((e: MouseEvent) => {
const sidebar = document.querySelector("[data-sidebar]"); const sidebar = document.querySelector("[data-sidebar]");
@@ -297,9 +325,9 @@ const ChatAI = memo(
return ( return (
<div <div
data-tauri-drag-region data-tauri-drag-region
className={`h-full flex flex-col rounded-xl overflow-hidden`} className={`h-full flex flex-col rounded-xl relative`}
> >
{!setIsSidebarOpen && ( {showChatHistory && !setIsSidebarOpen && (
<ChatSidebar <ChatSidebar
isSidebarOpen={isSidebarOpenChat} isSidebarOpen={isSidebarOpenChat}
chats={chats} chats={chats}
@@ -320,6 +348,7 @@ const ChatAI = memo(
reconnect={reconnect} reconnect={reconnect}
isChatPage={isChatPage} isChatPage={isChatPage}
setIsLogin={setIsLoginChat} setIsLogin={setIsLoginChat}
showChatHistory={showChatHistory}
/> />
{isLogin ? ( {isLogin ? (
<ChatContent <ChatContent
@@ -335,12 +364,16 @@ const ChatAI = memo(
timedoutShow={timedoutShow} timedoutShow={timedoutShow}
errorShow={errorShow} errorShow={errorShow}
Question={Question} Question={Question}
handleSendMessage={(value) => handleSendMessage(value, activeChat)} handleSendMessage={(value) =>
handleSendMessage(value, activeChat)
}
getFileUrl={getFileUrl} getFileUrl={getFileUrl}
/> />
) : ( ) : (
<ConnectPrompt /> <ConnectPrompt />
)} )}
{showPrevSuggestion ? <PrevSuggestion sendMessage={init} /> : null}
</div> </div>
); );
} }

View File

@@ -29,6 +29,8 @@ import { useChatStore } from "@/stores/chatStore";
import type { Chat } from "./types"; import type { Chat } from "./types";
import { useConnectStore } from "@/stores/connectStore"; import { useConnectStore } from "@/stores/connectStore";
import platformAdapter from "@/utils/platformAdapter"; import platformAdapter from "@/utils/platformAdapter";
import { list_coco_servers } from "@/commands";
interface ChatHeaderProps { interface ChatHeaderProps {
onCreateNewChat: () => void; onCreateNewChat: () => void;
onOpenChatAI: () => void; onOpenChatAI: () => void;
@@ -38,6 +40,7 @@ interface ChatHeaderProps {
reconnect: (server?: IServer) => void; reconnect: (server?: IServer) => void;
setIsLogin: (isLogin: boolean) => void; setIsLogin: (isLogin: boolean) => void;
isChatPage?: boolean; isChatPage?: boolean;
showChatHistory?: boolean;
} }
export function ChatHeader({ export function ChatHeader({
@@ -48,6 +51,7 @@ export function ChatHeader({
reconnect, reconnect,
setIsLogin, setIsLogin,
isChatPage = false, isChatPage = false,
showChatHistory,
}: ChatHeaderProps) { }: ChatHeaderProps) {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -63,8 +67,11 @@ export function ChatHeader({
const currentService = useConnectStore((state) => state.currentService); const currentService = useConnectStore((state) => state.currentService);
const setCurrentService = useConnectStore((state) => state.setCurrentService); const setCurrentService = useConnectStore((state) => state.setCurrentService);
const fetchServers = useCallback(async (resetSelection: boolean) => { const isTauri = useAppStore((state) => state.isTauri);
platformAdapter.invokeBackend("list_coco_servers")
const fetchServers = useCallback(
async (resetSelection: boolean) => {
list_coco_servers()
.then((res: any) => { .then((res: any) => {
const enabledServers = (res as IServer[]).filter( const enabledServers = (res as IServer[]).filter(
(server) => server.enabled !== false (server) => server.enabled !== false
@@ -87,10 +94,12 @@ export function ChatHeader({
.catch((err: any) => { .catch((err: any) => {
console.error(err); console.error(err);
}); });
}, [currentService?.id]); },
[currentService?.id]
);
useEffect(() => { useEffect(() => {
fetchServers(true); isTauri && fetchServers(true);
const unlisten = platformAdapter.listenEvent("login_or_logout", (event) => { const unlisten = platformAdapter.listenEvent("login_or_logout", (event) => {
console.log("Login or Logout:", currentService, event); console.log("Login or Logout:", currentService, event);
@@ -103,8 +112,6 @@ export function ChatHeader({
}; };
}, []); }, []);
const switchServer = async (server: IServer) => { const switchServer = async (server: IServer) => {
if (!server) return; if (!server) return;
try { try {
@@ -149,6 +156,7 @@ export function ChatHeader({
data-tauri-drag-region data-tauri-drag-region
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{isTauri && (
<button <button
data-sidebar-button data-sidebar-button
onClick={(e) => { onClick={(e) => {
@@ -159,18 +167,22 @@ export function ChatHeader({
> >
<HistoryIcon /> <HistoryIcon />
</button> </button>
)}
<Menu> <Menu>
<MenuButton className="flex items-center gap-1 rounded-full bg-white dark:bg-[#202126] p-1 text-sm/6 font-semibold text-gray-800 dark:text-white border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none"> <MenuButton className="px-2 flex items-center gap-1 rounded-full bg-white dark:bg-[#202126] p-1 text-sm/6 font-semibold text-gray-800 dark:text-white border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none">
<img <img
src={logoImg} src={logoImg}
className="w-4 h-4" className="w-4 h-4"
alt={t("assistant.message.logo")} alt={t("assistant.message.logo")}
/> />
Coco AI Coco AI
{showChatHistory && isTauri ? (
<ChevronDownIcon className="size-4 text-gray-500 dark:text-gray-400" /> <ChevronDownIcon className="size-4 text-gray-500 dark:text-gray-400" />
) : null}
</MenuButton> </MenuButton>
{showChatHistory && isTauri ? (
<MenuItems <MenuItems
transition transition
anchor="bottom end" anchor="bottom end"
@@ -187,14 +199,17 @@ export function ChatHeader({
</button> </button>
</MenuItem> </MenuItem>
</MenuItems> </MenuItems>
) : null}
</Menu> </Menu>
{showChatHistory && isTauri ? (
<button <button
onClick={onCreateNewChat} onClick={onCreateNewChat}
className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800" className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800"
> >
<MessageSquarePlus className="h-4 w-4" /> <MessageSquarePlus className="h-4 w-4" />
</button> </button>
) : null}
</div> </div>
<div> <div>
@@ -205,7 +220,7 @@ export function ChatHeader({
</h2> </h2>
</div> </div>
<div className="flex items-center gap-2"> {isTauri ? <div className="flex items-center gap-2">
<button <button
onClick={togglePin} onClick={togglePin}
className={`${isPinned ? "text-blue-500" : ""}`} className={`${isPinned ? "text-blue-500" : ""}`}
@@ -315,7 +330,7 @@ export function ChatHeader({
<WindowsFullIcon className="rotate-30 scale-x-[-1]" /> <WindowsFullIcon className="rotate-30 scale-x-[-1]" />
</button> </button>
)} )}
</div> </div>: <div/>}
</header> </header>
); );
} }

View File

@@ -25,11 +25,14 @@ export const ChatSidebar: React.FC<ChatSidebarProps> = ({
return ( return (
<div <div
data-sidebar data-sidebar
className={`fixed inset-y-0 left-0 z-50 w-64 transform transition-all duration-300 ease-in-out className={`
${isSidebarOpen ? "translate-x-0" : "-translate-x-[calc(100%)]"} h-[calc(100%+90px)] absolute top-0 left-0 z-10 w-64
md:relative md:translate-x-0 bg-gray-100 dark:bg-gray-800 transform transition-all duration-300 ease-in-out
${isSidebarOpen ? "translate-x-0" : "-translate-x-full"}
bg-gray-100 dark:bg-gray-800
border-r border-gray-200 dark:border-gray-700 rounded-tl-xl rounded-bl-xl border-r border-gray-200 dark:border-gray-700 rounded-tl-xl rounded-bl-xl
overflow-hidden`} overflow-hidden
`}
> >
<Sidebar <Sidebar
chats={chats} chats={chats}

View File

@@ -1,13 +1,15 @@
import { useConnectStore } from "@/stores/connectStore";
import clsx from "clsx"; import clsx from "clsx";
import { filesize } from "filesize"; import { filesize } from "filesize";
import { Files, Trash2, X } from "lucide-react"; import { Files, Trash2, X } from "lucide-react";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import Checkbox from "../Common/Checkbox";
import FileIcon from "../Common/Icons/FileIcon"; import { useConnectStore } from "@/stores/connectStore";
import Checkbox from "@/components/Common/Checkbox";
import FileIcon from "@/components/Common/Icons/FileIcon";
import { delete_attachment, get_attachment } from "@/commands"; import { delete_attachment, get_attachment } from "@/commands";
import { AttachmentHit } from "@/types/commands"; import { AttachmentHit } from "@/types/commands";
import { useAppStore } from "@/stores/appStore";
interface SessionFileProps { interface SessionFileProps {
sessionId: string; sessionId: string;
@@ -16,6 +18,8 @@ interface SessionFileProps {
const SessionFile = (props: SessionFileProps) => { const SessionFile = (props: SessionFileProps) => {
const { sessionId } = props; const { sessionId } = props;
const { t } = useTranslation(); const { t } = useTranslation();
const isTauri = useAppStore((state) => state.isTauri);
const currentService = useConnectStore((state) => state.currentService); const currentService = useConnectStore((state) => state.currentService);
const [uploadedFiles, setUploadedFiles] = useState<AttachmentHit[]>([]); const [uploadedFiles, setUploadedFiles] = useState<AttachmentHit[]>([]);
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
@@ -32,14 +36,20 @@ const SessionFile = (props: SessionFileProps) => {
}, [sessionId]); }, [sessionId]);
const getUploadedFiles = async () => { const getUploadedFiles = async () => {
if (isTauri) {
const response = await get_attachment({ serverId, sessionId }); const response = await get_attachment({ serverId, sessionId });
setUploadedFiles(response.hits.hits); setUploadedFiles(response.hits.hits);
} else {
}
}; };
const handleDelete = async (id: string) => { 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; if (!result) return;
getUploadedFiles(); getUploadedFiles();

View File

@@ -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<PrevSuggestionProps> = (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<string[]>([]);
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 (
<ul className="absolute left-2 bottom-2 flex flex-col gap-2">
{list.map((item) => {
return (
<li
key={item}
className="flex items-center self-start gap-2 px-3 py-2 leading-4 text-sm text-[#333] dark:text-[#d8d8d8] rounded-xl border border-black/15 dark:border-white/15 hover:!border-[#0072ff] hover:!text-[#0072ff] transition cursor-pointer"
onClick={() => sendMessage(item)}
>
{item}
<MoveRight className="size-4" />
</li>
);
})}
</ul>
);
};
export default PrevSuggestion;

View File

@@ -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 (
<a href="https://coco.rs/" target="_blank">
<img src={isDark ? logoDark : logoLight} alt="Logo" className="h-4" />
</a>
);
};
return (
<div className="flex items-center gap-[6px] text-xs text-[#666] dark:text-[#999]">
Powered by
{renderLogo()}
</div>
);
};
export default Copyright;

View File

@@ -1,4 +1,5 @@
import { File } from "lucide-react"; import { File } from "lucide-react";
import React from 'react';
import IconWrapper from "./IconWrapper"; import IconWrapper from "./IconWrapper";
import ThemedIcon from "./ThemedIcon"; import ThemedIcon from "./ThemedIcon";
@@ -11,7 +12,7 @@ interface ItemIconProps {
onClick?: React.MouseEventHandler<HTMLDivElement>; onClick?: React.MouseEventHandler<HTMLDivElement>;
} }
function ItemIcon({ const ItemIcon = React.memo(function ItemIcon({
item, item,
className = "w-5 h-5 flex-shrink-0", className = "w-5 h-5 flex-shrink-0",
onClick = () => {}, onClick = () => {},
@@ -34,7 +35,7 @@ function ItemIcon({
let selectedIcon = icons[item?.icon]; let selectedIcon = icons[item?.icon];
if (!selectedIcon) { if (!selectedIcon) {
selectedIcon=item?.icon selectedIcon = item?.icon;
} }
if (!selectedIcon) { if (!selectedIcon) {
@@ -65,6 +66,6 @@ function ItemIcon({
</IconWrapper> </IconWrapper>
); );
} }
} })
export default ItemIcon; export default ItemIcon;

View File

@@ -20,6 +20,19 @@ function TypeIcon({
const endpoint_http = useAppStore((state) => state.endpoint_http); const endpoint_http = useAppStore((state) => state.endpoint_http);
const connectorSource = useFindConnectorIcon(item); const connectorSource = useFindConnectorIcon(item);
if (item?.source?.icon) {
if (
item?.source?.icon.startsWith("http://") ||
item?.source?.icon.startsWith("https://")
) {
return (
<IconWrapper className={className} onClick={onClick}>
<img className={className} src={item?.source?.icon} alt="icon" />
</IconWrapper>
);
}
}
// If the icon is a valid base64-encoded image // If the icon is a valid base64-encoded image
const isBase64 = connectorSource?.icon?.startsWith("data:image/"); const isBase64 = connectorSource?.icon?.startsWith("data:image/");
if (isBase64) { if (isBase64) {

View File

@@ -2,21 +2,24 @@ import { ArrowDown01, Command, CornerDownLeft } from "lucide-react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import clsx from "clsx"; import clsx from "clsx";
import logoImg from "@/assets/icon.svg";
import { useSearchStore } from "@/stores/searchStore";
import TypeIcon from "@/components/Common/Icons/TypeIcon"; import TypeIcon from "@/components/Common/Icons/TypeIcon";
import { useAppStore } from "@/stores/appStore"; import Copyright from "@/components/Common/Copyright";
import { isMac } from "@/utils/platform"; import { isMac } from "@/utils/platform";
import PinOffIcon from "@/icons/PinOff"; import PinOffIcon from "@/icons/PinOff";
import PinIcon from "@/icons/Pin"; 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"; import { useUpdateStore } from "@/stores/updateStore";
interface FooterProps { interface FooterProps {
isTauri: boolean;
openSetting: () => void; openSetting: () => void;
setWindowAlwaysOnTop: (isPinned: boolean) => Promise<void>; setWindowAlwaysOnTop: (isPinned: boolean) => Promise<void>;
} }
export default function Footer({ export default function Footer({
isTauri,
openSetting, openSetting,
setWindowAlwaysOnTop, setWindowAlwaysOnTop,
}: FooterProps) { }: FooterProps) {
@@ -41,9 +44,10 @@ export default function Footer({
return ( return (
<div <div
data-tauri-drag-region data-tauri-drag-region={isTauri}
className="px-4 z-999 mx-[1px] h-8 absolute bottom-0 left-0 right-0 border-t border-gray-200 dark:border-gray-700 flex items-center justify-between rounded-xl rounded-t-none overflow-hidden" className="px-4 z-999 mx-[1px] h-8 absolute bottom-0 left-0 right-0 border-t border-gray-200 dark:border-gray-700 flex items-center justify-between rounded-xl rounded-t-none overflow-hidden"
> >
{isTauri ? (
<div className="flex items-center"> <div className="flex items-center">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
{sourceData?.source?.name ? ( {sourceData?.source?.name ? (
@@ -58,7 +62,10 @@ export default function Footer({
)} )}
<div className="relative text-xs text-gray-500 dark:text-gray-400"> <div className="relative text-xs text-gray-500 dark:text-gray-400">
{updateInfo?.available ? ( {updateInfo?.available ? (
<div className="cursor-pointer" onClick={() => setVisible(true)}> <div
className="cursor-pointer"
onClick={() => setVisible(true)}
>
<span>{t("search.footer.updateAvailable")}</span> <span>{t("search.footer.updateAvailable")}</span>
<span className="absolute top-0 -right-2 size-1.5 bg-[#FF3434] rounded-full"></span> <span className="absolute top-0 -right-2 size-1.5 bg-[#FF3434] rounded-full"></span>
</div> </div>
@@ -81,6 +88,9 @@ export default function Footer({
</button> </button>
</div> </div>
</div> </div>
) : (
<Copyright />
)}
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="gap-1 flex items-center text-[#666] dark:text-[#666] text-xs"> <div className="gap-1 flex items-center text-[#666] dark:text-[#666] text-xs">

View File

@@ -2,11 +2,14 @@ import { Command } from "lucide-react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { isMac } from "@/utils/platform"; import { isMac } from "@/utils/platform";
import { useShortcutsStore } from "@/stores/shortcutsStore";
import noDataImg from "@/assets/coconut-tree.png"; import noDataImg from "@/assets/coconut-tree.png";
export const NoResults = () => { export const NoResults = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const modeSwitch = useShortcutsStore((state) => state.modeSwitch);
return ( return (
<div <div
data-tauri-drag-region data-tauri-drag-region
@@ -30,7 +33,7 @@ export const NoResults = () => {
</span> </span>
)} )}
<span className="ml-1 w-5 h-5 rounded-[6px] border border-[#D8D8D8] flex justify-center items-center"> <span className="ml-1 w-5 h-5 rounded-[6px] border border-[#D8D8D8] flex justify-center items-center">
T {modeSwitch}
</span> </span>
</div> </div>
</div> </div>

View File

@@ -1,12 +1,12 @@
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 { Settings, LogOut, User, ChevronUp, Home } from "lucide-react";
// import { Link } from "react-router-dom"; // import { Link } from "react-router-dom";
import logoImg from "../assets/icon.svg";
import {useAppStore} from "@/stores/appStore"; import logoImg from "@/assets/icon.svg";
import {OctagonAlert, X} from 'lucide-react'; import { useAppStore } from "@/stores/appStore";
const Footer = () => { const Footer = () => {
const error = useAppStore((state) => state.error); const error = useAppStore((state) => state.error);
const setError = useAppStore((state) => state.setError); const setError = useAppStore((state) => state.setError);
@@ -14,11 +14,12 @@ const Footer = () => {
<div className="fixed bottom-0 left-0 right-0 bg-white dark:bg-gray-800 border-t dark:border-gray-700"> <div className="fixed bottom-0 left-0 right-0 bg-white dark:bg-gray-800 border-t dark:border-gray-700">
{/* Move the warning message outside the border */} {/* Move the warning message outside the border */}
{error && ( {error && (
<div <div className="fixed bottom-6 left-0 right-0 bg-yellow-100 dark:bg-yellow-900 border-l-4 border-yellow-500 rounded-lg shadow-lg p-4 m-4">
className="fixed bottom-6 left-0 right-0 bg-yellow-100 dark:bg-yellow-900 border-l-4 border-yellow-500 rounded-lg shadow-lg p-4 m-4">
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<OctagonAlert size={32} color="red" className="mr-2"/> <OctagonAlert size={32} color="red" className="mr-2" />
<span className="text-xs text-red-500 dark:text-red-400 flex-1">{error}</span> <span className="text-xs text-red-500 dark:text-red-400 flex-1">
{error}
</span>
<X <X
className="cursor-pointer ml-2" className="cursor-pointer ml-2"
onClick={() => setError("")} onClick={() => setError("")}
@@ -31,8 +32,7 @@ const Footer = () => {
<div className="max-w-6xl mx-auto px-4 h-8 flex items-center justify-between"> <div className="max-w-6xl mx-auto px-4 h-8 flex items-center justify-between">
<Menu as="div" className="relative"> <Menu as="div" className="relative">
<MenuButton <MenuButton className="h-7 flex items-center space-x-2 px-1 py-1.5 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
className="h-7 flex items-center space-x-2 px-1 py-1.5 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
<img <img
src={logoImg} src={logoImg}
className="w-5 h-5 text-gray-600 dark:text-gray-400" className="w-5 h-5 text-gray-600 dark:text-gray-400"
@@ -106,7 +106,6 @@ const Footer = () => {
</MenuItems> */} </MenuItems> */}
</Menu> </Menu>
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<span className="text-xs text-gray-500 dark:text-gray-400"> <span className="text-xs text-gray-500 dark:text-gray-400">
Version {process.env.VERSION || "v1.0.0"} Version {process.env.VERSION || "v1.0.0"}

View File

@@ -6,13 +6,14 @@ interface AutoResizeTextareaProps {
setInput: (value: string) => void; setInput: (value: string) => void;
handleKeyDown?: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void; handleKeyDown?: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void;
connected: boolean; connected: boolean;
chatPlaceholder?: string;
} }
// Forward ref to allow parent to interact with this component // Forward ref to allow parent to interact with this component
const AutoResizeTextarea = forwardRef< const AutoResizeTextarea = forwardRef<
{ reset: () => void; focus: () => void }, { reset: () => void; focus: () => void },
AutoResizeTextareaProps AutoResizeTextareaProps
>(({ input, setInput, handleKeyDown, connected }, ref) => { >(({ input, setInput, handleKeyDown, connected, chatPlaceholder }, ref) => {
const { t } = useTranslation(); const { t } = useTranslation();
const textareaRef = useRef<HTMLTextAreaElement>(null); const textareaRef = useRef<HTMLTextAreaElement>(null);
@@ -34,8 +35,10 @@ const AutoResizeTextarea = forwardRef<
autoCapitalize="none" autoCapitalize="none"
spellCheck="false" 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" 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') : ""} placeholder={
aria-label={t('search.textarea.ariaLabel')} connected ? chatPlaceholder || t("search.textarea.placeholder") : ""
}
aria-label={t("search.textarea.ariaLabel")}
value={input} value={input}
onChange={(e) => setInput(e.target.value)} onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => handleKeyDown?.(e)} onKeyDown={(e) => handleKeyDown?.(e)}

View File

@@ -20,7 +20,7 @@ interface State {
} }
interface ContextMenuProps { interface ContextMenuProps {
hideCoco: () => Promise<void>; hideCoco?: () => void;
} }
const ContextMenu = ({ hideCoco }: ContextMenuProps) => { const ContextMenu = ({ hideCoco }: ContextMenuProps) => {
@@ -54,7 +54,7 @@ const ContextMenu = ({ hideCoco }: ContextMenuProps) => {
setVisibleContextMenu(false); setVisibleContextMenu(false);
hideCoco(); hideCoco && hideCoco();
}, },
}, },
{ {

View File

@@ -2,6 +2,7 @@ import { ArrowBigLeft, Search, Send, Brain } from "lucide-react";
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import clsx from "clsx"; import clsx from "clsx";
import { useKeyPress } from "ahooks";
import ChatSwitch from "@/components/Common/ChatSwitch"; import ChatSwitch from "@/components/Common/ChatSwitch";
import AutoResizeTextarea from "./AutoResizeTextarea"; import AutoResizeTextarea from "./AutoResizeTextarea";
@@ -12,12 +13,11 @@ import { useSearchStore } from "@/stores/searchStore";
import { metaOrCtrlKey } from "@/utils/keyboardUtils"; import { metaOrCtrlKey } from "@/utils/keyboardUtils";
import SearchPopover from "./SearchPopover"; import SearchPopover from "./SearchPopover";
// import AudioRecording from "../AudioRecording"; // import AudioRecording from "../AudioRecording";
import { hide_coco } from "@/commands";
import { DataSource } from "@/types/commands"; import { DataSource } from "@/types/commands";
// import InputExtra from "./InputExtra"; // import InputExtra from "./InputExtra";
// import { useConnectStore } from "@/stores/connectStore"; // import { useConnectStore } from "@/stores/connectStore";
import { useShortcutsStore } from "@/stores/shortcutsStore"; import { useShortcutsStore } from "@/stores/shortcutsStore";
import { useKeyPress } from "ahooks"; import Copyright from "@/components/Common/Copyright";
interface ChatInputProps { interface ChatInputProps {
onSend: (message: string) => void; onSend: (message: string) => void;
@@ -46,6 +46,11 @@ interface ChatInputProps {
}) => Promise<string | string[] | null>; }) => Promise<string | string[] | null>;
getFileMetadata: (path: string) => Promise<any>; getFileMetadata: (path: string) => Promise<any>;
getFileIcon: (path: string, size: number) => Promise<string>; getFileIcon: (path: string, size: number) => Promise<string>;
hideCoco?: () => void;
hasFeature?: string[];
hasModules?: string[];
searchPlaceholder?: string;
chatPlaceholder?: string;
} }
export default function ChatInput({ export default function ChatInput({
@@ -64,16 +69,12 @@ export default function ChatInput({
isChatPage = false, isChatPage = false,
getDataSourcesByServer, getDataSourcesByServer,
setupWindowFocusListener, setupWindowFocusListener,
}: // checkScreenPermission, hasFeature = ["think", "search", "think_icon", "search_icon"],
// requestScreenPermission, hideCoco,
// getScreenMonitors, hasModules = [],
// getScreenWindows, searchPlaceholder,
// captureMonitorScreenshot, chatPlaceholder,
// captureWindowScreenshot, }: ChatInputProps) {
// openFileDialog,
// getFileMetadata,
// getFileIcon,
ChatInputProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const showTooltip = useAppStore( const showTooltip = useAppStore(
@@ -156,6 +157,7 @@ ChatInputProps) {
const handleSubmit = useCallback(() => { const handleSubmit = useCallback(() => {
const trimmedValue = inputValue.trim(); const trimmedValue = inputValue.trim();
console.log("handleSubmit", trimmedValue, disabled);
if (trimmedValue && !disabled) { if (trimmedValue && !disabled) {
changeInput(""); changeInput("");
onSend(trimmedValue); onSend(trimmedValue);
@@ -168,7 +170,7 @@ ChatInputProps) {
if (inputValue) { if (inputValue) {
changeInput(""); changeInput("");
} else if (!isPinned) { } else if (!isPinned) {
hide_coco(); hideCoco && hideCoco();
} }
}, [inputValue, isPinned]); }, [inputValue, isPinned]);
@@ -295,12 +297,17 @@ ChatInputProps) {
changeInput(value); changeInput(value);
}} }}
connected={connected} connected={connected}
handleKeyDown={(e) => { handleKeyDown={(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === "Enter") { if (e.key === "Enter") {
if (e.nativeEvent.isComposing) {
return;
}
console.log("handleKeyDown", e.nativeEvent.isComposing);
e.preventDefault(); e.preventDefault();
handleSubmit(); handleSubmit();
} }
}} }}
chatPlaceholder={chatPlaceholder}
/> />
) : ( ) : (
<input <input
@@ -311,7 +318,9 @@ ChatInputProps) {
autoCapitalize="none" autoCapitalize="none"
spellCheck="false" spellCheck="false"
className="text-base font-normal flex-1 outline-none min-w-[200px] text-[#333] dark:text-[#d8d8d8] placeholder-text-xs placeholder-[#999] dark:placeholder-gray-500 bg-transparent" className="text-base font-normal flex-1 outline-none min-w-[200px] text-[#333] dark:text-[#d8d8d8] placeholder-text-xs placeholder-[#999] dark:placeholder-gray-500 bg-transparent"
placeholder={t("search.input.searchPlaceholder")} placeholder={
searchPlaceholder || t("search.input.searchPlaceholder")
}
value={inputValue} value={inputValue}
onChange={(e) => { onChange={(e) => {
onSend(e.target.value); onSend(e.target.value);
@@ -390,7 +399,7 @@ ChatInputProps) {
<div className="absolute top-0 right-0 bottom-0 left-0 px-2 py-4 bg-red-500/10 rounded-md font-normal text-xs text-gray-400 flex items-center gap-4"> <div className="absolute top-0 right-0 bottom-0 left-0 px-2 py-4 bg-red-500/10 rounded-md font-normal text-xs text-gray-400 flex items-center gap-4">
{t("search.input.connectionError")} {t("search.input.connectionError")}
<div <div
className="h-[24px] px-2 bg-[#0061FF] rounded-[12px] font-normal text-xs text-white flex items-center justify-center cursor-pointer" className="px-1 h-[24px] text-[#0061FF] font-normal text-xs flex items-center justify-center cursor-pointer underline"
onClick={() => { onClick={() => {
reconnect(); reconnect();
setReconnectCountdown(10); setReconnectCountdown(10);
@@ -424,6 +433,7 @@ ChatInputProps) {
/> />
)} */} )} */}
{hasFeature.includes("think") && (
<button <button
className={clsx( className={clsx(
"flex items-center gap-1 p-1 h-6 rounded-lg transition hover:bg-[#EDEDED] dark:hover:bg-[#202126]", "flex items-center gap-1 p-1 h-6 rounded-lg transition hover:bg-[#EDEDED] dark:hover:bg-[#202126]",
@@ -450,12 +460,20 @@ ChatInputProps) {
</span> </span>
)} )}
</button> </button>
)}
{hasFeature.includes("search") && (
<SearchPopover <SearchPopover
isSearchActive={isSearchActive} isSearchActive={isSearchActive}
setIsSearchActive={setIsSearchActive} setIsSearchActive={setIsSearchActive}
getDataSourcesByServer={getDataSourcesByServer} getDataSourcesByServer={getDataSourcesByServer}
/> />
)}
{!hasFeature.includes("search") && !hasFeature.includes("think") ? (
<div className="px-2">
<Copyright />
</div>
) : null}
</div> </div>
) : ( ) : (
<div <div
@@ -464,7 +482,7 @@ ChatInputProps) {
></div> ></div>
)} )}
{isChatPage ? null : ( {isChatPage || hasModules?.length !== 2 ? null : (
<div className="relative w-16 flex justify-end items-center"> <div className="relative w-16 flex justify-end items-center">
{showTooltip && modifierKeyPressed ? ( {showTooltip && modifierKeyPressed ? (
<div <div

View File

@@ -2,13 +2,13 @@ import { useEffect, useState, useCallback, useRef } from "react";
import { debounce } from "lodash-es"; import { debounce } from "lodash-es";
import DropdownList from "./DropdownList"; import DropdownList from "./DropdownList";
import Footer from "./Footer";
import { SearchResults } from "@/components/Search/SearchResults"; import { SearchResults } from "@/components/Search/SearchResults";
import { useSearchStore } from "@/stores/searchStore"; import { useSearchStore } from "@/stores/searchStore";
import ContextMenu from "./ContextMenu"; import ContextMenu from "./ContextMenu";
import { NoResults } from "./NoResults"; import { NoResults } from "@/components/Common/UI/NoResults";
import Footer from "@/components/Common/UI/Footer";
interface SearchProps { interface SearchProps {
isTauri: boolean;
changeInput: (val: string) => void; changeInput: (val: string) => void;
isChatMode: boolean; isChatMode: boolean;
input: string; input: string;
@@ -18,12 +18,13 @@ interface SearchProps {
size: number, size: number,
queryStrings: any queryStrings: any
) => Promise<any>; ) => Promise<any>;
hideCoco: () => Promise<any>; hideCoco?: () => void;
openSetting: () => void; openSetting: () => void;
setWindowAlwaysOnTop: (isPinned: boolean) => Promise<void>; setWindowAlwaysOnTop: (isPinned: boolean) => Promise<void>;
} }
function Search({ function Search({
isTauri,
isChatMode, isChatMode,
input, input,
querySearch, querySearch,
@@ -81,7 +82,7 @@ function Search({
return ( return (
<div <div
ref={mainWindowRef} ref={mainWindowRef}
className={`h-[calc(100vh-90px)] pb-10 w-full relative`} className={`h-full pb-10 w-full relative`}
> >
{/* Search Results Panel */} {/* Search Results Panel */}
{suggests.length > 0 ? ( {suggests.length > 0 ? (
@@ -105,6 +106,7 @@ function Search({
)} )}
<Footer <Footer
isTauri={isTauri}
openSetting={openSetting} openSetting={openSetting}
setWindowAlwaysOnTop={setWindowAlwaysOnTop} setWindowAlwaysOnTop={setWindowAlwaysOnTop}
/> />

View File

@@ -16,7 +16,7 @@ interface SearchListItemProps {
showListRight?: boolean; showListRight?: boolean;
} }
const SearchListItem: React.FC<SearchListItemProps> = ({ const SearchListItem: React.FC<SearchListItemProps> = React.memo(({
item, item,
isSelected, isSelected,
currentIndex, currentIndex,
@@ -68,6 +68,6 @@ const SearchListItem: React.FC<SearchListItemProps> = ({
) : null} ) : null}
</div> </div>
); );
}; });
export default SearchListItem; export default SearchListItem;

View File

@@ -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 { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
import { ChevronDownIcon, RefreshCw, Layers, Globe } from "lucide-react"; import { ChevronDownIcon, RefreshCw, Layers, Globe } from "lucide-react";
import clsx from "clsx"; import clsx from "clsx";
@@ -8,7 +8,7 @@ import TypeIcon from "@/components/Common/Icons/TypeIcon";
import { useConnectStore } from "@/stores/connectStore"; import { useConnectStore } from "@/stores/connectStore";
import { useSearchStore } from "@/stores/searchStore"; import { useSearchStore } from "@/stores/searchStore";
import { DataSource } from "@/types/commands"; import { DataSource } from "@/types/commands";
import Checkbox from "../Common/Checkbox"; import Checkbox from "@/components/Common/Checkbox";
interface SearchPopoverProps { interface SearchPopoverProps {
isSearchActive: boolean; isSearchActive: boolean;
@@ -30,18 +30,26 @@ export default function SearchPopover({
const currentService = useConnectStore((state) => state.currentService); const currentService = useConnectStore((state) => state.currentService);
const [showDataSource, setShowDataSource] = useState(false);
const getDataSourceList = useCallback(async () => { const getDataSourceList = useCallback(async () => {
try { try {
const res: DataSource[] = await getDataSourcesByServer( const res: DataSource[] = await getDataSourcesByServer(
currentService?.id currentService?.id
); );
const data = [ if (res?.length === 0) {
setDataSourceList([]);
return;
}
const data = res?.length
? [
{ {
id: "all", id: "all",
name: "search.input.searchPopover.allScope", name: "search.input.searchPopover.allScope",
}, },
...(res || []), ...res,
]; ]
: [];
setDataSourceList(data); setDataSourceList(data);
} catch (err) { } catch (err) {
setDataSourceList([]); setDataSourceList([]);
@@ -49,6 +57,29 @@ export default function SearchPopover({
} }
}, [currentService?.id]); }, [currentService?.id]);
const popoverRef = useRef<HTMLDivElement>(null);
const buttonRef = useRef<HTMLButtonElement>(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(() => { useEffect(() => {
if (dataSourceList.length > 0) { if (dataSourceList.length > 0) {
onSelectDataSource("all", true, true); onSelectDataSource("all", true, true);
@@ -111,8 +142,16 @@ export default function SearchPopover({
</span> </span>
{dataSourceList?.length > 0 && ( {dataSourceList?.length > 0 && (
<Popover> <Popover className="relative">
<PopoverButton as="span" className={clsx("flex items-center")}> <PopoverButton
as="span"
ref={buttonRef}
className={clsx("flex items-center")}
onClick={(e) => {
e.stopPropagation();
setShowDataSource((prev) => !prev);
}}
>
<ChevronDownIcon <ChevronDownIcon
className={clsx("size-5", [ className={clsx("size-5", [
isSearchActive isSearchActive
@@ -122,9 +161,11 @@ export default function SearchPopover({
/> />
</PopoverButton> </PopoverButton>
{showDataSource ? (
<PopoverPanel <PopoverPanel
anchor="top start" static
className="min-w-[220px] bg-white dark:bg-[#202126] rounded-lg shadow-lg border border-gray-200 dark:border-gray-700" ref={popoverRef}
className="absolute z-50 left-0 bottom-6 min-w-[220px] max-h-[400px] overflow-y-auto bg-white dark:bg-[#202126] rounded-lg shadow-lg border border-gray-200 dark:border-gray-700"
> >
<div <div
className="text-sm px-[12px] py-[18px]" className="text-sm px-[12px] py-[18px]"
@@ -195,6 +236,7 @@ export default function SearchPopover({
</ul> </ul>
</div> </div>
</PopoverPanel> </PopoverPanel>
) : null}
</Popover> </Popover>
)} )}
</> </>

View File

@@ -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<any>;
queryDocuments: (
from: number,
size: number,
queryStrings: any
) => Promise<any>;
}
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<ChatAIRef>(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 = () => (
<div className="flex items-center justify-center h-full">loading...</div>
);
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<DataSource[]> => {
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 (
<div
data-tauri-drag-region={isTauri}
className={clsx(
"size-full m-auto overflow-hidden relative bg-no-repeat bg-cover bg-center bg-white dark:bg-black",
[
isTransitioned
? "bg-chat_bg_light dark:bg-chat_bg_dark"
: "bg-search_bg_light dark:bg-search_bg_dark",
],
{
"rounded-xl": !isWin,
"border border-[#E6E6E6] dark:border-[#272626]": isLinux,
}
)}
>
<div
data-tauri-drag-region={isTauri}
className={`p-2 pb-0 absolute w-full flex items-center justify-center transition-all duration-500 ${
isTransitioned
? "top-[calc(100%-90px)] h-[90px] border-t"
: "top-0 h-[90px] border-b"
} border-[#E6E6E6] dark:border-[#272626]`}
>
<InputBox
isChatMode={isChatMode}
inputValue={input}
onSend={handleSendMessage}
disabled={isTyping}
disabledChange={cancelChat}
changeMode={changeMode}
changeInput={setInput}
reconnect={reconnect}
isSearchActive={isSearchActive}
setIsSearchActive={toggleSearchActive}
isDeepThinkActive={isDeepThinkActive}
setIsDeepThinkActive={toggleDeepThinkActive}
hasFeature={hasFeature}
getDataSourcesByServer={getDataSourcesByServer}
setupWindowFocusListener={setupWindowFocusListener}
checkScreenPermission={checkScreenPermission}
requestScreenPermission={requestScreenPermission}
getScreenMonitors={getScreenMonitors}
getScreenWindows={getScreenWindows}
captureMonitorScreenshot={captureMonitorScreenshot}
captureWindowScreenshot={captureWindowScreenshot}
openFileDialog={openFileDialog}
getFileMetadata={getFileMetadata}
getFileIcon={getFileIcon}
hasModules={hasModules}
searchPlaceholder={searchPlaceholder}
chatPlaceholder={chatPlaceholder}
hideCoco={hideCoco}
/>
</div>
<div
data-tauri-drag-region={isTauri}
className={`absolute w-full transition-opacity duration-500 ${
isTransitioned ? "opacity-0 pointer-events-none" : "opacity-100"
} bottom-0 h-[calc(100%-90px)] `}
>
<Suspense fallback={<LoadingFallback />}>
<Search
key="Search"
isTauri={isTauri}
input={input}
isChatMode={isChatMode}
changeInput={setInput}
querySearch={querySearch}
queryDocuments={queryDocuments}
hideCoco={hideCoco}
openSetting={openSetting}
setWindowAlwaysOnTop={setWindowAlwaysOnTop}
/>
</Suspense>
</div>
<div
data-tauri-drag-region={isTauri}
className={`absolute w-full transition-all duration-500 select-auto ${
isTransitioned
? "top-0 opacity-100 pointer-events-auto"
: "-top-[506px] opacity-0 pointer-events-none"
} h-[calc(100%-90px)]`}
>
{isTransitioned && isChatMode ? (
<Suspense fallback={<LoadingFallback />}>
<ChatAI
ref={chatAIRef}
key="ChatAI"
isTransitioned={isTransitioned}
changeInput={setInput}
isSearchActive={isSearchActive}
isDeepThinkActive={isDeepThinkActive}
getFileUrl={getFileUrl}
showChatHistory={showChatHistory}
/>
</Suspense>
) : null}
</div>
<UpdateApp checkUpdate={checkUpdate} relaunchApp={relaunchApp} />
</div>
);
}
export default memo(SearchChat);

View File

@@ -1,11 +1,12 @@
import { ModifierKey, useShortcutsStore } from "@/stores/shortcutsStore";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { formatKey } from "@/utils/keyboardUtils";
import SettingsItem from "@/components/Settings/SettingsItem";
import { Command } from "lucide-react"; import { Command } from "lucide-react";
import { ChangeEvent, useEffect } from "react"; import { ChangeEvent, useEffect } from "react";
import { emit } from "@tauri-apps/api/event"; import { emit } from "@tauri-apps/api/event";
import { formatKey } from "@/utils/keyboardUtils";
import SettingsItem from "@/components/Settings/SettingsItem";
import { isMac } from "@/utils/platform"; import { isMac } from "@/utils/platform";
import { ModifierKey, useShortcutsStore } from "@/stores/shortcutsStore";
export const modifierKeys: ModifierKey[] = isMac export const modifierKeys: ModifierKey[] = isMac
? ["meta", "ctrl"] ? ["meta", "ctrl"]

View File

@@ -69,7 +69,7 @@ const UpdateApp = ({ checkUpdate, relaunchApp }: UpdateAppProps) => {
state.loading = true; state.loading = true;
await updateInfo?.downloadAndInstall((progress) => { await updateInfo?.downloadAndInstall((progress: any) => {
switch (progress.event) { switch (progress.event) {
case "Started": case "Started":
state.total = progress.data.contentLength; state.total = progress.data.contentLength;

View File

@@ -1,8 +1,9 @@
import { useCallback } from "react"; import { useCallback } from "react";
import { isTauri } from "@tauri-apps/api/core";
import type { Chat } from "@/components/Assistant/types"; 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 { 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( export function useChatActions(
currentServiceId: string | undefined, currentServiceId: string | undefined,
@@ -19,14 +20,27 @@ export function useChatActions(
changeInput?: (val: string) => void, changeInput?: (val: string) => void,
websocketSessionId?: string, websocketSessionId?: string,
) { ) {
const isTauri = useAppStore((state) => state.isTauri);
const chatClose = useCallback(async (activeChat?: Chat) => { const chatClose = useCallback(async (activeChat?: Chat) => {
if (!activeChat?._id || !currentServiceId) return; if (!activeChat?._id) return;
try { try {
let response: any = await close_session_chat({ let response: any
if (isTauri) {
if (!currentServiceId) return;
response = await close_session_chat({
serverId: currentServiceId, serverId: currentServiceId,
sessionId: activeChat?._id, sessionId: activeChat?._id,
}); });
response = JSON.parse(response || ""); 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); console.log("_close", response);
} catch (error) { } catch (error) {
console.error("chatClose:", error); console.error("chatClose:", error);
@@ -35,13 +49,24 @@ export function useChatActions(
const cancelChat = useCallback(async (activeChat?: Chat) => { const cancelChat = useCallback(async (activeChat?: Chat) => {
setCurChatEnd(true); setCurChatEnd(true);
if (!activeChat?._id || !currentServiceId) return; if (!activeChat?._id) return;
try { try {
let response: any = await cancel_session_chat({ let response: any
if (isTauri) {
if (!currentServiceId) return;
response = await cancel_session_chat({
serverId: currentServiceId, serverId: currentServiceId,
sessionId: activeChat?._id, sessionId: activeChat?._id,
}); });
response = JSON.parse(response || ""); 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); console.log("_cancel", response);
} catch (error) { } catch (error) {
console.error("cancelChat:", error); console.error("cancelChat:", error);
@@ -52,15 +77,29 @@ export function useChatActions(
chat: Chat, chat: Chat,
callback?: (chat: Chat) => void callback?: (chat: Chat) => void
) => { ) => {
if (!chat?._id || !currentServiceId) return; if (!chat?._id) return;
try { try {
let response: any = await session_chat_history({ let response: any
if (isTauri) {
if (!currentServiceId) return;
response = await session_chat_history({
serverId: currentServiceId, serverId: currentServiceId,
sessionId: chat?._id, sessionId: chat?._id,
from: 0, from: 0,
size: 20, size: 20,
}); });
response = JSON.parse(response || ""); 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 hits = response?.hits?.hits || [];
const updatedChat: Chat = { const updatedChat: Chat = {
...chat, ...chat,
@@ -76,20 +115,22 @@ export function useChatActions(
const createNewChat = useCallback( const createNewChat = useCallback(
async (value: string = "", activeChat?: Chat, id?: string) => { async (value: string = "", activeChat?: Chat, id?: string) => {
try {
setTimedoutShow(false); setTimedoutShow(false);
setErrorShow(false); setErrorShow(false);
chatClose(activeChat); await chatClose(activeChat);
clearAllChunkData(); clearAllChunkData();
setQuestion(value); setQuestion(value);
if (!currentServiceId) return; if (!(websocketSessionId || id)) {
try {
if (!(websocketSessionId || id)){
setErrorShow(true); setErrorShow(true);
console.error("websocketSessionId", websocketSessionId, id); console.error("websocketSessionId", websocketSessionId, id);
return; return;
} }
console.log("sourceDataIds", sourceDataIds, websocketSessionId, id); console.log("sourceDataIds", sourceDataIds, websocketSessionId, id);
let response: any = await new_chat({ let response: any
if (isTauri) {
if (!currentServiceId) return;
response = await new_chat({
serverId: currentServiceId, serverId: currentServiceId,
websocketId: websocketSessionId || id, websocketId: websocketSessionId || id,
message: value, message: value,
@@ -99,6 +140,25 @@ export function useChatActions(
datasource: sourceDataIds?.join(",") || "", 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); console.log("_new", response);
const newChat: Chat = response; const newChat: Chat = response;
curIdRef.current = response?.payload?.id; curIdRef.current = response?.payload?.id;
@@ -124,16 +184,19 @@ export function useChatActions(
const sendMessage = useCallback( const sendMessage = useCallback(
async (content: string, newChat: Chat, id?: string) => { async (content: string, newChat: Chat, id?: string) => {
if (!newChat?._id || !currentServiceId || !content) return; if (!newChat?._id || !content) return;
clearAllChunkData(); clearAllChunkData();
try { try {
if (!(websocketSessionId || id)){ if (!(websocketSessionId || id)) {
setErrorShow(true); setErrorShow(true);
console.error("websocketSessionId", websocketSessionId, id); console.error("websocketSessionId", websocketSessionId, id);
return; return;
} }
let response: any = await send_message({ let response: any
if (isTauri) {
if (!currentServiceId) return;
response = await send_message({
serverId: currentServiceId, serverId: currentServiceId,
websocketId: websocketSessionId || id, websocketId: websocketSessionId || id,
sessionId: newChat?._id, sessionId: newChat?._id,
@@ -145,6 +208,25 @@ export function useChatActions(
message: content, message: content,
}); });
response = JSON.parse(response || ""); 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(",") || "",
}, {
"WEBSOCKET-SESSION-ID": websocketSessionId || id,
})
if (error) {
setErrorShow(true);
console.error('_cancel', error);
return
}
response = res
}
console.log("_send", response); console.log("_send", response);
curIdRef.current = response[0]?._id; curIdRef.current = response[0]?._id;
@@ -178,13 +260,25 @@ export function useChatActions(
); );
const openSessionChat = useCallback(async (chat: Chat) => { const openSessionChat = useCallback(async (chat: Chat) => {
if (!chat?._id || !currentServiceId) return; if (!chat?._id) return;
try { try {
let response: any = await open_session_chat({ let response: any
if (isTauri) {
if (!currentServiceId) return;
response = await open_session_chat({
serverId: currentServiceId, serverId: currentServiceId,
sessionId: chat?._id, sessionId: chat?._id,
}); });
response = JSON.parse(response || ""); 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); console.log("_open", response);
return response; return response;
} catch (error) { } catch (error) {
@@ -194,14 +288,27 @@ export function useChatActions(
}, [currentServiceId]); }, [currentServiceId]);
const getChatHistory = useCallback(async () => { const getChatHistory = useCallback(async () => {
if (!currentServiceId) return [];
try { try {
let response: any = await chat_history({ let response: any
if (isTauri) {
if (!currentServiceId) return [];
response = await chat_history({
serverId: currentServiceId, serverId: currentServiceId,
from: 0, from: 0,
size: 20, size: 20,
}); });
response = JSON.parse(response || ""); 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); console.log("_history", response);
const hits = response?.hits?.hits || []; const hits = response?.hits?.hits || [];
return hits; return hits;
@@ -212,7 +319,7 @@ export function useChatActions(
}, [currentServiceId]); }, [currentServiceId]);
const createChatWindow = useCallback(async (createWin: any) => { const createChatWindow = useCallback(async (createWin: any) => {
if (isTauri()) { if (isTauri) {
createWin && createWin({ createWin && createWin({
label: "chat", label: "chat",
title: "Coco Chat", title: "Coco Chat",

View File

@@ -1,8 +1,6 @@
import { useEffect } from "react"; 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 useEscape = () => {
const handleEscape = async (event: KeyboardEvent) => { const handleEscape = async (event: KeyboardEvent) => {
@@ -12,16 +10,14 @@ const useEscape = () => {
event.preventDefault(); event.preventDefault();
// Hide the Tauri app window when 'Esc' is pressed // Hide the Tauri app window when 'Esc' is pressed
await hide_coco() await platformAdapter.invokeBackend("hide_coco");
console.log("App window hidden successfully."); console.log("App window hidden successfully.");
} }
}; };
useEffect(() => { useEffect(() => {
if (!isTauri()) return; const unlisten = platformAdapter.listenEvent("tauri://focus", () => {
const unlisten = listen("tauri://focus", () => {
// Add event listener for keydown // Add event listener for keydown
window.addEventListener("keydown", handleEscape); window.addEventListener("keydown", handleEscape);
}); });

View File

@@ -1,19 +1,26 @@
import { useConnectStore } from "@/stores/connectStore"; import { useConnectStore } from "@/stores/connectStore";
import { useAppStore } from "@/stores/appStore";
export function useFindConnectorIcon(item: any) { export function useFindConnectorIcon(item: any) {
const connector_data = useConnectStore((state) => state.connector_data); const connector_data = useConnectStore((state) => state.connector_data);
const datasourceData = useConnectStore((state) => state.datasourceData); const datasourceData = useConnectStore((state) => state.datasourceData);
const currentService = useConnectStore((state) => state.currentService); 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 id = item?.source?.id || "";
const result_source = datasourceData[currentService?.id]?.find( const result_source = datasourceData[currentServiceId]?.find(
(data: any) => data.id === id (data: any) => data.id === id
); );
const connector_id = result_source?.connector?.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 (data: any) => data.id === connector_id
); );

View File

@@ -76,6 +76,7 @@ export function useMessageHandler(
return; return;
} }
} catch (error) { } catch (error) {
setCurChatEnd(true);
console.error("parse error:", error); console.error("parse error:", error);
} }
}, },

View File

@@ -1,8 +1,16 @@
import { useState, useEffect, useCallback, useRef } from "react"; 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 { useAppStore, IServer } from "@/stores/appStore";
import { connect_to_server, disconnect } from "@/commands" 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 { interface WebSocketProps {
clientId: string; clientId: string;
@@ -21,41 +29,119 @@ export default function useWebSocket({
dealMsgRef, dealMsgRef,
onWebsocketSessionId, onWebsocketSessionId,
}: WebSocketProps) { }: WebSocketProps) {
const isTauri = useAppStore((state) => state.isTauri);
const endpoint_websocket = useAppStore((state) => state.endpoint_websocket);
const websocketIdRef = useRef<string>("");
const messageQueue = useRef<string[]>([]);
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); const [errorShow, setErrorShow] = useState(false);
// 1. WebSocket connects when loading or switching services // 1. WebSocket connects when loading or switching services
// src/components/Assistant/ChatHeader.tsx // src/components/Assistant/ChatHeader.tsx
// 2. If not connected or disconnected, input box has a connect button, clicking it will connect to WebSocket // 2. If not connected or disconnected, input box has a connect button, clicking it will connect to WebSocket
// src/components/Search/InputBox.tsx // src/components/Search/InputBox.tsx
const reconnect = useCallback(async (server?: IServer) => { const reconnect = useCallback(
async (server?: IServer) => {
if (isTauri) {
const targetServer = server || currentService; const targetServer = server || currentService;
console.log("reconnect_targetServer", targetServer?.id);
if (!targetServer?.id) return; if (!targetServer?.id) return;
try { try {
console.log("reconnect", targetServer.id, clientId); // console.log("reconnect", targetServer.id);
await connect_to_server(targetServer.id, clientId); await connect_to_server(targetServer.id, clientId);
} catch (error) { } catch (error) {
setConnected(false); setConnected(false);
console.error("Failed to connect:", error); console.error("Failed to connect:", error);
} }
}, [currentService]); } else {
connect();
}
},
[currentService]
);
const disconnectWS = async () => { const disconnectWS = async () => {
if (!connected) return; if (!connected) return;
if (isTauri) {
try { try {
console.log("disconnect"); console.log("disconnect");
await disconnect(clientId); await disconnectCommand(clientId);
setConnected(false); setConnected(false);
} catch (error) { } catch (error) {
console.error("Failed to disconnect:", error); console.error("Failed to disconnect:", error);
} }
} else {
disconnect();
}
}; };
const updateDealMsg = useCallback((newDealMsg: (msg: string) => void) => { const updateDealMsg = useCallback(
(newDealMsg: (msg: string) => void) => {
dealMsgRef.current = newDealMsg; dealMsgRef.current = newDealMsg;
}, [dealMsgRef]); },
[dealMsgRef]
const websocketIdRef = useRef<string>('') );
useEffect(() => { useEffect(() => {
if (!currentService?.id) return; if (!currentService?.id) return;
@@ -64,7 +150,9 @@ export default function useWebSocket({
let unlisten_message = null; let unlisten_message = null;
setErrorShow(false); setErrorShow(false);
unlisten_error = listen(`ws-error-${clientId}`, (event) => {
if (!isTauri) return;
unlisten_error = platformAdapter.listenEvent(`ws-error-${clientId}`, (event) => {
// { // {
// "error": { // "error": {
// "reason": "invalid login" // "reason": "invalid login"
@@ -76,7 +164,7 @@ export default function useWebSocket({
setErrorShow(true); setErrorShow(true);
}); });
unlisten_message = listen(`ws-message-${clientId}`, (event) => { unlisten_message = platformAdapter.listenEvent(`ws-message-${clientId}`, (event) => {
const msg = event.payload as string; const msg = event.payload as string;
console.log(`ws-message-${clientId}`, msg); console.log(`ws-message-${clientId}`, msg);
if (msg.includes("websocket-session-id")) { if (msg.includes("websocket-session-id")) {

View File

@@ -1,8 +1,6 @@
import { useEffect, useCallback } from "react"; import { useState, useEffect, useCallback } from "react";
import { getAllWindows, getCurrentWindow } from "@tauri-apps/api/window";
import { WebviewWindow } from "@tauri-apps/api/webviewWindow"; import platformAdapter from "@/utils/platformAdapter";
import { listen } from "@tauri-apps/api/event";
import { isTauri } from "@tauri-apps/api/core";
const defaultWindowConfig = { const defaultWindowConfig = {
label: "", label: "",
@@ -23,8 +21,20 @@ const defaultWindowConfig = {
}; };
export const useWindows = () => { export const useWindows = () => {
if (!isTauri()) return {} const [appWindow, setAppWindow] = useState<any>(null);
const appWindow = getCurrentWindow();
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 createWin = useCallback(async (options: any) => {
const args = { ...defaultWindowConfig, ...options }; const args = { ...defaultWindowConfig, ...options };
@@ -39,8 +49,9 @@ export const useWindows = () => {
return; return;
} }
const win = new WebviewWindow(args.label, args); const win = await platformAdapter.createWebviewWindow(args.label, args);
if(win) {
win.once("tauri://created", async () => { win.once("tauri://created", async () => {
console.log("tauri://created"); console.log("tauri://created");
// if (args.label.includes("main")) { // if (args.label.includes("main")) {
@@ -53,9 +64,11 @@ export const useWindows = () => {
} }
}); });
win.once("tauri://error", (error) => { win.once("tauri://error", (error: any) => {
console.error("error:", error); console.error("error:", error);
}); });
}
}, []); }, []);
const closeWin = useCallback(async (label: string) => { const closeWin = useCallback(async (label: string) => {
@@ -75,24 +88,24 @@ export const useWindows = () => {
}, []); }, []);
const getWin = useCallback(async (label: string) => { const getWin = useCallback(async (label: string) => {
return WebviewWindow.getByLabel(label); return platformAdapter.getWindowByLabel(label);
}, []); }, []);
const getAllWin = useCallback(async () => { const getAllWin = useCallback(async () => {
return getAllWindows(); return platformAdapter.getAllWindows();
}, []); }, []);
const listenEvents = useCallback(() => { const listenEvents = useCallback(() => {
let unlistenHandlers: { (): void; (): void; (): void; (): void; }[] = []; let unlistenHandlers: { (): void; (): void; (): void; (): void; }[] = [];
const setupListeners = async () => { const setupListeners = async () => {
const winCreateHandler = await listen("win-create", (event) => { const winCreateHandler = await platformAdapter.listenWindowEvent("win-create", (event) => {
console.log(event); console.log(event);
createWin(event.payload); createWin(event.payload);
}); });
unlistenHandlers.push(winCreateHandler); 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; if (!appWindow || !appWindow.label.includes("main")) return;
await appWindow.show(); await appWindow.show();
await appWindow.unminimize(); await appWindow.unminimize();
@@ -100,13 +113,13 @@ export const useWindows = () => {
}); });
unlistenHandlers.push(winShowHandler); 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; if (!appWindow || !appWindow.label.includes("main")) return;
await appWindow.hide(); await appWindow.hide();
}); });
unlistenHandlers.push(winHideHandler); unlistenHandlers.push(winHideHandler);
const winCloseHandler = await listen("win-close", async () => { const winCloseHandler = await platformAdapter.listenWindowEvent("win-close", async () => {
await appWindow.close(); await appWindow.close();
}); });
unlistenHandlers.push(winCloseHandler); unlistenHandlers.push(winCloseHandler);

View File

@@ -147,12 +147,13 @@
"version": "{{version}}", "version": "{{version}}",
"updateAvailable": "Update available", "updateAvailable": "Update available",
"select": "Select", "select": "Select",
"open": "Open" "open": "Open",
"powered": "Powered by Coco AI"
}, },
"input": { "input": {
"searchPlaceholder": "Search whatever you want ...", "searchPlaceholder": "Search whatever you want ...",
"connectionError": "Unable to connect to the server", "connectionError": "Unable to connect to the server",
"reconnect": "Reconnect", "reconnect": "Click here to reconnect.",
"connecting": "Connecting", "connecting": "Connecting",
"deepThink": "Deep Think", "deepThink": "Deep Think",
"search": "Search", "search": "Search",

View File

@@ -147,12 +147,13 @@
"version": "{{version}}", "version": "{{version}}",
"updateAvailable": "有可用更新", "updateAvailable": "有可用更新",
"select": "选择", "select": "选择",
"open": "打开" "open": "打开",
"powered": "由 Coco AI 提供支持"
}, },
"input": { "input": {
"searchPlaceholder": "搜索任何内容...", "searchPlaceholder": "搜索任何内容...",
"connectionError": "无法连接到服务器", "connectionError": "无法连接到服务器",
"reconnect": "重新连接", "reconnect": "点此重连",
"connecting": "连接中", "connecting": "连接中",
"deepThink": "深度思考", "deepThink": "深度思考",
"search": "联网搜索", "search": "联网搜索",

View File

@@ -1,13 +1,17 @@
import { useCallback, useEffect } from "react"; 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 platformAdapter from "@/utils/platformAdapter";
import { useShortcutsStore } from "@/stores/shortcutsStore"; import { useShortcutsStore } from "@/stores/shortcutsStore";
import { useStartupStore } from "@/stores/startupStore"; import { useStartupStore } from "@/stores/startupStore";
import { useKeyPress } from "ahooks";
import { modifierKeys } from "@/components/Settings/Advanced/components/Shortcuts"; import { modifierKeys } from "@/components/Settings/Advanced/components/Shortcuts";
import { useAppStore } from "@/stores/appStore";
function MainApp() { function MainApp() {
const setIsTauri = useAppStore((state) => state.setIsTauri);
setIsTauri(true);
const querySearch = useCallback(async (input: string) => { const querySearch = useCallback(async (input: string) => {
try { try {
const response: any = await platformAdapter.invokeBackend( const response: any = await platformAdapter.invokeBackend(
@@ -44,6 +48,11 @@ function MainApp() {
}, },
[] []
); );
const hideCoco = useCallback(() => {
return platformAdapter.hideWindow();
}, []);
const modifierKey = useShortcutsStore((state) => { const modifierKey = useShortcutsStore((state) => {
return state.modifierKey; return state.modifierKey;
}); });
@@ -129,7 +138,13 @@ function MainApp() {
); );
return ( return (
<SearchChat querySearch={querySearch} queryDocuments={queryDocuments} /> <SearchChat
isTauri={true}
querySearch={querySearch}
queryDocuments={queryDocuments}
hideCoco={hideCoco}
hasModules={["search", "chat"]}
/>
); );
} }

View File

@@ -8,7 +8,7 @@ import SettingsPanel from "@/components/Settings/SettingsPanel";
import GeneralSettings from "@/components/Settings/GeneralSettings"; import GeneralSettings from "@/components/Settings/GeneralSettings";
import AboutView from "@/components/Settings/AboutView"; import AboutView from "@/components/Settings/AboutView";
import Cloud from "@/components/Cloud/Cloud.tsx"; 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 { useTray } from "@/hooks/useTray";
import Advanced from "@/components/Settings/Advanced"; import Advanced from "@/components/Settings/Advanced";

100
src/pages/web/README.md Normal file
View File

@@ -0,0 +1,100 @@
# SearchChat Web Component API
## Props
### `serverUrl`
- **类型**: `string`
- **可选**: 是
- **默认值**: `""`
- **描述**: 设置服务器地址
### `headers`
- **类型**: `Record<string, unknown>`
- **可选**: 是
- **默认值**: `{}`
- **描述**: 请求头配置
### `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 (
<SearchChat
serverUrl=""
headers={{}}
width={680}
height={590}
hasModules={['search', 'chat']}
hasFeature={['think', 'search', 'think_active', 'search_active']}
hideCoco={() => console.log('hide')}
theme="dark"
searchPlaceholder=""
chatPlaceholder=""
showChatHistory={true}
setIsPinned={(isPinned) => console.log('isPinned:', isPinned)}
/>
);
}
```

View File

@@ -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<any>;
queryDocuments: (
from: number,
size: number,
queryStrings: any
) => Promise<any>;
}
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<ChatAIRef>(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 = () => (
<div className="flex items-center justify-center h-full">loading...</div>
);
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<DataSource[]> => {
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 (
<ErrorBoundary>
<div
data-tauri-drag-region
className={clsx(
"size-full m-auto overflow-hidden relative bg-no-repeat bg-cover bg-center",
[
isTransitioned
? "bg-chat_bg_light dark:bg-chat_bg_dark"
: "bg-search_bg_light dark:bg-search_bg_dark",
],
{
"rounded-xl": !isWin,
"border border-[#E6E6E6] dark:border-[#272626]": isLinux,
}
)}
>
<div
data-tauri-drag-region
className={`p-2 pb-0 absolute w-full flex items-center justify-center transition-all duration-500 ${
isTransitioned
? "top-[calc(100vh-90px)] h-[90px] border-t"
: "top-0 h-[90px] border-b"
} border-[#E6E6E6] dark:border-[#272626]`}
>
<InputBox
isChatMode={isChatMode}
inputValue={input}
onSend={handleSendMessage}
disabled={isTyping}
disabledChange={cancelChat}
changeMode={changeMode}
changeInput={setInput}
reconnect={reconnect}
isSearchActive={isSearchActive}
setIsSearchActive={toggleSearchActive}
isDeepThinkActive={isDeepThinkActive}
setIsDeepThinkActive={toggleDeepThinkActive}
getDataSourcesByServer={getDataSourcesByServer}
setupWindowFocusListener={setupWindowFocusListener}
checkScreenPermission={checkScreenPermission}
requestScreenPermission={requestScreenPermission}
getScreenMonitors={getScreenMonitors}
getScreenWindows={getScreenWindows}
captureMonitorScreenshot={captureMonitorScreenshot}
captureWindowScreenshot={captureWindowScreenshot}
openFileDialog={openFileDialog}
getFileMetadata={getFileMetadata}
getFileIcon={getFileIcon}
/>
</div>
<div
data-tauri-drag-region
className={`absolute w-full transition-opacity duration-500 ${
isTransitioned ? "opacity-0 pointer-events-none" : "opacity-100"
} bottom-0 h-[calc(100vh-90px)] `}
>
<Suspense fallback={<LoadingFallback />}>
<Search
key="Search"
input={input}
isChatMode={isChatMode}
changeInput={setInput}
querySearch={querySearch}
queryDocuments={queryDocuments}
hideCoco={hideCoco}
openSetting={openSetting}
setWindowAlwaysOnTop={setWindowAlwaysOnTop}
/>
</Suspense>
</div>
<div
data-tauri-drag-region
className={`absolute w-full transition-all duration-500 select-auto ${
isTransitioned
? "top-0 opacity-100 pointer-events-auto"
: "-top-[506px] opacity-0 pointer-events-none"
} h-[calc(100vh-90px)]`}
>
{isTransitioned && isChatMode ? (
<Suspense fallback={<LoadingFallback />}>
<ChatAI
ref={chatAIRef}
key="ChatAI"
isTransitioned={isTransitioned}
changeInput={setInput}
isSearchActive={isSearchActive}
isDeepThinkActive={isDeepThinkActive}
getFileUrl={getFileUrl}
/>
</Suspense>
) : null}
</div>
<UpdateApp checkUpdate={checkUpdate} relaunchApp={relaunchApp} />
</div>
</ErrorBoundary>
);
}
export default memo(SearchChat);

View File

@@ -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<string, unknown>;
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) => { const querySearch = useCallback(async (input: string) => {
console.log(input); console.log(input);
return await query_coco_fusion(`/query/_search?query=${input}`);
}, []); }, []);
const queryDocuments = useCallback( const queryDocuments = useCallback(
async (from: number, size: number, queryStrings: any) => { async (from: number, size: number, queryStrings: any) => {
console.log(from, size, queryStrings); 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 ( return (
<div className="w-[680px] h-[590px]"> <div
<SearchChat querySearch={querySearch} queryDocuments={queryDocuments} /> id="searchChat-container"
className={`coco-container ${theme}`}
data-theme={theme}
style={{ width: `${width}px`, height: `${height}px` }}
>
<SearchChat
isTauri={false}
hideCoco={hideCoco}
hasModules={hasModules}
defaultModule={defaultModule}
hasFeature={hasFeature}
theme={theme}
searchPlaceholder={searchPlaceholder}
chatPlaceholder={chatPlaceholder}
querySearch={querySearch}
queryDocuments={queryDocuments}
showChatHistory={showChatHistory}
setIsPinned={setIsPinned}
/>
</div> </div>
); );
} }

View File

@@ -52,6 +52,9 @@ export type IAppStore = {
showCocoShortcuts: string[]; showCocoShortcuts: string[];
setShowCocoShortcuts: (showCocoShortcuts: string[]) => void; setShowCocoShortcuts: (showCocoShortcuts: string[]) => void;
isTauri: boolean;
setIsTauri: (isTauri: boolean) => void;
visible: boolean; visible: boolean;
withVisibility: <T>(fn: () => Promise<T>) => Promise<T>; withVisibility: <T>(fn: () => Promise<T>) => Promise<T>;
}; };
@@ -107,6 +110,8 @@ export const useAppStore = create<IAppStore>()(
return set({ showCocoShortcuts }); return set({ showCocoShortcuts });
}, },
isTauri: true,
setIsTauri: (isTauri: boolean) => set({ isTauri }),
visible: false, visible: false,
withVisibility: async <T>(fn: () => Promise<T>) => { withVisibility: async <T>(fn: () => Promise<T>) => {
set({ visible: true }); set({ visible: true });

View File

@@ -1,7 +1,8 @@
import { create } from "zustand"; import { create } from "zustand";
import { persist } from "zustand/middleware"; import { persist } from "zustand/middleware";
import { produce } from "immer"; 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 CONNECTOR_CHANGE_EVENT = "connector_data_change";
const DATASOURCE_CHANGE_EVENT = "datasourceData_change"; const DATASOURCE_CHANGE_EVENT = "datasourceData_change";
@@ -53,7 +54,7 @@ export const useConnectStore = create<IConnectStore>()(
draft.connector_data[key] = connector_data; draft.connector_data[key] = connector_data;
}) })
); );
await emit(CONNECTOR_CHANGE_EVENT, { await platformAdapter.emitEvent(CONNECTOR_CHANGE_EVENT, {
connector_data, connector_data,
}); });
}, },
@@ -64,16 +65,16 @@ export const useConnectStore = create<IConnectStore>()(
draft.datasourceData[key] = datasourceData; draft.datasourceData[key] = datasourceData;
}) })
); );
await emit(DATASOURCE_CHANGE_EVENT, { await platformAdapter.emitEvent(DATASOURCE_CHANGE_EVENT, {
datasourceData, datasourceData,
}); });
}, },
initializeListeners: () => { initializeListeners: () => {
listen(CONNECTOR_CHANGE_EVENT, (event: any) => { platformAdapter.listenEvent(CONNECTOR_CHANGE_EVENT, (event: any) => {
const { connector_data } = event.payload; const { connector_data } = event.payload;
set({ connector_data }); set({ connector_data });
}); });
listen(DATASOURCE_CHANGE_EVENT, (event: any) => { platformAdapter.listenEvent(DATASOURCE_CHANGE_EVENT, (event: any) => {
const { datasourceData } = event.payload; const { datasourceData } = event.payload;
set({ datasourceData }); set({ datasourceData });
}); });

View File

@@ -1,4 +1,3 @@
import { Update } from "@tauri-apps/plugin-updater";
import { create } from "zustand"; import { create } from "zustand";
import { persist } from "zustand/middleware"; import { persist } from "zustand/middleware";
@@ -9,8 +8,8 @@ export type IUpdateStore = {
setSkipVersion: (skipVersion?: string) => void; setSkipVersion: (skipVersion?: string) => void;
isOptional: boolean; isOptional: boolean;
setIsOptional: (isOptional: boolean) => void; setIsOptional: (isOptional: boolean) => void;
updateInfo?: Update; updateInfo?: any;
setUpdateInfo: (updateInfo?: Update) => void; setUpdateInfo: (updateInfo?: any) => void;
}; };
export const useUpdateStore = create<IUpdateStore>()( export const useUpdateStore = create<IUpdateStore>()(
@@ -27,7 +26,7 @@ export const useUpdateStore = create<IUpdateStore>()(
setIsOptional: (isOptional: boolean) => { setIsOptional: (isOptional: boolean) => {
return set({ isOptional }); return set({ isOptional });
}, },
setUpdateInfo: (updateInfo?: Update) => { setUpdateInfo: (updateInfo?: any) => {
return set({ updateInfo }); return set({ updateInfo });
}, },
}), }),

View File

@@ -1,8 +1,8 @@
import { useEffect, useState } from "react"; 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 { hide_coco } from "@/commands"
import { useAppStore } from "@/stores/appStore";
import platformAdapter from "./platformAdapter";
// 1 // 1
export async function copyToClipboard(text: string) { export async function copyToClipboard(text: string) {
@@ -65,10 +65,11 @@ export const IsTauri = () => {
export const OpenURLWithBrowser = async (url: string) => { export const OpenURLWithBrowser = async (url: string) => {
if (!url) return; if (!url) return;
if (isTauri()) { if (IsTauri()) {
try { try {
await open(url); await open(url);
await hide_coco(); await hide_coco();
await platformAdapter.invokeBackend("hide_coco");
console.log("URL opened in default browser"); console.log("URL opened in default browser");
} catch (error) { } catch (error) {
console.error("Failed to open URL:", error); console.error("Failed to open URL:", error);

View File

@@ -1,7 +1,6 @@
import { useState } from "react"; import { createWebAdapter } from './webAdapter';
import { isTauri } from "@tauri-apps/api/core"; // import { createTauriAdapter } from './tauriAdapter';
import { convertFileSrc as tauriConvertFileSrc } from "@tauri-apps/api/core";
import type { OpenDialogOptions } from "@tauri-apps/plugin-dialog";
import { IShortcutsStore } from "@/stores/shortcutsStore"; import { IShortcutsStore } from "@/stores/shortcutsStore";
import { IStartupStore } from "@/stores/startupStore"; import { IStartupStore } from "@/stores/startupStore";
@@ -29,8 +28,19 @@ export interface EventPayloads {
open_settings: string | ""; open_settings: string | "";
tab_index: string | ""; tab_index: string | "";
login_or_logout: any; 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 "change-startup-store": IStartupStore
"show-coco": void;
"change-shortcuts-store": IShortcutsStore; "change-shortcuts-store": IShortcutsStore;
} }
@@ -40,7 +50,6 @@ export interface PlatformAdapter {
setWindowSize: (width: number, height: number) => Promise<void>; setWindowSize: (width: number, height: number) => Promise<void>;
hideWindow: () => Promise<void>; hideWindow: () => Promise<void>;
showWindow: () => Promise<void>; showWindow: () => Promise<void>;
isPlatformTauri: () => boolean;
convertFileSrc: (path: string) => string; convertFileSrc: (path: string) => string;
emitEvent: (event: string, payload?: any) => Promise<void>; emitEvent: (event: string, payload?: any) => Promise<void>;
listenEvent: <K extends keyof EventPayloads>( listenEvent: <K extends keyof EventPayloads>(
@@ -54,9 +63,9 @@ export interface PlatformAdapter {
getScreenshotableWindows: () => Promise<any[]>; getScreenshotableWindows: () => Promise<any[]>;
captureMonitorScreenshot: (id: number) => Promise<string>; captureMonitorScreenshot: (id: number) => Promise<string>;
captureWindowScreenshot: (id: number) => Promise<string>; captureWindowScreenshot: (id: number) => Promise<string>;
openFileDialog: ( openFileDialog: (options: {
options: OpenDialogOptions multiple: boolean;
) => Promise<string | string[] | null>; }) => Promise<string | string[] | null>;
getFileMetadata: (path: string) => Promise<any>; getFileMetadata: (path: string) => Promise<any>;
getFileIcon: (path: string, size: number) => Promise<string>; getFileIcon: (path: string, size: number) => Promise<string>;
checkUpdate: () => Promise<any>; checkUpdate: () => Promise<any>;
@@ -72,406 +81,18 @@ export interface PlatformAdapter {
show: () => Promise<void>; show: () => Promise<void>;
setFocus: () => Promise<void>; setFocus: () => Promise<void>;
center: () => Promise<void>; center: () => Promise<void>;
close: () => Promise<void>;
} | null>; } | null>;
createWindow: (label: string, options: any) => Promise<void>; createWindow: (label: string, options: any) => Promise<void>;
getAllWindows: () => Promise<any[]>;
getCurrentWindow: () => Promise<any>;
createWebviewWindow: (label: string, options: any) => Promise<any>;
listenWindowEvent: (event: string, callback: (event: any) => void) => Promise<() => void>;
isTauri: () => boolean;
openExternal: (url: string) => Promise<void>;
} }
// Create Tauri adapter functions
export const createTauriAdapter = (): PlatformAdapter => {
return {
async invokeBackend(command: string, args?: any): Promise<any> {
if (isTauri()) {
const { invoke } = await import("@tauri-apps/api/core");
return invoke(command, args);
}
return null;
},
async setWindowSize(width: number, height: number): Promise<void> {
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<void> {
if (isTauri()) {
const { invoke } = await import("@tauri-apps/api/core");
await invoke("hide_coco");
}
},
async showWindow(): Promise<void> {
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<K extends keyof EventPayloads>(
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<any> {
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<void> {
console.log(`Web mode simulated window resize: ${width}x${height}`);
// No actual operation needed in web environment
},
async hideWindow(): Promise<void> {
console.log("Web mode simulated window hide");
// No actual operation needed in web environment
},
async showWindow(): Promise<void> {
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<void> {
console.log("Web mode simulated event emit", event, payload);
},
async listenEvent<K extends keyof EventPayloads>(
event: K,
_callback: (event: { payload: EventPayloads[K] }) => void
): Promise<() => void> {
console.log("Web mode simulated event listen", event);
return () => {};
},
async setAlwaysOnTop(isPinned: boolean): Promise<void> {
console.log("Web mode simulated set always on top", isPinned);
},
async checkScreenRecordingPermission(): Promise<boolean> {
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<any[]> {
console.log("Web mode simulated get screenshotable monitors");
return [];
},
async getScreenshotableWindows(): Promise<any[]> {
console.log("Web mode simulated get screenshotable windows");
return [];
},
async captureMonitorScreenshot(id: number): Promise<string> {
console.log("Web mode simulated capture monitor screenshot", id);
return "";
},
async captureWindowScreenshot(id: number): Promise<string> {
console.log("Web mode simulated capture window screenshot", id);
return "";
},
async openFileDialog(options: OpenDialogOptions): Promise<null> {
console.log("Web mode simulated open file dialog", options);
return null;
},
async getFileMetadata(path: string): Promise<null> {
console.log("Web mode simulated get file metadata", path);
return null;
},
async getFileIcon(path: string, size: number): Promise<string> {
console.log("Web mode simulated get file icon", path, size);
return "";
},
async checkUpdate(): Promise<any> {
console.log("Web mode simulated check update");
return null;
},
async relaunchApp(): Promise<void> {
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 // Default adapter instance
const platformAdapter = createPlatformAdapter(); const platformAdapter: PlatformAdapter = typeof window !== 'undefined' ? createWebAdapter() : {} as PlatformAdapter;
export default platformAdapter; export default platformAdapter;
// Custom hook for using platform adapter
export const usePlatformAdapter = () => {
const [adapter] = useState<PlatformAdapter>(platformAdapter);
return adapter;
};

188
src/utils/tauriAdapter.ts Normal file
View File

@@ -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<any> {
const { invoke } = await import("@tauri-apps/api/core");
return invoke(command, args);
},
async setWindowSize(width: number, height: number): Promise<void> {
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<void> {
const { invoke } = await import("@tauri-apps/api/core");
await invoke("hide_coco");
},
async showWindow(): Promise<void> {
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<K extends keyof EventPayloads>(
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<any[]> {
const { getAllWindows } = await import("@tauri-apps/api/window");
return getAllWindows();
},
async getCurrentWindow(): Promise<any> {
const { getCurrentWindow } = await import("@tauri-apps/api/window");
return getCurrentWindow();
},
async createWebviewWindow(label: string, options: any): Promise<any> {
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<void> {
const { open } = await import("@tauri-apps/plugin-shell");
return open(url);
},
};
};

161
src/utils/webAdapter.ts Normal file
View File

@@ -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<any> {
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<void> {
console.log(`Web mode simulated window resize: ${width}x${height}`);
// No actual operation needed in web environment
},
async hideWindow(): Promise<void> {
console.log("Web mode simulated window hide");
// No actual operation needed in web environment
},
async showWindow(): Promise<void> {
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<void> {
console.log("Web mode simulated event emit", event, payload);
},
async listenEvent<K extends keyof EventPayloads>(
event: K,
_callback: (event: { payload: EventPayloads[K] }) => void
): Promise<() => void> {
console.log("Web mode simulated event listen", event);
return () => { };
},
async setAlwaysOnTop(isPinned: boolean): Promise<void> {
console.log("Web mode simulated set always on top", isPinned);
},
async checkScreenRecordingPermission(): Promise<boolean> {
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<any[]> {
console.log("Web mode simulated get screenshotable monitors");
return [];
},
async getScreenshotableWindows(): Promise<any[]> {
console.log("Web mode simulated get screenshotable windows");
return [];
},
async captureMonitorScreenshot(id: number): Promise<string> {
console.log("Web mode simulated capture monitor screenshot", id);
return "";
},
async captureWindowScreenshot(id: number): Promise<string> {
console.log("Web mode simulated capture window screenshot", id);
return "";
},
async openFileDialog(options: { multiple: boolean }): Promise<null> {
console.log("Web mode simulated open file dialog", options);
return null;
},
async getFileMetadata(path: string): Promise<null> {
console.log("Web mode simulated get file metadata", path);
return null;
},
async getFileIcon(path: string, size: number): Promise<string> {
console.log("Web mode simulated get file icon", path, size);
return "";
},
async checkUpdate(): Promise<any> {
console.log("Web mode simulated check update");
return null;
},
async relaunchApp(): Promise<void> {
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<any[]> {
console.log("Web mode simulated get all windows");
return [];
},
async getCurrentWindow(): Promise<any> {
console.log("Web mode simulated get current window");
return null;
},
async createWebviewWindow(label: string, options: any): Promise<any> {
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<void> {
console.log(`Web mode opening URL: ${url}`);
window.open(url, '_blank');
},
};
};

729
src/web.css Normal file
View File

@@ -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;
}
}

View File

@@ -1,6 +1,7 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
export default { 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: { theme: {
extend: { extend: {
backgroundColor: { backgroundColor: {

View File

@@ -3,18 +3,22 @@ import { writeFileSync, readFileSync } from 'fs';
import { join, resolve } from 'path'; import { join, resolve } from 'path';
export default defineConfig({ export default defineConfig({
entry: ['src/pages/web/SearchChat.tsx'], entry: ['src/pages/web/index.tsx'],
format: ['esm', 'cjs'], format: ['esm'],
dts: true, dts: true,
splitting: false, splitting: true,
sourcemap: true, sourcemap: false,
clean: true, clean: true,
treeshake: true,
minify: true,
env: {
BUILD_TARGET: 'web',
NODE_ENV: 'production',
},
external: [ external: [
'react', 'react',
'react-dom', 'react-dom',
], ],
treeshake: true,
minify: true,
esbuildOptions(options) { esbuildOptions(options) {
options.bundle = true; options.bundle = true;
options.platform = 'browser'; options.platform = 'browser';
@@ -28,6 +32,22 @@ export default defineConfig({
options.alias = { options.alias = {
'@': resolve(__dirname, './src') '@': 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: [ esbuildPlugins: [
{ {
@@ -38,36 +58,65 @@ export default defineConfig({
}, },
}, },
], ],
outDir: 'dist/search-chat', outDir: 'out/search-chat',
async onSuccess() { async onSuccess() {
const projectPackageJson = JSON.parse( const projectPackageJson = JSON.parse(
readFileSync(join(__dirname, 'package.json'), 'utf-8') readFileSync(join(__dirname, 'package.json'), 'utf-8')
); );
const packageJson = { const packageJson = {
name: "search-chat", name: "@infinilabs/search-chat",
version: "1.0.0", version: "1.0.10",
main: "SearchChat.cjs", main: "index.js",
module: "SearchChat.js", module: "index.js",
types: "SearchChat.d.ts", type: "module",
types: "index.d.ts",
dependencies: projectPackageJson.dependencies, dependencies: projectPackageJson.dependencies,
peerDependencies: { peerDependencies: {
"react": "^18.0.0", "react": "^18.0.0",
"react-dom": "^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 => const tauriDeps = Object.keys(packageJson.dependencies).filter(dep =>
dep.includes('@tauri-apps') || dep.includes('@tauri-apps') ||
dep.includes('tauri-plugin') dep.includes('tauri-plugin') ||
noNeedDeps.includes(dep)
); );
tauriDeps.forEach(dep => { tauriDeps.forEach(dep => {
delete packageJson.dependencies[dep]; delete packageJson.dependencies[dep];
}); });
writeFileSync( writeFileSync(
join(__dirname, 'dist/search-chat/package.json'), join(__dirname, 'out/search-chat/package.json'),
JSON.stringify(packageJson, null, 2) 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);
}
} }
}); });

View File

@@ -56,6 +56,11 @@ export default defineConfig(async () => ({
changeOrigin: true, changeOrigin: true,
secure: false, secure: false,
}, },
"/integration": {
target: process.env.COCO_SERVER_URL,
changeOrigin: true,
secure: false,
},
}, },
}, },
build: { build: {