diff --git a/docs/content.en/docs/release-notes/_index.md b/docs/content.en/docs/release-notes/_index.md index f33cb53b..2df3c1b2 100644 --- a/docs/content.en/docs/release-notes/_index.md +++ b/docs/content.en/docs/release-notes/_index.md @@ -57,6 +57,7 @@ Information about release notes of Coco Server is provided here. - style: search error styles #533 - fix: server image loading failure #534 - chore: skip register server that not logged in #536 +- refactor: service info related components #537 ## 0.4.0 (2025-04-27) diff --git a/src/components/Assistant/AssistantList.tsx b/src/components/Assistant/AssistantList.tsx index c7aa98b6..cdc13ee8 100644 --- a/src/components/Assistant/AssistantList.tsx +++ b/src/components/Assistant/AssistantList.tsx @@ -125,8 +125,6 @@ export function AssistantList({ assistantIDs = [] }: AssistantListProps) { response = res; } - console.log("assistant_search", response); - let assistantList = response?.hits?.hits ?? []; console.log("assistantList", assistantList); @@ -143,13 +141,13 @@ export function AssistantList({ assistantIDs = [] }: AssistantListProps) { } } - console.log("state.allAssistants", state.allAssistants); + //console.log("state.allAssistants", state.allAssistants); const matched = state.allAssistants.find((item: any) => { return item._id === currentAssistant?._id; }); - console.log("matched", matched); + //console.log("matched", matched); if (matched) { setCurrentAssistant(matched); @@ -318,7 +316,6 @@ export function AssistantList({ assistantIDs = [] }: AssistantListProps) { placeholder={t("assistant.popover.search")} className="w-full h-8 px-2 bg-transparent border rounded-md dark:border-white/10" onChange={(event) => { - console.log("onChange", event.target.value); setKeyword(event.target.value.trim()); }} /> diff --git a/src/components/Assistant/ChatHeader.tsx b/src/components/Assistant/ChatHeader.tsx index 54a7dd62..18492619 100644 --- a/src/components/Assistant/ChatHeader.tsx +++ b/src/components/Assistant/ChatHeader.tsx @@ -5,7 +5,7 @@ import HistoryIcon from "@/icons/History"; import PinOffIcon from "@/icons/PinOff"; import PinIcon from "@/icons/Pin"; import WindowsFullIcon from "@/icons/WindowsFull"; -import { useAppStore, IServer } from "@/stores/appStore"; +import { useAppStore } from "@/stores/appStore"; import type { Chat } from "@/types/chat"; import platformAdapter from "@/utils/platformAdapter"; import VisibleKey from "../Common/VisibleKey"; @@ -13,6 +13,8 @@ import { useShortcutsStore } from "@/stores/shortcutsStore"; import { HISTORY_PANEL_ID } from "@/constants"; import { AssistantList } from "./AssistantList"; import { ServerList } from "./ServerList"; +import { Server } from "@/types/server" + interface ChatHeaderProps { onCreateNewChat: () => void; @@ -20,7 +22,7 @@ interface ChatHeaderProps { setIsSidebarOpen: () => void; isSidebarOpen: boolean; activeChat: Chat | undefined; - reconnect: (server?: IServer) => void; + reconnect: (server?: Server) => void; isLogin: boolean; setIsLogin: (isLogin: boolean) => void; isChatPage?: boolean; diff --git a/src/components/Assistant/ServerList.tsx b/src/components/Assistant/ServerList.tsx index 49f230f3..3c274418 100644 --- a/src/components/Assistant/ServerList.tsx +++ b/src/components/Assistant/ServerList.tsx @@ -3,16 +3,18 @@ import { Settings, RefreshCw, Check, Server } from "lucide-react"; import { useTranslation } from "react-i18next"; import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react"; import { useKeyPress } from "ahooks"; +import { isNil } from "lodash-es"; import logoImg from "@/assets/icon.svg"; import ServerIcon from "@/icons/Server"; import VisibleKey from "../Common/VisibleKey"; import { useShortcutsStore } from "@/stores/shortcutsStore"; import platformAdapter from "@/utils/platformAdapter"; -import { useAppStore, IServer } from "@/stores/appStore"; +import { useAppStore } from "@/stores/appStore"; import { useChatStore } from "@/stores/chatStore"; import { useConnectStore } from "@/stores/connectStore"; -import { isNil } from "lodash-es"; +import { Server as IServer } from "@/types/server"; +import StatusIndicator from "@/components/Cloud/StatusIndicator"; interface ServerListProps { isLogin: boolean; @@ -215,17 +217,16 @@ export function ServerList({ {server.name}
- AI Assistant: {server.assistantCount || 1} + AI Assistant: {1}
-
{currentService?.id === server.id && ( diff --git a/src/components/Assistant/Splash.tsx b/src/components/Assistant/Splash.tsx index e5f7f915..e93e75ee 100644 --- a/src/components/Assistant/Splash.tsx +++ b/src/components/Assistant/Splash.tsx @@ -77,7 +77,7 @@ const Splash = () => { }); const settingsAssistantList = useMemo(() => { - console.log("assistantList", assistantList); + //console.log("assistantList", assistantList); return assistantList.filter((item) => { return settings?.display_assistants?.includes(item?._source?.id); diff --git a/src/components/Cloud/Cloud.tsx b/src/components/Cloud/Cloud.tsx index 886a2dad..fa3deae9 100644 --- a/src/components/Cloud/Cloud.tsx +++ b/src/components/Cloud/Cloud.tsx @@ -1,64 +1,32 @@ -import { useCallback, useEffect, useRef, useState } from "react"; -import { - CalendarSync, - Copy, - GitFork, - Globe, - PackageOpen, - RefreshCcw, - Trash2, -} from "lucide-react"; -import { v4 as uuidv4 } from "uuid"; -import { getCurrentWindow } from "@tauri-apps/api/window"; -import { - getCurrent as getCurrentDeepLinkUrls, - onOpenUrl, -} from "@tauri-apps/plugin-deep-link"; -import { useTranslation } from "react-i18next"; -import clsx from "clsx"; +import { useEffect, useRef, useState, useCallback } from "react"; import { emit } from "@tauri-apps/api/event"; -import { UserProfile } from "./UserProfile"; import { DataSourcesList } from "./DataSourcesList"; import { Sidebar } from "./Sidebar"; import { Connect } from "./Connect"; -import { OpenURLWithBrowser } from "@/utils"; import { useAppStore } from "@/stores/appStore"; import { useConnectStore } from "@/stores/connectStore"; -import bannerImg from "@/assets/images/coco-cloud-banner.jpeg"; -import SettingsToggle from "@/components/Settings/SettingsToggle"; -import Tooltip from "@/components/Common/Tooltip"; import { list_coco_servers, add_coco_server, - enable_server, - disable_server, - logout_coco_server, - remove_coco_server, refresh_coco_server_info, - handle_sso_callback, } from "@/commands"; +import ServiceInfo from "./ServiceInfo"; +import ServiceAuth from "./ServiceAuth"; export default function Cloud() { - const { t } = useTranslation(); - const SidebarRef = useRef<{ refreshData: () => void }>(null); const errors = useAppStore((state) => state.errors); - const addError = useAppStore((state) => state.addError); const [isConnect, setIsConnect] = useState(true); - const ssoRequestID = useAppStore((state) => state.ssoRequestID); - const setSSORequestID = useAppStore((state) => state.setSSORequestID); - const currentService = useConnectStore((state) => state.currentService); const setCurrentService = useConnectStore((state) => state.setCurrentService); const serverList = useConnectStore((state) => state.serverList); const setServerList = useConnectStore((state) => state.setServerList); - const [loading, setLoading] = useState(false); const [refreshLoading, setRefreshLoading] = useState(false); // fetch the servers @@ -68,7 +36,6 @@ export default function Cloud() { useEffect(() => { console.log("currentService", currentService); - setLoading(false); setRefreshLoading(false); setIsConnect(true); }, [JSON.stringify(currentService)]); @@ -131,178 +98,30 @@ export default function Cloud() { }); }; - const handleOAuthCallback = useCallback( - async (code: string | null, serverId: string | null) => { - if (!code || !serverId) { - addError("No authorization code received"); - return; - } - - try { - console.log("Handling OAuth callback:", { code, serverId }); - await handle_sso_callback({ - serverId: serverId, // Make sure 'server_id' is the correct argument - requestId: ssoRequestID, // Make sure 'request_id' is the correct argument - code: code, + const refreshClick = useCallback( + (id: string) => { + setRefreshLoading(true); + refresh_coco_server_info(id) + .then((res: any) => { + console.log("refresh_coco_server_info", id, res); + fetchServers(false).then((r) => { + console.log("fetchServers", r); + }); + // update currentService + setCurrentService(res); + emit("login_or_logout", true); + }) + .finally(() => { + setRefreshLoading(false); }); - - if (serverId != null) { - refreshClick(serverId); - } - - getCurrentWindow().setFocus(); - } catch (e) { - console.error("Sign in failed:", e); - } finally { - setLoading(false); - } }, - [ssoRequestID] + [fetchServers] ); - const handleUrl = (url: string) => { - try { - const urlObject = new URL(url.trim()); - console.log("handle urlObject:", urlObject); - - // pass request_id and check with local, if the request_id are same, then continue - const reqId = urlObject.searchParams.get("request_id"); - const code = urlObject.searchParams.get("code"); - - if (reqId != ssoRequestID) { - console.log("Request ID not matched, skip"); - addError("Request ID not matched, skip"); - return; - } - - const serverId = currentService?.id; - handleOAuthCallback(code, serverId); - } catch (err) { - console.error("Failed to parse URL:", err); - addError("Invalid URL format: " + err); - } - }; - - // Fetch the initial deep link intent - useEffect(() => { - // Function to handle pasted URL - const handlePaste = (event: any) => { - const pastedText = event.clipboardData.getData("text").trim(); - console.log("handle paste text:", pastedText); - if (isValidCallbackUrl(pastedText)) { - // Handle the URL as if it's a deep link - console.log("handle callback on paste:", pastedText); - handleUrl(pastedText); - } - }; - - // Function to check if the pasted URL is valid for our deep link scheme - const isValidCallbackUrl = (url: string) => { - return url && url.startsWith("coco://oauth_callback"); - }; - - // Adding event listener for paste events - document.addEventListener("paste", handlePaste); - - getCurrentDeepLinkUrls() - .then((urls) => { - console.log("URLs:", urls); - if (urls && urls.length > 0) { - if (isValidCallbackUrl(urls[0].trim())) { - handleUrl(urls[0]); - } - } - }) - .catch((err) => { - console.error("Failed to get initial URLs:", err); - addError("Failed to get initial URLs: " + err); - }); - - const unlisten = onOpenUrl((urls) => handleUrl(urls[0])); - - return () => { - unlisten.then((fn) => fn()); - document.removeEventListener("paste", handlePaste); - }; - }, [ssoRequestID]); - - const LoginClick = useCallback(() => { - if (loading) return; // Prevent multiple clicks if already loading - - let requestID = uuidv4(); - setSSORequestID(requestID); - - // Generate the login URL with the current appUid - const url = `${currentService?.auth_provider?.sso?.url}/?provider=${currentService?.id}&product=coco&request_id=${requestID}`; - - console.log("Open SSO link, requestID:", ssoRequestID, url); - - // Open the URL in a browser - OpenURLWithBrowser(url); - - // Start loading state - setLoading(true); - }, [ssoRequestID, loading, currentService]); - - const refreshClick = (id: string) => { - setRefreshLoading(true); - refresh_coco_server_info(id) - .then((res: any) => { - console.log("refresh_coco_server_info", id, res); - fetchServers(false).then((r) => { - console.log("fetchServers", r); - }); - // update currentService - setCurrentService(res); - emit("login_or_logout", true); - }) - .finally(() => { - setRefreshLoading(false); - }); - }; - function onAddServer() { setIsConnect(false); } - function onLogout(id: string) { - console.log("onLogout", id); - setRefreshLoading(true); - logout_coco_server(id) - .then((res: any) => { - console.log("logout_coco_server", id, JSON.stringify(res)); - refreshClick(id); - emit("login_or_logout", false); - }) - .finally(() => { - setRefreshLoading(false); - }); - } - - const removeServer = (id: string) => { - remove_coco_server(id).then((res: any) => { - console.log("remove_coco_server", id, JSON.stringify(res)); - fetchServers(true).then((r) => { - console.log("fetchServers", r); - }); - }); - }; - - const enable_coco_server = useCallback( - async (enabled: boolean) => { - if (enabled) { - await enable_server(currentService?.id); - } else { - await disable_server(currentService?.id); - } - - setCurrentService({ ...currentService, enabled }); - - await fetchServers(false); - }, - [currentService?.id] - ); - return (
{isConnect ? (
-
- banner { - const target = e.target as HTMLImageElement; - target.src = bannerImg; - }} - /> -
-
-
- -
- {currentService?.name} -
-
-
-
- + - - - {!currentService?.builtin && ( - - )} -
-
- -
-
- - {" "} - {currentService?.provider?.name} - - | - - {" "} - {currentService?.version?.number} - - | - - {currentService?.updated} - -
-

- {currentService?.provider?.description} -

-
- - {currentService?.auth_provider?.sso?.url ? ( -
-

- {t("cloud.accountInfo")} -

- {currentService?.profile ? ( - - ) : ( -
- {/* Login Button (conditionally rendered when not loading) */} - {!loading && ( - - )} - - {/* Cancel Button and Copy URL button while loading */} - {loading && ( -
- - -
- {t("cloud.manualCopyLink")} -
-
- )} - - {/* Privacy Policy Link */} - -
- )} -
- ) : null} + {currentService?.profile ? ( diff --git a/src/components/Cloud/ServiceAuth.tsx b/src/components/Cloud/ServiceAuth.tsx new file mode 100644 index 00000000..34359f45 --- /dev/null +++ b/src/components/Cloud/ServiceAuth.tsx @@ -0,0 +1,253 @@ +import { memo, useCallback, useEffect, useState } from "react"; +import { Copy } from "lucide-react"; +import { useTranslation } from "react-i18next"; +import { v4 as uuidv4 } from "uuid"; +import { emit } from "@tauri-apps/api/event"; +import { + getCurrent as getCurrentDeepLinkUrls, + onOpenUrl, +} from "@tauri-apps/plugin-deep-link"; +import { getCurrentWindow } from "@tauri-apps/api/window"; + +import { UserProfile } from "./UserProfile"; +import { OpenURLWithBrowser } from "@/utils"; +import { useConnectStore } from "@/stores/connectStore"; +import { useAppStore } from "@/stores/appStore"; +import { logout_coco_server, handle_sso_callback } from "@/commands"; + +interface ServiceAuthProps { + setRefreshLoading: (loading: boolean) => void; + refreshClick: (id: string) => void; +} + +const ServiceAuth = memo( + ({ setRefreshLoading, refreshClick }: ServiceAuthProps) => { + const { t } = useTranslation(); + + const ssoRequestID = useAppStore((state) => state.ssoRequestID); + const setSSORequestID = useAppStore((state) => state.setSSORequestID); + + const addError = useAppStore((state) => state.addError); + + const currentService = useConnectStore((state) => state.currentService); + + const [loading, setLoading] = useState(false); + + const LoginClick = useCallback(() => { + if (loading) return; // Prevent multiple clicks if already loading + + let requestID = uuidv4(); + setSSORequestID(requestID); + + // Generate the login URL with the current appUid + const url = `${currentService?.auth_provider?.sso?.url}/?provider=${currentService?.id}&product=coco&request_id=${requestID}`; + + console.log("Open SSO link, requestID:", ssoRequestID, url); + + // Open the URL in a browser + OpenURLWithBrowser(url); + + // Start loading state + setLoading(true); + }, [ssoRequestID, loading, currentService]); + + const onLogout = useCallback( + (id: string) => { + console.log("onLogout", id); + setRefreshLoading(true); + logout_coco_server(id) + .then((res: any) => { + console.log("logout_coco_server", id, JSON.stringify(res)); + refreshClick(id); + emit("login_or_logout", false); + }) + .finally(() => { + setRefreshLoading(false); + }); + }, + [refreshClick] + ); + + const handleOAuthCallback = useCallback( + async (code: string | null, serverId: string | null) => { + if (!code || !serverId) { + addError("No authorization code received"); + return; + } + + try { + console.log("Handling OAuth callback:", { code, serverId }); + await handle_sso_callback({ + serverId: serverId, // Make sure 'server_id' is the correct argument + requestId: ssoRequestID, // Make sure 'request_id' is the correct argument + code: code, + }); + + if (serverId != null) { + refreshClick(serverId); + } + + getCurrentWindow().setFocus(); + } catch (e) { + console.error("Sign in failed:", e); + } finally { + setLoading(false); + } + }, + [ssoRequestID] + ); + + const handleUrl = (url: string) => { + try { + const urlObject = new URL(url.trim()); + console.log("handle urlObject:", urlObject); + + // pass request_id and check with local, if the request_id are same, then continue + const reqId = urlObject.searchParams.get("request_id"); + const code = urlObject.searchParams.get("code"); + + if (reqId != ssoRequestID) { + console.log("Request ID not matched, skip"); + addError("Request ID not matched, skip"); + return; + } + + const serverId = currentService?.id; + handleOAuthCallback(code, serverId); + } catch (err) { + console.error("Failed to parse URL:", err); + addError("Invalid URL format: " + err); + } + }; + + // Fetch the initial deep link intent + useEffect(() => { + setLoading(false); + // Function to handle pasted URL + const handlePaste = (event: any) => { + const pastedText = event.clipboardData.getData("text").trim(); + console.log("handle paste text:", pastedText); + if (isValidCallbackUrl(pastedText)) { + // Handle the URL as if it's a deep link + console.log("handle callback on paste:", pastedText); + handleUrl(pastedText); + } + }; + + // Function to check if the pasted URL is valid for our deep link scheme + const isValidCallbackUrl = (url: string) => { + return url && url.startsWith("coco://oauth_callback"); + }; + + // Adding event listener for paste events + document.addEventListener("paste", handlePaste); + + getCurrentDeepLinkUrls() + .then((urls) => { + console.log("URLs:", urls); + if (urls && urls.length > 0) { + if (isValidCallbackUrl(urls[0].trim())) { + handleUrl(urls[0]); + } + } + }) + .catch((err) => { + console.error("Failed to get initial URLs:", err); + addError("Failed to get initial URLs: " + err); + }); + + const unlisten = onOpenUrl((urls) => handleUrl(urls[0])); + + return () => { + unlisten.then((fn) => fn()); + document.removeEventListener("paste", handlePaste); + }; + }, [ssoRequestID]); + + if (!currentService?.auth_provider?.sso?.url) { + return null; + } + + return ( +
+

+ {t("cloud.accountInfo")} +

+ {currentService?.profile ? ( + + ) : ( +
+ {/* Login Button (conditionally rendered when not loading) */} + {!loading && } + + {/* Cancel Button and Copy URL button while loading */} + {loading && ( + setLoading(false)} + onCopy={() => { + navigator.clipboard.writeText( + `${currentService?.auth_provider?.sso?.url}/?provider=${currentService?.id}&product=coco&request_id=${ssoRequestID}` + ); + }} + /> + )} + + {/* Privacy Policy Link */} + +
+ )} +
+ ); + } +); + +export default ServiceAuth; + +const LoginButton = memo(({ LoginClick }: { LoginClick: () => void }) => { + const { t } = useTranslation(); + return ( + + ); +}); + +const LoadingState = memo( + ({ onCancel, onCopy }: { onCancel: () => void; onCopy: () => void }) => { + const { t } = useTranslation(); + return ( +
+ + +
+ {t("cloud.manualCopyLink")} +
+
+ ); + } +); diff --git a/src/components/Cloud/ServiceBanner.tsx b/src/components/Cloud/ServiceBanner.tsx new file mode 100644 index 00000000..28df6212 --- /dev/null +++ b/src/components/Cloud/ServiceBanner.tsx @@ -0,0 +1,26 @@ +import { memo } from "react"; + +import bannerImg from "@/assets/images/coco-cloud-banner.jpeg"; +import { useConnectStore } from "@/stores/connectStore"; + +interface ServiceBannerProps {} + +const ServiceBanner = memo(({}: ServiceBannerProps) => { + const currentService = useConnectStore((state) => state.currentService); + + return ( +
+ banner { + const target = e.target as HTMLImageElement; + target.src = bannerImg; + }} + /> +
+ ); +}); + +export default ServiceBanner; diff --git a/src/components/Cloud/ServiceHeader.tsx b/src/components/Cloud/ServiceHeader.tsx new file mode 100644 index 00000000..7b3811c6 --- /dev/null +++ b/src/components/Cloud/ServiceHeader.tsx @@ -0,0 +1,104 @@ +import { memo, useCallback } from "react"; +import { Globe, RefreshCcw, Trash2 } from "lucide-react"; +import { useTranslation } from "react-i18next"; +import clsx from "clsx"; + +import Tooltip from "@/components/Common/Tooltip"; +import SettingsToggle from "@/components/Settings/SettingsToggle"; +import { OpenURLWithBrowser } from "@/utils"; +import { useConnectStore } from "@/stores/connectStore"; +import { enable_server, disable_server, remove_coco_server } from "@/commands"; + +interface ServiceHeaderProps { + refreshLoading?: boolean; + refreshClick: (id: string) => void; + fetchServers: (force: boolean) => Promise; +} + +const ServiceHeader = memo( + ({ refreshLoading, refreshClick, fetchServers }: ServiceHeaderProps) => { + const { t } = useTranslation(); + + const currentService = useConnectStore((state) => state.currentService); + const setCurrentService = useConnectStore( + (state) => state.setCurrentService + ); + + const enable_coco_server = useCallback( + async (enabled: boolean) => { + if (enabled) { + await enable_server(currentService?.id); + } else { + await disable_server(currentService?.id); + } + + setCurrentService({ ...currentService, enabled }); + + await fetchServers(false); + }, + [currentService?.id] + ); + + const removeServer = (id: string) => { + remove_coco_server(id).then((res: any) => { + console.log("remove_coco_server", id, JSON.stringify(res)); + fetchServers(true).then((r) => { + console.log("fetchServers", r); + }); + }); + }; + + return ( +
+
+ +
+ {currentService?.name} +
+
+
+
+ + + + + {!currentService?.builtin && ( + + )} +
+
+ ); + } +); + +export default ServiceHeader; diff --git a/src/components/Cloud/ServiceInfo.tsx b/src/components/Cloud/ServiceInfo.tsx new file mode 100644 index 00000000..fbc9783e --- /dev/null +++ b/src/components/Cloud/ServiceInfo.tsx @@ -0,0 +1,31 @@ +import { memo } from "react"; + +import ServiceBanner from "./ServiceBanner"; +import ServiceHeader from "./ServiceHeader"; +import ServiceMetadata from "./ServiceMetadata"; + +interface ServiceInfoProps { + refreshLoading?: boolean; + refreshClick: (id: string) => void; + fetchServers: (force: boolean) => Promise; +} + +const ServiceInfo = memo( + ({ refreshLoading, refreshClick, fetchServers }: ServiceInfoProps) => { + return ( + <> + + + + + + + ); + } +); + +export default ServiceInfo; diff --git a/src/components/Cloud/ServiceMetadata.tsx b/src/components/Cloud/ServiceMetadata.tsx new file mode 100644 index 00000000..2161b7d6 --- /dev/null +++ b/src/components/Cloud/ServiceMetadata.tsx @@ -0,0 +1,33 @@ +import { memo } from "react"; +import { PackageOpen, GitFork, CalendarSync } from "lucide-react"; + +import { useConnectStore } from "@/stores/connectStore"; + +interface ServiceMetadataProps {} + +const ServiceMetadata = memo(({}: ServiceMetadataProps) => { + const currentService = useConnectStore((state) => state.currentService); + + return ( +
+
+ + {currentService?.provider?.name} + + | + + {currentService?.version?.number} + + | + + {currentService?.updated} + +
+

+ {currentService?.provider?.description} +

+
+ ); +}); + +export default ServiceMetadata; diff --git a/src/components/Cloud/Sidebar.tsx b/src/components/Cloud/Sidebar.tsx index 7ad37881..8a5eeb5d 100644 --- a/src/components/Cloud/Sidebar.tsx +++ b/src/components/Cloud/Sidebar.tsx @@ -1,13 +1,20 @@ import { useTranslation } from "react-i18next"; -import { forwardRef } from "react"; +import { forwardRef, useMemo, useCallback } from "react"; import { Plus } from "lucide-react"; import cocoLogoImg from "@/assets/app-icon.png"; import { useConnectStore } from "@/stores/connectStore"; +import { Server } from "@/types/server"; +import StatusIndicator from "./StatusIndicator"; interface SidebarProps { onAddServer: () => void; - serverList: any[]; + serverList: Server[]; +} + +interface ServerGroups { + builtinServers: JSX.Element[]; + customServers: JSX.Element[]; } export const Sidebar = forwardRef<{ refreshData: () => void }, SidebarProps>( @@ -18,69 +25,84 @@ export const Sidebar = forwardRef<{ refreshData: () => void }, SidebarProps>( (state) => state.setCurrentService ); - const onAddServerClick = () => { - onAddServer(); - }; + const getServerItemClassName = useCallback((isSelected: boolean) => { + return `flex cursor-pointer 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 whitespace-nowrap ${ + isSelected + ? "dark:bg-blue-900/20 dark:bg-blue-900 border border-[#0087ff]" + : "bg-gray-50 dark:bg-gray-900 border border-[#e6e6e6] dark:border-gray-700" + }`; + }, []); // Extracted server item rendering - const renderServerItem = (item: any) => { - return ( -
setCurrentService(item)} - > - LogoImg { - const target = e.target as HTMLImageElement; - target.src = cocoLogoImg; - }} - /> - {item?.name} -
- -
+ const renderServerItem = useCallback( + (item: Server) => { + const isSelected = currentService?.id === item.id; + return ( +
setCurrentService(item)} + > + {`${item.name} { + const target = e.target as HTMLImageElement; + target.src = cocoLogoImg; + }} + /> + + {item.name} + +
+ +
+ ); + }, + [currentService] + ); + + const { builtinServers, customServers } = useMemo(() => { + const groups = serverList.reduce( + (acc, item) => { + const renderedItem = renderServerItem(item); + if (item?.builtin) { + acc.builtinServers.push(renderedItem); + } else { + acc.customServers.push(renderedItem); + } + return acc; + }, + { builtinServers: [], customServers: [] } ); - }; + return groups; + }, [serverList, renderServerItem]); return (
{/* Render Built-in Servers */} -
- {serverList - .filter((item) => item?.builtin) - .map((item) => renderServerItem(item))} -
+ {builtinServers}
{t("cloud.sidebar.yourServers")}
{/* Render Non-Built-in Servers */} -
- {serverList - .filter((item) => !item?.builtin) - .map((item) => renderServerItem(item))} -
+ {customServers}
diff --git a/src/components/Cloud/StatusIndicator.tsx b/src/components/Cloud/StatusIndicator.tsx new file mode 100644 index 00000000..6aa4cb71 --- /dev/null +++ b/src/components/Cloud/StatusIndicator.tsx @@ -0,0 +1,43 @@ +import { useMemo, memo } from "react"; + +import { Status } from "@/types/server"; + +type StatusIndicatorProps = { + enabled: boolean; + public: boolean; + hasProfile: boolean; + status?: Status; +}; + +const StatusIndicator = memo( + ({ enabled, public: isPublic, hasProfile, status }: StatusIndicatorProps) => { + // If service is enabled AND (public OR (private AND logged in)) AND service is available + // Otherwise show gray status + const isActive = + enabled && (isPublic || (!isPublic && hasProfile)) && status; + + const statusColorClass = useMemo(() => { + if (!isActive) return "bg-gray-400 dark:bg-gray-600"; + switch (status) { + case "green": + return "bg-green-500"; + case "yellow": + return "bg-yellow-500"; + case "red": + return "bg-red-500"; + default: + return "bg-gray-400 dark:bg-gray-600"; + } + }, [isActive, status]); + + return ( +
+ ); + } +); + +export default StatusIndicator; \ No newline at end of file diff --git a/src/components/Cloud/UserProfile.tsx b/src/components/Cloud/UserProfile.tsx index 6e90e220..e2f02257 100644 --- a/src/components/Cloud/UserProfile.tsx +++ b/src/components/Cloud/UserProfile.tsx @@ -1,16 +1,6 @@ import { User, LogOut } from "lucide-react"; -interface UserPreferences { - theme: "dark" | "light"; - language: string; -} -interface UserInfo { - name: string; - email: string; - avatar?: string; - roles: string[]; // ["admin", "editor"] - preferences: UserPreferences; -} +import { UserProfile as UserInfo } from "@/types/server"; interface UserProfileProps { server: string; //server's id diff --git a/src/components/Settings/GeneralSettings.tsx b/src/components/Settings/GeneralSettings.tsx index aade496b..62f6a025 100644 --- a/src/components/Settings/GeneralSettings.tsx +++ b/src/components/Settings/GeneralSettings.tsx @@ -21,7 +21,7 @@ import { useCreation } from "ahooks"; import SettingsItem from "./SettingsItem"; import SettingsToggle from "./SettingsToggle"; import { ShortcutItem } from "./ShortcutItem"; -import { Shortcut } from "./shortcut"; +import { Shortcut } from "../../types/shortcut"; import { useShortcutEditor } from "@/hooks/useShortcutEditor"; import { useAppStore } from "@/stores/appStore"; import { AppTheme } from "@/types/index"; diff --git a/src/hooks/useShortcutEditor.ts b/src/hooks/useShortcutEditor.ts index f036022c..8bc9b6db 100644 --- a/src/hooks/useShortcutEditor.ts +++ b/src/hooks/useShortcutEditor.ts @@ -1,7 +1,7 @@ import { useState, useCallback, useEffect } from "react"; import { useHotkeys } from "react-hotkeys-hook"; -import { Shortcut } from "@/components/Settings/shortcut"; +import { Shortcut } from "@/types/shortcut"; import { normalizeKey, isModifierKey, sortKeys } from "@/utils/keyboardUtils"; const RESERVED_SHORTCUTS = [ diff --git a/src/hooks/useWebSocket.ts b/src/hooks/useWebSocket.ts index 6d27087d..ccd62330 100644 --- a/src/hooks/useWebSocket.ts +++ b/src/hooks/useWebSocket.ts @@ -1,8 +1,9 @@ import { useEffect, useCallback, useRef } from "react"; import { useWebSocket as useWebSocketAHook } from "ahooks"; -import { useAppStore, IServer } from "@/stores/appStore"; +import { useAppStore } from "@/stores/appStore"; import platformAdapter from "@/utils/platformAdapter"; +import { Server } from "@/types/server"; enum ReadyState { Connecting = 0, @@ -15,7 +16,7 @@ interface WebSocketProps { clientId: string; connected: boolean; setConnected: (connected: boolean) => void; - currentService: IServer | null; + currentService: Server | null; dealMsgRef: React.MutableRefObject<((msg: string) => void) | null>; onWebsocketSessionId?: (sessionId: string) => void; } @@ -101,7 +102,7 @@ export default function useWebSocket({ // 2. If not connected or disconnected, input box has a connect button, clicking it will connect to WebSocket // src/components/Search/InputBox.tsx const reconnect = useCallback( - async (server?: IServer) => { + async (server?: Server) => { if (isTauri) { const targetServer = server || currentService; if (!targetServer?.id) return; diff --git a/src/stores/appStore.ts b/src/stores/appStore.ts index 30ee2cf5..399a5e55 100644 --- a/src/stores/appStore.ts +++ b/src/stores/appStore.ts @@ -6,26 +6,6 @@ import platformAdapter from "@/utils/platformAdapter"; const ENDPOINT_CHANGE_EVENT = "endpoint-changed"; -export interface IServer { - id: string; - name: string; - endpoint: string; - provider: { - icon: string; - }; - enabled: boolean; - public: boolean; - profile?: any; - available?: boolean; - health?: { - status: string; - }; - assistantCount?: number; - minimal_client_version?: { - number: number; - }; -} - interface ErrorMessage { id: string; type: "error" | "warning" | "info"; diff --git a/src/stores/connectStore.ts b/src/stores/connectStore.ts index 57debf1e..a6d72b30 100644 --- a/src/stores/connectStore.ts +++ b/src/stores/connectStore.ts @@ -3,6 +3,7 @@ import { persist, subscribeWithSelector } from "zustand/middleware"; import { produce } from "immer"; import platformAdapter from "@/utils/platformAdapter"; +import { Server } from "@/types/server" const CONNECTOR_CHANGE_EVENT = "connector_data_change"; const DATASOURCE_CHANGE_EVENT = "datasourceData_change"; @@ -12,10 +13,10 @@ type keyArrayObject = { }; export type IConnectStore = { - serverList: any[]; - setServerList: (servers: []) => void; - currentService: any; - setCurrentService: (service: any) => void; + serverList: Server[]; + setServerList: (servers: Server[]) => void; + currentService: Server; + setCurrentService: (service: Server) => void; connector_data: keyArrayObject; setConnectorData: (connector_data: any[], key: string) => void; datasourceData: keyArrayObject; @@ -41,7 +42,7 @@ export const useConnectStore = create()( persist( (set) => ({ serverList: [], - setServerList: (serverList: []) => { + setServerList: (serverList: Server[]) => { console.log("set serverList:", serverList); set( produce((draft) => { @@ -49,7 +50,35 @@ export const useConnectStore = create()( }) ); }, - currentService: "default_coco_server", + // ... existing code ... + currentService: { + id: "default_coco_server", + builtin: true, + enabled: true, + 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" + }, + public: false, + available: true, + auth_provider: { + sso: { + url: "https://coco.infini.cloud/sso/login/" + } + }, + priority: 0 + }, +// ... existing code ... setCurrentService: (server: any) => { console.log("set default server:", server); set( diff --git a/src/types/server.ts b/src/types/server.ts new file mode 100644 index 00000000..8726b9a7 --- /dev/null +++ b/src/types/server.ts @@ -0,0 +1,65 @@ +interface Provider { + name: string; + icon?: string; + banner?: string; + description?: string; + eula?: string; + privacy_policy?: string; + website?: string; + auth_provider?: { + sso: { + url: string; + }; + }; +} + +interface Version { + number: string; +} + +interface MinimalClientVersion { + number: string; +} + +export type Status = 'green' | 'yellow' | 'red'; + +interface Health { + services?: Record; + status: Status; +} + +interface Preferences { + theme?: string; + language?: string; +} + +export interface UserProfile { + id: string; + name: string; + email: string; + avatar?: string; + preferences?: Preferences; +} + +export interface Server { + id: string; + builtin: boolean; + name: string; + endpoint: string; + provider: Provider; + version: Version; + minimal_client_version?: MinimalClientVersion; + updated?: string; + enabled: boolean; + public: boolean; + available: boolean; + health?: Health; + profile?: UserProfile; + auth_provider: { + sso: { + url: string; + }; + }; + priority: number; + +} \ No newline at end of file diff --git a/src/components/Settings/shortcut.ts b/src/types/shortcut.ts similarity index 100% rename from src/components/Settings/shortcut.ts rename to src/types/shortcut.ts diff --git a/src/utils/index.ts b/src/utils/index.ts index 08ef8dc5..2af5709e 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -27,7 +27,7 @@ export async function copyToClipboard(text: string) { document.execCommand("copy"); console.info("Copy Success"); } catch (error) { - console.info("Copy Failed"); + console.error("Copy Failed"); } document.body.removeChild(textArea); } @@ -68,7 +68,7 @@ export const IsTauri = () => { ); }; -export const OpenURLWithBrowser = async (url: string) => { +export const OpenURLWithBrowser = async (url?: string) => { if (!url) return; if (IsTauri()) { try {