mirror of
https://github.com/infinilabs/coco-app.git
synced 2025-12-16 11:37:47 +01:00
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:
@@ -27,7 +27,7 @@
|
||||
"dotenv": "^16.4.7",
|
||||
"i18next": "^23.16.2",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.453.0",
|
||||
"lucide-react": "^0.461.0",
|
||||
"mermaid": "^11.4.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
@@ -55,6 +55,7 @@
|
||||
"@types/react-katex": "^3.0.4",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"immer": "^10.1.1",
|
||||
"postcss": "^8.4.47",
|
||||
"tailwindcss": "^3.4.14",
|
||||
"typescript": "^5.2.2",
|
||||
|
||||
23
pnpm-lock.yaml
generated
23
pnpm-lock.yaml
generated
@@ -60,8 +60,8 @@ importers:
|
||||
specifier: ^4.17.21
|
||||
version: 4.17.21
|
||||
lucide-react:
|
||||
specifier: ^0.453.0
|
||||
version: 0.453.0(react@18.3.1)
|
||||
specifier: ^0.461.0
|
||||
version: 0.461.0(react@18.3.1)
|
||||
mermaid:
|
||||
specifier: ^11.4.0
|
||||
version: 11.4.0
|
||||
@@ -106,7 +106,7 @@ importers:
|
||||
version: 11.0.3
|
||||
zustand:
|
||||
specifier: ^5.0.0
|
||||
version: 5.0.0(@types/react@18.3.11)(react@18.3.1)
|
||||
version: 5.0.0(@types/react@18.3.11)(immer@10.1.1)(react@18.3.1)
|
||||
devDependencies:
|
||||
'@tauri-apps/cli':
|
||||
specifier: '>=2.0.0'
|
||||
@@ -138,6 +138,9 @@ importers:
|
||||
autoprefixer:
|
||||
specifier: ^10.4.20
|
||||
version: 10.4.20(postcss@8.4.47)
|
||||
immer:
|
||||
specifier: ^10.1.1
|
||||
version: 10.1.1
|
||||
postcss:
|
||||
specifier: ^8.4.47
|
||||
version: 8.4.47
|
||||
@@ -1440,6 +1443,9 @@ packages:
|
||||
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
immer@10.1.1:
|
||||
resolution: {integrity: sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==}
|
||||
|
||||
inline-style-parser@0.2.4:
|
||||
resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==}
|
||||
|
||||
@@ -1577,8 +1583,8 @@ packages:
|
||||
lru-cache@5.1.1:
|
||||
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
||||
|
||||
lucide-react@0.453.0:
|
||||
resolution: {integrity: sha512-kL+RGZCcJi9BvJtzg2kshO192Ddy9hv3ij+cPrVPWSRzgCWCVazoQJxOjAwgK53NomL07HB7GPHW120FimjNhQ==}
|
||||
lucide-react@0.461.0:
|
||||
resolution: {integrity: sha512-Scpw3D/dV1bgVRC5Kh774RCm99z0iZpPv75M6kg7QL1lLvkQ1rmI1Sjjic1aGp1ULBwd7FokV6ry0g+d6pMB+w==}
|
||||
peerDependencies:
|
||||
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc
|
||||
|
||||
@@ -3616,6 +3622,8 @@ snapshots:
|
||||
dependencies:
|
||||
safer-buffer: 2.1.2
|
||||
|
||||
immer@10.1.1: {}
|
||||
|
||||
inline-style-parser@0.2.4: {}
|
||||
|
||||
internmap@1.0.1: {}
|
||||
@@ -3726,7 +3734,7 @@ snapshots:
|
||||
dependencies:
|
||||
yallist: 3.1.1
|
||||
|
||||
lucide-react@0.453.0(react@18.3.1):
|
||||
lucide-react@0.461.0(react@18.3.1):
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
|
||||
@@ -4708,9 +4716,10 @@ snapshots:
|
||||
|
||||
yaml@2.6.0: {}
|
||||
|
||||
zustand@5.0.0(@types/react@18.3.11)(react@18.3.1):
|
||||
zustand@5.0.0(@types/react@18.3.11)(immer@10.1.1)(react@18.3.1):
|
||||
optionalDependencies:
|
||||
'@types/react': 18.3.11
|
||||
immer: 10.1.1
|
||||
react: 18.3.1
|
||||
|
||||
zwitch@2.0.4: {}
|
||||
|
||||
@@ -47,14 +47,20 @@ export const tauriFetch = async <T = any>({
|
||||
const auth = authStore?.state?.auth
|
||||
console.log("auth", auth)
|
||||
|
||||
if (baseURL.endsWith("/")) {
|
||||
baseURL = baseURL.slice(0, -1);
|
||||
}
|
||||
|
||||
if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
||||
// If not, prepend the defaultPrefix
|
||||
url = baseURL + url;
|
||||
}
|
||||
|
||||
if (method !== "GET") {
|
||||
headers["Content-Type"] = "application/json";
|
||||
}
|
||||
|
||||
headers["X-API-TOKEN"] = headers["X-API-TOKEN"] || auth?.token || "";
|
||||
headers["X-API-TOKEN"] = headers["X-API-TOKEN"] || (auth && auth[endpoint_http]?.token) || "";
|
||||
|
||||
// debug API
|
||||
const requestInfo = {
|
||||
|
||||
BIN
src/assets/images/coco-cloud-banner.jpeg
Normal file
BIN
src/assets/images/coco-cloud-banner.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 66 KiB |
@@ -16,6 +16,7 @@ import source_default_img from "@/assets/images/source_default.png";
|
||||
import source_default_dark_img from "@/assets/images/source_default_dark.png";
|
||||
import file_efault_img from "@/assets/images/file_efault.png";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
import { useConnectStore } from "@/stores/connectStore";
|
||||
|
||||
type ISearchData = Record<string, any[]>;
|
||||
|
||||
@@ -46,8 +47,9 @@ function DropdownList({
|
||||
|
||||
const { theme } = useTheme();
|
||||
|
||||
const connector_data = useAppStore((state) => state.connector_data);
|
||||
const datasourceData = useAppStore((state) => state.datasourceData);
|
||||
const connector_data = useConnectStore((state) => state.connector_data);
|
||||
const datasourceData = useConnectStore((state) => state.datasourceData);
|
||||
|
||||
const endpoint_http = useAppStore((state) => state.endpoint_http);
|
||||
const setSourceData = useSearchStore((state) => state.setSourceData);
|
||||
|
||||
@@ -163,13 +165,13 @@ function DropdownList({
|
||||
function findConnectorIcon(item: any) {
|
||||
const id = item?._source?.source?.id || "";
|
||||
|
||||
const result_source = datasourceData.find(
|
||||
const result_source = datasourceData[endpoint_http]?.find(
|
||||
(data: any) => data._source.id === id
|
||||
);
|
||||
|
||||
const connector_id = result_source?._source?.connector?.id;
|
||||
|
||||
const result_connector = connector_data.find(
|
||||
const result_connector = connector_data[endpoint_http]?.find(
|
||||
(data: any) => data._source.id === connector_id
|
||||
);
|
||||
|
||||
@@ -184,7 +186,7 @@ function DropdownList({
|
||||
return theme === "dark" ? source_default_dark_img : source_default_img;
|
||||
}
|
||||
|
||||
if (icons?.includes("http")) {
|
||||
if (icons?.startsWith("http://") || icons?.startsWith("https://")) {
|
||||
return icons;
|
||||
} else {
|
||||
return endpoint_http + icons;
|
||||
@@ -201,7 +203,7 @@ function DropdownList({
|
||||
return file_efault_img;
|
||||
}
|
||||
|
||||
if (selectedIcon?.includes("http")) {
|
||||
if (selectedIcon?.startsWith("http://") || selectedIcon?.startsWith("https://")) {
|
||||
return selectedIcon;
|
||||
} else {
|
||||
return endpoint_http + selectedIcon;
|
||||
@@ -218,7 +220,7 @@ function DropdownList({
|
||||
return theme === "dark" ? source_default_dark_img : source_default_img;
|
||||
}
|
||||
|
||||
if (selectedIcon?.includes("http")) {
|
||||
if (selectedIcon?.startsWith("http://") || selectedIcon?.startsWith("https://")) {
|
||||
return selectedIcon;
|
||||
} else {
|
||||
return endpoint_http + selectedIcon;
|
||||
|
||||
@@ -11,6 +11,7 @@ import source_default_dark_img from "@/assets/images/source_default_dark.png";
|
||||
import { useSearchStore } from "@/stores/searchStore";
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
import { useConnectStore } from "@/stores/connectStore";
|
||||
|
||||
interface FooterProps {
|
||||
isChat: boolean;
|
||||
@@ -19,8 +20,10 @@ interface FooterProps {
|
||||
|
||||
export default function Footer({ name }: FooterProps) {
|
||||
const sourceData = useSearchStore((state) => state.sourceData);
|
||||
const connector_data = useAppStore((state) => state.connector_data);
|
||||
const datasourceData = useAppStore((state) => state.datasourceData);
|
||||
|
||||
const connector_data = useConnectStore((state) => state.connector_data);
|
||||
const datasourceData = useConnectStore((state) => state.datasourceData);
|
||||
|
||||
const endpoint_http = useAppStore((state) => state.endpoint_http);
|
||||
|
||||
const { theme } = useTheme();
|
||||
@@ -28,13 +31,13 @@ export default function Footer({ name }: FooterProps) {
|
||||
function findConnectorIcon(item: any) {
|
||||
const id = item?._source?.source?.id || "";
|
||||
|
||||
const result_source = datasourceData.find(
|
||||
const result_source = datasourceData[endpoint_http]?.find(
|
||||
(data: any) => data._source.id === id
|
||||
);
|
||||
|
||||
const connector_id = result_source?._source?.connector?.id;
|
||||
|
||||
const result_connector = connector_data.find(
|
||||
const result_connector = connector_data[endpoint_http]?.find(
|
||||
(data: any) => data._source.id === connector_id
|
||||
);
|
||||
|
||||
@@ -49,7 +52,7 @@ export default function Footer({ name }: FooterProps) {
|
||||
return theme === "dark" ? source_default_dark_img : source_default_img;
|
||||
}
|
||||
|
||||
if (icons?.includes("http")) {
|
||||
if (icons?.startsWith("http://") || icons?.startsWith("https://")) {
|
||||
return icons;
|
||||
} else {
|
||||
return endpoint_http + icons;
|
||||
|
||||
@@ -70,14 +70,22 @@ export default function ChatInput({
|
||||
}
|
||||
}, [inputValue, disabled, onSend]);
|
||||
|
||||
const pressedKeys = new Set<string>();
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
pressedKeys.add(e.code);
|
||||
|
||||
if (e.code === "MetaLeft" || e.code === "MetaRight") {
|
||||
setIsCommandPressed(true);
|
||||
}
|
||||
|
||||
if (e.metaKey) {
|
||||
if (pressedKeys.has("MetaLeft") || pressedKeys.has("MetaRight")) {
|
||||
e.preventDefault();
|
||||
switch (e.code) {
|
||||
case "Comma":
|
||||
setIsCommandPressed(false);
|
||||
break;
|
||||
case "KeyI":
|
||||
handleToggleFocus();
|
||||
break;
|
||||
@@ -88,7 +96,7 @@ export default function ChatInput({
|
||||
console.log("KeyM");
|
||||
break;
|
||||
case "Enter":
|
||||
isChatMode && (curChatEnd ? handleSubmit() : disabledChange());
|
||||
isChatMode && (curChatEnd ? handleSubmit() : disabledChange?.());
|
||||
break;
|
||||
case "KeyO":
|
||||
console.log("KeyO");
|
||||
@@ -107,10 +115,19 @@ export default function ChatInput({
|
||||
}
|
||||
}
|
||||
},
|
||||
[handleToggleFocus, isChatMode, handleSubmit]
|
||||
[
|
||||
handleToggleFocus,
|
||||
isChatMode,
|
||||
handleSubmit,
|
||||
setSourceData,
|
||||
setIsCommandPressed,
|
||||
disabledChange,
|
||||
curChatEnd,
|
||||
]
|
||||
);
|
||||
|
||||
const handleKeyUp = useCallback((e: KeyboardEvent) => {
|
||||
pressedKeys.delete(e.code);
|
||||
if (e.code === "MetaLeft" || e.code === "MetaRight") {
|
||||
setIsCommandPressed(false);
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ function Search({ isChatMode, input }: SearchProps) {
|
||||
};
|
||||
}
|
||||
|
||||
const debouncedSearch = useCallback(debounce(getSuggest, 300), [input]);
|
||||
const debouncedSearch = useCallback(debounce(getSuggest, 500), [input]);
|
||||
|
||||
useEffect(() => {
|
||||
!isChatMode && !sourceData && debouncedSearch();
|
||||
|
||||
@@ -1,7 +1,18 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Cloud } from "lucide-react";
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import {
|
||||
RefreshCcw,
|
||||
Globe,
|
||||
PackageOpen,
|
||||
GitFork,
|
||||
CalendarSync,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import {
|
||||
onOpenUrl,
|
||||
getCurrent as getCurrentDeepLinkUrls,
|
||||
} from "@tauri-apps/plugin-deep-link";
|
||||
|
||||
import { UserProfile } from "./UserProfile";
|
||||
import { DataSourcesList } from "./DataSourcesList";
|
||||
@@ -11,39 +22,54 @@ import { OpenBrowserURL } from "@/utils/index";
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
import { useAuthStore } from "@/stores/authStore";
|
||||
import { tauriFetch } from "@/api/tauriFetchClient";
|
||||
import {
|
||||
onOpenUrl,
|
||||
getCurrent as getCurrentDeepLinkUrls,
|
||||
} from "@tauri-apps/plugin-deep-link";
|
||||
import { useConnectStore } from "@/stores/connectStore";
|
||||
import bannerImg from "@/assets/images/coco-cloud-banner.jpeg";
|
||||
|
||||
export default function CocoCloud() {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const [isConnect] = useState(true);
|
||||
const [isConnect, setIsConnect] = useState(true);
|
||||
|
||||
const app_uid = useAppStore((state) => state.app_uid);
|
||||
const setAppUid = useAppStore((state) => state.setAppUid);
|
||||
const endpoint_http = useAppStore((state) => state.endpoint_http);
|
||||
const setEndpoint = useAppStore((state) => state.setEndpoint);
|
||||
const endpoint = useAppStore((state) => state.endpoint);
|
||||
|
||||
const { auth, setAuth } = useAuthStore();
|
||||
const auth = useAuthStore((state) => state.auth);
|
||||
const setAuth = useAuthStore((state) => state.setAuth);
|
||||
const userInfo = useAuthStore((state) => state.userInfo);
|
||||
const setUserInfo = useAuthStore((state) => state.setUserInfo);
|
||||
const defaultService = useConnectStore((state) => state.defaultService);
|
||||
const currentService = useConnectStore((state) => state.currentService);
|
||||
const setDefaultService = useConnectStore((state) => state.setDefaultService);
|
||||
const setCurrentService = useConnectStore((state) => state.setCurrentService);
|
||||
const deleteOtherService = useConnectStore(
|
||||
(state) => state.deleteOtherService
|
||||
);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [refreshLoading, setRefreshLoading] = useState(false);
|
||||
|
||||
const getProfile = async () => {
|
||||
useEffect(() => {
|
||||
console.log("currentService", currentService);
|
||||
setLoading(false);
|
||||
setRefreshLoading(false);
|
||||
setError(null);
|
||||
setEndpoint(currentService.endpoint);
|
||||
setIsConnect(true);
|
||||
}, [JSON.stringify(currentService)]);
|
||||
|
||||
const getProfile = useCallback(async () => {
|
||||
const response: any = await tauriFetch({
|
||||
url: `/provider/account/profile`,
|
||||
url: `/account/profile`,
|
||||
method: "GET",
|
||||
});
|
||||
console.log("getProfile", response);
|
||||
setUserInfo(response.data || {});
|
||||
};
|
||||
setUserInfo(response.data || {}, endpoint);
|
||||
}, [endpoint]);
|
||||
|
||||
const handleOAuthCallback = async (
|
||||
code: string | null,
|
||||
provider: string | null
|
||||
) => {
|
||||
const handleOAuthCallback = useCallback(
|
||||
async (code: string | null, provider: string | null) => {
|
||||
if (!code) {
|
||||
setError("No authorization code received");
|
||||
return;
|
||||
@@ -66,14 +92,18 @@ export default function CocoCloud() {
|
||||
);
|
||||
|
||||
if (response.data?.access_token) {
|
||||
await setAuth({
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -83,12 +113,14 @@ export default function CocoCloud() {
|
||||
} catch (e) {
|
||||
console.error("Sign in failed:", error);
|
||||
setError("Sign in failed: catch");
|
||||
await setAuth(undefined);
|
||||
await setAuth(undefined, endpoint);
|
||||
throw error;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
},
|
||||
[app_uid, endpoint]
|
||||
);
|
||||
|
||||
const handleUrl = (url: string) => {
|
||||
try {
|
||||
@@ -108,7 +140,6 @@ export default function CocoCloud() {
|
||||
// default:
|
||||
// console.log("Unhandled deep link path:", urlObject.pathname);
|
||||
// }
|
||||
|
||||
} catch (err) {
|
||||
console.error("Failed to parse URL:", err);
|
||||
setError("Invalid URL format");
|
||||
@@ -135,87 +166,167 @@ export default function CocoCloud() {
|
||||
return () => {
|
||||
unlisten.then((fn) => fn());
|
||||
};
|
||||
}, []);
|
||||
}, [app_uid]);
|
||||
|
||||
const LoginClick = useCallback(() => {
|
||||
if (loading) return;
|
||||
setAuth(undefined, endpoint);
|
||||
|
||||
function LoginClick() {
|
||||
let uid = uuidv4();
|
||||
setAppUid(uid);
|
||||
|
||||
console.log("LoginClick", uid, currentService.auth_provider.sso.url);
|
||||
|
||||
OpenBrowserURL(
|
||||
`${endpoint_http}/sso/login/?provider=coco-cloud&product=coco&request_id=${uid}`
|
||||
`${currentService.auth_provider.sso.url}/?provider=coco-cloud&product=coco&request_id=${uid}`
|
||||
);
|
||||
|
||||
setLoading(true);
|
||||
}, [JSON.stringify(currentService)]);
|
||||
|
||||
function goToHref(url: string) {
|
||||
OpenBrowserURL(url);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen bg-gray-50">
|
||||
<Sidebar />
|
||||
const refreshClick = useCallback(() => {
|
||||
setRefreshLoading(true);
|
||||
tauriFetch({
|
||||
url: `/provider/_info`,
|
||||
method: "GET",
|
||||
})
|
||||
.then((res) => {
|
||||
setEndpoint(res.data.endpoint);
|
||||
setCurrentService(res.data || {});
|
||||
if (res.data?.endpoint === "https://coco.infini.cloud/") {
|
||||
setDefaultService(res.data);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
})
|
||||
.finally(() => {
|
||||
setRefreshLoading(false);
|
||||
});
|
||||
}, [JSON.stringify(defaultService)]);
|
||||
|
||||
<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>
|
||||
{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}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isConnect ? (
|
||||
<div className="max-w-4xl mx-auto px-4 py-8">
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<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-2 px-4 py-2 bg-white rounded-md border border-gray-200">
|
||||
<Cloud className="w-5 h-5 text-blue-500" />
|
||||
<span className="font-medium text-[#333]">Coco Cloud</span>
|
||||
<div className="flex items-center text-gray-900 dark:text-white font-medium">
|
||||
{currentService.name}
|
||||
</div>
|
||||
<span className="px-3 py-1 text-sm text-blue-600 bg-blue-50 rounded-md">
|
||||
Available
|
||||
</span>
|
||||
</div>
|
||||
<button className="p-2 text-gray-500 hover:text-gray-700">
|
||||
<Cloud className="w-5 h-5" />
|
||||
<div className="flex gap-2">
|
||||
<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 className="mb-8">
|
||||
<div className="text-sm text-gray-500 mb-4">
|
||||
<span>Service provision: INFINI Labs</span>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400 mb-2 flex">
|
||||
<span className="flex items-center gap-1">
|
||||
<PackageOpen className="w-4 h-4" />{" "}
|
||||
{currentService.provider.name}
|
||||
</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>Update time: 2023-05-12</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<CalendarSync className="w-4 h-4" /> {currentService.updated}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-gray-600 leading-relaxed">
|
||||
Coco Cloud provides users with a cloud storage and data
|
||||
integration platform that supports account registration and data
|
||||
source management. Users can integrate multiple data sources
|
||||
(such as Google Drive, yuque, GitHub, etc.), easily access and
|
||||
search for files, documents and codes across platforms, and
|
||||
achieve efficient data collaboration and management.
|
||||
<p className="text-gray-600 dark:text-gray-300 leading-relaxed">
|
||||
{currentService.provider.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{currentService.auth_provider.sso.url ? (
|
||||
<div className="mb-8">
|
||||
<h2 className="text-lg font-medium text-gray-900 mb-4">
|
||||
<h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
|
||||
Account Information
|
||||
</h2>
|
||||
{auth ? (
|
||||
<UserProfile userInfo={userInfo} />
|
||||
{auth && auth[endpoint] ? (
|
||||
<UserProfile userInfo={userInfo[endpoint]} />
|
||||
) : (
|
||||
<div>
|
||||
<button
|
||||
className="px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"
|
||||
className="px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors mb-3"
|
||||
onClick={LoginClick}
|
||||
>
|
||||
{loading ? "Login..." : "Login"}
|
||||
</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>
|
||||
) : (
|
||||
<ConnectService />
|
||||
<ConnectService setIsConnect={setIsConnect} />
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -1,52 +1,121 @@
|
||||
import React, { useState } from 'react';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
import React, { useState, useCallback } from "react";
|
||||
import { ChevronLeft } from "lucide-react";
|
||||
|
||||
export function ConnectService() {
|
||||
const [sourceName, setSourceName] = useState('');
|
||||
import { useConnectStore } from "@/stores/connectStore";
|
||||
import { tauriFetch } from "@/api/tauriFetchClient";
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
|
||||
interface ConnectServiceProps {
|
||||
setIsConnect: (isConnect: boolean) => void;
|
||||
}
|
||||
|
||||
export function ConnectService({ setIsConnect }: ConnectServiceProps) {
|
||||
const addOtherServices = useConnectStore((state) => state.addOtherServices);
|
||||
const setCurrentService = useConnectStore((state) => state.setCurrentService);
|
||||
const defaultService = useConnectStore((state) => state.defaultService);
|
||||
const otherServices = useConnectStore((state) => state.otherServices);
|
||||
|
||||
const setEndpoint = useAppStore((state) => state.setEndpoint);
|
||||
|
||||
const [endpointLink, setEndpointLink] = useState("");
|
||||
const [refreshLoading, setRefreshLoading] = useState(false);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
console.log('Connecting Google Drive with name:', sourceName);
|
||||
console.log("Connecting Google Drive with name:", endpointLink);
|
||||
};
|
||||
|
||||
const goBack = () => {
|
||||
setIsConnect(true);
|
||||
};
|
||||
|
||||
const addService = useCallback(() => {
|
||||
if (!endpointLink) return;
|
||||
if (!endpointLink.startsWith("http://") && !endpointLink.startsWith("https://")) {
|
||||
return
|
||||
}
|
||||
setRefreshLoading(true);
|
||||
//
|
||||
let baseURL = endpointLink;
|
||||
if (baseURL.endsWith("/")) {
|
||||
baseURL = baseURL.slice(0, -1);
|
||||
}
|
||||
|
||||
tauriFetch({
|
||||
url: `${baseURL}/provider/_info`,
|
||||
method: "GET",
|
||||
})
|
||||
.then((res) => {
|
||||
if (
|
||||
res.data?.endpoint === defaultService.endpoint ||
|
||||
otherServices.some(
|
||||
(item: any) => item.endpoint === res.data?.endpoint
|
||||
)
|
||||
) {
|
||||
console.error(`${res.data?.endpoint} Repeated`);
|
||||
} else {
|
||||
addOtherServices(res.data);
|
||||
setCurrentService(res.data);
|
||||
setEndpoint(res.data.endpoint);
|
||||
setIsConnect(true);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
})
|
||||
.finally(() => {
|
||||
setRefreshLoading(false);
|
||||
});
|
||||
}, [endpointLink]);
|
||||
|
||||
return (
|
||||
<div className="p-8 max-w-4xl">
|
||||
<div className="mb-8">
|
||||
<button className="flex items-center text-gray-600 hover:text-gray-900">
|
||||
<ArrowLeft className="w-5 h-5 mr-2" />
|
||||
<span>Connect Google Drive</span>
|
||||
<div className="max-w-4xl">
|
||||
<div className="flex items-center gap-2 mb-8">
|
||||
<button
|
||||
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"
|
||||
onClick={goBack}
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
</button>
|
||||
<div className="text-xl text-[#101010] dark:text-white">
|
||||
Connecting to third-party services
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-8">
|
||||
<p className="text-gray-600">
|
||||
Coco needs to obtain authorization from your Google Drive account
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
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>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div>
|
||||
<label htmlFor="sourceName" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Data Source Name
|
||||
<label
|
||||
htmlFor="endpoint"
|
||||
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2.5"
|
||||
>
|
||||
Server address
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<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"
|
||||
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"
|
||||
/>
|
||||
</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"
|
||||
onClick={addService}
|
||||
>
|
||||
Save
|
||||
{refreshLoading ? "Connecting..." : "Connect"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { Link2, Trash2 } from "lucide-react";
|
||||
import { Link2 } from "lucide-react";
|
||||
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
import source_default_img from "@/assets/images/source_default.png";
|
||||
import source_default_dark_img from "@/assets/images/source_default_dark.png";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
import { useConnectStore } from "@/stores/connectStore";
|
||||
|
||||
interface Account {
|
||||
email: string;
|
||||
@@ -7,56 +13,88 @@ interface Account {
|
||||
|
||||
interface DataSourceItemProps {
|
||||
name: string;
|
||||
type: string;
|
||||
accounts: Account[];
|
||||
connector: any;
|
||||
accounts?: Account[];
|
||||
}
|
||||
|
||||
export function DataSourceItem({ name, type, accounts }: DataSourceItemProps) {
|
||||
const isConnected = accounts.length > 0;
|
||||
export function DataSourceItem({ name, connector }: DataSourceItemProps) {
|
||||
// const isConnected = true;
|
||||
|
||||
const { theme } = useTheme();
|
||||
|
||||
const connector_data = useConnectStore((state) => state.connector_data);
|
||||
const endpoint_http = useAppStore((state) => state.endpoint_http);
|
||||
|
||||
function findConnectorIcon() {
|
||||
const connector_id = connector?.id;
|
||||
|
||||
const result_connector = connector_data[endpoint_http]?.find(
|
||||
(data: any) => data._source.id === connector_id
|
||||
);
|
||||
|
||||
return result_connector?._source;
|
||||
}
|
||||
|
||||
function getTypeIcon() {
|
||||
const connectorSource = findConnectorIcon();
|
||||
const icons = connectorSource?.icon;
|
||||
|
||||
if (!icons) {
|
||||
return theme === "dark" ? source_default_dark_img : source_default_img;
|
||||
}
|
||||
|
||||
if (icons?.startsWith("http://") || icons?.startsWith("https://")) {
|
||||
return icons;
|
||||
} else {
|
||||
return endpoint_http + icons;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<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 space-x-3">
|
||||
<img src={`/icons/${type}.svg`} alt={name} className="w-6 h-6" />
|
||||
<span className="font-medium">{name}</span>
|
||||
<img src={getTypeIcon()} alt={name} className="w-6 h-6" />
|
||||
<span className="font-medium text-gray-900 dark:text-white">
|
||||
{name}
|
||||
</span>
|
||||
</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" />
|
||||
</button>
|
||||
</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"}
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
{accounts.map((account, index) => (
|
||||
{/* {accounts.map((account, index) => (
|
||||
<div
|
||||
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="w-8 h-8 bg-gray-100 rounded-full flex items-center justify-center">
|
||||
<span className="text-sm text-gray-500">
|
||||
<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 dark:text-gray-400">
|
||||
{account.email[0].toUpperCase()}
|
||||
</span>
|
||||
</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}`}
|
||||
</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 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}
|
||||
</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" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
))} */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,36 +1,55 @@
|
||||
import { DataSourceItem } from './DataSourceItem';
|
||||
import { useEffect, useState } from "react";
|
||||
import { RefreshCcw } from "lucide-react";
|
||||
|
||||
import { DataSourceItem } from "./DataSourceItem";
|
||||
import { useConnectStore } from "@/stores/connectStore";
|
||||
import { tauriFetch } from "@/api/tauriFetchClient";
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
|
||||
export function DataSourcesList() {
|
||||
const dataSources = [
|
||||
{
|
||||
id: 'google-drive',
|
||||
name: 'Google Drive',
|
||||
type: 'google',
|
||||
accounts: [
|
||||
{ email: 'an121245@gmail.com', lastSync: '2025-01-02 09:50 AM' },
|
||||
{ email: '9paiii@gmail.com', lastSync: '2025-01-02 09:50 AM' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'yuque',
|
||||
name: 'Yuque',
|
||||
type: 'yuque',
|
||||
accounts: []
|
||||
},
|
||||
{
|
||||
id: 'github',
|
||||
name: 'Github',
|
||||
type: 'github',
|
||||
accounts: []
|
||||
const datasourceData = useConnectStore((state) => state.datasourceData);
|
||||
const setDatasourceData = useConnectStore((state) => state.setDatasourceData);
|
||||
|
||||
const endpoint_http = useAppStore((state) => state.endpoint_http);
|
||||
|
||||
const [refreshLoading, setRefreshLoading] = useState(false);
|
||||
|
||||
async function getDatasourceData() {
|
||||
setRefreshLoading(true);
|
||||
try {
|
||||
const response = await tauriFetch({
|
||||
url: `/datasource/_search`,
|
||||
method: "GET",
|
||||
});
|
||||
console.log("datasource", response);
|
||||
const data = response.data?.hits?.hits || [];
|
||||
setDatasourceData(data, endpoint_http);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch user data:", error);
|
||||
}
|
||||
];
|
||||
setRefreshLoading(false);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getDatasourceData()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<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">
|
||||
{dataSources.map(source => (
|
||||
<DataSourceItem key={source.id} {...source} />
|
||||
{datasourceData[endpoint_http]?.map((source) => (
|
||||
<DataSourceItem key={source._id} {...source._source} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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 (
|
||||
<div className="w-64 border-r border-gray-200 bg-white">
|
||||
<div className="p-4">
|
||||
<div className="flex items-center space-x-2 px-3 py-2 bg-blue-50 text-blue-600 rounded-lg mb-6">
|
||||
<Cloud className="w-5 h-5" />
|
||||
<span className="font-medium">Coco Cloud</span>
|
||||
<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 py-8">
|
||||
<div
|
||||
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 ${
|
||||
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" />
|
||||
<button className="text-blue-600 hover:text-blue-700">
|
||||
<Cloud className="w-4 h-4" />
|
||||
<button className="text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300">
|
||||
{defaultHealth ? (
|
||||
<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">
|
||||
<div className="text-sm font-medium text-gray-500 px-3 mb-2">
|
||||
<div className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">
|
||||
Third-party services
|
||||
</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" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { User, Edit, LogOut } from "lucide-react";
|
||||
import { User, LogOut } from "lucide-react";
|
||||
|
||||
import { useAuthStore } from "@/stores/authStore";
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
|
||||
interface UserPreferences {
|
||||
theme: "dark" | "light";
|
||||
language: string;
|
||||
}
|
||||
interface UserInfo {
|
||||
username: string;
|
||||
name: string;
|
||||
email: string;
|
||||
avatar?: string;
|
||||
roles: string[]; // ["admin", "editor"]
|
||||
@@ -21,45 +22,40 @@ interface UserProfileProps {
|
||||
export function UserProfile({ userInfo }: UserProfileProps) {
|
||||
const setAuth = useAuthStore((state) => state.setAuth);
|
||||
const setUserInfo = useAuthStore((state) => state.setUserInfo);
|
||||
const endpoint = useAppStore((state) => state.endpoint);
|
||||
|
||||
const handleLogout = () => {
|
||||
setAuth(undefined);
|
||||
setUserInfo({});
|
||||
setAuth(undefined, endpoint);
|
||||
setUserInfo({}, endpoint);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="w-12 h-12 bg-gray-100 rounded-full flex items-center justify-center">
|
||||
{userInfo.avatar ? (
|
||||
<img
|
||||
src={userInfo.avatar}
|
||||
alt=""
|
||||
className="w-6 h-6"
|
||||
/>
|
||||
<div className="w-12 h-12 bg-gray-100 dark:bg-gray-700 rounded-full flex items-center justify-center">
|
||||
{userInfo?.avatar ? (
|
||||
<img 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 className="flex-1">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="font-medium text-gray-900">
|
||||
{userInfo.username || "-"}
|
||||
<div className="flex items-center space-x-4">
|
||||
<span className="font-medium text-gray-900 dark:text-white">
|
||||
{userInfo?.name || "-"}
|
||||
</span>
|
||||
<button className="text-gray-400 hover:text-gray-600">
|
||||
<Edit className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
<span className="text-sm text-gray-500">{userInfo.email || "-"}</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="flex items-center space-x-1 text-red-500 hover:text-red-600"
|
||||
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" />
|
||||
<span>Logout</span>
|
||||
</button>
|
||||
</div>
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{userInfo?.email || "-"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -57,8 +57,8 @@ export default function ChatInput() {
|
||||
method: "GET",
|
||||
});
|
||||
setInfo(JSON.stringify(response));
|
||||
console.log(response.status); // e.g. 200
|
||||
console.log(response.statusText); // e.g. "OK"
|
||||
// console.log(response.status); // e.g. 200
|
||||
// console.log(response.statusText); // e.g. "OK"
|
||||
} catch (error) {
|
||||
console.error("Error sending message:", error);
|
||||
setInfo(JSON.stringify(error));
|
||||
|
||||
@@ -225,7 +225,7 @@ const ChatAI = forwardRef<ChatAIRef, ChatAIProps>(
|
||||
if (isTauri()) {
|
||||
createWin && createWin({
|
||||
label: "chat",
|
||||
title: "Coco AI",
|
||||
title: "Coco Chat",
|
||||
dragDropEnabled: true,
|
||||
center: true,
|
||||
width: 900,
|
||||
|
||||
@@ -14,7 +14,7 @@ const ChatSwitch: React.FC<ChatSwitchProps> = ({ isChatMode, onChange }) => {
|
||||
(event: KeyboardEvent) => {
|
||||
if (event.metaKey && event.key === "t") {
|
||||
event.preventDefault();
|
||||
console.log("Switch mode triggered");
|
||||
// console.log("Switch mode triggered");
|
||||
handleToggle();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -5,14 +5,16 @@ import {formatter} from "@/utils/index"
|
||||
import source_default_img from "@/assets/images/source_default.png";
|
||||
import source_default_dark_img from "@/assets/images/source_default_dark.png";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
import { useConnectStore } from "@/stores/connectStore";
|
||||
|
||||
interface DocumentDetailProps {
|
||||
document: any;
|
||||
}
|
||||
|
||||
export const DocumentDetail: React.FC<DocumentDetailProps> = ({ document }) => {
|
||||
const connector_data = useAppStore((state) => state.connector_data);
|
||||
const datasourceData = useAppStore((state) => state.datasourceData);
|
||||
const connector_data = useConnectStore((state) => state.connector_data);
|
||||
const datasourceData = useConnectStore((state) => state.datasourceData);
|
||||
|
||||
const endpoint_http = useAppStore((state) => state.endpoint_http);
|
||||
|
||||
const { theme } = useTheme();
|
||||
@@ -20,13 +22,13 @@ export const DocumentDetail: React.FC<DocumentDetailProps> = ({ document }) => {
|
||||
function findConnectorIcon(item: any) {
|
||||
const id = item?._source?.source?.id || "";
|
||||
|
||||
const result_source = datasourceData.find(
|
||||
const result_source = datasourceData[endpoint_http]?.find(
|
||||
(data: any) => data._source.id === id
|
||||
);
|
||||
|
||||
const connector_id = result_source?._source?.connector?.id;
|
||||
|
||||
const result_connector = connector_data.find(
|
||||
const result_connector = connector_data[endpoint_http]?.find(
|
||||
(data: any) => data._source.id === connector_id
|
||||
);
|
||||
|
||||
@@ -41,7 +43,7 @@ export const DocumentDetail: React.FC<DocumentDetailProps> = ({ document }) => {
|
||||
return theme === "dark" ? source_default_dark_img : source_default_img;
|
||||
}
|
||||
|
||||
if (icons?.includes("http")) {
|
||||
if (icons?.startsWith("http://") || icons?.startsWith("https://")) {
|
||||
return icons;
|
||||
} else {
|
||||
return endpoint_http + icons;
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useSearchStore } from "@/stores/searchStore";
|
||||
import { SearchHeader } from "./SearchHeader";
|
||||
import file_efault_img from "@/assets/images/file_efault.png";
|
||||
import noDataImg from "@/assets/coconut-tree.png";
|
||||
import { useConnectStore } from "@/stores/connectStore";
|
||||
|
||||
interface DocumentListProps {
|
||||
onSelectDocument: (id: string) => void;
|
||||
@@ -25,8 +26,9 @@ export const DocumentList: React.FC<DocumentListProps> = ({
|
||||
getDocDetail,
|
||||
isChatMode,
|
||||
}) => {
|
||||
const connector_data = useAppStore((state) => state.connector_data);
|
||||
const datasourceData = useAppStore((state) => state.datasourceData);
|
||||
const connector_data = useConnectStore((state) => state.connector_data);
|
||||
const datasourceData = useConnectStore((state) => state.datasourceData);
|
||||
|
||||
const sourceData = useSearchStore((state) => state.sourceData);
|
||||
const endpoint_http = useAppStore((state) => state.endpoint_http);
|
||||
|
||||
@@ -109,13 +111,13 @@ export const DocumentList: React.FC<DocumentListProps> = ({
|
||||
function findConnectorIcon(item: any) {
|
||||
const id = item?._source?.source?.id || "";
|
||||
|
||||
const result_source = datasourceData.find(
|
||||
const result_source = datasourceData[endpoint_http]?.find(
|
||||
(data: any) => data._source.id === id
|
||||
);
|
||||
|
||||
const connector_id = result_source?._source?.connector?.id;
|
||||
|
||||
const result_connector = connector_data.find(
|
||||
const result_connector = connector_data[endpoint_http]?.find(
|
||||
(data: any) => data._source.id === connector_id
|
||||
);
|
||||
|
||||
@@ -132,7 +134,7 @@ export const DocumentList: React.FC<DocumentListProps> = ({
|
||||
return file_efault_img;
|
||||
}
|
||||
|
||||
if (selectedIcon?.includes("http")) {
|
||||
if (selectedIcon?.startsWith("http://") || selectedIcon?.startsWith("https://")) {
|
||||
return selectedIcon;
|
||||
} else {
|
||||
return endpoint_http + selectedIcon;
|
||||
|
||||
@@ -19,7 +19,7 @@ function DropdownList({ selected, suggests }: DropdownListProps) {
|
||||
try {
|
||||
if (isTauri()) {
|
||||
await open(url);
|
||||
console.log("URL opened in default browser");
|
||||
// console.log("URL opened in default browser");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to open URL:", error);
|
||||
@@ -27,12 +27,12 @@ function DropdownList({ selected, suggests }: DropdownListProps) {
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
console.log(
|
||||
"handleKeyDown",
|
||||
e.key,
|
||||
showIndex,
|
||||
e.key >= "0" && e.key <= "9" && showIndex
|
||||
);
|
||||
// console.log(
|
||||
// "handleKeyDown",
|
||||
// e.key,
|
||||
// showIndex,
|
||||
// e.key >= "0" && e.key <= "9" && showIndex
|
||||
// );
|
||||
if (!suggests.length) return;
|
||||
|
||||
if (e.key === "ArrowUp") {
|
||||
@@ -51,7 +51,7 @@ function DropdownList({ selected, suggests }: DropdownListProps) {
|
||||
}
|
||||
|
||||
if (e.key === "Enter" && selectedItem !== null) {
|
||||
console.log("Enter key pressed", selectedItem);
|
||||
// console.log("Enter key pressed", selectedItem);
|
||||
const item = suggests[selectedItem];
|
||||
if (item?._source?.url) {
|
||||
handleOpenURL(item?._source?.url);
|
||||
@@ -61,7 +61,7 @@ function DropdownList({ selected, suggests }: DropdownListProps) {
|
||||
}
|
||||
|
||||
if (e.key >= "0" && e.key <= "9" && showIndex) {
|
||||
console.log(`number ${e.key}`);
|
||||
// console.log(`number ${e.key}`);
|
||||
const item = suggests[parseInt(e.key, 10)];
|
||||
if (item?._source?.url) {
|
||||
handleOpenURL(item?._source?.url);
|
||||
@@ -72,7 +72,7 @@ function DropdownList({ selected, suggests }: DropdownListProps) {
|
||||
};
|
||||
|
||||
const handleKeyUp = (e: KeyboardEvent) => {
|
||||
console.log("handleKeyUp", e.key);
|
||||
// console.log("handleKeyUp", e.key);
|
||||
if (!suggests.length) return;
|
||||
|
||||
if (!e.metaKey) {
|
||||
|
||||
@@ -29,7 +29,7 @@ export default function Account() {
|
||||
|
||||
const setupAuthListener = async () => {
|
||||
try {
|
||||
if (!auth) {
|
||||
if (!(auth && auth[endpoint_http])) {
|
||||
// Replace the current route with signin
|
||||
// navigate("/signin", { replace: true });
|
||||
}
|
||||
@@ -55,7 +55,7 @@ export default function Account() {
|
||||
|
||||
cleanup();
|
||||
};
|
||||
}, [auth]);
|
||||
}, [JSON.stringify(auth)]);
|
||||
|
||||
async function signIn() {
|
||||
let res: (url: URL) => void;
|
||||
@@ -114,7 +114,7 @@ export default function Account() {
|
||||
user_id,
|
||||
expires,
|
||||
plan: { upgraded: false, last_checked: 0 },
|
||||
});
|
||||
}, endpoint_http);
|
||||
|
||||
getCurrentWindow()
|
||||
.setFocus()
|
||||
@@ -123,7 +123,7 @@ export default function Account() {
|
||||
return navigate("/");
|
||||
} catch (error) {
|
||||
console.error("Sign in failed:", error);
|
||||
await setAuth(undefined);
|
||||
await setAuth(undefined, endpoint_http);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
Sun,
|
||||
Power,
|
||||
Tags,
|
||||
// Trash2,
|
||||
} from "lucide-react";
|
||||
import { isTauri, invoke } from "@tauri-apps/api/core";
|
||||
import {
|
||||
@@ -21,6 +22,8 @@ import { Shortcut } from "./shortcut";
|
||||
import { useShortcutEditor } from "@/hooks/useShortcutEditor";
|
||||
import { ThemeOption } from "./index2";
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
// import { useAuthStore } from "@/stores/authStore";
|
||||
// import { useConnectStore } from "@/stores/connectStore";
|
||||
|
||||
export default function GeneralSettings() {
|
||||
const [launchAtLogin, setLaunchAtLogin] = useState(true);
|
||||
@@ -28,6 +31,10 @@ export default function GeneralSettings() {
|
||||
const showTooltip = useAppStore((state) => state.showTooltip);
|
||||
const setShowTooltip = useAppStore((state) => state.setShowTooltip);
|
||||
|
||||
// const setAuth = useAuthStore((state) => state.setAuth);
|
||||
// const setUserInfo = useAuthStore((state) => state.setUserInfo);
|
||||
// const endpoint = useAppStore((state) => state.endpoint);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchAutoStartStatus = async () => {
|
||||
if (isTauri()) {
|
||||
@@ -83,14 +90,14 @@ export default function GeneralSettings() {
|
||||
getCurrentShortcut();
|
||||
}, []);
|
||||
|
||||
const changeShortcut =(key: Shortcut) => {
|
||||
setShortcut(key)
|
||||
const changeShortcut = (key: Shortcut) => {
|
||||
setShortcut(key);
|
||||
//
|
||||
if (key.length === 0) return;
|
||||
invoke("change_shortcut", { key: key?.join("+") }).catch((err) => {
|
||||
console.error("Failed to save hotkey:", err);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const { isEditing, currentKeys, startEditing, saveShortcut, cancelEditing } =
|
||||
useShortcutEditor(shortcut, changeShortcut);
|
||||
@@ -115,6 +122,15 @@ export default function GeneralSettings() {
|
||||
saveShortcut();
|
||||
};
|
||||
|
||||
// const clearAllCache = useCallback(() => {
|
||||
// setAuth(undefined, endpoint);
|
||||
// setUserInfo({}, endpoint);
|
||||
|
||||
// useConnectStore.persist.clearStorage();
|
||||
|
||||
// useAppStore.persist.clearStorage();
|
||||
// }, [endpoint]);
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
@@ -205,6 +221,23 @@ export default function GeneralSettings() {
|
||||
Manage Favorites
|
||||
</button>
|
||||
</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>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "@headlessui/react";
|
||||
import { Settings, Puzzle, User, Settings2, Info } from "lucide-react";
|
||||
import { Settings, Puzzle, Settings2, Info, Server } from "lucide-react";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
|
||||
import SettingsPanel from "./SettingsPanel";
|
||||
@@ -25,7 +25,7 @@ function SettingsPage() {
|
||||
const tabs = [
|
||||
{ name: "General", icon: Settings },
|
||||
{ name: "Extensions", icon: Puzzle },
|
||||
{ name: "Connect", icon: User },
|
||||
{ name: "Connect", icon: Server },
|
||||
{ name: "Advanced", icon: Settings2 },
|
||||
{ name: "About", icon: Info },
|
||||
];
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useRouteError } from "react-router-dom";
|
||||
|
||||
import errorImg from "./assets/error_page.png";
|
||||
import ApiDetails from "@/components/AppAI/ApiDetails";
|
||||
|
||||
export default function ErrorPage() {
|
||||
const error: any = useRouteError();
|
||||
@@ -9,14 +10,21 @@ export default function ErrorPage() {
|
||||
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="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">
|
||||
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 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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ApiDetails />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -25,7 +33,8 @@ export default function ErrorPage() {
|
||||
<div className="error-content">
|
||||
<h1 className="error-title">Oops!</h1>
|
||||
<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 className="error-details">
|
||||
<i>{error.statusText || error.message}</i>
|
||||
|
||||
@@ -18,9 +18,9 @@ export default function useSettingsWindow() {
|
||||
const url = tab ? `/ui/settings?tab=${tab}` : `/ui/settings`;
|
||||
const options: CreateWindowOptions = {
|
||||
label: "settings",
|
||||
title: "Settings Window",
|
||||
title: "Coco Settings",
|
||||
width: 1000,
|
||||
height: 600,
|
||||
height: 700,
|
||||
alwaysOnTop: false,
|
||||
shadow: true,
|
||||
decorations: true,
|
||||
@@ -29,6 +29,7 @@ export default function useSettingsWindow() {
|
||||
minimizable: false,
|
||||
maximizable: false,
|
||||
dragDropEnabled: true,
|
||||
resizable: false,
|
||||
center: true,
|
||||
url,
|
||||
};
|
||||
@@ -45,6 +46,21 @@ export default function useSettingsWindow() {
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
if (e.metaKey) {
|
||||
switch (e.code) {
|
||||
case "Comma":
|
||||
openSettingsWindow()
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
[openSettingsWindow]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const unlisten = listen("open_settings", (event) => {
|
||||
console.log("open_settings event received:", event);
|
||||
@@ -52,11 +68,13 @@ export default function useSettingsWindow() {
|
||||
|
||||
openSettingsWindow(tab);
|
||||
});
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
|
||||
return () => {
|
||||
unlisten.then((fn) => fn());
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, []);
|
||||
}, [openSettingsWindow, handleKeyDown]);
|
||||
|
||||
return { openSettingsWindow };
|
||||
}
|
||||
|
||||
@@ -62,9 +62,13 @@
|
||||
@apply box-border border-[--border];
|
||||
}
|
||||
|
||||
html{
|
||||
@apply h-full;
|
||||
}
|
||||
|
||||
body,
|
||||
#root {
|
||||
@apply text-gray-900 antialiased;
|
||||
@apply h-full text-gray-900 antialiased;
|
||||
}
|
||||
|
||||
.dark body,
|
||||
|
||||
@@ -10,14 +10,17 @@ import { useAppStore } from "@/stores/appStore";
|
||||
import { useAuthStore } from "@/stores/authStore";
|
||||
import { tauriFetch } from "@/api/tauriFetchClient";
|
||||
import ApiDetails from "@/components/AppAI/ApiDetails";
|
||||
import { useConnectStore } from "@/stores/connectStore";
|
||||
|
||||
export default function DesktopApp() {
|
||||
const initializeListeners = useAppStore((state) => state.initializeListeners);
|
||||
const initializeListeners_auth = useAuthStore(
|
||||
(state) => state.initializeListeners
|
||||
);
|
||||
const setConnectorData = useAppStore((state) => state.setConnectorData);
|
||||
const setDatasourceData = useAppStore((state) => state.setDatasourceData);
|
||||
const setConnectorData = useConnectStore((state) => state.setConnectorData);
|
||||
const setDatasourceData = useConnectStore((state) => state.setDatasourceData);
|
||||
|
||||
const endpoint_http = useAppStore((state) => state.endpoint_http);
|
||||
|
||||
useEffect(() => {
|
||||
initializeListeners();
|
||||
@@ -35,7 +38,7 @@ export default function DesktopApp() {
|
||||
});
|
||||
console.log("connector", response);
|
||||
const data = response.data?.hits?.hits || [];
|
||||
setConnectorData(data);
|
||||
setConnectorData(data, endpoint_http);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch user data:", error);
|
||||
}
|
||||
@@ -49,7 +52,7 @@ export default function DesktopApp() {
|
||||
});
|
||||
console.log("datasource", response);
|
||||
const data = response.data?.hits?.hits || [];
|
||||
setDatasourceData(data);
|
||||
setDatasourceData(data, endpoint_http);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch user data:", error);
|
||||
}
|
||||
@@ -91,7 +94,7 @@ export default function DesktopApp() {
|
||||
return (
|
||||
<div
|
||||
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
|
||||
? "bg-chat_bg_light dark:bg-chat_bg_dark"
|
||||
: "bg-search_bg_light dark:bg-search_bg_dark"
|
||||
|
||||
@@ -15,10 +15,6 @@ export type IAppStore = {
|
||||
endpoint_http: string,
|
||||
endpoint_websocket: string,
|
||||
setEndpoint: (endpoint: AppEndpoint) => void,
|
||||
connector_data: any[],
|
||||
setConnectorData: (connector_data: any[]) => void,
|
||||
datasourceData: any[],
|
||||
setDatasourceData: (datasourceData: any[]) => void,
|
||||
initializeListeners: () => void;
|
||||
};
|
||||
|
||||
@@ -53,18 +49,6 @@ export const useAppStore = create<IAppStore>()(
|
||||
endpoint_websocket
|
||||
});
|
||||
},
|
||||
connector_data: [],
|
||||
setConnectorData: async (connector_data: any[]) => {
|
||||
set({
|
||||
connector_data
|
||||
});
|
||||
},
|
||||
datasourceData: [],
|
||||
setDatasourceData: async (datasourceData: any[]) => {
|
||||
set({
|
||||
datasourceData
|
||||
});
|
||||
},
|
||||
initializeListeners: () => {
|
||||
listen(ENDPOINT_CHANGE_EVENT, (event: any) => {
|
||||
const { endpoint, endpoint_http, endpoint_websocket } = event.payload;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
import { listen, emit } from '@tauri-apps/api/event';
|
||||
import { produce } from 'immer'
|
||||
|
||||
const AUTH_CHANGE_EVENT = 'auth-changed';
|
||||
const USERINFO_CHANGE_EVENT = 'userInfo-changed';
|
||||
@@ -10,19 +11,27 @@ export type Plan = {
|
||||
last_checked: number;
|
||||
};
|
||||
|
||||
export type AuthStore = {
|
||||
export type AuthProp = {
|
||||
token: string;
|
||||
user_id?: string | null;
|
||||
expires?: number;
|
||||
plan?: Plan | null;
|
||||
};
|
||||
|
||||
type AuthMapProp = {
|
||||
[key: string]: AuthProp;
|
||||
};
|
||||
|
||||
type userInfoMapProp = {
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
export type IAuthStore = {
|
||||
[x: string]: any;
|
||||
auth: AuthStore | undefined;
|
||||
userInfo: any;
|
||||
setAuth: (auth: AuthStore | undefined) => void;
|
||||
resetAuth: () => void;
|
||||
auth: AuthMapProp | undefined;
|
||||
userInfo: userInfoMapProp;
|
||||
setAuth: (auth: AuthProp | undefined, key: string) => void;
|
||||
resetAuth: (key: string) => void;
|
||||
initializeListeners: () => void;
|
||||
};
|
||||
|
||||
@@ -31,24 +40,43 @@ export const useAuthStore = create<IAuthStore>()(
|
||||
(set) => ({
|
||||
auth: undefined,
|
||||
userInfo: {},
|
||||
setAuth: async (auth) => {
|
||||
set({ auth })
|
||||
await emit(AUTH_CHANGE_EVENT, {
|
||||
auth
|
||||
});
|
||||
},
|
||||
resetAuth: async () => {
|
||||
set({ auth: undefined })
|
||||
setAuth: async (auth, key) => {
|
||||
set(
|
||||
produce((draft) => {
|
||||
draft.auth[key] = auth
|
||||
})
|
||||
);
|
||||
|
||||
await emit(AUTH_CHANGE_EVENT, {
|
||||
auth: undefined
|
||||
auth: {
|
||||
[key]: auth
|
||||
}
|
||||
});
|
||||
},
|
||||
setUserInfo: async (userInfo: any) => {
|
||||
set({ userInfo })
|
||||
resetAuth: async (key: string) => {
|
||||
set(
|
||||
produce((draft) => {
|
||||
draft.auth[key] = undefined
|
||||
})
|
||||
);
|
||||
|
||||
await emit(AUTH_CHANGE_EVENT, {
|
||||
auth: {
|
||||
[key]: undefined
|
||||
}
|
||||
});
|
||||
},
|
||||
setUserInfo: async (userInfo: any, key: string) => {
|
||||
set(
|
||||
produce((draft) => {
|
||||
draft.userInfo[key] = userInfo
|
||||
})
|
||||
);
|
||||
|
||||
await emit(USERINFO_CHANGE_EVENT, {
|
||||
userInfo
|
||||
userInfo: {
|
||||
[key]: userInfo
|
||||
}
|
||||
});
|
||||
},
|
||||
initializeListeners: () => {
|
||||
|
||||
124
src/stores/connectStore.ts
Normal file
124
src/stores/connectStore.ts
Normal 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,
|
||||
}),
|
||||
}
|
||||
)
|
||||
);
|
||||
Reference in New Issue
Block a user