feat: add locales switch (#144)

* feat: add locales

* feat: add locales

* feat: add listen language
This commit is contained in:
BiggerRain
2025-02-18 09:40:00 +08:00
committed by GitHub
parent 4ba842f18b
commit e9ec1be42f
36 changed files with 1263 additions and 883 deletions

View File

@@ -15,6 +15,7 @@
"inputbox", "inputbox",
"katex", "katex",
"khtml", "khtml",
"languagedetector",
"localstorage", "localstorage",
"lucide", "lucide",
"maximizable", "maximizable",

View File

@@ -19,14 +19,15 @@
"@tauri-apps/plugin-http": "~2.0.1", "@tauri-apps/plugin-http": "~2.0.1",
"@tauri-apps/plugin-os": "^2.2.0", "@tauri-apps/plugin-os": "^2.2.0",
"@tauri-apps/plugin-shell": ">=2.0.0", "@tauri-apps/plugin-shell": ">=2.0.0",
"@tauri-apps/plugin-updater": "^2.3.0",
"@tauri-apps/plugin-websocket": "~2", "@tauri-apps/plugin-websocket": "~2",
"@tauri-apps/plugin-window": "2.0.0-alpha.1", "@tauri-apps/plugin-window": "2.0.0-alpha.1",
"@tauri-apps/plugin-updater": "^2.3.0",
"ahooks": "^3.8.4", "ahooks": "^3.8.4",
"axios": "^1.7.7", "axios": "^1.7.7",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"i18next": "^23.16.2", "i18next": "^23.16.2",
"i18next-browser-languagedetector": "^8.0.3",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"lucide-react": "^0.461.0", "lucide-react": "^0.461.0",
"mermaid": "^11.4.0", "mermaid": "^11.4.0",

23
pnpm-lock.yaml generated
View File

@@ -59,6 +59,9 @@ importers:
i18next: i18next:
specifier: ^23.16.2 specifier: ^23.16.2
version: 23.16.2 version: 23.16.2
i18next-browser-languagedetector:
specifier: ^8.0.3
version: 8.0.3
lodash: lodash:
specifier: ^4.17.21 specifier: ^4.17.21
version: 4.17.21 version: 4.17.21
@@ -554,46 +557,55 @@ packages:
resolution: {integrity: sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==} resolution: {integrity: sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.24.0': '@rollup/rollup-linux-arm-musleabihf@4.24.0':
resolution: {integrity: sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==} resolution: {integrity: sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.24.0': '@rollup/rollup-linux-arm64-gnu@4.24.0':
resolution: {integrity: sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==} resolution: {integrity: sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.24.0': '@rollup/rollup-linux-arm64-musl@4.24.0':
resolution: {integrity: sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==} resolution: {integrity: sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-linux-powerpc64le-gnu@4.24.0': '@rollup/rollup-linux-powerpc64le-gnu@4.24.0':
resolution: {integrity: sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==} resolution: {integrity: sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==}
cpu: [ppc64] cpu: [ppc64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-gnu@4.24.0': '@rollup/rollup-linux-riscv64-gnu@4.24.0':
resolution: {integrity: sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==} resolution: {integrity: sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-s390x-gnu@4.24.0': '@rollup/rollup-linux-s390x-gnu@4.24.0':
resolution: {integrity: sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==} resolution: {integrity: sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==}
cpu: [s390x] cpu: [s390x]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.24.0': '@rollup/rollup-linux-x64-gnu@4.24.0':
resolution: {integrity: sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==} resolution: {integrity: sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.24.0': '@rollup/rollup-linux-x64-musl@4.24.0':
resolution: {integrity: sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==} resolution: {integrity: sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-win32-arm64-msvc@4.24.0': '@rollup/rollup-win32-arm64-msvc@4.24.0':
resolution: {integrity: sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==} resolution: {integrity: sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==}
@@ -652,24 +664,28 @@ packages:
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
'@tauri-apps/cli-linux-arm64-musl@2.2.7': '@tauri-apps/cli-linux-arm64-musl@2.2.7':
resolution: {integrity: sha512-+8HZ+txff/Y3YjAh80XcLXcX8kpGXVdr1P8AfjLHxHdS6QD4Md+acSxGTTNbplmHuBaSHJvuTvZf9tU1eDCTDg==} resolution: {integrity: sha512-+8HZ+txff/Y3YjAh80XcLXcX8kpGXVdr1P8AfjLHxHdS6QD4Md+acSxGTTNbplmHuBaSHJvuTvZf9tU1eDCTDg==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
'@tauri-apps/cli-linux-x64-gnu@2.2.7': '@tauri-apps/cli-linux-x64-gnu@2.2.7':
resolution: {integrity: sha512-ahlSnuCnUntblp9dG7/w5ZWZOdzRFi3zl0oScgt7GF4KNAOEa7duADsxPA4/FT2hLRa0SvpqtD4IYFvCxoVv3Q==} resolution: {integrity: sha512-ahlSnuCnUntblp9dG7/w5ZWZOdzRFi3zl0oScgt7GF4KNAOEa7duADsxPA4/FT2hLRa0SvpqtD4IYFvCxoVv3Q==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
'@tauri-apps/cli-linux-x64-musl@2.2.7': '@tauri-apps/cli-linux-x64-musl@2.2.7':
resolution: {integrity: sha512-+qKAWnJRSX+pjjRbKAQgTdFY8ecdcu8UdJ69i7wn3ZcRn2nMMzOO2LOMOTQV42B7/Q64D1pIpmZj9yblTMvadA==} resolution: {integrity: sha512-+qKAWnJRSX+pjjRbKAQgTdFY8ecdcu8UdJ69i7wn3ZcRn2nMMzOO2LOMOTQV42B7/Q64D1pIpmZj9yblTMvadA==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
'@tauri-apps/cli-win32-arm64-msvc@2.2.7': '@tauri-apps/cli-win32-arm64-msvc@2.2.7':
resolution: {integrity: sha512-aa86nRnrwT04u9D9fhf5JVssuAZlUCCc8AjqQjqODQjMd4BMA2+d4K9qBMpEG/1kVh95vZaNsLogjEaqSTTw4A==} resolution: {integrity: sha512-aa86nRnrwT04u9D9fhf5JVssuAZlUCCc8AjqQjqODQjMd4BMA2+d4K9qBMpEG/1kVh95vZaNsLogjEaqSTTw4A==}
@@ -1426,6 +1442,9 @@ packages:
html-url-attributes@3.0.1: html-url-attributes@3.0.1:
resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==}
i18next-browser-languagedetector@8.0.3:
resolution: {integrity: sha512-beOOLArattPBc2YZG5IXGJytdYFgUR7cS8Wd6HT4IczIoWKgmTspOQ2yasaGklelVo5seLPmnEKvLHR+E/MdWQ==}
i18next@23.16.2: i18next@23.16.2:
resolution: {integrity: sha512-dFyxwLXxEQK32f6tITBMaRht25mZPJhQ0WbC0p3bO2mWBal9lABTMqSka5k+GLSRWLzeJBKDpH7BeIA9TZI7Jg==} resolution: {integrity: sha512-dFyxwLXxEQK32f6tITBMaRht25mZPJhQ0WbC0p3bO2mWBal9lABTMqSka5k+GLSRWLzeJBKDpH7BeIA9TZI7Jg==}
@@ -3607,6 +3626,10 @@ snapshots:
html-url-attributes@3.0.1: {} html-url-attributes@3.0.1: {}
i18next-browser-languagedetector@8.0.3:
dependencies:
'@babel/runtime': 7.25.9
i18next@23.16.2: i18next@23.16.2:
dependencies: dependencies:
'@babel/runtime': 7.25.9 '@babel/runtime': 7.25.9

View File

@@ -1,4 +1,5 @@
import React, { useEffect, useRef } from "react"; import React, { useEffect, useRef } from "react";
import { useTranslation } from "react-i18next";
interface AutoResizeTextareaProps { interface AutoResizeTextareaProps {
input: string; input: string;
@@ -11,6 +12,7 @@ const AutoResizeTextarea: React.FC<AutoResizeTextareaProps> = ({
setInput, setInput,
handleKeyDown, handleKeyDown,
}) => { }) => {
const { t } = useTranslation();
const textareaRef = useRef<HTMLTextAreaElement>(null); const textareaRef = useRef<HTMLTextAreaElement>(null);
useEffect(() => { useEffect(() => {
@@ -25,7 +27,7 @@ const AutoResizeTextarea: React.FC<AutoResizeTextareaProps> = ({
<textarea <textarea
ref={textareaRef} 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" 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} value={input}
onChange={(e) => setInput(e.target.value)} onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}

View File

@@ -1,15 +1,23 @@
import {forwardRef, memo, useCallback, useEffect, useImperativeHandle, useRef, useState,} from "react"; import {
forwardRef,
memo,
useCallback,
useEffect,
useImperativeHandle,
useRef,
useState,
} from "react";
import { MessageSquarePlus, PanelLeft } from "lucide-react"; import { MessageSquarePlus, PanelLeft } from "lucide-react";
import { isTauri } from "@tauri-apps/api/core"; import { isTauri } from "@tauri-apps/api/core";
import { useTranslation } from "react-i18next";
import { ChatMessage } from "./ChatMessage"; import { ChatMessage } from "./ChatMessage";
import type { Chat, Message } from "./types"; import type { Chat, Message } from "./types";
import {tauriFetch} from "../../api/tauriFetchClient"; import { tauriFetch } from "@/api/tauriFetchClient";
import {useWebSocket} from "../../hooks/useWebSocket"; import { useWebSocket } from "@/hooks/useWebSocket";
import {useChatStore} from "../../stores/chatStore"; import { useChatStore } from "@/stores/chatStore";
import {useWindows} from "../../hooks/useWindows"; import { useWindows } from "@/hooks/useWindows";
import { clientEnv } from "@/utils/env"; import { clientEnv } from "@/utils/env";
// import { useAppStore } from '@/stores/appStore'; // import { useAppStore } from '@/stores/appStore';
interface ChatAIProps { interface ChatAIProps {
@@ -26,13 +34,18 @@ export interface ChatAIRef {
reconnect: () => void; reconnect: () => void;
} }
const ChatAI = memo(forwardRef<ChatAIRef, ChatAIProps>( const ChatAI = memo(
({isTransitioned, changeInput, isSearchActive, isDeepThinkActive}, ref) => { forwardRef<ChatAIRef, ChatAIProps>(
(
{ isTransitioned, changeInput, isSearchActive, isDeepThinkActive },
ref
) => {
const { t } = useTranslation();
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
init: init, init: init,
cancelChat: cancelChat, cancelChat: cancelChat,
connected: connected, connected: connected,
reconnect: reconnect reconnect: reconnect,
})); }));
// const appStore = useAppStore(); // const appStore = useAppStore();
@@ -56,7 +69,7 @@ const ChatAI = memo(forwardRef<ChatAIRef, ChatAIProps>(
curIdRef.current = curId; curIdRef.current = curId;
const handleMessageChunk = useCallback((chunk: string) => { const handleMessageChunk = useCallback((chunk: string) => {
setCurMessage(prev => prev + chunk); setCurMessage((prev) => prev + chunk);
}, []); }, []);
// console.log("chat useWebSocket", clientEnv.COCO_WEBSOCKET_URL) // console.log("chat useWebSocket", clientEnv.COCO_WEBSOCKET_URL)
@@ -81,7 +94,7 @@ const ChatAI = memo(forwardRef<ChatAIRef, ChatAIProps>(
try { try {
const chunkData = JSON.parse(cleanedData); const chunkData = JSON.parse(cleanedData);
if (chunkData.reply_to_message === curIdRef.current) { if (chunkData.reply_to_message === curIdRef.current) {
handleMessageChunk(chunkData.message_chunk) handleMessageChunk(chunkData.message_chunk);
return chunkData.message_chunk; return chunkData.message_chunk;
} }
} catch (error) { } catch (error) {
@@ -93,8 +106,8 @@ const ChatAI = memo(forwardRef<ChatAIRef, ChatAIProps>(
); );
useEffect(() => { useEffect(() => {
setConnected(connected) setConnected(connected);
}, [connected]) }, [connected]);
const simulateAssistantResponse = useCallback(() => { const simulateAssistantResponse = useCallback(() => {
if (messages.length === 0 || !activeChat?._id) return; if (messages.length === 0 || !activeChat?._id) return;
@@ -236,7 +249,8 @@ const ChatAI = memo(forwardRef<ChatAIRef, ChatAIProps>(
async function openChatAI() { async function openChatAI() {
if (isTauri()) { if (isTauri()) {
createWin && createWin({ createWin &&
createWin({
label: "chat", label: "chat",
title: "Coco Chat", title: "Coco Chat",
dragDropEnabled: true, dragDropEnabled: true,
@@ -270,7 +284,6 @@ const ChatAI = memo(forwardRef<ChatAIRef, ChatAIProps>(
<PanelLeft className="h-4 w-4" /> <PanelLeft className="h-4 w-4" />
</button> </button>
<button <button
onClick={() => { onClick={() => {
createNewChat(); createNewChat();
@@ -282,8 +295,7 @@ const ChatAI = memo(forwardRef<ChatAIRef, ChatAIProps>(
</header> </header>
{/* Chat messages */} {/* Chat messages */}
<div <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">
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) => ( {activeChat?.messages?.map((message, index) => (
<ChatMessage <ChatMessage
key={message._id + index} key={message._id + index}
@@ -308,11 +320,23 @@ const ChatAI = memo(forwardRef<ChatAIRef, ChatAIProps>(
isTyping={!curChatEnd} isTyping={!curChatEnd}
/> />
) : null} ) : 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-[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 ref={messagesEndRef} /> <div ref={messagesEndRef} />
</div> </div>
</div> </div>
); );
} }
)); )
);
export default ChatAI; export default ChatAI;

View File

@@ -6,6 +6,7 @@ import {
useRef, useRef,
useEffect, useEffect,
} from "react"; } from "react";
import { useTranslation } from "react-i18next";
import AutoResizeTextarea from "./AutoResizeTextarea"; import AutoResizeTextarea from "./AutoResizeTextarea";
import StopIcon from "@/icons/Stop"; import StopIcon from "@/icons/Stop";
@@ -31,6 +32,7 @@ export function ChatInput({
isDeepThinkActive, isDeepThinkActive,
setIsDeepThinkActive, setIsDeepThinkActive,
}: ChatInputProps) { }: ChatInputProps) {
const { t } = useTranslation();
const [input, setInput] = useState(""); const [input, setInput] = useState("");
const textareaRef = useRef<HTMLTextAreaElement>(null); const textareaRef = useRef<HTMLTextAreaElement>(null);
@@ -120,7 +122,9 @@ export function ChatInput({
<button <button
type="button" type="button"
className={`h-5 px-2 inline-flex items-center border rounded-[10px] transition-colors relative ${ 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} onClick={DeepThinkClick}
> >
@@ -131,14 +135,20 @@ export function ChatInput({
: "text-[#333] dark:text-white" : "text-[#333] dark:text-white"
}`} }`}
/> />
<span className={isDeepThinkActive ? "text-[#0072FF]" : "dark:text-white"}> <span
Deep Think className={
isDeepThinkActive ? "text-[#0072FF]" : "dark:text-white"
}
>
{t("assistant.input.deepThink")}
</span> </span>
</button> </button>
<button <button
type="button" type="button"
className={`h-5 px-2 inline-flex items-center border rounded-[10px] transition-colors relative ${ 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} onClick={SearchClick}
> >
@@ -149,8 +159,12 @@ export function ChatInput({
: "text-[#333] dark:text-white" : "text-[#333] dark:text-white"
}`} }`}
/> />
<span className={isSearchActive ? "text-[#0072FF]" : "dark:text-white"}> <span
Search className={
isSearchActive ? "text-[#0072FF]" : "dark:text-white"
}
>
{t("assistant.input.search")}
</span> </span>
</button> </button>
{/* <button {/* <button

View File

@@ -6,6 +6,7 @@ import Markdown from "./Markdown";
import { formatThinkingMessage } from "@/utils/index"; import { formatThinkingMessage } from "@/utils/index";
import logoImg from "@/assets/icon.svg"; import logoImg from "@/assets/icon.svg";
import { SourceResult } from "./SourceResult"; import { SourceResult } from "./SourceResult";
import { useTranslation } from "react-i18next";
interface ChatMessageProps { interface ChatMessageProps {
message: Message; message: Message;
@@ -13,6 +14,7 @@ interface ChatMessageProps {
} }
export function ChatMessage({ message, isTyping }: ChatMessageProps) { export function ChatMessage({ message, isTyping }: ChatMessageProps) {
const { t } = useTranslation();
const [isThinkingExpanded, setIsThinkingExpanded] = useState(true); const [isThinkingExpanded, setIsThinkingExpanded] = useState(true);
const [responseTime, setResponseTime] = useState(0); const [responseTime, setResponseTime] = useState(0);
const startTimeRef = useRef<number | null>(null); 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]"> <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 ? (
{isAssistant ? "Coco AI" : ""} <img
src={logoImg}
className="w-6 h-6"
alt={t("assistant.message.logo")}
/>
) : null}
{isAssistant ? t("assistant.message.aiName") : ""}
</p> </p>
<div className="prose dark:prose-invert prose-sm max-w-none"> <div className="prose dark:prose-invert prose-sm max-w-none">
<div className="text-[#333] dark:text-[#d8d8d8] leading-relaxed"> <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]" /> <Brain className="w-4 h-4 animate-pulse text-[#999999]" />
<span className="text-xs text-[#999999] italic"> <span className="text-xs text-[#999999] italic">
AI is thinking... {t("assistant.message.thinking")}
</span> </span>
</> </>
) : ( ) : (
<> <>
<Brain className="w-4 h-4 text-[#999999]" /> <Brain className="w-4 h-4 text-[#999999]" />
<span className="text-xs text-[#999999]"> <span className="text-xs text-[#999999]">
Thought for {responseTime.toFixed(1)} seconds {t("assistant.message.thoughtTime", {
time: responseTime.toFixed(1),
})}
</span> </span>
</> </>
)} )}

View File

@@ -1,4 +1,6 @@
import { useTranslation } from "react-i18next";
import { MessageSquare, Plus } from "lucide-react"; import { MessageSquare, Plus } from "lucide-react";
import type { Chat } from "./types"; import type { Chat } from "./types";
interface SidebarProps { interface SidebarProps {
chats: Chat[]; chats: Chat[];
@@ -16,6 +18,8 @@ export function Sidebar({
onSelectChat, onSelectChat,
className = "", className = "",
}: SidebarProps) { }: SidebarProps) {
const { t } = useTranslation();
return ( return (
<div className={`h-full flex flex-col ${className}`}> <div className={`h-full flex flex-col ${className}`}>
<div className="p-4"> <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`} 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]`} /> <Plus className={`h-4 w-4 text-[#0072FF] dark:text-[#0072FF]`} />
New Chat {t("assistant.sidebar.newChat")}
</button> </button>
</div> </div>
<div className="flex-1 overflow-y-auto px-3 pb-3 space-y-2 custom-scrollbar"> <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} key={chat._id}
className={`group relative rounded-xl transition-all ${ className={`group relative rounded-xl transition-all ${
activeChat._id === chat._id activeChat._id === chat._id
? 'bg-gray-100/80 dark:bg-gray-700/50' ? "bg-gray-100/80 dark:bg-gray-700/50"
: 'hover:bg-gray-50/80 dark:hover:bg-gray-600/30' : "hover:bg-gray-50/80 dark:hover:bg-gray-600/30"
}`} }`}
> >
<button <button
@@ -44,15 +48,17 @@ export function Sidebar({
<MessageSquare <MessageSquare
className={`h-4 w-4 flex-shrink-0 ${ className={`h-4 w-4 flex-shrink-0 ${
activeChat._id === chat._id activeChat._id === chat._id
? 'text-[#0072FF] dark:text-[#0072FF]' ? "text-[#0072FF] dark:text-[#0072FF]"
: 'text-gray-400 dark:text-gray-500' : "text-gray-400 dark:text-gray-500"
}`} }`}
/> />
<span className={`truncate ${ <span
className={`truncate ${
activeChat._id === chat._id activeChat._id === chat._id
? 'text-gray-900 dark:text-white font-medium' ? "text-gray-900 dark:text-white font-medium"
: 'text-gray-600 dark:text-gray-300' : "text-gray-600 dark:text-gray-300"
}`}> }`}
>
{chat.title || chat._id} {chat.title || chat._id}
</span> </span>
</button> </button>

View File

@@ -5,6 +5,8 @@ import {
SquareArrowOutUpRight, SquareArrowOutUpRight,
} from "lucide-react"; } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next";
import { OpenURLWithBrowser } from "@/utils/index"; import { OpenURLWithBrowser } from "@/utils/index";
interface SourceResultProps { interface SourceResultProps {
@@ -21,6 +23,7 @@ interface SourceItem {
} }
export function SourceResult({ text }: SourceResultProps) { export function SourceResult({ text }: SourceResultProps) {
const { t } = useTranslation();
const [isSourceExpanded, setIsSourceExpanded] = useState(false); const [isSourceExpanded, setIsSourceExpanded] = useState(false);
if (!text?.includes("<Source")) { if (!text?.includes("<Source")) {
@@ -42,13 +45,11 @@ export function SourceResult({ text }: SourceResultProps) {
const sourceData = getSourceData(); const sourceData = getSourceData();
return ( return (
<div <div className={`mt-2 ${
className={`mt-2 ${
isSourceExpanded isSourceExpanded
? "rounded-lg overflow-hidden border border-[#E6E6E6] dark:border-[#272626]" ? "rounded-lg overflow-hidden border border-[#E6E6E6] dark:border-[#272626]"
: "" : ""
}`} }`}>
>
<button <button
onClick={() => setIsSourceExpanded((prev) => !prev)} onClick={() => setIsSourceExpanded((prev) => !prev)}
className={`inline-flex justify-between items-center gap-2 px-2 py-1 rounded-xl transition-colors ${ 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"> <div className="flex gap-2">
<Search className="w-4 h-4 text-[#999999] dark:text-[#999999]" /> <Search className="w-4 h-4 text-[#999999] dark:text-[#999999]" />
<span className="text-xs 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> </span>
</div> </div>
{isSourceExpanded ? ( {isSourceExpanded ? (

View File

@@ -15,6 +15,7 @@ import {
getCurrent as getCurrentDeepLinkUrls, getCurrent as getCurrentDeepLinkUrls,
} from "@tauri-apps/plugin-deep-link"; } from "@tauri-apps/plugin-deep-link";
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import { useTranslation } from "react-i18next";
import { UserProfile } from "./UserProfile"; import { UserProfile } from "./UserProfile";
import { DataSourcesList } from "./DataSourcesList"; import { DataSourcesList } from "./DataSourcesList";
@@ -26,6 +27,8 @@ import { useConnectStore } from "@/stores/connectStore";
import bannerImg from "@/assets/images/coco-cloud-banner.jpeg"; import bannerImg from "@/assets/images/coco-cloud-banner.jpeg";
export default function Cloud() { export default function Cloud() {
const { t } = useTranslation();
const SidebarRef = useRef<{ refreshData: () => void }>(null); const SidebarRef = useRef<{ refreshData: () => void }>(null);
const error = useAppStore((state) => state.error); const error = useAppStore((state) => state.error);
@@ -371,7 +374,7 @@ export default function Cloud() {
{currentService?.auth_provider?.sso?.url ? ( {currentService?.auth_provider?.sso?.url ? (
<div className="mb-8"> <div className="mb-8">
<h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4"> <h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
Account Information {t('cloud.accountInfo')}
</h2> </h2>
{currentService?.profile ? ( {currentService?.profile ? (
<UserProfile <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" className="px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors mb-3"
onClick={LoginClick} onClick={LoginClick}
> >
Login {t('cloud.login')}
</button> </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" 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 onClick={() => setLoading(false)} // Reset loading state
> >
Cancel {t('cloud.cancel')}
</button> </button>
<button <button
onClick={() => { onClick={() => {
@@ -423,7 +426,7 @@ export default function Cloud() {
) )
} }
> >
EULA | Privacy Policy {t('cloud.privacyPolicy')}
</button> </button>
</div> </div>
)} )}

View File

@@ -1,7 +1,8 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { ChevronLeft } from "lucide-react"; import { ChevronLeft } from "lucide-react";
import {useAppStore} from "@/stores/appStore"; import { useTranslation } from "react-i18next";
import { useAppStore } from "@/stores/appStore";
interface ConnectServiceProps { interface ConnectServiceProps {
setIsConnect: (isConnect: boolean) => void; setIsConnect: (isConnect: boolean) => void;
@@ -9,9 +10,10 @@ interface ConnectServiceProps {
} }
export function Connect({ setIsConnect, onAddServer }: ConnectServiceProps) { export function Connect({ setIsConnect, onAddServer }: ConnectServiceProps) {
const { t } = useTranslation();
const [endpointLink, setEndpointLink] = useState(""); const [endpointLink, setEndpointLink] = useState("");
const [refreshLoading, ] = useState(false); const [refreshLoading] = useState(false);
const [errorMessage, setErrorMessage] = useState(''); // State to store the error message const [errorMessage, setErrorMessage] = useState(""); // State to store the error message
const setError = useAppStore((state) => state.setError); 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 setIsConnect(true); // Only set as connected if the server is added successfully
} catch (err: any) { } catch (err: any) {
// Handle the error if something goes wrong // Handle the error if something goes wrong
const errorMessage = typeof err === 'string' ? err : err?.message || 'An unknown error occurred.'; const errorMessage =
typeof err === "string"
? err
: err?.message || "An unknown error occurred.";
setErrorMessage("ERR:" + errorMessage); setErrorMessage("ERR:" + errorMessage);
setError(errorMessage); setError(errorMessage);
console.error('Error:', errorMessage); console.error("Error:", errorMessage);
} }
}; };
// Function to close the error message // Function to close the error message
const closeError = () => { const closeError = () => {
setErrorMessage(''); setErrorMessage("");
}; };
return ( return (
@@ -52,16 +57,13 @@ export function Connect({ setIsConnect, onAddServer }: ConnectServiceProps) {
<ChevronLeft className="w-4 h-4" /> <ChevronLeft className="w-4 h-4" />
</button> </button>
<div className="text-xl text-[#101010] dark:text-white"> <div className="text-xl text-[#101010] dark:text-white">
Connecting to Your Coco-Server {t("cloud.connect.title")}
</div> </div>
</div> </div>
<div className="mb-8"> <div className="mb-8">
<p className="text-gray-600 dark:text-gray-400"> <p className="text-gray-600 dark:text-gray-400">
Running your own private instance of coco-server ensures complete control over {t("cloud.connect.description")}
your data, keeping it secure and accessible only within your environment.
Enjoy enhanced privacy, better performance, and seamless integration with your
internal systems.
</p> </p>
</div> </div>
@@ -71,14 +73,14 @@ export function Connect({ setIsConnect, onAddServer }: ConnectServiceProps) {
htmlFor="endpoint" htmlFor="endpoint"
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2.5" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2.5"
> >
Server address {t("cloud.connect.serverAddress")}
</label> </label>
<div className="flex gap-2"> <div className="flex gap-2">
<input <input
type="text" type="text"
id="endpoint" id="endpoint"
value={endpointLink} value={endpointLink}
placeholder="For example: https://coco.infini.cloud/" placeholder={t("cloud.connect.serverPlaceholder")}
onChange={(e) => setEndpointLink(e.target.value)} 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" 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"
/> />
@@ -87,7 +89,9 @@ export function Connect({ setIsConnect, onAddServer }: ConnectServiceProps) {
className="px-6 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors" 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> </button>
</div> </div>
</div> </div>
@@ -95,30 +99,28 @@ export function Connect({ setIsConnect, onAddServer }: ConnectServiceProps) {
{/*//TODO move to outer container, move error state to global*/} {/*//TODO move to outer container, move error state to global*/}
{errorMessage && ( {errorMessage && (
<div className="mb-8">
<div <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> <span>{errorMessage}</span>
<button <button
onClick={closeError} onClick={closeError}
style={{ style={{
background: 'none', background: "none",
border: 'none', border: "none",
color: 'red', color: "red",
cursor: 'pointer' cursor: "pointer",
}} }}
> ></button>
</button>
</div> </div>
</div> </div>
)} )}
</div> </div>
); );
} }

View File

@@ -1,3 +1,4 @@
import { useTranslation } from "react-i18next";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { RefreshCcw } from "lucide-react"; import { RefreshCcw } from "lucide-react";
@@ -7,6 +8,7 @@ import {useAppStore} from "@/stores/appStore";
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
export function DataSourcesList({ server }: { server: string }) { export function DataSourcesList({ server }: { server: string }) {
const { t } = useTranslation();
const datasourceData = useConnectStore((state) => state.datasourceData); const datasourceData = useConnectStore((state) => state.datasourceData);
const setError = useAppStore((state) => state.setError); const setError = useAppStore((state) => state.setError);
const [refreshLoading, setRefreshLoading] = useState(false); const [refreshLoading, setRefreshLoading] = useState(false);
@@ -24,9 +26,7 @@ export function DataSourcesList({server}: { server: string }) {
setError(err); setError(err);
throw err; // Propagate error back up throw err; // Propagate error back up
}) })
.finally(() => { .finally(() => {});
});
//fetch datasource data //fetch datasource data
invoke("get_datasources_by_server", { id: server }) invoke("get_datasources_by_server", { id: server })
@@ -38,8 +38,7 @@ export function DataSourcesList({server}: { server: string }) {
setError(err); setError(err);
throw err; // Propagate error back up throw err; // Propagate error back up
}) })
.finally(() => { .finally(() => {});
});
} }
async function getDatasourceData() { async function getDatasourceData() {
@@ -54,13 +53,13 @@ export function DataSourcesList({server}: { server: string }) {
} }
useEffect(() => { useEffect(() => {
getDatasourceData() getDatasourceData();
}, []) }, []);
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<h2 className="flex justify-between text-xl font-medium text-gray-900 dark:text-white"> <h2 className="flex justify-between text-xl font-medium text-gray-900 dark:text-white">
Data Source {t("cloud.dataSource.title")}
<button <button
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 rounded-[6px] bg-white dark:bg-gray-800 border border-[rgba(228,229,239,1)] dark:border-gray-700" 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()} onClick={() => getDatasourceData()}

View File

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

View File

@@ -1,3 +1,4 @@
import { useTranslation } from "react-i18next";
import { forwardRef } from "react"; import { forwardRef } from "react";
import { Plus } from "lucide-react"; import { Plus } from "lucide-react";
@@ -9,10 +10,13 @@ interface SidebarProps {
serverList: any[]; serverList: any[];
} }
export const Sidebar = forwardRef<{ refreshData: () => void; }, SidebarProps>( export const Sidebar = forwardRef<{ refreshData: () => void }, SidebarProps>(
({ onAddServer, serverList }, _ref) => { ({ onAddServer, serverList }, _ref) => {
const { t } = useTranslation();
const currentService = useConnectStore((state) => state.currentService); const currentService = useConnectStore((state) => state.currentService);
const setCurrentService = useConnectStore((state) => state.setCurrentService); const setCurrentService = useConnectStore(
(state) => state.setCurrentService
);
const onAddServerClick = () => { const onAddServerClick = () => {
onAddServer(); onAddServer();
@@ -24,8 +28,9 @@ export const Sidebar = forwardRef<{ refreshData: () => void; }, SidebarProps>(
<div <div
key={item?.id} 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 ${ 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 currentService?.id === item?.id
: "bg-gray-50 dark:bg-gray-900" // Default background color when not selected ? "dark:bg-blue-900/20 dark:bg-blue-900"
: "bg-gray-50 dark:bg-gray-900"
}`} }`}
onClick={() => setCurrentService(item)} onClick={() => setCurrentService(item)}
> >
@@ -58,7 +63,7 @@ export const Sidebar = forwardRef<{ refreshData: () => void; }, SidebarProps>(
</div> </div>
<div className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2"> <div className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">
Your Coco-Servers {t("cloud.sidebar.yourServers")}
</div> </div>
{/* Render Non-Built-in Servers */} {/* Render Non-Built-in Servers */}

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
import { useEffect, useRef, useImperativeHandle, forwardRef } from "react"; import { useEffect, useRef, useImperativeHandle, forwardRef } from "react";
import { useTranslation } from "react-i18next";
interface AutoResizeTextareaProps { interface AutoResizeTextareaProps {
input: string; input: string;
@@ -12,6 +13,7 @@ const AutoResizeTextarea = forwardRef<
{ reset: () => void; focus: () => void }, { reset: () => void; focus: () => void },
AutoResizeTextareaProps AutoResizeTextareaProps
>(({ input, setInput, handleKeyDown, connected }, ref) => { >(({ input, setInput, handleKeyDown, connected }, ref) => {
const { t } = useTranslation();
const textareaRef = useRef<HTMLTextAreaElement>(null); const textareaRef = useRef<HTMLTextAreaElement>(null);
useEffect(() => { useEffect(() => {
@@ -47,8 +49,8 @@ const AutoResizeTextarea = forwardRef<
autoCapitalize="none" autoCapitalize="none"
spellCheck="false" 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" 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 ..." : ""} placeholder={connected ? t('search.textarea.placeholder') : ""}
aria-label="Ask whatever you want ..." aria-label={t('search.textarea.ariaLabel')}
value={input} value={input}
onChange={(e) => setInput(e.target.value)} onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => handleKeyDown?.(e)} onKeyDown={(e) => handleKeyDown?.(e)}

View File

@@ -1,4 +1,5 @@
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next";
import { formatter } from "@/utils/index"; import { formatter } from "@/utils/index";
import TypeIcon from "@/components/Common/Icons/TypeIcon"; import TypeIcon from "@/components/Common/Icons/TypeIcon";
@@ -8,10 +9,12 @@ interface DocumentDetailProps {
} }
export const DocumentDetail: React.FC<DocumentDetailProps> = ({ document }) => { export const DocumentDetail: React.FC<DocumentDetailProps> = ({ document }) => {
const { t } = useTranslation();
return ( return (
<div className="p-4"> <div className="p-4">
<div className="font-normal text-xs text-[#666] dark:text-[#999] mb-2"> <div className="font-normal text-xs text-[#666] dark:text-[#999] mb-2">
Details {t('search.document.details')}
</div> </div>
{/* <div className="mb-4"> {/* <div className="mb-4">
@@ -30,65 +33,64 @@ export const DocumentDetail: React.FC<DocumentDetailProps> = ({ document }) => {
<div className="py-4 mt-4"> <div className="py-4 mt-4">
<div className="flex justify-between flex-wrap font-normal text-xs mb-2.5"> <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"> <div className="text-[#333] dark:text-[#D8D8D8] text-right w-60 break-words">
{document?.title || "-"} {document?.title || "-"}
</div> </div>
</div> </div>
<div className="flex justify-between flex-wrap font-normal text-xs mb-2.5"> <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"> <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" /> <TypeIcon item={document} className="w-4 h-4 mr-1" />
{document?.source?.name || "-"} {document?.source?.name || "-"}
</div> </div>
</div> </div>
{/* <div className="flex justify-between font-normal text-xs mb-2.5">
<div className="text-[#666]">Where</div> {document?.updated && (
<div className="text-[#333] dark:text-[#D8D8D8] text-right w-56 break-words">
-
</div>
</div> */}
{document?.updated ? (
<div className="flex justify-between flex-wrap font-normal text-xs mb-2.5"> <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"> <div className="text-[#333] dark:text-[#D8D8D8] text-right w-56 break-words">
{document?.updated || "-"} {document?.updated || "-"}
</div> </div>
</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="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"> <div className="text-[#333] dark:text-[#D8D8D8] text-right w-56 break-words">
{document?.last_updated_by?.user?.username || "-"} {document?.last_updated_by?.user?.username || "-"}
</div> </div>
</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="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"> <div className="text-[#333] dark:text-[#D8D8D8] text-right w-56 break-words">
{document?.owner?.username || "-"} {document?.owner?.username || "-"}
</div> </div>
</div> </div>
) : null} )}
{document?.type ? (
{document?.type && (
<div className="flex justify-between flex-wrap font-normal text-xs mb-2.5"> <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"> <div className="text-[#333] dark:text-[#D8D8D8] text-right w-56 break-words">
{document?.type || "-"} {document?.type || "-"}
</div> </div>
</div> </div>
) : null} )}
{document?.size ? (
{document?.size && (
<div className="flex justify-between flex-wrap font-normal text-xs mb-2.5"> <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"> <div className="text-[#333] dark:text-[#D8D8D8] text-right w-56 break-words">
{formatter.bytes(document?.size || 0)} {formatter.bytes(document?.size || 0)}
</div> </div>
</div> </div>
) : null} )}
</div> </div>
</div> </div>
); );

View File

@@ -2,6 +2,7 @@ import React, { useState, useRef, useEffect, useCallback } from "react";
import { useInfiniteScroll } from "ahooks"; import { useInfiniteScroll } from "ahooks";
import { isTauri, invoke } from "@tauri-apps/api/core"; import { isTauri, invoke } from "@tauri-apps/api/core";
import { open } from "@tauri-apps/plugin-shell"; import { open } from "@tauri-apps/plugin-shell";
import { useTranslation } from "react-i18next";
import { useSearchStore } from "@/stores/searchStore"; import { useSearchStore } from "@/stores/searchStore";
import { SearchHeader } from "./SearchHeader"; import { SearchHeader } from "./SearchHeader";
@@ -28,6 +29,7 @@ export const DocumentList: React.FC<DocumentListProps> = ({
viewMode, viewMode,
setViewMode, setViewMode,
}) => { }) => {
const { t } = useTranslation();
const sourceData = useSearchStore((state) => state.sourceData); const sourceData = useSearchStore((state) => state.sourceData);
const [selectedItem, setSelectedItem] = useState<number | null>(null); const [selectedItem, setSelectedItem] = useState<number | null>(null);
@@ -200,11 +202,9 @@ export const DocumentList: React.FC<DocumentListProps> = ({
}, [selectedItem]); }, [selectedItem]);
return ( return (
<div <div className={`border-r border-gray-200 dark:border-gray-700 flex flex-col h-full ${
className={`border-r border-gray-200 dark:border-gray-700 flex flex-col h-full ${
viewMode === "list" ? "w-[100%]" : "w-[50%]" viewMode === "list" ? "w-[100%]" : "w-[50%]"
}`} }`}>
>
<div className="px-2 flex-shrink-0"> <div className="px-2 flex-shrink-0">
<SearchHeader <SearchHeader
total={total} total={total}
@@ -213,10 +213,7 @@ export const DocumentList: React.FC<DocumentListProps> = ({
/> />
</div> </div>
<div <div ref={containerRef} className="flex-1 overflow-y-auto custom-scrollbar">
ref={containerRef}
className="flex-1 overflow-y-auto custom-scrollbar"
>
{data?.list.map((hit: any, index: number) => { {data?.list.map((hit: any, index: number) => {
const isSelected = selectedItem === index; const isSelected = selectedItem === index;
const item = hit.document; const item = hit.document;
@@ -246,7 +243,7 @@ export const DocumentList: React.FC<DocumentListProps> = ({
{loading && ( {loading && (
<div className="flex justify-center py-4"> <div className="flex justify-center py-4">
<span>Loading...</span> <span>{t('search.list.loading')}</span>
</div> </div>
)} )}
@@ -255,9 +252,13 @@ export const DocumentList: React.FC<DocumentListProps> = ({
data-tauri-drag-region data-tauri-drag-region
className="h-full w-full flex flex-col items-center" 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]"> <div className="mt-4 text-sm text-[#999] dark:text-[#666]">
No Results {t('search.list.noResults')}
</div> </div>
</div> </div>
)} )}

View File

@@ -1,5 +1,6 @@
import { ArrowDown01, Command, CornerDownLeft } from "lucide-react"; import { ArrowDown01, Command, CornerDownLeft } from "lucide-react";
import { emit } from "@tauri-apps/api/event"; import { emit } from "@tauri-apps/api/event";
import { useTranslation } from "react-i18next";
import logoImg from "@/assets/icon.svg"; import logoImg from "@/assets/icon.svg";
import { useSearchStore } from "@/stores/searchStore"; import { useSearchStore } from "@/stores/searchStore";
@@ -12,6 +13,7 @@ interface FooterProps {
} }
export default function Footer({}: FooterProps) { export default function Footer({}: FooterProps) {
const { t } = useTranslation();
const sourceData = useSearchStore((state) => state.sourceData); const sourceData = useSearchStore((state) => state.sourceData);
function openSetting() { function openSetting() {
@@ -32,23 +34,18 @@ export default function Footer({}: FooterProps) {
src={logoImg} src={logoImg}
className="w-4 h-4 cursor-pointer" className="w-4 h-4 cursor-pointer"
onClick={openSetting} onClick={openSetting}
alt={t('search.footer.logoAlt')}
/> />
)} )}
<span className="text-xs text-gray-500 dark:text-gray-400"> <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> </span>
</div> </div>
{/* {name ? (
<div className="flex gap-2 items-center text-[#666] text-xs">
<AppWindowMac className="w-5 h-5" /> {name}
</div>
) : null} */}
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="gap-1 flex items-center text-[#666] dark:text-[#666] text-xs"> <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"> <kbd className="coco-modal-footer-commands-key pr-1">
{isMac ? ( {isMac ? (
<Command className="w-3 h-3" /> <Command className="w-3 h-3" />
@@ -64,7 +61,7 @@ export default function Footer({}: FooterProps) {
</kbd> </kbd>
</div> </div>
<div className="flex items-center text-[#666] dark:text-[#666] text-xs"> <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"> <kbd className="coco-modal-footer-commands-key pr-1">
<CornerDownLeft className="w-3 h-3" /> <CornerDownLeft className="w-3 h-3" />
</kbd> </kbd>

View File

@@ -2,6 +2,7 @@ import { ArrowBigLeft, Search, Send, Globe, Brain } from "lucide-react";
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import { listen } from "@tauri-apps/api/event"; import { listen } from "@tauri-apps/api/event";
import { invoke, isTauri } from "@tauri-apps/api/core"; import { invoke, isTauri } from "@tauri-apps/api/core";
import { useTranslation } from "react-i18next";
import ChatSwitch from "@/components/Common/ChatSwitch"; import ChatSwitch from "@/components/Common/ChatSwitch";
import AutoResizeTextarea from "./AutoResizeTextarea"; import AutoResizeTextarea from "./AutoResizeTextarea";
@@ -40,6 +41,9 @@ export default function ChatInput({
isDeepThinkActive, isDeepThinkActive,
setIsDeepThinkActive, setIsDeepThinkActive,
}: ChatInputProps) { }: ChatInputProps) {
const { t } = useTranslation();
const showTooltip = useAppStore( const showTooltip = useAppStore(
(state: { showTooltip: boolean }) => state.showTooltip (state: { showTooltip: boolean }) => state.showTooltip
); );
@@ -259,7 +263,7 @@ export default function ChatInput({
autoCapitalize="none" autoCapitalize="none"
spellCheck="false" 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" 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} value={inputValue}
onChange={(e) => { onChange={(e) => {
onSend(e.target.value); onSend(e.target.value);
@@ -347,12 +351,12 @@ export default function ChatInput({
{!connected && isChatMode ? ( {!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"> <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 <div
className="w-[96px] h-[24px] bg-[#0061FF] rounded-[12px] font-normal text-xs text-white flex items-center justify-center cursor-pointer" className="w-[96px] h-[24px] bg-[#0061FF] rounded-[12px] font-normal text-xs text-white flex items-center justify-center cursor-pointer"
onClick={ReconnectClick} onClick={ReconnectClick}
> >
Reconnect ({countdown}) {t('search.input.reconnect')} ({countdown})
</div> </div>
</div> </div>
) : null} ) : null}
@@ -376,7 +380,7 @@ export default function ChatInput({
}`} }`}
/> />
<span className={isDeepThinkActive ? "text-[#0072FF]" : ""}> <span className={isDeepThinkActive ? "text-[#0072FF]" : ""}>
Deep Think {t('search.input.deepThink')}
</span> </span>
</button> </button>
<button <button
@@ -391,7 +395,7 @@ export default function ChatInput({
}`} }`}
/> />
<span className={isSearchActive ? "text-[#0072FF]" : ""}> <span className={isSearchActive ? "text-[#0072FF]" : ""}>
Search {t('search.input.search')}
</span> </span>
</button> </button>
{/*<button*/} {/*<button*/}

View File

@@ -14,9 +14,8 @@ export default function ListRight({
isSelected, isSelected,
showIndex, showIndex,
currentIndex, currentIndex,
goToTwoPage goToTwoPage,
}: ListRightProps) { }: ListRightProps) {
return ( return (
<div className="flex-1 text-right min-w-[160px] h-full pl-5 text-[12px] flex gap-2 items-center justify-end relative"> <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 : ( {item?.rich_categories ? null : (
@@ -49,45 +48,59 @@ export default function ListRight({
}} }}
/> />
<span <span
className={`${isSelected ? "text-[#C8C8C8]" : "text-[#666]"} text-right truncate`} className={`${
isSelected ? "text-[#C8C8C8]" : "text-[#666]"
} text-right truncate`}
> >
{item?.rich_categories?.map( {item?.rich_categories?.map((rich_item: any, index: number) => {
(rich_item: any, index: number) => {
if ( if (
item?.rich_categories.length > 2 && item?.rich_categories.length > 2 &&
index === item?.rich_categories.length - 1 index === item?.rich_categories.length - 1
) )
return ""; return "";
return (index !== 0 ? "/" : "") + rich_item?.label; return (index !== 0 ? "/" : "") + rich_item?.label;
} })}
)}
</span> </span>
{item?.rich_categories.length > 2 ? ( {item?.rich_categories.length > 2 ? (
<span className={`${isSelected ? "text-[#C8C8C8]" : "text-[#666]"} text-right truncate`}> <span
className={`${
isSelected ? "text-[#C8C8C8]" : "text-[#666]"
} text-right truncate`}
>
{"/" + item?.rich_categories?.at(-1)?.label} {"/" + item?.rich_categories?.at(-1)?.label}
</span> </span>
) : null} ) : null}
</div> </div>
) : item?.category || item?.subcategory ? ( ) : item?.category || item?.subcategory ? (
<span <span
className={`text-[12px] truncate ${isSelected ? "text-[#DCDCDC]" : "text-[#999] dark:text-[#666]" className={`text-[12px] truncate ${
isSelected ? "text-[#DCDCDC]" : "text-[#999] dark:text-[#666]"
}`} }`}
> >
{(item?.category || "") + (item?.subcategory ? `/${item?.subcategory}` : "")} {(item?.category || "") +
(item?.subcategory ? `/${item?.subcategory}` : "")}
</span> </span>
) : ( ) : (
<span <span
className={`text-[12px] truncate ${isSelected ? "text-[#DCDCDC]" : "text-[#999] dark:text-[#666]" 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 || ""} {item?.last_updated_by?.user?.username ||
item?.owner?.username ||
item?.updated ||
item?.created ||
item?.type ||
""}
</span> </span>
)} )}
{isSelected ? ( {isSelected ? (
<div <div
className={`absolute ${showIndex && currentIndex < 10 ? "right-7" : "right-0" className={`absolute ${
} 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 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_#950599]"
: "shadow-[-6px_0px_6px_2px_#fff] dark:shadow-[-6px_0px_6px_2px_#000]" : "shadow-[-6px_0px_6px_2px_#fff] dark:shadow-[-6px_0px_6px_2px_#000]"
}`} }`}
@@ -98,7 +111,8 @@ export default function ListRight({
{showIndex && currentIndex < 10 ? ( {showIndex && currentIndex < 10 ? (
<div <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 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_#950599]"
: "shadow-[-6px_0px_6px_2px_#fff] dark:shadow-[-6px_0px_6px_2px_#000]" : "shadow-[-6px_0px_6px_2px_#fff] dark:shadow-[-6px_0px_6px_2px_#000]"
}`} }`}

View File

@@ -2,6 +2,7 @@ import { useEffect, useState, useCallback, useRef } from "react";
import { Command } from "lucide-react"; import { Command } from "lucide-react";
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
// import { isTauri } from "@tauri-apps/api/core"; // import { isTauri } from "@tauri-apps/api/core";
import { useTranslation } from "react-i18next";
import DropdownList from "./DropdownList"; import DropdownList from "./DropdownList";
import Footer from "./Footer"; import Footer from "./Footer";
@@ -18,6 +19,7 @@ interface SearchProps {
} }
function Search({ isChatMode, input }: SearchProps) { function Search({ isChatMode, input }: SearchProps) {
const { t } = useTranslation();
const sourceData = useSearchStore((state) => state.sourceData); const sourceData = useSearchStore((state) => state.sourceData);
const [IsError, setIsError] = useState<boolean>(false); 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" /> <img src={noDataImg} alt="no-data" className="w-16 h-16 mt-24" />
<div className="mt-4 text-sm text-[#999] dark:text-[#666]"> <div className="mt-4 text-sm text-[#999] dark:text-[#666]">
No Results {t('search.main.noResults')}
</div> </div>
<div className="mt-10 text-sm text-[#333] dark:text-[#D8D8D8] flex"> <div className="mt-10 text-sm text-[#333] dark:text-[#D8D8D8] flex">
Ask Coco AI {t('search.main.askCoco')}
{isMac ? ( {isMac ? (
<span className="ml-3 w-5 h-5 rounded-[6px] border border-[#D8D8D8] flex justify-center items-center"> <span className="ml-3 w-5 h-5 rounded-[6px] border border-[#D8D8D8] flex justify-center items-center">
<Command className="w-3 h-3" /> <Command className="w-3 h-3" />

View File

@@ -1,5 +1,6 @@
import React from "react"; import React from "react";
import { AlignLeft, Columns2 } from "lucide-react"; import { AlignLeft, Columns2 } from "lucide-react";
import { useTranslation } from "react-i18next";
interface SearchHeaderProps { interface SearchHeaderProps {
total: number; total: number;
@@ -12,14 +13,16 @@ export const SearchHeader: React.FC<SearchHeaderProps> = ({
viewMode, viewMode,
setViewMode, setViewMode,
}) => { }) => {
const { t } = useTranslation();
return ( return (
<div className="flex items-center justify-between py-1"> <div className="flex items-center justify-between py-1">
<div className="text-xs text-gray-600 dark:text-gray-400"> <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"> <span className="px-1 font-medium text-gray-900 dark:text-gray-100">
{total} {total}
</span> </span>
results {t('search.header.results')}
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<div className="flex bg-gray-100 dark:bg-gray-800 rounded-lg p-0.5"> <div className="flex bg-gray-100 dark:bg-gray-800 rounded-lg p-0.5">

View File

@@ -1,4 +1,5 @@
import { Globe, Github } from "lucide-react"; import { Globe, Github } from "lucide-react";
import { useTranslation } from "react-i18next";
import { useTheme } from "@/contexts/ThemeContext"; import { useTheme } from "@/contexts/ThemeContext";
import { OpenURLWithBrowser } from "@/utils"; import { OpenURLWithBrowser } from "@/utils";
@@ -6,30 +7,41 @@ import logoLight from "@/assets/images/logo-text-light.svg";
import logoDark from "@/assets/images/logo-text-dark.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 { theme } = useTheme();
const logo = theme === 'dark' ? logoDark : logoLight const logo = theme === "dark" ? logoDark : logoLight;
return ( return (
<div className="flex justify-center items-center flex-col h-[calc(100vh-170px)]"> <div className="flex justify-center items-center flex-col h-[calc(100vh-170px)]">
<div> <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>
<div className="mt-8 font-medium text-gray-900 dark:text-gray-100"> <div className="mt-8 font-medium text-gray-900 dark:text-gray-100">
Search, Connect, CollaborateAll in one place {t('settings.about.slogan')}
</div> </div>
<div className="flex justify-center items-center mt-10"> <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
<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> 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>
<div className="mt-8 text-sm text-gray-500 dark:text-gray-400"> <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>
<div className="mt-4 text-sm text-gray-500 dark:text-gray-400"> <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>
</div> </div>
) );
} }

View File

@@ -1,5 +1,6 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { Globe } from "lucide-react"; import { Globe } from "lucide-react";
import { useTranslation } from "react-i18next";
import SettingsItem from "./SettingsItem"; import SettingsItem from "./SettingsItem";
import { useAppStore } from "@/stores/appStore"; import { useAppStore } from "@/stores/appStore";
@@ -12,6 +13,7 @@ const ENDPOINTS = [
]; ];
export default function AdvancedSettings() { export default function AdvancedSettings() {
const { t } = useTranslation();
const endpoint = useAppStore(state => state.endpoint); const endpoint = useAppStore(state => state.endpoint);
const setEndpoint = useAppStore(state => state.setEndpoint); const setEndpoint = useAppStore(state => state.setEndpoint);
@@ -25,20 +27,18 @@ export default function AdvancedSettings() {
<div className="space-y-8"> <div className="space-y-8">
<div> <div>
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4"> <h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Advanced Settings {t('settings.advanced.title')}
</h2> </h2>
<div className="space-y-6"> <div className="space-y-6">
<SettingsItem <SettingsItem
icon={Globe} icon={Globe}
title="API Endpoint" title={t('settings.advanced.endpoint.title')}
description="Domain name for interface and websocket" description={t('settings.advanced.endpoint.description')}
> >
<div className={`p-4 rounded-lg`}> <div className={`p-4 rounded-lg`}>
<select <select
value={endpoint} value={endpoint}
onChange={(e) => onChange={(e) => onChangeEndpoint(e.target.value as AppEndpoint)}
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`} 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 }) => ( {ENDPOINTS.map(({ value, label }) => (

View File

@@ -8,12 +8,15 @@ import {
Power, Power,
Tags, Tags,
// Trash2, // Trash2,
Globe,
} from "lucide-react"; } from "lucide-react";
import { useTranslation } from "react-i18next";
import { isTauri, invoke } from "@tauri-apps/api/core"; import { isTauri, invoke } from "@tauri-apps/api/core";
import { import {
isEnabled, isEnabled,
// enable, disable // enable, disable
} from "@tauri-apps/plugin-autostart"; } from "@tauri-apps/plugin-autostart";
import { emit } from '@tauri-apps/api/event';
import SettingsItem from "./SettingsItem"; import SettingsItem from "./SettingsItem";
import SettingsToggle from "./SettingsToggle"; import SettingsToggle from "./SettingsToggle";
@@ -34,6 +37,7 @@ export function ThemeOption({
theme: AppTheme; theme: AppTheme;
}) { }) {
const { theme: currentTheme, changeTheme } = useTheme(); const { theme: currentTheme, changeTheme } = useTheme();
const { t } = useTranslation();
const isSelected = currentTheme === theme; const isSelected = currentTheme === theme;
@@ -45,28 +49,26 @@ export function ThemeOption({
? "border-blue-500 bg-blue-50 dark:bg-blue-900/20" ? "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" : "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`} } flex flex-col items-center justify-center space-y-2 transition-all`}
title={title}
> >
<Icon className={`w-6 h-6 ${isSelected ? "text-blue-500" : ""}`} /> <Icon className={`w-6 h-6 ${isSelected ? "text-blue-500" : ""}`} />
<span <span className={`text-sm font-medium ${isSelected ? "text-blue-500" : ""}`}>
className={`text-sm font-medium ${isSelected ? "text-blue-500" : ""}`} {t(`settings.appearance.${theme}`)}
>
{title}
</span> </span>
</button> </button>
); );
} }
export default function GeneralSettings() { export default function GeneralSettings() {
const { t, i18n } = useTranslation();
const [launchAtLogin, setLaunchAtLogin] = useState(true); const [launchAtLogin, setLaunchAtLogin] = useState(true);
const showTooltip = useAppStore((state) => state.showTooltip); const showTooltip = useAppStore((state) => state.showTooltip);
const setShowTooltip = useAppStore((state) => state.setShowTooltip); 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 () => { const fetchAutoStartStatus = async () => {
if (isTauri()) { if (isTauri()) {
try { try {
@@ -78,9 +80,6 @@ export default function GeneralSettings() {
} }
}; };
fetchAutoStartStatus();
}, []);
const enableAutoStart = async () => { const enableAutoStart = async () => {
if (isTauri()) { if (isTauri()) {
try { try {
@@ -118,7 +117,11 @@ export default function GeneralSettings() {
} }
useEffect(() => { useEffect(() => {
fetchAutoStartStatus();
getCurrentShortcut(); getCurrentShortcut();
if (language) {
i18n.changeLanguage(language);
}
}, []); }, []);
const changeShortcut = (key: Shortcut) => { const changeShortcut = (key: Shortcut) => {
@@ -162,31 +165,42 @@ export default function GeneralSettings() {
// useAppStore.persist.clearStorage(); // useAppStore.persist.clearStorage();
// }, [endpoint]); // }, [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 ( return (
<div className="space-y-8"> <div className="space-y-8">
<div> <div>
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4"> <h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
General Settings {t('settings.general')}
</h2> </h2>
<div className="space-y-6"> <div className="space-y-6">
<SettingsItem <SettingsItem
icon={Power} icon={Power}
title="Startup" title={t('settings.startup.title')}
description="Automatically start Coco when you login" description={t('settings.startup.description')}
> >
<SettingsToggle <SettingsToggle
checked={launchAtLogin} checked={launchAtLogin}
onChange={(value) => onChange={(value) => value ? enableAutoStart() : disableAutoStart()}
value ? enableAutoStart() : disableAutoStart() label={t('settings.startup.toggle')}
}
label="Launch at login"
/> />
</SettingsItem> </SettingsItem>
<SettingsItem <SettingsItem
icon={Command} icon={Command}
title="Coco Hotkey" title={t('settings.hotkey.title')}
description="Global shortcut to open Coco" description={t('settings.hotkey.description')}
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<ShortcutItem <ShortcutItem
@@ -200,75 +214,47 @@ export default function GeneralSettings() {
</div> </div>
</SettingsItem> </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 <SettingsItem
icon={Palette} icon={Palette}
title="Appearance" title={t('settings.appearance.title')}
description="Choose your preferred theme" description={t('settings.appearance.description')}
> >
<div></div> <div></div>
</SettingsItem> </SettingsItem>
<div className="grid grid-cols-3 gap-4"> <div className="grid grid-cols-3 gap-4">
<ThemeOption icon={Sun} title="Light" theme="light" /> <ThemeOption icon={Sun} title={t('settings.appearance.light')} theme="light" />
<ThemeOption icon={Moon} title="Dark" theme="dark" /> <ThemeOption icon={Moon} title={t('settings.appearance.dark')} theme="dark" />
<ThemeOption icon={Monitor} title="Auto" theme="auto" /> <ThemeOption icon={Monitor} title={t('settings.appearance.auto')} theme="auto" />
</div> </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 <SettingsItem
icon={Tags} icon={Tags}
title="Tooltip" title={t('settings.tooltip.title')}
description="Tooltip display for shortcut keys" description={t('settings.tooltip.description')}
> >
<SettingsToggle <SettingsToggle
checked={showTooltip} checked={showTooltip}
onChange={(value) => setShowTooltip(value)} onChange={(value) => setShowTooltip(value)}
label="Tooltip display" label={t('settings.tooltip.toggle')}
/> />
</SettingsItem> </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> </div>
</div> </div>

View File

@@ -1,5 +1,8 @@
import { formatKey, sortKeys } from "@/utils/keyboardUtils";
import { X } from "lucide-react"; import { X } from "lucide-react";
import { useTranslation } from "react-i18next";
import { formatKey, sortKeys } from "@/utils/keyboardUtils";
interface ShortcutItemProps { interface ShortcutItemProps {
shortcut: string[]; shortcut: string[];
isEditing: boolean; isEditing: boolean;
@@ -17,6 +20,8 @@ export function ShortcutItem({
onSave, onSave,
onCancel, onCancel,
}: ShortcutItemProps) { }: ShortcutItemProps) {
const { t } = useTranslation();
const renderKeys = (keys: string[]) => { const renderKeys = (keys: string[]) => {
const sortedKeys = sortKeys(keys); const sortedKeys = sortKeys(keys);
return sortedKeys.map((key, index) => ( return sortedKeys.map((key, index) => (
@@ -30,9 +35,7 @@ export function ShortcutItem({
}; };
return ( return (
<div <div className={`flex items-center justify-between p-4 rounded-lg bg-gray-50 dark:bg-gray-700`}>
className={`flex items-center justify-between p-4 rounded-lg bg-gray-50 dark:bg-gray-700`}
>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
{isEditing ? ( {isEditing ? (
<> <>
@@ -41,7 +44,7 @@ export function ShortcutItem({
renderKeys(currentKeys) renderKeys(currentKeys)
) : ( ) : (
<span className={`italic text-gray-500 dark:text-gray-400`}> <span className={`italic text-gray-500 dark:text-gray-400`}>
Press keys... {t('settings.shortcut.pressKeys')}
</span> </span>
)} )}
</div> </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 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`} disabled:opacity-50 disabled:cursor-not-allowed`}
> >
Save {t('settings.shortcut.save')}
</button> </button>
<button <button
onClick={onCancel} onClick={onCancel}
@@ -69,7 +72,7 @@ export function ShortcutItem({
onClick={onEdit} 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`} 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> </button>
</> </>
)} )}

View File

@@ -1,10 +1,14 @@
import i18n from "i18next"; import i18n from "i18next";
import { initReactI18next } from "react-i18next"; import { initReactI18next } from "react-i18next";
import LanguageDetector from 'i18next-browser-languagedetector';
import enTranslation from "./locales/en/translation.json"; import enTranslation from "./locales/en/translation.json";
import zhTranslation from "./locales/zh/translation.json"; import zhTranslation from "./locales/zh/translation.json";
i18n.use(initReactI18next).init({ i18n
.use(LanguageDetector)
.use(initReactI18next)
.init({
resources: { resources: {
en: { en: {
translation: enTranslation, translation: enTranslation,

View File

@@ -1,7 +1,165 @@
{ {
"welcome": "Welcome to Coco App", "settings": {
"home": "Home", "general": "General Settings",
"settings": "Settings", "startup": {
"activeTheme": "Current theme:", "title": "Startup",
"InputMessage": "Input your message here..." "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"
}
}
} }

View File

@@ -1,7 +1,165 @@
{ {
"welcome": "欢迎使用 Coco App", "settings": {
"home": "主页", "general": "通用设置",
"settings": "设置", "startup": {
"activeTheme": "当前主题:", "title": "启动项",
"InputMessage": "在此输入您的消息..." "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": "添加新服务器"
}
}
} }

View File

@@ -4,6 +4,7 @@ import { RouterProvider } from "react-router-dom";
import { ThemeProvider } from "./contexts/ThemeContext"; import { ThemeProvider } from "./contexts/ThemeContext";
import { router } from "./routes/index"; import { router } from "./routes/index";
import './i18n';
import "./main.css"; import "./main.css";

View File

@@ -2,6 +2,7 @@ import { useEffect, useState } from "react";
import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "@headlessui/react"; import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "@headlessui/react";
import { Settings, Puzzle, Settings2, Info, Server } from "lucide-react"; import { Settings, Puzzle, Settings2, Info, Server } from "lucide-react";
import { useSearchParams } from "react-router-dom"; import { useSearchParams } from "react-router-dom";
import { useTranslation } from "react-i18next";
import SettingsPanel from "@/components/Settings/SettingsPanel"; import SettingsPanel from "@/components/Settings/SettingsPanel";
import GeneralSettings from "@/components/Settings/GeneralSettings"; import GeneralSettings from "@/components/Settings/GeneralSettings";
@@ -11,6 +12,7 @@ import Footer from "@/components/Footer";
import ApiDetails from "@/components/Common/ApiDetails"; import ApiDetails from "@/components/Common/ApiDetails";
function SettingsPage() { function SettingsPage() {
const { t } = useTranslation();
const [defaultIndex, setDefaultIndex] = useState<number>(0); const [defaultIndex, setDefaultIndex] = useState<number>(0);
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
@@ -21,11 +23,11 @@ function SettingsPage() {
}, [name]); }, [name]);
const tabs = [ const tabs = [
{ name: "General", icon: Settings }, { name: t('settings.tabs.general'), icon: Settings },
{ name: "Extensions", icon: Puzzle }, { name: t('settings.tabs.extensions'), icon: Puzzle },
{ name: "Connect", icon: Server }, { name: t('settings.tabs.connect'), icon: Server },
{ name: "Advanced", icon: Settings2 }, { name: t('settings.tabs.advanced'), icon: Settings2 },
{ name: "About", icon: Info }, { name: t('settings.tabs.about'), icon: Info },
]; ];
return ( return (
@@ -71,7 +73,7 @@ function SettingsPage() {
<TabPanel> <TabPanel>
<SettingsPanel title=""> <SettingsPanel title="">
<div className="text-gray-600 dark:text-gray-400"> <div className="text-gray-600 dark:text-gray-400">
Extensions settings content {t('settings.tabs.extensionsContent')}
</div> </div>
</SettingsPanel> </SettingsPanel>
</TabPanel> </TabPanel>
@@ -81,7 +83,7 @@ function SettingsPage() {
<TabPanel> <TabPanel>
<SettingsPanel title=""> <SettingsPanel title="">
<div className="text-gray-600 dark:text-gray-400"> <div className="text-gray-600 dark:text-gray-400">
Advanced Settings content {t('settings.tabs.advancedContent')}
</div> </div>
</SettingsPanel> </SettingsPanel>
</TabPanel> </TabPanel>

View File

@@ -1,11 +1,15 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { Outlet, useLocation } from "react-router-dom"; 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 useEscape from "@/hooks/useEscape";
import useSettingsWindow from "@/hooks/useSettingsWindow"; import useSettingsWindow from "@/hooks/useSettingsWindow";
export default function Layout() { export default function Layout() {
const location = useLocation(); const location = useLocation();
function updateBodyClass(path: string) { function updateBodyClass(path: string) {
const body = document.body; const body = document.body;
body.className = ""; body.className = "";
@@ -14,6 +18,7 @@ export default function Layout() {
body.classList.add("input-body"); body.classList.add("input-body");
} }
} }
useEffect(() => { useEffect(() => {
updateBodyClass(location.pathname); updateBodyClass(location.pathname);
}, [location.pathname]); }, [location.pathname]);
@@ -22,5 +27,23 @@ export default function Layout() {
useSettingsWindow(); 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 />; return <Outlet />;
} }

View File

@@ -19,11 +19,12 @@ export type IAppStore = {
// ssoServerID: string; // ssoServerID: string;
// setSSOServerID: (ssoServerID: string) => void, // setSSOServerID: (ssoServerID: string) => void,
endpoint: AppEndpoint, endpoint: AppEndpoint,
endpoint_http: string, endpoint_http: string,
endpoint_websocket: string, endpoint_websocket: string,
setEndpoint: (endpoint: AppEndpoint) => void, setEndpoint: (endpoint: AppEndpoint) => void,
language: string;
setLanguage: (language: string) => void;
initializeListeners: () => void; initializeListeners: () => void;
}; };
@@ -62,6 +63,8 @@ export const useAppStore = create<IAppStore>()(
endpoint_websocket endpoint_websocket
}); });
}, },
language: "en",
setLanguage: (language: string) => set({ language }),
initializeListeners: () => { initializeListeners: () => {
listen(ENDPOINT_CHANGE_EVENT, (event: any) => { listen(ENDPOINT_CHANGE_EVENT, (event: any) => {
const { endpoint, endpoint_http, endpoint_websocket } = event.payload; const { endpoint, endpoint_http, endpoint_websocket } = event.payload;
@@ -79,6 +82,7 @@ export const useAppStore = create<IAppStore>()(
endpoint: state.endpoint, endpoint: state.endpoint,
endpoint_http: state.endpoint_http, endpoint_http: state.endpoint_http,
endpoint_websocket: state.endpoint_websocket, endpoint_websocket: state.endpoint_websocket,
language: state.language,
}), }),
} }
) )