mirror of
https://github.com/infinilabs/coco-app.git
synced 2025-12-16 19:47:43 +01:00
feat: add locales switch (#144)
* feat: add locales * feat: add locales * feat: add listen language
This commit is contained in:
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@@ -15,6 +15,7 @@
|
|||||||
"inputbox",
|
"inputbox",
|
||||||
"katex",
|
"katex",
|
||||||
"khtml",
|
"khtml",
|
||||||
|
"languagedetector",
|
||||||
"localstorage",
|
"localstorage",
|
||||||
"lucide",
|
"lucide",
|
||||||
"maximizable",
|
"maximizable",
|
||||||
|
|||||||
@@ -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
23
pnpm-lock.yaml
generated
@@ -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
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 ? (
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()}
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
|
|
||||||
export function Divider() {
|
|
||||||
return (
|
|
||||||
<div className="relative my-6">
|
|
||||||
<div className="absolute inset-0 flex items-center">
|
|
||||||
<div className="w-full border-t border-gray-600"></div>
|
|
||||||
</div>
|
|
||||||
<div className="relative flex justify-center text-sm">
|
|
||||||
<span className="px-2 text-gray-400 bg-gray-800">or continue with</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { forwardRef } from "react";
|
import { 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 */}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react";
|
|
||||||
import { ChevronDown, Globe2 } from "lucide-react";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
|
|
||||||
interface Language {
|
|
||||||
code: string;
|
|
||||||
name: string;
|
|
||||||
flag: string;
|
|
||||||
keyboard: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const languages: Language[] = [
|
|
||||||
{ code: "en", name: "English", flag: "🇺🇸", keyboard: "E" },
|
|
||||||
{ code: "zh", name: "中文", flag: "🇨🇳", keyboard: "Z" },
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function LangToggle() {
|
|
||||||
const { i18n } = useTranslation();
|
|
||||||
const [currentLng, setCurrentLng] = useState(languages[0]);
|
|
||||||
const changeLanguage = (lng: Language) => {
|
|
||||||
setCurrentLng(lng);
|
|
||||||
i18n.changeLanguage(lng.code);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Menu>
|
|
||||||
<MenuButton className="inline-flex items-center gap-2 rounded-md py-1.5 px-3 text-sm/6 font-semibold dark:text-white shadow-inner dark:shadow-white/10 focus:outline-none dark:data-[hover]:bg-gray-700 dark:data-[open]:bg-gray-700 data-[focus]:outline-1 dark:data-[focus]:outline-white">
|
|
||||||
<Globe2 className="h-4 w-4 text-gray-600" />
|
|
||||||
<span className="text-base">{currentLng.flag}</span>
|
|
||||||
<span>{currentLng.name}</span>
|
|
||||||
<ChevronDown className="size-4 dark:fill-white/60" />
|
|
||||||
</MenuButton>
|
|
||||||
<MenuItems
|
|
||||||
anchor="bottom end"
|
|
||||||
className="w-[160px] origin-top-right rounded-xl border dark:border-white/5 dark:bg-white/5 p-1 text-sm/6 dark:text-white transition duration-100 ease-out [--anchor-gap:var(--spacing-1)] focus:outline-none data-[closed]:scale-95 data-[closed]:opacity-0"
|
|
||||||
>
|
|
||||||
{languages.map((language) => (
|
|
||||||
<MenuItem key={language.code}>
|
|
||||||
<button
|
|
||||||
className="group flex w-full items-center gap-2 rounded-lg py-1.5 px-3 dark:data-[focus]:bg-white/10"
|
|
||||||
onClick={() => changeLanguage(language)}
|
|
||||||
>
|
|
||||||
<span className="mr-1 text-base">{language.flag}</span>
|
|
||||||
<span>{language.name}</span>
|
|
||||||
<kbd className="ml-auto hidden font-sans text-xs dark:text-white/50 group-data-[focus]:inline">
|
|
||||||
⌘{language.keyboard}
|
|
||||||
</kbd>
|
|
||||||
</button>
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</MenuItems>
|
|
||||||
</Menu>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useEffect, useRef, useImperativeHandle, forwardRef } from "react";
|
import { 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)}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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*/}
|
||||||
|
|||||||
@@ -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]"
|
||||||
}`}
|
}`}
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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, Collaborate—All 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>
|
||||||
)
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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 }) => (
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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": "添加新服务器"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user