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
This commit is contained in:
BiggerRain
2025-01-25 16:20:49 +08:00
committed by GitHub
parent 6a272bda50
commit fbfb980fec
30 changed files with 947 additions and 333 deletions

View File

@@ -27,7 +27,7 @@
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"i18next": "^23.16.2", "i18next": "^23.16.2",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"lucide-react": "^0.453.0", "lucide-react": "^0.461.0",
"mermaid": "^11.4.0", "mermaid": "^11.4.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
@@ -55,6 +55,7 @@
"@types/react-katex": "^3.0.4", "@types/react-katex": "^3.0.4",
"@vitejs/plugin-react": "^4.2.1", "@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"immer": "^10.1.1",
"postcss": "^8.4.47", "postcss": "^8.4.47",
"tailwindcss": "^3.4.14", "tailwindcss": "^3.4.14",
"typescript": "^5.2.2", "typescript": "^5.2.2",

23
pnpm-lock.yaml generated
View File

@@ -60,8 +60,8 @@ importers:
specifier: ^4.17.21 specifier: ^4.17.21
version: 4.17.21 version: 4.17.21
lucide-react: lucide-react:
specifier: ^0.453.0 specifier: ^0.461.0
version: 0.453.0(react@18.3.1) version: 0.461.0(react@18.3.1)
mermaid: mermaid:
specifier: ^11.4.0 specifier: ^11.4.0
version: 11.4.0 version: 11.4.0
@@ -106,7 +106,7 @@ importers:
version: 11.0.3 version: 11.0.3
zustand: zustand:
specifier: ^5.0.0 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: devDependencies:
'@tauri-apps/cli': '@tauri-apps/cli':
specifier: '>=2.0.0' specifier: '>=2.0.0'
@@ -138,6 +138,9 @@ importers:
autoprefixer: autoprefixer:
specifier: ^10.4.20 specifier: ^10.4.20
version: 10.4.20(postcss@8.4.47) version: 10.4.20(postcss@8.4.47)
immer:
specifier: ^10.1.1
version: 10.1.1
postcss: postcss:
specifier: ^8.4.47 specifier: ^8.4.47
version: 8.4.47 version: 8.4.47
@@ -1440,6 +1443,9 @@ packages:
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
immer@10.1.1:
resolution: {integrity: sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==}
inline-style-parser@0.2.4: inline-style-parser@0.2.4:
resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==} resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==}
@@ -1577,8 +1583,8 @@ packages:
lru-cache@5.1.1: lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
lucide-react@0.453.0: lucide-react@0.461.0:
resolution: {integrity: sha512-kL+RGZCcJi9BvJtzg2kshO192Ddy9hv3ij+cPrVPWSRzgCWCVazoQJxOjAwgK53NomL07HB7GPHW120FimjNhQ==} resolution: {integrity: sha512-Scpw3D/dV1bgVRC5Kh774RCm99z0iZpPv75M6kg7QL1lLvkQ1rmI1Sjjic1aGp1ULBwd7FokV6ry0g+d6pMB+w==}
peerDependencies: peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc
@@ -3616,6 +3622,8 @@ snapshots:
dependencies: dependencies:
safer-buffer: 2.1.2 safer-buffer: 2.1.2
immer@10.1.1: {}
inline-style-parser@0.2.4: {} inline-style-parser@0.2.4: {}
internmap@1.0.1: {} internmap@1.0.1: {}
@@ -3726,7 +3734,7 @@ snapshots:
dependencies: dependencies:
yallist: 3.1.1 yallist: 3.1.1
lucide-react@0.453.0(react@18.3.1): lucide-react@0.461.0(react@18.3.1):
dependencies: dependencies:
react: 18.3.1 react: 18.3.1
@@ -4708,9 +4716,10 @@ snapshots:
yaml@2.6.0: {} 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: optionalDependencies:
'@types/react': 18.3.11 '@types/react': 18.3.11
immer: 10.1.1
react: 18.3.1 react: 18.3.1
zwitch@2.0.4: {} zwitch@2.0.4: {}

View File

