feat: add connect services (#111)

* feat: add connect services

* chore: adjust auth and userInfo

* chore: add responsiveness to auth and userInfo

* chore: add dark css

* chore: handle /, to join the baseURL and url.

* chore: use http:// or https:// rather http

* chore: handle /, to join the baseURL and url.

* chore: active current service

* chore: connect

* chore: data source & connect data

* feat: add handleKeyDown open settings

* chore: settings name
This commit is contained in:
BiggerRain
2025-01-25 16:20:49 +08:00
committed by GitHub
parent 6a272bda50
commit fbfb980fec
30 changed files with 947 additions and 333 deletions

View File

@@ -27,7 +27,7 @@
"dotenv": "^16.4.7",
"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
View File

@@ -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: {}

View File

@@ -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 = {

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View File

@@ -16,6 +16,7 @@ import source_default_img from "@/assets/images/source_default.png";
import source_default_dark_img from "@/assets/images/source_default_dark.png";
import 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;

View File

@@ -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;

View File

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

View File

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

View File

@@ -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>

View File

@@ -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>
);

View File

@@ -1,4 +1,10 @@
import { Link2, Trash2 } from "lucide-react";
import { Link2 } from "lucide-react";
import { useAppStore } from "@/stores/appStore";
import source_default_img from "@/assets/images/source_default.png";
import source_default_dark_img from "@/assets/images/source_default_dark.png";
import { useTheme } from "@/contexts/ThemeContext";
import { useConnectStore } from "@/stores/connectStore";
interface Account {
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>
);
}

View File

@@ -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>

View File

@@ -1,24 +1,160 @@
import { Cloud, Plus } from "lucide-react";
import { useState, useEffect } from "react";
import { Plus } from "lucide-react";
import cocoLogoImg from "@/assets/app-icon.png";
import { tauriFetch } from "@/api/tauriFetchClient";
import { useConnectStore } from "@/stores/connectStore";
import { useAppStore } from "@/stores/appStore";
interface SidebarProps {
addService: () => void;
}
type StringBooleanMap = {
[key: string]: boolean;
};
export function Sidebar({ addService }: SidebarProps) {
const defaultService = useConnectStore((state) => state.defaultService);
const currentService = useConnectStore((state) => state.currentService);
const otherServices = useConnectStore((state) => state.otherServices);
const setCurrentService = useConnectStore((state) => state.setCurrentService);
const setEndpoint = useAppStore((state) => state.setEndpoint);
const [defaultHealth, setDefaultHealth] = useState(false);
const [otherHealth, setOtherHealth] = useState<StringBooleanMap>({});
const addServiceClick = () => {
addService();
};
useEffect(() => {
getDefaultHealth();
}, []);
useEffect(() => {
getOtherHealth(currentService);
setEndpoint(currentService.endpoint);
}, [currentService.endpoint]);
const getDefaultHealth = () => {
let baseURL = defaultService.endpoint
if (baseURL.endsWith("/")) {
baseURL = baseURL.slice(0, -1);
}
tauriFetch({
url: `${baseURL}/health`,
method: "GET",
})
.then((res) => {
// "services": {
// "system_cluster": "yellow"
// },
// "status": "yellow"
setDefaultHealth(res.data?.status !== "red");
})
.catch((err) => {
console.error(err);
});
};
const getOtherHealth = (item: any) => {
if (!item.endpoint) return;
//
let baseURL = item.endpoint
if (baseURL.endsWith("/")) {
baseURL = baseURL.slice(0, -1);
}
tauriFetch({
url: `${baseURL}/health`,
method: "GET",
})
.then((res) => {
let obj = {
...otherHealth,
[item.endpoint]: res.data?.status !== "red",
};
setOtherHealth(obj);
})
.catch((err) => {
console.error(err);
});
};
export function Sidebar() {
return (
<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>

View File

@@ -1,13 +1,14 @@
import { User, Edit, LogOut } from "lucide-react";
import { User, LogOut } from "lucide-react";
import { useAuthStore } from "@/stores/authStore";
import { 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>
);
}

View File

@@ -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));

