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 00000000..711b8af2 Binary files /dev/null and b/src/assets/images/coco-cloud-banner.jpeg differ diff --git a/src/components/AppAI/DropdownList.tsx b/src/components/AppAI/DropdownList.tsx index 7ccada8b..170fca5d 100644 --- a/src/components/AppAI/DropdownList.tsx +++ b/src/components/AppAI/DropdownList.tsx @@ -16,6 +16,7 @@ import source_default_img from "@/assets/images/source_default.png"; import source_default_dark_img from "@/assets/images/source_default_dark.png"; import file_efault_img from "@/assets/images/file_efault.png"; import { useTheme } from "@/contexts/ThemeContext"; +import { useConnectStore } from "@/stores/connectStore"; type ISearchData = Record; @@ -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, + }), + } + ) +);