mirror of
https://github.com/infinilabs/coco-app.git
synced 2025-12-16 11:37:47 +01:00
feat: add locales switch (#144)
* feat: add locales * feat: add locales * feat: add listen language
This commit is contained in:
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@@ -15,6 +15,7 @@
|
||||
"inputbox",
|
||||
"katex",
|
||||
"khtml",
|
||||
"languagedetector",
|
||||
"localstorage",
|
||||
"lucide",
|
||||
"maximizable",
|
||||
|
||||
@@ -19,14 +19,15 @@
|
||||
"@tauri-apps/plugin-http": "~2.0.1",
|
||||
"@tauri-apps/plugin-os": "^2.2.0",
|
||||
"@tauri-apps/plugin-shell": ">=2.0.0",
|
||||
"@tauri-apps/plugin-updater": "^2.3.0",
|
||||
"@tauri-apps/plugin-websocket": "~2",
|
||||
"@tauri-apps/plugin-window": "2.0.0-alpha.1",
|
||||
"@tauri-apps/plugin-updater": "^2.3.0",
|
||||
"ahooks": "^3.8.4",
|
||||
"axios": "^1.7.7",
|
||||
"clsx": "^2.1.1",
|
||||
"dotenv": "^16.4.7",
|
||||
"i18next": "^23.16.2",
|
||||
"i18next-browser-languagedetector": "^8.0.3",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.461.0",
|
||||
"mermaid": "^11.4.0",
|
||||
|
||||
23
pnpm-lock.yaml
generated
23
pnpm-lock.yaml
generated
@@ -59,6 +59,9 @@ importers:
|
||||
i18next:
|
||||
specifier: ^23.16.2
|
||||
version: 23.16.2
|
||||
i18next-browser-languagedetector:
|
||||
specifier: ^8.0.3
|
||||
version: 8.0.3
|
||||
lodash:
|
||||
specifier: ^4.17.21
|
||||
version: 4.17.21
|
||||
@@ -554,46 +557,55 @@ packages:
|
||||
resolution: {integrity: sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.24.0':
|
||||
resolution: {integrity: sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.24.0':
|
||||
resolution: {integrity: sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.24.0':
|
||||
resolution: {integrity: sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-powerpc64le-gnu@4.24.0':
|
||||
resolution: {integrity: sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.24.0':
|
||||
resolution: {integrity: sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.24.0':
|
||||
resolution: {integrity: sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.24.0':
|
||||
resolution: {integrity: sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.24.0':
|
||||
resolution: {integrity: sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-win32-arm64-msvc@4.24.0':
|
||||
resolution: {integrity: sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==}
|
||||
@@ -652,24 +664,28 @@ packages:
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tauri-apps/cli-linux-arm64-musl@2.2.7':
|
||||
resolution: {integrity: sha512-+8HZ+txff/Y3YjAh80XcLXcX8kpGXVdr1P8AfjLHxHdS6QD4Md+acSxGTTNbplmHuBaSHJvuTvZf9tU1eDCTDg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tauri-apps/cli-linux-x64-gnu@2.2.7':
|
||||
resolution: {integrity: sha512-ahlSnuCnUntblp9dG7/w5ZWZOdzRFi3zl0oScgt7GF4KNAOEa7duADsxPA4/FT2hLRa0SvpqtD4IYFvCxoVv3Q==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tauri-apps/cli-linux-x64-musl@2.2.7':
|
||||
resolution: {integrity: sha512-+qKAWnJRSX+pjjRbKAQgTdFY8ecdcu8UdJ69i7wn3ZcRn2nMMzOO2LOMOTQV42B7/Q64D1pIpmZj9yblTMvadA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tauri-apps/cli-win32-arm64-msvc@2.2.7':
|
||||
resolution: {integrity: sha512-aa86nRnrwT04u9D9fhf5JVssuAZlUCCc8AjqQjqODQjMd4BMA2+d4K9qBMpEG/1kVh95vZaNsLogjEaqSTTw4A==}
|
||||
@@ -1426,6 +1442,9 @@ packages:
|
||||
html-url-attributes@3.0.1:
|
||||
resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==}
|
||||
|
||||
i18next-browser-languagedetector@8.0.3:
|
||||
resolution: {integrity: sha512-beOOLArattPBc2YZG5IXGJytdYFgUR7cS8Wd6HT4IczIoWKgmTspOQ2yasaGklelVo5seLPmnEKvLHR+E/MdWQ==}
|
||||
|
||||
i18next@23.16.2:
|
||||
resolution: {integrity: sha512-dFyxwLXxEQK32f6tITBMaRht25mZPJhQ0WbC0p3bO2mWBal9lABTMqSka5k+GLSRWLzeJBKDpH7BeIA9TZI7Jg==}
|
||||
|
||||
@@ -3607,6 +3626,10 @@ snapshots:
|
||||
|
||||
html-url-attributes@3.0.1: {}
|
||||
|
||||
i18next-browser-languagedetector@8.0.3:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.25.9
|
||||
|
||||
i18next@23.16.2:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.25.9
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface AutoResizeTextareaProps {
|
||||
input: string;
|
||||
@@ -11,6 +12,7 @@ const AutoResizeTextarea: React.FC<AutoResizeTextareaProps> = ({
|
||||
setInput,
|
||||
handleKeyDown,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -25,7 +27,7 @@ const AutoResizeTextarea: React.FC<AutoResizeTextareaProps> = ({
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
className="text-xs flex-1 outline-none min-w-[200px] text-[#333] dark:text-[#d8d8d8] placeholder-text-xs placeholder-[#999] dark:placeholder-gray-500 bg-transparent"
|
||||
placeholder="Ask whatever you want ..."
|
||||
placeholder={t('search.textarea.placeholder')}
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
|
||||
@@ -1,318 +1,342 @@
|
||||
import {forwardRef, memo, useCallback, useEffect, useImperativeHandle, useRef, useState,} from "react";
|
||||
import {MessageSquarePlus, PanelLeft} from "lucide-react";
|
||||
import {isTauri} from "@tauri-apps/api/core";
|
||||
|
||||
import {ChatMessage} from "./ChatMessage";
|
||||
import type {Chat, Message} from "./types";
|
||||
import {tauriFetch} from "../../api/tauriFetchClient";
|
||||
import {useWebSocket} from "../../hooks/useWebSocket";
|
||||
import {useChatStore} from "../../stores/chatStore";
|
||||
import {useWindows} from "../../hooks/useWindows";
|
||||
import {clientEnv} from "@/utils/env";
|
||||
import {
|
||||
forwardRef,
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { MessageSquarePlus, PanelLeft } from "lucide-react";
|
||||
import { isTauri } from "@tauri-apps/api/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { ChatMessage } from "./ChatMessage";
|
||||
import type { Chat, Message } from "./types";
|
||||
import { tauriFetch } from "@/api/tauriFetchClient";
|
||||
import { useWebSocket } from "@/hooks/useWebSocket";
|
||||
import { useChatStore } from "@/stores/chatStore";
|
||||
import { useWindows } from "@/hooks/useWindows";
|
||||
import { clientEnv } from "@/utils/env";
|
||||
// import { useAppStore } from '@/stores/appStore';
|
||||
|
||||
interface ChatAIProps {
|
||||
isTransitioned: boolean;
|
||||
changeInput: (val: string) => void;
|
||||
isSearchActive?: boolean;
|
||||
isDeepThinkActive?: boolean;
|
||||
isTransitioned: boolean;
|
||||
changeInput: (val: string) => void;
|
||||
isSearchActive?: boolean;
|
||||
isDeepThinkActive?: boolean;
|
||||
}
|
||||
|
||||
export interface ChatAIRef {
|
||||
init: (value: string) => void;
|
||||
cancelChat: () => void;
|
||||
connected: boolean;
|
||||
reconnect: () => void;
|
||||
init: (value: string) => void;
|
||||
cancelChat: () => void;
|
||||
connected: boolean;
|
||||
reconnect: () => void;
|
||||
}
|
||||
|
||||
const ChatAI = memo(forwardRef<ChatAIRef, ChatAIProps>(
|
||||
({isTransitioned, changeInput, isSearchActive, isDeepThinkActive}, ref) => {
|
||||
useImperativeHandle(ref, () => ({
|
||||
init: init,
|
||||
cancelChat: cancelChat,
|
||||
connected: connected,
|
||||
reconnect: reconnect
|
||||
}));
|
||||
const ChatAI = memo(
|
||||
forwardRef<ChatAIRef, ChatAIProps>(
|
||||
(
|
||||
{ isTransitioned, changeInput, isSearchActive, isDeepThinkActive },
|
||||
ref
|
||||
) => {
|
||||
const { t } = useTranslation();
|
||||
useImperativeHandle(ref, () => ({
|
||||
init: init,
|
||||
cancelChat: cancelChat,
|
||||
connected: connected,
|
||||
reconnect: reconnect,
|
||||
}));
|
||||
|
||||
// const appStore = useAppStore();
|
||||
// const appStore = useAppStore();
|
||||
|
||||
const {createWin} = useWindows();
|
||||
const { createWin } = useWindows();
|
||||
|
||||
const {curChatEnd, setCurChatEnd, setConnected} = useChatStore();
|
||||
const { curChatEnd, setCurChatEnd, setConnected } = useChatStore();
|
||||
|
||||
const [activeChat, setActiveChat] = useState<Chat>();
|
||||
const [isTyping, setIsTyping] = useState(false);
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const [activeChat, setActiveChat] = useState<Chat>();
|
||||
const [isTyping, setIsTyping] = useState(false);
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [websocketId, setWebsocketId] = useState("");
|
||||
const [curMessage, setCurMessage] = useState("");
|
||||
const [curId, setCurId] = useState("");
|
||||
const [websocketId, setWebsocketId] = useState("");
|
||||
const [curMessage, setCurMessage] = useState("");
|
||||
const [curId, setCurId] = useState("");
|
||||
|
||||
const curChatEndRef = useRef(curChatEnd);
|
||||
curChatEndRef.current = curChatEnd;
|
||||
const curChatEndRef = useRef(curChatEnd);
|
||||
curChatEndRef.current = curChatEnd;
|
||||
|
||||
const curIdRef = useRef(curId);
|
||||
curIdRef.current = curId;
|
||||
const curIdRef = useRef(curId);
|
||||
curIdRef.current = curId;
|
||||
|
||||
const handleMessageChunk = useCallback((chunk: string) => {
|
||||
setCurMessage(prev => prev + chunk);
|
||||
}, []);
|
||||
const handleMessageChunk = useCallback((chunk: string) => {
|
||||
setCurMessage((prev) => prev + chunk);
|
||||
}, []);
|
||||
|
||||
// console.log("chat useWebSocket", clientEnv.COCO_WEBSOCKET_URL)
|
||||
const {messages, setMessages, connected, reconnect} = useWebSocket(
|
||||
clientEnv.COCO_WEBSOCKET_URL,
|
||||
(msg) => {
|
||||
// console.log("msg", msg);
|
||||
// console.log("chat useWebSocket", clientEnv.COCO_WEBSOCKET_URL)
|
||||
const { messages, setMessages, connected, reconnect } = useWebSocket(
|
||||
clientEnv.COCO_WEBSOCKET_URL,
|
||||
(msg) => {
|
||||
// console.log("msg", msg);
|
||||
|
||||
if (msg.includes("websocket-session-id")) {
|
||||
const array = msg.split(" ");
|
||||
setWebsocketId(array[2]);
|
||||
}
|
||||
if (msg.includes("websocket-session-id")) {
|
||||
const array = msg.split(" ");
|
||||
setWebsocketId(array[2]);
|
||||
}
|
||||
|
||||
if (msg.includes("PRIVATE")) {
|
||||
if (
|
||||
msg.includes("assistant finished output") ||
|
||||
curChatEndRef.current
|
||||
) {
|
||||
setCurChatEnd(true);
|
||||
} else {
|
||||
const cleanedData = msg.replace(/^PRIVATE /, "");
|
||||
try {
|
||||
const chunkData = JSON.parse(cleanedData);
|
||||
if (chunkData.reply_to_message === curIdRef.current) {
|
||||
handleMessageChunk(chunkData.message_chunk)
|
||||
return chunkData.message_chunk;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("JSON Parse error:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setConnected(connected)
|
||||
}, [connected])
|
||||
|
||||
const simulateAssistantResponse = useCallback(() => {
|
||||
if (messages.length === 0 || !activeChat?._id) return;
|
||||
|
||||
console.log("messages", messages);
|
||||
|
||||
const assistantMessage: Message = {
|
||||
_id: activeChat._id,
|
||||
_source: {
|
||||
type: "assistant",
|
||||
message: messages,
|
||||
},
|
||||
};
|
||||
|
||||
const updatedChat = {
|
||||
...activeChat,
|
||||
messages: [...(activeChat.messages || []), assistantMessage],
|
||||
};
|
||||
setMessages("");
|
||||
setCurMessage("");
|
||||
console.log("updatedChat", updatedChat);
|
||||
setActiveChat(updatedChat);
|
||||
|
||||
const timer = setTimeout(() => setIsTyping(false), 1000);
|
||||
return () => clearTimeout(timer);
|
||||
}, [activeChat?._id]);
|
||||
|
||||
// websocket
|
||||
useEffect(() => {
|
||||
if (curChatEnd) {
|
||||
simulateAssistantResponse();
|
||||
}
|
||||
}, [messages, curChatEnd]);
|
||||
|
||||
const scrollToBottom = () => {
|
||||
messagesEndRef.current?.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "end",
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
scrollToBottom();
|
||||
}, [activeChat?.messages, isTyping, curMessage]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
chatClose();
|
||||
setMessages("");
|
||||
setCurMessage("");
|
||||
setActiveChat(undefined);
|
||||
setIsTyping(false);
|
||||
setCurChatEnd(true);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const createNewChat = useCallback(async (value: string = "") => {
|
||||
chatClose();
|
||||
try {
|
||||
const response = await tauriFetch({
|
||||
url: "/chat/_new",
|
||||
method: "POST",
|
||||
});
|
||||
console.log("_new", response);
|
||||
const newChat: Chat = response.data;
|
||||
|
||||
setActiveChat(newChat);
|
||||
handleSendMessage(value, newChat);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch user data:", error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const init = (value: string) => {
|
||||
if (!curChatEnd) return;
|
||||
if (!activeChat?._id) {
|
||||
createNewChat(value);
|
||||
if (msg.includes("PRIVATE")) {
|
||||
if (
|
||||
msg.includes("assistant finished output") ||
|
||||
curChatEndRef.current
|
||||
) {
|
||||
setCurChatEnd(true);
|
||||
} else {
|
||||
handleSendMessage(value);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSendMessage = async (content: string, newChat?: Chat) => {
|
||||
newChat = newChat || activeChat;
|
||||
if (!newChat?._id || !content) return;
|
||||
try {
|
||||
const response = await tauriFetch({
|
||||
url: `/chat/${newChat?._id}/_send?search=${isSearchActive}&deep_thinking=${isDeepThinkActive}`,
|
||||
method: "POST",
|
||||
headers: {
|
||||
"WEBSOCKET-SESSION-ID": websocketId,
|
||||
},
|
||||
body: JSON.stringify({message: content}),
|
||||
});
|
||||
console.log("_send", response, websocketId);
|
||||
setCurId(response.data[0]?._id);
|
||||
const updatedChat: Chat = {
|
||||
...newChat,
|
||||
messages: [...(newChat?.messages || []), ...(response.data || [])],
|
||||
};
|
||||
changeInput("");
|
||||
console.log("updatedChat2", updatedChat);
|
||||
setActiveChat(updatedChat);
|
||||
setIsTyping(true);
|
||||
setCurChatEnd(false);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch user data:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const chatClose = async () => {
|
||||
if (!activeChat?._id) return;
|
||||
try {
|
||||
const response = await tauriFetch({
|
||||
url: `/chat/${activeChat._id}/_close`,
|
||||
method: "POST",
|
||||
});
|
||||
console.log("_close", response);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch user data:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const cancelChat = async () => {
|
||||
setCurChatEnd(true);
|
||||
setIsTyping(false);
|
||||
if (!activeChat?._id) return;
|
||||
try {
|
||||
const response = await tauriFetch({
|
||||
url: `/chat/${activeChat._id}/_cancel`,
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
console.log("_cancel", response);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch user data:", error);
|
||||
}
|
||||
};
|
||||
|
||||
async function openChatAI() {
|
||||
if (isTauri()) {
|
||||
createWin && createWin({
|
||||
label: "chat",
|
||||
title: "Coco Chat",
|
||||
dragDropEnabled: true,
|
||||
center: true,
|
||||
width: 1000,
|
||||
height: 800,
|
||||
alwaysOnTop: false,
|
||||
skipTaskbar: false,
|
||||
decorations: true,
|
||||
closable: true,
|
||||
url: "/ui/chat",
|
||||
});
|
||||
const cleanedData = msg.replace(/^PRIVATE /, "");
|
||||
try {
|
||||
const chunkData = JSON.parse(cleanedData);
|
||||
if (chunkData.reply_to_message === curIdRef.current) {
|
||||
handleMessageChunk(chunkData.message_chunk);
|
||||
return chunkData.message_chunk;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("JSON Parse error:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (!isTransitioned) return null;
|
||||
useEffect(() => {
|
||||
setConnected(connected);
|
||||
}, [connected]);
|
||||
|
||||
return (
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className={`h-[500px] flex flex-col rounded-xl overflow-hidden`}
|
||||
const simulateAssistantResponse = useCallback(() => {
|
||||
if (messages.length === 0 || !activeChat?._id) return;
|
||||
|
||||
console.log("messages", messages);
|
||||
|
||||
const assistantMessage: Message = {
|
||||
_id: activeChat._id,
|
||||
_source: {
|
||||
type: "assistant",
|
||||
message: messages,
|
||||
},
|
||||
};
|
||||
|
||||
const updatedChat = {
|
||||
...activeChat,
|
||||
messages: [...(activeChat.messages || []), assistantMessage],
|
||||
};
|
||||
setMessages("");
|
||||
setCurMessage("");
|
||||
console.log("updatedChat", updatedChat);
|
||||
setActiveChat(updatedChat);
|
||||
|
||||
const timer = setTimeout(() => setIsTyping(false), 1000);
|
||||
return () => clearTimeout(timer);
|
||||
}, [activeChat?._id]);
|
||||
|
||||
// websocket
|
||||
useEffect(() => {
|
||||
if (curChatEnd) {
|
||||
simulateAssistantResponse();
|
||||
}
|
||||
}, [messages, curChatEnd]);
|
||||
|
||||
const scrollToBottom = () => {
|
||||
messagesEndRef.current?.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "end",
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
scrollToBottom();
|
||||
}, [activeChat?.messages, isTyping, curMessage]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
chatClose();
|
||||
setMessages("");
|
||||
setCurMessage("");
|
||||
setActiveChat(undefined);
|
||||
setIsTyping(false);
|
||||
setCurChatEnd(true);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const createNewChat = useCallback(async (value: string = "") => {
|
||||
chatClose();
|
||||
try {
|
||||
const response = await tauriFetch({
|
||||
url: "/chat/_new",
|
||||
method: "POST",
|
||||
});
|
||||
console.log("_new", response);
|
||||
const newChat: Chat = response.data;
|
||||
|
||||
setActiveChat(newChat);
|
||||
handleSendMessage(value, newChat);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch user data:", error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const init = (value: string) => {
|
||||
if (!curChatEnd) return;
|
||||
if (!activeChat?._id) {
|
||||
createNewChat(value);
|
||||
} else {
|
||||
handleSendMessage(value);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSendMessage = async (content: string, newChat?: Chat) => {
|
||||
newChat = newChat || activeChat;
|
||||
if (!newChat?._id || !content) return;
|
||||
try {
|
||||
const response = await tauriFetch({
|
||||
url: `/chat/${newChat?._id}/_send?search=${isSearchActive}&deep_thinking=${isDeepThinkActive}`,
|
||||
method: "POST",
|
||||
headers: {
|
||||
"WEBSOCKET-SESSION-ID": websocketId,
|
||||
},
|
||||
body: JSON.stringify({ message: content }),
|
||||
});
|
||||
console.log("_send", response, websocketId);
|
||||
setCurId(response.data[0]?._id);
|
||||
const updatedChat: Chat = {
|
||||
...newChat,
|
||||
messages: [...(newChat?.messages || []), ...(response.data || [])],
|
||||
};
|
||||
changeInput("");
|
||||
console.log("updatedChat2", updatedChat);
|
||||
setActiveChat(updatedChat);
|
||||
setIsTyping(true);
|
||||
setCurChatEnd(false);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch user data:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const chatClose = async () => {
|
||||
if (!activeChat?._id) return;
|
||||
try {
|
||||
const response = await tauriFetch({
|
||||
url: `/chat/${activeChat._id}/_close`,
|
||||
method: "POST",
|
||||
});
|
||||
console.log("_close", response);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch user data:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const cancelChat = async () => {
|
||||
setCurChatEnd(true);
|
||||
setIsTyping(false);
|
||||
if (!activeChat?._id) return;
|
||||
try {
|
||||
const response = await tauriFetch({
|
||||
url: `/chat/${activeChat._id}/_cancel`,
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
console.log("_cancel", response);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch user data:", error);
|
||||
}
|
||||
};
|
||||
|
||||
async function openChatAI() {
|
||||
if (isTauri()) {
|
||||
createWin &&
|
||||
createWin({
|
||||
label: "chat",
|
||||
title: "Coco Chat",
|
||||
dragDropEnabled: true,
|
||||
center: true,
|
||||
width: 1000,
|
||||
height: 800,
|
||||
alwaysOnTop: false,
|
||||
skipTaskbar: false,
|
||||
decorations: true,
|
||||
closable: true,
|
||||
url: "/ui/chat",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!isTransitioned) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className={`h-[500px] flex flex-col rounded-xl overflow-hidden`}
|
||||
>
|
||||
<header
|
||||
data-tauri-drag-region
|
||||
className={`flex items-center justify-between py-2 px-1`}
|
||||
>
|
||||
<button
|
||||
onClick={() => openChatAI()}
|
||||
className={`p-2 rounded-lg transition-colors text-[#333] dark:text-[#d8d8d8]`}
|
||||
>
|
||||
<header
|
||||
data-tauri-drag-region
|
||||
className={`flex items-center justify-between py-2 px-1`}
|
||||
>
|
||||
<button
|
||||
onClick={() => openChatAI()}
|
||||
className={`p-2 rounded-lg transition-colors text-[#333] dark:text-[#d8d8d8]`}
|
||||
>
|
||||
<PanelLeft className="h-4 w-4"/>
|
||||
</button>
|
||||
<PanelLeft className="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
createNewChat();
|
||||
}}
|
||||
className={`p-2 rounded-lg transition-colors text-[#333] dark:text-[#d8d8d8]`}
|
||||
>
|
||||
<MessageSquarePlus className="h-4 w-4" />
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
createNewChat();
|
||||
}}
|
||||
className={`p-2 rounded-lg transition-colors text-[#333] dark:text-[#d8d8d8]`}
|
||||
>
|
||||
<MessageSquarePlus className="h-4 w-4"/>
|
||||
</button>
|
||||
</header>
|
||||
|
||||
{/* Chat messages */}
|
||||
{/* Chat messages */}
|
||||
<div className="w-full overflow-x-hidden overflow-y-auto border-t border-[rgba(0,0,0,0.1)] dark:border-[rgba(255,255,255,0.15)] custom-scrollbar">
|
||||
{activeChat?.messages?.map((message, index) => (
|
||||
<ChatMessage
|
||||
key={message._id + index}
|
||||
message={message}
|
||||
isTyping={
|
||||
isTyping &&
|
||||
index === (activeChat.messages?.length || 0) - 1 &&
|
||||
message._source?.type === "assistant"
|
||||
}
|
||||
/>
|
||||
))}
|
||||
{!curChatEnd && activeChat?._id ? (
|
||||
<ChatMessage
|
||||
key={"last"}
|
||||
message={{
|
||||
_id: activeChat?._id,
|
||||
_source: {
|
||||
type: "assistant",
|
||||
message: curMessage,
|
||||
},
|
||||
}}
|
||||
isTyping={!curChatEnd}
|
||||
/>
|
||||
) : null}
|
||||
{!connected && (
|
||||
<div className="absolute top-0 right-0 bottom-0 left-0 px-2 py-4 bg-red-500/10 rounded-md font-normal text-xs text-gray-400 flex items-center gap-4">
|
||||
{t("assistant.chat.connectionError")}
|
||||
<div
|
||||
className="w-full overflow-x-hidden overflow-y-auto border-t border-[rgba(0,0,0,0.1)] dark:border-[rgba(255,255,255,0.15)] custom-scrollbar">
|
||||
{activeChat?.messages?.map((message, index) => (
|
||||
<ChatMessage
|
||||
key={message._id + index}
|
||||
message={message}
|
||||
isTyping={
|
||||
isTyping &&
|
||||
index === (activeChat.messages?.length || 0) - 1 &&
|
||||
message._source?.type === "assistant"
|
||||
}
|
||||
/>
|
||||
))}
|
||||
{!curChatEnd && activeChat?._id ? (
|
||||
<ChatMessage
|
||||
key={"last"}
|
||||
message={{
|
||||
_id: activeChat?._id,
|
||||
_source: {
|
||||
type: "assistant",
|
||||
message: curMessage,
|
||||
},
|
||||
}}
|
||||
isTyping={!curChatEnd}
|
||||
/>
|
||||
) : null}
|
||||
<div ref={messagesEndRef}/>
|
||||
className="w-[96px] h-[24px] bg-[#0061FF] rounded-[12px] font-normal text-xs text-white flex items-center justify-center cursor-pointer"
|
||||
onClick={reconnect}
|
||||
>
|
||||
{t("assistant.chat.reconnect")}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
</div>
|
||||
)}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
));
|
||||
)
|
||||
);
|
||||
|
||||
export default ChatAI;
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
useRef,
|
||||
useEffect,
|
||||
} from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import AutoResizeTextarea from "./AutoResizeTextarea";
|
||||
import StopIcon from "@/icons/Stop";
|
||||
@@ -31,6 +32,7 @@ export function ChatInput({
|
||||
isDeepThinkActive,
|
||||
setIsDeepThinkActive,
|
||||
}: ChatInputProps) {
|
||||
const { t } = useTranslation();
|
||||
const [input, setInput] = useState("");
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
@@ -120,7 +122,9 @@ export function ChatInput({
|
||||
<button
|
||||
type="button"
|
||||
className={`h-5 px-2 inline-flex items-center border rounded-[10px] transition-colors relative ${
|
||||
isDeepThinkActive ? "bg-[rgba(0,114,255,0.3)] border-[rgba(0,114,255,0.3)]" : "border-[#262727]"
|
||||
isDeepThinkActive
|
||||
? "bg-[rgba(0,114,255,0.3)] border-[rgba(0,114,255,0.3)]"
|
||||
: "border-[#262727]"
|
||||
}`}
|
||||
onClick={DeepThinkClick}
|
||||
>
|
||||
@@ -131,14 +135,20 @@ export function ChatInput({
|
||||
: "text-[#333] dark:text-white"
|
||||
}`}
|
||||
/>
|
||||
<span className={isDeepThinkActive ? "text-[#0072FF]" : "dark:text-white"}>
|
||||
Deep Think
|
||||
<span
|
||||
className={
|
||||
isDeepThinkActive ? "text-[#0072FF]" : "dark:text-white"
|
||||
}
|
||||
>
|
||||
{t("assistant.input.deepThink")}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`h-5 px-2 inline-flex items-center border rounded-[10px] transition-colors relative ${
|
||||
isSearchActive ? "bg-[rgba(0,114,255,0.3)] border-[rgba(0,114,255,0.3)]" : "border-[#262727]"
|
||||
isSearchActive
|
||||
? "bg-[rgba(0,114,255,0.3)] border-[rgba(0,114,255,0.3)]"
|
||||
: "border-[#262727]"
|
||||
}`}
|
||||
onClick={SearchClick}
|
||||
>
|
||||
@@ -149,8 +159,12 @@ export function ChatInput({
|
||||
: "text-[#333] dark:text-white"
|
||||
}`}
|
||||
/>
|
||||
<span className={isSearchActive ? "text-[#0072FF]" : "dark:text-white"}>
|
||||
Search
|
||||
<span
|
||||
className={
|
||||
isSearchActive ? "text-[#0072FF]" : "dark:text-white"
|
||||
}
|
||||
>
|
||||
{t("assistant.input.search")}
|
||||
</span>
|
||||
</button>
|
||||
{/* <button
|
||||
|
||||
@@ -6,6 +6,7 @@ import Markdown from "./Markdown";
|
||||
import { formatThinkingMessage } from "@/utils/index";
|
||||
import logoImg from "@/assets/icon.svg";
|
||||
import { SourceResult } from "./SourceResult";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface ChatMessageProps {
|
||||
message: Message;
|
||||
@@ -13,6 +14,7 @@ interface ChatMessageProps {
|
||||
}
|
||||
|
||||
export function ChatMessage({ message, isTyping }: ChatMessageProps) {
|
||||
const { t } = useTranslation();
|
||||
const [isThinkingExpanded, setIsThinkingExpanded] = useState(true);
|
||||
const [responseTime, setResponseTime] = useState(0);
|
||||
const startTimeRef = useRef<number | null>(null);
|
||||
@@ -46,8 +48,14 @@ export function ChatMessage({ message, isTyping }: ChatMessageProps) {
|
||||
}`}
|
||||
>
|
||||
<p className="flex items-center gap-4 font-semibold text-sm text-[#333] dark:text-[#d8d8d8]">
|
||||
{isAssistant ? <img src={logoImg} className="w-6 h-6" /> : null}
|
||||
{isAssistant ? "Coco AI" : ""}
|
||||
{isAssistant ? (
|
||||
<img
|
||||
src={logoImg}
|
||||
className="w-6 h-6"
|
||||
alt={t("assistant.message.logo")}
|
||||
/>
|
||||
) : null}
|
||||
{isAssistant ? t("assistant.message.aiName") : ""}
|
||||
</p>
|
||||
<div className="prose dark:prose-invert prose-sm max-w-none">
|
||||
<div className="text-[#333] dark:text-[#d8d8d8] leading-relaxed">
|
||||
@@ -70,14 +78,16 @@ export function ChatMessage({ message, isTyping }: ChatMessageProps) {
|
||||
<>
|
||||
<Brain className="w-4 h-4 animate-pulse text-[#999999]" />
|
||||
<span className="text-xs text-[#999999] italic">
|
||||
AI is thinking...
|
||||
{t("assistant.message.thinking")}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Brain className="w-4 h-4 text-[#999999]" />
|
||||
<span className="text-xs text-[#999999]">
|
||||
Thought for {responseTime.toFixed(1)} seconds
|
||||
{t("assistant.message.thoughtTime", {
|
||||
time: responseTime.toFixed(1),
|
||||
})}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { MessageSquare, Plus } from "lucide-react";
|
||||
|
||||
import type { Chat } from "./types";
|
||||
interface SidebarProps {
|
||||
chats: Chat[];
|
||||
@@ -16,6 +18,8 @@ export function Sidebar({
|
||||
onSelectChat,
|
||||
className = "",
|
||||
}: SidebarProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className={`h-full flex flex-col ${className}`}>
|
||||
<div className="p-4">
|
||||
@@ -24,7 +28,7 @@ export function Sidebar({
|
||||
className={`w-full flex items-center gap-3 px-4 py-3 text-sm font-medium rounded-xl transition-all border border-[#E6E6E6] dark:border-[#272626] text-gray-700 hover:bg-gray-50/80 active:bg-gray-100/80 dark:text-white dark:hover:bg-gray-600/50 dark:active:bg-gray-500/50`}
|
||||
>
|
||||
<Plus className={`h-4 w-4 text-[#0072FF] dark:text-[#0072FF]`} />
|
||||
New Chat
|
||||
{t("assistant.sidebar.newChat")}
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto px-3 pb-3 space-y-2 custom-scrollbar">
|
||||
@@ -33,8 +37,8 @@ export function Sidebar({
|
||||
key={chat._id}
|
||||
className={`group relative rounded-xl transition-all ${
|
||||
activeChat._id === chat._id
|
||||
? 'bg-gray-100/80 dark:bg-gray-700/50'
|
||||
: 'hover:bg-gray-50/80 dark:hover:bg-gray-600/30'
|
||||
? "bg-gray-100/80 dark:bg-gray-700/50"
|
||||
: "hover:bg-gray-50/80 dark:hover:bg-gray-600/30"
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
@@ -44,15 +48,17 @@ export function Sidebar({
|
||||
<MessageSquare
|
||||
className={`h-4 w-4 flex-shrink-0 ${
|
||||
activeChat._id === chat._id
|
||||
? 'text-[#0072FF] dark:text-[#0072FF]'
|
||||
: 'text-gray-400 dark:text-gray-500'
|
||||
? "text-[#0072FF] dark:text-[#0072FF]"
|
||||
: "text-gray-400 dark:text-gray-500"
|
||||
}`}
|
||||
/>
|
||||
<span className={`truncate ${
|
||||
activeChat._id === chat._id
|
||||
? 'text-gray-900 dark:text-white font-medium'
|
||||
: 'text-gray-600 dark:text-gray-300'
|
||||
}`}>
|
||||
<span
|
||||
className={`truncate ${
|
||||
activeChat._id === chat._id
|
||||
? "text-gray-900 dark:text-white font-medium"
|
||||
: "text-gray-600 dark:text-gray-300"
|
||||
}`}
|
||||
>
|
||||
{chat.title || chat._id}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
@@ -5,6 +5,8 @@ import {
|
||||
SquareArrowOutUpRight,
|
||||
} from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { OpenURLWithBrowser } from "@/utils/index";
|
||||
|
||||
interface SourceResultProps {
|
||||
@@ -21,6 +23,7 @@ interface SourceItem {
|
||||
}
|
||||
|
||||
export function SourceResult({ text }: SourceResultProps) {
|
||||
const { t } = useTranslation();
|
||||
const [isSourceExpanded, setIsSourceExpanded] = useState(false);
|
||||
|
||||
if (!text?.includes("<Source")) {
|
||||
@@ -42,13 +45,11 @@ export function SourceResult({ text }: SourceResultProps) {
|
||||
const sourceData = getSourceData();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`mt-2 ${
|
||||
isSourceExpanded
|
||||
? "rounded-lg overflow-hidden border border-[#E6E6E6] dark:border-[#272626]"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
<div className={`mt-2 ${
|
||||
isSourceExpanded
|
||||
? "rounded-lg overflow-hidden border border-[#E6E6E6] dark:border-[#272626]"
|
||||
: ""
|
||||
}`}>
|
||||
<button
|
||||
onClick={() => setIsSourceExpanded((prev) => !prev)}
|
||||
className={`inline-flex justify-between items-center gap-2 px-2 py-1 rounded-xl transition-colors ${
|
||||
@@ -58,7 +59,7 @@ export function SourceResult({ text }: SourceResultProps) {
|
||||
<div className="flex gap-2">
|
||||
<Search className="w-4 h-4 text-[#999999] dark:text-[#999999]" />
|
||||
<span className="text-xs text-[#999999] dark:text-[#999999]">
|
||||
Found {totalResults} results
|
||||
{t('assistant.source.foundResults', { count: Number(totalResults) })}
|
||||
</span>
|
||||
</div>
|
||||
{isSourceExpanded ? (
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
getCurrent as getCurrentDeepLinkUrls,
|
||||
} from "@tauri-apps/plugin-deep-link";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { UserProfile } from "./UserProfile";
|
||||
import { DataSourcesList } from "./DataSourcesList";
|
||||
@@ -26,6 +27,8 @@ import { useConnectStore } from "@/stores/connectStore";
|
||||
import bannerImg from "@/assets/images/coco-cloud-banner.jpeg";
|
||||
|
||||
export default function Cloud() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const SidebarRef = useRef<{ refreshData: () => void }>(null);
|
||||
|
||||
const error = useAppStore((state) => state.error);
|
||||
@@ -371,7 +374,7 @@ export default function Cloud() {
|
||||
{currentService?.auth_provider?.sso?.url ? (
|
||||
<div className="mb-8">
|
||||
<h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
|
||||
Account Information
|
||||
{t('cloud.accountInfo')}
|
||||
</h2>
|
||||
{currentService?.profile ? (
|
||||
<UserProfile
|
||||
@@ -387,7 +390,7 @@ export default function Cloud() {
|
||||
className="px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors mb-3"
|
||||
onClick={LoginClick}
|
||||
>
|
||||
Login
|
||||
{t('cloud.login')}
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -398,7 +401,7 @@ export default function Cloud() {
|
||||
className="px-6 py-2 text-white bg-red-500 rounded-md hover:bg-red-600 transition-colors mb-3"
|
||||
onClick={() => setLoading(false)} // Reset loading state
|
||||
>
|
||||
Cancel
|
||||
{t('cloud.cancel')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
@@ -423,7 +426,7 @@ export default function Cloud() {
|
||||
)
|
||||
}
|
||||
>
|
||||
EULA | Privacy Policy
|
||||
{t('cloud.privacyPolicy')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import React, { useState } from "react";
|
||||
import { ChevronLeft } from "lucide-react";
|
||||
import {useAppStore} from "@/stores/appStore";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
|
||||
interface ConnectServiceProps {
|
||||
setIsConnect: (isConnect: boolean) => void;
|
||||
onAddServer: (endpoint: string) => void;
|
||||
}
|
||||
|
||||
export function Connect({ setIsConnect, onAddServer }: ConnectServiceProps) {
|
||||
export function Connect({ setIsConnect, onAddServer }: ConnectServiceProps) {
|
||||
const { t } = useTranslation();
|
||||
const [endpointLink, setEndpointLink] = useState("");
|
||||
const [refreshLoading, ] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState(''); // State to store the error message
|
||||
const [refreshLoading] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState(""); // State to store the error message
|
||||
|
||||
const setError = useAppStore((state) => state.setError);
|
||||
|
||||
@@ -30,16 +32,19 @@ export function Connect({ setIsConnect, onAddServer }: ConnectServiceProps) {
|
||||
setIsConnect(true); // Only set as connected if the server is added successfully
|
||||
} catch (err: any) {
|
||||
// Handle the error if something goes wrong
|
||||
const errorMessage = typeof err === 'string' ? err : err?.message || 'An unknown error occurred.';
|
||||
setErrorMessage("ERR:"+errorMessage);
|
||||
const errorMessage =
|
||||
typeof err === "string"
|
||||
? err
|
||||
: err?.message || "An unknown error occurred.";
|
||||
setErrorMessage("ERR:" + errorMessage);
|
||||
setError(errorMessage);
|
||||
console.error('Error:', errorMessage);
|
||||
console.error("Error:", errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
// Function to close the error message
|
||||
const closeError = () => {
|
||||
setErrorMessage('');
|
||||
setErrorMessage("");
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -52,16 +57,13 @@ export function Connect({ setIsConnect, onAddServer }: ConnectServiceProps) {
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
</button>
|
||||
<div className="text-xl text-[#101010] dark:text-white">
|
||||
Connecting to Your Coco-Server
|
||||
{t("cloud.connect.title")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-8">
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
Running your own private instance of coco-server ensures complete control over
|
||||
your data, keeping it secure and accessible only within your environment.
|
||||
Enjoy enhanced privacy, better performance, and seamless integration with your
|
||||
internal systems.
|
||||
{t("cloud.connect.description")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -71,23 +73,25 @@ export function Connect({ setIsConnect, onAddServer }: ConnectServiceProps) {
|
||||
htmlFor="endpoint"
|
||||
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2.5"
|
||||
>
|
||||
Server address
|
||||
{t("cloud.connect.serverAddress")}
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
id="endpoint"
|
||||
value={endpointLink}
|
||||
placeholder="For example: https://coco.infini.cloud/"
|
||||
placeholder={t("cloud.connect.serverPlaceholder")}
|
||||
onChange={(e) => setEndpointLink(e.target.value)}
|
||||
className="text-[#101010] dark:text-white flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-800"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="px-6 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
|
||||
onClick={()=>onAddServerClick(endpointLink)}
|
||||
onClick={() => onAddServerClick(endpointLink)}
|
||||
>
|
||||
{refreshLoading ? "Connecting..." : "Connect"}
|
||||
{refreshLoading
|
||||
? t("cloud.connect.connecting")
|
||||
: t("cloud.connect.connect")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -95,30 +99,28 @@ export function Connect({ setIsConnect, onAddServer }: ConnectServiceProps) {
|
||||
|
||||
{/*//TODO move to outer container, move error state to global*/}
|
||||
{errorMessage && (
|
||||
<div className="mb-8">
|
||||
<div
|
||||
className="mb-8"
|
||||
style={{
|
||||
color: "red",
|
||||
marginTop: "10px",
|
||||
display: "block", // Makes sure the error message starts on a new line
|
||||
marginBottom: "10px",
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
color: 'red',
|
||||
marginTop: '10px',
|
||||
display: 'block', // Makes sure the error message starts on a new line
|
||||
marginBottom: '10px',
|
||||
}}>
|
||||
<span>{errorMessage}</span>
|
||||
<button
|
||||
onClick={closeError}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
color: 'red',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
<span>{errorMessage}</span>
|
||||
<button
|
||||
onClick={closeError}
|
||||
style={{
|
||||
background: "none",
|
||||
border: "none",
|
||||
color: "red",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
></button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,80 +1,79 @@
|
||||
import {useEffect, useState} from "react";
|
||||
import {RefreshCcw} from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useEffect, useState } from "react";
|
||||
import { RefreshCcw } from "lucide-react";
|
||||
|
||||
import {DataSourceItem} from "./DataSourceItem";
|
||||
import {useConnectStore} from "@/stores/connectStore";
|
||||
import {useAppStore} from "@/stores/appStore";
|
||||
import {invoke} from "@tauri-apps/api/core";
|
||||
import { DataSourceItem } from "./DataSourceItem";
|
||||
import { useConnectStore } from "@/stores/connectStore";
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
export function DataSourcesList({server}: { server: string }) {
|
||||
const datasourceData = useConnectStore((state) => state.datasourceData);
|
||||
const setError = useAppStore((state) => state.setError);
|
||||
const [refreshLoading, setRefreshLoading] = useState(false);
|
||||
const setDatasourceData = useConnectStore((state) => state.setDatasourceData);
|
||||
const setConnectorData = useConnectStore((state) => state.setConnectorData);
|
||||
export function DataSourcesList({ server }: { server: string }) {
|
||||
const { t } = useTranslation();
|
||||
const datasourceData = useConnectStore((state) => state.datasourceData);
|
||||
const setError = useAppStore((state) => state.setError);
|
||||
const [refreshLoading, setRefreshLoading] = useState(false);
|
||||
const setDatasourceData = useConnectStore((state) => state.setDatasourceData);
|
||||
const setConnectorData = useConnectStore((state) => state.setConnectorData);
|
||||
|
||||
function initServerAppData({server}: { server: string }) {
|
||||
//fetch datasource data
|
||||
invoke("get_connectors_by_server", {id: server})
|
||||
.then((res: any) => {
|
||||
console.log("get_connectors_by_server", res);
|
||||
setConnectorData(res, server);
|
||||
})
|
||||
.catch((err: any) => {
|
||||
setError(err);
|
||||
throw err; // Propagate error back up
|
||||
})
|
||||
.finally(() => {
|
||||
function initServerAppData({ server }: { server: string }) {
|
||||
//fetch datasource data
|
||||
invoke("get_connectors_by_server", { id: server })
|
||||
.then((res: any) => {
|
||||
console.log("get_connectors_by_server", res);
|
||||
setConnectorData(res, server);
|
||||
})
|
||||
.catch((err: any) => {
|
||||
setError(err);
|
||||
throw err; // Propagate error back up
|
||||
})
|
||||
.finally(() => {});
|
||||
|
||||
});
|
||||
//fetch datasource data
|
||||
invoke("get_datasources_by_server", { id: server })
|
||||
.then((res: any) => {
|
||||
console.log("get_datasources_by_server", res);
|
||||
setDatasourceData(res, server);
|
||||
})
|
||||
.catch((err: any) => {
|
||||
setError(err);
|
||||
throw err; // Propagate error back up
|
||||
})
|
||||
.finally(() => {});
|
||||
}
|
||||
|
||||
//fetch datasource data
|
||||
invoke("get_datasources_by_server", {id: server})
|
||||
.then((res: any) => {
|
||||
console.log("get_datasources_by_server", res);
|
||||
setDatasourceData(res, server);
|
||||
})
|
||||
.catch((err: any) => {
|
||||
setError(err);
|
||||
throw err; // Propagate error back up
|
||||
})
|
||||
.finally(() => {
|
||||
});
|
||||
async function getDatasourceData() {
|
||||
setRefreshLoading(true);
|
||||
try {
|
||||
initServerAppData({ server });
|
||||
} catch (e) {
|
||||
setError(e);
|
||||
} finally {
|
||||
setRefreshLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function getDatasourceData() {
|
||||
setRefreshLoading(true);
|
||||
try {
|
||||
initServerAppData({server});
|
||||
} catch (e) {
|
||||
setError(e);
|
||||
} finally {
|
||||
setRefreshLoading(false);
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
getDatasourceData();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
getDatasourceData()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<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">
|
||||
{datasourceData[server]?.map((source) => (
|
||||
<DataSourceItem key={source.id} {...source} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<h2 className="flex justify-between text-xl font-medium text-gray-900 dark:text-white">
|
||||
{t("cloud.dataSource.title")}
|
||||
<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">
|
||||
{datasourceData[server]?.map((source) => (
|
||||
<DataSourceItem key={source.id} {...source} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
|
||||
export function Divider() {
|
||||
return (
|
||||
<div className="relative my-6">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<div className="w-full border-t border-gray-600"></div>
|
||||
</div>
|
||||
<div className="relative flex justify-center text-sm">
|
||||
<span className="px-2 text-gray-400 bg-gray-800">or continue with</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { forwardRef } from "react";
|
||||
import { Plus } from "lucide-react";
|
||||
|
||||
@@ -9,75 +10,79 @@ interface SidebarProps {
|
||||
serverList: any[];
|
||||
}
|
||||
|
||||
export const Sidebar = forwardRef<{ refreshData: () => void; }, SidebarProps>(
|
||||
({ onAddServer, serverList }, _ref) => {
|
||||
const currentService = useConnectStore((state) => state.currentService);
|
||||
const setCurrentService = useConnectStore((state) => state.setCurrentService);
|
||||
export const Sidebar = forwardRef<{ refreshData: () => void }, SidebarProps>(
|
||||
({ onAddServer, serverList }, _ref) => {
|
||||
const { t } = useTranslation();
|
||||
const currentService = useConnectStore((state) => state.currentService);
|
||||
const setCurrentService = useConnectStore(
|
||||
(state) => state.setCurrentService
|
||||
);
|
||||
|
||||
const onAddServerClick = () => {
|
||||
onAddServer();
|
||||
};
|
||||
|
||||
// Extracted server item rendering
|
||||
const renderServerItem = (item: any) => {
|
||||
return (
|
||||
<div
|
||||
key={item?.id}
|
||||
className={`flex cursor-pointer items-center space-x-2 px-3 py-2 bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400 rounded-lg mb-2 ${
|
||||
currentService?.id === item?.id ? "dark:bg-blue-900/20 dark:bg-blue-900" // Apply background color when selected
|
||||
: "bg-gray-50 dark:bg-gray-900" // Default background color when not selected
|
||||
}`}
|
||||
onClick={() => setCurrentService(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">
|
||||
{item?.available ? (
|
||||
<div className="w-3 h-3 rounded-full bg-[#00DB5E]" />
|
||||
) : (
|
||||
<div className="w-3 h-3 rounded-full bg-[#FF4747]" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const onAddServerClick = () => {
|
||||
onAddServer();
|
||||
};
|
||||
|
||||
// Extracted server item rendering
|
||||
const renderServerItem = (item: any) => {
|
||||
return (
|
||||
<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">
|
||||
{/* Render Built-in Servers */}
|
||||
<div>
|
||||
{serverList
|
||||
.filter((item) => item?.builtin)
|
||||
.map((item) => renderServerItem(item))}
|
||||
</div>
|
||||
|
||||
<div className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">
|
||||
Your Coco-Servers
|
||||
</div>
|
||||
|
||||
{/* Render Non-Built-in Servers */}
|
||||
<div>
|
||||
{serverList
|
||||
.filter((item) => !item?.builtin)
|
||||
.map((item) => renderServerItem(item))}
|
||||
</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={onAddServerClick}
|
||||
>
|
||||
<Plus className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
key={item?.id}
|
||||
className={`flex cursor-pointer items-center space-x-2 px-3 py-2 bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400 rounded-lg mb-2 ${
|
||||
currentService?.id === item?.id
|
||||
? "dark:bg-blue-900/20 dark:bg-blue-900"
|
||||
: "bg-gray-50 dark:bg-gray-900"
|
||||
}`}
|
||||
onClick={() => setCurrentService(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">
|
||||
{item?.available ? (
|
||||
<div className="w-3 h-3 rounded-full bg-[#00DB5E]" />
|
||||
) : (
|
||||
<div className="w-3 h-3 rounded-full bg-[#FF4747]" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<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">
|
||||
{/* Render Built-in Servers */}
|
||||
<div>
|
||||
{serverList
|
||||
.filter((item) => item?.builtin)
|
||||
.map((item) => renderServerItem(item))}
|
||||
</div>
|
||||
|
||||
<div className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">
|
||||
{t("cloud.sidebar.yourServers")}
|
||||
</div>
|
||||
|
||||
{/* Render Non-Built-in Servers */}
|
||||
<div>
|
||||
{serverList
|
||||
.filter((item) => !item?.builtin)
|
||||
.map((item) => renderServerItem(item))}
|
||||
</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={onAddServerClick}
|
||||
>
|
||||
<Plus className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -1,19 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
interface SocialButtonProps {
|
||||
icon: React.ReactNode;
|
||||
provider: string;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export function SocialButton({ icon, provider, onClick }: SocialButtonProps) {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className="w-full flex items-center justify-center gap-3 px-4 py-2 bg-gray-700 hover:bg-gray-600 text-white rounded-lg transition-colors"
|
||||
>
|
||||
{icon}
|
||||
<span>Continue with {provider}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -18,10 +18,10 @@ interface UserProfileProps {
|
||||
onLogout: (server: string) => void;
|
||||
}
|
||||
|
||||
export function UserProfile({ server,userInfo,onLogout }: UserProfileProps) {
|
||||
export function UserProfile({ server, userInfo, onLogout }: UserProfileProps) {
|
||||
const handleLogout = () => {
|
||||
onLogout(server);
|
||||
console.log("Logout",server);
|
||||
console.log("Logout", server);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react";
|
||||
import { ChevronDown, Globe2 } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface Language {
|
||||
code: string;
|
||||
name: string;
|
||||
flag: string;
|
||||
keyboard: string;
|
||||
}
|
||||
|
||||
const languages: Language[] = [
|
||||
{ code: "en", name: "English", flag: "🇺🇸", keyboard: "E" },
|
||||
{ code: "zh", name: "中文", flag: "🇨🇳", keyboard: "Z" },
|
||||
];
|
||||
|
||||
export default function LangToggle() {
|
||||
const { i18n } = useTranslation();
|
||||
const [currentLng, setCurrentLng] = useState(languages[0]);
|
||||
const changeLanguage = (lng: Language) => {
|
||||
setCurrentLng(lng);
|
||||
i18n.changeLanguage(lng.code);
|
||||
};
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
<MenuButton className="inline-flex items-center gap-2 rounded-md py-1.5 px-3 text-sm/6 font-semibold dark:text-white shadow-inner dark:shadow-white/10 focus:outline-none dark:data-[hover]:bg-gray-700 dark:data-[open]:bg-gray-700 data-[focus]:outline-1 dark:data-[focus]:outline-white">
|
||||
<Globe2 className="h-4 w-4 text-gray-600" />
|
||||
<span className="text-base">{currentLng.flag}</span>
|
||||
<span>{currentLng.name}</span>
|
||||
<ChevronDown className="size-4 dark:fill-white/60" />
|
||||
</MenuButton>
|
||||
<MenuItems
|
||||
anchor="bottom end"
|
||||
className="w-[160px] origin-top-right rounded-xl border dark:border-white/5 dark:bg-white/5 p-1 text-sm/6 dark:text-white transition duration-100 ease-out [--anchor-gap:var(--spacing-1)] focus:outline-none data-[closed]:scale-95 data-[closed]:opacity-0"
|
||||
>
|
||||
{languages.map((language) => (
|
||||
<MenuItem key={language.code}>
|
||||
<button
|
||||
className="group flex w-full items-center gap-2 rounded-lg py-1.5 px-3 dark:data-[focus]:bg-white/10"
|
||||
onClick={() => changeLanguage(language)}
|
||||
>
|
||||
<span className="mr-1 text-base">{language.flag}</span>
|
||||
<span>{language.name}</span>
|
||||
<kbd className="ml-auto hidden font-sans text-xs dark:text-white/50 group-data-[focus]:inline">
|
||||
⌘{language.keyboard}
|
||||
</kbd>
|
||||
</button>
|
||||
</MenuItem>
|
||||
))}
|
||||
</MenuItems>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useRef, useImperativeHandle, forwardRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface AutoResizeTextareaProps {
|
||||
input: string;
|
||||
@@ -12,6 +13,7 @@ const AutoResizeTextarea = forwardRef<
|
||||
{ reset: () => void; focus: () => void },
|
||||
AutoResizeTextareaProps
|
||||
>(({ input, setInput, handleKeyDown, connected }, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -47,8 +49,8 @@ const AutoResizeTextarea = forwardRef<
|
||||
autoCapitalize="none"
|
||||
spellCheck="false"
|
||||
className="text-base flex-1 outline-none min-w-[200px] text-[#333] dark:text-[#d8d8d8] placeholder-text-xs placeholder-[#999] dark:placeholder-gray-500 bg-transparent"
|
||||
placeholder={connected ? "Ask whatever you want ..." : ""}
|
||||
aria-label="Ask whatever you want ..."
|
||||
placeholder={connected ? t('search.textarea.placeholder') : ""}
|
||||
aria-label={t('search.textarea.ariaLabel')}
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={(e) => handleKeyDown?.(e)}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { formatter } from "@/utils/index";
|
||||
import TypeIcon from "@/components/Common/Icons/TypeIcon";
|
||||
@@ -8,10 +9,12 @@ interface DocumentDetailProps {
|
||||
}
|
||||
|
||||
export const DocumentDetail: React.FC<DocumentDetailProps> = ({ document }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<div className="font-normal text-xs text-[#666] dark:text-[#999] mb-2">
|
||||
Details
|
||||
{t('search.document.details')}
|
||||
</div>
|
||||
|
||||
{/* <div className="mb-4">
|
||||
@@ -30,65 +33,64 @@ export const DocumentDetail: React.FC<DocumentDetailProps> = ({ document }) => {
|
||||
|
||||
<div className="py-4 mt-4">
|
||||
<div className="flex justify-between flex-wrap font-normal text-xs mb-2.5">
|
||||
<div className="text-[#666]">Name</div>
|
||||
<div className="text-[#666]">{t('search.document.name')}</div>
|
||||
<div className="text-[#333] dark:text-[#D8D8D8] text-right w-60 break-words">
|
||||
{document?.title || "-"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between flex-wrap font-normal text-xs mb-2.5">
|
||||
<div className="text-[#666]">Source</div>
|
||||
<div className="text-[#666]">{t('search.document.source')}</div>
|
||||
<div className="text-[#333] dark:text-[#D8D8D8] flex justify-end text-right w-56 break-words">
|
||||
<TypeIcon item={document} className="w-4 h-4 mr-1" />
|
||||
{document?.source?.name || "-"}
|
||||
</div>
|
||||
</div>
|
||||
{/* <div className="flex justify-between font-normal text-xs mb-2.5">
|
||||
<div className="text-[#666]">Where</div>
|
||||
<div className="text-[#333] dark:text-[#D8D8D8] text-right w-56 break-words">
|
||||
-
|
||||
</div>
|
||||
</div> */}
|
||||
{document?.updated ? (
|
||||
|
||||
{document?.updated && (
|
||||
<div className="flex justify-between flex-wrap font-normal text-xs mb-2.5">
|
||||
<div className="text-[#666]">Updated at</div>
|
||||
<div className="text-[#666]">{t('search.document.updatedAt')}</div>
|
||||
<div className="text-[#333] dark:text-[#D8D8D8] text-right w-56 break-words">
|
||||
{document?.updated || "-"}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{document?.last_updated_by?.user?.username ? (
|
||||
)}
|
||||
|
||||
{document?.last_updated_by?.user?.username && (
|
||||
<div className="flex justify-between flex-wrap font-normal text-xs mb-2.5">
|
||||
<div className="text-[#666]">Update by</div>
|
||||
<div className="text-[#666]">{t('search.document.updatedBy')}</div>
|
||||
<div className="text-[#333] dark:text-[#D8D8D8] text-right w-56 break-words">
|
||||
{document?.last_updated_by?.user?.username || "-"}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{document?.owner?.username ? (
|
||||
)}
|
||||
|
||||
{document?.owner?.username && (
|
||||
<div className="flex justify-between flex-wrap font-normal text-xs mb-2.5">
|
||||
<div className="text-[#666]">Created by</div>
|
||||
<div className="text-[#666]">{t('search.document.createdBy')}</div>
|
||||
<div className="text-[#333] dark:text-[#D8D8D8] text-right w-56 break-words">
|
||||
{document?.owner?.username || "-"}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{document?.type ? (
|
||||
)}
|
||||
|
||||
{document?.type && (
|
||||
<div className="flex justify-between flex-wrap font-normal text-xs mb-2.5">
|
||||
<div className="text-[#666]">Type</div>
|
||||
<div className="text-[#666]">{t('search.document.type')}</div>
|
||||
<div className="text-[#333] dark:text-[#D8D8D8] text-right w-56 break-words">
|
||||
{document?.type || "-"}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{document?.size ? (
|
||||
)}
|
||||
|
||||
{document?.size && (
|
||||
<div className="flex justify-between flex-wrap font-normal text-xs mb-2.5">
|
||||
<div className="text-[#666]">Size</div>
|
||||
<div className="text-[#666]">{t('search.document.size')}</div>
|
||||
<div className="text-[#333] dark:text-[#D8D8D8] text-right w-56 break-words">
|
||||
{formatter.bytes(document?.size || 0)}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useState, useRef, useEffect, useCallback } from "react";
|
||||
import { useInfiniteScroll } from "ahooks";
|
||||
import { isTauri, invoke } from "@tauri-apps/api/core";
|
||||
import { open } from "@tauri-apps/plugin-shell";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useSearchStore } from "@/stores/searchStore";
|
||||
import { SearchHeader } from "./SearchHeader";
|
||||
@@ -28,6 +29,7 @@ export const DocumentList: React.FC<DocumentListProps> = ({
|
||||
viewMode,
|
||||
setViewMode,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const sourceData = useSearchStore((state) => state.sourceData);
|
||||
|
||||
const [selectedItem, setSelectedItem] = useState<number | null>(null);
|
||||
@@ -200,11 +202,9 @@ export const DocumentList: React.FC<DocumentListProps> = ({
|
||||
}, [selectedItem]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`border-r border-gray-200 dark:border-gray-700 flex flex-col h-full ${
|
||||
viewMode === "list" ? "w-[100%]" : "w-[50%]"
|
||||
}`}
|
||||
>
|
||||
<div className={`border-r border-gray-200 dark:border-gray-700 flex flex-col h-full ${
|
||||
viewMode === "list" ? "w-[100%]" : "w-[50%]"
|
||||
}`}>
|
||||
<div className="px-2 flex-shrink-0">
|
||||
<SearchHeader
|
||||
total={total}
|
||||
@@ -213,10 +213,7 @@ export const DocumentList: React.FC<DocumentListProps> = ({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="flex-1 overflow-y-auto custom-scrollbar"
|
||||
>
|
||||
<div ref={containerRef} className="flex-1 overflow-y-auto custom-scrollbar">
|
||||
{data?.list.map((hit: any, index: number) => {
|
||||
const isSelected = selectedItem === index;
|
||||
const item = hit.document;
|
||||
@@ -246,7 +243,7 @@ export const DocumentList: React.FC<DocumentListProps> = ({
|
||||
|
||||
{loading && (
|
||||
<div className="flex justify-center py-4">
|
||||
<span>Loading...</span>
|
||||
<span>{t('search.list.loading')}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -255,9 +252,13 @@ export const DocumentList: React.FC<DocumentListProps> = ({
|
||||
data-tauri-drag-region
|
||||
className="h-full w-full flex flex-col items-center"
|
||||
>
|
||||
<img src={noDataImg} alt="no-data" className="w-16 h-16 mt-24" />
|
||||
<img
|
||||
src={noDataImg}
|
||||
alt={t('search.list.noDataAlt')}
|
||||
className="w-16 h-16 mt-24"
|
||||
/>
|
||||
<div className="mt-4 text-sm text-[#999] dark:text-[#666]">
|
||||
No Results
|
||||
{t('search.list.noResults')}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ArrowDown01, Command, CornerDownLeft } from "lucide-react";
|
||||
import { emit } from "@tauri-apps/api/event";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import logoImg from "@/assets/icon.svg";
|
||||
import { useSearchStore } from "@/stores/searchStore";
|
||||
@@ -12,6 +13,7 @@ interface FooterProps {
|
||||
}
|
||||
|
||||
export default function Footer({}: FooterProps) {
|
||||
const { t } = useTranslation();
|
||||
const sourceData = useSearchStore((state) => state.sourceData);
|
||||
|
||||
function openSetting() {
|
||||
@@ -32,23 +34,18 @@ export default function Footer({}: FooterProps) {
|
||||
src={logoImg}
|
||||
className="w-4 h-4 cursor-pointer"
|
||||
onClick={openSetting}
|
||||
alt={t('search.footer.logoAlt')}
|
||||
/>
|
||||
)}
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{sourceData?.source?.name || "v1.0.0"}
|
||||
{sourceData?.source?.name || t('search.footer.version', { version: 'v1.0.0' })}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* {name ? (
|
||||
<div className="flex gap-2 items-center text-[#666] text-xs">
|
||||
<AppWindowMac className="w-5 h-5" /> {name}
|
||||
</div>
|
||||
) : null} */}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="gap-1 flex items-center text-[#666] dark:text-[#666] text-xs">
|
||||
<span className="mr-1.5 ">Select:</span>
|
||||
<span className="mr-1.5">{t('search.footer.select')}:</span>
|
||||
<kbd className="coco-modal-footer-commands-key pr-1">
|
||||
{isMac ? (
|
||||
<Command className="w-3 h-3" />
|
||||
@@ -64,7 +61,7 @@ export default function Footer({}: FooterProps) {
|
||||
</kbd>
|
||||
</div>
|
||||
<div className="flex items-center text-[#666] dark:text-[#666] text-xs">
|
||||
<span className="mr-1.5 ">Open: </span>
|
||||
<span className="mr-1.5">{t('search.footer.open')}: </span>
|
||||
<kbd className="coco-modal-footer-commands-key pr-1">
|
||||
<CornerDownLeft className="w-3 h-3" />
|
||||
</kbd>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { ArrowBigLeft, Search, Send, Globe, Brain } from "lucide-react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { invoke, isTauri } from "@tauri-apps/api/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import ChatSwitch from "@/components/Common/ChatSwitch";
|
||||
import AutoResizeTextarea from "./AutoResizeTextarea";
|
||||
@@ -40,6 +41,9 @@ export default function ChatInput({
|
||||
isDeepThinkActive,
|
||||
setIsDeepThinkActive,
|
||||
}: ChatInputProps) {
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const showTooltip = useAppStore(
|
||||
(state: { showTooltip: boolean }) => state.showTooltip
|
||||
);
|
||||
@@ -259,7 +263,7 @@ export default function ChatInput({
|
||||
autoCapitalize="none"
|
||||
spellCheck="false"
|
||||
className="text-base font-normal flex-1 outline-none min-w-[200px] text-[#333] dark:text-[#d8d8d8] placeholder-text-xs placeholder-[#999] dark:placeholder-gray-500 bg-transparent"
|
||||
placeholder="Search whatever you want ..."
|
||||
placeholder={t('search.input.searchPlaceholder')}
|
||||
value={inputValue}
|
||||
onChange={(e) => {
|
||||
onSend(e.target.value);
|
||||
@@ -347,12 +351,12 @@ export default function ChatInput({
|
||||
|
||||
{!connected && isChatMode ? (
|
||||
<div className="absolute top-0 right-0 bottom-0 left-0 px-2 py-4 bg-red-500/10 rounded-md font-normal text-xs text-gray-400 flex items-center gap-4">
|
||||
Unable to connect to the server
|
||||
{t('search.input.connectionError')}
|
||||
<div
|
||||
className="w-[96px] h-[24px] bg-[#0061FF] rounded-[12px] font-normal text-xs text-white flex items-center justify-center cursor-pointer"
|
||||
onClick={ReconnectClick}
|
||||
>
|
||||
Reconnect ({countdown})
|
||||
{t('search.input.reconnect')} ({countdown})
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
@@ -376,7 +380,7 @@ export default function ChatInput({
|
||||
}`}
|
||||
/>
|
||||
<span className={isDeepThinkActive ? "text-[#0072FF]" : ""}>
|
||||
Deep Think
|
||||
{t('search.input.deepThink')}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
@@ -391,7 +395,7 @@ export default function ChatInput({
|
||||
}`}
|
||||
/>
|
||||
<span className={isSearchActive ? "text-[#0072FF]" : ""}>
|
||||
Search
|
||||
{t('search.input.search')}
|
||||
</span>
|
||||
</button>
|
||||
{/*<button*/}
|
||||
|
||||
@@ -2,110 +2,124 @@ import TypeIcon from "@/components/Common/Icons/TypeIcon";
|
||||
import RichIcon from "@/components/Common/Icons/RichIcon";
|
||||
|
||||
interface ListRightProps {
|
||||
item: any;
|
||||
isSelected: boolean;
|
||||
showIndex: boolean;
|
||||
currentIndex: number;
|
||||
goToTwoPage: (item: any) => void;
|
||||
item: any;
|
||||
isSelected: boolean;
|
||||
showIndex: boolean;
|
||||
currentIndex: number;
|
||||
goToTwoPage: (item: any) => void;
|
||||
}
|
||||
|
||||
export default function ListRight({
|
||||
item,
|
||||
isSelected,
|
||||
showIndex,
|
||||
currentIndex,
|
||||
goToTwoPage
|
||||
item,
|
||||
isSelected,
|
||||
showIndex,
|
||||
currentIndex,
|
||||
goToTwoPage,
|
||||
}: ListRightProps) {
|
||||
|
||||
return (
|
||||
<div className="flex-1 text-right min-w-[160px] h-full pl-5 text-[12px] flex gap-2 items-center justify-end relative">
|
||||
{item?.rich_categories ? null : (
|
||||
<div
|
||||
className="w-4 h-4 cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
goToTwoPage(item);
|
||||
}}
|
||||
>
|
||||
<TypeIcon
|
||||
item={item}
|
||||
className="w-4 h-4 cursor-pointer"
|
||||
onClick={(e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
goToTwoPage(item);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{item?.rich_categories ? (
|
||||
<div className="flex items-center justify-end max-w-[calc(100%-20px)] whitespace-nowrap">
|
||||
<RichIcon
|
||||
item={item}
|
||||
className="w-4 h-4 mr-2 cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
goToTwoPage(item);
|
||||
}}
|
||||
/>
|
||||
<span
|
||||
className={`${isSelected ? "text-[#C8C8C8]" : "text-[#666]"} text-right truncate`}
|
||||
>
|
||||
{item?.rich_categories?.map(
|
||||
(rich_item: any, index: number) => {
|
||||
if (
|
||||
item?.rich_categories.length > 2 &&
|
||||
index === item?.rich_categories.length - 1
|
||||
)
|
||||
return "";
|
||||
return (index !== 0 ? "/" : "") + rich_item?.label;
|
||||
}
|
||||
)}
|
||||
</span>
|
||||
{item?.rich_categories.length > 2 ? (
|
||||
<span className={`${isSelected ? "text-[#C8C8C8]" : "text-[#666]"} text-right truncate`}>
|
||||
{"/" + item?.rich_categories?.at(-1)?.label}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
) : item?.category || item?.subcategory ? (
|
||||
<span
|
||||
className={`text-[12px] truncate ${isSelected ? "text-[#DCDCDC]" : "text-[#999] dark:text-[#666]"
|
||||
}`}
|
||||
>
|
||||
{(item?.category || "") + (item?.subcategory ? `/${item?.subcategory}` : "")}
|
||||
</span>
|
||||
) : (
|
||||
<span
|
||||
className={`text-[12px] truncate ${isSelected ? "text-[#DCDCDC]" : "text-[#999] dark:text-[#666]"
|
||||
}`}
|
||||
>
|
||||
{item?.last_updated_by?.user?.username || item?.owner?.username || item?.updated || item?.created || item?.type || ""}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{isSelected ? (
|
||||
<div
|
||||
className={`absolute ${showIndex && currentIndex < 10 ? "right-7" : "right-0"
|
||||
} w-4 h-4 flex items-end justify-center font-normal text-xs text-[#333] leading-[14px] bg-[#ccc] dark:bg-[#6B6B6B] rounded-md ${isSelected
|
||||
? "shadow-[-6px_0px_6px_2px_#950599]"
|
||||
: "shadow-[-6px_0px_6px_2px_#fff] dark:shadow-[-6px_0px_6px_2px_#000]"
|
||||
}`}
|
||||
>
|
||||
↩︎
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{showIndex && currentIndex < 10 ? (
|
||||
<div
|
||||
className={`absolute right-0 w-4 h-4 flex items-center justify-center font-normal text-xs text-[#333] leading-[14px] bg-[#ccc] dark:bg-[#6B6B6B] rounded-md ${isSelected
|
||||
? "shadow-[-6px_0px_6px_2px_#950599]"
|
||||
: "shadow-[-6px_0px_6px_2px_#fff] dark:shadow-[-6px_0px_6px_2px_#000]"
|
||||
}`}
|
||||
>
|
||||
{currentIndex}
|
||||
</div>
|
||||
) : null}
|
||||
return (
|
||||
<div className="flex-1 text-right min-w-[160px] h-full pl-5 text-[12px] flex gap-2 items-center justify-end relative">
|
||||
{item?.rich_categories ? null : (
|
||||
<div
|
||||
className="w-4 h-4 cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
goToTwoPage(item);
|
||||
}}
|
||||
>
|
||||
<TypeIcon
|
||||
item={item}
|
||||
className="w-4 h-4 cursor-pointer"
|
||||
onClick={(e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
goToTwoPage(item);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
)}
|
||||
|
||||
{item?.rich_categories ? (
|
||||
<div className="flex items-center justify-end max-w-[calc(100%-20px)] whitespace-nowrap">
|
||||
<RichIcon
|
||||
item={item}
|
||||
className="w-4 h-4 mr-2 cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
goToTwoPage(item);
|
||||
}}
|
||||
/>
|
||||
<span
|
||||
className={`${
|
||||
isSelected ? "text-[#C8C8C8]" : "text-[#666]"
|
||||
} text-right truncate`}
|
||||
>
|
||||
{item?.rich_categories?.map((rich_item: any, index: number) => {
|
||||
if (
|
||||
item?.rich_categories.length > 2 &&
|
||||
index === item?.rich_categories.length - 1
|
||||
)
|
||||
return "";
|
||||
return (index !== 0 ? "/" : "") + rich_item?.label;
|
||||
})}
|
||||
</span>
|
||||
{item?.rich_categories.length > 2 ? (
|
||||
<span
|
||||
className={`${
|
||||
isSelected ? "text-[#C8C8C8]" : "text-[#666]"
|
||||
} text-right truncate`}
|
||||
>
|
||||
{"/" + item?.rich_categories?.at(-1)?.label}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
) : item?.category || item?.subcategory ? (
|
||||
<span
|
||||
className={`text-[12px] truncate ${
|
||||
isSelected ? "text-[#DCDCDC]" : "text-[#999] dark:text-[#666]"
|
||||
}`}
|
||||
>
|
||||
{(item?.category || "") +
|
||||
(item?.subcategory ? `/${item?.subcategory}` : "")}
|
||||
</span>
|
||||
) : (
|
||||
<span
|
||||
className={`text-[12px] truncate ${
|
||||
isSelected ? "text-[#DCDCDC]" : "text-[#999] dark:text-[#666]"
|
||||
}`}
|
||||
>
|
||||
{item?.last_updated_by?.user?.username ||
|
||||
item?.owner?.username ||
|
||||
item?.updated ||
|
||||
item?.created ||
|
||||
item?.type ||
|
||||
""}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{isSelected ? (
|
||||
<div
|
||||
className={`absolute ${
|
||||
showIndex && currentIndex < 10 ? "right-7" : "right-0"
|
||||
} w-4 h-4 flex items-end justify-center font-normal text-xs text-[#333] leading-[14px] bg-[#ccc] dark:bg-[#6B6B6B] rounded-md ${
|
||||
isSelected
|
||||
? "shadow-[-6px_0px_6px_2px_#950599]"
|
||||
: "shadow-[-6px_0px_6px_2px_#fff] dark:shadow-[-6px_0px_6px_2px_#000]"
|
||||
}`}
|
||||
>
|
||||
↩︎
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{showIndex && currentIndex < 10 ? (
|
||||
<div
|
||||
className={`absolute right-0 w-4 h-4 flex items-center justify-center font-normal text-xs text-[#333] leading-[14px] bg-[#ccc] dark:bg-[#6B6B6B] rounded-md ${
|
||||
isSelected
|
||||
? "shadow-[-6px_0px_6px_2px_#950599]"
|
||||
: "shadow-[-6px_0px_6px_2px_#fff] dark:shadow-[-6px_0px_6px_2px_#000]"
|
||||
}`}
|
||||
>
|
||||
{currentIndex}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { useEffect, useState, useCallback, useRef } from "react";
|
||||
import { Command } from "lucide-react";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
// import { isTauri } from "@tauri-apps/api/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import DropdownList from "./DropdownList";
|
||||
import Footer from "./Footer";
|
||||
@@ -18,6 +19,7 @@ interface SearchProps {
|
||||
}
|
||||
|
||||
function Search({ isChatMode, input }: SearchProps) {
|
||||
const { t } = useTranslation();
|
||||
const sourceData = useSearchStore((state) => state.sourceData);
|
||||
|
||||
const [IsError, setIsError] = useState<boolean>(false);
|
||||
@@ -149,10 +151,10 @@ function Search({ isChatMode, input }: SearchProps) {
|
||||
>
|
||||
<img src={noDataImg} alt="no-data" className="w-16 h-16 mt-24" />
|
||||
<div className="mt-4 text-sm text-[#999] dark:text-[#666]">
|
||||
No Results
|
||||
{t('search.main.noResults')}
|
||||
</div>
|
||||
<div className="mt-10 text-sm text-[#333] dark:text-[#D8D8D8] flex">
|
||||
Ask Coco AI
|
||||
{t('search.main.askCoco')}
|
||||
{isMac ? (
|
||||
<span className="ml-3 w-5 h-5 rounded-[6px] border border-[#D8D8D8] flex justify-center items-center">
|
||||
<Command className="w-3 h-3" />
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from "react";
|
||||
import { AlignLeft, Columns2 } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface SearchHeaderProps {
|
||||
total: number;
|
||||
@@ -12,14 +13,16 @@ export const SearchHeader: React.FC<SearchHeaderProps> = ({
|
||||
viewMode,
|
||||
setViewMode,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between py-1">
|
||||
<div className="text-xs text-gray-600 dark:text-gray-400">
|
||||
Found
|
||||
{t('search.header.found')}
|
||||
<span className="px-1 font-medium text-gray-900 dark:text-gray-100">
|
||||
{total}
|
||||
</span>
|
||||
results
|
||||
{t('search.header.results')}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<div className="flex bg-gray-100 dark:bg-gray-800 rounded-lg p-0.5">
|
||||
|
||||
@@ -1,35 +1,47 @@
|
||||
import { Globe, Github } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
import { OpenURLWithBrowser } from "@/utils";
|
||||
import logoLight from "@/assets/images/logo-text-light.svg";
|
||||
import logoDark from "@/assets/images/logo-text-dark.svg";
|
||||
|
||||
export default function AboutView(){
|
||||
|
||||
export default function AboutView() {
|
||||
const { t } = useTranslation();
|
||||
const { theme } = useTheme();
|
||||
|
||||
const logo = theme === 'dark' ? logoDark : logoLight
|
||||
const logo = theme === "dark" ? logoDark : logoLight;
|
||||
|
||||
return (
|
||||
<div className="flex justify-center items-center flex-col h-[calc(100vh-170px)]">
|
||||
<div>
|
||||
<img src={logo} className="w-48 dark:text-white"/>
|
||||
<img src={logo} className="w-48 dark:text-white" alt={t('settings.about.logo')} />
|
||||
</div>
|
||||
<div className="mt-8 font-medium text-gray-900 dark:text-gray-100">
|
||||
Search, Connect, Collaborate—All in one place
|
||||
{t('settings.about.slogan')}
|
||||
</div>
|
||||
<div className="flex justify-center items-center mt-10">
|
||||
<button onClick={() => OpenURLWithBrowser('https://coco.rs')} className="w-6 h-6 mr-2.5 flex justify-center rounded-[6px] border-[1px] gray-200 dark:border-gray-700"><Globe className="w-3 text-blue-500"/></button>
|
||||
<button onClick={() => OpenURLWithBrowser('https://github.com/infinilabs/coco-app')} className="w-6 h-6 flex justify-center rounded-[6px] border-[1px] gray-200 dark:border-gray-700" ><Github className="w-3 text-blue-500"/></button>
|
||||
<button
|
||||
onClick={() => OpenURLWithBrowser("https://coco.rs")}
|
||||
className="w-6 h-6 mr-2.5 flex justify-center rounded-[6px] border-[1px] gray-200 dark:border-gray-700"
|
||||
aria-label={t('settings.about.website')}
|
||||
>
|
||||
<Globe className="w-3 text-blue-500" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => OpenURLWithBrowser("https://github.com/infinilabs/coco-app")}
|
||||
className="w-6 h-6 flex justify-center rounded-[6px] border-[1px] gray-200 dark:border-gray-700"
|
||||
aria-label={t('settings.about.github')}
|
||||
>
|
||||
<Github className="w-3 text-blue-500" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-8 text-sm text-gray-500 dark:text-gray-400">
|
||||
Version 1.0.0
|
||||
{t('settings.about.version', { version: '1.0.0' })}
|
||||
</div>
|
||||
<div className="mt-4 text-sm text-gray-500 dark:text-gray-400">
|
||||
©{new Date().getFullYear()} INFINI Labs, All Rights Reserved.
|
||||
{t('settings.about.copyright', { year: new Date().getFullYear() })}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect } from "react";
|
||||
import { Globe } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import SettingsItem from "./SettingsItem";
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
@@ -12,6 +13,7 @@ const ENDPOINTS = [
|
||||
];
|
||||
|
||||
export default function AdvancedSettings() {
|
||||
const { t } = useTranslation();
|
||||
const endpoint = useAppStore(state => state.endpoint);
|
||||
const setEndpoint = useAppStore(state => state.setEndpoint);
|
||||
|
||||
@@ -25,20 +27,18 @@ export default function AdvancedSettings() {
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
||||
Advanced Settings
|
||||
{t('settings.advanced.title')}
|
||||
</h2>
|
||||
<div className="space-y-6">
|
||||
<SettingsItem
|
||||
icon={Globe}
|
||||
title="API Endpoint"
|
||||
description="Domain name for interface and websocket"
|
||||
title={t('settings.advanced.endpoint.title')}
|
||||
description={t('settings.advanced.endpoint.description')}
|
||||
>
|
||||
<div className={`p-4 rounded-lg`}>
|
||||
<select
|
||||
value={endpoint}
|
||||
onChange={(e) =>
|
||||
onChangeEndpoint(e.target.value as AppEndpoint)
|
||||
}
|
||||
onChange={(e) => onChangeEndpoint(e.target.value as AppEndpoint)}
|
||||
className={`w-full px-3 py-2 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white border-gray-300 text-gray-900 dark:bg-gray-800 dark:border-gray-600 dark:text-white`}
|
||||
>
|
||||
{ENDPOINTS.map(({ value, label }) => (
|
||||
|
||||
@@ -8,12 +8,15 @@ import {
|
||||
Power,
|
||||
Tags,
|
||||
// Trash2,
|
||||
Globe,
|
||||
} from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { isTauri, invoke } from "@tauri-apps/api/core";
|
||||
import {
|
||||
isEnabled,
|
||||
// enable, disable
|
||||
} from "@tauri-apps/plugin-autostart";
|
||||
import { emit } from '@tauri-apps/api/event';
|
||||
|
||||
import SettingsItem from "./SettingsItem";
|
||||
import SettingsToggle from "./SettingsToggle";
|
||||
@@ -34,6 +37,7 @@ export function ThemeOption({
|
||||
theme: AppTheme;
|
||||
}) {
|
||||
const { theme: currentTheme, changeTheme } = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const isSelected = currentTheme === theme;
|
||||
|
||||
@@ -45,41 +49,36 @@ export function ThemeOption({
|
||||
? "border-blue-500 bg-blue-50 dark:bg-blue-900/20"
|
||||
: "border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600"
|
||||
} flex flex-col items-center justify-center space-y-2 transition-all`}
|
||||
title={title}
|
||||
>
|
||||
<Icon className={`w-6 h-6 ${isSelected ? "text-blue-500" : ""}`} />
|
||||
<span
|
||||
className={`text-sm font-medium ${isSelected ? "text-blue-500" : ""}`}
|
||||
>
|
||||
{title}
|
||||
<span className={`text-sm font-medium ${isSelected ? "text-blue-500" : ""}`}>
|
||||
{t(`settings.appearance.${theme}`)}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export default function GeneralSettings() {
|
||||
const { t, i18n } = useTranslation();
|
||||
|
||||
const [launchAtLogin, setLaunchAtLogin] = useState(true);
|
||||
|
||||
const showTooltip = useAppStore((state) => state.showTooltip);
|
||||
const setShowTooltip = useAppStore((state) => state.setShowTooltip);
|
||||
const language = useAppStore((state) => state.language);
|
||||
const setLanguage = useAppStore((state) => state.setLanguage);
|
||||
|
||||
// const setAuth = useAuthStore((state) => state.setAuth);
|
||||
// const setUserInfo = useAuthStore((state) => state.setUserInfo);
|
||||
// const endpoint = useAppStore((state) => state.endpoint);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchAutoStartStatus = async () => {
|
||||
if (isTauri()) {
|
||||
try {
|
||||
const status = await isEnabled();
|
||||
setLaunchAtLogin(status);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch autostart status:", error);
|
||||
}
|
||||
const fetchAutoStartStatus = async () => {
|
||||
if (isTauri()) {
|
||||
try {
|
||||
const status = await isEnabled();
|
||||
setLaunchAtLogin(status);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch autostart status:", error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchAutoStartStatus();
|
||||
}, []);
|
||||
}
|
||||
};
|
||||
|
||||
const enableAutoStart = async () => {
|
||||
if (isTauri()) {
|
||||
@@ -118,7 +117,11 @@ export default function GeneralSettings() {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchAutoStartStatus();
|
||||
getCurrentShortcut();
|
||||
if (language) {
|
||||
i18n.changeLanguage(language);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const changeShortcut = (key: Shortcut) => {
|
||||
@@ -162,31 +165,42 @@ export default function GeneralSettings() {
|
||||
// useAppStore.persist.clearStorage();
|
||||
// }, [endpoint]);
|
||||
|
||||
const currentLanguage = language || i18n.language;
|
||||
|
||||
const changeLanguage = async (lang: string) => {
|
||||
i18n.changeLanguage(lang);
|
||||
setLanguage(lang);
|
||||
//
|
||||
try {
|
||||
await emit('language-changed', { language: lang });
|
||||
} catch (error) {
|
||||
console.error('Failed to emit language change event:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
||||
General Settings
|
||||
{t('settings.general')}
|
||||
</h2>
|
||||
<div className="space-y-6">
|
||||
<SettingsItem
|
||||
icon={Power}
|
||||
title="Startup"
|
||||
description="Automatically start Coco when you login"
|
||||
title={t('settings.startup.title')}
|
||||
description={t('settings.startup.description')}
|
||||
>
|
||||
<SettingsToggle
|
||||
checked={launchAtLogin}
|
||||
onChange={(value) =>
|
||||
value ? enableAutoStart() : disableAutoStart()
|
||||
}
|
||||
label="Launch at login"
|
||||
onChange={(value) => value ? enableAutoStart() : disableAutoStart()}
|
||||
label={t('settings.startup.toggle')}
|
||||
/>
|
||||
</SettingsItem>
|
||||
|
||||
<SettingsItem
|
||||
icon={Command}
|
||||
title="Coco Hotkey"
|
||||
description="Global shortcut to open Coco"
|
||||
title={t('settings.hotkey.title')}
|
||||
description={t('settings.hotkey.description')}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<ShortcutItem
|
||||
@@ -200,75 +214,47 @@ export default function GeneralSettings() {
|
||||
</div>
|
||||
</SettingsItem>
|
||||
|
||||
{/* <SettingsItem
|
||||
icon={Monitor}
|
||||
title="Window Mode"
|
||||
description="Choose how Coco appears on your screen"
|
||||
>
|
||||
<SettingsSelect
|
||||
options={["Standard Window", "Compact Mode", "Full Screen"]}
|
||||
/>
|
||||
</SettingsItem> */}
|
||||
|
||||
<SettingsItem
|
||||
icon={Palette}
|
||||
title="Appearance"
|
||||
description="Choose your preferred theme"
|
||||
title={t('settings.appearance.title')}
|
||||
description={t('settings.appearance.description')}
|
||||
>
|
||||
<div></div>
|
||||
</SettingsItem>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<ThemeOption icon={Sun} title="Light" theme="light" />
|
||||
<ThemeOption icon={Moon} title="Dark" theme="dark" />
|
||||
<ThemeOption icon={Monitor} title="Auto" theme="auto" />
|
||||
<ThemeOption icon={Sun} title={t('settings.appearance.light')} theme="light" />
|
||||
<ThemeOption icon={Moon} title={t('settings.appearance.dark')} theme="dark" />
|
||||
<ThemeOption icon={Monitor} title={t('settings.appearance.auto')} theme="auto" />
|
||||
</div>
|
||||
|
||||
<SettingsItem
|
||||
icon={Globe}
|
||||
title={t('settings.language.title')}
|
||||
description={t('settings.language.description')}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<select
|
||||
value={currentLanguage}
|
||||
onChange={(e) => changeLanguage(e.target.value)}
|
||||
className="px-3 py-1.5 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="en">{t('settings.language.english')}</option>
|
||||
<option value="zh">{t('settings.language.chinese')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</SettingsItem>
|
||||
|
||||
<SettingsItem
|
||||
icon={Tags}
|
||||
title="Tooltip"
|
||||
description="Tooltip display for shortcut keys"
|
||||
title={t('settings.tooltip.title')}
|
||||
description={t('settings.tooltip.description')}
|
||||
>
|
||||
<SettingsToggle
|
||||
checked={showTooltip}
|
||||
onChange={(value) => setShowTooltip(value)}
|
||||
label="Tooltip display"
|
||||
label={t('settings.tooltip.toggle')}
|
||||
/>
|
||||
</SettingsItem>
|
||||
|
||||
{/* <SettingsItem
|
||||
icon={Layout}
|
||||
title="Text Size"
|
||||
description="Adjust the application text size"
|
||||
>
|
||||
<SettingsSelect options={["Small", "Medium", "Large"]} />
|
||||
</SettingsItem> */}
|
||||
|
||||
{/* <SettingsItem
|
||||
icon={Star}
|
||||
title="Favorites"
|
||||
description="Manage your favorite commands"
|
||||
>
|
||||
<button className="text-sm text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 font-medium transition-colors duration-200">
|
||||
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,5 +1,8 @@
|
||||
import { formatKey, sortKeys } from "@/utils/keyboardUtils";
|
||||
import { X } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { formatKey, sortKeys } from "@/utils/keyboardUtils";
|
||||
|
||||
interface ShortcutItemProps {
|
||||
shortcut: string[];
|
||||
isEditing: boolean;
|
||||
@@ -17,6 +20,8 @@ export function ShortcutItem({
|
||||
onSave,
|
||||
onCancel,
|
||||
}: ShortcutItemProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const renderKeys = (keys: string[]) => {
|
||||
const sortedKeys = sortKeys(keys);
|
||||
return sortedKeys.map((key, index) => (
|
||||
@@ -30,9 +35,7 @@ export function ShortcutItem({
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex items-center justify-between p-4 rounded-lg bg-gray-50 dark:bg-gray-700`}
|
||||
>
|
||||
<div className={`flex items-center justify-between p-4 rounded-lg bg-gray-50 dark:bg-gray-700`}>
|
||||
<div className="flex items-center gap-4">
|
||||
{isEditing ? (
|
||||
<>
|
||||
@@ -41,7 +44,7 @@ export function ShortcutItem({
|
||||
renderKeys(currentKeys)
|
||||
) : (
|
||||
<span className={`italic text-gray-500 dark:text-gray-400`}>
|
||||
Press keys...
|
||||
{t('settings.shortcut.pressKeys')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -52,7 +55,7 @@ export function ShortcutItem({
|
||||
className={`px-3 py-1 text-sm rounded bg-blue-500 text-white hover:bg-blue-600 dark:bg-blue-600 dark:text-white dark:hover:bg-blue-700
|
||||
disabled:opacity-50 disabled:cursor-not-allowed`}
|
||||
>
|
||||
Save
|
||||
{t('settings.shortcut.save')}
|
||||
</button>
|
||||
<button
|
||||
onClick={onCancel}
|
||||
@@ -69,7 +72,7 @@ export function ShortcutItem({
|
||||
onClick={onEdit}
|
||||
className={`px-3 py-1 text-sm rounded bg-gray-200 text-gray-700 hover:bg-gray-300 dark:bg-gray-600 dark:text-gray-200 dark:hover:bg-gray-500`}
|
||||
>
|
||||
Edit
|
||||
{t('settings.shortcut.edit')}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
30
src/i18n.ts
30
src/i18n.ts
@@ -1,23 +1,27 @@
|
||||
import i18n from "i18next";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||
|
||||
import enTranslation from "./locales/en/translation.json";
|
||||
import zhTranslation from "./locales/zh/translation.json";
|
||||
|
||||
i18n.use(initReactI18next).init({
|
||||
resources: {
|
||||
en: {
|
||||
translation: enTranslation,
|
||||
i18n
|
||||
.use(LanguageDetector)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
resources: {
|
||||
en: {
|
||||
translation: enTranslation,
|
||||
},
|
||||
zh: {
|
||||
translation: zhTranslation,
|
||||
},
|
||||
},
|
||||
zh: {
|
||||
translation: zhTranslation,
|
||||
lng: "en",
|
||||
fallbackLng: "en",
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
},
|
||||
lng: "en",
|
||||
fallbackLng: "en",
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
|
||||
@@ -1,7 +1,165 @@
|
||||
{
|
||||
"welcome": "Welcome to Coco App",
|
||||
"home": "Home",
|
||||
"settings": "Settings",
|
||||
"activeTheme": "Current theme:",
|
||||
"InputMessage": "Input your message here..."
|
||||
"settings": {
|
||||
"general": "General Settings",
|
||||
"startup": {
|
||||
"title": "Startup",
|
||||
"description": "Automatically start Coco when you login",
|
||||
"toggle": "Launch at login"
|
||||
},
|
||||
"hotkey": {
|
||||
"title": "Coco Hotkey",
|
||||
"description": "Global shortcut to open Coco"
|
||||
},
|
||||
"appearance": {
|
||||
"title": "Appearance",
|
||||
"description": "Choose your preferred theme",
|
||||
"light": "Light",
|
||||
"dark": "Dark",
|
||||
"auto": "Auto"
|
||||
},
|
||||
"language": {
|
||||
"title": "Language",
|
||||
"description": "Choose your preferred language",
|
||||
"english": "English",
|
||||
"chinese": "中文"
|
||||
},
|
||||
"tooltip": {
|
||||
"title": "Tooltip",
|
||||
"description": "Tooltip display for shortcut keys",
|
||||
"toggle": "Tooltip display"
|
||||
},
|
||||
"shortcut": {
|
||||
"pressKeys": "Press keys...",
|
||||
"save": "Save",
|
||||
"edit": "Edit"
|
||||
},
|
||||
"about": {
|
||||
"logo": "Coco Logo",
|
||||
"slogan": "Search, Connect, Collaborate—All in one place",
|
||||
"website": "Visit Website",
|
||||
"github": "Visit GitHub",
|
||||
"version": "Version {{version}}",
|
||||
"copyright": "©{{year}} INFINI Labs, All Rights Reserved."
|
||||
},
|
||||
"advanced": {
|
||||
"title": "Advanced Settings",
|
||||
"endpoint": {
|
||||
"title": "API Endpoint",
|
||||
"description": "Domain name for interface and websocket"
|
||||
}
|
||||
},
|
||||
"tabs": {
|
||||
"general": "General",
|
||||
"extensions": "Extensions",
|
||||
"connect": "Connect",
|
||||
"advanced": "Advanced",
|
||||
"about": "About",
|
||||
"extensionsContent": "Extensions settings content",
|
||||
"advancedContent": "Advanced Settings content"
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"textarea": {
|
||||
"placeholder": "Ask whatever you want ...",
|
||||
"ariaLabel": "Ask whatever you want"
|
||||
},
|
||||
"document": {
|
||||
"details": "Details",
|
||||
"name": "Name",
|
||||
"source": "Source",
|
||||
"updatedAt": "Updated at",
|
||||
"updatedBy": "Updated by",
|
||||
"createdBy": "Created by",
|
||||
"type": "Type",
|
||||
"size": "Size"
|
||||
},
|
||||
"list": {
|
||||
"loading": "Loading...",
|
||||
"noResults": "No Results",
|
||||
"noDataAlt": "No data image"
|
||||
},
|
||||
"footer": {
|
||||
"logoAlt": "Coco Logo",
|
||||
"version": "{{version}}",
|
||||
"select": "Select",
|
||||
"open": "Open"
|
||||
},
|
||||
"input": {
|
||||
"searchPlaceholder": "Search whatever you want ...",
|
||||
"connectionError": "Unable to connect to the server",
|
||||
"reconnect": "Reconnect",
|
||||
"deepThink": "Deep Think",
|
||||
"search": "Search"
|
||||
},
|
||||
"main": {
|
||||
"noDataAlt": "No data image",
|
||||
"noResults": "No Results",
|
||||
"askCoco": "Ask Coco AI"
|
||||
},
|
||||
"header": {
|
||||
"found": "Found",
|
||||
"results": "results"
|
||||
}
|
||||
},
|
||||
"assistant": {
|
||||
"chat": {
|
||||
"openChat": "Open Chat Window",
|
||||
"newChat": "New Chat",
|
||||
"connectionError": "Unable to connect to the server",
|
||||
"reconnect": "Reconnect"
|
||||
},
|
||||
"input": {
|
||||
"stopMessage": "Stop message",
|
||||
"deepThink": "Deep Think",
|
||||
"deepThinkTooltip": "Enable deep thinking mode",
|
||||
"search": "Search",
|
||||
"searchTooltip": "Enable search mode"
|
||||
},
|
||||
"message": {
|
||||
"logo": "Coco AI Logo",
|
||||
"aiName": "Coco AI",
|
||||
"thinking": "AI is thinking...",
|
||||
"thoughtTime": "Thought for {{time}} seconds",
|
||||
"thinkingButton": "View thinking process"
|
||||
},
|
||||
"sidebar": {
|
||||
"newChat": "New Chat",
|
||||
"newChatTooltip": "Create a new chat",
|
||||
"selectChat": "Select this chat",
|
||||
"untitledChat": "Untitled Chat"
|
||||
},
|
||||
"source": {
|
||||
"foundResults": "Found {{count}} results"
|
||||
}
|
||||
},
|
||||
"cloud": {
|
||||
"banner": "Banner Image",
|
||||
"accountInfo": "Account Information",
|
||||
"login": "Login",
|
||||
"cancel": "Cancel",
|
||||
"copyUrl": "Copy URL",
|
||||
"privacyPolicy": "EULA | Privacy Policy",
|
||||
"connect": {
|
||||
"back": "Back",
|
||||
"title": "Connecting to Your Coco-Server",
|
||||
"description": "Running your own private instance of coco-server ensures complete control over your data, keeping it secure and accessible only within your environment. Enjoy enhanced privacy, better performance, and seamless integration with your internal systems.",
|
||||
"serverAddress": "Server address",
|
||||
"serverPlaceholder": "For example: https://coco.infini.cloud/",
|
||||
"connecting": "Connecting...",
|
||||
"connect": "Connect",
|
||||
"closeError": "Close error message"
|
||||
},
|
||||
"dataSource": {
|
||||
"title": "Data Source",
|
||||
"refresh": "Refresh data source list"
|
||||
},
|
||||
"sidebar": {
|
||||
"selectServer": "Select Server",
|
||||
"serverLogo": "Server Logo",
|
||||
"serverOnline": "Server Online",
|
||||
"serverOffline": "Server Offline",
|
||||
"yourServers": "Your Coco-Servers",
|
||||
"addServer": "Add New Server"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,165 @@
|
||||
{
|
||||
"welcome": "欢迎使用 Coco App",
|
||||
"home": "主页",
|
||||
"settings": "设置",
|
||||
"activeTheme": "当前主题:",
|
||||
"InputMessage": "在此输入您的消息..."
|
||||
"settings": {
|
||||
"general": "通用设置",
|
||||
"startup": {
|
||||
"title": "启动项",
|
||||
"description": "登录时自动启动 Coco",
|
||||
"toggle": "开机自启"
|
||||
},
|
||||
"hotkey": {
|
||||
"title": "快捷键",
|
||||
"description": "打开 Coco 的全局快捷键"
|
||||
},
|
||||
"appearance": {
|
||||
"title": "外观",
|
||||
"description": "选择您喜欢的主题",
|
||||
"light": "浅色",
|
||||
"dark": "深色",
|
||||
"auto": "自动"
|
||||
},
|
||||
"language": {
|
||||
"title": "语言",
|
||||
"description": "选择您的首选语言",
|
||||
"english": "English",
|
||||
"chinese": "中文"
|
||||
},
|
||||
"tooltip": {
|
||||
"title": "提示",
|
||||
"description": "快捷键提示显示",
|
||||
"toggle": "显示提示"
|
||||
},
|
||||
"shortcut": {
|
||||
"pressKeys": "请按键...",
|
||||
"save": "保存",
|
||||
"edit": "编辑"
|
||||
},
|
||||
"about": {
|
||||
"logo": "Coco 标志",
|
||||
"slogan": "搜索、连接、协作 — 一站式解决方案",
|
||||
"website": "访问官网",
|
||||
"github": "访问 GitHub",
|
||||
"version": "版本 {{version}}",
|
||||
"copyright": "©{{year}} INFINI Labs,保留所有权利。"
|
||||
},
|
||||
"advanced": {
|
||||
"title": "高级设置",
|
||||
"endpoint": {
|
||||
"title": "API 接口",
|
||||
"description": "接口和 WebSocket 的域名"
|
||||
}
|
||||
},
|
||||
"tabs": {
|
||||
"general": "通用",
|
||||
"extensions": "扩展",
|
||||
"connect": "连接",
|
||||
"advanced": "高级",
|
||||
"about": "关于",
|
||||
"extensionsContent": "扩展设置内容",
|
||||
"advancedContent": "高级设置内容"
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"textarea": {
|
||||
"placeholder": "问我任何问题...",
|
||||
"ariaLabel": "输入你想问的问题"
|
||||
},
|
||||
"document": {
|
||||
"details": "详细信息",
|
||||
"name": "名称",
|
||||
"source": "来源",
|
||||
"updatedAt": "更新时间",
|
||||
"updatedBy": "更新者",
|
||||
"createdBy": "创建者",
|
||||
"type": "类型",
|
||||
"size": "大小"
|
||||
},
|
||||
"list": {
|
||||
"loading": "加载中...",
|
||||
"noResults": "暂无结果",
|
||||
"noDataAlt": "无数据图片"
|
||||
},
|
||||
"footer": {
|
||||
"logoAlt": "Coco 图标",
|
||||
"version": "{{version}}",
|
||||
"select": "选择",
|
||||
"open": "打开"
|
||||
},
|
||||
"input": {
|
||||
"searchPlaceholder": "搜索任何内容...",
|
||||
"connectionError": "无法连接到服务器",
|
||||
"reconnect": "重新连接",
|
||||
"deepThink": "深度思考",
|
||||
"search": "搜索"
|
||||
},
|
||||
"main": {
|
||||
"noDataAlt": "无数据图片",
|
||||
"noResults": "暂无结果",
|
||||
"askCoco": "询问 Coco AI"
|
||||
},
|
||||
"header": {
|
||||
"found": "找到",
|
||||
"results": "个结果"
|
||||
}
|
||||
},
|
||||
"assistant": {
|
||||
"chat": {
|
||||
"openChat": "打开聊天窗口",
|
||||
"newChat": "新建对话",
|
||||
"connectionError": "无法连接到服务器",
|
||||
"reconnect": "重新连接"
|
||||
},
|
||||
"input": {
|
||||
"stopMessage": "停止生成",
|
||||
"deepThink": "深度思考",
|
||||
"deepThinkTooltip": "启用深度思考模式",
|
||||
"search": "搜索",
|
||||
"searchTooltip": "启用搜索模式"
|
||||
},
|
||||
"message": {
|
||||
"logo": "Coco AI 图标",
|
||||
"aiName": "Coco AI",
|
||||
"thinking": "AI 正在思考...",
|
||||
"thoughtTime": "思考了 {{time}} 秒",
|
||||
"thinkingButton": "查看思考过程"
|
||||
},
|
||||
"sidebar": {
|
||||
"newChat": "新建对话",
|
||||
"newChatTooltip": "创建新的对话",
|
||||
"selectChat": "选择此对话",
|
||||
"untitledChat": "未命名对话"
|
||||
},
|
||||
"source": {
|
||||
"foundResults": "找到 {{count}} 个结果"
|
||||
}
|
||||
},
|
||||
"cloud": {
|
||||
"banner": "横幅图片",
|
||||
"accountInfo": "账户信息",
|
||||
"login": "登录",
|
||||
"cancel": "取消",
|
||||
"copyUrl": "复制链接",
|
||||
"privacyPolicy": "用户协议 | 隐私政策",
|
||||
"connect": {
|
||||
"back": "返回",
|
||||
"title": "连接到您的 Coco-Server",
|
||||
"description": "运行您自己的私有 coco-server 实例可以确保对数据的完全控制,使其在您的环境中保持安全且仅可访问。享受增强的隐私保护、更好的性能和与内部系统的无缝集成。",
|
||||
"serverAddress": "服务器地址",
|
||||
"serverPlaceholder": "例如:https://coco.infini.cloud/",
|
||||
"connecting": "连接中...",
|
||||
"connect": "连接",
|
||||
"closeError": "关闭错误提示"
|
||||
},
|
||||
"dataSource": {
|
||||
"title": "数据源",
|
||||
"refresh": "刷新数据源列表"
|
||||
},
|
||||
"sidebar": {
|
||||
"selectServer": "选择服务器",
|
||||
"serverLogo": "服务器图标",
|
||||
"serverOnline": "服务器在线",
|
||||
"serverOffline": "服务器离线",
|
||||
"yourServers": "您的 Coco-Servers",
|
||||
"addServer": "添加新服务器"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { RouterProvider } from "react-router-dom";
|
||||
|
||||
import { ThemeProvider } from "./contexts/ThemeContext";
|
||||
import { router } from "./routes/index";
|
||||
import './i18n';
|
||||
|
||||
import "./main.css";
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useEffect, useState } from "react";
|
||||
import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "@headlessui/react";
|
||||
import { Settings, Puzzle, Settings2, Info, Server } from "lucide-react";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import SettingsPanel from "@/components/Settings/SettingsPanel";
|
||||
import GeneralSettings from "@/components/Settings/GeneralSettings";
|
||||
@@ -11,6 +12,7 @@ import Footer from "@/components/Footer";
|
||||
import ApiDetails from "@/components/Common/ApiDetails";
|
||||
|
||||
function SettingsPage() {
|
||||
const { t } = useTranslation();
|
||||
const [defaultIndex, setDefaultIndex] = useState<number>(0);
|
||||
|
||||
const [searchParams] = useSearchParams();
|
||||
@@ -21,11 +23,11 @@ function SettingsPage() {
|
||||
}, [name]);
|
||||
|
||||
const tabs = [
|
||||
{ name: "General", icon: Settings },
|
||||
{ name: "Extensions", icon: Puzzle },
|
||||
{ name: "Connect", icon: Server },
|
||||
{ name: "Advanced", icon: Settings2 },
|
||||
{ name: "About", icon: Info },
|
||||
{ name: t('settings.tabs.general'), icon: Settings },
|
||||
{ name: t('settings.tabs.extensions'), icon: Puzzle },
|
||||
{ name: t('settings.tabs.connect'), icon: Server },
|
||||
{ name: t('settings.tabs.advanced'), icon: Settings2 },
|
||||
{ name: t('settings.tabs.about'), icon: Info },
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -71,7 +73,7 @@ function SettingsPage() {
|
||||
<TabPanel>
|
||||
<SettingsPanel title="">
|
||||
<div className="text-gray-600 dark:text-gray-400">
|
||||
Extensions settings content
|
||||
{t('settings.tabs.extensionsContent')}
|
||||
</div>
|
||||
</SettingsPanel>
|
||||
</TabPanel>
|
||||
@@ -81,7 +83,7 @@ function SettingsPage() {
|
||||
<TabPanel>
|
||||
<SettingsPanel title="">
|
||||
<div className="text-gray-600 dark:text-gray-400">
|
||||
Advanced Settings content
|
||||
{t('settings.tabs.advancedContent')}
|
||||
</div>
|
||||
</SettingsPanel>
|
||||
</TabPanel>
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { useEffect } from "react";
|
||||
import { Outlet, useLocation } from "react-router-dom";
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
|
||||
import { useAppStore } from '@/stores/appStore';
|
||||
import useEscape from "@/hooks/useEscape";
|
||||
import useSettingsWindow from "@/hooks/useSettingsWindow";
|
||||
|
||||
export default function Layout() {
|
||||
const location = useLocation();
|
||||
|
||||
function updateBodyClass(path: string) {
|
||||
const body = document.body;
|
||||
body.className = "";
|
||||
@@ -14,6 +18,7 @@ export default function Layout() {
|
||||
body.classList.add("input-body");
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
updateBodyClass(location.pathname);
|
||||
}, [location.pathname]);
|
||||
@@ -22,5 +27,23 @@ export default function Layout() {
|
||||
|
||||
useSettingsWindow();
|
||||
|
||||
const { i18n } = useTranslation();
|
||||
const language = useAppStore((state) => state.language);
|
||||
|
||||
useEffect(() => {
|
||||
if (language) {
|
||||
i18n.changeLanguage(language);
|
||||
}
|
||||
|
||||
const unlistenLanguageChange = listen('language-changed', (event: any) => {
|
||||
const { language } = event.payload;
|
||||
i18n.changeLanguage(language);
|
||||
});
|
||||
|
||||
return () => {
|
||||
unlistenLanguageChange.then(unlisten => unlisten());
|
||||
};
|
||||
}, []);
|
||||
|
||||
return <Outlet />;
|
||||
}
|
||||
|
||||
@@ -19,11 +19,12 @@ export type IAppStore = {
|
||||
// ssoServerID: string;
|
||||
// setSSOServerID: (ssoServerID: string) => void,
|
||||
|
||||
|
||||
endpoint: AppEndpoint,
|
||||
endpoint_http: string,
|
||||
endpoint_websocket: string,
|
||||
setEndpoint: (endpoint: AppEndpoint) => void,
|
||||
language: string;
|
||||
setLanguage: (language: string) => void;
|
||||
initializeListeners: () => void;
|
||||
};
|
||||
|
||||
@@ -62,6 +63,8 @@ export const useAppStore = create<IAppStore>()(
|
||||
endpoint_websocket
|
||||
});
|
||||
},
|
||||
language: "en",
|
||||
setLanguage: (language: string) => set({ language }),
|
||||
initializeListeners: () => {
|
||||
listen(ENDPOINT_CHANGE_EVENT, (event: any) => {
|
||||
const { endpoint, endpoint_http, endpoint_websocket } = event.payload;
|
||||
@@ -79,6 +82,7 @@ export const useAppStore = create<IAppStore>()(
|
||||
endpoint: state.endpoint,
|
||||
endpoint_http: state.endpoint_http,
|
||||
endpoint_websocket: state.endpoint_websocket,
|
||||
language: state.language,
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user