View File

@@ -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,

View File

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

View File

@@ -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;

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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;
}
}

View File

@@ -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()) {
@@ -84,13 +91,13 @@ export default function GeneralSettings() {
}, []);
const changeShortcut = (key: Shortcut) => {
setShortcut(key)
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>

View File

@@ -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 },
];

View File

@@ -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>

View File

@@ -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 };
}

View File

@@ -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,

View File

@@ -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"

View File

@@ -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;

View File

@@ -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
View File

@@ -0,0 +1,124 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { produce } from 'immer'
type keyArrayObject = {
[key: string]: any[];
};
export type IConnectStore = {
defaultService: any;
setDefaultService: (service: any) => void;
otherServices: any[];
addOtherServices: (service: any) => void;
deleteOtherService: (service: any) => void;
currentService: any;
setCurrentService: (service: any) => void;
connector_data: keyArrayObject,
setConnectorData: (connector_data: any[], key: string) => void,
datasourceData: keyArrayObject,
setDatasourceData: (datasourceData: any[], key: string) => void,
};
export const useConnectStore = create<IConnectStore>()(
persist(
(set) => ({
defaultService: {
"name": "Coco Cloud",
"endpoint": "https://coco.infini.cloud/",
"provider": {
"name": "INFINI Labs",
"icon": "https://coco.infini.cloud/icon.png",
"website": "http://infinilabs.com",
"eula": "http://infinilabs.com/eula.txt",
"privacy_policy": "http://infinilabs.com/privacy_policy.txt",
"banner": "https://coco.infini.cloud/banner.jpg",
"description": "Coco AI Server - Search, Connect, Collaborate, AI-powered enterprise search, all in one space."
},
"version": {
"number": "1.0.0_SNAPSHOT"
},
"updated": "2025-01-24T12:12:17.326286927+08:00",
"public": false,
"auth_provider": {
"sso": {
"url": "https://coco.infini.cloud/sso/login/"
}
}
},
setDefaultService: (defaultService: any) => set(
produce((draft) => {
draft.defaultService = defaultService
})
),
otherServices: [],
addOtherServices: (otherService: any) => {
set(produce((draft) => {
draft.otherServices.push(otherService);
}))
},
deleteOtherService: (service: any) => {
set(produce((draft) => {
draft.otherServices = draft.otherServices.filter(
(item: any) => item.endpoint !== service.endpoint
);
draft.currentService = draft.defaultService;
}))
},
currentService: {
"name": "Coco Cloud",
"endpoint": "https://coco.infini.cloud/",
"provider": {
"name": "INFINI Labs",
"icon": "https://coco.infini.cloud/icon.png",
"website": "http://infinilabs.com",
"eula": "http://infinilabs.com/eula.txt",
"privacy_policy": "http://infinilabs.com/privacy_policy.txt",
"banner": "https://coco.infini.cloud/banner.jpg",
"description": "Coco AI Server - Search, Connect, Collaborate, AI-powered enterprise search, all in one space."
},
"version": {
"number": "1.0.0_SNAPSHOT"
},
"updated": "2025-01-24T12:12:17.326286927+08:00",
"public": false,
"auth_provider": {
"sso": {
"url": "https://coco.infini.cloud/sso/login/"
}
}
},
setCurrentService: (currentService: any) => {
set(produce((draft) => {
draft.currentService = currentService;
}))
},
connector_data: {},
setConnectorData: async (connector_data: any[], key: string) => {
set(
produce((draft) => {
draft.connector_data[key] = connector_data
})
);
},
datasourceData: {},
setDatasourceData: async (datasourceData: any[], key: string) => {
set(
produce((draft) => {
draft.datasourceData[key] = datasourceData
})
);
},
}),
{
name: "connect-store",
partialize: (state) => ({
defaultService: state.defaultService,
otherServices: state.otherServices,
currentService: state.currentService,
connector_data: state.connector_data,
datasourceData: state.datasourceData,
}),
}
)
);