-
-

{
- 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 (
+
+

{
+ 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)}
- >
-

{
- 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)}
+ >
+

{
+ 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 {