From fbfb980fece7f45498a56fe11f53d5c224f95df7 Mon Sep 17 00:00:00 2001 From: BiggerRain <15911122312@163.COM> Date: Sat, 25 Jan 2025 16:20:49 +0800 Subject: [PATCH] feat: add connect services (#111) * feat: add connect services * chore: adjust auth and userInfo * chore: add responsiveness to auth and userInfo * chore: add dark css * chore: handle /, to join the baseURL and url. * chore: use http:// or https:// rather http * chore: handle /, to join the baseURL and url. * chore: active current service * chore: connect * chore: data source & connect data * feat: add handleKeyDown open settings * chore: settings name --- package.json | 3 +- pnpm-lock.yaml | 23 +- src/api/tauriFetchClient.ts | 10 +- src/assets/images/coco-cloud-banner.jpeg | Bin 0 -> 67350 bytes src/components/AppAI/DropdownList.tsx | 16 +- src/components/AppAI/Footer.tsx | 13 +- src/components/AppAI/InputBox.tsx | 23 +- src/components/AppAI/Search.tsx | 2 +- src/components/Auth/CocoCloud.tsx | 327 +++++++++++++------ src/components/Auth/ConnectService.tsx | 133 ++++++-- src/components/Auth/DataSourceItem.tsx | 80 +++-- src/components/Auth/DataSourcesList.tsx | 73 +++-- src/components/Auth/Sidebar.tsx | 164 +++++++++- src/components/Auth/UserProfile.tsx | 46 ++- src/components/Chat.tsx | 4 +- src/components/ChatAI/Chat.tsx | 2 +- src/components/SearchChat/ChatSwitch.tsx | 2 +- src/components/SearchChat/DocumentDetail.tsx | 12 +- src/components/SearchChat/DocumentList.tsx | 12 +- src/components/SearchChat/DropdownList.tsx | 20 +- src/components/Settings/Account.tsx | 8 +- src/components/Settings/GeneralSettings.tsx | 39 ++- src/components/Settings/index2.tsx | 4 +- src/error-page.tsx | 17 +- src/hooks/useSettingsWindow.ts | 24 +- src/main.css | 6 +- src/pages/app/index.tsx | 13 +- src/stores/appStore.ts | 16 - src/stores/authStore.ts | 64 +++- src/stores/connectStore.ts | 124 +++++++ 30 files changed, 947 insertions(+), 333 deletions(-) create mode 100644 src/assets/images/coco-cloud-banner.jpeg create mode 100644 src/stores/connectStore.ts diff --git a/package.json b/package.json index a95a743c..b0ede2b0 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "dotenv": "^16.4.7", "i18next": "^23.16.2", "lodash": "^4.17.21", - "lucide-react": "^0.453.0", + "lucide-react": "^0.461.0", "mermaid": "^11.4.0", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -55,6 +55,7 @@ "@types/react-katex": "^3.0.4", "@vitejs/plugin-react": "^4.2.1", "autoprefixer": "^10.4.20", + "immer": "^10.1.1", "postcss": "^8.4.47", "tailwindcss": "^3.4.14", "typescript": "^5.2.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 12b9bf81..76793841 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -60,8 +60,8 @@ importers: specifier: ^4.17.21 version: 4.17.21 lucide-react: - specifier: ^0.453.0 - version: 0.453.0(react@18.3.1) + specifier: ^0.461.0 + version: 0.461.0(react@18.3.1) mermaid: specifier: ^11.4.0 version: 11.4.0 @@ -106,7 +106,7 @@ importers: version: 11.0.3 zustand: specifier: ^5.0.0 - version: 5.0.0(@types/react@18.3.11)(react@18.3.1) + version: 5.0.0(@types/react@18.3.11)(immer@10.1.1)(react@18.3.1) devDependencies: '@tauri-apps/cli': specifier: '>=2.0.0' @@ -138,6 +138,9 @@ importers: autoprefixer: specifier: ^10.4.20 version: 10.4.20(postcss@8.4.47) + immer: + specifier: ^10.1.1 + version: 10.1.1 postcss: specifier: ^8.4.47 version: 8.4.47 @@ -1440,6 +1443,9 @@ packages: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} + immer@10.1.1: + resolution: {integrity: sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==} + inline-style-parser@0.2.4: resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==} @@ -1577,8 +1583,8 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - lucide-react@0.453.0: - resolution: {integrity: sha512-kL+RGZCcJi9BvJtzg2kshO192Ddy9hv3ij+cPrVPWSRzgCWCVazoQJxOjAwgK53NomL07HB7GPHW120FimjNhQ==} + lucide-react@0.461.0: + resolution: {integrity: sha512-Scpw3D/dV1bgVRC5Kh774RCm99z0iZpPv75M6kg7QL1lLvkQ1rmI1Sjjic1aGp1ULBwd7FokV6ry0g+d6pMB+w==} peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc @@ -3616,6 +3622,8 @@ snapshots: dependencies: safer-buffer: 2.1.2 + immer@10.1.1: {} + inline-style-parser@0.2.4: {} internmap@1.0.1: {} @@ -3726,7 +3734,7 @@ snapshots: dependencies: yallist: 3.1.1 - lucide-react@0.453.0(react@18.3.1): + lucide-react@0.461.0(react@18.3.1): dependencies: react: 18.3.1 @@ -4708,9 +4716,10 @@ snapshots: yaml@2.6.0: {} - zustand@5.0.0(@types/react@18.3.11)(react@18.3.1): + zustand@5.0.0(@types/react@18.3.11)(immer@10.1.1)(react@18.3.1): optionalDependencies: '@types/react': 18.3.11 + immer: 10.1.1 react: 18.3.1 zwitch@2.0.4: {} diff --git a/src/api/tauriFetchClient.ts b/src/api/tauriFetchClient.ts index 2132e5f4..cfc6b975 100644 --- a/src/api/tauriFetchClient.ts +++ b/src/api/tauriFetchClient.ts @@ -47,14 +47,20 @@ export const tauriFetch = async ({ const auth = authStore?.state?.auth console.log("auth", auth) + if (baseURL.endsWith("/")) { + baseURL = baseURL.slice(0, -1); + } - url = baseURL + url; + if (!url.startsWith("http://") && !url.startsWith("https://")) { + // If not, prepend the defaultPrefix + url = baseURL + url; + } if (method !== "GET") { headers["Content-Type"] = "application/json"; } - headers["X-API-TOKEN"] = headers["X-API-TOKEN"] || auth?.token || ""; + headers["X-API-TOKEN"] = headers["X-API-TOKEN"] || (auth && auth[endpoint_http]?.token) || ""; // debug API const requestInfo = { diff --git a/src/assets/images/coco-cloud-banner.jpeg b/src/assets/images/coco-cloud-banner.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..711b8af244a5fa78b40a528e837c02897ca8260c GIT binary patch literal 67350 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 @@ -46,8 +47,9 @@ function DropdownList({ const { theme } = useTheme(); - const connector_data = useAppStore((state) => state.connector_data); - const datasourceData = useAppStore((state) => state.datasourceData); + const connector_data = useConnectStore((state) => state.connector_data); + const datasourceData = useConnectStore((state) => state.datasourceData); + const endpoint_http = useAppStore((state) => state.endpoint_http); const setSourceData = useSearchStore((state) => state.setSourceData); @@ -163,13 +165,13 @@ function DropdownList({ function findConnectorIcon(item: any) { const id = item?._source?.source?.id || ""; - const result_source = datasourceData.find( + const result_source = datasourceData[endpoint_http]?.find( (data: any) => data._source.id === id ); const connector_id = result_source?._source?.connector?.id; - const result_connector = connector_data.find( + const result_connector = connector_data[endpoint_http]?.find( (data: any) => data._source.id === connector_id ); @@ -184,7 +186,7 @@ function DropdownList({ return theme === "dark" ? source_default_dark_img : source_default_img; } - if (icons?.includes("http")) { + if (icons?.startsWith("http://") || icons?.startsWith("https://")) { return icons; } else { return endpoint_http + icons; @@ -201,7 +203,7 @@ function DropdownList({ return file_efault_img; } - if (selectedIcon?.includes("http")) { + if (selectedIcon?.startsWith("http://") || selectedIcon?.startsWith("https://")) { return selectedIcon; } else { return endpoint_http + selectedIcon; @@ -218,7 +220,7 @@ function DropdownList({ return theme === "dark" ? source_default_dark_img : source_default_img; } - if (selectedIcon?.includes("http")) { + if (selectedIcon?.startsWith("http://") || selectedIcon?.startsWith("https://")) { return selectedIcon; } else { return endpoint_http + selectedIcon; diff --git a/src/components/AppAI/Footer.tsx b/src/components/AppAI/Footer.tsx index 249ac693..33df0869 100644 --- a/src/components/AppAI/Footer.tsx +++ b/src/components/AppAI/Footer.tsx @@ -11,6 +11,7 @@ import source_default_dark_img from "@/assets/images/source_default_dark.png"; import { useSearchStore } from "@/stores/searchStore"; import { useAppStore } from "@/stores/appStore"; import { useTheme } from "@/contexts/ThemeContext"; +import { useConnectStore } from "@/stores/connectStore"; interface FooterProps { isChat: boolean; @@ -19,8 +20,10 @@ interface FooterProps { export default function Footer({ name }: FooterProps) { const sourceData = useSearchStore((state) => state.sourceData); - const connector_data = useAppStore((state) => state.connector_data); - const datasourceData = useAppStore((state) => state.datasourceData); + + const connector_data = useConnectStore((state) => state.connector_data); + const datasourceData = useConnectStore((state) => state.datasourceData); + const endpoint_http = useAppStore((state) => state.endpoint_http); const { theme } = useTheme(); @@ -28,13 +31,13 @@ export default function Footer({ name }: FooterProps) { function findConnectorIcon(item: any) { const id = item?._source?.source?.id || ""; - const result_source = datasourceData.find( + const result_source = datasourceData[endpoint_http]?.find( (data: any) => data._source.id === id ); const connector_id = result_source?._source?.connector?.id; - const result_connector = connector_data.find( + const result_connector = connector_data[endpoint_http]?.find( (data: any) => data._source.id === connector_id ); @@ -49,7 +52,7 @@ export default function Footer({ name }: FooterProps) { return theme === "dark" ? source_default_dark_img : source_default_img; } - if (icons?.includes("http")) { + if (icons?.startsWith("http://") || icons?.startsWith("https://")) { return icons; } else { return endpoint_http + icons; diff --git a/src/components/AppAI/InputBox.tsx b/src/components/AppAI/InputBox.tsx index 0872afd8..f2012651 100644 --- a/src/components/AppAI/InputBox.tsx +++ b/src/components/AppAI/InputBox.tsx @@ -70,14 +70,22 @@ export default function ChatInput({ } }, [inputValue, disabled, onSend]); + const pressedKeys = new Set(); + const handleKeyDown = useCallback( (e: KeyboardEvent) => { + pressedKeys.add(e.code); + if (e.code === "MetaLeft" || e.code === "MetaRight") { setIsCommandPressed(true); } - if (e.metaKey) { + if (pressedKeys.has("MetaLeft") || pressedKeys.has("MetaRight")) { + e.preventDefault(); switch (e.code) { + case "Comma": + setIsCommandPressed(false); + break; case "KeyI": handleToggleFocus(); break; @@ -88,7 +96,7 @@ export default function ChatInput({ console.log("KeyM"); break; case "Enter": - isChatMode && (curChatEnd ? handleSubmit() : disabledChange()); + isChatMode && (curChatEnd ? handleSubmit() : disabledChange?.()); break; case "KeyO": console.log("KeyO"); @@ -107,10 +115,19 @@ export default function ChatInput({ } } }, - [handleToggleFocus, isChatMode, handleSubmit] + [ + handleToggleFocus, + isChatMode, + handleSubmit, + setSourceData, + setIsCommandPressed, + disabledChange, + curChatEnd, + ] ); const handleKeyUp = useCallback((e: KeyboardEvent) => { + pressedKeys.delete(e.code); if (e.code === "MetaLeft" || e.code === "MetaRight") { setIsCommandPressed(false); } diff --git a/src/components/AppAI/Search.tsx b/src/components/AppAI/Search.tsx index 0868240b..0c66e9f8 100644 --- a/src/components/AppAI/Search.tsx +++ b/src/components/AppAI/Search.tsx @@ -113,7 +113,7 @@ function Search({ isChatMode, input }: SearchProps) { }; } - const debouncedSearch = useCallback(debounce(getSuggest, 300), [input]); + const debouncedSearch = useCallback(debounce(getSuggest, 500), [input]); useEffect(() => { !isChatMode && !sourceData && debouncedSearch(); diff --git a/src/components/Auth/CocoCloud.tsx b/src/components/Auth/CocoCloud.tsx index 2ec4960b..d0f4bc58 100644 --- a/src/components/Auth/CocoCloud.tsx +++ b/src/components/Auth/CocoCloud.tsx @@ -1,7 +1,18 @@ -import { useState, useEffect } from "react"; -import { Cloud } from "lucide-react"; +import { useState, useEffect, useCallback } from "react"; +import { + RefreshCcw, + Globe, + PackageOpen, + GitFork, + CalendarSync, + Trash2, +} from "lucide-react"; import { v4 as uuidv4 } from "uuid"; import { getCurrentWindow } from "@tauri-apps/api/window"; +import { + onOpenUrl, + getCurrent as getCurrentDeepLinkUrls, +} from "@tauri-apps/plugin-deep-link"; import { UserProfile } from "./UserProfile"; import { DataSourcesList } from "./DataSourcesList"; @@ -11,84 +22,105 @@ import { OpenBrowserURL } from "@/utils/index"; import { useAppStore } from "@/stores/appStore"; import { useAuthStore } from "@/stores/authStore"; import { tauriFetch } from "@/api/tauriFetchClient"; -import { - onOpenUrl, - getCurrent as getCurrentDeepLinkUrls, -} from "@tauri-apps/plugin-deep-link"; +import { useConnectStore } from "@/stores/connectStore"; +import bannerImg from "@/assets/images/coco-cloud-banner.jpeg"; export default function CocoCloud() { const [error, setError] = useState(null); - const [isConnect] = useState(true); + const [isConnect, setIsConnect] = useState(true); const app_uid = useAppStore((state) => state.app_uid); const setAppUid = useAppStore((state) => state.setAppUid); - const endpoint_http = useAppStore((state) => state.endpoint_http); + const setEndpoint = useAppStore((state) => state.setEndpoint); + const endpoint = useAppStore((state) => state.endpoint); - const { auth, setAuth } = useAuthStore(); + const auth = useAuthStore((state) => state.auth); + const setAuth = useAuthStore((state) => state.setAuth); const userInfo = useAuthStore((state) => state.userInfo); const setUserInfo = useAuthStore((state) => state.setUserInfo); + const defaultService = useConnectStore((state) => state.defaultService); + const currentService = useConnectStore((state) => state.currentService); + const setDefaultService = useConnectStore((state) => state.setDefaultService); + const setCurrentService = useConnectStore((state) => state.setCurrentService); + const deleteOtherService = useConnectStore( + (state) => state.deleteOtherService + ); const [loading, setLoading] = useState(false); + const [refreshLoading, setRefreshLoading] = useState(false); - const getProfile = async () => { + useEffect(() => { + console.log("currentService", currentService); + setLoading(false); + setRefreshLoading(false); + setError(null); + setEndpoint(currentService.endpoint); + setIsConnect(true); + }, [JSON.stringify(currentService)]); + + const getProfile = useCallback(async () => { const response: any = await tauriFetch({ - url: `/provider/account/profile`, + url: `/account/profile`, method: "GET", }); console.log("getProfile", response); - setUserInfo(response.data || {}); - }; + setUserInfo(response.data || {}, endpoint); + }, [endpoint]); - const handleOAuthCallback = async ( - code: string | null, - provider: string | null - ) => { - if (!code) { - setError("No authorization code received"); - return; - } - - try { - console.log("Handling OAuth callback:", { code, provider }); - const response: any = await tauriFetch({ - url: `/auth/request_access_token?request_id=${app_uid}`, - method: "GET", - headers: { - "X-API-TOKEN": code, - }, - }); - console.log( - "response", - `/auth/request_access_token?request_id=${app_uid}`, - code, - response - ); - - if (response.data?.access_token) { - await setAuth({ - token: response.data?.access_token, - expires: response.data?.expire_at, - plan: { upgraded: false, last_checked: 0 }, - }); - - getProfile(); - } else { - setError("Sign in failed: " + response.data?.error?.reason); + const handleOAuthCallback = useCallback( + async (code: string | null, provider: string | null) => { + if (!code) { + setError("No authorization code received"); + return; } - getCurrentWindow() - .setFocus() - .catch(() => {}); - } catch (e) { - console.error("Sign in failed:", error); - setError("Sign in failed: catch"); - await setAuth(undefined); - throw error; - } finally { - setLoading(false); - } - }; + try { + console.log("Handling OAuth callback:", { code, provider }); + const response: any = await tauriFetch({ + url: `/auth/request_access_token?request_id=${app_uid}`, + method: "GET", + headers: { + "X-API-TOKEN": code, + }, + }); + console.log( + "response", + `/auth/request_access_token?request_id=${app_uid}`, + code, + response + ); + + if (response.data?.access_token) { + await setAuth( + { + token: response.data?.access_token, + expires: response.data?.expire_at, + plan: { upgraded: false, last_checked: 0 }, + }, + endpoint + ); + + getProfile(); + } else { + await setAuth(undefined, endpoint); + setError("Sign in failed: " + response.data?.error?.reason); + } + + getCurrentWindow() + .setFocus() + .catch(() => {}); + } catch (e) { + console.error("Sign in failed:", error); + setError("Sign in failed: catch"); + await setAuth(undefined, endpoint); + throw error; + } finally { + setLoading(false); + } + }, + [app_uid, endpoint] + ); const handleUrl = (url: string) => { try { @@ -108,7 +140,6 @@ export default function CocoCloud() { // default: // console.log("Unhandled deep link path:", urlObject.pathname); // } - } catch (err) { console.error("Failed to parse URL:", err); setError("Invalid URL format"); @@ -135,87 +166,167 @@ export default function CocoCloud() { return () => { unlisten.then((fn) => fn()); }; - }, []); + }, [app_uid]); + + const LoginClick = useCallback(() => { + if (loading) return; + setAuth(undefined, endpoint); - function LoginClick() { let uid = uuidv4(); setAppUid(uid); + console.log("LoginClick", uid, currentService.auth_provider.sso.url); + OpenBrowserURL( - `${endpoint_http}/sso/login/?provider=coco-cloud&product=coco&request_id=${uid}` + `${currentService.auth_provider.sso.url}/?provider=coco-cloud&product=coco&request_id=${uid}` ); setLoading(true); + }, [JSON.stringify(currentService)]); + + function goToHref(url: string) { + OpenBrowserURL(url); } - return ( -
- + const refreshClick = useCallback(() => { + setRefreshLoading(true); + tauriFetch({ + url: `/provider/_info`, + method: "GET", + }) + .then((res) => { + setEndpoint(res.data.endpoint); + setCurrentService(res.data || {}); + if (res.data?.endpoint === "https://coco.infini.cloud/") { + setDefaultService(res.data); + } + }) + .catch((err) => { + console.error(err); + }) + .finally(() => { + setRefreshLoading(false); + }); + }, [JSON.stringify(defaultService)]); -
+ function addService() { + setIsConnect(false); + } + + const deleteClick = useCallback(() => { + deleteOtherService(currentService); + setAuth(undefined, endpoint); + setUserInfo({}, endpoint); + }, [JSON.stringify(currentService), endpoint]); + + return ( +
+ + +
{error && ( -
+
Error: {error}
)}
{isConnect ? ( -
-
+
+
+ banner +
+
-
- - Coco Cloud +
+ {currentService.name}
- - Available -
- +
+ + + {currentService.endpoint !== defaultService.endpoint ? ( + + ) : null} +
-
- Service provision: INFINI Labs +
+ + {" "} + {currentService.provider.name} + | - Version Number: v2.3.0 + + {" "} + {currentService.version.number} + | - Update time: 2023-05-12 + + {currentService.updated} +
-

- Coco Cloud provides users with a cloud storage and data - integration platform that supports account registration and data - source management. Users can integrate multiple data sources - (such as Google Drive, yuque, GitHub, etc.), easily access and - search for files, documents and codes across platforms, and - achieve efficient data collaboration and management. +

+ {currentService.provider.description}

-
-

- Account Information -

- {auth ? ( - - ) : ( - - )} -
+ {currentService.auth_provider.sso.url ? ( +
+

+ Account Information +

+ {auth && auth[endpoint] ? ( + + ) : ( +
+ + +
+ )} +
+ ) : null} - {auth ? : null} + {auth && auth[endpoint] ? : null}
) : ( - + )}
diff --git a/src/components/Auth/ConnectService.tsx b/src/components/Auth/ConnectService.tsx index 423f227e..4d32d4e9 100644 --- a/src/components/Auth/ConnectService.tsx +++ b/src/components/Auth/ConnectService.tsx @@ -1,53 +1,122 @@ -import React, { useState } from 'react'; -import { ArrowLeft } from 'lucide-react'; +import React, { useState, useCallback } from "react"; +import { ChevronLeft } from "lucide-react"; -export function ConnectService() { - const [sourceName, setSourceName] = useState(''); +import { useConnectStore } from "@/stores/connectStore"; +import { tauriFetch } from "@/api/tauriFetchClient"; +import { useAppStore } from "@/stores/appStore"; + +interface ConnectServiceProps { + setIsConnect: (isConnect: boolean) => void; +} + +export function ConnectService({ setIsConnect }: ConnectServiceProps) { + const addOtherServices = useConnectStore((state) => state.addOtherServices); + const setCurrentService = useConnectStore((state) => state.setCurrentService); + const defaultService = useConnectStore((state) => state.defaultService); + const otherServices = useConnectStore((state) => state.otherServices); + + const setEndpoint = useAppStore((state) => state.setEndpoint); + + const [endpointLink, setEndpointLink] = useState(""); + const [refreshLoading, setRefreshLoading] = useState(false); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); - console.log('Connecting Google Drive with name:', sourceName); + console.log("Connecting Google Drive with name:", endpointLink); }; + const goBack = () => { + setIsConnect(true); + }; + + const addService = useCallback(() => { + if (!endpointLink) return; + if (!endpointLink.startsWith("http://") && !endpointLink.startsWith("https://")) { + return + } + setRefreshLoading(true); + // + let baseURL = endpointLink; + if (baseURL.endsWith("/")) { + baseURL = baseURL.slice(0, -1); + } + + tauriFetch({ + url: `${baseURL}/provider/_info`, + method: "GET", + }) + .then((res) => { + if ( + res.data?.endpoint === defaultService.endpoint || + otherServices.some( + (item: any) => item.endpoint === res.data?.endpoint + ) + ) { + console.error(`${res.data?.endpoint} Repeated`); + } else { + addOtherServices(res.data); + setCurrentService(res.data); + setEndpoint(res.data.endpoint); + setIsConnect(true); + } + }) + .catch((err) => { + console.error(err); + }) + .finally(() => { + setRefreshLoading(false); + }); + }, [endpointLink]); + return ( -
-
- +
+ Connecting to third-party services +
-

- Coco needs to obtain authorization from your Google Drive account +

+ Third-party services are provided by other platforms or providers, and + users can integrate these services into Coco AI to expand the scope of + search data.

- - setSourceName(e.target.value)} - className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" - placeholder="Your Google Drive" - /> -
- -
- + Server address + +
+ setEndpointLink(e.target.value)} + className="text-[#101010] dark:text-white flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-800" + /> + +
); -} \ No newline at end of file +} diff --git a/src/components/Auth/DataSourceItem.tsx b/src/components/Auth/DataSourceItem.tsx index e8c72b7a..cff046c2 100644 --- a/src/components/Auth/DataSourceItem.tsx +++ b/src/components/Auth/DataSourceItem.tsx @@ -1,4 +1,10 @@ -import { Link2, Trash2 } from "lucide-react"; +import { Link2 } from "lucide-react"; + +import { useAppStore } from "@/stores/appStore"; +import source_default_img from "@/assets/images/source_default.png"; +import source_default_dark_img from "@/assets/images/source_default_dark.png"; +import { useTheme } from "@/contexts/ThemeContext"; +import { useConnectStore } from "@/stores/connectStore"; interface Account { email: string; @@ -7,56 +13,88 @@ interface Account { interface DataSourceItemProps { name: string; - type: string; - accounts: Account[]; + connector: any; + accounts?: Account[]; } -export function DataSourceItem({ name, type, accounts }: DataSourceItemProps) { - const isConnected = accounts.length > 0; +export function DataSourceItem({ name, connector }: DataSourceItemProps) { + // const isConnected = true; + + const { theme } = useTheme(); + + const connector_data = useConnectStore((state) => state.connector_data); + const endpoint_http = useAppStore((state) => state.endpoint_http); + + function findConnectorIcon() { + const connector_id = connector?.id; + + const result_connector = connector_data[endpoint_http]?.find( + (data: any) => data._source.id === connector_id + ); + + return result_connector?._source; + } + + function getTypeIcon() { + const connectorSource = findConnectorIcon(); + const icons = connectorSource?.icon; + + if (!icons) { + return theme === "dark" ? source_default_dark_img : source_default_img; + } + + if (icons?.startsWith("http://") || icons?.startsWith("https://")) { + return icons; + } else { + return endpoint_http + icons; + } + } return ( -
+
- {name} - {name} + {name} + + {name} +
-
-
+ {/*
{isConnected ? "Manage" : "Connect Accounts"} -
+
*/} - {accounts.map((account, index) => ( + {/* {accounts.map((account, index) => (
-
- +
+ {account.email[0].toUpperCase()}
-
+
{index === 0 ? "My network disk" : `Network disk ${index + 1}`}
-
{account.email}
+
{account.email}
- - Recently Synced: {account.lastSync} + + Recently Synced: {account.lastSync} -
- ))} + ))} */}
); } diff --git a/src/components/Auth/DataSourcesList.tsx b/src/components/Auth/DataSourcesList.tsx index 213b5f2d..2ea705b6 100644 --- a/src/components/Auth/DataSourcesList.tsx +++ b/src/components/Auth/DataSourcesList.tsx @@ -1,38 +1,57 @@ -import { DataSourceItem } from './DataSourceItem'; +import { useEffect, useState } from "react"; +import { RefreshCcw } from "lucide-react"; + +import { DataSourceItem } from "./DataSourceItem"; +import { useConnectStore } from "@/stores/connectStore"; +import { tauriFetch } from "@/api/tauriFetchClient"; +import { useAppStore } from "@/stores/appStore"; export function DataSourcesList() { - const dataSources = [ - { - id: 'google-drive', - name: 'Google Drive', - type: 'google', - accounts: [ - { email: 'an121245@gmail.com', lastSync: '2025-01-02 09:50 AM' }, - { email: '9paiii@gmail.com', lastSync: '2025-01-02 09:50 AM' } - ] - }, - { - id: 'yuque', - name: 'Yuque', - type: 'yuque', - accounts: [] - }, - { - id: 'github', - name: 'Github', - type: 'github', - accounts: [] + const datasourceData = useConnectStore((state) => state.datasourceData); + const setDatasourceData = useConnectStore((state) => state.setDatasourceData); + + const endpoint_http = useAppStore((state) => state.endpoint_http); + + const [refreshLoading, setRefreshLoading] = useState(false); + + async function getDatasourceData() { + setRefreshLoading(true); + try { + const response = await tauriFetch({ + url: `/datasource/_search`, + method: "GET", + }); + console.log("datasource", response); + const data = response.data?.hits?.hits || []; + setDatasourceData(data, endpoint_http); + } catch (error) { + console.error("Failed to fetch user data:", error); } - ]; + setRefreshLoading(false); + } + + useEffect(() => { + getDatasourceData() + }, []) return (
-

Data Source

+

+ Data Source + +

- {dataSources.map(source => ( - + {datasourceData[endpoint_http]?.map((source) => ( + ))}
); -} \ No newline at end of file +} diff --git a/src/components/Auth/Sidebar.tsx b/src/components/Auth/Sidebar.tsx index fe94b8a5..c2828ab3 100644 --- a/src/components/Auth/Sidebar.tsx +++ b/src/components/Auth/Sidebar.tsx @@ -1,24 +1,160 @@ -import { Cloud, Plus } from "lucide-react"; +import { useState, useEffect } from "react"; +import { Plus } from "lucide-react"; + +import cocoLogoImg from "@/assets/app-icon.png"; +import { tauriFetch } from "@/api/tauriFetchClient"; +import { useConnectStore } from "@/stores/connectStore"; +import { useAppStore } from "@/stores/appStore"; + +interface SidebarProps { + addService: () => void; +} + +type StringBooleanMap = { + [key: string]: boolean; +}; + +export function Sidebar({ addService }: SidebarProps) { + const defaultService = useConnectStore((state) => state.defaultService); + const currentService = useConnectStore((state) => state.currentService); + const otherServices = useConnectStore((state) => state.otherServices); + const setCurrentService = useConnectStore((state) => state.setCurrentService); + + const setEndpoint = useAppStore((state) => state.setEndpoint); + + const [defaultHealth, setDefaultHealth] = useState(false); + const [otherHealth, setOtherHealth] = useState({}); + + const addServiceClick = () => { + addService(); + }; + + useEffect(() => { + getDefaultHealth(); + }, []); + + useEffect(() => { + getOtherHealth(currentService); + setEndpoint(currentService.endpoint); + }, [currentService.endpoint]); + + const getDefaultHealth = () => { + let baseURL = defaultService.endpoint + if (baseURL.endsWith("/")) { + baseURL = baseURL.slice(0, -1); + } + tauriFetch({ + url: `${baseURL}/health`, + method: "GET", + }) + .then((res) => { + // "services": { + // "system_cluster": "yellow" + // }, + // "status": "yellow" + setDefaultHealth(res.data?.status !== "red"); + }) + .catch((err) => { + console.error(err); + }); + }; + + const getOtherHealth = (item: any) => { + if (!item.endpoint) return; + // + let baseURL = item.endpoint + if (baseURL.endsWith("/")) { + baseURL = baseURL.slice(0, -1); + } + tauriFetch({ + url: `${baseURL}/health`, + method: "GET", + }) + .then((res) => { + let obj = { + ...otherHealth, + [item.endpoint]: res.data?.status !== "red", + }; + setOtherHealth(obj); + }) + .catch((err) => { + console.error(err); + }); + }; -export function Sidebar() { return ( -
-
-
- - Coco Cloud +
+
+
{ + setCurrentService(defaultService); + setEndpoint(defaultService.endpoint); + getDefaultHealth(); + }} + > + cocoLogoImg + + {defaultService.name}
-
-
-
- Third-party services -
+
+ Third-party services +
- +
+ ))} + +
+
diff --git a/src/components/Auth/UserProfile.tsx b/src/components/Auth/UserProfile.tsx index 7df23782..c042e421 100644 --- a/src/components/Auth/UserProfile.tsx +++ b/src/components/Auth/UserProfile.tsx @@ -1,13 +1,14 @@ -import { User, Edit, LogOut } from "lucide-react"; +import { User, LogOut } from "lucide-react"; import { useAuthStore } from "@/stores/authStore"; +import { useAppStore } from "@/stores/appStore"; interface UserPreferences { theme: "dark" | "light"; language: string; } interface UserInfo { - username: string; + name: string; email: string; avatar?: string; roles: string[]; // ["admin", "editor"] @@ -21,45 +22,40 @@ interface UserProfileProps { export function UserProfile({ userInfo }: UserProfileProps) { const setAuth = useAuthStore((state) => state.setAuth); const setUserInfo = useAuthStore((state) => state.setUserInfo); + const endpoint = useAppStore((state) => state.endpoint); const handleLogout = () => { - setAuth(undefined); - setUserInfo({}); + setAuth(undefined, endpoint); + setUserInfo({}, endpoint); }; return (
-
- {userInfo.avatar ? ( - +
+ {userInfo?.avatar ? ( + ) : ( - + )}
-
- - {userInfo.username || "-"} +
+ + {userInfo?.name || "-"} -
- {userInfo.email || "-"} + + {userInfo?.email || "-"} +
-
); } diff --git a/src/components/Chat.tsx b/src/components/Chat.tsx index cbdb879d..4a0ee929 100644 --- a/src/components/Chat.tsx +++ b/src/components/Chat.tsx @@ -57,8 +57,8 @@ export default function ChatInput() { method: "GET", }); setInfo(JSON.stringify(response)); - console.log(response.status); // e.g. 200 - console.log(response.statusText); // e.g. "OK" + // console.log(response.status); // e.g. 200 + // console.log(response.statusText); // e.g. "OK" } catch (error) { console.error("Error sending message:", error); setInfo(JSON.stringify(error)); diff --git a/src/components/ChatAI/Chat.tsx b/src/components/ChatAI/Chat.tsx index 8eec78b0..834770c1 100644 --- a/src/components/ChatAI/Chat.tsx +++ b/src/components/ChatAI/Chat.tsx @@ -225,7 +225,7 @@ const ChatAI = forwardRef( if (isTauri()) { createWin && createWin({ label: "chat", - title: "Coco AI", + title: "Coco Chat", dragDropEnabled: true, center: true, width: 900, diff --git a/src/components/SearchChat/ChatSwitch.tsx b/src/components/SearchChat/ChatSwitch.tsx index a54509f0..633bca8d 100644 --- a/src/components/SearchChat/ChatSwitch.tsx +++ b/src/components/SearchChat/ChatSwitch.tsx @@ -14,7 +14,7 @@ const ChatSwitch: React.FC = ({ isChatMode, onChange }) => { (event: KeyboardEvent) => { if (event.metaKey && event.key === "t") { event.preventDefault(); - console.log("Switch mode triggered"); + // console.log("Switch mode triggered"); handleToggle(); } }, diff --git a/src/components/SearchChat/DocumentDetail.tsx b/src/components/SearchChat/DocumentDetail.tsx index 5576568b..3ce39fa1 100644 --- a/src/components/SearchChat/DocumentDetail.tsx +++ b/src/components/SearchChat/DocumentDetail.tsx @@ -5,14 +5,16 @@ import {formatter} from "@/utils/index" import source_default_img from "@/assets/images/source_default.png"; import source_default_dark_img from "@/assets/images/source_default_dark.png"; import { useTheme } from "@/contexts/ThemeContext"; +import { useConnectStore } from "@/stores/connectStore"; interface DocumentDetailProps { document: any; } export const DocumentDetail: React.FC = ({ document }) => { - const connector_data = useAppStore((state) => state.connector_data); - const datasourceData = useAppStore((state) => state.datasourceData); + const connector_data = useConnectStore((state) => state.connector_data); + const datasourceData = useConnectStore((state) => state.datasourceData); + const endpoint_http = useAppStore((state) => state.endpoint_http); const { theme } = useTheme(); @@ -20,13 +22,13 @@ export const DocumentDetail: React.FC = ({ document }) => { function findConnectorIcon(item: any) { const id = item?._source?.source?.id || ""; - const result_source = datasourceData.find( + const result_source = datasourceData[endpoint_http]?.find( (data: any) => data._source.id === id ); const connector_id = result_source?._source?.connector?.id; - const result_connector = connector_data.find( + const result_connector = connector_data[endpoint_http]?.find( (data: any) => data._source.id === connector_id ); @@ -41,7 +43,7 @@ export const DocumentDetail: React.FC = ({ document }) => { return theme === "dark" ? source_default_dark_img : source_default_img; } - if (icons?.includes("http")) { + if (icons?.startsWith("http://") || icons?.startsWith("https://")) { return icons; } else { return endpoint_http + icons; diff --git a/src/components/SearchChat/DocumentList.tsx b/src/components/SearchChat/DocumentList.tsx index 99f4ebfb..7ee67b8c 100644 --- a/src/components/SearchChat/DocumentList.tsx +++ b/src/components/SearchChat/DocumentList.tsx @@ -9,6 +9,7 @@ import { useSearchStore } from "@/stores/searchStore"; import { SearchHeader } from "./SearchHeader"; import file_efault_img from "@/assets/images/file_efault.png"; import noDataImg from "@/assets/coconut-tree.png"; +import { useConnectStore } from "@/stores/connectStore"; interface DocumentListProps { onSelectDocument: (id: string) => void; @@ -25,8 +26,9 @@ export const DocumentList: React.FC = ({ getDocDetail, isChatMode, }) => { - const connector_data = useAppStore((state) => state.connector_data); - const datasourceData = useAppStore((state) => state.datasourceData); + const connector_data = useConnectStore((state) => state.connector_data); + const datasourceData = useConnectStore((state) => state.datasourceData); + const sourceData = useSearchStore((state) => state.sourceData); const endpoint_http = useAppStore((state) => state.endpoint_http); @@ -109,13 +111,13 @@ export const DocumentList: React.FC = ({ function findConnectorIcon(item: any) { const id = item?._source?.source?.id || ""; - const result_source = datasourceData.find( + const result_source = datasourceData[endpoint_http]?.find( (data: any) => data._source.id === id ); const connector_id = result_source?._source?.connector?.id; - const result_connector = connector_data.find( + const result_connector = connector_data[endpoint_http]?.find( (data: any) => data._source.id === connector_id ); @@ -132,7 +134,7 @@ export const DocumentList: React.FC = ({ return file_efault_img; } - if (selectedIcon?.includes("http")) { + if (selectedIcon?.startsWith("http://") || selectedIcon?.startsWith("https://")) { return selectedIcon; } else { return endpoint_http + selectedIcon; diff --git a/src/components/SearchChat/DropdownList.tsx b/src/components/SearchChat/DropdownList.tsx index a1f38151..1e1fdfd8 100644 --- a/src/components/SearchChat/DropdownList.tsx +++ b/src/components/SearchChat/DropdownList.tsx @@ -19,7 +19,7 @@ function DropdownList({ selected, suggests }: DropdownListProps) { try { if (isTauri()) { await open(url); - console.log("URL opened in default browser"); + // console.log("URL opened in default browser"); } } catch (error) { console.error("Failed to open URL:", error); @@ -27,12 +27,12 @@ function DropdownList({ selected, suggests }: DropdownListProps) { }; const handleKeyDown = (e: KeyboardEvent) => { - console.log( - "handleKeyDown", - e.key, - showIndex, - e.key >= "0" && e.key <= "9" && showIndex - ); + // console.log( + // "handleKeyDown", + // e.key, + // showIndex, + // e.key >= "0" && e.key <= "9" && showIndex + // ); if (!suggests.length) return; if (e.key === "ArrowUp") { @@ -51,7 +51,7 @@ function DropdownList({ selected, suggests }: DropdownListProps) { } if (e.key === "Enter" && selectedItem !== null) { - console.log("Enter key pressed", selectedItem); + // console.log("Enter key pressed", selectedItem); const item = suggests[selectedItem]; if (item?._source?.url) { handleOpenURL(item?._source?.url); @@ -61,7 +61,7 @@ function DropdownList({ selected, suggests }: DropdownListProps) { } if (e.key >= "0" && e.key <= "9" && showIndex) { - console.log(`number ${e.key}`); + // console.log(`number ${e.key}`); const item = suggests[parseInt(e.key, 10)]; if (item?._source?.url) { handleOpenURL(item?._source?.url); @@ -72,7 +72,7 @@ function DropdownList({ selected, suggests }: DropdownListProps) { }; const handleKeyUp = (e: KeyboardEvent) => { - console.log("handleKeyUp", e.key); + // console.log("handleKeyUp", e.key); if (!suggests.length) return; if (!e.metaKey) { diff --git a/src/components/Settings/Account.tsx b/src/components/Settings/Account.tsx index 08bffd3c..f4c5c406 100644 --- a/src/components/Settings/Account.tsx +++ b/src/components/Settings/Account.tsx @@ -29,7 +29,7 @@ export default function Account() { const setupAuthListener = async () => { try { - if (!auth) { + if (!(auth && auth[endpoint_http])) { // Replace the current route with signin // navigate("/signin", { replace: true }); } @@ -55,7 +55,7 @@ export default function Account() { cleanup(); }; - }, [auth]); + }, [JSON.stringify(auth)]); async function signIn() { let res: (url: URL) => void; @@ -114,7 +114,7 @@ export default function Account() { user_id, expires, plan: { upgraded: false, last_checked: 0 }, - }); + }, endpoint_http); getCurrentWindow() .setFocus() @@ -123,7 +123,7 @@ export default function Account() { return navigate("/"); } catch (error) { console.error("Sign in failed:", error); - await setAuth(undefined); + await setAuth(undefined, endpoint_http); throw error; } } diff --git a/src/components/Settings/GeneralSettings.tsx b/src/components/Settings/GeneralSettings.tsx index 46c0d7d2..628add0d 100644 --- a/src/components/Settings/GeneralSettings.tsx +++ b/src/components/Settings/GeneralSettings.tsx @@ -7,6 +7,7 @@ import { Sun, Power, Tags, + // Trash2, } from "lucide-react"; import { isTauri, invoke } from "@tauri-apps/api/core"; import { @@ -21,6 +22,8 @@ import { Shortcut } from "./shortcut"; import { useShortcutEditor } from "@/hooks/useShortcutEditor"; import { ThemeOption } from "./index2"; import { useAppStore } from "@/stores/appStore"; +// import { useAuthStore } from "@/stores/authStore"; +// import { useConnectStore } from "@/stores/connectStore"; export default function GeneralSettings() { const [launchAtLogin, setLaunchAtLogin] = useState(true); @@ -28,6 +31,10 @@ export default function GeneralSettings() { const showTooltip = useAppStore((state) => state.showTooltip); const setShowTooltip = useAppStore((state) => state.setShowTooltip); + // const setAuth = useAuthStore((state) => state.setAuth); + // const setUserInfo = useAuthStore((state) => state.setUserInfo); + // const endpoint = useAppStore((state) => state.endpoint); + useEffect(() => { const fetchAutoStartStatus = async () => { if (isTauri()) { @@ -83,14 +90,14 @@ export default function GeneralSettings() { getCurrentShortcut(); }, []); - const changeShortcut =(key: Shortcut) => { - setShortcut(key) + const changeShortcut = (key: Shortcut) => { + setShortcut(key); // if (key.length === 0) return; invoke("change_shortcut", { key: key?.join("+") }).catch((err) => { console.error("Failed to save hotkey:", err); }); - } + }; const { isEditing, currentKeys, startEditing, saveShortcut, cancelEditing } = useShortcutEditor(shortcut, changeShortcut); @@ -115,6 +122,15 @@ export default function GeneralSettings() { saveShortcut(); }; + // const clearAllCache = useCallback(() => { + // setAuth(undefined, endpoint); + // setUserInfo({}, endpoint); + + // useConnectStore.persist.clearStorage(); + + // useAppStore.persist.clearStorage(); + // }, [endpoint]); + return (
@@ -205,6 +221,23 @@ export default function GeneralSettings() { Manage Favorites */} + + {/* +
+
+ +
+
+
*/}
diff --git a/src/components/Settings/index2.tsx b/src/components/Settings/index2.tsx index 33c2e16a..a7a8c892 100644 --- a/src/components/Settings/index2.tsx +++ b/src/components/Settings/index2.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "@headlessui/react"; -import { Settings, Puzzle, User, Settings2, Info } from "lucide-react"; +import { Settings, Puzzle, Settings2, Info, Server } from "lucide-react"; import { useSearchParams } from "react-router-dom"; import SettingsPanel from "./SettingsPanel"; @@ -25,7 +25,7 @@ function SettingsPage() { const tabs = [ { name: "General", icon: Settings }, { name: "Extensions", icon: Puzzle }, - { name: "Connect", icon: User }, + { name: "Connect", icon: Server }, { name: "Advanced", icon: Settings2 }, { name: "About", icon: Info }, ]; diff --git a/src/error-page.tsx b/src/error-page.tsx index 9269828a..e06c8789 100644 --- a/src/error-page.tsx +++ b/src/error-page.tsx @@ -1,22 +1,30 @@ import { useRouteError } from "react-router-dom"; import errorImg from "./assets/error_page.png"; +import ApiDetails from "@/components/AppAI/ApiDetails"; export default function ErrorPage() { const error: any = useRouteError(); console.error(error); - return ( + return (
- error-page + error-page
- Sorry, there is an error in your Coco App. Please contact the administrator. + Sorry, there is an error in your Coco App. Please contact the + administrator.
{error.statusText || error.message}
+ +
); @@ -25,7 +33,8 @@ export default function ErrorPage() {

Oops!

- Sorry, there is an error in your Coco App. Please contact the administrator. + Sorry, there is an error in your Coco App. Please contact the + administrator.

{error.statusText || error.message} diff --git a/src/hooks/useSettingsWindow.ts b/src/hooks/useSettingsWindow.ts index 9dc51cf9..8f058a95 100644 --- a/src/hooks/useSettingsWindow.ts +++ b/src/hooks/useSettingsWindow.ts @@ -18,9 +18,9 @@ export default function useSettingsWindow() { const url = tab ? `/ui/settings?tab=${tab}` : `/ui/settings`; const options: CreateWindowOptions = { label: "settings", - title: "Settings Window", + title: "Coco Settings", width: 1000, - height: 600, + height: 700, alwaysOnTop: false, shadow: true, decorations: true, @@ -29,6 +29,7 @@ export default function useSettingsWindow() { minimizable: false, maximizable: false, dragDropEnabled: true, + resizable: false, center: true, url, }; @@ -45,6 +46,21 @@ export default function useSettingsWindow() { }); }, []); + const handleKeyDown = useCallback( + (e: KeyboardEvent) => { + if (e.metaKey) { + switch (e.code) { + case "Comma": + openSettingsWindow() + break; + default: + break; + } + } + }, + [openSettingsWindow] + ); + useEffect(() => { const unlisten = listen("open_settings", (event) => { console.log("open_settings event received:", event); @@ -52,11 +68,13 @@ export default function useSettingsWindow() { openSettingsWindow(tab); }); + window.addEventListener("keydown", handleKeyDown); return () => { unlisten.then((fn) => fn()); + window.addEventListener("keydown", handleKeyDown); }; - }, []); + }, [openSettingsWindow, handleKeyDown]); return { openSettingsWindow }; } diff --git a/src/main.css b/src/main.css index 44ed4f96..8ef8e93a 100644 --- a/src/main.css +++ b/src/main.css @@ -62,9 +62,13 @@ @apply box-border border-[--border]; } + html{ + @apply h-full; + } + body, #root { - @apply text-gray-900 antialiased; + @apply h-full text-gray-900 antialiased; } .dark body, diff --git a/src/pages/app/index.tsx b/src/pages/app/index.tsx index 5d24edf0..6bb077b9 100644 --- a/src/pages/app/index.tsx +++ b/src/pages/app/index.tsx @@ -10,14 +10,17 @@ import { useAppStore } from "@/stores/appStore"; import { useAuthStore } from "@/stores/authStore"; import { tauriFetch } from "@/api/tauriFetchClient"; import ApiDetails from "@/components/AppAI/ApiDetails"; +import { useConnectStore } from "@/stores/connectStore"; export default function DesktopApp() { const initializeListeners = useAppStore((state) => state.initializeListeners); const initializeListeners_auth = useAuthStore( (state) => state.initializeListeners ); - const setConnectorData = useAppStore((state) => state.setConnectorData); - const setDatasourceData = useAppStore((state) => state.setDatasourceData); + const setConnectorData = useConnectStore((state) => state.setConnectorData); + const setDatasourceData = useConnectStore((state) => state.setDatasourceData); + + const endpoint_http = useAppStore((state) => state.endpoint_http); useEffect(() => { initializeListeners(); @@ -35,7 +38,7 @@ export default function DesktopApp() { }); console.log("connector", response); const data = response.data?.hits?.hits || []; - setConnectorData(data); + setConnectorData(data, endpoint_http); } catch (error) { console.error("Failed to fetch user data:", error); } @@ -49,7 +52,7 @@ export default function DesktopApp() { }); console.log("datasource", response); const data = response.data?.hits?.hits || []; - setDatasourceData(data); + setDatasourceData(data, endpoint_http); } catch (error) { console.error("Failed to fetch user data:", error); } @@ -91,7 +94,7 @@ export default function DesktopApp() { return (

void, - connector_data: any[], - setConnectorData: (connector_data: any[]) => void, - datasourceData: any[], - setDatasourceData: (datasourceData: any[]) => void, initializeListeners: () => void; }; @@ -53,18 +49,6 @@ export const useAppStore = create()( endpoint_websocket }); }, - connector_data: [], - setConnectorData: async (connector_data: any[]) => { - set({ - connector_data - }); - }, - datasourceData: [], - setDatasourceData: async (datasourceData: any[]) => { - set({ - datasourceData - }); - }, initializeListeners: () => { listen(ENDPOINT_CHANGE_EVENT, (event: any) => { const { endpoint, endpoint_http, endpoint_websocket } = event.payload; diff --git a/src/stores/authStore.ts b/src/stores/authStore.ts index 52bd1dc7..074b6903 100644 --- a/src/stores/authStore.ts +++ b/src/stores/authStore.ts @@ -1,6 +1,7 @@ import { create } from "zustand"; import { persist } from "zustand/middleware"; import { listen, emit } from '@tauri-apps/api/event'; +import { produce } from 'immer' const AUTH_CHANGE_EVENT = 'auth-changed'; const USERINFO_CHANGE_EVENT = 'userInfo-changed'; @@ -10,19 +11,27 @@ export type Plan = { last_checked: number; }; -export type AuthStore = { +export type AuthProp = { token: string; user_id?: string | null; expires?: number; plan?: Plan | null; }; +type AuthMapProp = { + [key: string]: AuthProp; +}; + +type userInfoMapProp = { + [key: string]: any; +}; + export type IAuthStore = { [x: string]: any; - auth: AuthStore | undefined; - userInfo: any; - setAuth: (auth: AuthStore | undefined) => void; - resetAuth: () => void; + auth: AuthMapProp | undefined; + userInfo: userInfoMapProp; + setAuth: (auth: AuthProp | undefined, key: string) => void; + resetAuth: (key: string) => void; initializeListeners: () => void; }; @@ -31,24 +40,43 @@ export const useAuthStore = create()( (set) => ({ auth: undefined, userInfo: {}, - setAuth: async (auth) => { - set({ auth }) - await emit(AUTH_CHANGE_EVENT, { - auth - }); - }, - resetAuth: async () => { - set({ auth: undefined }) + setAuth: async (auth, key) => { + set( + produce((draft) => { + draft.auth[key] = auth + }) + ); await emit(AUTH_CHANGE_EVENT, { - auth: undefined + auth: { + [key]: auth + } }); }, - setUserInfo: async (userInfo: any) => { - set({ userInfo }) + resetAuth: async (key: string) => { + set( + produce((draft) => { + draft.auth[key] = undefined + }) + ); + + await emit(AUTH_CHANGE_EVENT, { + auth: { + [key]: undefined + } + }); + }, + setUserInfo: async (userInfo: any, key: string) => { + set( + produce((draft) => { + draft.userInfo[key] = userInfo + }) + ); await emit(USERINFO_CHANGE_EVENT, { - userInfo + userInfo: { + [key]: userInfo + } }); }, initializeListeners: () => { @@ -65,7 +93,7 @@ export const useAuthStore = create()( }), { name: "auth-store", - partialize: (state) => ({ + partialize: (state) => ({ auth: state.auth, userInfo: state.userInfo }), diff --git a/src/stores/connectStore.ts b/src/stores/connectStore.ts new file mode 100644 index 00000000..f27f2ef5 --- /dev/null +++ b/src/stores/connectStore.ts @@ -0,0 +1,124 @@ +import { create } from "zustand"; +import { persist } from "zustand/middleware"; +import { produce } from 'immer' + +type keyArrayObject = { + [key: string]: any[]; +}; + +export type IConnectStore = { + defaultService: any; + setDefaultService: (service: any) => void; + otherServices: any[]; + addOtherServices: (service: any) => void; + deleteOtherService: (service: any) => void; + currentService: any; + setCurrentService: (service: any) => void; + connector_data: keyArrayObject, + setConnectorData: (connector_data: any[], key: string) => void, + datasourceData: keyArrayObject, + setDatasourceData: (datasourceData: any[], key: string) => void, +}; + +export const useConnectStore = create()( + persist( + (set) => ({ + defaultService: { + "name": "Coco Cloud", + "endpoint": "https://coco.infini.cloud/", + "provider": { + "name": "INFINI Labs", + "icon": "https://coco.infini.cloud/icon.png", + "website": "http://infinilabs.com", + "eula": "http://infinilabs.com/eula.txt", + "privacy_policy": "http://infinilabs.com/privacy_policy.txt", + "banner": "https://coco.infini.cloud/banner.jpg", + "description": "Coco AI Server - Search, Connect, Collaborate, AI-powered enterprise search, all in one space." + }, + "version": { + "number": "1.0.0_SNAPSHOT" + }, + "updated": "2025-01-24T12:12:17.326286927+08:00", + "public": false, + "auth_provider": { + "sso": { + "url": "https://coco.infini.cloud/sso/login/" + } + } + }, + setDefaultService: (defaultService: any) => set( + produce((draft) => { + draft.defaultService = defaultService + }) + ), + otherServices: [], + addOtherServices: (otherService: any) => { + set(produce((draft) => { + draft.otherServices.push(otherService); + })) + }, + deleteOtherService: (service: any) => { + set(produce((draft) => { + draft.otherServices = draft.otherServices.filter( + (item: any) => item.endpoint !== service.endpoint + ); + draft.currentService = draft.defaultService; + })) + }, + currentService: { + "name": "Coco Cloud", + "endpoint": "https://coco.infini.cloud/", + "provider": { + "name": "INFINI Labs", + "icon": "https://coco.infini.cloud/icon.png", + "website": "http://infinilabs.com", + "eula": "http://infinilabs.com/eula.txt", + "privacy_policy": "http://infinilabs.com/privacy_policy.txt", + "banner": "https://coco.infini.cloud/banner.jpg", + "description": "Coco AI Server - Search, Connect, Collaborate, AI-powered enterprise search, all in one space." + }, + "version": { + "number": "1.0.0_SNAPSHOT" + }, + "updated": "2025-01-24T12:12:17.326286927+08:00", + "public": false, + "auth_provider": { + "sso": { + "url": "https://coco.infini.cloud/sso/login/" + } + } + }, + setCurrentService: (currentService: any) => { + set(produce((draft) => { + draft.currentService = currentService; + })) + }, + connector_data: {}, + setConnectorData: async (connector_data: any[], key: string) => { + set( + produce((draft) => { + draft.connector_data[key] = connector_data + }) + ); + }, + datasourceData: {}, + setDatasourceData: async (datasourceData: any[], key: string) => { + set( + produce((draft) => { + draft.datasourceData[key] = datasourceData + }) + ); + }, + }), + { + name: "connect-store", + partialize: (state) => ({ + defaultService: state.defaultService, + otherServices: state.otherServices, + currentService: state.currentService, + connector_data: state.connector_data, + datasourceData: state.datasourceData, + }), + } + ) +);