@@ -47,14 +47,20 @@ export const tauriFetch = async <T = any>({
const auth = authStore?.state?.auth const auth = authStore?.state?.auth
console.log("auth", 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") { if (method !== "GET") {
headers["Content-Type"] = "application/json"; 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 // debug API
const requestInfo = { const requestInfo = {

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View File

@@ -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 source_default_dark_img from "@/assets/images/source_default_dark.png";
import file_efault_img from "@/assets/images/file_efault.png"; import file_efault_img from "@/assets/images/file_efault.png";
import { useTheme } from "@/contexts/ThemeContext"; import { useTheme } from "@/contexts/ThemeContext";
import { useConnectStore } from "@/stores/connectStore";
type ISearchData = Record<string, any[]>; type ISearchData = Record<string, any[]>;
@@ -46,8 +47,9 @@ function DropdownList({
const { theme } = useTheme(); const { theme } = useTheme();
const connector_data = useAppStore((state) => state.connector_data); const connector_data = useConnectStore((state) => state.connector_data);
const datasourceData = useAppStore((state) => state.datasourceData); const datasourceData = useConnectStore((state) => state.datasourceData);
const endpoint_http = useAppStore((state) => state.endpoint_http); const endpoint_http = useAppStore((state) => state.endpoint_http);
const setSourceData = useSearchStore((state) => state.setSourceData); const setSourceData = useSearchStore((state) => state.setSourceData);
@@ -163,13 +165,13 @@ function DropdownList({
function findConnectorIcon(item: any) { function findConnectorIcon(item: any) {
const id = item?._source?.source?.id || ""; const id = item?._source?.source?.id || "";
const result_source = datasourceData.find( const result_source = datasourceData[endpoint_http]?.find(
(data: any) => data._source.id === id (data: any) => data._source.id === id
); );
const connector_id = result_source?._source?.connector?.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 (data: any) => data._source.id === connector_id
); );
@@ -184,7 +186,7 @@ function DropdownList({
return theme === "dark" ? source_default_dark_img : source_default_img; return theme === "dark" ? source_default_dark_img : source_default_img;
} }
if (icons?.includes("http")) { if (icons?.startsWith("http://") || icons?.startsWith("https://")) {
return icons; return icons;
} else { } else {
return endpoint_http + icons; return endpoint_http + icons;
@@ -201,7 +203,7 @@ function DropdownList({
return file_efault_img; return file_efault_img;
} }
if (selectedIcon?.includes("http")) { if (selectedIcon?.startsWith("http://") || selectedIcon?.startsWith("https://")) {
return selectedIcon; return selectedIcon;
} else { } else {
return endpoint_http + selectedIcon; return endpoint_http + selectedIcon;
@@ -218,7 +220,7 @@ function DropdownList({
return theme === "dark" ? source_default_dark_img : source_default_img; return theme === "dark" ? source_default_dark_img : source_default_img;
} }
if (selectedIcon?.includes("http")) { if (selectedIcon?.startsWith("http://") || selectedIcon?.startsWith("https://")) {
return selectedIcon; return selectedIcon;
} else { } else {
return endpoint_http + selectedIcon; return endpoint_http + selectedIcon;

View File

@@ -11,6 +11,7 @@ import source_default_dark_img from "@/assets/images/source_default_dark.png";
import { useSearchStore } from "@/stores/searchStore"; import { useSearchStore } from "@/stores/searchStore";
import { useAppStore } from "@/stores/appStore"; import { useAppStore } from "@/stores/appStore";
import { useTheme } from "@/contexts/ThemeContext"; import { useTheme } from "@/contexts/ThemeContext";
import { useConnectStore } from "@/stores/connectStore";
interface FooterProps { interface FooterProps {
isChat: boolean; isChat: boolean;
@@ -19,8 +20,10 @@ interface FooterProps {
export default function Footer({ name }: FooterProps) { export default function Footer({ name }: FooterProps) {
const sourceData = useSearchStore((state) => state.sourceData); 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 endpoint_http = useAppStore((state) => state.endpoint_http);
const { theme } = useTheme(); const { theme } = useTheme();
@@ -28,13 +31,13 @@ export default function Footer({ name }: FooterProps) {
function findConnectorIcon(item: any) { function findConnectorIcon(item: any) {
const id = item?._source?.source?.id || ""; const id = item?._source?.source?.id || "";
const result_source = datasourceData.find( const result_source = datasourceData[endpoint_http]?.find(
(data: any) => data._source.id === id (data: any) => data._source.id === id
); );
const connector_id = result_source?._source?.connector?.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 (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; return theme === "dark" ? source_default_dark_img : source_default_img;
} }
if (icons?.includes("http")) { if (icons?.startsWith("http://") || icons?.startsWith("https://")) {
return icons; return icons;
} else { } else {
return endpoint_http + icons; return endpoint_http + icons;

View File

@@ -70,14 +70,22 @@ export default function ChatInput({
} }
}, [inputValue, disabled, onSend]); }, [inputValue, disabled, onSend]);
const pressedKeys = new Set<string>();
const handleKeyDown = useCallback( const handleKeyDown = useCallback(
(e: KeyboardEvent) => { (e: KeyboardEvent) => {
pressedKeys.add(e.code);
if (e.code === "MetaLeft" || e.code === "MetaRight") { if (e.code === "MetaLeft" || e.code === "MetaRight") {
setIsCommandPressed(true); setIsCommandPressed(true);
} }
if (e.metaKey) { if (pressedKeys.has("MetaLeft") || pressedKeys.has("MetaRight")) {
e.preventDefault();
switch (e.code) { switch (e.code) {
case "Comma":
setIsCommandPressed(false);
break;
case "KeyI": case "KeyI":
handleToggleFocus(); handleToggleFocus();
break; break;
@@ -88,7 +96,7 @@ export default function ChatInput({
console.log("KeyM"); console.log("KeyM");
break; break;
case "Enter": case "Enter":
isChatMode && (curChatEnd ? handleSubmit() : disabledChange()); isChatMode && (curChatEnd ? handleSubmit() : disabledChange?.());
break; break;
case "KeyO": case "KeyO":
console.log("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) => { const handleKeyUp = useCallback((e: KeyboardEvent) => {
pressedKeys.delete(e.code);
if (e.code === "MetaLeft" || e.code === "MetaRight") { if (e.code === "MetaLeft" || e.code === "MetaRight") {
setIsCommandPressed(false); setIsCommandPressed(false);
} }

View File

@@ -113,7 +113,7 @@ function Search({ isChatMode, input }: SearchProps) {
}; };
} }
const debouncedSearch = useCallback(debounce(getSuggest, 300), [input]); const debouncedSearch = useCallback(debounce(getSuggest, 500), [input]);
useEffect(() => { useEffect(() => {
!isChatMode && !sourceData && debouncedSearch(); !isChatMode && !sourceData && debouncedSearch();

View File

@@ -1,7 +1,18 @@
import { useState, useEffect } from "react"; import { useState, useEffect, useCallback } from "react";
import { Cloud } from "lucide-react"; import {
RefreshCcw,
Globe,
PackageOpen,
GitFork,
CalendarSync,
Trash2,
} from "lucide-react";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import { getCurrentWindow } from "@tauri-apps/api/window"; import { getCurrentWindow } from "@tauri-apps/api/window";
import {
onOpenUrl,
getCurrent as getCurrentDeepLinkUrls,
} from "@tauri-apps/plugin-deep-link";
import { UserProfile } from "./UserProfile"; import { UserProfile } from "./UserProfile";
import { DataSourcesList } from "./DataSourcesList"; import { DataSourcesList } from "./DataSourcesList";
@@ -11,84 +22,105 @@ import { OpenBrowserURL } from "@/utils/index";
import { useAppStore } from "@/stores/appStore"; import { useAppStore } from "@/stores/appStore";
import { useAuthStore } from "@/stores/authStore"; import { useAuthStore } from "@/stores/authStore";
import { tauriFetch } from "@/api/tauriFetchClient"; import { tauriFetch } from "@/api/tauriFetchClient";
import { import { useConnectStore } from "@/stores/connectStore";
onOpenUrl, import bannerImg from "@/assets/images/coco-cloud-banner.jpeg";
getCurrent as getCurrentDeepLinkUrls,
} from "@tauri-apps/plugin-deep-link";
export default function CocoCloud() { export default function CocoCloud() {
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [isConnect] = useState(true); const [isConnect, setIsConnect] = useState(true);
const app_uid = useAppStore((state) => state.app_uid); const app_uid = useAppStore((state) => state.app_uid);
const setAppUid = useAppStore((state) => state.setAppUid); 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 userInfo = useAuthStore((state) => state.userInfo);
const setUserInfo = useAuthStore((state) => state.setUserInfo); 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 [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({ const response: any = await tauriFetch({
url: `/provider/account/profile`, url: `/account/profile`,
method: "GET", method: "GET",
}); });
console.log("getProfile", response); console.log("getProfile", response);
setUserInfo(response.data || {}); setUserInfo(response.data || {}, endpoint);
}; }, [endpoint]);
const handleOAuthCallback = async ( const handleOAuthCallback = useCallback(
code: string | null, async (code: string | null, provider: string | null) => {
provider: string | null if (!code) {
) => { setError("No authorization code received");
if (!code) { return;
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);
} }
getCurrentWindow() try {
.setFocus() console.log("Handling OAuth callback:", { code, provider });
.catch(() => {}); const response: any = await tauriFetch({
} catch (e) { url: `/auth/request_access_token?request_id=${app_uid}`,
console.error("Sign in failed:", error); method: "GET",
setError("Sign in failed: catch"); headers: {
await setAuth(undefined); "X-API-TOKEN": code,
throw error; },
} finally { });
setLoading(false); 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) => { const handleUrl = (url: string) => {
try { try {
@@ -108,7 +140,6 @@ export default function CocoCloud() {
// default: // default:
// console.log("Unhandled deep link path:", urlObject.pathname); // console.log("Unhandled deep link path:", urlObject.pathname);
// } // }
} catch (err) { } catch (err) {
console.error("Failed to parse URL:", err); console.error("Failed to parse URL:", err);
setError("Invalid URL format"); setError("Invalid URL format");
@@ -135,87 +166,167 @@ export default function CocoCloud() {
return () => { return () => {
unlisten.then((fn) => fn()); unlisten.then((fn) => fn());
}; };
}, []); }, [app_uid]);
const LoginClick = useCallback(() => {
if (loading) return;
setAuth(undefined, endpoint);
function LoginClick() {
let uid = uuidv4(); let uid = uuidv4();
setAppUid(uid); setAppUid(uid);
console.log("LoginClick", uid, currentService.auth_provider.sso.url);
OpenBrowserURL( 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); setLoading(true);
}, [JSON.stringify(currentService)]);
function goToHref(url: string) {
OpenBrowserURL(url);
} }
return ( const refreshClick = useCallback(() => {
<div className="flex min-h-screen bg-gray-50"> setRefreshLoading(true);
<Sidebar /> 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)]);
<main className="flex-1"> function addService() {
setIsConnect(false);
}
const deleteClick = useCallback(() => {
deleteOtherService(currentService);
setAuth(undefined, endpoint);
setUserInfo({}, endpoint);
}, [JSON.stringify(currentService), endpoint]);
return (
<div className="flex bg-gray-50 dark:bg-gray-900">
<Sidebar addService={addService} />
<main className="flex-1 p-4 py-8">
<div> <div>
{error && ( {error && (
<div className="text-red-500 dark:text-red-400 p-4"> <div className="text-red-500 dark:text-red-400 mb-4">
Error: {error} Error: {error}
</div> </div>
)} )}
</div> </div>
{isConnect ? ( {isConnect ? (
<div className="max-w-4xl mx-auto px-4 py-8"> <div className="max-w-4xl mx-auto">
<div className="flex items-center justify-between mb-8"> <div className="w-full rounded-[4px] bg-[rgba(229,229,229,1)] dark:bg-gray-800 mb-6">
<img
src={currentService.provider.banner || bannerImg}
alt="banner"
/>
</div>
<div className="flex items-center justify-between mb-4">
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
<div className="flex items-center space-x-2 px-4 py-2 bg-white rounded-md border border-gray-200"> <div className="flex items-center text-gray-900 dark:text-white font-medium">
<Cloud className="w-5 h-5 text-blue-500" /> {currentService.name}
<span className="font-medium text-[#333]">Coco Cloud</span>
</div> </div>
<span className="px-3 py-1 text-sm text-blue-600 bg-blue-50 rounded-md">
Available
</span>
</div> </div>
<button className="p-2 text-gray-500 hover:text-gray-700"> <div className="flex gap-2">
<Cloud className="w-5 h-5" /> <button
</button> className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 rounded-[6px] bg-white dark:bg-gray-800 border border-[rgba(228,229,239,1)] dark:border-gray-700"
onClick={() => goToHref(currentService.provider.website)}
>
<Globe className="w-3.5 h-3.5" />
</button>
<button
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 rounded-[6px] bg-white dark:bg-gray-800 border border-[rgba(228,229,239,1)] dark:border-gray-700"
onClick={() => refreshClick()}
>
<RefreshCcw
className={`w-3.5 h-3.5 ${
refreshLoading ? "animate-spin" : ""
}`}
/>
</button>
{currentService.endpoint !== defaultService.endpoint ? (
<button
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 rounded-[6px] bg-white dark:bg-gray-800 border border-[rgba(228,229,239,1)] dark:border-gray-700"
onClick={() => deleteClick()}
>
<Trash2 className="w-3.5 h-3.5 text-[#ff4747]" />
</button>
) : null}
</div>
</div> </div>
<div className="mb-8"> <div className="mb-8">
<div className="text-sm text-gray-500 mb-4"> <div className="text-sm text-gray-500 dark:text-gray-400 mb-2 flex">
<span>Service provision: INFINI Labs</span> <span className="flex items-center gap-1">
<PackageOpen className="w-4 h-4" />{" "}
{currentService.provider.name}
</span>
<span className="mx-4">|</span> <span className="mx-4">|</span>
<span>Version Number: v2.3.0</span> <span className="flex items-center gap-1">
<GitFork className="w-4 h-4" />{" "}
{currentService.version.number}
</span>
<span className="mx-4">|</span> <span className="mx-4">|</span>
<span>Update time: 2023-05-12</span> <span className="flex items-center gap-1">
<CalendarSync className="w-4 h-4" /> {currentService.updated}
</span>
</div> </div>
<p className="text-gray-600 leading-relaxed"> <p className="text-gray-600 dark:text-gray-300 leading-relaxed">
Coco Cloud provides users with a cloud storage and data {currentService.provider.description}
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.
</p> </p>
</div> </div>
<div className="mb-8"> {currentService.auth_provider.sso.url ? (
<h2 className="text-lg font-medium text-gray-900 mb-4"> <div className="mb-8">
Account Information <h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
</h2> Account Information
{auth ? ( </h2>
<UserProfile userInfo={userInfo} /> {auth && auth[endpoint] ? (
) : ( <UserProfile userInfo={userInfo[endpoint]} />
<button ) : (
className="px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors" <div>
onClick={LoginClick} <button
> className="px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors mb-3"
{loading ? "Login..." : "Login"} onClick={LoginClick}
</button> >
)} {loading ? "Login..." : "Login"}
</div> </button>
<button
className="text-xs text-[#0096FB] dark:text-blue-400 block"
onClick={() =>
goToHref(currentService.provider.privacy_policy)
}
>
EULA | Privacy Policy
</button>
</div>
)}
</div>
) : null}
{auth ? <DataSourcesList /> : null} {auth && auth[endpoint] ? <DataSourcesList /> : null}
</div> </div>
) : ( ) : (
<ConnectService /> <ConnectService setIsConnect={setIsConnect} />
)} )}
</main> </main>
</div> </div>

View File

@@ -1,53 +1,122 @@
import React, { useState } from 'react'; import React, { useState, useCallback } from "react";
import { ArrowLeft } from 'lucide-react'; import { ChevronLeft } from "lucide-react";
export function ConnectService() { import { useConnectStore } from "@/stores/connectStore";
const [sourceName, setSourceName] = useState(''); 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) => { const handleSubmit = (e: React.FormEvent) => {
e.preventDefault(); 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 ( return (
<div className="p-8 max-w-4xl"> <div className="max-w-4xl">
<div className="mb-8"> <div className="flex items-center gap-2 mb-8">
<button className="flex items-center text-gray-600 hover:text-gray-900"> <button
<ArrowLeft className="w-5 h-5 mr-2" /> className=" text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 border border-[rgba(228,229,239,1)] dark:border-gray-700 p-1"
<span>Connect Google Drive</span> onClick={goBack}
>
<ChevronLeft className="w-4 h-4" />
</button> </button>
<div className="text-xl text-[#101010] dark:text-white">
Connecting to third-party services
</div>
</div> </div>
<div className="mb-8"> <div className="mb-8">
<p className="text-gray-600"> <p className="text-gray-600 dark:text-gray-400">
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.
</p> </p>
</div> </div>
<form onSubmit={handleSubmit} className="space-y-6"> <form onSubmit={handleSubmit} className="space-y-6">
<div> <div>
<label htmlFor="sourceName" className="block text-sm font-medium text-gray-700 mb-1"> <label
Data Source Name htmlFor="endpoint"
</label> className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2.5"
<input
type="text"
id="sourceName"
value={sourceName}
onChange={(e) => 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"
/>
</div>
<div className="flex justify-end">
<button
type="submit"
className="px-6 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
> >
Save Server address
</button> </label>
<div className="flex gap-2">
<input
type="text"
id="endpoint"
value={endpointLink}
placeholder="For example: https://coco.infini.cloud/"
onChange={(e) => 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"
/>
<button
type="submit"
className="px-6 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
onClick={addService}
>
{refreshLoading ? "Connecting..." : "Connect"}
</button>
</div>
</div> </div>
</form> </form>
</div> </div>
); );
} }

View File

@@ -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 { interface Account {
email: string; email: string;
@@ -7,56 +13,88 @@ interface Account {
interface DataSourceItemProps { interface DataSourceItemProps {
name: string; name: string;
type: string; connector: any;
accounts: Account[]; accounts?: Account[];
} }
export function DataSourceItem({ name, type, accounts }: DataSourceItemProps) { export function DataSourceItem({ name, connector }: DataSourceItemProps) {
const isConnected = accounts.length > 0; // 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 ( return (
<div className="border border-gray-200 rounded-lg p-4"> <div className="border border-gray-200 dark:border-gray-700 rounded-lg p-4 bg-white dark:bg-gray-800">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
<img src={`/icons/${type}.svg`} alt={name} className="w-6 h-6" /> <img src={getTypeIcon()} alt={name} className="w-6 h-6" />
<span className="font-medium">{name}</span> <span className="font-medium text-gray-900 dark:text-white">
{name}
</span>
</div> </div>
<button className="text-blue-500 hover:text-blue-600 flex items-center space-x-1"> <button className="text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300 flex items-center space-x-1">
<Link2 className="w-4 h-4" /> <Link2 className="w-4 h-4" />
</button> </button>
</div> </div>
<div className="text-sm text-gray-500 mb-2"> {/* <div className="text-sm text-gray-500 dark:text-gray-400 mb-2">
{isConnected ? "Manage" : "Connect Accounts"} {isConnected ? "Manage" : "Connect Accounts"}
</div> </div> */}
{accounts.map((account, index) => ( {/* {accounts.map((account, index) => (
<div <div
key={account.email} key={account.email}
className="flex items-center justify-between py-2 border-t border-gray-100" className="flex items-center justify-between py-2 border-t border-gray-100 dark:border-gray-700"
> >
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<div className="w-8 h-8 bg-gray-100 rounded-full flex items-center justify-center"> <div className="w-8 h-8 bg-gray-100 dark:bg-gray-700 rounded-full flex items-center justify-center">
<span className="text-sm text-gray-500"> <span className="text-sm text-gray-500 dark:text-gray-400">
{account.email[0].toUpperCase()} {account.email[0].toUpperCase()}
</span> </span>
</div> </div>
<div> <div>
<div className="text-sm font-medium"> <div className="text-sm font-medium text-gray-900 dark:text-white">
{index === 0 ? "My network disk" : `Network disk ${index + 1}`} {index === 0 ? "My network disk" : `Network disk ${index + 1}`}
</div> </div>
<div className="text-xs text-gray-500">{account.email}</div> <div className="text-xs text-gray-500 dark:text-gray-400">{account.email}</div>
</div> </div>
</div> </div>
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<span className="text-xs text-gray-500"> <span className="text-xs text-gray-500 dark:text-gray-400">
Recently Synced: {account.lastSync} Recently Synced: {account.lastSync}
</span> </span>
<button className="text-gray-400 hover:text-gray-600"> <button className="text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300">
<Trash2 className="w-4 h-4" /> <Trash2 className="w-4 h-4" />
</button> </button>
</div> </div>
</div> </div>
))} ))} */}
</div> </div>
); );
} }

View File

@@ -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() { export function DataSourcesList() {
const dataSources = [ const datasourceData = useConnectStore((state) => state.datasourceData);
{ const setDatasourceData = useConnectStore((state) => state.setDatasourceData);
id: 'google-drive',
name: 'Google Drive', const endpoint_http = useAppStore((state) => state.endpoint_http);
type: 'google',
accounts: [ const [refreshLoading, setRefreshLoading] = useState(false);
{ email: 'an121245@gmail.com', lastSync: '2025-01-02 09:50 AM' },
{ email: '9paiii@gmail.com', lastSync: '2025-01-02 09:50 AM' } async function getDatasourceData() {
] setRefreshLoading(true);
}, try {
{ const response = await tauriFetch({
id: 'yuque', url: `/datasource/_search`,
name: 'Yuque', method: "GET",
type: 'yuque', });
accounts: [] console.log("datasource", response);
}, const data = response.data?.hits?.hits || [];
{ setDatasourceData(data, endpoint_http);
id: 'github', } catch (error) {
name: 'Github', console.error("Failed to fetch user data:", error);
type: 'github',
accounts: []
} }
]; setRefreshLoading(false);
}
useEffect(() => {
getDatasourceData()
}, [])
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<h2 className="text-xl font-medium text-gray-900">Data Source</h2> <h2 className="flex justify-between text-xl font-medium text-gray-900 dark:text-white">
Data Source
<button
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 rounded-[6px] bg-white dark:bg-gray-800 border border-[rgba(228,229,239,1)] dark:border-gray-700"
onClick={() => getDatasourceData()}
>
<RefreshCcw
className={`w-3.5 h-3.5 ${refreshLoading ? "animate-spin" : ""}`}
/>
</button>
</h2>
<div className="space-y-4"> <div className="space-y-4">
{dataSources.map(source => ( {datasourceData[endpoint_http]?.map((source) => (
<DataSourceItem key={source.id} {...source} /> <DataSourceItem key={source._id} {...source._source} />
))} ))}
</div> </div>
</div> </div>
); );
} }

View File

@@ -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<StringBooleanMap>({});
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 ( return (
<div className="w-64 border-r border-gray-200 bg-white"> <div className="w-64 min-h-[550px] border-r border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800">
<div className="p-4"> <div className="p-4 py-8">
<div className="flex items-center space-x-2 px-3 py-2 bg-blue-50 text-blue-600 rounded-lg mb-6"> <div
<Cloud className="w-5 h-5" /> className={`flex items-center space-x-2 px-3 py-2 bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400 rounded-lg mb-6 ${
<span className="font-medium">Coco Cloud</span> currentService.endpoint === defaultService.endpoint
? "border border-[rgba(0,135,255,1)]"
: ""
}`}
onClick={() => {
setCurrentService(defaultService);
setEndpoint(defaultService.endpoint);
getDefaultHealth();
}}
>
<img
src={defaultService.provider.icon || cocoLogoImg}
alt="cocoLogoImg"
className="w-5 h-5"
/>
<span className="font-medium">{defaultService.name}</span>
<div className="flex-1" /> <div className="flex-1" />
<button className="text-blue-600 hover:text-blue-700"> <button className="text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300">
<Cloud className="w-4 h-4" /> {defaultHealth ? (
<div className="w-3 h-3 rounded-full bg-[#00DB5E]"></div>
) : (
<div className="w-3 h-3 rounded-full bg-[#FF4747]"></div>
)}
</button> </button>
</div> </div>
<div className="space-y-2"> <div className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">
<div className="text-sm font-medium text-gray-500 px-3 mb-2"> Third-party services
Third-party services </div>
</div>
<button className="w-full flex items-center justify-center p-2 border-2 border-dashed border-gray-200 rounded-lg text-gray-400 hover:text-gray-600 hover:border-gray-300"> {otherServices?.map((item, index) => (
<div
key={item.name + index}
className={`flex items-center space-x-2 px-3 py-2 bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400 rounded-lg mb-2 ${
currentService.endpoint === item.endpoint
? "border border-[rgba(0,135,255,1)]"
: ""
}`}
onClick={() => {
setEndpoint(item.endpoint);
setCurrentService(item);
getOtherHealth(item);
}}
>
<img
src={item.provider.icon || cocoLogoImg}
alt="LogoImg"
className="w-5 h-5"
/>
<span className="font-medium">{item.name}</span>
<div className="flex-1" />
<button className="text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300">
{otherHealth[item.endpoint] ? (
<div className="w-3 h-3 rounded-full bg-[#00DB5E]"></div>
) : (
<div className="w-3 h-3 rounded-full bg-[#FF4747]"></div>
)}
</button>
</div>
))}
<div className="space-y-2">
<button
className="w-full flex items-center justify-center p-2 border-2 border-dashed border-gray-200 dark:border-gray-700 rounded-lg text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-600"
onClick={addServiceClick}
>
<Plus className="w-5 h-5" /> <Plus className="w-5 h-5" />
</button> </button>
</div> </div>

View File

@@ -1,13 +1,14 @@
import { User, Edit, LogOut } from "lucide-react"; import { User, LogOut } from "lucide-react";
import { useAuthStore } from "@/stores/authStore"; import { useAuthStore } from "@/stores/authStore";
import { useAppStore } from "@/stores/appStore";
interface UserPreferences { interface UserPreferences {
theme: "dark" | "light"; theme: "dark" | "light";
language: string; language: string;
} }
interface UserInfo { interface UserInfo {
username: string; name: string;
email: string; email: string;
avatar?: string; avatar?: string;
roles: string[]; // ["admin", "editor"] roles: string[]; // ["admin", "editor"]
@@ -21,45 +22,40 @@ interface UserProfileProps {
export function UserProfile({ userInfo }: UserProfileProps) { export function UserProfile({ userInfo }: UserProfileProps) {
const setAuth = useAuthStore((state) => state.setAuth); const setAuth = useAuthStore((state) => state.setAuth);
const setUserInfo = useAuthStore((state) => state.setUserInfo); const setUserInfo = useAuthStore((state) => state.setUserInfo);
const endpoint = useAppStore((state) => state.endpoint);
const handleLogout = () => { const handleLogout = () => {
setAuth(undefined); setAuth(undefined, endpoint);
setUserInfo({}); setUserInfo({}, endpoint);
}; };
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<div className="w-12 h-12 bg-gray-100 rounded-full flex items-center justify-center"> <div className="w-12 h-12 bg-gray-100 dark:bg-gray-700 rounded-full flex items-center justify-center">
{userInfo.avatar ? ( {userInfo?.avatar ? (
<img <img src={userInfo?.avatar} alt="" className="w-6 h-6" />
src={userInfo.avatar}
alt=""
className="w-6 h-6"
/>
) : ( ) : (
<User className="w-6 h-6 text-gray-500" /> <User className="w-6 h-6 text-gray-500 dark:text-gray-400" />
)} )}
</div> </div>
<div className="flex-1"> <div className="flex-1">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-4">
<span className="font-medium text-gray-900"> <span className="font-medium text-gray-900 dark:text-white">
{userInfo.username || "-"} {userInfo?.name || "-"}
</span> </span>
<button className="text-gray-400 hover:text-gray-600"> <button
<Edit className="w-4 h-4" /> onClick={handleLogout}
className="flex items-center p-1 text-red-500 hover:text-red-600 dark:text-red-400 dark:hover:text-red-300 border border-[rgba(228,229,239,1)] dark:border-gray-700"
>
<LogOut className="w-4 h-4" />
</button> </button>
</div> </div>
<span className="text-sm text-gray-500">{userInfo.email || "-"}</span> <span className="text-sm text-gray-500 dark:text-gray-400">
{userInfo?.email || "-"}
</span>
</div> </div>
</div> </div>
<button
onClick={handleLogout}
className="flex items-center space-x-1 text-red-500 hover:text-red-600"
>
<LogOut className="w-4 h-4" />
<span>Logout</span>
</button>
</div> </div>
); );
} }

View File

@@ -57,8 +57,8 @@ export default function ChatInput() {
method: "GET", method: "GET",
}); });
setInfo(JSON.stringify(response)); setInfo(JSON.stringify(response));
console.log(response.status); // e.g. 200 // console.log(response.status); // e.g. 200
console.log(response.statusText); // e.g. "OK" // console.log(response.statusText); // e.g. "OK"
} catch (error) { } catch (error) {
console.error("Error sending message:", error); console.error("Error sending message:", error);
setInfo(JSON.stringify(error)); setInfo(JSON.stringify(error));

View File

@@ -225,7 +225,7 @@ const ChatAI = forwardRef<ChatAIRef, ChatAIProps>(
if (isTauri()) { if (isTauri()) {
createWin && createWin({ createWin && createWin({
label: "chat", label: "chat",
title: "Coco AI", title: "Coco Chat",
dragDropEnabled: true, dragDropEnabled: true,
center: true, center: true,
width: 900, width: 900,

View File

@@ -14,7 +14,7 @@ const ChatSwitch: React.FC<ChatSwitchProps> = ({ isChatMode, onChange }) => {
(event: KeyboardEvent) => { (event: KeyboardEvent) => {
if (event.metaKey && event.key === "t") { if (event.metaKey && event.key === "t") {
event.preventDefault(); event.preventDefault();
console.log("Switch mode triggered"); // console.log("Switch mode triggered");
handleToggle(); handleToggle();
} }
}, },

View File

@@ -5,14 +5,16 @@ import {formatter} from "@/utils/index"
import source_default_img from "@/assets/images/source_default.png"; import source_default_img from "@/assets/images/source_default.png";
import source_default_dark_img from "@/assets/images/source_default_dark.png"; import source_default_dark_img from "@/assets/images/source_default_dark.png";
import { useTheme } from "@/contexts/ThemeContext"; import { useTheme } from "@/contexts/ThemeContext";
import { useConnectStore } from "@/stores/connectStore";
interface DocumentDetailProps { interface DocumentDetailProps {
document: any; document: any;
} }
export const DocumentDetail: React.FC<DocumentDetailProps> = ({ document }) => { export const DocumentDetail: React.FC<DocumentDetailProps> = ({ document }) => {
const connector_data = useAppStore((state) => state.connector_data); const connector_data = useConnectStore((state) => state.connector_data);
const datasourceData = useAppStore((state) => state.datasourceData); const datasourceData = useConnectStore((state) => state.datasourceData);
const endpoint_http = useAppStore((state) => state.endpoint_http); const endpoint_http = useAppStore((state) => state.endpoint_http);
const { theme } = useTheme(); const { theme } = useTheme();
@@ -20,13 +22,13 @@ export const DocumentDetail: React.FC<DocumentDetailProps> = ({ document }) => {
function findConnectorIcon(item: any) { function findConnectorIcon(item: any) {
const id = item?._source?.source?.id || ""; const id = item?._source?.source?.id || "";
const result_source = datasourceData.find( const result_source = datasourceData[endpoint_http]?.find(
(data: any) => data._source.id === id (data: any) => data._source.id === id
); );
const connector_id = result_source?._source?.connector?.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 (data: any) => data._source.id === connector_id
); );
@@ -41,7 +43,7 @@ export const DocumentDetail: React.FC<DocumentDetailProps> = ({ document }) => {
return theme === "dark" ? source_default_dark_img : source_default_img; return theme === "dark" ? source_default_dark_img : source_default_img;
} }
if (icons?.includes("http")) { if (icons?.startsWith("http://") || icons?.startsWith("https://")) {
return icons; return icons;
} else { } else {
return endpoint_http + icons; return endpoint_http + icons;

View File

@@ -9,6 +9,7 @@ import { useSearchStore } from "@/stores/searchStore";
import { SearchHeader } from "./SearchHeader"; import { SearchHeader } from "./SearchHeader";
import file_efault_img from "@/assets/images/file_efault.png"; import file_efault_img from "@/assets/images/file_efault.png";
import noDataImg from "@/assets/coconut-tree.png"; import noDataImg from "@/assets/coconut-tree.png";
import { useConnectStore } from "@/stores/connectStore";
interface DocumentListProps { interface DocumentListProps {
onSelectDocument: (id: string) => void; onSelectDocument: (id: string) => void;
@@ -25,8 +26,9 @@ export const DocumentList: React.FC<DocumentListProps> = ({
getDocDetail, getDocDetail,
isChatMode, isChatMode,
}) => { }) => {
const connector_data = useAppStore((state) => state.connector_data); const connector_data = useConnectStore((state) => state.connector_data);
const datasourceData = useAppStore((state) => state.datasourceData); const datasourceData = useConnectStore((state) => state.datasourceData);
const sourceData = useSearchStore((state) => state.sourceData); const sourceData = useSearchStore((state) => state.sourceData);
const endpoint_http = useAppStore((state) => state.endpoint_http); const endpoint_http = useAppStore((state) => state.endpoint_http);
@@ -109,13 +111,13 @@ export const DocumentList: React.FC<DocumentListProps> = ({
function findConnectorIcon(item: any) { function findConnectorIcon(item: any) {
const id = item?._source?.source?.id || ""; const id = item?._source?.source?.id || "";
const result_source = datasourceData.find( const result_source = datasourceData[endpoint_http]?.find(
(data: any) => data._source.id === id (data: any) => data._source.id === id
); );
const connector_id = result_source?._source?.connector?.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 (data: any) => data._source.id === connector_id
); );
@@ -132,7 +134,7 @@ export const DocumentList: React.FC<DocumentListProps> = ({
return file_efault_img; return file_efault_img;
} }
if (selectedIcon?.includes("http")) { if (selectedIcon?.startsWith("http://") || selectedIcon?.startsWith("https://")) {
return selectedIcon; return selectedIcon;
} else { } else {
return endpoint_http + selectedIcon; return endpoint_http + selectedIcon;

View File

@@ -19,7 +19,7 @@ function DropdownList({ selected, suggests }: DropdownListProps) {
try { try {
if (isTauri()) { if (isTauri()) {
await open(url); await open(url);
console.log("URL opened in default browser"); // console.log("URL opened in default browser");
} }
} catch (error) { } catch (error) {
console.error("Failed to open URL:", error); console.error("Failed to open URL:", error);
@@ -27,12 +27,12 @@ function DropdownList({ selected, suggests }: DropdownListProps) {
}; };
const handleKeyDown = (e: KeyboardEvent) => { const handleKeyDown = (e: KeyboardEvent) => {
console.log( // console.log(
"handleKeyDown", // "handleKeyDown",
e.key, // e.key,
showIndex, // showIndex,
e.key >= "0" && e.key <= "9" && showIndex // e.key >= "0" && e.key <= "9" && showIndex
); // );
if (!suggests.length) return; if (!suggests.length) return;
if (e.key === "ArrowUp") { if (e.key === "ArrowUp") {
@@ -51,7 +51,7 @@ function DropdownList({ selected, suggests }: DropdownListProps) {
} }
if (e.key === "Enter" && selectedItem !== null) { if (e.key === "Enter" && selectedItem !== null) {
console.log("Enter key pressed", selectedItem); // console.log("Enter key pressed", selectedItem);
const item = suggests[selectedItem]; const item = suggests[selectedItem];
if (item?._source?.url) { if (item?._source?.url) {
handleOpenURL(item?._source?.url); handleOpenURL(item?._source?.url);
@@ -61,7 +61,7 @@ function DropdownList({ selected, suggests }: DropdownListProps) {
} }
if (e.key >= "0" && e.key <= "9" && showIndex) { 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)]; const item = suggests[parseInt(e.key, 10)];
if (item?._source?.url) { if (item?._source?.url) {
handleOpenURL(item?._source?.url); handleOpenURL(item?._source?.url);
@@ -72,7 +72,7 @@ function DropdownList({ selected, suggests }: DropdownListProps) {
}; };
const handleKeyUp = (e: KeyboardEvent) => { const handleKeyUp = (e: KeyboardEvent) => {
console.log("handleKeyUp", e.key); // console.log("handleKeyUp", e.key);
if (!suggests.length) return; if (!suggests.length) return;
if (!e.metaKey) { if (!e.metaKey) {

View File

@@ -29,7 +29,7 @@ export default function Account() {
const setupAuthListener = async () => { const setupAuthListener = async () => {
try { try {
if (!auth) { if (!(auth && auth[endpoint_http])) {
// Replace the current route with signin // Replace the current route with signin
// navigate("/signin", { replace: true }); // navigate("/signin", { replace: true });
} }
@@ -55,7 +55,7 @@ export default function Account() {
cleanup(); cleanup();
}; };
}, [auth]); }, [JSON.stringify(auth)]);
async function signIn() { async function signIn() {
let res: (url: URL) => void; let res: (url: URL) => void;
@@ -114,7 +114,7 @@ export default function Account() {
user_id, user_id,
expires, expires,
plan: { upgraded: false, last_checked: 0 }, plan: { upgraded: false, last_checked: 0 },
}); }, endpoint_http);
getCurrentWindow() getCurrentWindow()
.setFocus() .setFocus()
@@ -123,7 +123,7 @@ export default function Account() {
return navigate("/"); return navigate("/");
} catch (error) { } catch (error) {
console.error("Sign in failed:", error); console.error("Sign in failed:", error);
await setAuth(undefined); await setAuth(undefined, endpoint_http);
throw error; throw error;
} }
} }

View File

@@ -7,6 +7,7 @@ import {
Sun, Sun,
Power, Power,
Tags, Tags,
// Trash2,
} from "lucide-react"; } from "lucide-react";
import { isTauri, invoke } from "@tauri-apps/api/core"; import { isTauri, invoke } from "@tauri-apps/api/core";
import { import {
@@ -21,6 +22,8 @@ import { Shortcut } from "./shortcut";
import { useShortcutEditor } from "@/hooks/useShortcutEditor"; import { useShortcutEditor } from "@/hooks/useShortcutEditor";
import { ThemeOption } from "./index2"; import { ThemeOption } from "./index2";
import { useAppStore } from "@/stores/appStore"; import { useAppStore } from "@/stores/appStore";
// import { useAuthStore } from "@/stores/authStore";
// import { useConnectStore } from "@/stores/connectStore";
export default function GeneralSettings() { export default function GeneralSettings() {
const [launchAtLogin, setLaunchAtLogin] = useState(true); const [launchAtLogin, setLaunchAtLogin] = useState(true);
@@ -28,6 +31,10 @@ export default function GeneralSettings() {
const showTooltip = useAppStore((state) => state.showTooltip); const showTooltip = useAppStore((state) => state.showTooltip);
const setShowTooltip = useAppStore((state) => state.setShowTooltip); 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(() => { useEffect(() => {
const fetchAutoStartStatus = async () => { const fetchAutoStartStatus = async () => {
if (isTauri()) { if (isTauri()) {
@@ -83,14 +90,14 @@ export default function GeneralSettings() {
getCurrentShortcut(); getCurrentShortcut();
}, []); }, []);
const changeShortcut =(key: Shortcut) => { const changeShortcut = (key: Shortcut) => {
setShortcut(key) setShortcut(key);
// //
if (key.length === 0) return; if (key.length === 0) return;
invoke("change_shortcut", { key: key?.join("+") }).catch((err) => { invoke("change_shortcut", { key: key?.join("+") }).catch((err) => {
console.error("Failed to save hotkey:", err); console.error("Failed to save hotkey:", err);
}); });
} };
const { isEditing, currentKeys, startEditing, saveShortcut, cancelEditing } = const { isEditing, currentKeys, startEditing, saveShortcut, cancelEditing } =
useShortcutEditor(shortcut, changeShortcut); useShortcutEditor(shortcut, changeShortcut);
@@ -115,6 +122,15 @@ export default function GeneralSettings() {
saveShortcut(); saveShortcut();
}; };
// const clearAllCache = useCallback(() => {
// setAuth(undefined, endpoint);
// setUserInfo({}, endpoint);
// useConnectStore.persist.clearStorage();
// useAppStore.persist.clearStorage();
// }, [endpoint]);
return ( return (
<div className="space-y-8"> <div className="space-y-8">
<div> <div>
@@ -205,6 +221,23 @@ export default function GeneralSettings() {
Manage Favorites Manage Favorites
</button> </button>
</SettingsItem> */} </SettingsItem> */}
{/* <SettingsItem
icon={Trash2}
title="Clear Cache"
description="Clear cached data and settings"
>
<div className="space-y-2">
<div className="flex gap-2">
<button
onClick={clearAllCache}
className=" px-4 py-2 text-red-500 hover:text-red-600 dark:text-red-400 dark:hover:text-red-300 border border-red-200 dark:border-red-800 rounded-md hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors"
>
Clear All Cache
</button>
</div>
</div>
</SettingsItem> */}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,6 +1,6 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "@headlessui/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 { useSearchParams } from "react-router-dom";
import SettingsPanel from "./SettingsPanel"; import SettingsPanel from "./SettingsPanel";
@@ -25,7 +25,7 @@ function SettingsPage() {
const tabs = [ const tabs = [
{ name: "General", icon: Settings }, { name: "General", icon: Settings },
{ name: "Extensions", icon: Puzzle }, { name: "Extensions", icon: Puzzle },
{ name: "Connect", icon: User }, { name: "Connect", icon: Server },
{ name: "Advanced", icon: Settings2 }, { name: "Advanced", icon: Settings2 },
{ name: "About", icon: Info }, { name: "About", icon: Info },
]; ];

View File

@@ -1,22 +1,30 @@
import { useRouteError } from "react-router-dom"; import { useRouteError } from "react-router-dom";
import errorImg from "./assets/error_page.png"; import errorImg from "./assets/error_page.png";
import ApiDetails from "@/components/AppAI/ApiDetails";
export default function ErrorPage() { export default function ErrorPage() {
const error: any = useRouteError(); const error: any = useRouteError();
console.error(error); console.error(error);
return ( return (
<div className="w-full h-screen bg-white shadow-[0px_16px_32px_0px_rgba(0,0,0,0.4)] rounded-xl border-[2px] border-[#E6E6E6] m-auto"> <div className="w-full h-screen bg-white shadow-[0px_16px_32px_0px_rgba(0,0,0,0.4)] rounded-xl border-[2px] border-[#E6E6E6] m-auto">
<div className="flex flex-col justify-center items-center"> <div className="flex flex-col justify-center items-center">
<img src={errorImg} alt="error-page" className="w-[221px] h-[154px] mb-8 mt-[72px]"/> <img
src={errorImg}
alt="error-page"
className="w-[221px] h-[154px] mb-8 mt-[72px]"
/>
<div className="w-[380px] h-[46px] px-5 font-normal text-base text-[rgba(0,0,0,0.85)] leading-[25px] text-center mb-4"> <div className="w-[380px] h-[46px] px-5 font-normal text-base text-[rgba(0,0,0,0.85)] leading-[25px] text-center mb-4">
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.
</div> </div>
<div className="w-[380px] h-[45px] font-normal text-[10px] text-[rgba(135,135,135,0.85)] leading-[16px] text-center"> <div className="w-[380px] h-[45px] font-normal text-[10px] text-[rgba(135,135,135,0.85)] leading-[16px] text-center">
<i>{error.statusText || error.message}</i> <i>{error.statusText || error.message}</i>
</div> </div>
</div> </div>
<ApiDetails />
</div> </div>
); );
@@ -25,7 +33,8 @@ export default function ErrorPage() {
<div className="error-content"> <div className="error-content">
<h1 className="error-title">Oops!</h1> <h1 className="error-title">Oops!</h1>
<p className="error-message"> <p className="error-message">
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.
</p> </p>
<p className="error-details"> <p className="error-details">
<i>{error.statusText || error.message}</i> <i>{error.statusText || error.message}</i>

View File

@@ -18,9 +18,9 @@ export default function useSettingsWindow() {
const url = tab ? `/ui/settings?tab=${tab}` : `/ui/settings`; const url = tab ? `/ui/settings?tab=${tab}` : `/ui/settings`;
const options: CreateWindowOptions = { const options: CreateWindowOptions = {
label: "settings", label: "settings",
title: "Settings Window", title: "Coco Settings",
width: 1000, width: 1000,
height: 600, height: 700,
alwaysOnTop: false, alwaysOnTop: false,
shadow: true, shadow: true,
decorations: true, decorations: true,
@@ -29,6 +29,7 @@ export default function useSettingsWindow() {
minimizable: false, minimizable: false,
maximizable: false, maximizable: false,
dragDropEnabled: true, dragDropEnabled: true,
resizable: false,
center: true, center: true,
url, 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(() => { useEffect(() => {
const unlisten = listen("open_settings", (event) => { const unlisten = listen("open_settings", (event) => {
console.log("open_settings event received:", event); console.log("open_settings event received:", event);
@@ -52,11 +68,13 @@ export default function useSettingsWindow() {
openSettingsWindow(tab); openSettingsWindow(tab);
}); });
window.addEventListener("keydown", handleKeyDown);
return () => { return () => {
unlisten.then((fn) => fn()); unlisten.then((fn) => fn());
window.addEventListener("keydown", handleKeyDown);
}; };
}, []); }, [openSettingsWindow, handleKeyDown]);
return { openSettingsWindow }; return { openSettingsWindow };
} }

View File

@@ -62,9 +62,13 @@
@apply box-border border-[--border]; @apply box-border border-[--border];
} }
html{
@apply h-full;
}
body, body,
#root { #root {
@apply text-gray-900 antialiased; @apply h-full text-gray-900 antialiased;
} }
.dark body, .dark body,

View File

@@ -10,14 +10,17 @@ import { useAppStore } from "@/stores/appStore";
import { useAuthStore } from "@/stores/authStore"; import { useAuthStore } from "@/stores/authStore";
import { tauriFetch } from "@/api/tauriFetchClient"; import { tauriFetch } from "@/api/tauriFetchClient";
import ApiDetails from "@/components/AppAI/ApiDetails"; import ApiDetails from "@/components/AppAI/ApiDetails";
import { useConnectStore } from "@/stores/connectStore";
export default function DesktopApp() { export default function DesktopApp() {
const initializeListeners = useAppStore((state) => state.initializeListeners); const initializeListeners = useAppStore((state) => state.initializeListeners);
const initializeListeners_auth = useAuthStore( const initializeListeners_auth = useAuthStore(
(state) => state.initializeListeners (state) => state.initializeListeners
); );
const setConnectorData = useAppStore((state) => state.setConnectorData); const setConnectorData = useConnectStore((state) => state.setConnectorData);
const setDatasourceData = useAppStore((state) => state.setDatasourceData); const setDatasourceData = useConnectStore((state) => state.setDatasourceData);
const endpoint_http = useAppStore((state) => state.endpoint_http);
useEffect(() => { useEffect(() => {
initializeListeners(); initializeListeners();
@@ -35,7 +38,7 @@ export default function DesktopApp() {
}); });
console.log("connector", response); console.log("connector", response);
const data = response.data?.hits?.hits || []; const data = response.data?.hits?.hits || [];
setConnectorData(data); setConnectorData(data, endpoint_http);
} catch (error) { } catch (error) {
console.error("Failed to fetch user data:", error); console.error("Failed to fetch user data:", error);
} }
@@ -49,7 +52,7 @@ export default function DesktopApp() {
}); });
console.log("datasource", response); console.log("datasource", response);
const data = response.data?.hits?.hits || []; const data = response.data?.hits?.hits || [];
setDatasourceData(data); setDatasourceData(data, endpoint_http);
} catch (error) { } catch (error) {
console.error("Failed to fetch user data:", error); console.error("Failed to fetch user data:", error);
} }
@@ -91,7 +94,7 @@ export default function DesktopApp() {
return ( return (
<div <div
data-tauri-drag-region data-tauri-drag-region
className={`w-[680px] h-[590px] m-auto rounded-xl overflow-hidden relative border border-[#E6E6E6] dark:border-[#272626] ${ className={`w-full h-full m-auto rounded-xl overflow-hidden relative border border-[#E6E6E6] dark:border-[#272626] ${
isTransitioned isTransitioned
? "bg-chat_bg_light dark:bg-chat_bg_dark" ? "bg-chat_bg_light dark:bg-chat_bg_dark"
: "bg-search_bg_light dark:bg-search_bg_dark" : "bg-search_bg_light dark:bg-search_bg_dark"

View File

@@ -15,10 +15,6 @@ export type IAppStore = {
endpoint_http: string, endpoint_http: string,
endpoint_websocket: string, endpoint_websocket: string,
setEndpoint: (endpoint: AppEndpoint) => void, setEndpoint: (endpoint: AppEndpoint) => void,
connector_data: any[],
setConnectorData: (connector_data: any[]) => void,
datasourceData: any[],
setDatasourceData: (datasourceData: any[]) => void,
initializeListeners: () => void; initializeListeners: () => void;
}; };
@@ -53,18 +49,6 @@ export const useAppStore = create<IAppStore>()(
endpoint_websocket endpoint_websocket
}); });
}, },
connector_data: [],
setConnectorData: async (connector_data: any[]) => {
set({
connector_data
});
},
datasourceData: [],
setDatasourceData: async (datasourceData: any[]) => {
set({
datasourceData
});
},
initializeListeners: () => { initializeListeners: () => {
listen(ENDPOINT_CHANGE_EVENT, (event: any) => { listen(ENDPOINT_CHANGE_EVENT, (event: any) => {
const { endpoint, endpoint_http, endpoint_websocket } = event.payload; const { endpoint, endpoint_http, endpoint_websocket } = event.payload;

View File

@@ -1,6 +1,7 @@
import { create } from "zustand"; import { create } from "zustand";
import { persist } from "zustand/middleware"; import { persist } from "zustand/middleware";
import { listen, emit } from '@tauri-apps/api/event'; import { listen, emit } from '@tauri-apps/api/event';
import { produce } from 'immer'
const AUTH_CHANGE_EVENT = 'auth-changed'; const AUTH_CHANGE_EVENT = 'auth-changed';
const USERINFO_CHANGE_EVENT = 'userInfo-changed'; const USERINFO_CHANGE_EVENT = 'userInfo-changed';
@@ -10,19 +11,27 @@ export type Plan = {
last_checked: number; last_checked: number;
}; };
export type AuthStore = { export type AuthProp = {
token: string; token: string;
user_id?: string | null; user_id?: string | null;
expires?: number; expires?: number;
plan?: Plan | null; plan?: Plan | null;
}; };
type AuthMapProp = {
[key: string]: AuthProp;
};
type userInfoMapProp = {
[key: string]: any;
};
export type IAuthStore = { export type IAuthStore = {
[x: string]: any; [x: string]: any;
auth: AuthStore | undefined; auth: AuthMapProp | undefined;
userInfo: any; userInfo: userInfoMapProp;
setAuth: (auth: AuthStore | undefined) => void; setAuth: (auth: AuthProp | undefined, key: string) => void;
resetAuth: () => void; resetAuth: (key: string) => void;
initializeListeners: () => void; initializeListeners: () => void;
}; };
@@ -31,24 +40,43 @@ export const useAuthStore = create<IAuthStore>()(
(set) => ({ (set) => ({
auth: undefined, auth: undefined,
userInfo: {}, userInfo: {},
setAuth: async (auth) => { setAuth: async (auth, key) => {
set({ auth }) set(
await emit(AUTH_CHANGE_EVENT, { produce((draft) => {
auth draft.auth[key] = auth
}); })
}, );
resetAuth: async () => {
set({ auth: undefined })
await emit(AUTH_CHANGE_EVENT, { await emit(AUTH_CHANGE_EVENT, {
auth: undefined auth: {
[key]: auth
}
}); });
}, },
setUserInfo: async (userInfo: any) => { resetAuth: async (key: string) => {
set({ userInfo }) 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, { await emit(USERINFO_CHANGE_EVENT, {
userInfo userInfo: {
[key]: userInfo
}
}); });
}, },
initializeListeners: () => { initializeListeners: () => {
@@ -65,7 +93,7 @@ export const useAuthStore = create<IAuthStore>()(
}), }),
{ {
name: "auth-store", name: "auth-store",
partialize: (state) => ({ partialize: (state) => ({
auth: state.auth, auth: state.auth,
userInfo: state.userInfo userInfo: state.userInfo
}), }),

124
src/stores/connectStore.ts Normal file
View File

@@ -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<IConnectStore>()(
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,
}),
}
)
);