diff --git a/.github/ISSUE_TEMPLATE/0_bug_report.yml b/.github/ISSUE_TEMPLATE/0_bug_report.yml index 3b0515d5..ac3712af 100644 --- a/.github/ISSUE_TEMPLATE/0_bug_report.yml +++ b/.github/ISSUE_TEMPLATE/0_bug_report.yml @@ -18,7 +18,7 @@ body: options: - label: I understand that issues are for feedback and problem solving, not for complaining in the comment section, and will provide as much information as possible to help solve the problem. required: true - - label: I've looked at pinned issues and searched for existing [Open Issues](https://github.com/CherryHQ/cherry-studio/issues) and [Closed Issues]( https://github.com/CherryHQ/cherry-studio/issues?q=is%3Aissue%20state%3Aclosed%20), no similar issue was found. + - label: I've looked at pinned issues and searched for existing [Open Issues](https://github.com/CherryHQ/cherry-studio/issues), [Closed Issues](https://github.com/CherryHQ/cherry-studio/issues?q=is%3Aissue%20state%3Aclosed), and [Discussions](https://github.com/CherryHQ/cherry-studio/discussions), no similar issue or discussion was found. required: true - label: I've filled in short, clear headings so that developers can quickly identify a rough idea of what to expect when flipping through the list of issues. And not "a suggestion", "stuck", etc. required: true @@ -48,8 +48,8 @@ body: id: description attributes: label: Bug Description - description: Please be as detailed as possible when describing the problem - placeholder: Tell us what happened... + description: Please be as detailed as possible when describing the problem. Please provide screenshots or screen recordings whenever possible to help us better understand the issue. + placeholder: Tell us what happened... (Remember to attach screenshots/recordings if applicable) validations: required: true @@ -57,12 +57,14 @@ body: id: reproduction attributes: label: Steps To Reproduce - description: Provide detailed steps to reproduce the issue so that our developers can reproduce the issue accurately + description: Provide detailed steps to reproduce the issue so that our developers can reproduce the issue accurately. Please include screenshots or screen recordings for each step when possible. placeholder: | 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error + + Remember to attach screenshots/recordings for each step when possible! validations: required: true diff --git a/.github/ISSUE_TEMPLATE/1_feature_request.yml b/.github/ISSUE_TEMPLATE/1_feature_request.yml index 831b5f10..19376055 100644 --- a/.github/ISSUE_TEMPLATE/1_feature_request.yml +++ b/.github/ISSUE_TEMPLATE/1_feature_request.yml @@ -18,7 +18,7 @@ body: options: - label: I understand that issues are for reporting problems and requesting features, not for off-topic comments, and I will provide as much detail as possible to help resolve the issue. required: true - - label: I have checked the pinned issues and searched through the existing [open issues](https://github.com/CherryHQ/cherry-studio/issues) and [closed issues](https://github.com/CherryHQ/cherry-studio/issues?q=is%3Aissue%20state%3Aclosed) and did not find a similar suggestion. + - label: I have checked the pinned issues and searched through the existing [open issues](https://github.com/CherryHQ/cherry-studio/issues), [closed issues](https://github.com/CherryHQ/cherry-studio/issues?q=is%3Aissue%20state%3Aclosed), and [discussions](https://github.com/CherryHQ/cherry-studio/discussions) and did not find a similar suggestion. required: true - label: I have provided a short and descriptive title so that developers can quickly understand the issue when browsing the issue list, rather than vague titles like "A suggestion" or "Stuck." required: true @@ -50,8 +50,8 @@ body: id: problem attributes: label: Is your feature request related to an existing issue? - description: Please briefly describe the problem you are experiencing. - placeholder: I often feel frustrated because... + description: Please briefly describe the problem you are experiencing. If possible, include screenshots or recordings to help illustrate the current situation or pain points. + placeholder: I often feel frustrated because... (Remember to attach screenshots/recordings if applicable) validations: required: true @@ -59,7 +59,7 @@ body: id: solution attributes: label: Desired Solution - description: Please briefly describe what you would like to happen. + description: Please briefly describe what you would like to happen. You can include mockups, screenshots, or screen recordings to better illustrate your proposed solution. validations: required: true @@ -67,10 +67,10 @@ body: id: alternatives attributes: label: Alternative Solutions - description: Please briefly describe any alternative solutions or features you have considered. + description: Please briefly describe any alternative solutions or features you have considered. Feel free to include screenshots or mockups of alternative approaches. - type: textarea id: additional attributes: label: Additional Information - description: Add any other context or screenshots related to your feature request. + description: Add any other context, screenshots, mockups or recordings that can help us better understand your feature request. diff --git a/.github/ISSUE_TEMPLATE/2_question.yml b/.github/ISSUE_TEMPLATE/2_question.yml index d191daf7..f980e691 100644 --- a/.github/ISSUE_TEMPLATE/2_question.yml +++ b/.github/ISSUE_TEMPLATE/2_question.yml @@ -17,6 +17,8 @@ body: options: - label: I understand that issues are meant for feedback and problem-solving, not for venting, and I will provide as much detail as possible to help resolve the issue. required: true + - label: I have checked the pinned issues and searched through the existing [open issues](https://github.com/CherryHQ/cherry-studio/issues), [closed issues](https://github.com/CherryHQ/cherry-studio/issues?q=is%3Aissue%20state%3Aclosed), and [discussions](https://github.com/CherryHQ/cherry-studio/discussions) and did not find a similar suggestion. + required: true - label: I confirm that I am here to ask questions and discuss issues, not to report bugs or request features. required: true @@ -45,8 +47,8 @@ body: id: question attributes: label: Your Question - description: Please describe your issue in detail. - placeholder: Please explain your issue as clearly as possible... + description: Please describe your issue in detail. Include screenshots or screen recordings whenever possible to help us better understand your question. + placeholder: Please explain your issue as clearly as possible...(Remember to attach screenshots/recordings if applicable) validations: required: true @@ -54,14 +56,14 @@ body: id: context attributes: label: Context - description: Please provide some background information to help us better understand your question - placeholder: "For example: use case, solutions you've tried, etc." + description: Please provide some background information to help us better understand your question. Screenshots or recordings of your current setup or situation can be very helpful. + placeholder: "For example: use case, solutions you've tried, etc. Don't forget to include relevant screenshots/recordings!" - type: textarea id: additional attributes: label: Additional Information - description: Any other relevant information, screenshots, or code examples + description: Any other relevant information, screenshots, recordings, or code examples that can help us better assist you render: shell - type: dropdown diff --git a/README.md b/README.md index 9e76772c..68b6d745 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,20 @@ Cherry Studio is a desktop client that supports for multiple LLM providers, avai - 📝 Complete Markdown Rendering - 🤲 Easy Content Sharing +# 📝 TODO + +- [x] Quick popup (read clipboard, quick question, explain, translate, summarize) +- [x] Comparison of multi-model answers +- [x] Support login using SSO provided by service providers +- [ ] All models support networking (in development...) +- [ ] Launch of the first official version +- [ ] Plugin functionality (JavaScript) +- [ ] Browser extension (highlight text to translate, summarize, add to knowledge base) +- [ ] iOS & Android client +- [ ] AI notes +- [ ] Voice input and output (AI call) +- [ ] Data backup supports custom backup content + # 🖥️ Develop ## IDE Setup diff --git a/docs/README.ja.md b/docs/README.ja.md index 67004385..10c05f06 100644 --- a/docs/README.ja.md +++ b/docs/README.ja.md @@ -61,6 +61,20 @@ Cherry Studioは、複数のLLMプロバイダーをサポートするデスク - 📝 完全な Markdown レンダリング - 🤲 簡単な共有機能 +# 📝 TODO + +- [x] クイックポップアップ(クリップボードの読み取り、簡単な質問、説明、翻訳、要約) +- [x] 複数モデルの回答の比較 +- [x] サービスプロバイダーが提供するSSOを使用したログインをサポート +- [ ] すべてのモデルがネットワークをサポート(開発中...) +- [ ] 最初の公式バージョンのリリース +- [ ] プラグイン機能(JavaScript) +- [ ] ブラウザ拡張機能(テキストをハイライトして翻訳、要約、ナレッジベースに追加) +- [ ] iOS & Android クライアント +- [ ] AIノート +- [ ] 音声入出力(AIコール) +- [ ] データバックアップはカスタムバックアップコンテンツをサポート + # 🖥️ 開発 ## IDEの設定 diff --git a/docs/README.zh.md b/docs/README.zh.md index 208021b2..3afddfaa 100644 --- a/docs/README.zh.md +++ b/docs/README.zh.md @@ -61,6 +61,20 @@ Cherry Studio 是一款支持多个大语言模型(LLM)服务商的桌面客 - 📝 完整的 Markdown 渲染 - 🤲 便捷的内容分享功能 +# 📝 待辦事項 + +- [x] 快捷彈窗 (讀取剪貼簿、快速提問、解釋、翻譯、總結) +- [x] 多模型回答對比 +- [x] 支援使用服務供應商提供的 SSO 進行登入 +- [ ] 全部模型支援連網(開發中...) +- [ ] 推出第一個正式版 +- [ ] 插件功能(JavaScript) +- [ ] 瀏覽器插件(劃詞翻譯、總結、新增至知識庫) +- [ ] iOS & Android 客戶端 +- [ ] AI 筆記 +- [ ] 語音輸入輸出(AI 通話) +- [ ] 資料備份支援自訂備份內容 + # 🖥️ 开发 ## IDE 设置 diff --git a/electron-builder.yml b/electron-builder.yml index 1f3e8eed..99b06e38 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -82,7 +82,9 @@ releaseInfo: releaseNotes: | 消息分组支持网格模式 知识库支持多选 - 支持库增加 DRAFTS, EPUB + 知识库添加目录支持显示进度 + 知识库支持 DRAFTS, EPUB、代码等 知识库支持调节匹配度阈值 添加 NotebookLM, Coze 小程序 增加话题提示词 + OpenRouter 支持 Web 搜索 diff --git a/package.json b/package.json index f7868716..2b85792f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "CherryStudio", - "version": "0.9.25", + "version": "0.9.27", "private": true, "description": "A powerful AI assistant for producer.", "main": "./out/main/index.js", @@ -94,6 +94,7 @@ "@types/markdown-it": "^14", "@types/md5": "^2.3.5", "@types/node": "^18.19.9", + "@types/pako": "^1.0.2", "@types/react": "^18.2.48", "@types/react-dom": "^18.2.18", "@types/react-infinite-scroll-component": "^5.0.0", diff --git a/packages/shared/config/constant.ts b/packages/shared/config/constant.ts index 1c7b5758..0ef50188 100644 --- a/packages/shared/config/constant.ts +++ b/packages/shared/config/constant.ts @@ -90,7 +90,10 @@ export const textExts = [ '.groovy', // Gradle 构建文件 '.kts', // Kotlin Script 文件 '.java', // Java 代码文件 - '.cs' // C# 代码文件 + '.cs', // C# 代码文件 + '.cpp', // C++ 代码文件 + '.c', // C++ 代码文件 + '.h' // C++ 头文件 ] export const ZOOM_SHORTCUTS = [ diff --git a/src/main/index.ts b/src/main/index.ts index 0f0c72cd..c3c9553a 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -1,5 +1,5 @@ import { electronApp, optimizer } from '@electron-toolkit/utils' -import { app, BrowserWindow } from 'electron' +import { app } from 'electron' import installExtension, { REDUX_DEVTOOLS } from 'electron-devtools-installer' import { registerIpc } from './ipc' @@ -46,15 +46,13 @@ if (!app.requestSingleInstanceLock()) { new TrayService() app.on('activate', function () { - // On macOS it's common to re-create a window in the app when the - // dock icon is clicked and there are no other windows open. - if (BrowserWindow.getAllWindows().length === 0) { + const mainWindow = windowService.getMainWindow() + if (!mainWindow || mainWindow.isDestroyed()) { windowService.createMainWindow() } else { windowService.showMainWindow() } }) - registerShortcuts(mainWindow) registerIpc(mainWindow, app) @@ -68,12 +66,7 @@ if (!app.requestSingleInstanceLock()) { // Listen for second instance app.on('second-instance', () => { - const mainWindow = BrowserWindow.getAllWindows()[0] - if (mainWindow) { - mainWindow.isMinimized() && mainWindow.restore() - mainWindow.show() - mainWindow.focus() - } + windowService.showMainWindow() }) app.on('browser-window-created', (_, window) => { diff --git a/src/main/loader/index.ts b/src/main/loader/index.ts index f23c58b1..2c6afa74 100644 --- a/src/main/loader/index.ts +++ b/src/main/loader/index.ts @@ -123,13 +123,22 @@ export async function addFileLoader( // JSON类型 if (['.json'].includes(file.ext)) { - const jsonObject = JSON.parse(fileContent) - const loaderReturn = await ragApplication.addLoader(new JsonLoader({ object: jsonObject })) - return { - entriesAdded: loaderReturn.entriesAdded, - uniqueId: loaderReturn.uniqueId, - uniqueIds: [loaderReturn.uniqueId], - loaderType: loaderReturn.loaderType + let jsonObject = {} + let jsonParsed = true + try { + jsonObject = JSON.parse(fileContent) + } catch (error) { + jsonParsed = false + Logger.warn('[KnowledgeBase] failed parsing json file, failling back to text processing:', file.path, error) + } + if (jsonParsed) { + const loaderReturn = await ragApplication.addLoader(new JsonLoader({ object: jsonObject })) + return { + entriesAdded: loaderReturn.entriesAdded, + uniqueId: loaderReturn.uniqueId, + uniqueIds: [loaderReturn.uniqueId], + loaderType: loaderReturn.loaderType + } } } diff --git a/src/main/services/KnowledgeService.ts b/src/main/services/KnowledgeService.ts index ee847092..b2b906e4 100644 --- a/src/main/services/KnowledgeService.ts +++ b/src/main/services/KnowledgeService.ts @@ -15,6 +15,8 @@ import { FileType, KnowledgeBaseParams, KnowledgeItem } from '@types' import { app } from 'electron' import { v4 as uuidv4 } from 'uuid' +import { windowService } from './WindowService' + class KnowledgeService { private storageDir = path.join(app.getPath('userData'), 'Data', 'KnowledgeBase') @@ -83,10 +85,23 @@ class KnowledgeService { ): Promise => { const ragApplication = await this.getRagApplication(base) + const sendDirectoryProcessingPercent = (totalFiles: number, processedFiles: number) => { + const mainWindow = windowService.getMainWindow() + mainWindow?.webContents.send(base.id, (processedFiles / totalFiles) * 100) + } + if (item.type === 'directory') { const directory = item.content as string const files = getAllFiles(directory) - const loaderPromises = files.map((file) => addFileLoader(ragApplication, file, base, forceReload)) + const totalFiles = files.length + let processedFiles = 0 + const loaderPromises = files.map(async (file) => { + const result = await addFileLoader(ragApplication, file, base, forceReload) + processedFiles++ + + sendDirectoryProcessingPercent(totalFiles, processedFiles) + return result + }) const loaderResults = await Promise.all(loaderPromises) const uniqueIds = loaderResults.map((result) => result.uniqueId) return { diff --git a/src/main/services/ShortcutService.ts b/src/main/services/ShortcutService.ts index d9920981..79ddf9b7 100644 --- a/src/main/services/ShortcutService.ts +++ b/src/main/services/ShortcutService.ts @@ -70,10 +70,12 @@ const convertShortcutRecordedByKeyboardEventKeyValueToElectronGlobalShortcutForm return accelerator .map((key) => { switch (key) { + case 'Command': + return 'CommandOrControl' case 'Control': - return 'CommandOrControl' + return 'Control' case 'Ctrl': - return 'CommandOrControl' + return 'Control' case 'ArrowUp': return 'Up' case 'ArrowDown': diff --git a/src/main/services/WindowService.ts b/src/main/services/WindowService.ts index 1ce7eb8a..414c568d 100644 --- a/src/main/services/WindowService.ts +++ b/src/main/services/WindowService.ts @@ -28,6 +28,7 @@ export class WindowService { public createMainWindow(): BrowserWindow { if (this.mainWindow && !this.mainWindow.isDestroyed()) { + this.mainWindow.show() return this.mainWindow } @@ -248,17 +249,32 @@ export class WindowService { event.preventDefault() mainWindow.hide() }) + + mainWindow.on('closed', () => { + this.mainWindow = null + }) + + mainWindow.on('show', () => { + if (this.miniWindow && !this.miniWindow.isDestroyed()) { + this.miniWindow.hide() + } + }) } public showMainWindow() { - if (this.mainWindow) { + if (this.miniWindow && !this.miniWindow.isDestroyed()) { + this.miniWindow.hide() + } + + if (this.mainWindow && !this.mainWindow.isDestroyed()) { if (this.mainWindow.isMinimized()) { - return this.mainWindow.restore() + this.mainWindow.restore() } this.mainWindow.show() this.mainWindow.focus() } else { - this.createMainWindow() + this.mainWindow = this.createMainWindow() + this.mainWindow.focus() } } @@ -269,7 +285,10 @@ export class WindowService { return } - if (this.selectionMenuWindow) { + if (this.mainWindow && !this.mainWindow.isDestroyed()) { + this.mainWindow.hide() + } + if (this.selectionMenuWindow && !this.selectionMenuWindow.isDestroyed()) { this.selectionMenuWindow.hide() } diff --git a/src/main/utils/file.ts b/src/main/utils/file.ts index 8f837a26..c8d34945 100644 --- a/src/main/utils/file.ts +++ b/src/main/utils/file.ts @@ -18,6 +18,10 @@ export function getAllFiles(dirPath: string, arrayOfFiles: FileType[] = []): Fil const files = fs.readdirSync(dirPath) files.forEach((file) => { + if (file.startsWith('.')) { + return + } + const fullPath = path.join(dirPath, file) if (fs.statSync(fullPath).isDirectory()) { arrayOfFiles = getAllFiles(fullPath, arrayOfFiles) diff --git a/src/preload/index.d.ts b/src/preload/index.d.ts index 0dc6d477..7ae73dfc 100644 --- a/src/preload/index.d.ts +++ b/src/preload/index.d.ts @@ -119,6 +119,9 @@ declare global { encrypt: (text: string, secretKey: string, iv: string) => Promise<{ iv: string; encryptedData: string }> decrypt: (encryptedData: string, iv: string, secretKey: string) => Promise } + shell: { + openExternal: (url: string, options?: OpenExternalOptions) => Promise + } } } } diff --git a/src/preload/index.ts b/src/preload/index.ts index 080ffc22..079dd041 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -1,6 +1,6 @@ import { electronAPI } from '@electron-toolkit/preload' import { FileType, KnowledgeBaseParams, KnowledgeItem, Shortcut, WebDavConfig } from '@types' -import { contextBridge, ipcRenderer, OpenDialogOptions } from 'electron' +import { contextBridge, ipcRenderer, OpenDialogOptions, shell } from 'electron' // Custom APIs for renderer const api = { @@ -104,6 +104,9 @@ const api = { encrypt: (text: string, secretKey: string, iv: string) => ipcRenderer.invoke('aes:encrypt', text, secretKey, iv), decrypt: (encryptedData: string, iv: string, secretKey: string) => ipcRenderer.invoke('aes:decrypt', encryptedData, iv, secretKey) + }, + shell: { + openExternal: shell.openExternal } } diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 11f15171..84d877c6 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -10,6 +10,7 @@ import TopViewContainer from './components/TopView' import AntdProvider from './context/AntdProvider' import { SyntaxHighlighterProvider } from './context/SyntaxHighlighterProvider' import { ThemeProvider } from './context/ThemeProvider' +import NavigationHandler from './handler/NavigationHandler' import AgentsPage from './pages/agents/AgentsPage' import AppsPage from './pages/apps/AppsPage' import FilesPage from './pages/files/FilesPage' @@ -28,6 +29,8 @@ function App(): JSX.Element { + + {/* 添加导航处理组件 */} } /> diff --git a/src/renderer/src/assets/images/apps/abacus.webp b/src/renderer/src/assets/images/apps/abacus.webp new file mode 100644 index 00000000..ab708397 Binary files /dev/null and b/src/renderer/src/assets/images/apps/abacus.webp differ diff --git a/src/renderer/src/assets/images/apps/dify.webp b/src/renderer/src/assets/images/apps/dify.webp new file mode 100644 index 00000000..1a3620e4 Binary files /dev/null and b/src/renderer/src/assets/images/apps/dify.webp differ diff --git a/src/renderer/src/assets/images/apps/lambdachat.webp b/src/renderer/src/assets/images/apps/lambdachat.webp new file mode 100644 index 00000000..d291dcf9 Binary files /dev/null and b/src/renderer/src/assets/images/apps/lambdachat.webp differ diff --git a/src/renderer/src/assets/images/apps/lechat.png b/src/renderer/src/assets/images/apps/lechat.png new file mode 100644 index 00000000..0ed97c81 Binary files /dev/null and b/src/renderer/src/assets/images/apps/lechat.png differ diff --git a/src/renderer/src/assets/images/apps/wpslingxi.webp b/src/renderer/src/assets/images/apps/wpslingxi.webp new file mode 100644 index 00000000..4c2c3b83 Binary files /dev/null and b/src/renderer/src/assets/images/apps/wpslingxi.webp differ diff --git a/src/renderer/src/components/DragableList/index.tsx b/src/renderer/src/components/DragableList/index.tsx index 6be5dcd6..05ed09c4 100644 --- a/src/renderer/src/components/DragableList/index.tsx +++ b/src/renderer/src/components/DragableList/index.tsx @@ -46,23 +46,28 @@ const DragableList: FC> = ({ {(provided) => ( -
+
{list.map((item, index) => { const id = item.id || item return ( - + {(provided) => (
+ style={{ + ...listStyle, + ...provided.draggableProps.style, + marginBottom: 8 + }}> {children(item, index)}
)}
) })} + {provided.placeholder}
)} diff --git a/src/renderer/src/components/MinApp/index.tsx b/src/renderer/src/components/MinApp/index.tsx index 03d49a62..86cecbf7 100644 --- a/src/renderer/src/components/MinApp/index.tsx +++ b/src/renderer/src/components/MinApp/index.tsx @@ -1,5 +1,5 @@ /* eslint-disable react/no-unknown-property */ -import { CloseOutlined, ExportOutlined, PushpinOutlined, ReloadOutlined } from '@ant-design/icons' +import { CloseOutlined, CodeOutlined, ExportOutlined, PushpinOutlined, ReloadOutlined } from '@ant-design/icons' import { isMac, isWindows } from '@renderer/config/constant' import { AppLogo } from '@renderer/config/env' import { DEFAULT_MIN_APPS } from '@renderer/config/minapps' @@ -42,7 +42,11 @@ const PopupContainer: React.FC = ({ app, resolve }) => { } MinApp.onClose = onClose - + const openDevTools = () => { + if (webviewRef.current) { + webviewRef.current.openDevTools() + } + } const onReload = () => { if (webviewRef.current) { webviewRef.current.src = app.url @@ -60,7 +64,7 @@ const PopupContainer: React.FC = ({ app, resolve }) => { const newPinned = isPinned ? pinned.filter((item) => item.id !== app.id) : [...pinned, app] updatePinnedMinapps(newPinned) } - + const isInDevelopment = process.env.NODE_ENV === 'development' const Title = () => { return ( @@ -79,6 +83,11 @@ const PopupContainer: React.FC = ({ app, resolve }) => { )} + {isInDevelopment && ( + + )} diff --git a/src/renderer/src/components/Popups/MinAppsPopover.tsx b/src/renderer/src/components/Popups/MinAppsPopover.tsx index 9a3c7da3..c180defe 100644 --- a/src/renderer/src/components/Popups/MinAppsPopover.tsx +++ b/src/renderer/src/components/Popups/MinAppsPopover.tsx @@ -4,7 +4,7 @@ import App from '@renderer/pages/apps/App' import { Popover } from 'antd' import { Empty } from 'antd' import { isEmpty } from 'lodash' -import { FC, useState, useEffect } from 'react' +import { FC, useEffect, useState } from 'react' import { useHotkeys } from 'react-hotkeys-hook' import styled from 'styled-components' @@ -26,19 +26,19 @@ const MinAppsPopover: FC = ({ children }) => { setOpen(false) } - const [maxHeight, setMaxHeight] = useState(window.innerHeight - 100); + const [maxHeight, setMaxHeight] = useState(window.innerHeight - 100) useEffect(() => { const handleResize = () => { - setMaxHeight(window.innerHeight - 100); - }; + setMaxHeight(window.innerHeight - 100) + } - window.addEventListener('resize', handleResize); + window.addEventListener('resize', handleResize) return () => { - window.removeEventListener('resize', handleResize); - }; - }, []); + window.removeEventListener('resize', handleResize) + } + }, []) const content = ( @@ -74,9 +74,9 @@ const PopoverContent = styled(Scrollbar)<{ maxHeight: number }>` ` const AppsContainer = styled.div` -display: grid; + display: grid; grid-template-columns: repeat(6, minmax(90px, 1fr)); gap: 18px; -`; +` export default MinAppsPopover diff --git a/src/renderer/src/config/minapps.ts b/src/renderer/src/config/minapps.ts index e7ab6c71..68e89242 100644 --- a/src/renderer/src/config/minapps.ts +++ b/src/renderer/src/config/minapps.ts @@ -1,10 +1,12 @@ import ThreeMinTopAppLogo from '@renderer/assets/images/apps/3mintop.png?url' +import AbacusLogo from '@renderer/assets/images/apps/abacus.webp?url' import AIStudioLogo from '@renderer/assets/images/apps/aistudio.svg?url' import BaiduAiAppLogo from '@renderer/assets/images/apps/baidu-ai.png?url' import BaicuanAppLogo from '@renderer/assets/images/apps/baixiaoying.webp?url' import BoltAppLogo from '@renderer/assets/images/apps/bolt.svg?url' import CozeAppLogo from '@renderer/assets/images/apps/coze.webp?url' import DevvAppLogo from '@renderer/assets/images/apps/devv.png?url' +import DifyAppLogo from '@renderer/assets/images/apps/dify.webp?url' import DoubaoAppLogo from '@renderer/assets/images/apps/doubao.png?url' import DuckDuckGoAppLogo from '@renderer/assets/images/apps/duckduckgo.webp?url' import FeloAppLogo from '@renderer/assets/images/apps/felo.png?url' @@ -16,6 +18,8 @@ import GrokAppLogo from '@renderer/assets/images/apps/grok.png?url' import HikaLogo from '@renderer/assets/images/apps/hika.webp?url' import HuggingChatLogo from '@renderer/assets/images/apps/huggingchat.svg?url' import KimiAppLogo from '@renderer/assets/images/apps/kimi.jpg?url' +import LambdaChatLogo from '@renderer/assets/images/apps/lambdachat.webp?url' +import LeChatLogo from '@renderer/assets/images/apps/lechat.png?url' import MetasoAppLogo from '@renderer/assets/images/apps/metaso.webp?url' import NamiAiLogo from '@renderer/assets/images/apps/nm.png?url' import NamiAiSearchLogo from '@renderer/assets/images/apps/nm-search.webp?url' @@ -29,6 +33,7 @@ import SparkDeskAppLogo from '@renderer/assets/images/apps/sparkdesk.png?url' import ThinkAnyLogo from '@renderer/assets/images/apps/thinkany.webp?url' import TiangongAiLogo from '@renderer/assets/images/apps/tiangong.png?url' import WanZhiAppLogo from '@renderer/assets/images/apps/wanzhi.jpg?url' +import WPSLingXiLogo from '@renderer/assets/images/apps/wpslingxi.webp?url' import XiaoYiAppLogo from '@renderer/assets/images/apps/xiaoyi.webp?url' import TencentYuanbaoAppLogo from '@renderer/assets/images/apps/yuanbao.png?url' import YuewenAppLogo from '@renderer/assets/images/apps/yuewen.png?url' @@ -314,6 +319,41 @@ export const DEFAULT_MIN_APPS: MinAppType[] = [ logo: CozeAppLogo, url: 'https://www.coze.com/space', bodered: true + }, + { + id: 'dify', + name: 'Dify', + logo: DifyAppLogo, + url: 'https://cloud.dify.ai/apps', + bodered: true + }, + { + id: 'wpslingxi', + name: 'WPS灵犀', + logo: WPSLingXiLogo, + url: 'https://copilot.wps.cn/', + bodered: true + }, + { + id: 'lechat', + name: 'LeChat', + logo: LeChatLogo, + url: 'https://chat.mistral.ai/chat', + bodered: true + }, + { + id: 'abacus', + name: 'Abacus', + logo: AbacusLogo, + url: 'https://apps.abacus.ai/chatllm', + bodered: true + }, + { + id: 'lambdachat', + name: 'Lambda Chat', + logo: LambdaChatLogo, + url: 'https://lambda.chat/', + bodered: true } ] diff --git a/src/renderer/src/config/models.ts b/src/renderer/src/config/models.ts index b49b2845..95458b4a 100644 --- a/src/renderer/src/config/models.ts +++ b/src/renderer/src/config/models.ts @@ -150,7 +150,8 @@ const visionAllowedModels = [ 'gpt-4o(?:-[\\w-]+)?', 'chatgpt-4o(?:-[\\w-]+)?', 'o1(?:-[\\w-]+)?', - 'deepseek-vl(?:[\\w-]+)?' + 'deepseek-vl(?:[\\w-]+)?', + 'kimi-latest' ] const visionExcludedModels = ['gpt-4-\\d+-preview', 'gpt-4-turbo-preview', 'gpt-4-32k', 'gpt-4-\\d+'] @@ -163,8 +164,7 @@ export const VISION_REGEX = new RegExp( export const TEXT_TO_IMAGE_REGEX = /flux|diffusion|stabilityai|sd-|dall|cogview|janus/i export const REASONING_REGEX = /^(o\d+(?:-[\w-]+)?|.*\b(?:reasoner|thinking)\b.*|.*-[rR]\d+.*)$/i -export const EMBEDDING_REGEX = - /(?:^text-|embed|rerank|davinci|babbage|bge-|e5-|LLM2Vec|retrieval|uae-|gte-|jina-clip|jina-embeddings)/i +export const EMBEDDING_REGEX = /(?:^text-|embed|bge-|e5-|LLM2Vec|retrieval|uae-|gte-|jina-clip|jina-embeddings)/i export const NOT_SUPPORTED_REGEX = /(?:^tts|rerank|whisper|speech)/i export function getModelLogo(modelId: string) { @@ -178,6 +178,7 @@ export function getModelLogo(modelId: string) { pixtral: isLight ? PixtralModelLogo : PixtralModelLogoDark, jina: isLight ? JinaModelLogo : JinaModelLogoDark, abab: isLight ? MinimaxModelLogo : MinimaxModelLogoDark, + minimax: isLight ? MinimaxModelLogo : MinimaxModelLogoDark, o3: isLight ? ChatGPTo1ModelLogo : ChatGPTo1ModelLogoDark, o1: isLight ? ChatGPTo1ModelLogo : ChatGPTo1ModelLogoDark, 'gpt-3': isLight ? ChatGPT35ModelLogo : ChatGPT35ModelLogoDark, @@ -194,13 +195,16 @@ export function getModelLogo(modelId: string) { glm: isLight ? ChatGLMModelLogo : ChatGLMModelLogoDark, deepseek: isLight ? DeepSeekModelLogo : DeepSeekModelLogoDark, qwen: isLight ? QwenModelLogo : QwenModelLogoDark, - qwq: isLight ? QwenModelLogo : QwenModelLogoDark, + 'qwq-': isLight ? QwenModelLogo : QwenModelLogoDark, + 'qvq-': isLight ? QwenModelLogo : QwenModelLogoDark, + Omni: isLight ? QwenModelLogo : QwenModelLogoDark, gemma: isLight ? GemmaModelLogo : GemmaModelLogoDark, 'yi-': isLight ? YiModelLogo : YiModelLogoDark, llama: isLight ? LlamaModelLogo : LlamaModelLogoDark, mixtral: isLight ? MistralModelLogo : MistralModelLogo, mistral: isLight ? MistralModelLogo : MistralModelLogoDark, moonshot: isLight ? MoonshotModelLogo : MoonshotModelLogoDark, + kimi: isLight ? MoonshotModelLogo : MoonshotModelLogoDark, phi: isLight ? MicrosoftModelLogo : MicrosoftModelLogoDark, baichuan: isLight ? BaichuanModelLogo : BaichuanModelLogoDark, claude: isLight ? ClaudeModelLogo : ClaudeModelLogoDark, @@ -1385,7 +1389,7 @@ export const SYSTEM_MODELS: Record = { name: 'claude-3-5-sonnet-20241022', group: 'Claude' }, - { + { id: 'gemini-2.0-flash', provider: 'dmxapi', name: 'gemini-2.0-flash', @@ -1599,6 +1603,10 @@ export function isEmbeddingModel(model: Model): boolean { return false } + if (model.provider === 'doubao') { + return EMBEDDING_REGEX.test(model.name) + } + return EMBEDDING_REGEX.test(model.id) || model.type?.includes('embedding') || false } @@ -1607,6 +1615,10 @@ export function isVisionModel(model: Model): boolean { return false } + if (model.provider === 'doubao') { + return VISION_REGEX.test(model.name) || model.type?.includes('vision') || false + } + return VISION_REGEX.test(model.id) || model.type?.includes('vision') || false } @@ -1615,6 +1627,10 @@ export function isReasoningModel(model: Model): boolean { return false } + if (model.provider === 'doubao') { + return REASONING_REGEX.test(model.name) || model.type?.includes('reasoning') || false + } + return REASONING_REGEX.test(model.id) || model.type?.includes('reasoning') || false } @@ -1667,6 +1683,16 @@ export function isWebSearchModel(model: Model): boolean { return model?.id?.startsWith('glm-4-') } + if (provider.id === 'dashscope') { + const models = ['qwen-turbo', 'qwen-max', 'qwen-plus'] + // matches id like qwen-max-0919, qwen-max-latest + return models.some((i) => model.id.startsWith(i)) + } + + if (provider.id === 'openrouter') { + return true + } + return false } @@ -1679,6 +1705,21 @@ export function getOpenAIWebSearchParams(assistant: Assistant, model: Model): Re return { enable_enhancement: true } } + if (model.provider === 'dashscope') { + return { + enable_search: true, + search_options: { + forced_search: true + } + } + } + + if (model.provider === 'openrouter') { + return { + plugins: [{ id: 'web' }] + } + } + return { tools: webSearchTools } diff --git a/src/renderer/src/config/providers.ts b/src/renderer/src/config/providers.ts index 9d2837de..a68d81ae 100644 --- a/src/renderer/src/config/providers.ts +++ b/src/renderer/src/config/providers.ts @@ -212,13 +212,13 @@ export const PROVIDER_CONFIG = { }, dmxapi: { api: { - url: 'https://www.dmxapi.com' + url: 'https://www.dmxapi.cn' }, websites: { - official: 'https://www.dmxapi.com/register?aff=81aj/', - apiKey: 'https://www.dmxapi.com/register?aff=81aj', - docs: 'https://dmxapi.com/models.html#code-block', - models: 'https://www.dmxapi.com/pricing' + official: 'https://www.dmxapi.cn/register?aff=bwwY', + apiKey: 'https://www.dmxapi.cn/register?aff=bwwY', + docs: 'https://dmxapi.cn/models.html#code-block', + models: 'https://www.dmxapi.cn/pricing' } }, perplexity: { diff --git a/src/renderer/src/config/translate.ts b/src/renderer/src/config/translate.ts index ee5c2acf..fa35acea 100644 --- a/src/renderer/src/config/translate.ts +++ b/src/renderer/src/config/translate.ts @@ -55,5 +55,20 @@ export const TranslateLanguageOptions = [ value: 'arabic', label: i18n.t('languages.arabic'), emoji: '🇸🇦' + }, + { + value: 'german', + label: i18n.t('languages.german'), + emoji: '🇩🇪' } ] + +export const translateLanguageOptions = (): typeof TranslateLanguageOptions => { + return TranslateLanguageOptions.map((option) => { + return { + value: option.value, + label: i18n.t(`languages.${option.value}`), + emoji: option.emoji + } + }) +} diff --git a/src/renderer/src/context/SyntaxHighlighterProvider.tsx b/src/renderer/src/context/SyntaxHighlighterProvider.tsx index 4cf59f08..cbdee21d 100644 --- a/src/renderer/src/context/SyntaxHighlighterProvider.tsx +++ b/src/renderer/src/context/SyntaxHighlighterProvider.tsx @@ -60,7 +60,7 @@ export const SyntaxHighlighterProvider: React.FC = ({ childre const mappedLanguage = languageMap[language] || language - code = code.trimEnd() + code = code?.trimEnd() ?? '' const escapedCode = code?.replace(/[<>]/g, (char) => ({ '<': '<', '>': '>' })[char]!) try { diff --git a/src/renderer/src/databases/index.ts b/src/renderer/src/databases/index.ts index cf90000f..8d54c840 100644 --- a/src/renderer/src/databases/index.ts +++ b/src/renderer/src/databases/index.ts @@ -1,4 +1,4 @@ -import { FileType, KnowledgeItem, Topic } from '@renderer/types' +import { FileType, KnowledgeItem, Topic, TranslateHistory } from '@renderer/types' import { Dexie, type EntityTable } from 'dexie' // Database declaration (move this to its own module also) @@ -7,6 +7,7 @@ export const db = new Dexie('CherryStudio') as Dexie & { topics: EntityTable, 'id'> settings: EntityTable<{ id: string; value: any }, 'id'> knowledge_notes: EntityTable + translate_history: EntityTable } db.version(1).stores({ @@ -26,4 +27,12 @@ db.version(3).stores({ knowledge_notes: '&id, baseId, type, content, created_at, updated_at' }) +db.version(4).stores({ + files: 'id, name, origin_name, path, size, ext, type, created_at, count', + topics: '&id, messages', + settings: '&id, value', + knowledge_notes: '&id, baseId, type, content, created_at, updated_at', + translate_history: '&id, sourceText, targetText, sourceLanguage, targetLanguage, createdAt' +}) + export default db diff --git a/src/renderer/src/handler/NavigationHandler.tsx b/src/renderer/src/handler/NavigationHandler.tsx new file mode 100644 index 00000000..c780e467 --- /dev/null +++ b/src/renderer/src/handler/NavigationHandler.tsx @@ -0,0 +1,17 @@ +import { useHotkeys } from 'react-hotkeys-hook' +import { useNavigate } from 'react-router-dom' + +const NavigationHandler: React.FC = () => { + const navigate = useNavigate() + useHotkeys( + 'meta+, ! ctrl+,', + function () { + navigate('/settings/provider') + }, + { splitKey: '!' } + ) + + return null +} + +export default NavigationHandler diff --git a/src/renderer/src/hooks/useKnowledge.ts b/src/renderer/src/hooks/useKnowledge.ts index dbb90ca8..4de51ca1 100644 --- a/src/renderer/src/hooks/useKnowledge.ts +++ b/src/renderer/src/hooks/useKnowledge.ts @@ -198,6 +198,27 @@ export const useKnowledge = (baseId: string) => { return base?.items.filter((item) => item.type === type && item.processingStatus !== undefined) || [] } + // 获取目录处理进度 + const getDirectoryProcessingPercent = (itemId?: string) => { + const [percent, setPercent] = useState(0) + + useEffect(() => { + if (!itemId) { + return + } + + const cleanup = window.electron.ipcRenderer.on(itemId, (_, progressingPercent: number) => { + setPercent(progressingPercent) + }) + + return () => { + cleanup() + } + }, [itemId]) + + return percent + } + // 清除已完成的项目 const clearCompleted = () => { dispatch(clearCompletedProcessing({ baseId })) @@ -280,6 +301,7 @@ export const useKnowledge = (baseId: string) => { refreshItem, getProcessingStatus, getProcessingItemsByType, + getDirectoryProcessingPercent, clearCompleted, clearAll, removeItem, diff --git a/src/renderer/src/hooks/useModel.ts b/src/renderer/src/hooks/useModel.ts index 05b164e3..69fa983f 100644 --- a/src/renderer/src/hooks/useModel.ts +++ b/src/renderer/src/hooks/useModel.ts @@ -1,7 +1,13 @@ import { useProviders } from './useProvider' -export function useModel(id?: string) { +export function useModel(id?: string, providerId?: string) { const { providers } = useProviders() const allModels = providers.map((p) => p.models).flat() - return allModels.find((m) => m.id === id) + return allModels.find((m) => { + if (providerId) { + return m.id === id && m.provider === providerId + } else { + return m.id === id + } + }) } diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index beee13c6..b15f092f 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -51,6 +51,7 @@ "settings.reasoning_effort.high": "high", "settings.reasoning_effort.low": "low", "settings.reasoning_effort.medium": "medium", + "settings.reasoning_effort.off": "off", "settings.reasoning_effort.tip": "Only supports reasoning models", "title": "Assistants" }, @@ -120,6 +121,8 @@ "settings.top_p.tip": "Default value is 1, the smaller the value, the less variety in the answers, the easier to understand, the larger the value, the larger the range of the AI's vocabulary, the more diverse", "settings.max_tokens.confirm": "Enable max tokens limit", "settings.max_tokens.confirm_content": "Enable max tokens limit, affects the length of the result. Need to consider the context limit of the model, otherwise an error will be reported", + "settings.thought_auto_collapse": "Automatically Collapse Thought Content", + "settings.thought_auto_collapse.tip": "Automatically collapse thought content after thinking ends", "suggestions.title": "Suggested Questions", "thinking": "Thinking", "topics.auto_rename": "Auto Rename", @@ -136,10 +139,13 @@ "topics.pinned": "Pinned Topics", "topics.title": "Topics", "topics.unpinned": "Unpinned Topics", + "topics.delete.shortcut": "Hold {{key}} to delete directly", "translate": "Translate", "topics.prompt": "Topic Prompts", "topics.prompt.tips": "Topic Prompts: Additional supplementary prompts provided for the current topic", - "topics.prompt.edit.title": "Edit Topic Prompts" + "topics.prompt.edit.title": "Edit Topic Prompts", + "artifacts.button.openExternal": "Open in external browser", + "artifacts.preview.openExternal.error.content": "Error opening the external browser." }, "common": { "add": "Add", @@ -314,7 +320,8 @@ "korean": "Korean", "portuguese": "Portuguese", "russian": "Russian", - "spanish": "Spanish" + "spanish": "Spanish", + "german": "German" }, "mermaid": { "download": { @@ -331,6 +338,18 @@ }, "title": "Mermaid Diagram" }, + "plantuml": { + "download": { + "png": "Download PNG", + "svg": "Download SVG", + "failed": "Download failed, please check the network" + }, + "tabs": { + "preview": "Preview", + "source": "Source" + }, + "title": "PlantUML Diagram" + }, "message": { "api.check.model.title": "Select the model to use for detection", "api.connection.failed": "Connection failed", @@ -352,7 +371,7 @@ "error.invalid.enter.model": "Please select a model", "error.invalid.proxy.url": "Invalid proxy URL", "error.invalid.webdav": "Invalid WebDAV settings", - "error.notion.export": "Notion import failed", + "error.notion.export": "Failed to export to Notion. Please check connection status and configuration according to documentation", "error.notion.no_api_key": "Notion ApiKey or Notion DatabaseID is not configured", "group.delete.content": "Deleting a group message will delete the user's question and all assistant's answers", "group.delete.title": "Delete Group Message", @@ -374,13 +393,13 @@ "reset.double.confirm.title": "DATA LOST !!!", "restore.success": "Restored successfully", "save.success.title": "Saved successfully", - "success.notion.export": "Notion import successful", + "success.notion.export": "Successfully exported to Notion", "switch.disabled": "Please wait for the current reply to complete", "topic.added": "New topic added", "upgrade.success.button": "Restart", "upgrade.success.content": "Please restart the application to complete the upgrade", "upgrade.success.title": "Upgrade successfully", - "warn.notion.exporting": "Notion is importing, please do not import repeatedly", + "warn.notion.exporting": "Exporting to Notion, please do not request export repeatedly!", "error.invalid.api.host": "Invalid API Host", "error.invalid.api.key": "Invalid API Key" }, @@ -563,35 +582,42 @@ "notion.api_key_placeholder": "Enter Notion API Key", "notion.database_id": "Notion Database ID", "notion.database_id_placeholder": "Enter Notion Database ID", + "notion.page_name_key": "Page Title Field Name", + "notion.page_name_key_placeholder": "Enter page title field name, default is Name", "notion.title": "Notion Configuration", + "notion.help": "Notion Configuration Documentation", "notion.check": { "button": "Check", - "fail": "Connection failed, please check the configuration", + "fail": "Connection failed, please check network and Api_key and Database_id", "success": "Connection successful", - "error": "Connection error, please check the network", + "error": "Connection error, please check network configuration and Api_key and Database_id", "empty_api_key": "Api_key is not configured", "empty_database_id": "Database_id is not configured" }, "title": "Data Settings", - "webdav.autoSync": "Auto Backup", - "webdav.autoSync.off": "Off", - "webdav.backup.button": "Backup to WebDAV", - "webdav.host": "WebDAV Host", - "webdav.host.placeholder": "http://localhost:8080", - "webdav.hours": "Hours", - "webdav.lastSync": "Last Backup", - "webdav.minutes": "Minutes", - "webdav.noSync": "Waiting for next backup", - "webdav.password": "WebDAV Password", - "webdav.path": "WebDAV Path", - "webdav.path.placeholder": "/backup", - "webdav.restore.button": "Restore from WebDAV", - "webdav.restore.content": "Restore from WebDAV will overwrite the current data, continue?", - "webdav.restore.title": "Restore from WebDAV", - "webdav.syncError": "Backup Error", - "webdav.syncStatus": "Backup Status", - "webdav.title": "WebDAV", - "webdav.user": "WebDAV User" + "webdav": { + "autoSync": "Auto Backup", + "autoSync.off": "Off", + "backup.button": "Backup to WebDAV", + "host": "WebDAV Host", + "host.placeholder": "http://localhost:8080", + "minute_interval_one": "{{count}} minute", + "minute_interval_other": "{{count}} minutes", + "hour_interval_one": "{{count}} hour", + "hour_interval_other": "{{count}} hours", + "lastSync": "Last Backup", + "noSync": "Waiting for next backup", + "password": "WebDAV Password", + "path": "WebDAV Path", + "path.placeholder": "/backup", + "restore.button": "Restore from WebDAV", + "restore.content": "Restore from WebDAV will overwrite the current data, continue?", + "restore.title": "Restore from WebDAV", + "syncError": "Backup Error", + "syncStatus": "Backup Status", + "title": "WebDAV", + "user": "WebDAV User" + } }, "display.custom.css": "Custom CSS", "display.custom.css.placeholder": "/* Put custom CSS here */", @@ -734,7 +760,8 @@ "toggle_show_topics": "Toggle Topics", "zoom_in": "Zoom In", "zoom_out": "Zoom Out", - "zoom_reset": "Reset Zoom" + "zoom_reset": "Reset Zoom", + "show_settings": "Open Settings" }, "theme.auto": "Auto", "theme.dark": "Dark", @@ -753,6 +780,7 @@ "translate": { "any.language": "Any language", "button.translate": "Translate", + "tooltip.newline": "Newline", "close": "Close", "confirm": { "content": "Translation will replace the original text, continue?", @@ -763,7 +791,14 @@ "input.placeholder": "Enter text to translate", "output.placeholder": "Translation", "processing": "Translation in progress...", - "title": "Translation" + "title": "Translation", + "history": { + "title": "Translation History", + "empty": "No translation history", + "clear": "Clear History", + "delete": "Delete", + "clear_description": "Clear history will delete all translation history, continue?" + } }, "tray": { "quit": "Quit", diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index 16f489ba..65830643 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -52,6 +52,7 @@ "settings.reasoning_effort.high": "長い", "settings.reasoning_effort.low": "短い", "settings.reasoning_effort.medium": "中程度", + "settings.reasoning_effort.off": "オフ", "settings.reasoning_effort.tip": "この設定は推論モデルのみサポートしています" }, "auth": { @@ -120,6 +121,8 @@ "settings.top_p.tip": "デフォルト値は1で、値が小さいほど回答の多様性が減り、理解しやすくなります。値が大きいほど、AIの語彙範囲が広がり、多様性が増します", "settings.max_tokens.confirm": "最大トークン制限を有効にする", "settings.max_tokens.confirm_content": "最大トークン制限を有効にすると、モデルが生成できる最大トークン数が制限されます。これにより、返される結果の長さに影響が出る可能性があります。モデルのコンテキスト制限に基づいて設定する必要があります。そうしないとエラーが発生します", + "settings.thought_auto_collapse": "思考内容を自動的に折りたたむ", + "settings.thought_auto_collapse.tip": "思考が終了したら思考内容を自動的に折りたたみます", "suggestions.title": "提案された質問", "thinking": "思考中...", "topics.auto_rename": "自動リネーム", @@ -136,10 +139,13 @@ "topics.pinned": "トピックを固定", "topics.title": "トピック", "topics.unpinned": "固定解除", + "topics.delete.shortcut": "{{key}}キーを押しながらで直接削除", "translate": "翻訳", "topics.prompt": "トピック提示語", "topics.prompt.tips": "トピック提示語:現在のトピックに対して追加の補足提示語を提供", - "topics.prompt.edit.title": "トピック提示語を編集する" + "topics.prompt.edit.title": "トピック提示語を編集する", + "artifacts.button.openExternal": "外部ブラウザで開く", + "artifacts.preview.openExternal.error.content": "外部ブラウザの起動に失敗しました。" }, "common": { "add": "追加", @@ -309,6 +315,7 @@ "chinese-traditional": "繁体字中国語", "english": "英語", "french": "フランス語", + "german": "ドイツ語", "italian": "イタリア語", "japanese": "日本語", "korean": "韓国語", @@ -351,7 +358,7 @@ "error.invalid.enter.model": "モデルを選択してください", "error.invalid.proxy.url": "無効なプロキシURL", "error.invalid.webdav": "無効なWebDAV設定", - "error.notion.export": "Notion インポートに失敗", + "error.notion.export": "Notionへのエクスポートに失敗しました。接続状態と設定を確認してください", "error.notion.no_api_key": "Notion ApiKey または Notion DatabaseID が設定されていません", "group.delete.content": "分組メッセージを削除するとユーザーの質問と助け手の回答がすべて削除されます", "group.delete.title": "分組メッセージを削除", @@ -373,13 +380,13 @@ "reset.double.confirm.title": "データが失われます!!!", "restore.success": "復元に成功しました", "save.success.title": "保存に成功しました", - "success.notion.export": "Notion へのインポートに成功", + "success.notion.export": "Notionへのエクスポートに成功しました", "switch.disabled": "現在の応答が完了するまで切り替えを無効にします", "topic.added": "新しいトピックが追加されました", "upgrade.success.button": "再起動", "upgrade.success.content": "アップグレードを完了するためにアプリケーションを再起動してください", "upgrade.success.title": "アップグレードに成功しました", - "warn.notion.exporting": "Notion 正在インポート中です。重複インポートしないでください。", + "warn.notion.exporting": "Notionにエクスポート中です。重複してエクスポートしないでください! ", "error.enter.name": "ナレッジベース名を入力してください", "error.invalid.api.host": "無効なAPIアドレスです", "error.invalid.api.key": "無効なAPIキーです" @@ -563,16 +570,42 @@ "notion.api_key_placeholder": "Notion APIキーを入力してください", "notion.database_id": "Notion データベースID", "notion.database_id_placeholder": "Notion データベースIDを入力してください", + "notion.page_name_key": "ページタイトルフィールド名", + "notion.page_name_key_placeholder": "ページタイトルフィールド名を入力してください。デフォルトは Name です", "notion.title": "Notion 設定", + "notion.help": "Notion 設定ドキュメント", "notion.check": { "button": "確認", - "fail": "接続に失敗しました。設定を確認してください。", + "fail": "接続エラー、ネットワーク設定とApi_keyとDatabase_idを確認してください", "success": "接続に成功しました。", - "error": "接続エラーが発生しました。ネットワークを確認してください。", + "error": "接続エラー、ネットワーク設定とApi_keyとDatabase_idを確認してください", "empty_api_key": "Api_keyが設定されていません", "empty_database_id": "Database_idが設定されていません" }, "title": "データ設定", + "webdav": { + "autoSync": "自動バックアップ", + "autoSync.off": "オフ", + "backup.button": "WebDAVにバックアップ", + "host": "WebDAVホスト", + "host.placeholder": "http://localhost:8080", + "minute_interval_one": "{{count}} 分", + "minute_interval_other": "{{count}} 分", + "hour_interval_one": "{{count}} 時間", + "hour_interval_other": "{{count}} 時間", + "lastSync": "最終バックアップ", + "noSync": "次回のバックアップを待機中", + "password": "WebDAVパスワード", + "path": "WebDAVパス", + "path.placeholder": "/backup", + "restore.button": "WebDAVから復元", + "restore.content": "WebDAVから復元すると現在のデータが上書きされます。続行しますか?", + "restore.title": "WebDAVから復元", + "syncError": "バックアップエラー", + "syncStatus": "バックアップ状態", + "title": "WebDAV", + "user": "WebDAVユーザー" + }, "webdav.autoSync": "自動バックアップ", "webdav.autoSync.off": "オフ", "webdav.backup.button": "WebDAVにバックアップ", @@ -591,7 +624,11 @@ "webdav.syncError": "バックアップエラー", "webdav.syncStatus": "バックアップ状態", "webdav.title": "WebDAV", - "webdav.user": "WebDAVユーザー" + "webdav.user": "WebDAVユーザー", + "minute_interval_one": "{{count}} 分", + "minute_interval_other": "{{count}} 分", + "hour_interval_one": "{{count}} 時間", + "hour_interval_other": "{{count}} 時間" }, "display.custom.css": "カスタムCSS", "display.custom.css.placeholder": "/* ここにカスタムCSSを入力 */", @@ -734,7 +771,8 @@ "toggle_show_topics": "トピックの表示を切り替え", "zoom_in": "ズームイン", "zoom_out": "ズームアウト", - "zoom_reset": "ズームをリセット" + "zoom_reset": "ズームをリセット", + "show_settings": "設定を開く" }, "theme.auto": "自動", "theme.dark": "ダークテーマ", @@ -753,6 +791,7 @@ "translate": { "any.language": "任意の言語", "button.translate": "翻訳", + "tooltip.newline": "改行", "close": "閉じる", "confirm": { "content": "翻訳すると元のテキストが上書きされます。続行しますか?", diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index 35b5cbd5..ef062c7e 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -52,6 +52,7 @@ "settings.reasoning_effort.high": "Длинная", "settings.reasoning_effort.low": "Короткая", "settings.reasoning_effort.medium": "Средняя", + "settings.reasoning_effort.off": "Выключено", "settings.reasoning_effort.tip": "Эта настройка поддерживается только моделями с рассуждением" }, "auth": { @@ -120,6 +121,8 @@ "settings.top_p.tip": "Значение по умолчанию 1, чем меньше значение, тем меньше вариативности в ответах, тем проще понять, чем больше значение, тем больше вариативности в ответах, тем больше разнообразие", "settings.max_tokens.confirm": "Включить лимит максимальных токенов", "settings.max_tokens.confirm_content": "Включить лимит максимальных токенов, влияет на длину результата. Нужно учитывать контекст модели, иначе будет ошибка", + "settings.thought_auto_collapse": "Автоматически сворачивать содержание мыслей", + "settings.thought_auto_collapse.tip": "Автоматически сворачивать содержание мыслей после завершения размышления", "suggestions.title": "Предложенные вопросы", "thinking": "Мыслим", "topics.auto_rename": "Автопереименование", @@ -136,10 +139,13 @@ "topics.pinned": "Закрепленные темы", "topics.title": "Топики", "topics.unpinned": "Открепленные темы", + "topics.delete.shortcut": "Удерживайте {{key}} для мгновенного удаления", "translate": "Перевести", "topics.prompt": "Тематические подсказки", "topics.prompt.tips": "Тематические подсказки: Дополнительные подсказки, предоставленные для текущей темы", - "topics.prompt.edit.title": "Редактировать подсказки темы" + "topics.prompt.edit.title": "Редактировать подсказки темы", + "artifacts.button.openExternal": "Открыть во внешнем браузере", + "artifacts.preview.openExternal.error.content": "Внешний браузер открылся с ошибкой" }, "common": { "add": "Добавить", @@ -309,6 +315,7 @@ "chinese-traditional": "Китайский традиционный", "english": "Английский", "french": "Французский", + "german": "Немецкий", "italian": "Итальянский", "japanese": "Японский", "korean": "Корейский", @@ -352,7 +359,7 @@ "error.invalid.enter.model": "Пожалуйста, выберите модель", "error.invalid.proxy.url": "Неверный URL прокси", "error.invalid.webdav": "Неверные настройки WebDAV", - "error.notion.export": "Импорт в Notion не удался", + "error.notion.export": "Ошибка экспорта в Notion, пожалуйста, проверьте состояние подключения и настройки в документации", "error.notion.no_api_key": "Notion ApiKey или Notion DatabaseID не настроен", "group.delete.content": "Удаление группы сообщений удалит пользовательский вопрос и все ответы помощника", "group.delete.title": "Удалить группу сообщений", @@ -374,13 +381,13 @@ "reset.double.confirm.title": "ДАННЫЕ БУДУТ УТЕРЯНЫ !!!", "restore.success": "Успешно восстановлено", "save.success.title": "Успешно сохранено", - "success.notion.export": "Импорт в Notion выполнен успешно", + "success.notion.export": "Успешный экспорт в Notion", "switch.disabled": "Пожалуйста, дождитесь завершения текущего ответа", "topic.added": "Новый топик добавлен", "upgrade.success.button": "Перезапустить", "upgrade.success.content": "Пожалуйста, перезапустите приложение для завершения обновления", "upgrade.success.title": "Обновление успешно", - "warn.notion.exporting": "Идет импорт в Notion, пожалуйста, не повторяйте импорт", + "warn.notion.exporting": "Экспортируется в Notion, пожалуйста, не отправляйте повторные запросы!", "error.invalid.api.host": "Неверный API адрес", "error.invalid.api.key": "Неверный API ключ" }, @@ -563,35 +570,44 @@ "notion.api_key_placeholder": "Введите ключ API Notion", "notion.database_id": "ID базы данных Notion", "notion.database_id_placeholder": "Введите ID базы данных Notion", + "notion.page_name_key": "Название поля заголовка страницы", + "notion.page_name_key_placeholder": "Введите название поля заголовка страницы, по умолчанию Name", "notion.title": "Настройки Notion", + "notion.help": "Документация по настройке Notion", "notion.check": { "button": "Проверить", - "fail": "Ошибка подключения, проверьте настройки", + "fail": "Не удалось подключиться, пожалуйста, проверьте сеть и правильность Api_key и Database_id", "success": "Подключение успешно", - "error": "Ошибка подключения, проверьте сеть", + "error": "Аномалия в подключении, пожалуйста, проверьте настройки сети, а также правильность Api_key и Database_id", "empty_api_key": "Не настроен Api_key", "empty_database_id": "Не настроен Database_id" }, "title": "Настройки данных", - "webdav.autoSync": "Автоматическое резервное копирование", - "webdav.autoSync.off": "Выключено", - "webdav.backup.button": "Резервное копирование на WebDAV", - "webdav.host": "Хост WebDAV", - "webdav.host.placeholder": "http://localhost:8080", - "webdav.hours": "часов", - "webdav.lastSync": "Последняя синхронизация", - "webdav.minutes": "минут", - "webdav.noSync": "Ожидание следующего резервного копирования", - "webdav.password": "Пароль WebDAV", - "webdav.path": "Путь WebDAV", - "webdav.path.placeholder": "/backup", - "webdav.restore.button": "Восстановление с WebDAV", - "webdav.restore.content": "Восстановление с WebDAV перезапишет текущие данные, продолжить?", - "webdav.restore.title": "Восстановление с WebDAV", - "webdav.syncError": "Ошибка резервного копирования", - "webdav.syncStatus": "Статус резервного копирования", - "webdav.title": "WebDAV", - "webdav.user": "Пользователь WebDAV" + "webdav": { + "autoSync": "Автоматическое резервное копирование", + "autoSync.off": "Выключено", + "backup.button": "Резервное копирование на WebDAV", + "host": "Хост WebDAV", + "host.placeholder": "http://localhost:8080", + "minute_interval_one": "{{count}} минута", + "minute_interval_few": "{{count}} минуты", + "minute_interval_many": "{{count}} минут", + "hour_interval_one": "{{count}} час", + "hour_interval_few": "{{count}} часа", + "hour_interval_many": "{{count}} часов", + "lastSync": "Последняя синхронизация", + "noSync": "Ожидание следующего резервного копирования", + "password": "Пароль WebDAV", + "path": "Путь WebDAV", + "path.placeholder": "/backup", + "restore.button": "Восстановление с WebDAV", + "restore.content": "Восстановление с WebDAV перезапишет текущие данные, продолжить?", + "restore.title": "Восстановление с WebDAV", + "syncError": "Ошибка резервного копирования", + "syncStatus": "Статус резервного копирования", + "title": "WebDAV", + "user": "Пользователь WebDAV" + } }, "display.custom.css": "Пользовательский CSS", "display.custom.css.placeholder": "/* Здесь введите пользовательский CSS */", @@ -733,7 +749,8 @@ "toggle_show_topics": "Переключить отображение топиков", "zoom_in": "Увеличить", "zoom_out": "Уменьшить", - "zoom_reset": "Сбросить масштаб" + "zoom_reset": "Сбросить масштаб", + "show_settings": "Открыть настройки" }, "theme.auto": "Автоматически", "theme.dark": "Темная", @@ -753,6 +770,7 @@ "translate": { "any.language": "Любой язык", "button.translate": "Перевести", + "tooltip.newline": "Перевести", "close": "Закрыть", "confirm": { "content": "Перевод заменит исходный текст, продолжить?", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index a5a051b4..72204c44 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -51,6 +51,7 @@ "settings.reasoning_effort.high": "长", "settings.reasoning_effort.low": "短", "settings.reasoning_effort.medium": "中", + "settings.reasoning_effort.off": "关", "settings.reasoning_effort.tip": "该设置仅支持推理模型", "title": "助手" }, @@ -73,6 +74,8 @@ "add.assistant.title": "添加助手", "artifacts.button.download": "下载", "artifacts.button.preview": "预览", + "artifacts.button.openExternal": "外部浏览器打开", + "artifacts.preview.openExternal.error.content": "外部浏览器打开出错", "assistant.search.placeholder": "搜索", "deeply_thought": "已深度思考(用时 {{secounds}} 秒)", "default.description": "你好,我是默认助手。你可以立刻开始跟我聊天。", @@ -120,6 +123,8 @@ "settings.top_p.tip": "默认值为 1,值越小,AI 生成的内容越单调,也越容易理解;值越大,AI 回复的词汇围越大,越多样化", "settings.max_tokens.confirm": "开启消息长度限制", "settings.max_tokens.confirm_content": "开启消息长度限制后,单次交互所用的最大 Token 数, 会影响返回结果的长度。要根据模型上下文限制来设置,否则会报错", + "settings.thought_auto_collapse": "思考内容自动折叠", + "settings.thought_auto_collapse.tip": "思考结束后思考内容自动折叠", "suggestions.title": "建议的问题", "thinking": "思考中", "topics.auto_rename": "生成话题名", @@ -136,10 +141,10 @@ "topics.pinned": "固定话题", "topics.title": "话题", "topics.unpinned": "取消固定", + "topics.delete.shortcut": "按住 {{key}} 可直接删除", "translate": "翻译", "topics.prompt": "话题提示词", - "topics.prompt.tips": "话题提示词: 针对当前话题提供额外的补充提示词", - "topics.prompt.edit.title": "编辑话题提示词" + "topics.prompt.tips": "话题提示词: 针对当前话题提供额外的补充提示词" }, "common": { "add": "添加", @@ -314,7 +319,8 @@ "korean": "韩文", "portuguese": "葡萄牙文", "russian": "俄文", - "spanish": "西班牙文" + "spanish": "西班牙文", + "german": "德文" }, "mermaid": { "download": { @@ -331,6 +337,18 @@ }, "title": "Mermaid 图表" }, + "plantuml": { + "title": "PlantUML 图表", + "download": { + "png": "下载 PNG", + "svg": "下载 SVG", + "failed": "下载失败,请检查网络" + }, + "tabs": { + "preview": "预览", + "source": "源码" + } + }, "message": { "api.check.model.title": "请选择要检测的模型", "api.connection.failed": "连接失败", @@ -354,7 +372,7 @@ "error.invalid.enter.model": "请选择一个模型", "error.invalid.proxy.url": "无效的代理地址", "error.invalid.webdav": "无效的 WebDAV 设置", - "error.notion.export": "Notion 导入失败", + "error.notion.export": "导出Notion错误,请检查连接状态并对照文档检查配置", "error.notion.no_api_key": "未配置Notion ApiKey或Notion DatabaseID", "group.delete.content": "删除分组消息会删除用户提问和所有助手的回答", "group.delete.title": "删除分组消息", @@ -376,13 +394,13 @@ "reset.double.confirm.title": "数据丢失!!!", "restore.success": "恢复成功", "save.success.title": "保存成功", - "success.notion.export": "导入Notion成功", + "success.notion.export": "成功导出到Notion", "switch.disabled": "请等待当前回复完成后操作", "topic.added": "话题添加成功", "upgrade.success.button": "重启", "upgrade.success.content": "重启用以完成升级", "upgrade.success.title": "升级成功", - "warn.notion.exporting": "Notion正在导入,请勿重复导入" + "warn.notion.exporting": "正在导出到Notion, 请勿重复请求导出!" }, "minapp": { "sidebar.add.title": "添加到侧边栏", @@ -563,35 +581,46 @@ "notion.api_key_placeholder": "请输入Notion 密钥", "notion.database_id": "Notion 数据库 ID", "notion.database_id_placeholder": "请输入Notion 数据库 ID", + "notion.page_name_key": "页面标题字段名", + "notion.page_name_key_placeholder": "请输入页面标题字段名,默认为 Name", "notion.title": "Notion 配置", + "notion.help": "Notion 配置文档", "notion.check": { "button": "检查", - "fail": "连接失败,请检查配置", + "fail": "连接失败,请检查网络及Api_key和Database_id是否正确", "success": "连接成功", - "error": "连接异常,请检查网络", + "error": "连接异常,请检查网络及Api_key和Database_id是否正确", "empty_api_key": "未配置Api_key", "empty_database_id": "未配置Database_id" }, "title": "数据设置", - "webdav.autoSync": "自动备份", - "webdav.autoSync.off": "关闭", - "webdav.backup.button": "备份到 WebDAV", - "webdav.host": "WebDAV 地址", - "webdav.host.placeholder": "http://localhost:8080", - "webdav.hours": "小时", - "webdav.lastSync": "上次备份时间", - "webdav.minutes": "分钟", - "webdav.noSync": "等待下次备份", - "webdav.password": "WebDAV 密码", - "webdav.path": "WebDAV 路径", - "webdav.path.placeholder": "/backup", - "webdav.restore.button": "从 WebDAV 恢复", - "webdav.restore.content": "从 WebDAV 恢复将覆盖当前数据,是否继续?", - "webdav.restore.title": "从 WebDAV 恢复", - "webdav.syncError": "备份错误", - "webdav.syncStatus": "备份状态", - "webdav.title": "WebDAV", - "webdav.user": "WebDAV 用户名" + "webdav": { + "autoSync": "自动备份", + "autoSync.off": "关闭", + "backup.button": "备份到 WebDAV", + "host": "WebDAV 地址", + "host.placeholder": "http://localhost:8080", + "minute_interval_one": "{{count}} 分钟", + "minute_interval_other": "{{count}} 分钟", + "hour_interval_one": "{{count}} 小时", + "hour_interval_other": "{{count}} 小时", + "lastSync": "上次备份时间", + "noSync": "等待下次备份", + "password": "WebDAV 密码", + "path": "WebDAV 路径", + "path.placeholder": "/backup", + "restore.button": "从 WebDAV 恢复", + "restore.content": "从 WebDAV 恢复将覆盖当前数据,是否继续?", + "restore.title": "从 WebDAV 恢复", + "syncError": "备份错误", + "syncStatus": "备份状态", + "title": "WebDAV", + "user": "WebDAV 用户名" + }, + "minute_interval_one": "{{count}} 分钟", + "minute_interval_other": "{{count}} 分钟", + "hour_interval_one": "{{count}} 小时", + "hour_interval_other": "{{count}} 小时" }, "display.custom.css": "自定义 CSS", "display.custom.css.placeholder": "/* 这里写自定义CSS */", @@ -728,6 +757,7 @@ "reset_to_default": "重置为默认", "search_message": "搜索消息", "show_app": "显示应用", + "show_settings": "打开设置", "title": "快捷方式", "toggle_new_context": "清除上下文", "toggle_show_assistants": "切换助手显示", @@ -753,6 +783,7 @@ "translate": { "any.language": "任意语言", "button.translate": "翻译", + "tooltip.newline": "换行", "close": "关闭", "confirm": { "content": "翻译后将覆盖原文,是否继续?", @@ -763,7 +794,14 @@ "input.placeholder": "输入文本进行翻译", "output.placeholder": "翻译", "processing": "翻译中...", - "title": "翻译" + "title": "翻译", + "history": { + "title": "翻译历史", + "empty": "暂无翻译历史", + "clear": "清空历史", + "delete": "删除", + "clear_description": "清空历史将删除所有翻译历史记录,是否继续?" + } }, "tray": { "quit": "退出", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index da003c51..d2dd26ae 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -51,6 +51,7 @@ "settings.reasoning_effort.high": "長", "settings.reasoning_effort.low": "短", "settings.reasoning_effort.medium": "中", + "settings.reasoning_effort.off": "關", "settings.reasoning_effort.tip": "該設置僅支持推理模型", "title": "助手" }, @@ -120,6 +121,8 @@ "settings.top_p.tip": "模型生成文本的隨機程度。值越小,AI 生成的內容越單調,也越容易理解;值越大,AI 回覆的詞彙範圍越大,越多樣化", "settings.max_tokens.confirm": "啟用消息長度限制", "settings.max_tokens.confirm_content": "啟用消息長度限制後,單次交互所用的最大 Token 數, 會影響返回結果的長度。要根據模型上下文限制來設置,否則會報錯", + "settings.thought_auto_collapse": "思考內容自動折疊", + "settings.thought_auto_collapse.tip": "思考結束後思考內容自動折疊", "suggestions.title": "建議的問題", "thinking": "思考中", "topics.auto_rename": "自動重新命名", @@ -136,10 +139,13 @@ "topics.pinned": "固定話題", "topics.title": "話題", "topics.unpinned": "取消固定", + "topics.delete.shortcut": "按住 {{key}} 可直接刪除", "translate": "翻譯", "topics.prompt": "話題提示詞", "topics.prompt.tips": "話題提示詞:針對目前話題提供額外的補充提示詞", - "topics.prompt.edit.title": "編輯話題提示詞" + "topics.prompt.edit.title": "編輯話題提示詞", + "artifacts.button.openExternal": "外部瀏覽器打開", + "artifacts.preview.openExternal.error.content": "外部瀏覽器打開出錯" }, "common": { "add": "添加", @@ -314,7 +320,8 @@ "korean": "韓文", "portuguese": "葡萄牙文", "russian": "俄文", - "spanish": "西班牙文" + "spanish": "西班牙文", + "german": "德文" }, "mermaid": { "download": { @@ -352,7 +359,7 @@ "error.invalid.enter.model": "請選擇一個模型", "error.invalid.proxy.url": "無效的代理 URL", "error.invalid.webdav": "無效的 WebDAV 設定", - "error.notion.export": "Notion 匯入失敗", + "error.notion.export": "導出Notion錯誤,請檢查連接狀態並對照文檔檢查配置", "error.notion.no_api_key": "未配置 Notion ApiKey 或 Notion DatabaseID", "group.delete.content": "刪除分組消息會刪除用戶提問和所有助手的回答", "group.delete.title": "刪除分組消息", @@ -374,13 +381,13 @@ "reset.double.confirm.title": "資料將會丟失!!!", "restore.success": "恢復成功", "save.success.title": "保存成功", - "success.notion.export": "匯入 Notion 成功", + "success.notion.export": "成功導出到Notion", "switch.disabled": "請等待當前回覆完成", "topic.added": "新話題已添加", "upgrade.success.button": "重新啟動", "upgrade.success.content": "請重新啟動應用以完成升級", "upgrade.success.title": "升級成功", - "warn.notion.exporting": "Notion 正在匯入,請勿重複匯入", + "warn.notion.exporting": "正在導出到Notion,請勿重複請求導出!", "error.invalid.api.host": "無效的 API 位址", "error.invalid.api.key": "無效的 API 密鑰" }, @@ -557,41 +564,52 @@ "title": "清除緩存" }, "data.title": "數據目錄", - "notion.api_key": "Notion 金鑰", + "notion.api_key": "Notion 密鑰", "notion.api_key_placeholder": "請輸入Notion 密鑰", "notion.database_id": "Notion 資料庫 ID", "notion.database_id_placeholder": "請輸入Notion 資料庫 ID", + "notion.page_name_key": "頁面標題欄位名稱", + "notion.page_name_key_placeholder": "請輸入頁面標題欄位名稱,預設為 Name", "notion.title": "Notion 配置", + "notion.help": "Notion 配置文檔", "notion.check": { "button": "檢查", - "fail": "連線失敗,請檢查配置", + "fail": "連接失敗,請檢查網絡及Api_key和Database_id是否正確", "success": "連線成功", - "error": "連線異常,請檢查網路", + "error": "連接異常,請檢查網絡及Api_key和Database_id是否正確", "empty_api_key": "未配置Api_key", "empty_database_id": "未配置Database_id" }, "title": "數據設定", - "webdav.autoSync": "自動備份", - "webdav.autoSync.off": "關閉", - "webdav.backup.button": "從 WebDAV 備份", - "webdav.host": "WebDAV 主機位址", - "webdav.host.placeholder": "http://localhost:8080", - "webdav.hours": "小時", - "webdav.lastSync": "上次同步時間", - "webdav.minutes": "分鐘", - "webdav.noSync": "等待下次備份", - "webdav.password": "WebDAV 密碼", - "webdav.path": "WebDAV Path", - "webdav.path.placeholder": "/backup", - "webdav.restore.button": "從 WebDAV 恢復", - "webdav.restore.content": "從 WebDAV 恢復將覆蓋當前資料,是否繼續?", - "webdav.restore.title": "從 WebDAV 恢復", - "webdav.syncError": "備份錯誤", - "webdav.syncStatus": "備份狀態", - "webdav.title": "WebDAV", - "webdav.user": "WebDAV 使用者名稱", + "webdav": { + "autoSync": "自動備份", + "autoSync.off": "關閉", + "backup.button": "備份到 WebDAV", + "host": "WebDAV 主機位址", + "host.placeholder": "http://localhost:8080", + "minute_interval_one": "{{count}} 分鐘", + "minute_interval_other": "{{count}} 分鐘", + "hour_interval_one": "{{count}} 小時", + "hour_interval_other": "{{count}} 小時", + "lastSync": "上次備份時間", + "noSync": "等待下次備份", + "password": "WebDAV 密碼", + "path": "WebDAV 路徑", + "path.placeholder": "/backup", + "restore.button": "從 WebDAV 恢復", + "restore.content": "從 WebDAV 恢復將覆蓋當前資料,是否繼續?", + "restore.title": "從 WebDAV 恢復", + "syncError": "備份錯誤", + "syncStatus": "備份狀態", + "title": "WebDAV", + "user": "WebDAV 使用者名稱" + }, "app_data": "應用數據", - "app_logs": "應用日誌" + "app_logs": "應用日誌", + "minute_interval_one": "{{count}} 分鐘", + "minute_interval_other": "{{count}} 分鐘", + "hour_interval_one": "{{count}} 小時", + "hour_interval_other": "{{count}} 小時" }, "display.custom.css": "自定義 CSS", "display.custom.css.placeholder": "/* 這裡寫自定義 CSS */", @@ -733,7 +751,8 @@ "toggle_show_topics": "切換話題顯示", "zoom_in": "放大界面", "zoom_out": "縮小界面", - "zoom_reset": "重置縮放" + "zoom_reset": "重置縮放", + "show_settings": "打開設定" }, "theme.auto": "自動", "theme.dark": "深色主題", @@ -753,6 +772,7 @@ "translate": { "any.language": "任意語言", "button.translate": "翻譯", + "tooltip.newline": "換行", "close": "關閉", "confirm": { "content": "翻譯後將覆蓋原文,是否繼續?", diff --git a/src/renderer/src/pages/agents/components/AddAgentPopup.tsx b/src/renderer/src/pages/agents/components/AddAgentPopup.tsx index 506a28c8..8e9927ce 100644 --- a/src/renderer/src/pages/agents/components/AddAgentPopup.tsx +++ b/src/renderer/src/pages/agents/components/AddAgentPopup.tsx @@ -25,7 +25,7 @@ type FieldType = { id: string name: string prompt: string - knowledge_base_id: string[] + knowledge_base_ids: string[] } const PopupContainer: React.FC = ({ resolve }) => { @@ -57,9 +57,9 @@ const PopupContainer: React.FC = ({ resolve }) => { const _agent: Agent = { id: uuid(), name: values.name, - knowledge_bases: values.knowledge_base_id - .map((id) => knowledgeState.bases.find((t) => t.id === id)) - .filter((base): base is KnowledgeBase => base !== undefined), + knowledge_bases: values.knowledge_base_ids + ?.map((id) => knowledgeState.bases.find((t) => t.id === id)) + ?.filter((base): base is KnowledgeBase => base !== undefined), emoji: _emoji, prompt: values.prompt, defaultModel: getDefaultModel(), @@ -156,7 +156,7 @@ const PopupContainer: React.FC = ({ resolve }) => { />
{showKnowledgeIcon && ( - + = ({ assistant: _assistant, activeTopic, setActiveTopic const borderRadius = showTopicTime ? 12 : 'var(--list-item-border-radius)' + const [deletingTopicId, setDeletingTopicId] = useState(null) + const deleteTimerRef = useRef() + + const handleDeleteClick = useCallback((topicId: string, e: React.MouseEvent) => { + e.stopPropagation() + + if (deleteTimerRef.current) { + clearTimeout(deleteTimerRef.current) + } + + setDeletingTopicId(topicId) + + deleteTimerRef.current = setTimeout(() => setDeletingTopicId(null), 2000) + }, []) + + const onClearMessages = useCallback(() => { + window.keyv.set(EVENT_NAMES.CHAT_COMPLETION_PAUSED, true) + store.dispatch(setGenerating(false)) + EventEmitter.emit(EVENT_NAMES.CLEAR_MESSAGES) + }, []) + + const handleConfirmDelete = useCallback( + async (topic: Topic, e: React.MouseEvent) => { + e.stopPropagation() + if (assistant.topics.length === 1) { + return onClearMessages() + } + await modelGenerating() + const index = findIndex(assistant.topics, (t) => t.id === topic.id) + setActiveTopic(assistant.topics[index + 1 === assistant.topics.length ? index - 1 : index + 1]) + removeTopic(topic) + setDeletingTopicId(null) + }, + [assistant.topics, onClearMessages, removeTopic, setActiveTopic] + ) + const onPinTopic = useCallback( (topic: Topic) => { const updatedTopic = { ...topic, pinned: !topic.pinned } @@ -54,7 +91,7 @@ const Topics: FC = ({ assistant: _assistant, activeTopic, setActiveTopic async (topic: Topic) => { await modelGenerating() const index = findIndex(assistant.topics, (t) => t.id === topic.id) - setActiveTopic(assistant.topics[index + 1 === assistant.topics.length ? 0 : index + 1]) + setActiveTopic(assistant.topics[index + 1 === assistant.topics.length ? index - 1 : index + 1]) removeTopic(topic) }, [assistant.topics, removeTopic, setActiveTopic] @@ -78,12 +115,6 @@ const Topics: FC = ({ assistant: _assistant, activeTopic, setActiveTopic [setActiveTopic] ) - const onClearMessages = useCallback(() => { - window.keyv.set(EVENT_NAMES.CHAT_COMPLETION_PAUSED, true) - store.dispatch(setGenerating(false)) - EventEmitter.emit(EVENT_NAMES.CLEAR_MESSAGES) - }, []) - const getTopicMenuItems = useCallback( (topic: Topic) => { const menus: MenuProps['items'] = [ @@ -219,7 +250,7 @@ const Topics: FC = ({ assistant: _assistant, activeTopic, setActiveTopic return menus }, - [assistant, assistants, onClearMessages, onPinTopic, onDeleteTopic, onMoveTopic, t, updateTopic] + [assistant, assistants, onClearMessages, onDeleteTopic, onPinTopic, onMoveTopic, t, updateTopic] ) return ( @@ -244,17 +275,34 @@ const Topics: FC = ({ assistant: _assistant, activeTopic, setActiveTopic )} {topic.pinned && } {isActive && !topic.pinned && ( - { - e.stopPropagation() - if (assistant.topics.length === 1) { - return onClearMessages() - } - onDeleteTopic(topic) - }}> - - + +
+ {t('chat.topics.delete.shortcut', { key: isMac ? '⌘' : 'Ctrl' })} +
+ + }> + { + if (e.ctrlKey || e.metaKey) { + handleConfirmDelete(topic, e) + } else if (deletingTopicId === topic.id) { + handleConfirmDelete(topic, e) + } else { + handleDeleteClick(topic.id, e) + } + }}> + {deletingTopicId === topic.id ? ( + + ) : ( + + )} + +
)} diff --git a/src/renderer/src/pages/knowledge/KnowledgeContent.tsx b/src/renderer/src/pages/knowledge/KnowledgeContent.tsx index ef4bb511..237a1e52 100644 --- a/src/renderer/src/pages/knowledge/KnowledgeContent.tsx +++ b/src/renderer/src/pages/knowledge/KnowledgeContent.tsx @@ -53,6 +53,7 @@ const KnowledgeContent: FC = ({ selectedBase }) => { addSitemap, removeItem, getProcessingStatus, + getDirectoryProcessingPercent, addNote, addDirectory } = useKnowledge(selectedBase.id || '') @@ -64,6 +65,8 @@ const KnowledgeContent: FC = ({ selectedBase }) => { return null } + const progressingPercent = getDirectoryProcessingPercent(base?.id) + const handleAddFile = () => { if (disabled) { return @@ -239,7 +242,7 @@ const KnowledgeContent: FC = ({ selectedBase }) => { {item.uniqueId && + + setName(e.target.value)} + onBlur={onUpdate} + style={{ flex: 1 }} + /> + {t('common.prompt')} diff --git a/src/renderer/src/pages/settings/DataSettings/DataSettings.tsx b/src/renderer/src/pages/settings/DataSettings/DataSettings.tsx index 9fc325b6..887c564d 100644 --- a/src/renderer/src/pages/settings/DataSettings/DataSettings.tsx +++ b/src/renderer/src/pages/settings/DataSettings/DataSettings.tsx @@ -5,9 +5,9 @@ import MinApp from '@renderer/components/MinApp' import { useTheme } from '@renderer/context/ThemeProvider' import { backup, reset, restore } from '@renderer/services/BackupService' import { RootState, useAppDispatch } from '@renderer/store' -import { setNotionApiKey, setNotionDatabaseID } from '@renderer/store/settings' +import { setNotionApiKey, setNotionDatabaseID, setNotionPageNameKey } from '@renderer/store/settings' import { AppInfo } from '@renderer/types' -import { Button, Modal, Typography } from 'antd' +import { Button, Modal, Tooltip, Typography } from 'antd' import Input from 'antd/es/input/Input' import { FC, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -23,10 +23,9 @@ const NotionSettings: FC = () => { const { theme } = useTheme() const dispatch = useAppDispatch() - // 这里可以添加 Notion 相关的状态和逻辑 - // 例如: const notionApiKey = useSelector((state: RootState) => state.settings.notionApiKey) const notionDatabaseID = useSelector((state: RootState) => state.settings.notionDatabaseID) + const notionPageNameKey = useSelector((state: RootState) => state.settings.notionPageNameKey) const handleNotionTokenChange = (e: React.ChangeEvent) => { dispatch(setNotionApiKey(e.target.value)) @@ -35,6 +34,11 @@ const NotionSettings: FC = () => { const handleNotionDatabaseIdChange = (e: React.ChangeEvent) => { dispatch(setNotionDatabaseID(e.target.value)) } + + const handleNotionPageNameKeyChange = (e: React.ChangeEvent) => { + dispatch(setNotionPageNameKey(e.target.value)) + } + const handleNotionConnectionCheck = () => { if (notionApiKey === null) { window.message.error(t('settings.data.notion.check.empty_api_key')) @@ -73,15 +77,17 @@ const NotionSettings: FC = () => { {t('settings.data.notion.title')} - + + + {t('settings.data.notion.database_id')} - + { + + {t('settings.data.notion.page_name_key')} + + + + + {t('settings.data.notion.api_key')} - + { style={{ width: 250 }} placeholder={t('settings.data.notion.api_key_placeholder')} /> - + {/* 添加分割线 */} diff --git a/src/renderer/src/pages/settings/DataSettings/WebDavSettings.tsx b/src/renderer/src/pages/settings/DataSettings/WebDavSettings.tsx index d641ee8f..dd9033dc 100644 --- a/src/renderer/src/pages/settings/DataSettings/WebDavSettings.tsx +++ b/src/renderer/src/pages/settings/DataSettings/WebDavSettings.tsx @@ -40,7 +40,7 @@ const WebDavSettings: FC = () => { const dispatch = useAppDispatch() - const { t, i18n } = useTranslation() + const { t } = useTranslation() const { webdavSync } = useRuntime() @@ -163,7 +163,6 @@ const WebDavSettings: FC = () => { {t('settings.general.backup.title')} - {/* 添加 在线备份 在线还原 按钮 */} @@ -177,19 +176,15 @@ const WebDavSettings: FC = () => { {t('settings.data.webdav.autoSync')} {webdavSync && syncInterval > 0 && ( diff --git a/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx b/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx index af234315..acacf19d 100644 --- a/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx +++ b/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx @@ -10,7 +10,7 @@ import { import { HStack } from '@renderer/components/Layout' import ModelTags from '@renderer/components/ModelTags' import OAuthButton from '@renderer/components/OAuth/OAuthButton' -import { EMBEDDING_REGEX, getModelLogo, REASONING_REGEX, VISION_REGEX } from '@renderer/config/models' +import { getModelLogo, isEmbeddingModel, isReasoningModel, isVisionModel } from '@renderer/config/models' import { PROVIDER_CONFIG } from '@renderer/config/providers' import { useTheme } from '@renderer/context/ThemeProvider' import { useAssistants, useDefaultModel } from '@renderer/hooks/useAssistant' @@ -192,9 +192,9 @@ const ProviderSetting: FC = ({ provider: _provider }) => { const modelTypeContent = (model: Model) => { // 获取默认选中的类型 const defaultTypes = [ - ...(VISION_REGEX.test(model.id) ? ['vision'] : []), - ...(EMBEDDING_REGEX.test(model.id) ? ['embedding'] : []), - ...(REASONING_REGEX.test(model.id) ? ['reasoning'] : []) + ...(isVisionModel(model) ? ['vision'] : []), + ...(isEmbeddingModel(model) ? ['embedding'] : []), + ...(isReasoningModel(model) ? ['reasoning'] : []) ] as ModelType[] // 合并现有选择和默认类型 @@ -206,9 +206,21 @@ const ProviderSetting: FC = ({ provider: _provider }) => { value={selectedTypes} onChange={(types) => onUpdateModelTypes(model, types as ModelType[])} options={[ - { label: t('models.type.vision'), value: 'vision', disabled: VISION_REGEX.test(model.id) }, - { label: t('models.type.embedding'), value: 'embedding', disabled: EMBEDDING_REGEX.test(model.id) }, - { label: t('models.type.reasoning'), value: 'reasoning', disabled: REASONING_REGEX.test(model.id) } + { + label: t('models.type.vision'), + value: 'vision', + disabled: isVisionModel(model) + }, + { + label: t('models.type.embedding'), + value: 'embedding', + disabled: isEmbeddingModel(model) + }, + { + label: t('models.type.reasoning'), + value: 'reasoning', + disabled: isReasoningModel(model) + } ]} /> diff --git a/src/renderer/src/pages/translate/TranslatePage.tsx b/src/renderer/src/pages/translate/TranslatePage.tsx index c6bf516e..f82cdc0f 100644 --- a/src/renderer/src/pages/translate/TranslatePage.tsx +++ b/src/renderer/src/pages/translate/TranslatePage.tsx @@ -1,18 +1,27 @@ -import { CheckOutlined, SendOutlined, SettingOutlined, SwapOutlined, WarningOutlined } from '@ant-design/icons' +import { + CheckOutlined, + DeleteOutlined, + HistoryOutlined, + SendOutlined, + SettingOutlined, + WarningOutlined +} from '@ant-design/icons' import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar' import CopyIcon from '@renderer/components/Icons/CopyIcon' import { isLocalAi } from '@renderer/config/env' -import { TranslateLanguageOptions } from '@renderer/config/translate' +import { translateLanguageOptions } from '@renderer/config/translate' import db from '@renderer/databases' import { useDefaultModel } from '@renderer/hooks/useAssistant' import { fetchTranslate } from '@renderer/services/ApiService' import { getDefaultTranslateAssistant } from '@renderer/services/AssistantService' -import { Assistant, Message } from '@renderer/types' +import { Assistant, Message, TranslateHistory } from '@renderer/types' import { runAsyncFunction, uuid } from '@renderer/utils' -import { Button, Select, Space } from 'antd' -import TextArea from 'antd/es/input/TextArea' +import { Button, Dropdown, Empty, Flex, Popconfirm, Select, Space, Tooltip } from 'antd' +import TextArea, { TextAreaRef } from 'antd/es/input/TextArea' +import dayjs from 'dayjs' +import { useLiveQuery } from 'dexie-react-hooks' import { isEmpty } from 'lodash' -import { FC, useEffect, useState } from 'react' +import { FC, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { Link } from 'react-router-dom' import styled from 'styled-components' @@ -29,11 +38,42 @@ const TranslatePage: FC = () => { const { translateModel } = useDefaultModel() const [loading, setLoading] = useState(false) const [copied, setCopied] = useState(false) + const [historyDrawerVisible, setHistoryDrawerVisible] = useState(false) + const contentContainerRef = useRef(null) + const textAreaRef = useRef(null) + + const translateHistory = useLiveQuery(() => db.translate_history.orderBy('createdAt').reverse().toArray(), []) _text = text _result = result _targetLanguage = targetLanguage + const saveTranslateHistory = async ( + sourceText: string, + targetText: string, + sourceLanguage: string, + targetLanguage: string + ) => { + const history: TranslateHistory = { + id: uuid(), + sourceText, + targetText, + sourceLanguage, + targetLanguage, + createdAt: new Date().toISOString() + } + console.log('🌟TEO🌟 ~ saveTranslateHistory ~ history:', history) + await db.translate_history.add(history) + } + + const deleteHistory = async (id: string) => { + db.translate_history.delete(id) + } + + const clearHistory = async () => { + db.translate_history.clear() + } + const onTranslate = async () => { if (!text.trim()) { return @@ -62,7 +102,17 @@ const TranslatePage: FC = () => { } setLoading(true) - await fetchTranslate({ message, assistant, onResponse: (text) => setResult(text) }) + let translatedText = '' + await fetchTranslate({ + message, + assistant, + onResponse: (text) => { + translatedText = text + setResult(text) + } + }) + + await saveTranslateHistory(text, translatedText, 'any', targetLanguage) setLoading(false) } @@ -72,6 +122,12 @@ const TranslatePage: FC = () => { setTimeout(() => setCopied(false), 2000) } + const onHistoryItemClick = (history: TranslateHistory) => { + setText(history.sourceText) + setResult(history.targetText) + setTargetLanguage(history.targetLanguage) + } + useEffect(() => { isEmpty(text) && setResult('') }, [text]) @@ -83,6 +139,13 @@ const TranslatePage: FC = () => { }) }, []) + const onKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey && !e.ctrlKey && !e.metaKey) { + e.preventDefault() + onTranslate() + } + } + const SettingButton = () => { if (isLocalAi) { return null @@ -109,104 +172,163 @@ const TranslatePage: FC = () => { } return ( - + - {t('translate.title')} + + {t('translate.title')} + + )} + + {translateHistory && translateHistory.length ? ( + + {translateHistory.map((item) => ( + , + danger: true, + onClick: () => deleteHistory(item.id) + } + ] + }}> + onHistoryItemClick(item)}> + + {item.sourceText} + {item.targetText} + {dayjs(item.createdAt).format('MM/DD HH:mm')} + + + + ))} + + ) : ( + + + + )} + + + + + +