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",
"VITE",
"walkdir",
"wavesurfer",
"webviews",
"xzvf",
"yuque",

View File

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

View File

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

169
pnpm-lock.yaml generated
View File

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

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

View File

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

View File

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

View File

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

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

View File

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

View File

@@ -2,21 +2,24 @@ import { ArrowDown01, Command, CornerDownLeft } from "lucide-react";
import { useTranslation } from "react-i18next";
import clsx from "clsx";
import logoImg from "@/assets/icon.svg";
import { useSearchStore } from "@/stores/searchStore";
import TypeIcon from "@/components/Common/Icons/TypeIcon";
import { useAppStore } from "@/stores/appStore";
import Copyright from "@/components/Common/Copyright";
import { isMac } from "@/utils/platform";
import PinOffIcon from "@/icons/PinOff";
import PinIcon from "@/icons/Pin";
import logoImg from "@/assets/icon.svg";
import { useAppStore } from "@/stores/appStore";
import { useSearchStore } from "@/stores/searchStore";
import { useUpdateStore } from "@/stores/updateStore";
interface FooterProps {
isTauri: boolean;
openSetting: () => void;
setWindowAlwaysOnTop: (isPinned: boolean) => Promise<void>;
}
export default function Footer({
isTauri,
openSetting,
setWindowAlwaysOnTop,
}: FooterProps) {
@@ -41,46 +44,53 @@ export default function Footer({
return (
<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"
>
<div className="flex items-center">
<div className="flex items-center space-x-2">
{sourceData?.source?.name ? (
<TypeIcon item={sourceData} className="w-4 h-4" />
) : (
<img
src={logoImg}
className="w-4 h-4 cursor-pointer"
onClick={openSetting}
alt={t("search.footer.logoAlt")}
/>
)}
<div className="relative text-xs text-gray-500 dark:text-gray-400">
{updateInfo?.available ? (
<div className="cursor-pointer" onClick={() => setVisible(true)}>
<span>{t("search.footer.updateAvailable")}</span>
<span className="absolute top-0 -right-2 size-1.5 bg-[#FF3434] rounded-full"></span>
</div>
{isTauri ? (
<div className="flex items-center">
<div className="flex items-center space-x-2">
{sourceData?.source?.name ? (
<TypeIcon item={sourceData} className="w-4 h-4" />
) : (
sourceData?.source?.name ||
t("search.footer.version", {
version: process.env.VERSION || "v1.0.0",
})
<img
src={logoImg}
className="w-4 h-4 cursor-pointer"
onClick={openSetting}
alt={t("search.footer.logoAlt")}
/>
)}
</div>
<div className="relative text-xs text-gray-500 dark:text-gray-400">
{updateInfo?.available ? (
<div
className="cursor-pointer"
onClick={() => setVisible(true)}
>
<span>{t("search.footer.updateAvailable")}</span>
<span className="absolute top-0 -right-2 size-1.5 bg-[#FF3434] rounded-full"></span>
</div>
) : (
sourceData?.source?.name ||
t("search.footer.version", {
version: process.env.VERSION || "v1.0.0",
})
)}
</div>
<button
onClick={togglePin}
className={clsx({
"text-blue-500": isPinned,
"pl-2": updateInfo?.available,
})}
>
{isPinned ? <PinIcon /> : <PinOffIcon />}
</button>
<button
onClick={togglePin}
className={clsx({
"text-blue-500": isPinned,
"pl-2": updateInfo?.available,
})}
>
{isPinned ? <PinIcon /> : <PinOffIcon />}
</button>
</div>
</div>
</div>
) : (
<Copyright />
)}
<div className="flex items-center gap-3">
<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 { isMac } from "@/utils/platform";
import { useShortcutsStore } from "@/stores/shortcutsStore";
import noDataImg from "@/assets/coconut-tree.png";
export const NoResults = () => {
const { t } = useTranslation();
const modeSwitch = useShortcutsStore((state) => state.modeSwitch);
return (
<div
data-tauri-drag-region
@@ -30,7 +33,7 @@ export const NoResults = () => {
</span>
)}
<span className="ml-1 w-5 h-5 rounded-[6px] border border-[#D8D8D8] flex justify-center items-center">
T
{modeSwitch}
</span>
</div>
</div>

View File

@@ -1,49 +1,49 @@
import {Menu, MenuButton,} from "@headlessui/react";
import { Menu, MenuButton } from "@headlessui/react";
import { OctagonAlert, X } from "lucide-react";
// import { Settings, LogOut, User, ChevronUp, Home } from "lucide-react";
// import { Link } from "react-router-dom";
import logoImg from "../assets/icon.svg";
import {useAppStore} from "@/stores/appStore";
import {OctagonAlert, X} from 'lucide-react';
import logoImg from "@/assets/icon.svg";
import { useAppStore } from "@/stores/appStore";
const Footer = () => {
const error = useAppStore((state) => state.error);
const setError = useAppStore((state) => state.setError);
const error = useAppStore((state) => state.error);
const setError = useAppStore((state) => state.setError);
return (
<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 */}
{error && (
<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">
<div className="flex items-center space-x-4">
<OctagonAlert size={32} color="red" className="mr-2" />
<span className="text-xs text-red-500 dark:text-red-400 flex-1">
{error}
</span>
<X
className="cursor-pointer ml-2"
onClick={() => setError("")}
size={32}
color="gray"
/>
</div>
</div>
)}
return (
<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 */}
{error && (
<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">
<div className="flex items-center space-x-4">
<OctagonAlert size={32} color="red" className="mr-2"/>
<span className="text-xs text-red-500 dark:text-red-400 flex-1">{error}</span>
<X
className="cursor-pointer ml-2"
onClick={() => setError("")}
size={32}
color="gray"
/>
</div>
</div>
)}
<div className="max-w-6xl mx-auto px-4 h-8 flex items-center justify-between">
<Menu as="div" className="relative">
<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">
<img
src={logoImg}
className="w-5 h-5 text-gray-600 dark:text-gray-400"
/>
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
<div className="max-w-6xl mx-auto px-4 h-8 flex items-center justify-between">
<Menu as="div" className="relative">
<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">
<img
src={logoImg}
className="w-5 h-5 text-gray-600 dark:text-gray-400"
/>
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
Coco
</span>
{/* <ChevronUp className="w-4 h-4 text-gray-500 dark:text-gray-400" /> */}
</MenuButton>
{/* <ChevronUp className="w-4 h-4 text-gray-500 dark:text-gray-400" /> */}
</MenuButton>
{/* <MenuItems className="absolute bottom-full mb-2 left-0 w-64 rounded-lg bg-white dark:bg-gray-800 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
{/* <MenuItems className="absolute bottom-full mb-2 left-0 w-64 rounded-lg bg-white dark:bg-gray-800 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
<div className="p-1">
<MenuItem>
{({ active }) => (
@@ -104,21 +104,20 @@ const Footer = () => {
</MenuItem>
</div>
</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">
Version {process.env.VERSION || "v1.0.0"}
</span>
{/* <div className="h-4 w-px bg-gray-200 dark:bg-gray-700" />
{/* <div className="h-4 w-px bg-gray-200 dark:bg-gray-700" />
<button className="text-xs text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white transition-colors">
Check for Updates
</button> */}
</div>
</div>
</div>
);
</div>
</div>
);
};
export default Footer;

View File

@@ -6,13 +6,14 @@ interface AutoResizeTextareaProps {
setInput: (value: string) => void;
handleKeyDown?: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void;
connected: boolean;
chatPlaceholder?: string;
}
// Forward ref to allow parent to interact with this component
const AutoResizeTextarea = forwardRef<
{ reset: () => void; focus: () => void },
AutoResizeTextareaProps
>(({ input, setInput, handleKeyDown, connected }, ref) => {
>(({ input, setInput, handleKeyDown, connected, chatPlaceholder }, ref) => {
const { t } = useTranslation();
const textareaRef = useRef<HTMLTextAreaElement>(null);
@@ -34,8 +35,10 @@ const AutoResizeTextarea = forwardRef<
autoCapitalize="none"
spellCheck="false"
className="text-base flex-1 outline-none min-w-[200px] text-[#333] dark:text-[#d8d8d8] placeholder-text-xs placeholder-[#999] dark:placeholder-gray-500 bg-transparent"
placeholder={connected ? t('search.textarea.placeholder') : ""}
aria-label={t('search.textarea.ariaLabel')}
placeholder={
connected ? chatPlaceholder || t("search.textarea.placeholder") : ""
}
aria-label={t("search.textarea.ariaLabel")}
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => handleKeyDown?.(e)}

View File

@@ -20,7 +20,7 @@ interface State {
}
interface ContextMenuProps {
hideCoco: () => Promise<void>;
hideCoco?: () => void;
}
const ContextMenu = ({ hideCoco }: ContextMenuProps) => {
@@ -54,7 +54,7 @@ const ContextMenu = ({ hideCoco }: ContextMenuProps) => {
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 { useTranslation } from "react-i18next";
import clsx from "clsx";
import { useKeyPress } from "ahooks";
import ChatSwitch from "@/components/Common/ChatSwitch";
import AutoResizeTextarea from "./AutoResizeTextarea";
@@ -12,12 +13,11 @@ import { useSearchStore } from "@/stores/searchStore";
import { metaOrCtrlKey } from "@/utils/keyboardUtils";
import SearchPopover from "./SearchPopover";
// import AudioRecording from "../AudioRecording";
import { hide_coco } from "@/commands";
import { DataSource } from "@/types/commands";
// import InputExtra from "./InputExtra";
// import { useConnectStore } from "@/stores/connectStore";
import { useShortcutsStore } from "@/stores/shortcutsStore";
import { useKeyPress } from "ahooks";
import Copyright from "@/components/Common/Copyright";
interface ChatInputProps {
onSend: (message: string) => void;
@@ -46,6 +46,11 @@ interface ChatInputProps {
}) => Promise<string | string[] | null>;
getFileMetadata: (path: string) => Promise<any>;
getFileIcon: (path: string, size: number) => Promise<string>;
hideCoco?: () => void;
hasFeature?: string[];
hasModules?: string[];
searchPlaceholder?: string;
chatPlaceholder?: string;
}
export default function ChatInput({
@@ -64,16 +69,12 @@ export default function ChatInput({
isChatPage = false,
getDataSourcesByServer,
setupWindowFocusListener,
}: // checkScreenPermission,
// requestScreenPermission,
// getScreenMonitors,
// getScreenWindows,
// captureMonitorScreenshot,
// captureWindowScreenshot,
// openFileDialog,
// getFileMetadata,
// getFileIcon,
ChatInputProps) {
hasFeature = ["think", "search", "think_icon", "search_icon"],
hideCoco,
hasModules = [],
searchPlaceholder,
chatPlaceholder,
}: ChatInputProps) {
const { t } = useTranslation();
const showTooltip = useAppStore(
@@ -156,6 +157,7 @@ ChatInputProps) {
const handleSubmit = useCallback(() => {
const trimmedValue = inputValue.trim();
console.log("handleSubmit", trimmedValue, disabled);
if (trimmedValue && !disabled) {
changeInput("");
onSend(trimmedValue);
@@ -168,7 +170,7 @@ ChatInputProps) {
if (inputValue) {
changeInput("");
} else if (!isPinned) {
hide_coco();
hideCoco && hideCoco();
}
}, [inputValue, isPinned]);
@@ -295,12 +297,17 @@ ChatInputProps) {
changeInput(value);
}}
connected={connected}
handleKeyDown={(e) => {
handleKeyDown={(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === "Enter") {
if (e.nativeEvent.isComposing) {
return;
}
console.log("handleKeyDown", e.nativeEvent.isComposing);
e.preventDefault();
handleSubmit();
}
}}
chatPlaceholder={chatPlaceholder}
/>
) : (
<input
@@ -311,7 +318,9 @@ ChatInputProps) {
autoCapitalize="none"
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"
placeholder={t("search.input.searchPlaceholder")}
placeholder={
searchPlaceholder || t("search.input.searchPlaceholder")
}
value={inputValue}
onChange={(e) => {
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">
{t("search.input.connectionError")}
<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={() => {
reconnect();
setReconnectCountdown(10);
@@ -424,38 +433,47 @@ ChatInputProps) {
/>
)} */}
<button
className={clsx(
"flex items-center gap-1 p-1 h-6 rounded-lg transition hover:bg-[#EDEDED] dark:hover:bg-[#202126]",
{
"!bg-[rgba(0,114,255,0.3)]": isDeepThinkActive,
}
)}
onClick={DeepThinkClick}
>
<Brain
className={`size-4 ${
isDeepThinkActive
? "text-[#0072FF] dark:text-[#0072FF]"
: "text-[#333] dark:text-white"
}`}
/>
{isDeepThinkActive && (
<span
className={
isDeepThinkActive ? "text-[#0072FF]" : "dark:text-white"
{hasFeature.includes("think") && (
<button
className={clsx(
"flex items-center gap-1 p-1 h-6 rounded-lg transition hover:bg-[#EDEDED] dark:hover:bg-[#202126]",
{
"!bg-[rgba(0,114,255,0.3)]": isDeepThinkActive,
}
>
{t("search.input.deepThink")}
</span>
)}
</button>
)}
onClick={DeepThinkClick}
>
<Brain
className={`size-4 ${
isDeepThinkActive
? "text-[#0072FF] dark:text-[#0072FF]"
: "text-[#333] dark:text-white"
}`}
/>
{isDeepThinkActive && (
<span
className={
isDeepThinkActive ? "text-[#0072FF]" : "dark:text-white"
}
>
{t("search.input.deepThink")}
</span>
)}
</button>
)}
<SearchPopover
isSearchActive={isSearchActive}
setIsSearchActive={setIsSearchActive}
getDataSourcesByServer={getDataSourcesByServer}
/>
{hasFeature.includes("search") && (
<SearchPopover
isSearchActive={isSearchActive}
setIsSearchActive={setIsSearchActive}
getDataSourcesByServer={getDataSourcesByServer}
/>
)}
{!hasFeature.includes("search") && !hasFeature.includes("think") ? (
<div className="px-2">
<Copyright />
</div>
) : null}
</div>
) : (
<div
@@ -464,7 +482,7 @@ ChatInputProps) {
></div>
)}
{isChatPage ? null : (
{isChatPage || hasModules?.length !== 2 ? null : (
<div className="relative w-16 flex justify-end items-center">
{showTooltip && modifierKeyPressed ? (
<div

View File

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

View File

@@ -16,7 +16,7 @@ interface SearchListItemProps {
showListRight?: boolean;
}
const SearchListItem: React.FC<SearchListItemProps> = ({
const SearchListItem: React.FC<SearchListItemProps> = React.memo(({
item,
isSelected,
currentIndex,
@@ -68,6 +68,6 @@ const SearchListItem: React.FC<SearchListItemProps> = ({
) : null}
</div>
);
};
});
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 { ChevronDownIcon, RefreshCw, Layers, Globe } from "lucide-react";
import clsx from "clsx";
@@ -8,7 +8,7 @@ import TypeIcon from "@/components/Common/Icons/TypeIcon";
import { useConnectStore } from "@/stores/connectStore";
import { useSearchStore } from "@/stores/searchStore";
import { DataSource } from "@/types/commands";
import Checkbox from "../Common/Checkbox";
import Checkbox from "@/components/Common/Checkbox";
interface SearchPopoverProps {
isSearchActive: boolean;
@@ -30,18 +30,26 @@ export default function SearchPopover({
const currentService = useConnectStore((state) => state.currentService);
const [showDataSource, setShowDataSource] = useState(false);
const getDataSourceList = useCallback(async () => {
try {
const res: DataSource[] = await getDataSourcesByServer(
currentService?.id
);
const data = [
{
id: "all",
name: "search.input.searchPopover.allScope",
},
...(res || []),
];
if (res?.length === 0) {
setDataSourceList([]);
return;
}
const data = res?.length
? [
{
id: "all",
name: "search.input.searchPopover.allScope",
},
...res,
]
: [];
setDataSourceList(data);
} catch (err) {
setDataSourceList([]);
@@ -49,6 +57,29 @@ export default function SearchPopover({
}
}, [currentService?.id]);
const popoverRef = useRef<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(() => {
if (dataSourceList.length > 0) {
onSelectDataSource("all", true, true);
@@ -111,8 +142,16 @@ export default function SearchPopover({
</span>
{dataSourceList?.length > 0 && (
<Popover>
<PopoverButton as="span" className={clsx("flex items-center")}>
<Popover className="relative">
<PopoverButton
as="span"
ref={buttonRef}
className={clsx("flex items-center")}
onClick={(e) => {
e.stopPropagation();
setShowDataSource((prev) => !prev);
}}
>
<ChevronDownIcon
className={clsx("size-5", [
isSearchActive
@@ -122,79 +161,82 @@ export default function SearchPopover({
/>
</PopoverButton>
<PopoverPanel
anchor="top start"
className="min-w-[220px] bg-white dark:bg-[#202126] rounded-lg shadow-lg border border-gray-200 dark:border-gray-700"
>
<div
className="text-sm px-[12px] py-[18px]"
onClick={(e) => {
e.stopPropagation();
}}
{showDataSource ? (
<PopoverPanel
static
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 className="flex justify-between mb-[18px]">
<span>{t("search.input.searchPopover.title")}</span>
<div
className="text-sm px-[12px] py-[18px]"
onClick={(e) => {
e.stopPropagation();
}}
>
<div className="flex justify-between mb-[18px]">
<span>{t("search.input.searchPopover.title")}</span>
<div
onClick={async () => {
setIsRefreshDataSource(true);
<div
onClick={async () => {
setIsRefreshDataSource(true);
getDataSourceList();
getDataSourceList();
setTimeout(() => {
setIsRefreshDataSource(false);
}, 1000);
}}
className="size-[24px] flex justify-center items-center rounded-lg border border-black/10 dark:border-white/10 cursor-pointer"
>
<RefreshCw
className={`size-3 text-[#0287FF] transition-transform duration-1000 ${
isRefreshDataSource ? "animate-spin" : ""
}`}
/>
setTimeout(() => {
setIsRefreshDataSource(false);
}, 1000);
}}
className="size-[24px] flex justify-center items-center rounded-lg border border-black/10 dark:border-white/10 cursor-pointer"
>
<RefreshCw
className={`size-3 text-[#0287FF] transition-transform duration-1000 ${
isRefreshDataSource ? "animate-spin" : ""
}`}
/>
</div>
</div>
<ul className="flex flex-col gap-[16px]">
{dataSourceList?.map((item, index) => {
const { id, name } = item;
const isAll = index === 0;
return (
<li
key={id}
className="flex justify-between items-center"
>
<div className="flex items-center gap-[8px]">
{isAll ? (
<Layers className="size-[16px] text-[#0287FF]" />
) : (
<TypeIcon item={item} className="size-[16px]" />
)}
<span>{isAll && name ? t(name) : name}</span>
</div>
<div className="flex justify-center items-center size-[24px]">
<Checkbox
checked={
isAll
? sourceDataIds.length ===
dataSourceList.length - 1
: sourceDataIds?.includes(id)
}
indeterminate={isAll}
onChange={(value) =>
onSelectDataSource(id, value, isAll)
}
/>
</div>
</li>
);
})}
</ul>
</div>
<ul className="flex flex-col gap-[16px]">
{dataSourceList?.map((item, index) => {
const { id, name } = item;
const isAll = index === 0;
return (
<li
key={id}
className="flex justify-between items-center"
>
<div className="flex items-center gap-[8px]">
{isAll ? (
<Layers className="size-[16px] text-[#0287FF]" />
) : (
<TypeIcon item={item} className="size-[16px]" />
)}
<span>{isAll && name ? t(name) : name}</span>
</div>
<div className="flex justify-center items-center size-[24px]">
<Checkbox
checked={
isAll
? sourceDataIds.length ===
dataSourceList.length - 1
: sourceDataIds?.includes(id)
}
indeterminate={isAll}
onChange={(value) =>
onSelectDataSource(id, value, isAll)
}
/>
</div>
</li>
);
})}
</ul>
</div>
</PopoverPanel>
</PopoverPanel>
) : null}
</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 { formatKey } from "@/utils/keyboardUtils";
import SettingsItem from "@/components/Settings/SettingsItem";
import { Command } from "lucide-react";
import { ChangeEvent, useEffect } from "react";
import { emit } from "@tauri-apps/api/event";
import { formatKey } from "@/utils/keyboardUtils";
import SettingsItem from "@/components/Settings/SettingsItem";
import { isMac } from "@/utils/platform";
import { ModifierKey, useShortcutsStore } from "@/stores/shortcutsStore";
export const modifierKeys: ModifierKey[] = isMac
? ["meta", "ctrl"]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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) => {
console.log(input);
return await query_coco_fusion(`/query/_search?query=${input}`);
}, []);
const queryDocuments = useCallback(
async (from: number, size: number, queryStrings: any) => {
console.log(from, size, queryStrings);
try {
let url = `/query/_search?query=${queryStrings.query}&datasource=${queryStrings.datasource}&from=${from}&size=${size}`;
if (queryStrings?.rich_categories) {
url = `/query/_search?query=${queryStrings.query}&rich_category=${queryStrings.rich_category}&from=${from}&size=${size}`;
}
return await query_coco_fusion(url);
} catch (error) {
console.error("query_coco_fusion error:", error);
throw error;
}
},
[]
);
return (
<div className="w-[680px] h-[590px]">
<SearchChat querySearch={querySearch} queryDocuments={queryDocuments} />
<div
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>
);
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,6 @@
import { useState } from "react";
import { isTauri } from "@tauri-apps/api/core";
import { convertFileSrc as tauriConvertFileSrc } from "@tauri-apps/api/core";
import type { OpenDialogOptions } from "@tauri-apps/plugin-dialog";
import { createWebAdapter } from './webAdapter';
// import { createTauriAdapter } from './tauriAdapter';
import { IShortcutsStore } from "@/stores/shortcutsStore";
import { IStartupStore } from "@/stores/startupStore";
@@ -29,8 +28,19 @@ export interface EventPayloads {
open_settings: string | "";
tab_index: string | "";
login_or_logout: any;
'show-coco': void;
'connector_data_change': void;
'datasourceData_change': void;
'ws-error': void;
'ws-message': void;
[key: `ws-error-${string}`]: {
error: {
reason: string;
};
status: number;
};
[key: `ws-message-${string}`]: string;
"change-startup-store": IStartupStore
"show-coco": void;
"change-shortcuts-store": IShortcutsStore;
}
@@ -40,7 +50,6 @@ export interface PlatformAdapter {
setWindowSize: (width: number, height: number) => Promise<void>;
hideWindow: () => Promise<void>;
showWindow: () => Promise<void>;
isPlatformTauri: () => boolean;
convertFileSrc: (path: string) => string;
emitEvent: (event: string, payload?: any) => Promise<void>;
listenEvent: <K extends keyof EventPayloads>(
@@ -54,9 +63,9 @@ export interface PlatformAdapter {
getScreenshotableWindows: () => Promise<any[]>;
captureMonitorScreenshot: (id: number) => Promise<string>;
captureWindowScreenshot: (id: number) => Promise<string>;
openFileDialog: (
options: OpenDialogOptions
) => Promise<string | string[] | null>;
openFileDialog: (options: {
multiple: boolean;
}) => Promise<string | string[] | null>;
getFileMetadata: (path: string) => Promise<any>;
getFileIcon: (path: string, size: number) => Promise<string>;
checkUpdate: () => Promise<any>;
@@ -72,406 +81,18 @@ export interface PlatformAdapter {
show: () => Promise<void>;
setFocus: () => Promise<void>;
center: () => Promise<void>;
close: () => Promise<void>;
} | null>;
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
const platformAdapter = createPlatformAdapter();
const platformAdapter: PlatformAdapter = typeof window !== 'undefined' ? createWebAdapter() : {} as PlatformAdapter;
export default platformAdapter;
// Custom hook for using platform adapter
export const usePlatformAdapter = () => {
const [adapter] = useState<PlatformAdapter>(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} */
export default {
content: ["./index.html", "./src/**/*.{html,js,jsx,ts,tsx}"],
content: ["./index.html", "./src/**/*.{html,js,jsx,ts,tsx}", "./src/**/*.css"],
important: '.coco-container',
theme: {
extend: {
backgroundColor: {

View File

@@ -3,18 +3,22 @@ import { writeFileSync, readFileSync } from 'fs';
import { join, resolve } from 'path';
export default defineConfig({
entry: ['src/pages/web/SearchChat.tsx'],
format: ['esm', 'cjs'],
entry: ['src/pages/web/index.tsx'],
format: ['esm'],
dts: true,
splitting: false,
sourcemap: true,
splitting: true,
sourcemap: false,
clean: true,
treeshake: true,
minify: true,
env: {
BUILD_TARGET: 'web',
NODE_ENV: 'production',
},
external: [
'react',
'react-dom',
],
treeshake: true,
minify: true,
esbuildOptions(options) {
options.bundle = true;
options.platform = 'browser';
@@ -28,6 +32,22 @@ export default defineConfig({
options.alias = {
'@': resolve(__dirname, './src')
}
options.external = [
'@tauri-apps/api',
'@tauri-apps/plugin-*',
'tauri-plugin-*',
];
options.treeShaking = true;
options.define = {
'process.env.BUILD_TARGET': '"web"',
'process.env.NODE_ENV': '"production"',
'process.env.DEBUG': 'false',
'process.env.IS_DEV': 'false',
};
options.pure = ['console.log'];
options.target = 'es2020';
options.legalComments = 'none';
options.ignoreAnnotations = false;
},
esbuildPlugins: [
{
@@ -38,36 +58,65 @@ export default defineConfig({
},
},
],
outDir: 'dist/search-chat',
outDir: 'out/search-chat',
async onSuccess() {
const projectPackageJson = JSON.parse(
readFileSync(join(__dirname, 'package.json'), 'utf-8')
);
const packageJson = {
name: "search-chat",
version: "1.0.0",
main: "SearchChat.cjs",
module: "SearchChat.js",
types: "SearchChat.d.ts",
name: "@infinilabs/search-chat",
version: "1.0.10",
main: "index.js",
module: "index.js",
type: "module",
types: "index.d.ts",
dependencies: projectPackageJson.dependencies,
peerDependencies: {
"react": "^18.0.0",
"react-dom": "^18.0.0"
},
"sideEffects": [
"*.css",
"*.scss"
],
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
}
};
const noNeedDeps = [
"@wavesurfer/react",
"dotenv",
"uuid",
"wavesurfer.js",
]
const tauriDeps = Object.keys(packageJson.dependencies).filter(dep =>
dep.includes('@tauri-apps') ||
dep.includes('tauri-plugin')
dep.includes('tauri-plugin') ||
noNeedDeps.includes(dep)
);
tauriDeps.forEach(dep => {
delete packageJson.dependencies[dep];
});
writeFileSync(
join(__dirname, 'dist/search-chat/package.json'),
join(__dirname, 'out/search-chat/package.json'),
JSON.stringify(packageJson, null, 2)
);
try {
const readmePath = join(__dirname, 'src/pages/web/README.md');
const readmeContent = readFileSync(readmePath, 'utf-8');
writeFileSync(
join(__dirname, 'out/search-chat/README.md'),
readmeContent
);
} catch (error) {
console.error('Failed to copy README.md:', error);
}
}
});

View File

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