From 06a5265580988a1d3a25e38092f1624cb48c9799 Mon Sep 17 00:00:00 2001 From: kangfenmao Date: Thu, 31 Jul 2025 17:39:58 +0800 Subject: [PATCH 01/22] docs: update how to i18n demo pic --- docs/technical/.assets.how-to-i18n/demo-2.png | Bin 39240 -> 41371 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/technical/.assets.how-to-i18n/demo-2.png b/docs/technical/.assets.how-to-i18n/demo-2.png index f1e63a0a643dc196647a12d6f97d2f876a522448..c507b43befef5257fbb1ffe087783a38877a3153 100644 GIT binary patch literal 41371 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 HE06yM$0C$T literal 39240 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 From 0de9e5eb24569a3784329f397af6c8f66c9b2af7 Mon Sep 17 00:00:00 2001 From: tomsun28 Date: Thu, 31 Jul 2025 18:00:08 +0800 Subject: [PATCH 02/22] refactor: update new zhipu ai dev docs website link (#8713) update new zhipu ai docs website link Co-authored-by: gongchao --- src/renderer/src/config/providers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/src/config/providers.ts b/src/renderer/src/config/providers.ts index 76095fe22b..fc071a2dd4 100644 --- a/src/renderer/src/config/providers.ts +++ b/src/renderer/src/config/providers.ts @@ -320,7 +320,7 @@ export const PROVIDER_CONFIG = { websites: { official: 'https://open.bigmodel.cn/', apiKey: 'https://open.bigmodel.cn/usercenter/apikeys', - docs: 'https://open.bigmodel.cn/dev/howuse/introduction', + docs: 'https://docs.bigmodel.cn/', models: 'https://open.bigmodel.cn/modelcenter/square' } }, From e634279481ef8879af9d018df2e8487ffa7c4838 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?George=C2=B7Dong?= <98630204+GeorgeDong32@users.noreply.github.com> Date: Thu, 31 Jul 2025 19:08:17 +0800 Subject: [PATCH 03/22] feat(models): refine Qwen model support and token limits (#8716) --- src/renderer/src/config/models.ts | 62 ++++++++++++++++++++----------- 1 file changed, 41 insertions(+), 21 deletions(-) diff --git a/src/renderer/src/config/models.ts b/src/renderer/src/config/models.ts index 7c14ee4b2c..a93b6b1b18 100644 --- a/src/renderer/src/config/models.ts +++ b/src/renderer/src/config/models.ts @@ -2663,6 +2663,17 @@ export function isQwenReasoningModel(model?: Model): boolean { return false } + const baseName = getLowerBaseModelName(model.id, '/') + + if (baseName.startsWith('qwen3')) { + if (baseName.includes('thinking')) { + return true + } else if (baseName.includes('instruct')) { + return false + } + return true + } + if (isSupportedThinkingTokenQwenModel(model)) { return true } @@ -2681,27 +2692,34 @@ export function isSupportedThinkingTokenQwenModel(model?: Model): boolean { const baseName = getLowerBaseModelName(model.id, '/') - if (baseName.includes('coder') || baseName.includes('qwen3-235b-a22b-instruct')) { + if (baseName.includes('coder')) { return false } - return ( - baseName.startsWith('qwen3') || - [ - 'qwen-plus', - 'qwen-plus-latest', - 'qwen-plus-0428', - 'qwen-plus-2025-04-28', - 'qwen-plus-0714', - 'qwen-plus-2025-07-14', - 'qwen-turbo', - 'qwen-turbo-latest', - 'qwen-turbo-0428', - 'qwen-turbo-2025-04-28', - 'qwen-turbo-0715', - 'qwen-turbo-2025-07-15' - ].includes(baseName) - ) + if (baseName.startsWith('qwen3')) { + if (baseName.includes('instruct')) { + return false + } + if (baseName.includes('thinking')) { + return true + } + return true + } + + return [ + 'qwen-plus', + 'qwen-plus-latest', + 'qwen-plus-0428', + 'qwen-plus-2025-04-28', + 'qwen-plus-0714', + 'qwen-plus-2025-07-14', + 'qwen-turbo', + 'qwen-turbo-latest', + 'qwen-turbo-0428', + 'qwen-turbo-2025-04-28', + 'qwen-turbo-0715', + 'qwen-turbo-2025-07-15' + ].includes(baseName) } export function isQwen3235BA22BThinkingModel(model?: Model): boolean { @@ -3070,11 +3088,13 @@ export const THINKING_TOKEN_MAP: Record = 'gemini-.*-pro.*$': { min: 128, max: 32768 }, // Qwen models - 'qwen3-235b-a22b-thinking(?:-[\\w-]+)$': { min: 0, max: 81_920 }, + 'qwen3-235b-a22b-thinking-2507$': { min: 0, max: 81_920 }, + 'qwen3-30b-a3b-thinking-2507$': { min: 0, max: 81_920 }, + 'qwen-plus-2025-07-28$': { min: 0, max: 81_920 }, + 'qwen3-1\\.7b$': { min: 0, max: 30_720 }, + 'qwen3-0\\.6b$': { min: 0, max: 30_720 }, 'qwen-plus-.*$': { min: 0, max: 38912 }, 'qwen-turbo-.*$': { min: 0, max: 38912 }, - 'qwen3-0\\.6b$': { min: 0, max: 30720 }, - 'qwen3-1\\.7b$': { min: 0, max: 30720 }, 'qwen3-.*$': { min: 1024, max: 38912 }, // Claude models From 10b7c70a59ee4722c38646c998e56e5e2bbd5f4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=86=8A=E5=8F=AF=E7=8B=B8?= Date: Thu, 31 Jul 2025 19:11:31 +0800 Subject: [PATCH 04/22] fix(prompt): resolve variable replacement in function mode and add UI features (#6581) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(prompt): fix variable replacement in function mode * fix(i18n): update available variables in prompt tip * feat(prompt): replace prompt variable in Prompt and AssistantPromptSettings components * fix(prompt): add fallback value if replace failed * feat(prompt): add hook and settings for automatic prompt replacement * feat(prompt): add supported variables and utility function to check if they exist * feat(prompt): enhance variable handling in prompt settings and tooltips * feat(i18n): add prompt settings translations for multiple languages * refactor(prompt): remove debug log from prompt processing * fix(prompt): handle model name variables and improve prompt processing * fix: correct variable replacement setting and update migration defaults * remove prompt settings * refactor: simplify model name replacement logic - Remove unnecessary assistant parameter from buildSystemPrompt function - Update all API clients to use the simplified function signature - Centralize model name replacement logic in promptVariableReplacer - Improve code maintainability by reducing parameter coupling * fix: eslint error * refactor: remove unused interval handling in usePromptProcessor * test: add tests, remove redundant replacing * feat: animate prompt substitution * chore: prepare for merge * refactor: update utils * refactor: remove getStoreSettings * refactor: update utils * style(Message/Prompt): 禁止文本选中以提升用户体验 * fix(Prompt): 修复内存泄漏问题,清除内部定时器 * refactor: move prompt replacement to api service --------- Co-authored-by: one Co-authored-by: icarus --- src/renderer/src/hooks/usePromptProcessor.ts | 34 +++++ .../src/pages/home/Messages/Prompt.tsx | 49 ++++++- .../AssistantPromptSettings.tsx | 8 +- src/renderer/src/services/ApiService.ts | 17 ++- src/renderer/src/store/thunk/messageThunk.ts | 3 - .../src/utils/__tests__/prompt.test.ts | 14 +- src/renderer/src/utils/prompt.ts | 135 ++++++++++-------- 7 files changed, 186 insertions(+), 74 deletions(-) create mode 100644 src/renderer/src/hooks/usePromptProcessor.ts diff --git a/src/renderer/src/hooks/usePromptProcessor.ts b/src/renderer/src/hooks/usePromptProcessor.ts new file mode 100644 index 0000000000..23f341a12f --- /dev/null +++ b/src/renderer/src/hooks/usePromptProcessor.ts @@ -0,0 +1,34 @@ +import { loggerService } from '@logger' +import { containsSupportedVariables, replacePromptVariables } from '@renderer/utils/prompt' +import { useEffect, useState } from 'react' + +const logger = loggerService.withContext('usePromptProcessor') + +interface PromptProcessor { + prompt: string + modelName?: string +} + +export function usePromptProcessor({ prompt, modelName }: PromptProcessor): string { + const [processedPrompt, setProcessedPrompt] = useState(prompt) + + useEffect(() => { + const processPrompt = async () => { + try { + if (containsSupportedVariables(prompt)) { + const result = await replacePromptVariables(prompt, modelName) + setProcessedPrompt(result) + } else { + setProcessedPrompt(prompt) + } + } catch (error) { + logger.error('Failed to process prompt variables, falling back:', error as Error) + setProcessedPrompt(prompt) + } + } + + processPrompt() + }, [prompt, modelName]) + + return processedPrompt +} diff --git a/src/renderer/src/pages/home/Messages/Prompt.tsx b/src/renderer/src/pages/home/Messages/Prompt.tsx index 7b8565f3c3..97fd5adfe5 100644 --- a/src/renderer/src/pages/home/Messages/Prompt.tsx +++ b/src/renderer/src/pages/home/Messages/Prompt.tsx @@ -1,7 +1,9 @@ import { useTheme } from '@renderer/context/ThemeProvider' +import { usePromptProcessor } from '@renderer/hooks/usePromptProcessor' import AssistantSettingsPopup from '@renderer/pages/settings/AssistantSettings' import { Assistant, Topic } from '@renderer/types' -import { FC } from 'react' +import { containsSupportedVariables } from '@renderer/utils/prompt' +import { FC, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' @@ -18,13 +20,50 @@ const Prompt: FC = ({ assistant, topic }) => { const topicPrompt = topic?.prompt || '' const isDark = theme === 'dark' + const processedPrompt = usePromptProcessor({ prompt, modelName: assistant.model?.name }) + + // 用于控制显示的状态 + const [displayText, setDisplayText] = useState(prompt) + const [isVisible, setIsVisible] = useState(true) + + useEffect(() => { + // 如果没有变量需要替换,直接显示处理后的内容 + if (!containsSupportedVariables(prompt)) { + setDisplayText(processedPrompt) + setIsVisible(true) + return + } + + // 如果有变量需要替换,先显示原始prompt + setDisplayText(prompt) + setIsVisible(true) + + // 延迟过渡 + let innerTimer: NodeJS.Timeout + const outerTimer = setTimeout(() => { + // 先淡出 + setIsVisible(false) + + // 切换内容并淡入 + innerTimer = setTimeout(() => { + setDisplayText(processedPrompt) + setIsVisible(true) + }, 300) + }, 300) + + return () => { + clearTimeout(outerTimer) + clearTimeout(innerTimer) + } + }, [prompt, processedPrompt]) + if (!prompt && !topicPrompt) { return null } return ( AssistantSettingsPopup.show({ assistant })} $isDark={isDark}> - {prompt} + {displayText} ) } @@ -38,13 +77,17 @@ const Container = styled.div<{ $isDark: boolean }>` margin-bottom: 0; ` -const Text = styled.div` +const Text = styled.div<{ $isVisible: boolean }>` color: var(--color-text-2); font-size: 12px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; + user-select: none; + + opacity: ${(props) => (props.$isVisible ? 1 : 0)}; + transition: opacity 0.3s ease-in-out; ` export default Prompt diff --git a/src/renderer/src/pages/settings/AssistantSettings/AssistantPromptSettings.tsx b/src/renderer/src/pages/settings/AssistantSettings/AssistantPromptSettings.tsx index 54ade12315..643f386f32 100644 --- a/src/renderer/src/pages/settings/AssistantSettings/AssistantPromptSettings.tsx +++ b/src/renderer/src/pages/settings/AssistantSettings/AssistantPromptSettings.tsx @@ -4,6 +4,7 @@ import { CloseCircleFilled, QuestionCircleOutlined } from '@ant-design/icons' import CodeEditor from '@renderer/components/CodeEditor' import EmojiPicker from '@renderer/components/EmojiPicker' import { Box, HSpaceBetweenStack, HStack } from '@renderer/components/Layout' +import { usePromptProcessor } from '@renderer/hooks/usePromptProcessor' import { estimateTextTokens } from '@renderer/services/TokenService' import { Assistant, AssistantSettings } from '@renderer/types' import { getLeadingEmoji } from '@renderer/utils' @@ -38,6 +39,11 @@ const AssistantPromptSettings: React.FC = ({ assistant, updateAssistant } updateTokenCount() }, [prompt]) + const processedPrompt = usePromptProcessor({ + prompt, + modelName: assistant.model?.name + }) + const onUpdate = () => { const _assistant = { ...assistant, name: name.trim(), emoji, prompt } updateAssistant(_assistant) @@ -112,7 +118,7 @@ const AssistantPromptSettings: React.FC = ({ assistant, updateAssistant } {showMarkdown ? ( setShowMarkdown(false)}> - {prompt} + {processedPrompt || prompt}
) : ( diff --git a/src/renderer/src/services/ApiService.ts b/src/renderer/src/services/ApiService.ts index 8976780449..c2aa95d220 100644 --- a/src/renderer/src/services/ApiService.ts +++ b/src/renderer/src/services/ApiService.ts @@ -41,7 +41,12 @@ import { removeSpecialCharactersForTopicName } from '@renderer/utils' import { isAbortError } from '@renderer/utils/error' import { extractInfoFromXML, ExtractResults } from '@renderer/utils/extract' import { findFileBlocks, getMainTextContent } from '@renderer/utils/messageUtils/find' -import { buildSystemPromptWithThinkTool, buildSystemPromptWithTools } from '@renderer/utils/prompt' +import { + buildSystemPromptWithThinkTool, + buildSystemPromptWithTools, + containsSupportedVariables, + replacePromptVariables +} from '@renderer/utils/prompt' import { findLast, isEmpty, takeRight } from 'lodash' import AiProvider from '../aiCore' @@ -426,6 +431,10 @@ export async function fetchChatCompletion({ }) { logger.debug('fetchChatCompletion', messages, assistant) + if (assistant.prompt && containsSupportedVariables(assistant.prompt)) { + assistant.prompt = await replacePromptVariables(assistant.prompt, assistant.model?.name) + } + const provider = getAssistantProvider(assistant) const AI = new AiProvider(provider) @@ -643,9 +652,13 @@ export async function fetchTranslate({ content, assistant, onResponse }: FetchTr } export async function fetchMessagesSummary({ messages, assistant }: { messages: Message[]; assistant: Assistant }) { - const prompt = (getStoreSetting('topicNamingPrompt') as string) || i18n.t('prompts.title') + let prompt = (getStoreSetting('topicNamingPrompt') as string) || i18n.t('prompts.title') const model = getTopNamingModel() || assistant.model || getDefaultModel() + if (prompt && containsSupportedVariables(prompt)) { + prompt = await replacePromptVariables(prompt, model.name) + } + // 总结上下文总是取最后5条消息 const contextMessages = takeRight(messages, 5) diff --git a/src/renderer/src/store/thunk/messageThunk.ts b/src/renderer/src/store/thunk/messageThunk.ts index b40d3b555d..1a55e5f2c9 100644 --- a/src/renderer/src/store/thunk/messageThunk.ts +++ b/src/renderer/src/store/thunk/messageThunk.ts @@ -17,7 +17,6 @@ import { createTranslationBlock, resetAssistantMessage } from '@renderer/utils/messageUtils/create' -import { buildSystemPrompt } from '@renderer/utils/prompt' import { getTopicQueue } from '@renderer/utils/queue' import { waitForTopicQueue } from '@renderer/utils/queue' import { t } from 'i18next' @@ -878,8 +877,6 @@ const fetchAndProcessAssistantResponseImpl = async ( // } // } - assistant.prompt = await buildSystemPrompt(assistant.prompt || '', assistant) - callbacks = createCallbacks({ blockManager, dispatch, diff --git a/src/renderer/src/utils/__tests__/prompt.test.ts b/src/renderer/src/utils/__tests__/prompt.test.ts index 3ae21dcaa4..898f3fc691 100644 --- a/src/renderer/src/utils/__tests__/prompt.test.ts +++ b/src/renderer/src/utils/__tests__/prompt.test.ts @@ -4,9 +4,9 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { AvailableTools, - buildSystemPrompt, buildSystemPromptWithThinkTool, buildSystemPromptWithTools, + replacePromptVariables, SYSTEM_PROMPT, THINK_TOOL_PROMPT, ToolUseExamples @@ -130,7 +130,7 @@ describe('prompt', () => { - 用户名称: {{username}}; ` const assistant = createMockAssistant('MyAssistant', 'Super-Model-X') - const result = await buildSystemPrompt(userPrompt, assistant) + const result = await replacePromptVariables(userPrompt, assistant.model?.name) const expectedPrompt = ` 以下是一些辅助信息: - 日期和时间: ${mockDate.toLocaleString()}; @@ -148,13 +148,13 @@ describe('prompt', () => { mockApi.getAppInfo.mockRejectedValue(new Error('API Error')) const userPrompt = 'System: {{system}}, Architecture: {{arch}}' - const result = await buildSystemPrompt(userPrompt) + const result = await replacePromptVariables(userPrompt) const expectedPrompt = 'System: Unknown System, Architecture: Unknown Architecture' expect(result).toEqual(expectedPrompt) }) it('should handle non-string input gracefully', async () => { - const result = await buildSystemPrompt(null as any) + const result = await replacePromptVariables(null as any) expect(result).toBe(null) }) }) @@ -173,7 +173,7 @@ describe('prompt', () => { Instructions: Be helpful. ` const assistant = createMockAssistant('Test Assistant', 'Advanced-AI-Model') - basePrompt = await buildSystemPrompt(initialPrompt, assistant) + basePrompt = await replacePromptVariables(initialPrompt, assistant.model?.name) expectedBasePrompt = ` System Information: - Date: ${mockDate.toLocaleDateString()} @@ -212,7 +212,7 @@ describe('prompt', () => { describe('buildSystemPromptWithTools', () => { it('should build a full prompt for "prompt" toolUseMode', async () => { const assistant = createMockAssistant('Test Assistant', 'Advanced-AI-Model') - const basePrompt = await buildSystemPrompt('Be helpful.', assistant) + const basePrompt = await replacePromptVariables('Be helpful.', assistant.model?.name) const tools = [createMockTool('web_search', 'Search the web')] const finalPrompt = buildSystemPromptWithTools(basePrompt, tools) @@ -236,7 +236,7 @@ describe('prompt', () => { Instructions: Be helpful. ` const assistant = createMockAssistant('Test Assistant', 'Advanced-AI-Model') - const basePrompt = await buildSystemPrompt(initialPrompt, assistant) + const basePrompt = await replacePromptVariables(initialPrompt, assistant.model?.name) const expectedBasePrompt = ` System Information: - Date: ${mockDate.toLocaleDateString()} diff --git a/src/renderer/src/utils/prompt.ts b/src/renderer/src/utils/prompt.ts index 0d17eb0e52..fe5d9b462b 100644 --- a/src/renderer/src/utils/prompt.ts +++ b/src/renderer/src/utils/prompt.ts @@ -1,6 +1,6 @@ import { loggerService } from '@logger' import store from '@renderer/store' -import { Assistant, MCPTool } from '@renderer/types' +import { MCPTool } from '@renderer/types' const logger = loggerService.withContext('Utils:Prompt') @@ -196,71 +196,90 @@ ${availableTools} ` } -export const buildSystemPrompt = async (userSystemPrompt: string, assistant?: Assistant): Promise => { - if (typeof userSystemPrompt === 'string') { - const now = new Date() - if (userSystemPrompt.includes('{{date}}')) { - const date = now.toLocaleDateString() - userSystemPrompt = userSystemPrompt.replace(/{{date}}/g, date) - } +const supportedVariables = [ + '{{username}}', + '{{date}}', + '{{time}}', + '{{datetime}}', + '{{system}}', + '{{language}}', + '{{arch}}', + '{{model_name}}' +] - if (userSystemPrompt.includes('{{time}}')) { - const time = now.toLocaleTimeString() - userSystemPrompt = userSystemPrompt.replace(/{{time}}/g, time) - } +export const containsSupportedVariables = (userSystemPrompt: string): boolean => { + return supportedVariables.some((variable) => userSystemPrompt.includes(variable)) +} - if (userSystemPrompt.includes('{{datetime}}')) { - const datetime = now.toLocaleString() - userSystemPrompt = userSystemPrompt.replace(/{{datetime}}/g, datetime) - } +export const replacePromptVariables = async (userSystemPrompt: string, modelName?: string): Promise => { + if (typeof userSystemPrompt !== 'string') { + logger.warn('User system prompt is not a string:', userSystemPrompt) + return userSystemPrompt + } - if (userSystemPrompt.includes('{{system}}')) { - try { - const systemType = await window.api.system.getDeviceType() - userSystemPrompt = userSystemPrompt.replace(/{{system}}/g, systemType) - } catch (error) { - logger.error('Failed to get system type:', error as Error) - userSystemPrompt = userSystemPrompt.replace(/{{system}}/g, 'Unknown System') - } - } + const now = new Date() + if (userSystemPrompt.includes('{{date}}')) { + const date = now.toLocaleDateString() + userSystemPrompt = userSystemPrompt.replace(/{{date}}/g, date) + } - if (userSystemPrompt.includes('{{language}}')) { - try { - const language = store.getState().settings.language - userSystemPrompt = userSystemPrompt.replace(/{{language}}/g, language) - } catch (error) { - logger.error('Failed to get language:', error as Error) - userSystemPrompt = userSystemPrompt.replace(/{{language}}/g, 'Unknown System Language') - } - } + if (userSystemPrompt.includes('{{time}}')) { + const time = now.toLocaleTimeString() + userSystemPrompt = userSystemPrompt.replace(/{{time}}/g, time) + } - if (userSystemPrompt.includes('{{arch}}')) { - try { - const appInfo = await window.api.getAppInfo() - userSystemPrompt = userSystemPrompt.replace(/{{arch}}/g, appInfo.arch) - } catch (error) { - logger.error('Failed to get architecture:', error as Error) - userSystemPrompt = userSystemPrompt.replace(/{{arch}}/g, 'Unknown Architecture') - } - } + if (userSystemPrompt.includes('{{datetime}}')) { + const datetime = now.toLocaleString() + userSystemPrompt = userSystemPrompt.replace(/{{datetime}}/g, datetime) + } - if (userSystemPrompt.includes('{{model_name}}')) { - try { - userSystemPrompt = userSystemPrompt.replace(/{{model_name}}/g, assistant?.model?.name || 'Unknown Model') - } catch (error) { - logger.error('Failed to get model name:', error as Error) - userSystemPrompt = userSystemPrompt.replace(/{{model_name}}/g, 'Unknown Model') - } + if (userSystemPrompt.includes('{{username}}')) { + try { + const userName = store.getState().settings.userName || 'Unknown Username' + userSystemPrompt = userSystemPrompt.replace(/{{username}}/g, userName) + } catch (error) { + logger.error('Failed to get username:', error as Error) + userSystemPrompt = userSystemPrompt.replace(/{{username}}/g, 'Unknown Username') } + } - if (userSystemPrompt.includes('{{username}}')) { - try { - const username = store.getState().settings.userName || 'Unknown Username' - userSystemPrompt = userSystemPrompt.replace(/{{username}}/g, username) - } catch (error) { - logger.error('Failed to get username:', error as Error) - userSystemPrompt = userSystemPrompt.replace(/{{username}}/g, 'Unknown Username') - } + if (userSystemPrompt.includes('{{system}}')) { + try { + const systemType = await window.api.system.getDeviceType() + userSystemPrompt = userSystemPrompt.replace(/{{system}}/g, systemType) + } catch (error) { + logger.error('Failed to get system type:', error as Error) + userSystemPrompt = userSystemPrompt.replace(/{{system}}/g, 'Unknown System') + } + } + + if (userSystemPrompt.includes('{{language}}')) { + try { + const language = store.getState().settings.language + userSystemPrompt = userSystemPrompt.replace(/{{language}}/g, language) + } catch (error) { + logger.error('Failed to get language:', error as Error) + userSystemPrompt = userSystemPrompt.replace(/{{language}}/g, 'Unknown System Language') + } + } + + if (userSystemPrompt.includes('{{arch}}')) { + try { + const appInfo = await window.api.getAppInfo() + userSystemPrompt = userSystemPrompt.replace(/{{arch}}/g, appInfo.arch) + } catch (error) { + logger.error('Failed to get architecture:', error as Error) + userSystemPrompt = userSystemPrompt.replace(/{{arch}}/g, 'Unknown Architecture') + } + } + + if (userSystemPrompt.includes('{{model_name}}')) { + try { + const name = modelName || store.getState().llm.defaultModel?.name + userSystemPrompt = userSystemPrompt.replace(/{{model_name}}/g, name) + } catch (error) { + logger.error('Failed to get model name:', error as Error) + userSystemPrompt = userSystemPrompt.replace(/{{model_name}}/g, 'Unknown Model') } } From 0113447481fc6050014d830094006b60ef457478 Mon Sep 17 00:00:00 2001 From: Konv Suu <2583695112@qq.com> Date: Thu, 31 Jul 2025 21:06:22 +0800 Subject: [PATCH 05/22] feat: add multi-select mode wrapper for message component (#8653) * feat: add multi-select mode wrapper for message component * fix: update * update * update * chore: minor updates * fix: add drag threshold --- .../src/pages/home/Messages/Message.tsx | 130 +++++++++++------- .../src/pages/home/Messages/SelectionBox.tsx | 36 +++-- 2 files changed, 108 insertions(+), 58 deletions(-) diff --git a/src/renderer/src/pages/home/Messages/Message.tsx b/src/renderer/src/pages/home/Messages/Message.tsx index a2e6633f77..0c20f0d842 100644 --- a/src/renderer/src/pages/home/Messages/Message.tsx +++ b/src/renderer/src/pages/home/Messages/Message.tsx @@ -2,6 +2,7 @@ import { loggerService } from '@logger' import Scrollbar from '@renderer/components/Scrollbar' import { useMessageEditing } from '@renderer/context/MessageEditingContext' import { useAssistant } from '@renderer/hooks/useAssistant' +import { useChatContext } from '@renderer/hooks/useChatContext' import { useMessageOperations } from '@renderer/hooks/useMessageOperations' import { useModel } from '@renderer/hooks/useModel' import { useSettings } from '@renderer/hooks/useSettings' @@ -38,6 +39,16 @@ interface Props { const logger = loggerService.withContext('MessageItem') +const WrapperContainer = ({ + isMultiSelectMode, + children +}: { + isMultiSelectMode: boolean + children: React.ReactNode +}) => { + return isMultiSelectMode ? : children +} + const MessageItem: FC = ({ message, topic, @@ -49,6 +60,7 @@ const MessageItem: FC = ({ }) => { const { t } = useTranslation() const { assistant, setModel } = useAssistant(message.assistantId) + const { isMultiSelectMode } = useChatContext(topic) const model = useModel(getMessageModelId(message), message.model?.provider) || message.model const { messageFont, fontSize, messageStyle } = useSettings() const { editMessageBlocks, resendUserMessageWithEdit, editMessage } = useMessageOperations(topic) @@ -122,7 +134,15 @@ const MessageItem: FC = ({ if (message.type === 'clear') { return ( - EventEmitter.emit(EVENT_NAMES.NEW_CONTEXT)}> + { + if (isMultiSelectMode) { + return + } + EventEmitter.emit(EVENT_NAMES.NEW_CONTEXT) + }}> {t('chat.message.new.context')} @@ -131,56 +151,64 @@ const MessageItem: FC = ({ } return ( - - - {isEditing && ( - + + - )} - {!isEditing && ( - <> - - - - - - {showMenubar && ( - - } - setModel={setModel} - /> - - )} - - )} - + {isEditing && ( + + )} + {!isEditing && ( + <> + + + + + + {showMenubar && ( + + } + setModel={setModel} + /> + + )} + + )} + + ) } @@ -232,9 +260,11 @@ const MessageFooter = styled.div<{ $isLastMessage: boolean; $messageStyle: 'plai margin-top: 8px; ` -const NewContextMessage = styled.div` +const NewContextMessage = styled.div<{ isMultiSelectMode: boolean }>` cursor: pointer; flex: 1; + + ${({ isMultiSelectMode }) => isMultiSelectMode && 'cursor: default;'} ` export default memo(MessageItem) diff --git a/src/renderer/src/pages/home/Messages/SelectionBox.tsx b/src/renderer/src/pages/home/Messages/SelectionBox.tsx index ab48b69a8e..2d12f0305c 100644 --- a/src/renderer/src/pages/home/Messages/SelectionBox.tsx +++ b/src/renderer/src/pages/home/Messages/SelectionBox.tsx @@ -17,9 +17,14 @@ const SelectionBox: React.FC = ({ const [isDragging, setIsDragging] = useState(false) const [dragStart, setDragStart] = useState({ x: 0, y: 0 }) const [dragCurrent, setDragCurrent] = useState({ x: 0, y: 0 }) + const [isMouseDown, setIsMouseDown] = useState(false) const dragSelectedIds = useRef>(new Set()) + // 拖拽阈值,只有移动距离超过这个值才开始框选 + // 避免触控板点击触发拖拽 + const DRAG_THRESHOLD = 5 + useEffect(() => { if (!isMultiSelectMode) return @@ -39,20 +44,30 @@ const SelectionBox: React.FC = ({ e.preventDefault() - setIsDragging(true) + setIsMouseDown(true) const pos = updateDragPos(e) setDragStart(pos) setDragCurrent(pos) dragSelectedIds.current.clear() - document.body.classList.add('no-select') } const handleMouseMove = (e: MouseEvent) => { + if (!isMouseDown) return + + const pos = updateDragPos(e) + + const deltaX = Math.abs(pos.x - dragStart.x) + const deltaY = Math.abs(pos.y - dragStart.y) + const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY) + + if (!isDragging && distance > DRAG_THRESHOLD) { + setIsDragging(true) + document.body.classList.add('no-select') + } + if (!isDragging) return e.preventDefault() - - const pos = updateDragPos(e) setDragCurrent(pos) // 计算当前框选矩形 @@ -69,6 +84,9 @@ const SelectionBox: React.FC = ({ const checkbox = el.querySelector('input[type="checkbox"]') as HTMLInputElement | null const isAlreadySelected = checkbox?.checked || false + // 清除上下文这类消息也会被选中,所以需要跳过 + if (!checkbox) return + // 如果已经被记录为拖动选中,跳过 if (dragSelectedIds.current.has(id)) return @@ -94,9 +112,11 @@ const SelectionBox: React.FC = ({ } const handleMouseUp = () => { - if (!isDragging) return - setIsDragging(false) - document.body.classList.remove('no-select') + setIsMouseDown(false) + if (isDragging) { + setIsDragging(false) + document.body.classList.remove('no-select') + } } const container = scrollContainerRef.current! @@ -110,7 +130,7 @@ const SelectionBox: React.FC = ({ window.removeEventListener('mouseup', handleMouseUp) document.body.classList.remove('no-select') } - }, [isMultiSelectMode, isDragging, dragStart, scrollContainerRef, messageElements, handleSelectMessage]) + }, [isMultiSelectMode, isDragging, isMouseDown, dragStart, scrollContainerRef, messageElements, handleSelectMessage]) if (!isDragging || !isMultiSelectMode) return null From 925cc6bb9b00a9f9b72d367231ad96d05a2e071d Mon Sep 17 00:00:00 2001 From: one Date: Thu, 31 Jul 2025 21:20:16 +0800 Subject: [PATCH 06/22] refactor: add feedback on saving assistant prompt (#8726) --- src/renderer/src/i18n/locales/en-us.json | 2 ++ src/renderer/src/i18n/locales/ja-jp.json | 2 ++ src/renderer/src/i18n/locales/ru-ru.json | 2 ++ src/renderer/src/i18n/locales/zh-cn.json | 2 ++ src/renderer/src/i18n/locales/zh-tw.json | 2 ++ .../AssistantPromptSettings.tsx | 23 +++++++++++++------ 6 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index fc34851baa..ff7a2c08f7 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -703,6 +703,7 @@ "no_results": "No results", "open": "Open", "paste": "Paste", + "preview": "Preview", "prompt": "Prompt", "provider": "Provider", "reasoning_content": "Deep reasoning", @@ -711,6 +712,7 @@ "rename": "Rename", "reset": "Reset", "save": "Save", + "saved": "Saved", "search": "Search", "select": "Select", "selectedItems": "Selected {{count}} items", diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index 31be073d7f..28b2a8cad0 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -703,6 +703,7 @@ "no_results": "検索結果なし", "open": "開く", "paste": "貼り付け", + "preview": "プレビュー", "prompt": "プロンプト", "provider": "プロバイダー", "reasoning_content": "深く考察済み", @@ -711,6 +712,7 @@ "rename": "名前を変更", "reset": "リセット", "save": "保存", + "saved": "保存されました", "search": "検索", "select": "選択", "selectedItems": "{{count}}件の項目を選択しました", diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index a368a910f5..aebe8beb9a 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -703,6 +703,7 @@ "no_results": "Результатов не найдено", "open": "Открыть", "paste": "Вставить", + "preview": "Предварительный просмотр", "prompt": "Промпт", "provider": "Провайдер", "reasoning_content": "Глубокий анализ", @@ -711,6 +712,7 @@ "rename": "Переименовать", "reset": "Сбросить", "save": "Сохранить", + "saved": "Сохранено", "search": "Поиск", "select": "Выбрать", "selectedItems": "Выбрано {{count}} элементов", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 1a7d633a33..f1d8c0c3f3 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -703,6 +703,7 @@ "no_results": "无结果", "open": "打开", "paste": "粘贴", + "preview": "预览", "prompt": "提示词", "provider": "提供商", "reasoning_content": "已深度思考", @@ -711,6 +712,7 @@ "rename": "重命名", "reset": "重置", "save": "保存", + "saved": "已保存", "search": "搜索", "select": "选择", "selectedItems": "已选择 {{count}} 项", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 13a6508873..e8eb0d1dd2 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -703,6 +703,7 @@ "no_results": "沒有結果", "open": "開啟", "paste": "貼上", + "preview": "預覽", "prompt": "提示詞", "provider": "供應商", "reasoning_content": "已深度思考", @@ -711,6 +712,7 @@ "rename": "重新命名", "reset": "重設", "save": "儲存", + "saved": "已儲存", "search": "搜尋", "select": "選擇", "selectedItems": "已選擇 {{count}} 項", diff --git a/src/renderer/src/pages/settings/AssistantSettings/AssistantPromptSettings.tsx b/src/renderer/src/pages/settings/AssistantSettings/AssistantPromptSettings.tsx index 643f386f32..bd24b5531a 100644 --- a/src/renderer/src/pages/settings/AssistantSettings/AssistantPromptSettings.tsx +++ b/src/renderer/src/pages/settings/AssistantSettings/AssistantPromptSettings.tsx @@ -1,6 +1,6 @@ import 'emoji-picker-element' -import { CloseCircleFilled, QuestionCircleOutlined } from '@ant-design/icons' +import { CloseCircleFilled } from '@ant-design/icons' import CodeEditor from '@renderer/components/CodeEditor' import EmojiPicker from '@renderer/components/EmojiPicker' import { Box, HSpaceBetweenStack, HStack } from '@renderer/components/Layout' @@ -9,6 +9,7 @@ import { estimateTextTokens } from '@renderer/services/TokenService' import { Assistant, AssistantSettings } from '@renderer/types' import { getLeadingEmoji } from '@renderer/utils' import { Button, Input, Popover } from 'antd' +import { Edit, Eye, HelpCircle } from 'lucide-react' import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import ReactMarkdown from 'react-markdown' @@ -29,7 +30,7 @@ const AssistantPromptSettings: React.FC = ({ assistant, updateAssistant } const [prompt, setPrompt] = useState(assistant.prompt) const [tokenCount, setTokenCount] = useState(0) const { t } = useTranslation() - const [showMarkdown, setShowMarkdown] = useState(prompt.length > 0) + const [showPreview, setShowPreview] = useState(prompt.length > 0) useEffect(() => { const updateTokenCount = async () => { @@ -47,6 +48,7 @@ const AssistantPromptSettings: React.FC = ({ assistant, updateAssistant } const onUpdate = () => { const _assistant = { ...assistant, name: name.trim(), emoji, prompt } updateAssistant(_assistant) + window.message.success(t('common.saved')) } const handleEmojiSelect = (selectedEmoji: string) => { @@ -112,12 +114,12 @@ const AssistantPromptSettings: React.FC = ({ assistant, updateAssistant } {t('common.prompt')} - + - {showMarkdown ? ( - setShowMarkdown(false)}> + {showPreview ? ( + setShowPreview(false)}> {processedPrompt || prompt}
@@ -147,8 +149,11 @@ const AssistantPromptSettings: React.FC = ({ assistant, updateAssistant } Tokens: {tokenCount} - @@ -160,6 +165,10 @@ const Container = styled.div` flex: 1; flex-direction: column; overflow: hidden; + + .ant-btn { + line-height: 0; + } ` const EmojiButtonWrapper = styled.div` From 50a9518de73b6f737a751a0067123f2905d6af04 Mon Sep 17 00:00:00 2001 From: kangfenmao Date: Thu, 31 Jul 2025 18:18:32 +0800 Subject: [PATCH 07/22] Revert "fix: resolve issue of top navigation bar being obscured by miniapp (#8517)" This reverts commit 0f7091f3a8da9b6b54a7382a961b196e7ed8f26d. --- .../src/components/MinApp/MinappPopupContainer.tsx | 13 +++++++------ .../src/components/MinApp/WebviewContainer.tsx | 4 ++-- src/renderer/src/hooks/useAppInit.ts | 13 ++++--------- 3 files changed, 13 insertions(+), 17 deletions(-) diff --git a/src/renderer/src/components/MinApp/MinappPopupContainer.tsx b/src/renderer/src/components/MinApp/MinappPopupContainer.tsx index 4a5df7fd8c..45cb25c488 100644 --- a/src/renderer/src/components/MinApp/MinappPopupContainer.tsx +++ b/src/renderer/src/components/MinApp/MinappPopupContainer.tsx @@ -143,7 +143,7 @@ const MinappPopupContainer: React.FC = () => { const { pinned, updatePinnedMinapps } = useMinapps() const { t } = useTranslation() const backgroundColor = useNavBackgroundColor() - const { isLeftNavbar, isTopNavbar } = useNavbarPosition() + const { isTopNavbar } = useNavbarPosition() const dispatch = useAppDispatch() /** control the drawer open or close */ @@ -165,6 +165,8 @@ const MinappPopupContainer: React.FC = () => { /** whether the minapps open link external is enabled */ const { minappsOpenLinkExternal } = useSettings() + const { isLeftNavbar } = useNavbarPosition() + const isInDevelopment = process.env.NODE_ENV === 'development' useBridge() @@ -403,7 +405,7 @@ const MinappPopupContainer: React.FC = () => { )} - + handleGoBack(appInfo.id)}> @@ -505,7 +507,6 @@ const MinappPopupContainer: React.FC = () => { closeIcon={null} style={{ marginLeft: isLeftNavbar ? 'var(--sidebar-width)' : 0, - marginTop: isTopNavbar ? 'var(--navbar-height)' : 0, backgroundColor: window.root.style.background }}> {/* 在所有小程序中显示GoogleLoginTip */} @@ -540,7 +541,7 @@ const TitleContainer = styled.div` padding-left: ${isMac ? '20px' : '10px'}; } [navbar-position='top'] & { - padding-left: ${isMac ? '20px' : '10px'}; + padding-left: ${isMac ? '80px' : '10px'}; border-bottom: 0.5px solid var(--color-border); } ` @@ -562,14 +563,14 @@ const TitleTextTooltip = styled.span` } ` -const ButtonsGroup = styled.div<{ isTopNavBar: boolean }>` +const ButtonsGroup = styled.div` display: flex; flex-direction: row; align-items: center; gap: 5px; -webkit-app-region: no-drag; &.windows { - margin-right: ${(props) => (props.isTopNavBar ? 0 : isWin ? '130px' : isLinux ? '100px' : 0)}; + margin-right: ${isWin ? '130px' : isLinux ? '100px' : 0}; background-color: var(--color-background-mute); border-radius: 50px; padding: 0 3px; diff --git a/src/renderer/src/components/MinApp/WebviewContainer.tsx b/src/renderer/src/components/MinApp/WebviewContainer.tsx index 2d63e805be..361bd39696 100644 --- a/src/renderer/src/components/MinApp/WebviewContainer.tsx +++ b/src/renderer/src/components/MinApp/WebviewContainer.tsx @@ -23,7 +23,7 @@ const WebviewContainer = memo( }) => { const webviewRef = useRef(null) const { enableSpellCheck } = useSettings() - const { isLeftNavbar, isTopNavbar } = useNavbarPosition() + const { isLeftNavbar } = useNavbarPosition() const setRef = (appid: string) => { onSetRefCallback(appid, null) @@ -74,7 +74,7 @@ const WebviewContainer = memo( const WebviewStyle: React.CSSProperties = { width: isLeftNavbar ? 'calc(100vw - var(--sidebar-width))' : '100vw', - height: isTopNavbar ? 'calc(100vh - var(--navbar-height) - var(--navbar-height))' : '100vh', + height: 'calc(100vh - var(--navbar-height))', backgroundColor: 'var(--color-background)', display: 'inline-flex' } diff --git a/src/renderer/src/hooks/useAppInit.ts b/src/renderer/src/hooks/useAppInit.ts index af7f44cb01..60b8ce448d 100644 --- a/src/renderer/src/hooks/useAppInit.ts +++ b/src/renderer/src/hooks/useAppInit.ts @@ -18,7 +18,7 @@ import { useEffect } from 'react' import { useDefaultModel } from './useAssistant' import useFullScreenNotice from './useFullScreenNotice' import { useRuntime } from './useRuntime' -import { useNavbarPosition, useSettings } from './useSettings' +import { useSettings } from './useSettings' import useUpdateHandler from './useUpdateHandler' const logger = loggerService.withContext('useAppInit') @@ -31,7 +31,6 @@ export function useAppInit() { const avatar = useLiveQuery(() => db.settings.get('image://avatar')) const { theme } = useTheme() const memoryConfig = useAppSelector(selectMemoryConfig) - const { isTopNavbar } = useNavbarPosition() useEffect(() => { document.getElementById('spinner')?.remove() @@ -86,17 +85,13 @@ export function useAppInit() { const transparentWindow = windowStyle === 'transparent' && isMac && !minappShow if (minappShow) { - if (isTopNavbar) { - window.root.style.background = 'var(--navbar-background)' - } else { - window.root.style.background = - windowStyle === 'transparent' && isMac ? 'var(--color-background)' : 'var(--navbar-background)' - } + window.root.style.background = + windowStyle === 'transparent' && isMac ? 'var(--color-background)' : 'var(--navbar-background)' return } window.root.style.background = transparentWindow ? 'var(--navbar-background-mac)' : 'var(--navbar-background)' - }, [windowStyle, minappShow, theme, isTopNavbar]) + }, [windowStyle, minappShow, theme]) useEffect(() => { if (isLocalAi) { From c214a6e56ed5234e2585af7c38642fc8250750b6 Mon Sep 17 00:00:00 2001 From: kangfenmao Date: Thu, 31 Jul 2025 19:00:42 +0800 Subject: [PATCH 08/22] feat: add API server settings component and integrate into tool settings - Introduced a new ApiServerSettings component for managing API server configurations. - Updated ToolSettings to include API server options and controls. - Enhanced GeneralSettings to improve proxy settings management. - Refactored UI elements for better organization and user experience. --- .../src/pages/settings/GeneralSettings.tsx | 59 +++++++++---------- .../src/pages/settings/SettingsPage.tsx | 16 +++-- .../ApiServerSettings/ApiServerSettings.tsx | 2 +- .../ApiServerSettings/index.tsx | 0 .../src/pages/settings/ToolSettings/index.tsx | 14 ++++- 5 files changed, 47 insertions(+), 44 deletions(-) rename src/renderer/src/pages/settings/{ => ToolSettings}/ApiServerSettings/ApiServerSettings.tsx (99%) rename src/renderer/src/pages/settings/{ => ToolSettings}/ApiServerSettings/index.tsx (100%) diff --git a/src/renderer/src/pages/settings/GeneralSettings.tsx b/src/renderer/src/pages/settings/GeneralSettings.tsx index d387b6bf16..3d80985276 100644 --- a/src/renderer/src/pages/settings/GeneralSettings.tsx +++ b/src/renderer/src/pages/settings/GeneralSettings.tsx @@ -1,4 +1,5 @@ import { InfoCircleOutlined } from '@ant-design/icons' +import { HStack } from '@renderer/components/Layout' import Selector from '@renderer/components/Selector' import { useTheme } from '@renderer/context/ThemeProvider' import { useEnableDeveloperMode, useSettings } from '@renderer/hooks/useSettings' @@ -199,37 +200,6 @@ const GeneralSettings: FC = () => { /> - - {t('settings.general.spell_check.label')} - - - {enableSpellCheck && ( - <> - - - {t('settings.general.spell_check.languages')} - - size={14} - multiple - value={spellCheckLanguages} - placeholder={t('settings.general.spell_check.languages')} - onChange={handleSpellCheckLanguagesChange} - options={spellCheckLanguageOptions.map((lang) => ({ - value: lang.value, - label: ( - - - {lang.flag} - - {lang.label} - - ) - }))} - /> - - - )} - {t('settings.proxy.mode.title')} @@ -251,6 +221,33 @@ const GeneralSettings: FC = () => { )} + + + {t('settings.general.spell_check.label')} + {enableSpellCheck && ( + + size={14} + multiple + value={spellCheckLanguages} + placeholder={t('settings.general.spell_check.languages')} + onChange={handleSpellCheckLanguagesChange} + options={spellCheckLanguageOptions.map((lang) => ({ + value: lang.value, + label: ( + + + {lang.flag} + + {lang.label} + + ) + }))} + /> + )} + + + + {t('settings.hardware_acceleration.title')} diff --git a/src/renderer/src/pages/settings/SettingsPage.tsx b/src/renderer/src/pages/settings/SettingsPage.tsx index b1ec523f41..157973c6c0 100644 --- a/src/renderer/src/pages/settings/SettingsPage.tsx +++ b/src/renderer/src/pages/settings/SettingsPage.tsx @@ -8,8 +8,8 @@ import { Info, MonitorCog, Package, + PencilRuler, Rocket, - Server, Settings2, SquareTerminal, TextCursorInput, @@ -21,7 +21,6 @@ import { Link, Route, Routes, useLocation } from 'react-router-dom' import styled from 'styled-components' import AboutSettings from './AboutSettings' -import { ApiServerSettings } from './ApiServerSettings' import DataSettings from './DataSettings/DataSettings' import DisplaySettings from './DisplaySettings/DisplaySettings' import GeneralSettings from './GeneralSettings' @@ -77,18 +76,18 @@ const SettingsPage: FC = () => { {t('settings.mcp.title')} - - - - {t('apiServer.title')} - - {t('memory.title')} + + + + {t('settings.tool.title')} + + @@ -133,7 +132,6 @@ const SettingsPage: FC = () => { } /> } /> } /> - } /> } /> } /> } /> diff --git a/src/renderer/src/pages/settings/ApiServerSettings/ApiServerSettings.tsx b/src/renderer/src/pages/settings/ToolSettings/ApiServerSettings/ApiServerSettings.tsx similarity index 99% rename from src/renderer/src/pages/settings/ApiServerSettings/ApiServerSettings.tsx rename to src/renderer/src/pages/settings/ToolSettings/ApiServerSettings/ApiServerSettings.tsx index 2fb22e9588..086c873525 100644 --- a/src/renderer/src/pages/settings/ApiServerSettings/ApiServerSettings.tsx +++ b/src/renderer/src/pages/settings/ToolSettings/ApiServerSettings/ApiServerSettings.tsx @@ -11,7 +11,7 @@ import { useSelector } from 'react-redux' import styled from 'styled-components' import { v4 as uuidv4 } from 'uuid' -import { SettingContainer } from '..' +import { SettingContainer } from '../..' const logger = loggerService.withContext('ApiServerSettings') const { Text, Title } = Typography diff --git a/src/renderer/src/pages/settings/ApiServerSettings/index.tsx b/src/renderer/src/pages/settings/ToolSettings/ApiServerSettings/index.tsx similarity index 100% rename from src/renderer/src/pages/settings/ApiServerSettings/index.tsx rename to src/renderer/src/pages/settings/ToolSettings/ApiServerSettings/index.tsx diff --git a/src/renderer/src/pages/settings/ToolSettings/index.tsx b/src/renderer/src/pages/settings/ToolSettings/index.tsx index e5229b0ba8..d7a3a1d046 100644 --- a/src/renderer/src/pages/settings/ToolSettings/index.tsx +++ b/src/renderer/src/pages/settings/ToolSettings/index.tsx @@ -2,23 +2,30 @@ import { GlobalOutlined } from '@ant-design/icons' import OcrIcon from '@renderer/components/Icons/OcrIcon' import { HStack } from '@renderer/components/Layout' import ListItem from '@renderer/components/ListItem' -import { FileCode } from 'lucide-react' +import { FileCode, Server } from 'lucide-react' import { FC, useState } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' +import ApiServerSettings from './ApiServerSettings/ApiServerSettings' import OcrSettings from './OcrSettings' import PreprocessSettings from './PreprocessSettings' import WebSearchSettings from './WebSearchSettings' +let _menu: string = 'web-search' + const ToolSettings: FC = () => { const { t } = useTranslation() - const [menu, setMenu] = useState('web-search') + const [menu, setMenu] = useState(_menu) const menuItems = [ { key: 'web-search', title: 'settings.tool.websearch.title', icon: }, { key: 'preprocess', title: 'settings.tool.preprocess.title', icon: }, - { key: 'ocr', title: 'settings.tool.ocr.title', icon: } + { key: 'ocr', title: 'settings.tool.ocr.title', icon: }, + { key: 'api-server', title: 'apiServer.title', icon: } ] + + _menu = menu + return ( @@ -36,6 +43,7 @@ const ToolSettings: FC = () => { {menu == 'web-search' && } {menu == 'preprocess' && } {menu == 'ocr' && } + {menu == 'api-server' && } ) } From 1efefad3eefd4f5ce9f9c43a9996d64e6deae31a Mon Sep 17 00:00:00 2001 From: kangfenmao Date: Thu, 31 Jul 2025 20:54:56 +0800 Subject: [PATCH 09/22] refactor: remove mac ocr --- electron-builder.yml | 3 - electron.vite.config.ts | 12 +- package.json | 7 +- scripts/after-pack.js | 2 +- src/main/knowledge/ocr/BaseOcrProvider.ts | 122 ---------- src/main/knowledge/ocr/DefaultOcrProvider.ts | 12 - src/main/knowledge/ocr/MacSysOcrProvider.ts | 130 ---------- src/main/knowledge/ocr/OcrProvider.ts | 26 -- src/main/knowledge/ocr/OcrProviderFactory.ts | 23 -- .../preprocess/BasePreprocessProvider.ts | 37 +-- .../preprocess/Doc2xPreprocessProvider.ts | 2 +- .../preprocess/MineruPreprocessProvider.ts | 4 +- src/main/services/FileStorage.ts | 8 +- src/main/services/KnowledgeService.ts | 14 +- .../src/hooks/useKnowledgeBaseForm.ts | 29 +-- src/renderer/src/hooks/useOcr.ts | 45 ---- src/renderer/src/i18n/locales/en-us.json | 21 +- src/renderer/src/i18n/locales/ja-jp.json | 21 +- src/renderer/src/i18n/locales/ru-ru.json | 21 +- src/renderer/src/i18n/locales/zh-cn.json | 19 +- src/renderer/src/i18n/locales/zh-tw.json | 19 +- src/renderer/src/i18n/translate/el-gr.json | 2 +- src/renderer/src/i18n/translate/es-es.json | 2 +- src/renderer/src/i18n/translate/fr-fr.json | 2 +- src/renderer/src/i18n/translate/pt-pt.json | 2 +- .../src/pages/knowledge/KnowledgeContent.tsx | 4 +- .../GeneralSettingsPanel.test.tsx.snap | 4 +- .../GeneralSettingsPanel.tsx | 8 +- .../pages/knowledge/items/KnowledgeFiles.tsx | 4 +- .../ToolSettings/OcrSettings/OcrSettings.tsx | 168 ------------- .../ToolSettings/OcrSettings/index.tsx | 58 ----- .../src/pages/settings/ToolSettings/index.tsx | 4 - src/renderer/src/queue/KnowledgeQueue.ts | 2 +- src/renderer/src/services/KnowledgeService.ts | 2 +- src/renderer/src/store/index.ts | 5 +- src/renderer/src/store/migrate.ts | 22 ++ src/renderer/src/store/ocr.ts | 46 ---- src/renderer/src/store/tabs.ts | 2 +- src/renderer/src/types/index.ts | 22 +- yarn.lock | 223 +++--------------- 40 files changed, 145 insertions(+), 1014 deletions(-) delete mode 100644 src/main/knowledge/ocr/BaseOcrProvider.ts delete mode 100644 src/main/knowledge/ocr/DefaultOcrProvider.ts delete mode 100644 src/main/knowledge/ocr/MacSysOcrProvider.ts delete mode 100644 src/main/knowledge/ocr/OcrProvider.ts delete mode 100644 src/main/knowledge/ocr/OcrProviderFactory.ts delete mode 100644 src/renderer/src/hooks/useOcr.ts delete mode 100644 src/renderer/src/pages/settings/ToolSettings/OcrSettings/OcrSettings.tsx delete mode 100644 src/renderer/src/pages/settings/ToolSettings/OcrSettings/index.tsx delete mode 100644 src/renderer/src/store/ocr.ts diff --git a/electron-builder.yml b/electron-builder.yml index e408059068..2756d4941b 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -50,11 +50,8 @@ files: - '!node_modules/rollup-plugin-visualizer' - '!node_modules/js-tiktoken' - '!node_modules/@tavily/core/node_modules/js-tiktoken' - - '!node_modules/pdf-parse/lib/pdf.js/{v1.9.426,v1.10.88,v2.0.550}' - '!node_modules/mammoth/{mammoth.browser.js,mammoth.browser.min.js}' - '!node_modules/selection-hook/prebuilds/**/*' # we rebuild .node, don't use prebuilds - - '!node_modules/pdfjs-dist/web/**/*' - - '!node_modules/pdfjs-dist/legacy/**/*' - '!node_modules/selection-hook/node_modules' # we don't need what in the node_modules dir - '!node_modules/selection-hook/src' # we don't need source files - '!**/*.{h,iobj,ipdb,tlog,recipe,vcxproj,vcxproj.filters,Makefile,*.Makefile}' # filter .node build files diff --git a/electron.vite.config.ts b/electron.vite.config.ts index bf64d71992..f7cbd950f2 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -26,13 +26,11 @@ export default defineConfig({ }, build: { rollupOptions: { - external: ['@libsql/client', 'bufferutil', 'utf-8-validate', '@cherrystudio/mac-system-ocr'], - output: isProd - ? { - manualChunks: undefined, // 彻底禁用代码分割 - 返回 null 强制单文件打包 - inlineDynamicImports: true // 内联所有动态导入,这是关键配置 - } - : undefined + external: ['@libsql/client', 'bufferutil', 'utf-8-validate'], + output: { + manualChunks: undefined, // 彻底禁用代码分割 - 返回 null 强制单文件打包 + inlineDynamicImports: true // 内联所有动态导入,这是关键配置 + } }, sourcemap: isDev }, diff --git a/package.json b/package.json index 7727d0a13f..ddf1a439e4 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,6 @@ "prepare": "git config blame.ignoreRevsFile .git-blame-ignore-revs && husky" }, "dependencies": { - "@cherrystudio/pdf-to-img-napi": "^0.0.1", "@libsql/client": "0.14.0", "@libsql/win32-x64-msvc": "^0.4.7", "@strongtz/win32-arm64-msvc": "^0.4.7", @@ -80,7 +79,6 @@ "node-stream-zip": "^1.15.0", "officeparser": "^4.2.0", "os-proxy-config": "^1.1.2", - "pdfjs-dist": "4.10.38", "selection-hook": "^1.0.8", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.1", @@ -229,6 +227,7 @@ "npx-scope-finder": "^1.2.0", "openai": "patch:openai@npm%3A5.1.0#~/.yarn/patches/openai-npm-5.1.0-0e7b3ccb07.patch", "p-queue": "^8.1.0", + "pdf-lib": "^1.17.1", "playwright": "^1.52.0", "prettier": "^3.5.3", "prettier-plugin-sort-json": "^4.1.1", @@ -279,11 +278,7 @@ "zipread": "^1.3.3", "zod": "^3.25.74" }, - "optionalDependencies": { - "@cherrystudio/mac-system-ocr": "^0.2.2" - }, "resolutions": { - "pdf-parse@npm:1.1.1": "patch:pdf-parse@npm%3A1.1.1#~/.yarn/patches/pdf-parse-npm-1.1.1-04a6109b2a.patch", "@langchain/openai@npm:^0.3.16": "patch:@langchain/openai@npm%3A0.3.16#~/.yarn/patches/@langchain-openai-npm-0.3.16-e525b59526.patch", "@langchain/openai@npm:>=0.1.0 <0.4.0": "patch:@langchain/openai@npm%3A0.3.16#~/.yarn/patches/@langchain-openai-npm-0.3.16-e525b59526.patch", "libsql@npm:^0.4.4": "patch:libsql@npm%3A0.4.7#~/.yarn/patches/libsql-npm-0.4.7-444e260fb1.patch", diff --git a/scripts/after-pack.js b/scripts/after-pack.js index 4b18d2dacd..e392098771 100644 --- a/scripts/after-pack.js +++ b/scripts/after-pack.js @@ -53,7 +53,7 @@ exports.default = async function (context) { * @param {string} nodeModulesPath */ function removeMacOnlyPackages(nodeModulesPath) { - const macOnlyPackages = ['@cherrystudio/mac-system-ocr'] + const macOnlyPackages = [] macOnlyPackages.forEach((packageName) => { const packagePath = path.join(nodeModulesPath, packageName) diff --git a/src/main/knowledge/ocr/BaseOcrProvider.ts b/src/main/knowledge/ocr/BaseOcrProvider.ts deleted file mode 100644 index 14f05cd202..0000000000 --- a/src/main/knowledge/ocr/BaseOcrProvider.ts +++ /dev/null @@ -1,122 +0,0 @@ -import fs from 'node:fs' -import path from 'node:path' - -import { windowService } from '@main/services/WindowService' -import { getFileExt } from '@main/utils/file' -import { FileMetadata, OcrProvider } from '@types' -import { app } from 'electron' -import pdfjs from 'pdfjs-dist' -import { TypedArray } from 'pdfjs-dist/types/src/display/api' - -export default abstract class BaseOcrProvider { - protected provider: OcrProvider - public storageDir = path.join(app.getPath('userData'), 'Data', 'Files') - - constructor(provider: OcrProvider) { - if (!provider) { - throw new Error('OCR provider is not set') - } - this.provider = provider - } - abstract parseFile(sourceId: string, file: FileMetadata): Promise<{ processedFile: FileMetadata; quota?: number }> - - /** - * 检查文件是否已经被预处理过 - * 统一检测方法:如果 Data/Files/{file.id} 是目录,说明已被预处理 - * @param file 文件信息 - * @returns 如果已处理返回处理后的文件信息,否则返回null - */ - public async checkIfAlreadyProcessed(file: FileMetadata): Promise { - try { - // 检查 Data/Files/{file.id} 是否是目录 - const preprocessDirPath = path.join(this.storageDir, file.id) - - if (fs.existsSync(preprocessDirPath)) { - const stats = await fs.promises.stat(preprocessDirPath) - - // 如果是目录,说明已经被预处理过 - if (stats.isDirectory()) { - // 查找目录中的处理结果文件 - const files = await fs.promises.readdir(preprocessDirPath) - - // 查找主要的处理结果文件(.md 或 .txt) - const processedFile = files.find((fileName) => fileName.endsWith('.md') || fileName.endsWith('.txt')) - - if (processedFile) { - const processedFilePath = path.join(preprocessDirPath, processedFile) - const processedStats = await fs.promises.stat(processedFilePath) - const ext = getFileExt(processedFile) - - return { - ...file, - name: file.name.replace(file.ext, ext), - path: processedFilePath, - ext: ext, - size: processedStats.size, - created_at: processedStats.birthtime.toISOString() - } - } - } - } - - return null - } catch (error) { - // 如果检查过程中出现错误,返回null表示未处理 - return null - } - } - - /** - * 辅助方法:延迟执行 - */ - public delay = (ms: number): Promise => { - return new Promise((resolve) => setTimeout(resolve, ms)) - } - - public async readPdf( - source: string | URL | TypedArray, - passwordCallback?: (fn: (password: string) => void, reason: string) => string - ) { - const documentLoadingTask = pdfjs.getDocument(source) - if (passwordCallback) { - documentLoadingTask.onPassword = passwordCallback - } - - const document = await documentLoadingTask.promise - return document - } - - public async sendOcrProgress(sourceId: string, progress: number): Promise { - const mainWindow = windowService.getMainWindow() - mainWindow?.webContents.send('file-ocr-progress', { - itemId: sourceId, - progress: progress - }) - } - - /** - * 将文件移动到附件目录 - * @param fileId 文件id - * @param filePaths 需要移动的文件路径数组 - * @returns 移动后的文件路径数组 - */ - public moveToAttachmentsDir(fileId: string, filePaths: string[]): string[] { - const attachmentsPath = path.join(this.storageDir, fileId) - if (!fs.existsSync(attachmentsPath)) { - fs.mkdirSync(attachmentsPath, { recursive: true }) - } - - const movedPaths: string[] = [] - - for (const filePath of filePaths) { - if (fs.existsSync(filePath)) { - const fileName = path.basename(filePath) - const destPath = path.join(attachmentsPath, fileName) - fs.copyFileSync(filePath, destPath) - fs.unlinkSync(filePath) // 删除原文件,实现"移动" - movedPaths.push(destPath) - } - } - return movedPaths - } -} diff --git a/src/main/knowledge/ocr/DefaultOcrProvider.ts b/src/main/knowledge/ocr/DefaultOcrProvider.ts deleted file mode 100644 index 83c8d51c91..0000000000 --- a/src/main/knowledge/ocr/DefaultOcrProvider.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { FileMetadata, OcrProvider } from '@types' - -import BaseOcrProvider from './BaseOcrProvider' - -export default class DefaultOcrProvider extends BaseOcrProvider { - constructor(provider: OcrProvider) { - super(provider) - } - public parseFile(): Promise<{ processedFile: FileMetadata }> { - throw new Error('Method not implemented.') - } -} diff --git a/src/main/knowledge/ocr/MacSysOcrProvider.ts b/src/main/knowledge/ocr/MacSysOcrProvider.ts deleted file mode 100644 index b18f59eb73..0000000000 --- a/src/main/knowledge/ocr/MacSysOcrProvider.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { loggerService } from '@logger' -import { isMac } from '@main/constant' -import { FileMetadata, OcrProvider } from '@types' -import * as fs from 'fs' -import * as path from 'path' -import { TextItem } from 'pdfjs-dist/types/src/display/api' - -import BaseOcrProvider from './BaseOcrProvider' - -const logger = loggerService.withContext('MacSysOcrProvider') - -export default class MacSysOcrProvider extends BaseOcrProvider { - private readonly MIN_TEXT_LENGTH = 1000 - private MacOCR: any - - private async initMacOCR() { - if (!isMac) { - throw new Error('MacSysOcrProvider is only available on macOS') - } - if (!this.MacOCR) { - try { - // @ts-ignore This module is optional and only installed/available on macOS. Runtime checks prevent execution on other platforms. - const module = await import('@cherrystudio/mac-system-ocr') - this.MacOCR = module.default - } catch (error) { - logger.error('Failed to load mac-system-ocr:', error as Error) - throw error - } - } - return this.MacOCR - } - - private getRecognitionLevel(level?: number) { - return level === 0 ? this.MacOCR.RECOGNITION_LEVEL_FAST : this.MacOCR.RECOGNITION_LEVEL_ACCURATE - } - - constructor(provider: OcrProvider) { - super(provider) - } - - private async processPages( - results: any, - totalPages: number, - sourceId: string, - writeStream: fs.WriteStream - ): Promise { - await this.initMacOCR() - // TODO: 下个版本后面使用批处理,以及p-queue来优化 - for (let i = 0; i < totalPages; i++) { - // Convert pages to buffers - const pageNum = i + 1 - const pageBuffer = await results.getPage(pageNum) - - // Process batch - const ocrResult = await this.MacOCR.recognizeFromBuffer(pageBuffer, { - ocrOptions: { - recognitionLevel: this.getRecognitionLevel(this.provider.options?.recognitionLevel), - minConfidence: this.provider.options?.minConfidence || 0.5 - } - }) - - // Write results in order - writeStream.write(ocrResult.text + '\n') - - // Update progress - await this.sendOcrProgress(sourceId, (pageNum / totalPages) * 100) - } - } - - public async isScanPdf(buffer: Buffer): Promise { - const doc = await this.readPdf(new Uint8Array(buffer)) - const pageLength = doc.numPages - let counts = 0 - const pagesToCheck = Math.min(pageLength, 10) - for (let i = 0; i < pagesToCheck; i++) { - const page = await doc.getPage(i + 1) - const pageData = await page.getTextContent() - const pageText = pageData.items.map((item) => (item as TextItem).str).join('') - counts += pageText.length - if (counts >= this.MIN_TEXT_LENGTH) { - return false - } - } - return true - } - - public async parseFile(sourceId: string, file: FileMetadata): Promise<{ processedFile: FileMetadata }> { - logger.info(`Starting OCR process for file: ${file.name}`) - if (file.ext === '.pdf') { - try { - const { pdf } = await import('@cherrystudio/pdf-to-img-napi') - const pdfBuffer = await fs.promises.readFile(file.path) - const results = await pdf(pdfBuffer, { - scale: 2 - }) - const totalPages = results.length - - const baseDir = path.dirname(file.path) - const baseName = path.basename(file.path, path.extname(file.path)) - const txtFileName = `${baseName}.txt` - const txtFilePath = path.join(baseDir, txtFileName) - - const writeStream = fs.createWriteStream(txtFilePath) - await this.processPages(results, totalPages, sourceId, writeStream) - - await new Promise((resolve, reject) => { - writeStream.end(() => { - logger.info(`OCR process completed successfully for ${file.origin_name}`) - resolve() - }) - writeStream.on('error', reject) - }) - const movedPaths = this.moveToAttachmentsDir(file.id, [txtFilePath]) - return { - processedFile: { - ...file, - name: txtFileName, - path: movedPaths[0], - ext: '.txt', - size: fs.statSync(movedPaths[0]).size - } - } - } catch (error) { - logger.error('Error during OCR process:', error as Error) - throw error - } - } - return { processedFile: file } - } -} diff --git a/src/main/knowledge/ocr/OcrProvider.ts b/src/main/knowledge/ocr/OcrProvider.ts deleted file mode 100644 index 07587f01e0..0000000000 --- a/src/main/knowledge/ocr/OcrProvider.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { FileMetadata, OcrProvider as Provider } from '@types' - -import BaseOcrProvider from './BaseOcrProvider' -import OcrProviderFactory from './OcrProviderFactory' - -export default class OcrProvider { - private sdk: BaseOcrProvider - constructor(provider: Provider) { - this.sdk = OcrProviderFactory.create(provider) - } - public async parseFile( - sourceId: string, - file: FileMetadata - ): Promise<{ processedFile: FileMetadata; quota?: number }> { - return this.sdk.parseFile(sourceId, file) - } - - /** - * 检查文件是否已经被预处理过 - * @param file 文件信息 - * @returns 如果已处理返回处理后的文件信息,否则返回null - */ - public async checkIfAlreadyProcessed(file: FileMetadata): Promise { - return this.sdk.checkIfAlreadyProcessed(file) - } -} diff --git a/src/main/knowledge/ocr/OcrProviderFactory.ts b/src/main/knowledge/ocr/OcrProviderFactory.ts deleted file mode 100644 index 34b8fe6f1d..0000000000 --- a/src/main/knowledge/ocr/OcrProviderFactory.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { loggerService } from '@logger' -import { isMac } from '@main/constant' -import { OcrProvider } from '@types' - -import BaseOcrProvider from './BaseOcrProvider' -import DefaultOcrProvider from './DefaultOcrProvider' -import MacSysOcrProvider from './MacSysOcrProvider' - -const logger = loggerService.withContext('OcrProviderFactory') - -export default class OcrProviderFactory { - static create(provider: OcrProvider): BaseOcrProvider { - switch (provider.id) { - case 'system': - if (!isMac) { - logger.warn('System OCR provider is only available on macOS') - } - return new MacSysOcrProvider(provider) - default: - return new DefaultOcrProvider(provider) - } - } -} diff --git a/src/main/knowledge/preprocess/BasePreprocessProvider.ts b/src/main/knowledge/preprocess/BasePreprocessProvider.ts index f8f31e67f3..7981e6f139 100644 --- a/src/main/knowledge/preprocess/BasePreprocessProvider.ts +++ b/src/main/knowledge/preprocess/BasePreprocessProvider.ts @@ -1,17 +1,18 @@ import fs from 'node:fs' import path from 'node:path' +import { loggerService } from '@logger' import { windowService } from '@main/services/WindowService' -import { getFileExt } from '@main/utils/file' +import { getFileExt, getTempDir } from '@main/utils/file' import { FileMetadata, PreprocessProvider } from '@types' -import { app } from 'electron' -import pdfjs from 'pdfjs-dist' -import { TypedArray } from 'pdfjs-dist/types/src/display/api' +import { PDFDocument } from 'pdf-lib' + +const logger = loggerService.withContext('BasePreprocessProvider') export default abstract class BasePreprocessProvider { protected provider: PreprocessProvider protected userId?: string - public storageDir = path.join(app.getPath('userData'), 'Data', 'Files') + public storageDir = path.join(getTempDir(), 'preprocess') constructor(provider: PreprocessProvider, userId?: string) { if (!provider) { @@ -19,7 +20,19 @@ export default abstract class BasePreprocessProvider { } this.provider = provider this.userId = userId + this.ensureDirectories() } + + private ensureDirectories() { + try { + if (!fs.existsSync(this.storageDir)) { + fs.mkdirSync(this.storageDir, { recursive: true }) + } + } catch (error) { + logger.error('Failed to create directories:', error as Error) + } + } + abstract parseFile(sourceId: string, file: FileMetadata): Promise<{ processedFile: FileMetadata; quota?: number }> abstract checkQuota(): Promise @@ -77,17 +90,11 @@ export default abstract class BasePreprocessProvider { return new Promise((resolve) => setTimeout(resolve, ms)) } - public async readPdf( - source: string | URL | TypedArray, - passwordCallback?: (fn: (password: string) => void, reason: string) => string - ) { - const documentLoadingTask = pdfjs.getDocument(source) - if (passwordCallback) { - documentLoadingTask.onPassword = passwordCallback + public async readPdf(buffer: Buffer) { + const pdfDoc = await PDFDocument.load(buffer) + return { + numPages: pdfDoc.getPageCount() } - - const document = await documentLoadingTask.promise - return document } public async sendPreprocessProgress(sourceId: string, progress: number): Promise { diff --git a/src/main/knowledge/preprocess/Doc2xPreprocessProvider.ts b/src/main/knowledge/preprocess/Doc2xPreprocessProvider.ts index f34b518c22..56349071c6 100644 --- a/src/main/knowledge/preprocess/Doc2xPreprocessProvider.ts +++ b/src/main/knowledge/preprocess/Doc2xPreprocessProvider.ts @@ -39,7 +39,7 @@ export default class Doc2xPreprocessProvider extends BasePreprocessProvider { private async validateFile(filePath: string): Promise { const pdfBuffer = await fs.promises.readFile(filePath) - const doc = await this.readPdf(new Uint8Array(pdfBuffer)) + const doc = await this.readPdf(pdfBuffer) // 文件页数小于1000页 if (doc.numPages >= 1000) { diff --git a/src/main/knowledge/preprocess/MineruPreprocessProvider.ts b/src/main/knowledge/preprocess/MineruPreprocessProvider.ts index 171bfb4ad7..afc19ae34a 100644 --- a/src/main/knowledge/preprocess/MineruPreprocessProvider.ts +++ b/src/main/knowledge/preprocess/MineruPreprocessProvider.ts @@ -115,7 +115,7 @@ export default class MineruPreprocessProvider extends BasePreprocessProvider { private async validateFile(filePath: string): Promise { const pdfBuffer = await fs.promises.readFile(filePath) - const doc = await this.readPdf(new Uint8Array(pdfBuffer)) + const doc = await this.readPdf(pdfBuffer) // 文件页数小于600页 if (doc.numPages >= 600) { @@ -178,7 +178,7 @@ export default class MineruPreprocessProvider extends BasePreprocessProvider { try { // 下载ZIP文件 const response = await axios.get(zipUrl, { responseType: 'arraybuffer' }) - fs.writeFileSync(zipPath, response.data) + fs.writeFileSync(zipPath, Buffer.from(response.data)) logger.info(`Downloaded ZIP file: ${zipPath}`) // 确保提取目录存在 diff --git a/src/main/services/FileStorage.ts b/src/main/services/FileStorage.ts index 129a87aaa0..08cf6b9c44 100644 --- a/src/main/services/FileStorage.ts +++ b/src/main/services/FileStorage.ts @@ -16,7 +16,7 @@ import { writeFileSync } from 'fs' import { readFile } from 'fs/promises' import officeParser from 'officeparser' import * as path from 'path' -import pdfjs from 'pdfjs-dist' +import { PDFDocument } from 'pdf-lib' import { chdir } from 'process' import { v4 as uuidv4 } from 'uuid' import WordExtractor from 'word-extractor' @@ -367,10 +367,8 @@ class FileStorage { const filePath = path.join(this.storageDir, id) const buffer = await fs.promises.readFile(filePath) - const doc = await pdfjs.getDocument({ data: buffer }).promise - const pages = doc.numPages - await doc.destroy() - return pages + const pdfDoc = await PDFDocument.load(buffer) + return pdfDoc.getPageCount() } public binaryImage = async (_: Electron.IpcMainInvokeEvent, id: string): Promise<{ data: Buffer; mime: string }> => { diff --git a/src/main/services/KnowledgeService.ts b/src/main/services/KnowledgeService.ts index c60aa39b6a..78a7e2d4a3 100644 --- a/src/main/services/KnowledgeService.ts +++ b/src/main/services/KnowledgeService.ts @@ -25,7 +25,6 @@ import { loggerService } from '@logger' import Embeddings from '@main/knowledge/embeddings/Embeddings' import { addFileLoader } from '@main/knowledge/loader' import { NoteLoader } from '@main/knowledge/loader/noteLoader' -import OcrProvider from '@main/knowledge/ocr/OcrProvider' import PreprocessProvider from '@main/knowledge/preprocess/PreprocessProvider' import Reranker from '@main/knowledge/reranker/Reranker' import { windowService } from '@main/services/WindowService' @@ -687,14 +686,9 @@ class KnowledgeService { userId: string ): Promise => { let fileToProcess: FileMetadata = file - if (base.preprocessOrOcrProvider && file.ext.toLowerCase() === '.pdf') { + if (base.preprocessProvider && file.ext.toLowerCase() === '.pdf') { try { - let provider: PreprocessProvider | OcrProvider - if (base.preprocessOrOcrProvider.type === 'preprocess') { - provider = new PreprocessProvider(base.preprocessOrOcrProvider.provider, userId) - } else { - provider = new OcrProvider(base.preprocessOrOcrProvider.provider) - } + const provider = new PreprocessProvider(base.preprocessProvider.provider, userId) // Check if file has already been preprocessed const alreadyProcessed = await provider.checkIfAlreadyProcessed(file) if (alreadyProcessed) { @@ -728,8 +722,8 @@ class KnowledgeService { userId: string ): Promise => { try { - if (base.preprocessOrOcrProvider && base.preprocessOrOcrProvider.type === 'preprocess') { - const provider = new PreprocessProvider(base.preprocessOrOcrProvider.provider, userId) + if (base.preprocessProvider && base.preprocessProvider.type === 'preprocess') { + const provider = new PreprocessProvider(base.preprocessProvider.provider, userId) return await provider.checkQuota() } throw new Error('No preprocess provider configured') diff --git a/src/renderer/src/hooks/useKnowledgeBaseForm.ts b/src/renderer/src/hooks/useKnowledgeBaseForm.ts index 4b7b000321..912acbd08e 100644 --- a/src/renderer/src/hooks/useKnowledgeBaseForm.ts +++ b/src/renderer/src/hooks/useKnowledgeBaseForm.ts @@ -1,6 +1,4 @@ -import { isMac } from '@renderer/config/constant' import { getEmbeddingMaxContext } from '@renderer/config/embedings' -import { useOcrProviders } from '@renderer/hooks/useOcr' import { usePreprocessProviders } from '@renderer/hooks/usePreprocess' import { useProviders } from '@renderer/hooks/useProvider' import { getModelUniqId } from '@renderer/services/ModelService' @@ -42,11 +40,10 @@ export const useKnowledgeBaseForm = (base?: KnowledgeBase) => { const [newBase, setNewBase] = useState(base || createInitialKnowledgeBase()) const { providers } = useProviders() const { preprocessProviders } = usePreprocessProviders() - const { ocrProviders } = useOcrProviders() const selectedDocPreprocessProvider = useMemo( - () => newBase.preprocessOrOcrProvider?.provider, - [newBase.preprocessOrOcrProvider] + () => newBase.preprocessProvider?.provider, + [newBase.preprocessProvider] ) const docPreprocessSelectOptions = useMemo(() => { @@ -57,14 +54,8 @@ export const useKnowledgeBaseForm = (base?: KnowledgeBase) => { .filter((p) => p.apiKey !== '' || p.id === 'mineru') .map((p) => ({ value: p.id, label: p.name })) } - const ocrOptions = { - label: t('settings.tool.ocr.provider'), - title: t('settings.tool.ocr.provider'), - options: ocrProviders.filter((p) => p.apiKey !== '').map((p) => ({ value: p.id, label: p.name })) - } - - return isMac ? [preprocessOptions, ocrOptions] : [preprocessOptions] - }, [ocrProviders, preprocessProviders, t]) + return [preprocessOptions] + }, [preprocessProviders, t]) const handleEmbeddingModelChange = useCallback( (value: string) => { @@ -92,21 +83,20 @@ export const useKnowledgeBaseForm = (base?: KnowledgeBase) => { const handleDocPreprocessChange = useCallback( (value: string) => { - const type = preprocessProviders.find((p) => p.id === value) ? 'preprocess' : 'ocr' - const provider = (type === 'preprocess' ? preprocessProviders : ocrProviders).find((p) => p.id === value) + const provider = preprocessProviders.find((p) => p.id === value) if (!provider) { - setNewBase((prev) => ({ ...prev, preprocessOrOcrProvider: undefined })) + setNewBase((prev) => ({ ...prev, preprocessProvider: undefined })) return } setNewBase((prev) => ({ ...prev, - preprocessOrOcrProvider: { - type, + preprocessProvider: { + type: 'preprocess', provider } })) }, - [preprocessProviders, ocrProviders] + [preprocessProviders] ) const handleChunkSizeChange = useCallback( @@ -152,7 +142,6 @@ export const useKnowledgeBaseForm = (base?: KnowledgeBase) => { const providerData = { providers, preprocessProviders, - ocrProviders, selectedDocPreprocessProvider, docPreprocessSelectOptions } diff --git a/src/renderer/src/hooks/useOcr.ts b/src/renderer/src/hooks/useOcr.ts deleted file mode 100644 index 7f83fd9c28..0000000000 --- a/src/renderer/src/hooks/useOcr.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { RootState } from '@renderer/store' -import { - setDefaultOcrProvider as _setDefaultOcrProvider, - updateOcrProvider as _updateOcrProvider, - updateOcrProviders as _updateOcrProviders -} from '@renderer/store/ocr' -import { OcrProvider } from '@renderer/types' -import { useDispatch, useSelector } from 'react-redux' - -export const useOcrProvider = (id: string) => { - const dispatch = useDispatch() - const ocrProviders = useSelector((state: RootState) => state.ocr.providers) - const provider = ocrProviders.find((provider) => provider.id === id) - if (!provider) { - throw new Error(`OCR provider with id ${id} not found`) - } - const updateOcrProvider = (ocrProvider: OcrProvider) => { - dispatch(_updateOcrProvider(ocrProvider)) - } - return { provider, updateOcrProvider } -} - -export const useOcrProviders = () => { - const dispatch = useDispatch() - const ocrProviders = useSelector((state: RootState) => state.ocr.providers) - return { - ocrProviders: ocrProviders, - updateOcrProviders: (ocrProviders: OcrProvider[]) => dispatch(_updateOcrProviders(ocrProviders)) - } -} - -export const useDefaultOcrProvider = () => { - const defaultProviderId = useSelector((state: RootState) => state.ocr.defaultProvider) - const { ocrProviders } = useOcrProviders() - const dispatch = useDispatch() - const provider = defaultProviderId ? ocrProviders.find((provider) => provider.id === defaultProviderId) : undefined - - const setDefaultOcrProvider = (ocrProvider: OcrProvider) => { - dispatch(_setDefaultOcrProvider(ocrProvider.id)) - } - const updateDefaultOcrProvider = (ocrProvider: OcrProvider) => { - dispatch(_updateOcrProvider(ocrProvider)) - } - return { provider, setDefaultOcrProvider, updateDefaultOcrProvider } -} diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index ff7a2c08f7..878055c724 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -922,7 +922,7 @@ "search_placeholder": "Enter text to search", "settings": { "preprocessing": "Preprocessing", - "preprocessing_tooltip": "Preprocess uploaded files with OCR", + "preprocessing_tooltip": "Preprocess uploaded files", "title": "Knowledge Base Settings" }, "sitemap_added": "Added successfully", @@ -3302,26 +3302,11 @@ }, "title": "Settings", "tool": { - "ocr": { - "mac_system_ocr_options": { - "min_confidence": "Minimum Confidence", - "mode": { - "accurate": "Accurate", - "fast": "Fast", - "title": "Recognition Mode" - } - }, - "provider": "OCR Provider", - "provider_placeholder": "Choose an OCR provider", - "title": "OCR Settings" - }, "preprocess": { "provider": "Pre Process Provider", "provider_placeholder": "Choose a Pre Process provider", - "title": "Pre Process" - }, - "preprocessOrOcr": { - "tooltip": "In Settings -> Tools, set a document preprocessing service provider or OCR. Document preprocessing can effectively improve the retrieval performance of complex format documents and scanned documents. OCR can only recognize text within images in documents or scanned PDF text." + "title": "Pre Process", + "tooltip": "In Settings -> Tools, set a document preprocessing service provider. Document preprocessing can effectively improve the retrieval performance of complex format documents and scanned documents." }, "title": "Tools Settings", "websearch": { diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index 28b2a8cad0..8c9e1635a9 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -922,7 +922,7 @@ "search_placeholder": "検索するテキストを入力", "settings": { "preprocessing": "預処理", - "preprocessing_tooltip": "アップロードされたファイルのOCR預処理", + "preprocessing_tooltip": "アップロードされたファイルの預処理", "title": "ナレッジベース設定" }, "sitemap_added": "追加成功", @@ -3302,26 +3302,11 @@ }, "title": "設定", "tool": { - "ocr": { - "mac_system_ocr_options": { - "min_confidence": "最小信頼度", - "mode": { - "accurate": "正確", - "fast": "速い", - "title": "認識モード" - } - }, - "provider": "OCRプロバイダー", - "provider_placeholder": "OCRプロバイダーを選択", - "title": "OCR(オーシーアール)" - }, "preprocess": { "provider": "プレプロセスプロバイダー", "provider_placeholder": "前処理プロバイダーを選択してください", - "title": "前処理" - }, - "preprocessOrOcr": { - "tooltip": "設定 → ツールで、ドキュメント前処理サービスプロバイダーまたはOCRを設定します。ドキュメント前処理は、複雑な形式のドキュメントやスキャンされたドキュメントの検索性能を効果的に向上させます。OCRは、ドキュメント内の画像内のテキストまたはスキャンされたPDFテキストのみを認識できます。" + "title": "前処理", + "tooltip": "設定 → ツールで、ドキュメント前処理サービスプロバイダーを設定します。ドキュメント前処理は、複雑な形式のドキュメントやスキャンされたドキュメントの検索性能を効果的に向上させます。" }, "title": "ツール設定", "websearch": { diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index aebe8beb9a..7c349611cd 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -922,7 +922,7 @@ "search_placeholder": "Введите текст для поиска", "settings": { "preprocessing": "Предварительная обработка", - "preprocessing_tooltip": "Предварительная обработка изображений с помощью OCR", + "preprocessing_tooltip": "Предварительная обработка документов", "title": "Настройки базы знаний" }, "sitemap_added": "添加成功", @@ -3302,26 +3302,11 @@ }, "title": "Настройки", "tool": { - "ocr": { - "mac_system_ocr_options": { - "min_confidence": "Минимальная достоверность", - "mode": { - "accurate": "Точный", - "fast": "Быстро", - "title": "Режим распознавания" - } - }, - "provider": "Поставщик OCR", - "provider_placeholder": "Выберите провайдера OCR", - "title": "OCR (оптическое распознавание символов)" - }, "preprocess": { "provider": "Предварительная обработка Поставщик", "provider_placeholder": "Выберите поставщика услуг предварительной обработки", - "title": "Предварительная обработка" - }, - "preprocessOrOcr": { - "tooltip": "В настройках (Настройки -> Инструменты) укажите поставщика услуги предварительной обработки документов или OCR. Предварительная обработка документов может значительно повысить эффективность поиска для документов сложных форматов и отсканированных документов. OCR способен распознавать только текст внутри изображений в документах или текст в отсканированных PDF." + "title": "Предварительная обработка", + "tooltip": "В настройках (Настройки -> Инструменты) укажите поставщика услуги предварительной обработки документов. Предварительная обработка документов может значительно повысить эффективность поиска для документов сложных форматов и отсканированных документов." }, "title": "Настройки инструментов", "websearch": { diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index f1d8c0c3f3..3a68ed7010 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -3302,26 +3302,11 @@ }, "title": "设置", "tool": { - "ocr": { - "mac_system_ocr_options": { - "min_confidence": "最低置信度", - "mode": { - "accurate": "准确", - "fast": "快速", - "title": "识别模式" - } - }, - "provider": "OCR 服务商", - "provider_placeholder": "选择一个 OCR 服务商", - "title": "OCR 文字识别" - }, "preprocess": { "provider": "文档预处理服务商", "provider_placeholder": "选择一个文档预处理服务商", - "title": "文档预处理" - }, - "preprocessOrOcr": { - "tooltip": "在设置 -> 工具中设置文档预处理服务商或OCR,文档预处理可以有效提升复杂格式文档与扫描版文档的检索效果,OCR仅可识别文档内图片或扫描版PDF的文本" + "title": "文档预处理", + "tooltip": "在设置 -> 工具中设置文档预处理服务商,文档预处理可以有效提升复杂格式文档与扫描版文档的检索效果" }, "title": "工具设置", "websearch": { diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index e8eb0d1dd2..f7d10e6849 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -3302,26 +3302,11 @@ }, "title": "設定", "tool": { - "ocr": { - "mac_system_ocr_options": { - "min_confidence": "最小置信度", - "mode": { - "accurate": "準確", - "fast": "快速", - "title": "識別模式" - } - }, - "provider": "OCR 供應商", - "provider_placeholder": "選擇一個OCR服務提供商", - "title": "OCR 文字識別" - }, "preprocess": { "provider": "前置處理供應商", "provider_placeholder": "選擇一個預處理供應商", - "title": "前置處理" - }, - "preprocessOrOcr": { - "tooltip": "在「設定」->「工具」中設定文件預處理服務供應商或OCR。文件預處理可有效提升複雜格式文件及掃描文件的檢索效能,而OCR僅能辨識文件內圖片文字或掃描PDF文字。" + "title": "前置處理", + "tooltip": "在「設定」->「工具」中設定文件預處理服務供應商。文件預處理可有效提升複雜格式文件及掃描文件的檢索效能" }, "title": "工具設定", "websearch": { diff --git a/src/renderer/src/i18n/translate/el-gr.json b/src/renderer/src/i18n/translate/el-gr.json index c7b9e5a8cd..f782c7c661 100644 --- a/src/renderer/src/i18n/translate/el-gr.json +++ b/src/renderer/src/i18n/translate/el-gr.json @@ -3304,7 +3304,7 @@ "provider_placeholder": "Επιλέξτε έναν πάροχο προεπεξεργασίας εγγράφων", "title": "Προεπεξεργασία Εγγράφων" }, - "preprocessOrOcr": { + "preprocess": { "tooltip": "Ορίστε πάροχο προεπεξεργασίας εγγράφων ή OCR στις Ρυθμίσεις -> Εργαλεία. Η προεπεξεργασία εγγράφων μπορεί να βελτιώσει σημαντικά την απόδοση αναζήτησης για έγγραφα πολύπλοκης μορφής ή εγγράφων σε μορφή σάρωσης. Το OCR μπορεί να αναγνωρίσει μόνο κείμενο μέσα σε εικόνες εγγράφων ή σε PDF σε μορφή σάρωσης." }, "title": "Ρυθμίσεις Εργαλείων", diff --git a/src/renderer/src/i18n/translate/es-es.json b/src/renderer/src/i18n/translate/es-es.json index 0b6ef5fbb6..1a0d8855dc 100644 --- a/src/renderer/src/i18n/translate/es-es.json +++ b/src/renderer/src/i18n/translate/es-es.json @@ -3304,7 +3304,7 @@ "provider_placeholder": "Selecciona un proveedor de preprocesamiento de documentos", "title": "Preprocesamiento de Documentos" }, - "preprocessOrOcr": { + "preprocess": { "tooltip": "Configure un proveedor de preprocesamiento de documentos o OCR en Configuración -> Herramientas. El preprocesamiento de documentos puede mejorar significativamente la eficacia de búsqueda en documentos con formatos complejos o versiones escaneadas. El OCR solo puede reconocer texto en imágenes o en archivos PDF escaneados." }, "title": "Configuración de Herramientas", diff --git a/src/renderer/src/i18n/translate/fr-fr.json b/src/renderer/src/i18n/translate/fr-fr.json index d6d2a52108..d6ad5527f9 100644 --- a/src/renderer/src/i18n/translate/fr-fr.json +++ b/src/renderer/src/i18n/translate/fr-fr.json @@ -3304,7 +3304,7 @@ "provider_placeholder": "Sélectionnez un fournisseur de traitement préalable de documents", "title": "Traitement Préliminaire de Documents" }, - "preprocessOrOcr": { + "preprocess": { "tooltip": "Configurer un fournisseur de prétraitement de documents ou OCR dans Paramètres -> Outils. Le prétraitement des documents améliore efficacement la précision de recherche pour les documents à format complexe ou les versions scannées, tandis que l'OCR permet uniquement d'extraire le texte contenu dans les images ou les PDF scannés." }, "title": "Paramètres des outils", diff --git a/src/renderer/src/i18n/translate/pt-pt.json b/src/renderer/src/i18n/translate/pt-pt.json index 2f9adea0b1..42c2613342 100644 --- a/src/renderer/src/i18n/translate/pt-pt.json +++ b/src/renderer/src/i18n/translate/pt-pt.json @@ -3304,7 +3304,7 @@ "provider_placeholder": "Selecione um prestador de serviços de pré-processamento de documentos", "title": "Pré-processamento de Documentos" }, - "preprocessOrOcr": { + "preprocess": { "tooltip": "Configure o provedor de pré-processamento de documentos ou OCR em Configurações -> Ferramentas. O pré-processamento de documentos pode melhorar significativamente a eficácia da busca em documentos com formatos complexos ou versões escaneadas. O OCR só consegue reconhecer texto em imagens ou PDFs escaneados." }, "title": "Configurações de Ferramentas", diff --git a/src/renderer/src/pages/knowledge/KnowledgeContent.tsx b/src/renderer/src/pages/knowledge/KnowledgeContent.tsx index d0c15d5540..8267f4c3e9 100644 --- a/src/renderer/src/pages/knowledge/KnowledgeContent.tsx +++ b/src/renderer/src/pages/knowledge/KnowledgeContent.tsx @@ -139,8 +139,8 @@ const KnowledgeContent: FC = ({ selectedBase }) => {
{base.rerankModel && {base.rerankModel.name}} - {base.preprocessOrOcrProvider && base.preprocessOrOcrProvider.type === 'preprocess' && ( - + {base.preprocessProvider && base.preprocessProvider.type === 'preprocess' && ( + )}
diff --git a/src/renderer/src/pages/knowledge/__tests__/__snapshots__/GeneralSettingsPanel.test.tsx.snap b/src/renderer/src/pages/knowledge/__tests__/__snapshots__/GeneralSettingsPanel.test.tsx.snap index d9bc68ed19..1c1dfe42b3 100644 --- a/src/renderer/src/pages/knowledge/__tests__/__snapshots__/GeneralSettingsPanel.test.tsx.snap +++ b/src/renderer/src/pages/knowledge/__tests__/__snapshots__/GeneralSettingsPanel.test.tsx.snap @@ -41,12 +41,10 @@ exports[`GeneralSettingsPanel > basic rendering > should match snapshot 1`] = ` class="settings-label" > settings.tool.preprocess.title - / - settings.tool.ocr.title ℹ️ diff --git a/src/renderer/src/pages/knowledge/components/KnowledgeSettings/GeneralSettingsPanel.tsx b/src/renderer/src/pages/knowledge/components/KnowledgeSettings/GeneralSettingsPanel.tsx index 010ad8cb40..836c8d74fc 100644 --- a/src/renderer/src/pages/knowledge/components/KnowledgeSettings/GeneralSettingsPanel.tsx +++ b/src/renderer/src/pages/knowledge/components/KnowledgeSettings/GeneralSettingsPanel.tsx @@ -6,7 +6,7 @@ import { isEmbeddingModel, isRerankModel } from '@renderer/config/models' import { useProviders } from '@renderer/hooks/useProvider' import { getModelUniqId } from '@renderer/services/ModelService' import { KnowledgeBase, PreprocessProvider } from '@renderer/types' -import { Input, Select, Slider } from 'antd' +import { Input, Select, SelectProps, Slider } from 'antd' import { useTranslation } from 'react-i18next' import { SettingsItem, SettingsPanel } from './styles' @@ -15,7 +15,7 @@ interface GeneralSettingsPanelProps { newBase: KnowledgeBase setNewBase: React.Dispatch> selectedDocPreprocessProvider?: PreprocessProvider - docPreprocessSelectOptions: any[] + docPreprocessSelectOptions: SelectProps['options'] handlers: { handleEmbeddingModelChange: (value: string) => void handleDimensionChange: (value: number | null) => void @@ -49,8 +49,8 @@ const GeneralSettingsPanel: React.FC = ({
- {t('settings.tool.preprocess.title')} / {t('settings.tool.ocr.title')} - + {t('settings.tool.preprocess.title')} +
setApiHost(e.target.value)} - onBlur={onUpdateApiHost} - /> - - - )} - - {hasObjectKey(ocrProvider, 'options') && ocrProvider.id === 'system' && ( - <> - - {t('settings.tool.ocr.mac_system_ocr_options.mode.title')} - onUpdateOptions('recognitionLevel', value)} - /> - - - - {t('settings.tool.ocr.mac_system_ocr_options.min_confidence')} - onUpdateOptions('minConfidence', value)} - min={0} - max={1} - step={0.1} - /> - - - )} - - ) -} - -const ProviderName = styled.span` - font-size: 14px; - font-weight: 500; -` -const ProviderLogo = styled(Avatar)` - border: 0.5px solid var(--color-border); -` - -export default OcrProviderSettings diff --git a/src/renderer/src/pages/settings/ToolSettings/OcrSettings/index.tsx b/src/renderer/src/pages/settings/ToolSettings/OcrSettings/index.tsx deleted file mode 100644 index 1a3b2d2b5f..0000000000 --- a/src/renderer/src/pages/settings/ToolSettings/OcrSettings/index.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { isMac } from '@renderer/config/constant' -import { useTheme } from '@renderer/context/ThemeProvider' -import { useDefaultOcrProvider, useOcrProviders } from '@renderer/hooks/useOcr' -import { PreprocessProvider } from '@renderer/types' -import { Select } from 'antd' -import { FC, useState } from 'react' -import { useTranslation } from 'react-i18next' - -import { SettingContainer, SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '../..' -import OcrProviderSettings from './OcrSettings' - -const OcrSettings: FC = () => { - const { ocrProviders } = useOcrProviders() - const { provider: defaultProvider, setDefaultOcrProvider } = useDefaultOcrProvider() - const { t } = useTranslation() - const [selectedProvider, setSelectedProvider] = useState(defaultProvider) - const { theme: themeMode } = useTheme() - - function updateSelectedOcrProvider(providerId: string) { - const provider = ocrProviders.find((p) => p.id === providerId) - if (!provider) { - return - } - setDefaultOcrProvider(provider) - setSelectedProvider(provider) - } - - return ( - - - {t('settings.tool.ocr.title')} - - - {t('settings.tool.ocr.provider')} -
-