From 720c5d608002e54a9b21942ad35e948182ed03f9 Mon Sep 17 00:00:00 2001 From: SuYao Date: Thu, 17 Jul 2025 11:40:15 +0800 Subject: [PATCH 001/204] fix: thinking not display (#8222) * feat(ThinkingTagExtraction): accumulate thinking content for improved processing - Introduced an `accumulatedThinkingContent` variable to gather content from multiple chunks before enqueuing. - Updated the `ThinkingDeltaChunk` to use the accumulated content instead of individual extraction results, enhancing the coherence of thinking messages. * feat(OpenAIAPIClient): enhance chunk processing for reasoning and content extraction - Updated the OpenAIAPIClient to handle additional fields in response chunks, including `reasoning_content` and `reasoning`, improving the extraction of relevant information. - Introduced a new mock implementation for testing OpenAI completions, ensuring accurate handling of thinking and text chunks in the response. - Enhanced unit tests to validate the processing of OpenAI thinking chunks, ensuring expected behavior and output. --- .../aiCore/clients/openai/OpenAIApiClient.ts | 5 +- .../feat/ThinkingTagExtractionMiddleware.ts | 5 +- .../src/services/__tests__/ApiService.test.ts | 475 +++++++++++++++++- 3 files changed, 481 insertions(+), 4 deletions(-) diff --git a/src/renderer/src/aiCore/clients/openai/OpenAIApiClient.ts b/src/renderer/src/aiCore/clients/openai/OpenAIApiClient.ts index 2034b3951e..80a611493d 100644 --- a/src/renderer/src/aiCore/clients/openai/OpenAIApiClient.ts +++ b/src/renderer/src/aiCore/clients/openai/OpenAIApiClient.ts @@ -710,7 +710,10 @@ export class OpenAIAPIClient extends OpenAIBaseClient< choice.delta && Object.keys(choice.delta).length > 0 && (!('content' in choice.delta) || - (typeof choice.delta.content === 'string' && choice.delta.content !== '')) + (typeof choice.delta.content === 'string' && choice.delta.content !== '') || + (typeof (choice.delta as any).reasoning_content === 'string' && + (choice.delta as any).reasoning_content !== '') || + (typeof (choice.delta as any).reasoning === 'string' && (choice.delta as any).reasoning !== '')) ) { contentSource = choice.delta } else if ('message' in choice) { diff --git a/src/renderer/src/aiCore/middleware/feat/ThinkingTagExtractionMiddleware.ts b/src/renderer/src/aiCore/middleware/feat/ThinkingTagExtractionMiddleware.ts index cf9bd918e1..425f29c705 100644 --- a/src/renderer/src/aiCore/middleware/feat/ThinkingTagExtractionMiddleware.ts +++ b/src/renderer/src/aiCore/middleware/feat/ThinkingTagExtractionMiddleware.ts @@ -66,7 +66,7 @@ export const ThinkingTagExtractionMiddleware: CompletionsMiddleware = let thinkingStartTime = 0 let isFirstTextChunk = true - + let accumulatedThinkingContent = '' const processedStream = resultFromUpstream.pipeThrough( new TransformStream({ transform(chunk: GenericChunk, controller) { @@ -101,9 +101,10 @@ export const ThinkingTagExtractionMiddleware: CompletionsMiddleware = } if (extractionResult.content?.trim()) { + accumulatedThinkingContent += extractionResult.content const thinkingDeltaChunk: ThinkingDeltaChunk = { type: ChunkType.THINKING_DELTA, - text: extractionResult.content, + text: accumulatedThinkingContent, thinking_millsec: thinkingStartTime > 0 ? Date.now() - thinkingStartTime : 0 } controller.enqueue(thinkingDeltaChunk) diff --git a/src/renderer/src/services/__tests__/ApiService.test.ts b/src/renderer/src/services/__tests__/ApiService.test.ts index 239e73b2a6..6fd37370d0 100644 --- a/src/renderer/src/services/__tests__/ApiService.test.ts +++ b/src/renderer/src/services/__tests__/ApiService.test.ts @@ -1,6 +1,7 @@ import { FinishReason, MediaModality } from '@google/genai' import { FunctionCall } from '@google/genai' import AiProvider from '@renderer/aiCore' +import { OpenAIAPIClient, ResponseChunkTransformerContext } from '@renderer/aiCore/clients' import { ApiClientFactory } from '@renderer/aiCore/clients/ApiClientFactory' import { BaseApiClient } from '@renderer/aiCore/clients/BaseApiClient' import { GeminiAPIClient } from '@renderer/aiCore/clients/gemini/GeminiAPIClient' @@ -14,8 +15,10 @@ import { TextStartChunk, ThinkingStartChunk } from '@renderer/types/chunk' -import { GeminiSdkRawChunk } from '@renderer/types/sdk' +import { GeminiSdkRawChunk, OpenAISdkRawChunk, OpenAISdkRawContentSource } from '@renderer/types/sdk' import { cloneDeep } from 'lodash' +import OpenAI from 'openai' +import { ChatCompletionChunk } from 'openai/resources' import { beforeEach, describe, expect, it, vi } from 'vitest' // Mock the ApiClientFactory @@ -615,6 +618,117 @@ const geminiToolUseChunks: GeminiSdkRawChunk[] = [ } as GeminiSdkRawChunk ] +const openaiCompletionChunks: OpenAISdkRawChunk[] = [ + { + id: 'cmpl-123', + created: 1715811200, + model: 'gpt-4o', + object: 'chat.completion.chunk', + choices: [ + { + delta: { + content: null, + role: 'assistant', + reasoning_content: '' + } as ChatCompletionChunk.Choice.Delta, + index: 0, + logprobs: null, + finish_reason: null + } as ChatCompletionChunk.Choice + ] + }, + { + id: 'cmpl-123', + created: 1715811200, + model: 'gpt-4o', + object: 'chat.completion.chunk', + choices: [ + { + delta: { + content: null, + role: 'assistant', + reasoning_content: '好的,用户打招呼说“你好' + } as ChatCompletionChunk.Choice.Delta, + index: 0, + logprobs: null, + finish_reason: null + } + ] + }, + { + id: 'cmpl-123', + created: 1715811200, + model: 'gpt-4o', + object: 'chat.completion.chunk', + choices: [ + { + delta: { + content: null, + role: 'assistant', + reasoning_content: '”,我需要友好回应。' + } as ChatCompletionChunk.Choice.Delta, + index: 0, + logprobs: null, + finish_reason: null + } + ] + }, + { + id: 'cmpl-123', + created: 1715811200, + model: 'gpt-4o', + object: 'chat.completion.chunk', + choices: [ + { + delta: { + content: '你好!有什么问题', + role: 'assistant', + reasoning_content: null + } as ChatCompletionChunk.Choice.Delta, + index: 0, + logprobs: null, + finish_reason: null + } + ] + }, + { + id: 'cmpl-123', + created: 1715811200, + model: 'gpt-4o', + object: 'chat.completion.chunk', + choices: [ + { + delta: { + content: '或者需要我帮忙的吗?', + role: 'assistant', + reasoning_content: null + } as ChatCompletionChunk.Choice.Delta, + index: 0, + logprobs: null, + finish_reason: null + } + ] + }, + { + id: 'cmpl-123', + created: 1715811200, + model: 'gpt-4o', + object: 'chat.completion.chunk', + choices: [ + { + delta: { + content: null, + role: 'assistant', + reasoning_content: null + } as ChatCompletionChunk.Choice.Delta, + index: 0, + logprobs: null, + finish_reason: 'stop' + } + ] + } +] + // 正确的 async generator 函数 async function* geminiChunkGenerator(): AsyncGenerator { for (const chunk of geminiChunks) { @@ -634,6 +748,276 @@ async function* geminiToolUseChunkGenerator(): AsyncGenerator } } +async function* openaiThinkingChunkGenerator(): AsyncGenerator { + for (const chunk of openaiCompletionChunks) { + yield chunk + } +} + +const mockOpenaiApiClient = { + createCompletions: vi.fn().mockImplementation(() => openaiThinkingChunkGenerator()), + getResponseChunkTransformer: vi.fn().mockImplementation(() => { + let hasBeenCollectedWebSearch = false + const collectWebSearchData = ( + chunk: OpenAISdkRawChunk, + contentSource: OpenAISdkRawContentSource, + context: ResponseChunkTransformerContext + ) => { + if (hasBeenCollectedWebSearch) { + return + } + // OpenAI annotations + // @ts-ignore - annotations may not be in standard type definitions + const annotations = contentSource.annotations || chunk.annotations + if (annotations && annotations.length > 0 && annotations[0].type === 'url_citation') { + hasBeenCollectedWebSearch = true + return { + results: annotations, + source: WebSearchSource.OPENAI + } + } + + // Grok citations + // @ts-ignore - citations may not be in standard type definitions + if (context.provider?.id === 'grok' && chunk.citations) { + hasBeenCollectedWebSearch = true + return { + // @ts-ignore - citations may not be in standard type definitions + results: chunk.citations, + source: WebSearchSource.GROK + } + } + + // Perplexity citations + // @ts-ignore - citations may not be in standard type definitions + if (context.provider?.id === 'perplexity' && chunk.search_results && chunk.search_results.length > 0) { + hasBeenCollectedWebSearch = true + return { + // @ts-ignore - citations may not be in standard type definitions + results: chunk.search_results, + source: WebSearchSource.PERPLEXITY + } + } + + // OpenRouter citations + // @ts-ignore - citations may not be in standard type definitions + if (context.provider?.id === 'openrouter' && chunk.citations && chunk.citations.length > 0) { + hasBeenCollectedWebSearch = true + return { + // @ts-ignore - citations may not be in standard type definitions + results: chunk.citations, + source: WebSearchSource.OPENROUTER + } + } + + // Zhipu web search + // @ts-ignore - web_search may not be in standard type definitions + if (context.provider?.id === 'zhipu' && chunk.web_search) { + hasBeenCollectedWebSearch = true + return { + // @ts-ignore - web_search may not be in standard type definitions + results: chunk.web_search, + source: WebSearchSource.ZHIPU + } + } + + // Hunyuan web search + // @ts-ignore - search_info may not be in standard type definitions + if (context.provider?.id === 'hunyuan' && chunk.search_info?.search_results) { + hasBeenCollectedWebSearch = true + return { + // @ts-ignore - search_info may not be in standard type definitions + results: chunk.search_info.search_results, + source: WebSearchSource.HUNYUAN + } + } + return null + } + + const toolCalls: OpenAI.Chat.Completions.ChatCompletionMessageToolCall[] = [] + let isFinished = false + let lastUsageInfo: any = null + + /** + * 统一的完成信号发送逻辑 + * - 有 finish_reason 时 + * - 无 finish_reason 但是流正常结束时 + */ + const emitCompletionSignals = (controller: TransformStreamDefaultController) => { + if (isFinished) return + + if (toolCalls.length > 0) { + controller.enqueue({ + type: ChunkType.MCP_TOOL_CREATED, + tool_calls: toolCalls + }) + } + + const usage = lastUsageInfo || { + prompt_tokens: 0, + completion_tokens: 0, + total_tokens: 0 + } + + controller.enqueue({ + type: ChunkType.LLM_RESPONSE_COMPLETE, + response: { usage } + }) + + // 防止重复发送 + isFinished = true + } + + let isFirstThinkingChunk = true + let isFirstTextChunk = true + return (context: ResponseChunkTransformerContext) => ({ + async transform(chunk: OpenAISdkRawChunk, controller: TransformStreamDefaultController) { + // 持续更新usage信息 + if (chunk.usage) { + lastUsageInfo = { + prompt_tokens: chunk.usage.prompt_tokens || 0, + completion_tokens: chunk.usage.completion_tokens || 0, + total_tokens: (chunk.usage.prompt_tokens || 0) + (chunk.usage.completion_tokens || 0) + } + } + + // 处理chunk + if ('choices' in chunk && chunk.choices && chunk.choices.length > 0) { + for (const choice of chunk.choices) { + if (!choice) continue + + // 对于流式响应,使用 delta;对于非流式响应,使用 message。 + // 然而某些 OpenAI 兼容平台在非流式请求时会错误地返回一个空对象的 delta 字段。 + // 如果 delta 为空对象或content为空,应当忽略它并回退到 message,避免造成内容缺失。 + let contentSource: OpenAISdkRawContentSource | null = null + if ( + 'delta' in choice && + choice.delta && + Object.keys(choice.delta).length > 0 && + (!('content' in choice.delta) || + (typeof choice.delta.content === 'string' && choice.delta.content !== '') || + (typeof (choice.delta as any).reasoning_content === 'string' && + (choice.delta as any).reasoning_content !== '') || + (typeof (choice.delta as any).reasoning === 'string' && (choice.delta as any).reasoning !== '')) + ) { + contentSource = choice.delta + } else if ('message' in choice) { + contentSource = choice.message + } + + if (!contentSource) { + if ('finish_reason' in choice && choice.finish_reason) { + emitCompletionSignals(controller) + } + continue + } + + const webSearchData = collectWebSearchData(chunk, contentSource, context) + if (webSearchData) { + controller.enqueue({ + type: ChunkType.LLM_WEB_SEARCH_COMPLETE, + llm_web_search: webSearchData + }) + } + + // 处理推理内容 (e.g. from OpenRouter DeepSeek-R1) + // @ts-ignore - reasoning_content is not in standard OpenAI types but some providers use it + const reasoningText = contentSource.reasoning_content || contentSource.reasoning + if (reasoningText) { + if (isFirstThinkingChunk) { + controller.enqueue({ + type: ChunkType.THINKING_START + } as ThinkingStartChunk) + isFirstThinkingChunk = false + } + controller.enqueue({ + type: ChunkType.THINKING_DELTA, + text: reasoningText + }) + } + + // 处理文本内容 + if (contentSource.content) { + if (isFirstTextChunk) { + controller.enqueue({ + type: ChunkType.TEXT_START + } as TextStartChunk) + isFirstTextChunk = false + } + controller.enqueue({ + type: ChunkType.TEXT_DELTA, + text: contentSource.content + }) + } + + // 处理工具调用 + if (contentSource.tool_calls) { + for (const toolCall of contentSource.tool_calls) { + if ('index' in toolCall) { + const { id, index, function: fun } = toolCall + if (fun?.name) { + toolCalls[index] = { + id: id || '', + function: { + name: fun.name, + arguments: fun.arguments || '' + }, + type: 'function' + } + } else if (fun?.arguments) { + toolCalls[index].function.arguments += fun.arguments + } + } else { + toolCalls.push(toolCall) + } + } + } + + // 处理finish_reason,发送流结束信号 + if ('finish_reason' in choice && choice.finish_reason) { + const webSearchData = collectWebSearchData(chunk, contentSource, context) + if (webSearchData) { + controller.enqueue({ + type: ChunkType.LLM_WEB_SEARCH_COMPLETE, + llm_web_search: webSearchData + }) + } + emitCompletionSignals(controller) + } + } + } + }, + + // 流正常结束时,检查是否需要发送完成信号 + flush(controller) { + if (isFinished) return + emitCompletionSignals(controller) + } + }) + }), + getSdkInstance: vi.fn(), + getRequestTransformer: vi.fn().mockImplementation(() => ({ + async transform(params: any) { + return { + payload: { + model: params.assistant?.model?.id || 'gpt-4o', + messages: params.messages || [], + tools: params.tools || [] + }, + metadata: {} + } + } + })), + convertMcpToolsToSdkTools: vi.fn(() => []), + convertSdkToolCallToMcpToolResponse: vi.fn(), + buildSdkMessages: vi.fn(() => []), + extractMessagesFromSdkPayload: vi.fn(() => []), + provider: {} as Provider, + useSystemPromptForTools: true, + getBaseURL: vi.fn(() => 'https://api.openai.com'), + getApiKey: vi.fn(() => 'mock-api-key') +} as unknown as OpenAIAPIClient + // 创建 mock 的 GeminiAPIClient const mockGeminiApiClient = { createCompletions: vi.fn().mockImplementation(() => geminiChunkGenerator()), @@ -1064,6 +1448,95 @@ describe('ApiService', () => { expect(filteredChunks).toEqual(expectedChunks) }) + it('should handle openai thinking chunk correctly', async () => { + const mockCreate = vi.mocked(ApiClientFactory.create) + mockCreate.mockReturnValue(mockOpenaiApiClient as unknown as BaseApiClient) + const AI = new AiProvider(mockProvider as Provider) + const result = await AI.completions({ + callType: 'test', + messages: [], + assistant: { + id: '1', + name: 'test', + prompt: 'test', + model: { + id: 'gpt-4o', + name: 'GPT-4o' + } + } as Assistant, + onChunk: mockOnChunk, + enableReasoning: true, + streamOutput: true + }) + + expect(result).toBeDefined() + expect(ApiClientFactory.create).toHaveBeenCalledWith(mockProvider) + expect(result.stream).toBeDefined() + + const stream = result.stream! as ReadableStream + const reader = stream.getReader() + + const chunks: GenericChunk[] = [] + + while (true) { + const { done, value } = await reader.read() + if (done) break + chunks.push(value) + } + + reader.releaseLock() + + const filteredChunks = chunks.map((chunk) => { + if (chunk.type === ChunkType.THINKING_DELTA || chunk.type === ChunkType.THINKING_COMPLETE) { + delete (chunk as any).thinking_millsec + return chunk + } + if (chunk.type === ChunkType.LLM_RESPONSE_COMPLETE) { + delete (chunk as any).response.usage + return chunk + } + return chunk + }) + const expectedChunks = [ + { + type: ChunkType.THINKING_START + }, + { + type: ChunkType.THINKING_DELTA, + text: '好的,用户打招呼说“你好' + }, + { + type: ChunkType.THINKING_DELTA, + text: '好的,用户打招呼说“你好”,我需要友好回应。' + }, + { + type: ChunkType.THINKING_COMPLETE, + text: '好的,用户打招呼说“你好”,我需要友好回应。' + }, + { + type: ChunkType.TEXT_START + }, + { + type: ChunkType.TEXT_DELTA, + text: '你好!有什么问题' + }, + { + type: ChunkType.TEXT_DELTA, + text: '你好!有什么问题或者需要我帮忙的吗?' + }, + { + type: ChunkType.TEXT_COMPLETE, + text: '你好!有什么问题或者需要我帮忙的吗?' + }, + { + type: ChunkType.LLM_RESPONSE_COMPLETE, + response: {} + } + ] + + expect(filteredChunks).toEqual(expectedChunks) + }) + // it('should extract tool use responses correctly', async () => { // const mockCreate = vi.mocked(ApiClientFactory.create) // mockCreate.mockReturnValue(mockGeminiToolUseApiClient as unknown as BaseApiClient) From 7549972048835c5842d674d0e44adc4fe1668504 Mon Sep 17 00:00:00 2001 From: SuYao Date: Thu, 17 Jul 2025 11:48:25 +0800 Subject: [PATCH 002/204] hotfix: enhance assistant topic validation in useActiveTopic hook (#8213) fix: enhance assistant topic validation in useActiveTopic hook Updated the useActiveTopic hook to ensure that the assistant and its topics are properly validated before accessing properties. This prevents potential errors when data is not fully loaded. --- src/renderer/src/hooks/useTopic.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/renderer/src/hooks/useTopic.ts b/src/renderer/src/hooks/useTopic.ts index e8b9077690..abb22cc921 100644 --- a/src/renderer/src/hooks/useTopic.ts +++ b/src/renderer/src/hooks/useTopic.ts @@ -34,7 +34,14 @@ export function useActiveTopic(assistantId: string, topic?: Topic) { useEffect(() => { // activeTopic not in assistant.topics - if (assistant && assistant.topics.length > 0 && !find(assistant.topics, { id: activeTopic?.id })) { + // 确保 assistant 和 assistant.topics 存在,避免在数据未完全加载时访问属性 + if ( + assistant && + assistant.topics && + Array.isArray(assistant.topics) && + assistant.topics.length > 0 && + !find(assistant.topics, { id: activeTopic?.id }) + ) { setActiveTopic(assistant.topics[0]) } }, [activeTopic?.id, assistant]) From 04afa61d55d66b87f22a3b074d33d243c4b50cca Mon Sep 17 00:00:00 2001 From: fullex <106392080+0xfullex@users.noreply.github.com> Date: Thu, 17 Jul 2025 11:50:37 +0800 Subject: [PATCH 003/204] fix(SelectionService): actionWindow show in center screen when in multi screen (#8133) fix(SelectionService): round center coordinates for action window positioning --- src/main/services/SelectionService.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/services/SelectionService.ts b/src/main/services/SelectionService.ts index 718025cd89..21520a3735 100644 --- a/src/main/services/SelectionService.ts +++ b/src/main/services/SelectionService.ts @@ -1257,14 +1257,15 @@ export class SelectionService { // Center of the screen if (!this.isFollowToolbar || !this.toolbarWindow) { - const centerX = workArea.x + (workArea.width - actionWindowWidth) / 2 - const centerY = workArea.y + (workArea.height - actionWindowHeight) / 2 + const centerX = Math.round(workArea.x + (workArea.width - actionWindowWidth) / 2) + const centerY = Math.round(workArea.y + (workArea.height - actionWindowHeight) / 2) + actionWindow.setPosition(centerX, centerY, false) actionWindow.setBounds({ width: actionWindowWidth, height: actionWindowHeight, - x: Math.round(centerX), - y: Math.round(centerY) + x: centerX, + y: centerY }) } else { // Follow toolbar position From 6bdb157af320fb42e3a4ccfc96e5786a6ae4e2f5 Mon Sep 17 00:00:00 2001 From: Konv Suu <2583695112@qq.com> Date: Thu, 17 Jul 2025 12:03:21 +0800 Subject: [PATCH 004/204] fix: set os attribute correctly to body (#8225) * feat: add os constants * update --- src/renderer/src/context/ThemeProvider.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/renderer/src/context/ThemeProvider.tsx b/src/renderer/src/context/ThemeProvider.tsx index 85b0dbae86..b818472076 100644 --- a/src/renderer/src/context/ThemeProvider.tsx +++ b/src/renderer/src/context/ThemeProvider.tsx @@ -1,4 +1,4 @@ -import { isMac } from '@renderer/config/constant' +import { isMac, isWin } from '@renderer/config/constant' import { useSettings } from '@renderer/hooks/useSettings' import useUserTheme from '@renderer/hooks/useUserTheme' import { ThemeMode } from '@renderer/types' @@ -40,7 +40,7 @@ export const ThemeProvider: React.FC = ({ children }) => { useEffect(() => { // Set initial theme and OS attributes on body - document.body.setAttribute('os', isMac ? 'mac' : 'windows') + document.body.setAttribute('os', isMac ? 'mac' : isWin ? 'windows' : 'linux') document.body.setAttribute('theme-mode', actualTheme) // if theme is old auto, then set theme to system From ff72c007c03ff47de21a4d0bf52a1ff1fb35cd89 Mon Sep 17 00:00:00 2001 From: beyondkmp Date: Thu, 17 Jul 2025 13:15:29 +0800 Subject: [PATCH 005/204] feat: add data parsing functionality in handleProvidersProtocolUrl (#8218) * feat: add data parsing functionality in handleProvidersProtocolUrl - Introduced a new ParseData function to decode and parse base64 encoded data from the URL parameters. - Added error handling to log when data is null or invalid, improving robustness of the handleProvidersProtocolUrl function. * fix: update data parsing in handleProvidersProtocolUrl and ProvidersList - Modified ParseData function to return a JSON string instead of an object for consistency. - Simplified data extraction in ProvidersList by directly parsing the addProviderData without base64 decoding, improving readability and performance. * fix: improve data parsing in handleProvidersProtocolUrl - Updated ParseData function to log the parsed result for better debugging. - Enhanced data extraction by replacing URL-safe characters back to their original form before parsing, ensuring accurate data retrieval. * fix: enhance error logging in ParseData function - Updated the ParseData function to log errors when parsing fails, improving debugging capabilities and robustness in handling invalid data. * format code --- .../services/urlschema/handle-providers.ts | 23 +++++++++++++++++-- .../pages/settings/ProviderSettings/index.tsx | 11 +-------- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/src/main/services/urlschema/handle-providers.ts b/src/main/services/urlschema/handle-providers.ts index 9a598fc459..f8b0661370 100644 --- a/src/main/services/urlschema/handle-providers.ts +++ b/src/main/services/urlschema/handle-providers.ts @@ -3,6 +3,17 @@ import Logger from 'electron-log' import { windowService } from '../WindowService' +function ParseData(data: string) { + try { + const result = JSON.parse(Buffer.from(data, 'base64').toString('utf-8')) + + return JSON.stringify(result) + } catch (error) { + Logger.error('ParseData error:', { error }) + return null + } +} + export async function handleProvidersProtocolUrl(url: URL) { switch (url.pathname) { case '/api-keys': { @@ -19,7 +30,13 @@ export async function handleProvidersProtocolUrl(url: URL) { // replace + and / to _ and - because + and / are processed by URLSearchParams const processedSearch = url.search.replaceAll('+', '_').replaceAll('/', '-') const params = new URLSearchParams(processedSearch) - const data = params.get('data') + const data = ParseData(params.get('data')?.replaceAll('_', '+').replaceAll('-', '/') || '') + + if (!data) { + Logger.error('handleProvidersProtocolUrl data is null or invalid') + return + } + const mainWindow = windowService.getMainWindow() const version = params.get('v') if (version == '1') { @@ -33,7 +50,9 @@ export async function handleProvidersProtocolUrl(url: URL) { !mainWindow.isDestroyed() && (await mainWindow.webContents.executeJavaScript(`typeof window.navigate === 'function'`)) ) { - mainWindow.webContents.executeJavaScript(`window.navigate('/settings/provider?addProviderData=${data}')`) + mainWindow.webContents.executeJavaScript( + `window.navigate('/settings/provider?addProviderData=${encodeURIComponent(data)}')` + ) if (isMac) { windowService.showMainWindow() diff --git a/src/renderer/src/pages/settings/ProviderSettings/index.tsx b/src/renderer/src/pages/settings/ProviderSettings/index.tsx index a89719c178..d7a3469a86 100644 --- a/src/renderer/src/pages/settings/ProviderSettings/index.tsx +++ b/src/renderer/src/pages/settings/ProviderSettings/index.tsx @@ -238,16 +238,7 @@ const ProvidersList: FC = () => { } try { - const base64Decode = (base64EncodedString: string) => - new TextDecoder().decode(Uint8Array.from(atob(base64EncodedString), (m) => m.charCodeAt(0))) - const { - id, - apiKey: newApiKey, - baseUrl, - type, - name - } = JSON.parse(base64Decode(addProviderData.replaceAll('_', '+').replaceAll('-', '/'))) - + const { id, apiKey: newApiKey, baseUrl, type, name } = JSON.parse(addProviderData) if (!id || !newApiKey || !baseUrl) { window.message.error(t('settings.models.provider_key_add_failed_by_invalid_data')) window.navigate('/settings/provider') From f01b7075fb7882ecda68f0df27cf155b05f8902d Mon Sep 17 00:00:00 2001 From: kangfenmao Date: Thu, 17 Jul 2025 11:50:57 +0800 Subject: [PATCH 006/204] refactor: replace message with window.message --- .../src/components/LocalBackupManager.tsx | 10 ++++----- .../src/components/WebdavBackupManager.tsx | 22 +++++++++---------- src/renderer/src/pages/apps/App.tsx | 6 ++--- src/renderer/src/pages/apps/NewAppButton.tsx | 14 ++++++------ .../src/pages/home/Messages/CitationsList.tsx | 2 +- .../src/pages/home/Messages/MessageTools.tsx | 4 ++-- .../components/KnowledgeSearchPopup.tsx | 2 +- 7 files changed, 30 insertions(+), 30 deletions(-) diff --git a/src/renderer/src/components/LocalBackupManager.tsx b/src/renderer/src/components/LocalBackupManager.tsx index de84b0ea74..387831ba1a 100644 --- a/src/renderer/src/components/LocalBackupManager.tsx +++ b/src/renderer/src/components/LocalBackupManager.tsx @@ -46,7 +46,7 @@ export function LocalBackupManager({ visible, onClose, localBackupDir, restoreMe total: files.length })) } catch (error: any) { - message.error(`${t('settings.data.local.backup.manager.fetch.error')}: ${error.message}`) + window.message.error(`${t('settings.data.local.backup.manager.fetch.error')}: ${error.message}`) } finally { setLoading(false) } @@ -91,13 +91,13 @@ export function LocalBackupManager({ visible, onClose, localBackupDir, restoreMe for (const key of selectedRowKeys) { await window.api.backup.deleteLocalBackupFile(key.toString(), localBackupDir) } - message.success( + window.message.success( t('settings.data.local.backup.manager.delete.success.multiple', { count: selectedRowKeys.length }) ) setSelectedRowKeys([]) await fetchBackupFiles() } catch (error: any) { - message.error(`${t('settings.data.local.backup.manager.delete.error')}: ${error.message}`) + window.message.error(`${t('settings.data.local.backup.manager.delete.error')}: ${error.message}`) } finally { setDeleting(false) } @@ -124,7 +124,7 @@ export function LocalBackupManager({ visible, onClose, localBackupDir, restoreMe message.success(t('settings.data.local.backup.manager.delete.success.single')) await fetchBackupFiles() } catch (error: any) { - message.error(`${t('settings.data.local.backup.manager.delete.error')}: ${error.message}`) + window.message.error(`${t('settings.data.local.backup.manager.delete.error')}: ${error.message}`) } finally { setDeleting(false) } @@ -151,7 +151,7 @@ export function LocalBackupManager({ visible, onClose, localBackupDir, restoreMe message.success(t('settings.data.local.backup.manager.restore.success')) onClose() // Close the modal } catch (error: any) { - message.error(`${t('settings.data.local.backup.manager.restore.error')}: ${error.message}`) + window.message.error(`${t('settings.data.local.backup.manager.restore.error')}: ${error.message}`) } finally { setRestoring(false) } diff --git a/src/renderer/src/components/WebdavBackupManager.tsx b/src/renderer/src/components/WebdavBackupManager.tsx index f0f2930686..fdd174b4a6 100644 --- a/src/renderer/src/components/WebdavBackupManager.tsx +++ b/src/renderer/src/components/WebdavBackupManager.tsx @@ -49,7 +49,7 @@ export function WebdavBackupManager({ visible, onClose, webdavConfig, restoreMet const fetchBackupFiles = useCallback(async () => { if (!webdavHost) { - message.error(t('message.error.invalid.webdav')) + window.message.error(t('message.error.invalid.webdav')) return } @@ -67,7 +67,7 @@ export function WebdavBackupManager({ visible, onClose, webdavConfig, restoreMet total: files.length })) } catch (error: any) { - message.error(`${t('settings.data.webdav.backup.manager.fetch.error')}: ${error.message}`) + window.message.error(`${t('settings.data.webdav.backup.manager.fetch.error')}: ${error.message}`) } finally { setLoading(false) } @@ -95,7 +95,7 @@ export function WebdavBackupManager({ visible, onClose, webdavConfig, restoreMet } if (!webdavHost) { - message.error(t('message.error.invalid.webdav')) + window.message.error(t('message.error.invalid.webdav')) return } @@ -118,13 +118,13 @@ export function WebdavBackupManager({ visible, onClose, webdavConfig, restoreMet webdavPath } as WebdavConfig) } - message.success( + window.message.success( t('settings.data.webdav.backup.manager.delete.success.multiple', { count: selectedRowKeys.length }) ) setSelectedRowKeys([]) await fetchBackupFiles() } catch (error: any) { - message.error(`${t('settings.data.webdav.backup.manager.delete.error')}: ${error.message}`) + window.message.error(`${t('settings.data.webdav.backup.manager.delete.error')}: ${error.message}`) } finally { setDeleting(false) } @@ -134,7 +134,7 @@ export function WebdavBackupManager({ visible, onClose, webdavConfig, restoreMet const handleDeleteSingle = async (fileName: string) => { if (!webdavHost) { - message.error(t('message.error.invalid.webdav')) + window.message.error(t('message.error.invalid.webdav')) return } @@ -154,10 +154,10 @@ export function WebdavBackupManager({ visible, onClose, webdavConfig, restoreMet webdavPass, webdavPath } as WebdavConfig) - message.success(t('settings.data.webdav.backup.manager.delete.success.single')) + window.message.success(t('settings.data.webdav.backup.manager.delete.success.single')) await fetchBackupFiles() } catch (error: any) { - message.error(`${t('settings.data.webdav.backup.manager.delete.error')}: ${error.message}`) + window.message.error(`${t('settings.data.webdav.backup.manager.delete.error')}: ${error.message}`) } finally { setDeleting(false) } @@ -167,7 +167,7 @@ export function WebdavBackupManager({ visible, onClose, webdavConfig, restoreMet const handleRestore = async (fileName: string) => { if (!webdavHost) { - message.error(t('message.error.invalid.webdav')) + window.message.error(t('message.error.invalid.webdav')) return } @@ -182,10 +182,10 @@ export function WebdavBackupManager({ visible, onClose, webdavConfig, restoreMet setRestoring(true) try { await (restoreMethod || restoreFromWebdav)(fileName) - message.success(t('settings.data.webdav.backup.manager.restore.success')) + window.message.success(t('settings.data.webdav.backup.manager.restore.success')) onClose() // 关闭模态框 } catch (error: any) { - message.error(`${t('settings.data.webdav.backup.manager.restore.error')}: ${error.message}`) + window.message.error(`${t('settings.data.webdav.backup.manager.restore.error')}: ${error.message}`) } finally { setRestoring(false) } diff --git a/src/renderer/src/pages/apps/App.tsx b/src/renderer/src/pages/apps/App.tsx index d8e751dee7..9fc48086da 100644 --- a/src/renderer/src/pages/apps/App.tsx +++ b/src/renderer/src/pages/apps/App.tsx @@ -4,7 +4,7 @@ import { useMinappPopup } from '@renderer/hooks/useMinappPopup' import { useMinapps } from '@renderer/hooks/useMinapps' import { MinAppType } from '@renderer/types' import type { MenuProps } from 'antd' -import { Dropdown, message } from 'antd' +import { Dropdown } from 'antd' import { FC } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' @@ -61,14 +61,14 @@ const App: FC = ({ app, onClick, size = 60, isLast }) => { const customApps = JSON.parse(content) const updatedApps = customApps.filter((customApp: MinAppType) => customApp.id !== app.id) await window.api.file.writeWithId('custom-minapps.json', JSON.stringify(updatedApps, null, 2)) - message.success(t('settings.miniapps.custom.remove_success')) + window.message.success(t('settings.miniapps.custom.remove_success')) const reloadedApps = [...ORIGIN_DEFAULT_MIN_APPS, ...(await loadCustomMiniApp())] updateDefaultMinApps(reloadedApps) updateMinapps(minapps.filter((item) => item.id !== app.id)) updatePinnedMinapps(pinned.filter((item) => item.id !== app.id)) updateDisabledMinapps(disabled.filter((item) => item.id !== app.id)) } catch (error) { - message.error(t('settings.miniapps.custom.remove_error')) + window.message.error(t('settings.miniapps.custom.remove_error')) console.error('Failed to remove custom mini app:', error) } } diff --git a/src/renderer/src/pages/apps/NewAppButton.tsx b/src/renderer/src/pages/apps/NewAppButton.tsx index a09f4c86f3..af76a25ee3 100644 --- a/src/renderer/src/pages/apps/NewAppButton.tsx +++ b/src/renderer/src/pages/apps/NewAppButton.tsx @@ -2,7 +2,7 @@ import { PlusOutlined, UploadOutlined } from '@ant-design/icons' import { loadCustomMiniApp, ORIGIN_DEFAULT_MIN_APPS, updateDefaultMinApps } from '@renderer/config/minapps' import { useMinapps } from '@renderer/hooks/useMinapps' import { MinAppType } from '@renderer/types' -import { Button, Form, Input, message, Modal, Radio, Upload } from 'antd' +import { Button, Form, Input, Modal, Radio, Upload } from 'antd' import type { UploadFile } from 'antd/es/upload/interface' import { FC, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -33,11 +33,11 @@ const NewAppButton: FC = ({ size = 60 }) => { // Check for duplicate ID if (customApps.some((app: MinAppType) => app.id === values.id)) { - message.error(t('settings.miniapps.custom.duplicate_ids', { ids: values.id })) + window.message.error(t('settings.miniapps.custom.duplicate_ids', { ids: values.id })) return } if (ORIGIN_DEFAULT_MIN_APPS.some((app: MinAppType) => app.id === values.id)) { - message.error(t('settings.miniapps.custom.conflicting_ids', { ids: values.id })) + window.message.error(t('settings.miniapps.custom.conflicting_ids', { ids: values.id })) return } @@ -51,7 +51,7 @@ const NewAppButton: FC = ({ size = 60 }) => { } customApps.push(newApp) await window.api.file.writeWithId('custom-minapps.json', JSON.stringify(customApps, null, 2)) - message.success(t('settings.miniapps.custom.save_success')) + window.message.success(t('settings.miniapps.custom.save_success')) setIsModalVisible(false) form.resetFields() setFileList([]) @@ -59,7 +59,7 @@ const NewAppButton: FC = ({ size = 60 }) => { updateDefaultMinApps(reloadedApps) updateMinapps([...minapps, newApp]) } catch (error) { - message.error(t('settings.miniapps.custom.save_error')) + window.message.error(t('settings.miniapps.custom.save_error')) console.error('Failed to save custom mini app:', error) } } @@ -74,14 +74,14 @@ const NewAppButton: FC = ({ size = 60 }) => { reader.onload = (event) => { const base64Data = event.target?.result if (typeof base64Data === 'string') { - message.success(t('settings.miniapps.custom.logo_upload_success')) + window.message.success(t('settings.miniapps.custom.logo_upload_success')) form.setFieldValue('logo', base64Data) } } reader.readAsDataURL(file) } catch (error) { console.error('Failed to read file:', error) - message.error(t('settings.miniapps.custom.logo_upload_error')) + window.message.error(t('settings.miniapps.custom.logo_upload_error')) } } } diff --git a/src/renderer/src/pages/home/Messages/CitationsList.tsx b/src/renderer/src/pages/home/Messages/CitationsList.tsx index 8174f556b1..56a8ce584c 100644 --- a/src/renderer/src/pages/home/Messages/CitationsList.tsx +++ b/src/renderer/src/pages/home/Messages/CitationsList.tsx @@ -125,7 +125,7 @@ const CopyButton: React.FC<{ content: string }> = ({ content }) => { .writeText(content) .then(() => { setCopied(true) - message.success(t('common.copied')) + window.message.success(t('common.copied')) setTimeout(() => setCopied(false), 2000) }) .catch(() => { diff --git a/src/renderer/src/pages/home/Messages/MessageTools.tsx b/src/renderer/src/pages/home/Messages/MessageTools.tsx index feb713c362..82503c83ef 100644 --- a/src/renderer/src/pages/home/Messages/MessageTools.tsx +++ b/src/renderer/src/pages/home/Messages/MessageTools.tsx @@ -114,7 +114,7 @@ const MessageTools: FC = ({ block }) => { try { const success = await window.api.mcp.abortTool(toolResponse.id) if (success) { - message.success({ content: t('message.tools.aborted'), key: 'abort-tool' }) + window.message.success({ content: t('message.tools.aborted'), key: 'abort-tool' }) } else { message.error({ content: t('message.tools.abort_failed'), key: 'abort-tool' }) } @@ -152,7 +152,7 @@ const MessageTools: FC = ({ block }) => { // Also confirm the current tool confirmToolAction(id) - message.success({ + window.message.success({ content: t('message.tools.autoApproveEnabled', 'Auto-approve enabled for this tool'), key: 'auto-approve' }) diff --git a/src/renderer/src/pages/knowledge/components/KnowledgeSearchPopup.tsx b/src/renderer/src/pages/knowledge/components/KnowledgeSearchPopup.tsx index 28eb009079..78397a8648 100644 --- a/src/renderer/src/pages/knowledge/components/KnowledgeSearchPopup.tsx +++ b/src/renderer/src/pages/knowledge/components/KnowledgeSearchPopup.tsx @@ -80,7 +80,7 @@ const PopupContainer: React.FC = ({ base, resolve }) => { message.success(t('message.copied')) } catch (error) { console.error('Failed to copy text:', error) - message.error(t('message.copyError') || 'Failed to copy text') + window.message.error(t('message.copyError') || 'Failed to copy text') } } From 30b080efbd5c255027a57541fa9b069badffd1b7 Mon Sep 17 00:00:00 2001 From: kangfenmao Date: Thu, 17 Jul 2025 13:39:11 +0800 Subject: [PATCH 007/204] fix: handle optional list length in DraggableVirtualList and update padding in QuickPanel --- .../CodeBlockView/HtmlArtifactsPopup.tsx | 3 +- .../components/DraggableList/virtual-list.tsx | 2 +- .../src/components/QuickPanel/view.tsx | 2 +- src/renderer/src/config/prompts.ts | 1 + src/renderer/src/i18n/locales/en-us.json | 8 +- src/renderer/src/i18n/locales/ja-jp.json | 8 +- src/renderer/src/i18n/locales/ru-ru.json | 8 +- src/renderer/src/i18n/locales/zh-cn.json | 14 ++- src/renderer/src/i18n/locales/zh-tw.json | 10 +- .../src/pages/home/Inputbar/Inputbar.tsx | 2 +- .../pages/knowledge/items/KnowledgeUrls.tsx | 6 +- src/renderer/src/pages/memory/index.tsx | 2 - .../AssistantMemorySettings.tsx | 7 +- .../MemorySettings/MemorySettings.tsx | 112 +++++++++++------- .../src/pages/translate/TranslatePage.tsx | 16 +-- 15 files changed, 115 insertions(+), 86 deletions(-) diff --git a/src/renderer/src/components/CodeBlockView/HtmlArtifactsPopup.tsx b/src/renderer/src/components/CodeBlockView/HtmlArtifactsPopup.tsx index 5e491c4052..98c90d4faf 100644 --- a/src/renderer/src/components/CodeBlockView/HtmlArtifactsPopup.tsx +++ b/src/renderer/src/components/CodeBlockView/HtmlArtifactsPopup.tsx @@ -226,10 +226,11 @@ const StyledModal = styled(Modal)<{ $isFullscreen?: boolean }>` } .ant-modal-header { - padding: 10px 12px !important; + padding: 10px !important; border-bottom: 1px solid var(--color-border); background: var(--color-background); margin-bottom: 0 !important; + border-radius: 0 !important; } ` diff --git a/src/renderer/src/components/DraggableList/virtual-list.tsx b/src/renderer/src/components/DraggableList/virtual-list.tsx index b8e51642e9..78b4c4a697 100644 --- a/src/renderer/src/components/DraggableList/virtual-list.tsx +++ b/src/renderer/src/components/DraggableList/virtual-list.tsx @@ -82,7 +82,7 @@ function DraggableVirtualList({ const parentRef = useRef(null) const virtualizer = useVirtualizer({ - count: list.length, + count: list?.length ?? 0, getScrollElement: useCallback(() => parentRef.current, []), getItemKey: itemKey, estimateSize: useCallback(() => 50, []), diff --git a/src/renderer/src/components/QuickPanel/view.tsx b/src/renderer/src/components/QuickPanel/view.tsx index 37fefdf3b2..dae10e9260 100644 --- a/src/renderer/src/components/QuickPanel/view.tsx +++ b/src/renderer/src/components/QuickPanel/view.tsx @@ -611,7 +611,7 @@ const QuickPanelContainer = styled.div<{ left: 0; right: 0; width: 100%; - padding: 0 30px 0 30px; + padding: 0 35px 0 35px; transform: translateY(-100%); transform-origin: bottom; transition: max-height 0.2s ease; diff --git a/src/renderer/src/config/prompts.ts b/src/renderer/src/config/prompts.ts index ead2dc1333..1e6d3bf60b 100644 --- a/src/renderer/src/config/prompts.ts +++ b/src/renderer/src/config/prompts.ts @@ -410,6 +410,7 @@ export const REFERENCE_PROMPT = `Please answer the question based on the referen - Please cite the context at the end of sentences when appropriate. - Please use the format of citation number [number] to reference the context in corresponding parts of your answer. - If a sentence comes from multiple contexts, please list all relevant citation numbers, e.g., [1][2]. Remember not to group citations at the end but list them in the corresponding parts of your answer. +- If all reference content is not relevant to the user's question, please answer based on your knowledge. ## My question is: diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 20aa889fff..2a3bdd8c31 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -460,7 +460,8 @@ "swap": "Swap", "topics": "Topics", "warning": "Warning", - "you": "You" + "you": "You", + "i_know": "I know" }, "docs": { "title": "Docs" @@ -2308,7 +2309,7 @@ }, "provider": "OCR Provider", "provider_placeholder": "Choose an OCR provider", - "title": "OCR" + "title": "OCR Settings" }, "preprocess": { "provider": "Pre Process Provider", @@ -2557,7 +2558,8 @@ "please_select_embedding_model": "Please select an embedding model", "select_embedding_model_placeholder": "Select Embedding Model", "embedding_dimensions": "Embedding Dimensions", - "stored_memories": "Stored Memories" + "stored_memories": "Stored Memories", + "global_memory_description": "To use memory features, please enable global memory in assistant settings." } } } diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index a58faf34f2..0bf8f99735 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -460,7 +460,8 @@ "swap": "交換", "topics": "トピック", "warning": "警告", - "you": "あなた" + "you": "あなた", + "i_know": "わかりました" }, "docs": { "title": "ドキュメント" @@ -2455,6 +2456,7 @@ "visualization": "可視化" }, "memory": { + "title": "グローバルメモリ", "add_memory": "メモリーを追加", "edit_memory": "メモリーを編集", "memory_content": "メモリー内容", @@ -2476,7 +2478,6 @@ "user": "ユーザー", "content": "内容", "score": "スコア", - "title": "メモリー", "memories_description": "{{total}}件中{{count}}件のメモリーを表示", "search_placeholder": "メモリーを検索...", "start_date": "開始日", @@ -2557,7 +2558,8 @@ "please_select_embedding_model": "埋め込みモデルを選択してください", "select_embedding_model_placeholder": "埋め込みモデルを選択", "embedding_dimensions": "埋め込み次元", - "stored_memories": "保存された記憶" + "stored_memories": "保存された記憶", + "global_memory_description": "メモリ機能を使用するには、アシスタント設定でグローバルメモリを有効にしてください。" } } } diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index f9831b64ef..bf053db843 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -460,7 +460,8 @@ "swap": "Поменять местами", "topics": "Топики", "warning": "Предупреждение", - "you": "Вы" + "you": "Вы", + "i_know": "Я понял" }, "docs": { "title": "Документация" @@ -2455,6 +2456,7 @@ "visualization": "Визуализация" }, "memory": { + "title": "Глобальная память", "add_memory": "Добавить память", "edit_memory": "Редактировать память", "memory_content": "Содержимое памяти", @@ -2531,7 +2533,6 @@ "total_memories": "всего воспоминаний", "default": "По умолчанию", "custom": "Пользовательский", - "title": "Воспоминания", "description": "Память позволяет хранить и управлять информацией о ваших взаимодействиях с ассистентом. Вы можете добавлять, редактировать и удалять воспоминания, а также фильтровать и искать их.", "global_memory_enabled": "Глобальная память включена", "global_memory": "Глобальная память", @@ -2557,7 +2558,8 @@ "please_select_embedding_model": "Пожалуйста, выберите модель для внедрения", "select_embedding_model_placeholder": "Выберите модель внедрения", "embedding_dimensions": "Размерность вложения", - "stored_memories": "Запасённые воспоминания" + "stored_memories": "Запасённые воспоминания", + "global_memory_description": "Для использования функций памяти необходимо включить глобальную память в настройках ассистента." } } } diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index f520aee3d7..3c9ebb0f51 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -460,7 +460,8 @@ "swap": "交换", "topics": "话题", "warning": "警告", - "you": "用户" + "you": "用户", + "i_know": "我知道了" }, "docs": { "title": "帮助文档" @@ -2308,7 +2309,7 @@ }, "provider": "OCR 服务商", "provider_placeholder": "选择一个 OCR 服务商", - "title": "OCR" + "title": "OCR 文字识别" }, "preprocess": { "provider": "文档预处理服务商", @@ -2455,6 +2456,7 @@ "visualization": "可视化" }, "memory": { + "title": "全局记忆", "settings": "设置", "statistics": "统计", "search": "搜索", @@ -2464,8 +2466,8 @@ "memory_content": "记忆内容", "please_enter_memory": "请输入记忆内容", "memory_placeholder": "输入记忆内容...", - "user_id": "用户ID", - "user_id_placeholder": "输入用户ID(可选)", + "user_id": "用户 ID", + "user_id_placeholder": "输入用户 ID(可选)", "load_failed": "加载记忆失败", "add_success": "记忆添加成功", "add_failed": "添加记忆失败", @@ -2480,7 +2482,6 @@ "user": "用户", "content": "内容", "score": "分数", - "title": "记忆", "memories_description": "显示 {{count}} / {{total}} 条记忆", "search_placeholder": "搜索记忆...", "start_date": "开始日期", @@ -2557,7 +2558,8 @@ "please_select_embedding_model": "请选择嵌入模型", "select_embedding_model_placeholder": "选择嵌入模型", "embedding_dimensions": "嵌入维度", - "stored_memories": "已存储记忆" + "stored_memories": "已存储记忆", + "global_memory_description": "需要开启助手设置中的全局记忆才能使用" } } } diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index baaedbb2da..afc62bbcf8 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -460,7 +460,8 @@ "swap": "交換", "topics": "話題", "warning": "警告", - "you": "您" + "you": "您", + "i_know": "我知道了" }, "docs": { "title": "說明文件" @@ -2308,7 +2309,7 @@ }, "provider": "OCR 供應商", "provider_placeholder": "選擇一個OCR服務提供商", - "title": "光學字符識別" + "title": "OCR 文字識別" }, "preprocess": { "provider": "前置處理供應商", @@ -2455,6 +2456,7 @@ "visualization": "視覺化" }, "memory": { + "title": "全域記憶", "add_memory": "新增記憶", "edit_memory": "編輯記憶", "memory_content": "記憶內容", @@ -2476,7 +2478,6 @@ "user": "使用者", "content": "內容", "score": "分數", - "title": "記憶", "memories_description": "顯示 {{count}} / {{total}} 條記憶", "search_placeholder": "搜尋記憶...", "start_date": "開始日期", @@ -2557,7 +2558,8 @@ "please_select_embedding_model": "請選擇一個嵌入模型", "select_embedding_model_placeholder": "選擇嵌入模型", "embedding_dimensions": "嵌入維度", - "stored_memories": "儲存的記憶" + "stored_memories": "儲存的記憶", + "global_memory_description": "需要開啟助手設定中的全域記憶才能使用" } } } diff --git a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx index 5c1acab130..94dcd72ee8 100644 --- a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx +++ b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx @@ -960,7 +960,7 @@ const InputBarContainer = styled.div` border: 0.5px solid var(--color-border); transition: all 0.2s ease; position: relative; - border-radius: 20px; + border-radius: 17px; padding-top: 8px; // 为拖动手柄留出空间 background-color: var(--color-background-opacity); diff --git a/src/renderer/src/pages/knowledge/items/KnowledgeUrls.tsx b/src/renderer/src/pages/knowledge/items/KnowledgeUrls.tsx index 4e3dcd80b0..eee852f957 100644 --- a/src/renderer/src/pages/knowledge/items/KnowledgeUrls.tsx +++ b/src/renderer/src/pages/knowledge/items/KnowledgeUrls.tsx @@ -6,7 +6,7 @@ import { useKnowledge } from '@renderer/hooks/useKnowledge' import FileItem from '@renderer/pages/files/FileItem' import { getProviderName } from '@renderer/services/ProviderService' import { KnowledgeBase, KnowledgeItem } from '@renderer/types' -import { Button, Dropdown, message, Tooltip } from 'antd' +import { Button, Dropdown, Tooltip } from 'antd' import dayjs from 'dayjs' import { Plus } from 'lucide-react' import { FC } from 'react' @@ -72,7 +72,7 @@ const KnowledgeUrls: FC = ({ selectedBase }) => { if (!urlItems.find((item) => item.content === url.trim())) { addUrl(url.trim()) } else { - message.success(t('knowledge.url_added')) + window.message.success(t('knowledge.url_added')) } } catch (e) { // Skip invalid URLs silently @@ -143,7 +143,7 @@ const KnowledgeUrls: FC = ({ selectedBase }) => { label: t('common.copy'), onClick: () => { navigator.clipboard.writeText(item.content as string) - message.success(t('message.copied')) + window.message.success(t('message.copied')) } } ] diff --git a/src/renderer/src/pages/memory/index.tsx b/src/renderer/src/pages/memory/index.tsx index bd5af9ad52..e4ea9020ea 100644 --- a/src/renderer/src/pages/memory/index.tsx +++ b/src/renderer/src/pages/memory/index.tsx @@ -539,8 +539,6 @@ const MemoriesPage = () => { title: t('memory.delete_user_confirm_title'), content: t('memory.delete_user_confirm_content', { user: userId }), icon: , - okText: t('common.yes'), - cancelText: t('common.no'), okType: 'danger', onOk: async () => { try { diff --git a/src/renderer/src/pages/settings/AssistantSettings/AssistantMemorySettings.tsx b/src/renderer/src/pages/settings/AssistantSettings/AssistantMemorySettings.tsx index a424617f17..6fb3e30ef2 100644 --- a/src/renderer/src/pages/settings/AssistantSettings/AssistantMemorySettings.tsx +++ b/src/renderer/src/pages/settings/AssistantSettings/AssistantMemorySettings.tsx @@ -1,10 +1,11 @@ -import { InfoCircleOutlined, SettingOutlined } from '@ant-design/icons' +import { InfoCircleOutlined } from '@ant-design/icons' import { Box } from '@renderer/components/Layout' import MemoryService from '@renderer/services/MemoryService' import { selectGlobalMemoryEnabled, selectMemoryConfig } from '@renderer/store/memory' import { Assistant, AssistantSettings } from '@renderer/types' import { Alert, Button, Card, Space, Switch, Tooltip, Typography } from 'antd' import { useForm } from 'antd/es/form/Form' +import { Settings2 } from 'lucide-react' import { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { useSelector } from 'react-redux' @@ -78,9 +79,7 @@ const AssistantMemorySettings: React.FC = ({ assistant, updateAssistant, - + + ) + )} diff --git a/src/renderer/src/pages/home/Chat.tsx b/src/renderer/src/pages/home/Chat.tsx index 0860440b69..d3aec9df98 100644 --- a/src/renderer/src/pages/home/Chat.tsx +++ b/src/renderer/src/pages/home/Chat.tsx @@ -1,20 +1,22 @@ import { loggerService } from '@logger' import { ContentSearch, ContentSearchRef } from '@renderer/components/ContentSearch' +import { HStack } from '@renderer/components/Layout' import MultiSelectActionPopup from '@renderer/components/Popups/MultiSelectionPopup' import { QuickPanelProvider } from '@renderer/components/QuickPanel' import { useAssistant } from '@renderer/hooks/useAssistant' import { useChatContext } from '@renderer/hooks/useChatContext' -import { useSettings } from '@renderer/hooks/useSettings' +import { useNavbarPosition, useSettings } from '@renderer/hooks/useSettings' import { useShortcut } from '@renderer/hooks/useShortcuts' -import { useShowTopics } from '@renderer/hooks/useStore' +import { useShowAssistants, useShowTopics } from '@renderer/hooks/useStore' import { Assistant, Topic } from '@renderer/types' import { classNames } from '@renderer/utils' import { Flex } from 'antd' import { debounce } from 'lodash' -import React, { FC, useMemo, useState } from 'react' +import React, { FC, useState } from 'react' import { useHotkeys } from 'react-hotkeys-hook' import styled from 'styled-components' +import ChatNavbar from './ChatNavbar' import Inputbar from './Inputbar/Inputbar' import Messages from './Messages/Messages' import Tabs from './Tabs' @@ -30,20 +32,16 @@ interface Props { const Chat: FC = (props) => { const { assistant } = useAssistant(props.assistant.id) - const { topicPosition, messageStyle, showAssistants } = useSettings() + const { topicPosition, messageStyle } = useSettings() const { showTopics } = useShowTopics() const { isMultiSelectMode } = useChatContext(props.activeTopic) + const { isTopNavbar } = useNavbarPosition() const mainRef = React.useRef(null) const contentSearchRef = React.useRef(null) const [filterIncludeUser, setFilterIncludeUser] = useState(false) - const maxWidth = useMemo(() => { - const showRightTopics = showTopics && topicPosition === 'right' - const minusAssistantsWidth = showAssistants ? '- var(--assistants-width)' : '' - const minusRightTopicsWidth = showRightTopics ? '- var(--assistants-width)' : '' - return `calc(100vw - var(--sidebar-width) ${minusAssistantsWidth} ${minusRightTopicsWidth})` - }, [showAssistants, showTopics, topicPosition]) + const maxWidth = useChatMaxWidth() useHotkeys('esc', () => { contentSearchRef.current?.disable() @@ -92,61 +90,103 @@ const Chat: FC = (props) => { const firstUpdateOrNoFirstUpdateHandler = debounce(() => { contentSearchRef.current?.silentSearch() }, 10) + const messagesComponentUpdateHandler = () => { if (firstUpdateCompleted) { firstUpdateOrNoFirstUpdateHandler() } } + const messagesComponentFirstUpdateHandler = () => { setTimeout(() => (firstUpdateCompleted = true), 300) firstUpdateOrNoFirstUpdateHandler() } + const mainHeight = isTopNavbar + ? 'calc(100vh - var(--navbar-height) - var(--navbar-height) - 12px)' + : 'calc(100vh - var(--navbar-height))' + return ( -
- - } - filter={contentSearchFilter} - includeUser={filterIncludeUser} - onIncludeUserChange={userOutlinedItemClickHandler} - /> - - - {isMultiSelectMode && } - -
- {topicPosition === 'right' && showTopics && ( - )} + +
+ + } + filter={contentSearchFilter} + includeUser={filterIncludeUser} + onIncludeUserChange={userOutlinedItemClickHandler} + /> + + + {isMultiSelectMode && } + +
+ {topicPosition === 'right' && showTopics && ( + + )} +
) } +export const useChatMaxWidth = () => { + const { showTopics, topicPosition } = useSettings() + const { isLeftNavbar } = useNavbarPosition() + const { showAssistants } = useShowAssistants() + const showRightTopics = showTopics && topicPosition === 'right' + const minusAssistantsWidth = showAssistants ? '- var(--assistants-width)' : '' + const minusRightTopicsWidth = showRightTopics ? '- var(--assistants-width)' : '' + return `calc(100vw - ${isLeftNavbar ? 'var(--sidebar-width)' : '0'} ${minusAssistantsWidth} ${minusRightTopicsWidth})` +} + const Container = styled.div` display: flex; - flex-direction: row; - height: 100%; + flex-direction: column; + height: calc(100vh - var(--navbar-height)); flex: 1; + [navbar-position='top'] & { + height: calc(100vh - var(--navbar-height) -6px); + background-color: var(--color-background); + border-top-left-radius: 10px; + border-bottom-left-radius: 10px; + overflow: hidden; + } ` const Main = styled(Flex)` - height: calc(100vh - var(--navbar-height)); + [navbar-position='left'] & { + height: calc(100vh - var(--navbar-height)); + } transform: translateZ(0); position: relative; ` diff --git a/src/renderer/src/pages/home/ChatNavbar.tsx b/src/renderer/src/pages/home/ChatNavbar.tsx new file mode 100644 index 0000000000..0172153e35 --- /dev/null +++ b/src/renderer/src/pages/home/ChatNavbar.tsx @@ -0,0 +1,183 @@ +import { NavbarHeader } from '@renderer/components/app/Navbar' +import { HStack } from '@renderer/components/Layout' +import SearchPopup from '@renderer/components/Popups/SearchPopup' +import { isMac } from '@renderer/config/constant' +import { useAssistant } from '@renderer/hooks/useAssistant' +import { useFullscreen } from '@renderer/hooks/useFullscreen' +import { modelGenerating } from '@renderer/hooks/useRuntime' +import { useSettings } from '@renderer/hooks/useSettings' +import { useShortcut } from '@renderer/hooks/useShortcuts' +import { useShowAssistants, useShowTopics } from '@renderer/hooks/useStore' +import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' +import { useAppDispatch } from '@renderer/store' +import { setNarrowMode } from '@renderer/store/settings' +import { Assistant, Topic } from '@renderer/types' +import { Tooltip } from 'antd' +import { t } from 'i18next' +import { Menu, PanelLeftClose, PanelRightClose, Search } from 'lucide-react' +import { FC, useCallback } from 'react' +import styled from 'styled-components' + +import AssistantsDrawer from './components/AssistantsDrawer' +import SelectModelButton from './components/SelectModelButton' +import UpdateAppButton from './components/UpdateAppButton' + +interface Props { + activeAssistant: Assistant + activeTopic: Topic + setActiveTopic: (topic: Topic) => void + setActiveAssistant: (assistant: Assistant) => void + position: 'left' | 'right' +} + +const HeaderNavbar: FC = ({ activeAssistant, setActiveAssistant, activeTopic, setActiveTopic }) => { + const { assistant } = useAssistant(activeAssistant.id) + const { showAssistants, toggleShowAssistants } = useShowAssistants() + const isFullscreen = useFullscreen() + const { topicPosition, narrowMode } = useSettings() + const { showTopics, toggleShowTopics } = useShowTopics() + const dispatch = useAppDispatch() + + // Function to toggle assistants with cooldown + const handleToggleShowAssistants = useCallback(() => { + if (showAssistants) { + toggleShowAssistants() + } else { + toggleShowAssistants() + } + }, [showAssistants, toggleShowAssistants]) + + const handleToggleShowTopics = useCallback(() => { + if (showTopics) { + toggleShowTopics() + } else { + toggleShowTopics() + } + }, [showTopics, toggleShowTopics]) + + useShortcut('toggle_show_assistants', handleToggleShowAssistants) + + useShortcut('toggle_show_topics', () => { + if (topicPosition === 'right') { + toggleShowTopics() + } else { + EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR) + } + }) + + useShortcut('search_message', () => { + SearchPopup.show() + }) + + const handleNarrowModeToggle = async () => { + await modelGenerating() + dispatch(setNarrowMode(!narrowMode)) + } + + const onShowAssistantsDrawer = () => { + AssistantsDrawer.show({ + activeAssistant, + setActiveAssistant, + activeTopic, + setActiveTopic + }) + } + + return ( + + + {showAssistants && ( + + + + + + )} + {!showAssistants && ( + + toggleShowAssistants()} + style={{ marginRight: 8, marginLeft: isMac && !isFullscreen ? 4 : -12 }}> + + + + )} + {!showAssistants && ( + + + + )} + + + + + + SearchPopup.show()}> + + + + + + + + + {topicPosition === 'right' && !showTopics && ( + + toggleShowTopics()}> + + + + )} + {topicPosition === 'right' && showTopics && ( + + handleToggleShowTopics()}> + + + + )} + + + ) +} + +export const NavbarIcon = styled.div` + -webkit-app-region: none; + border-radius: 8px; + height: 30px; + padding: 0 7px; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + transition: all 0.2s ease-in-out; + cursor: pointer; + .iconfont { + font-size: 18px; + color: var(--color-icon); + &.icon-a-addchat { + font-size: 20px; + } + &.icon-a-darkmode { + font-size: 20px; + } + &.icon-appstore { + font-size: 20px; + } + } + .anticon { + color: var(--color-icon); + font-size: 16px; + } + &:hover { + background-color: var(--color-background-mute); + color: var(--color-icon-white); + } +` + +const NarrowIcon = styled(NavbarIcon)` + @media (max-width: 1000px) { + display: none; + } +` + +export default HeaderNavbar diff --git a/src/renderer/src/pages/home/HomePage.tsx b/src/renderer/src/pages/home/HomePage.tsx index 78ac37bde3..66aa73c852 100644 --- a/src/renderer/src/pages/home/HomePage.tsx +++ b/src/renderer/src/pages/home/HomePage.tsx @@ -1,5 +1,5 @@ import { useAssistants } from '@renderer/hooks/useAssistant' -import { useSettings } from '@renderer/hooks/useSettings' +import { useNavbarPosition, useSettings } from '@renderer/hooks/useSettings' import { useActiveTopic } from '@renderer/hooks/useTopic' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import NavigationService from '@renderer/services/NavigationService' @@ -17,6 +17,7 @@ let _activeAssistant: Assistant const HomePage: FC = () => { const { assistants } = useAssistants() const navigate = useNavigate() + const { isLeftNavbar } = useNavbarPosition() const location = useLocation() const state = location.state @@ -81,14 +82,16 @@ const HomePage: FC = () => { return ( - - + {isLeftNavbar && ( + + )} + {showAssistants && ( { [isGrid, isGrouped, topic, multiModelMessageStyle, messages.length, selectedMessageId, gridPopoverTrigger] ) + const maxWidth = useChatMaxWidth() + return ( + className={classNames([multiModelMessageStyle, { 'multi-select-mode': isMultiSelectMode }])} + style={{ maxWidth }}> { } const GroupContainer = styled.div` + [navbar-position='left'] & { + max-width: calc(100vw - var(--sidebar-width) - var(--assistants-width) - 20px); + } &.horizontal, &.grid { padding: 4px 10px; diff --git a/src/renderer/src/pages/home/Messages/Messages.tsx b/src/renderer/src/pages/home/Messages/Messages.tsx index 968c4152d5..8e0532d7ae 100644 --- a/src/renderer/src/pages/home/Messages/Messages.tsx +++ b/src/renderer/src/pages/home/Messages/Messages.tsx @@ -379,7 +379,7 @@ const LoaderContainer = styled.div` const ScrollContainer = styled.div` display: flex; flex-direction: column-reverse; - padding: 10px 16px 20px; + padding: 10px 10px 20px; .multi-select-mode & { padding-bottom: 60px; } diff --git a/src/renderer/src/pages/home/Navbar.tsx b/src/renderer/src/pages/home/Navbar.tsx index 4ef2c7e673..7e65c25cfa 100644 --- a/src/renderer/src/pages/home/Navbar.tsx +++ b/src/renderer/src/pages/home/Navbar.tsx @@ -1,6 +1,5 @@ import { Navbar, NavbarLeft, NavbarRight } from '@renderer/components/app/Navbar' import { HStack } from '@renderer/components/Layout' -import FloatingSidebar from '@renderer/components/Popups/FloatingSidebar' import SearchPopup from '@renderer/components/Popups/SearchPopup' import { isMac } from '@renderer/config/constant' import { useAssistant } from '@renderer/hooks/useAssistant' @@ -15,10 +14,11 @@ import { setNarrowMode } from '@renderer/store/settings' import { Assistant, Topic } from '@renderer/types' import { Tooltip } from 'antd' import { t } from 'i18next' -import { MessageSquareDiff, PanelLeftClose, PanelRightClose, Search } from 'lucide-react' -import { FC, useCallback, useState } from 'react' +import { Menu, MessageSquareDiff, PanelLeftClose, PanelRightClose, Search } from 'lucide-react' +import { FC, useCallback } from 'react' import styled from 'styled-components' +import AssistantsDrawer from './components/AssistantsDrawer' import SelectModelButton from './components/SelectModelButton' import UpdateAppButton from './components/UpdateAppButton' @@ -37,32 +37,20 @@ const HeaderNavbar: FC = ({ activeAssistant, setActiveAssistant, activeTo const { topicPosition, narrowMode } = useSettings() const { showTopics, toggleShowTopics } = useShowTopics() const dispatch = useAppDispatch() - const [sidebarHideCooldown, setSidebarHideCooldown] = useState(false) // Function to toggle assistants with cooldown const handleToggleShowAssistants = useCallback(() => { if (showAssistants) { - // When hiding sidebar, set cooldown toggleShowAssistants() - setSidebarHideCooldown(true) - // setTimeout(() => { - // setSidebarHideCooldown(false) - // }, 10000) // 10 seconds cooldown } else { - // When showing sidebar, no cooldown needed toggleShowAssistants() } }, [showAssistants, toggleShowAssistants]) + const handleToggleShowTopics = useCallback(() => { if (showTopics) { - // When hiding sidebar, set cooldown toggleShowTopics() - setSidebarHideCooldown(true) - // setTimeout(() => { - // setSidebarHideCooldown(false) - // }, 10000) // 10 seconds cooldown } else { - // When showing sidebar, no cooldown needed toggleShowTopics() } }, [showTopics, toggleShowTopics]) @@ -86,6 +74,15 @@ const HeaderNavbar: FC = ({ activeAssistant, setActiveAssistant, activeTo dispatch(setNarrowMode(!narrowMode)) } + const onShowAssistantsDrawer = () => { + AssistantsDrawer.show({ + activeAssistant, + setActiveAssistant, + activeTopic, + setActiveTopic + }) + } + return ( {showAssistants && ( @@ -104,32 +101,20 @@ const HeaderNavbar: FC = ({ activeAssistant, setActiveAssistant, activeTo )} - {!showAssistants && !sidebarHideCooldown && ( - - - toggleShowAssistants()} - style={{ marginRight: 8, marginLeft: isMac && !isFullscreen ? 4 : -12 }}> - - - - - )} - {!showAssistants && sidebarHideCooldown && ( + {!showAssistants && ( toggleShowAssistants()} - style={{ marginRight: 8, marginLeft: isMac && !isFullscreen ? 4 : -12 }} - onMouseOut={() => setSidebarHideCooldown(false)}> + style={{ marginRight: 8, marginLeft: isMac && !isFullscreen ? 4 : -12 }}> )} + {!showAssistants && ( + + + + )} @@ -144,23 +129,9 @@ const HeaderNavbar: FC = ({ activeAssistant, setActiveAssistant, activeTo - {topicPosition === 'right' && !showTopics && !sidebarHideCooldown && ( - - - toggleShowTopics()}> - - - - - )} - {topicPosition === 'right' && !showTopics && sidebarHideCooldown && ( + {topicPosition === 'right' && !showTopics && ( - toggleShowTopics()} onMouseOut={() => setSidebarHideCooldown(false)}> + toggleShowTopics()}> diff --git a/src/renderer/src/pages/home/Tabs/AssistantsTab.tsx b/src/renderer/src/pages/home/Tabs/AssistantsTab.tsx index 20e3456be6..e4e25a1311 100644 --- a/src/renderer/src/pages/home/Tabs/AssistantsTab.tsx +++ b/src/renderer/src/pages/home/Tabs/AssistantsTab.tsx @@ -185,15 +185,11 @@ const AssistantAddItem = styled.div` padding-right: 35px; border-radius: var(--list-item-border-radius); border: 0.5px solid transparent; + margin-top: -8px; cursor: pointer; &:hover { - background-color: var(--color-background-soft); - } - - &.active { - background-color: var(--color-background-soft); - border: 0.5px solid var(--color-border); + background-color: var(--color-list-item-hover); } ` diff --git a/src/renderer/src/pages/home/Tabs/TopicsTab.tsx b/src/renderer/src/pages/home/Tabs/TopicsTab.tsx index d296ccdcf9..1f3997ff8a 100644 --- a/src/renderer/src/pages/home/Tabs/TopicsTab.tsx +++ b/src/renderer/src/pages/home/Tabs/TopicsTab.tsx @@ -5,6 +5,7 @@ import { EditOutlined, FolderOutlined, MenuOutlined, + PlusOutlined, PushpinOutlined, QuestionCircleOutlined, UploadOutlined @@ -24,7 +25,7 @@ import store from '@renderer/store' import { RootState } from '@renderer/store' import { setGenerating } from '@renderer/store/runtime' import { Assistant, Topic } from '@renderer/types' -import { removeSpecialCharactersForFileName } from '@renderer/utils' +import { classNames, removeSpecialCharactersForFileName } from '@renderer/utils' import { copyTopicAsMarkdown, copyTopicAsPlainText } from '@renderer/utils/copy' import { exportMarkdownToJoplin, @@ -48,13 +49,14 @@ interface Props { assistant: Assistant activeTopic: Topic setActiveTopic: (topic: Topic) => void + position: 'left' | 'right' } -const Topics: FC = ({ assistant: _assistant, activeTopic, setActiveTopic }) => { +const Topics: FC = ({ assistant: _assistant, activeTopic, setActiveTopic, position }) => { const { assistants } = useAssistants() const { assistant, removeTopic, moveTopic, updateTopic, updateTopics } = useAssistant(_assistant.id) const { t } = useTranslation() - const { showTopicTime, pinTopicsToTop, setTopicPosition } = useSettings() + const { showTopicTime, pinTopicsToTop, setTopicPosition, topicPosition } = useSettings() const renamingTopics = useSelector((state: RootState) => state.runtime.chat.renamingTopics) const newlyRenamedTopics = useSelector((state: RootState) => state.runtime.chat.newlyRenamedTopics) @@ -443,13 +445,21 @@ const Topics: FC = ({ assistant: _assistant, activeTopic, setActiveTopic return assistant.topics }, [assistant.topics, pinTopicsToTop]) + const singlealone = topicPosition === 'right' && position === 'right' + return ( + itemContainerStyle={{ paddingBottom: '8px' }} + header={ + EventEmitter.emit(EVENT_NAMES.ADD_NEW_TOPIC)}> + + {t('chat.add.topic.title')} + + }> {(topic) => { const isActive = topic.id === activeTopic?.id const topicName = topic.name.replace('`', '') @@ -466,7 +476,7 @@ const Topics: FC = ({ assistant: _assistant, activeTopic, setActiveTopic setTargetTopic(topic)} - className={isActive ? 'active' : ''} + className={classNames(isActive ? 'active' : '', singlealone ? 'singlealone' : '')} onClick={() => onSwitchTopic(topic)} style={{ borderRadius }}> {isPending(topic.id) && !isActive && } @@ -548,6 +558,7 @@ const TopicListItem = styled.div` } &.active { background-color: var(--color-list-item); + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); .menu { opacity: 1; &:hover { @@ -555,6 +566,16 @@ const TopicListItem = styled.div` } } } + &.singlealone { + border-radius: 0 !important; + &:hover { + background-color: var(--color-background-soft); + } + &.active { + border-left: 2px solid var(--color-primary); + box-shadow: none; + } + } ` const TopicNameContainer = styled.div` @@ -626,6 +647,31 @@ const PendingIndicator = styled.div.attrs({ background-color: var(--color-primary); ` +const AddTopicButton = styled.div` + display: flex; + align-items: center; + gap: 6px; + width: calc(100% - 10px); + padding: 7px 12px; + margin-bottom: 8px; + background: transparent; + color: var(--color-text-2); + font-size: 13px; + border-radius: var(--list-item-border-radius); + cursor: pointer; + transition: all 0.2s; + margin-top: -5px; + + &:hover { + background-color: var(--color-list-item-hover); + color: var(--color-text-1); + } + + .anticon { + font-size: 12px; + } +` + const TopicPromptText = styled.div` color: var(--color-text-2); font-size: 12px; diff --git a/src/renderer/src/pages/home/Tabs/components/AssistantItem.tsx b/src/renderer/src/pages/home/Tabs/components/AssistantItem.tsx index f0ae3e8883..0c169a7c24 100644 --- a/src/renderer/src/pages/home/Tabs/components/AssistantItem.tsx +++ b/src/renderer/src/pages/home/Tabs/components/AssistantItem.tsx @@ -390,6 +390,7 @@ const Container = styled.div` } &.active { background-color: var(--color-list-item); + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); } ` diff --git a/src/renderer/src/pages/home/Tabs/index.tsx b/src/renderer/src/pages/home/Tabs/index.tsx index 21f3a21e43..5a8c6b67d5 100644 --- a/src/renderer/src/pages/home/Tabs/index.tsx +++ b/src/renderer/src/pages/home/Tabs/index.tsx @@ -1,11 +1,10 @@ import AddAssistantPopup from '@renderer/components/Popups/AddAssistantPopup' import { useAssistants, useDefaultAssistant } from '@renderer/hooks/useAssistant' -import { useSettings } from '@renderer/hooks/useSettings' +import { useNavbarPosition, useSettings } from '@renderer/hooks/useSettings' import { useShowTopics } from '@renderer/hooks/useStore' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import { Assistant, Topic } from '@renderer/types' import { uuid } from '@renderer/utils' -import { Segmented as AntSegmented, SegmentedProps } from 'antd' import { FC, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' @@ -41,25 +40,22 @@ const HomeTabs: FC = ({ const [tab, setTab] = useState(position === 'left' ? _tab || 'assistants' : 'topic') const { topicPosition } = useSettings() const { defaultAssistant } = useDefaultAssistant() - const { showTopics, toggleShowTopics } = useShowTopics() + const { toggleShowTopics } = useShowTopics() + const { isLeftNavbar } = useNavbarPosition() const { t } = useTranslation() const borderStyle = '0.5px solid var(--color-border)' const border = - position === 'left' ? { borderRight: borderStyle } : { borderLeft: borderStyle, borderTopLeftRadius: 0 } + position === 'left' + ? { borderRight: isLeftNavbar ? borderStyle : 'none' } + : { borderLeft: isLeftNavbar ? borderStyle : 'none', borderTopLeftRadius: 0 } if (position === 'left' && topicPosition === 'left') { _tab = tab } - const showTab = !(position === 'left' && topicPosition === 'right') - - const assistantTab = { - label: t('assistants.abbr'), - value: 'assistants' - // icon: - } + const showTab = position === 'left' && topicPosition === 'left' const onCreateAssistant = async () => { const assistant = await AddAssistantPopup.show() @@ -97,41 +93,36 @@ const HomeTabs: FC = ({ if (position === 'right' && topicPosition === 'right' && tab === 'assistants') { setTab('topic') } - if (position === 'left' && topicPosition === 'right' && forceToSeeAllTab != true && tab !== 'assistants') { + if (position === 'left' && topicPosition === 'right' && tab === 'topic') { setTab('assistants') } }, [position, tab, topicPosition, forceToSeeAllTab]) return ( - {(showTab || (forceToSeeAllTab == true && !showTopics)) && ( - <> - - }, - { - label: t('settings.title'), - value: 'settings' - // icon: - } - ].filter(Boolean) as SegmentedProps['options'] - } - onChange={(value) => setTab(value as 'topic' | 'settings')} - block - /> - - + {position === 'left' && topicPosition === 'left' && ( + + setTab('assistants')}> + {t('assistants.abbr')} + + setTab('topic')}> + {t('common.topics')} + + setTab('settings')}> + {t('settings.title')} + + + )} + + {position === 'left' && topicPosition === 'right' && ( + + setTab('assistants')}> + {t('assistants.abbr')} + + setTab('settings')}> + {t('settings.title')} + + )} @@ -144,7 +135,12 @@ const HomeTabs: FC = ({ /> )} {tab === 'topic' && ( - + )} {tab === 'settings' && } @@ -157,7 +153,12 @@ const Container = styled.div` flex-direction: column; max-width: var(--assistants-width); min-width: var(--assistants-width); - background-color: var(--color-background); + [navbar-position='left'] & { + background-color: var(--color-background); + } + [navbar-position='top'] & { + min-height: calc(100vh - var(--navbar-height) - var(--navbar-height) - 12px); + } overflow: hidden; .collapsed { width: 0; @@ -169,72 +170,62 @@ const TabContent = styled.div` display: flex; flex: 1; flex-direction: column; - overflow-y: auto; + overflow-y: hidden; overflow-x: hidden; ` -const Divider = styled.div` - border-top: 0.5px solid var(--color-border); - margin-top: 10px; - margin-left: 10px; - margin-right: 10px; +const CustomTabs = styled.div` + display: flex; + margin: 0 12px; + padding: 6px 0; + border-bottom: 1px solid var(--color-border); + background: transparent; + [navbar-position='top'] & { + padding-top: 2px; + } ` -const Segmented = styled(AntSegmented)` - font-family: var(--font-family); +const TabItem = styled.button<{ active: boolean }>` + flex: 1; + height: 32px; + border: none; + background: transparent; + color: ${(props) => (props.active ? 'var(--color-text)' : 'var(--color-text-secondary)')}; + font-size: 13px; + font-weight: ${(props) => (props.active ? '600' : '400')}; + cursor: pointer; + border-radius: 8px; + margin: 0 2px; + position: relative; + display: flex; + align-items: center; + justify-content: center; - &.ant-segmented { - background-color: transparent; - margin: 0 10px; - margin-top: 10px; - padding: 0; - } - .ant-segmented-item { - overflow: hidden; - transition: none !important; - height: 34px; - line-height: 34px; - background-color: transparent; - user-select: none; - border-radius: var(--list-item-border-radius); - box-shadow: none; - } - .ant-segmented-item-selected, - .ant-segmented-item-selected:active { - transition: none !important; - background-color: var(--color-list-item); - } - .ant-segmented-item-label { - align-items: center; - display: flex; - flex-direction: row; - justify-content: center; - font-size: 13px; - height: 100%; - } - .ant-segmented-item-label[aria-selected='true'] { + &:hover { color: var(--color-text); } - .icon-business-smart-assistant { - margin-right: -2px; + + &:active { + transform: scale(0.98); } - .ant-segmented-thumb { - transition: none !important; - background-color: var(--color-list-item); - border-radius: var(--list-item-border-radius); - box-shadow: none; - &:hover { - background-color: transparent; - } + + &::after { + content: ''; + position: absolute; + bottom: -9px; + left: 50%; + transform: translateX(-50%); + width: ${(props) => (props.active ? '30px' : '0')}; + height: 3px; + background: var(--color-primary); + border-radius: 1px; + transition: all 0.2s ease; } - .ant-segmented-item-label, - .ant-segmented-item-icon { - display: flex; - align-items: center; + + &:hover::after { + width: ${(props) => (props.active ? '30px' : '16px')}; + background: ${(props) => (props.active ? 'var(--color-primary)' : 'var(--color-primary-soft)')}; } - /* These styles ensure the same appearance as before */ - border-radius: 0; - box-shadow: none; ` export default HomeTabs diff --git a/src/renderer/src/pages/home/components/AssistantsDrawer.tsx b/src/renderer/src/pages/home/components/AssistantsDrawer.tsx new file mode 100644 index 0000000000..58eed840c4 --- /dev/null +++ b/src/renderer/src/pages/home/components/AssistantsDrawer.tsx @@ -0,0 +1,92 @@ +import { TopView } from '@renderer/components/TopView' +import { isMac } from '@renderer/config/constant' +import { Assistant, Topic } from '@renderer/types' +import { Drawer } from 'antd' +import { useState } from 'react' + +import HomeTabs from '../Tabs' + +interface ShowParams { + activeAssistant: Assistant + setActiveAssistant: (assistant: Assistant) => void + activeTopic: Topic + setActiveTopic: (topic: Topic) => void +} + +interface Props extends ShowParams { + resolve: (data: any) => void +} + +const PopupContainer: React.FC = ({ + activeAssistant, + setActiveAssistant, + activeTopic, + setActiveTopic, + resolve +}) => { + const [open, setOpen] = useState(true) + + const onClose = () => { + setOpen(false) + setTimeout(resolve, 300) + } + + AssistantsDrawer.hide = onClose + + return ( + + { + setActiveAssistant(assistant) + onClose() + }} + setActiveTopic={(topic) => { + setActiveTopic(topic) + onClose() + }} + position="left" + /> + + ) +} + +const TopViewKey = 'AssistantsDrawer' + +export default class AssistantsDrawer { + static topviewId = 0 + static hide() { + TopView.hide(TopViewKey) + } + static show(props: ShowParams) { + return new Promise((resolve) => { + TopView.show( + { + resolve(v) + TopView.hide(TopViewKey) + }} + />, + TopViewKey + ) + }) + } +} diff --git a/src/renderer/src/pages/knowledge/KnowledgeContent.tsx b/src/renderer/src/pages/knowledge/KnowledgeContent.tsx index e1b5464781..e8948508e3 100644 --- a/src/renderer/src/pages/knowledge/KnowledgeContent.tsx +++ b/src/renderer/src/pages/knowledge/KnowledgeContent.tsx @@ -3,7 +3,7 @@ import { loggerService } from '@logger' import CustomTag from '@renderer/components/CustomTag' import { HStack } from '@renderer/components/Layout' import { useKnowledge } from '@renderer/hooks/useKnowledge' -import { NavbarIcon } from '@renderer/pages/home/Navbar' +import { NavbarIcon } from '@renderer/pages/home/ChatNavbar' import { getProviderName } from '@renderer/services/ProviderService' import { KnowledgeBase } from '@renderer/types' import { Button, Empty, Tabs, Tag, Tooltip } from 'antd' diff --git a/src/renderer/src/pages/launchpad/LaunchpadPage.tsx b/src/renderer/src/pages/launchpad/LaunchpadPage.tsx new file mode 100644 index 0000000000..c189ae0864 --- /dev/null +++ b/src/renderer/src/pages/launchpad/LaunchpadPage.tsx @@ -0,0 +1,217 @@ +import App from '@renderer/components/MinApp/MinApp' +import { useMinapps } from '@renderer/hooks/useMinapps' +import { useRuntime } from '@renderer/hooks/useRuntime' +import { useSettings } from '@renderer/hooks/useSettings' +import tabsService from '@renderer/services/TabsService' +import { FileSearch, Folder, Languages, LayoutGrid, Palette, Sparkle } from 'lucide-react' +import { FC, useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import { useNavigate } from 'react-router-dom' +import styled from 'styled-components' + +const LaunchpadPage: FC = () => { + const navigate = useNavigate() + const { t } = useTranslation() + const { defaultPaintingProvider } = useSettings() + const { pinned } = useMinapps() + const { openedKeepAliveMinapps } = useRuntime() + + const appMenuItems = [ + { + icon: , + text: t('title.apps'), + path: '/apps', + bgColor: 'linear-gradient(135deg, #8B5CF6, #A855F7)' // 小程序:紫色,代表多功能和灵活性 + }, + { + icon: , + text: t('title.knowledge'), + path: '/knowledge', + bgColor: 'linear-gradient(135deg, #10B981, #34D399)' // 知识库:翠绿色,代表生长和知识 + }, + { + icon: , + text: t('title.paintings'), + path: `/paintings/${defaultPaintingProvider}`, + bgColor: 'linear-gradient(135deg, #EC4899, #F472B6)' // 绘画:活力粉色,代表创造力和艺术 + }, + { + icon: , + text: t('title.agents'), + path: '/agents', + bgColor: 'linear-gradient(135deg, #6366F1, #4F46E5)' // AI助手:靛蓝渐变,代表智能和科技 + }, + { + icon: , + text: t('title.translate'), + path: '/translate', + bgColor: 'linear-gradient(135deg, #06B6D4, #0EA5E9)' // 翻译:明亮的青蓝色,代表沟通和流畅 + }, + { + icon: , + text: t('title.files'), + path: '/files', + bgColor: 'linear-gradient(135deg, #F59E0B, #FBBF24)' // 文件:金色,代表资源和重要性 + } + ] + + // 合并并排序小程序列表 + const sortedMinapps = useMemo(() => { + // 先添加固定的小程序,保持原有顺序 + const result = [...pinned] + + // 再添加其他已打开但未固定的小程序 + openedKeepAliveMinapps.forEach((app) => { + if (!result.some((pinnedApp) => pinnedApp.id === app.id)) { + result.push(app) + } + }) + + return result + }, [openedKeepAliveMinapps, pinned]) + + return ( + + +
+ {t('launchpad.apps')} + + {appMenuItems.map((item) => ( + navigate(item.path)}> + + {item.icon} + + {item.text} + + ))} + +
+ + {sortedMinapps.length > 0 && ( +
+ {t('launchpad.minapps')} + + {sortedMinapps.map((app) => ( + setTimeout(() => tabsService.closeTab('launchpad'), 350)}> + + + ))} + +
+ )} +
+
+ ) +} + +const Container = styled.div` + width: 100%; + flex: 1; + display: flex; + justify-content: center; + align-items: flex-start; + background-color: var(--color-background); + overflow-y: auto; + padding: 50px 0; +` + +const Content = styled.div` + max-width: 720px; + width: 100%; + display: flex; + flex-direction: column; + gap: 20px; +` + +const Section = styled.div` + display: flex; + flex-direction: column; + gap: 8px; +` + +const SectionTitle = styled.h2` + font-size: 14px; + font-weight: 600; + color: var(--color-text); + opacity: 0.8; + margin: 0; + padding: 0 36px; +` + +const Grid = styled.div` + display: grid; + grid-template-columns: repeat(6, 1fr); + gap: 8px; + padding: 0 8px; +` + +const AppIcon = styled.div` + display: flex; + flex-direction: column; + align-items: center; + cursor: pointer; + gap: 4px; + padding: 8px 4px; + border-radius: 16px; + transition: transform 0.2s ease; + + &:hover { + transform: scale(1.05); + } + + &:active { + transform: scale(0.95); + } +` + +const IconContainer = styled.div` + position: relative; + display: flex; + justify-content: center; + align-items: center; + width: 56px; + height: 56px; +` + +const IconWrapper = styled.div<{ bgColor: string }>` + width: 56px; + height: 56px; + border-radius: 16px; + background: ${(props) => props.bgColor}; + display: flex; + justify-content: center; + align-items: center; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + + .icon { + color: white; + width: 28px; + height: 28px; + } +` + +const AppName = styled.div` + font-size: 12px; + color: var(--color-text); + text-align: center; + width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +` + +const AppWrapper = styled.div` + padding: 8px 4px; + border-radius: 8px; + transition: transform 0.2s ease; + + &:hover { + transform: scale(1.05); + } + + &:active { + transform: scale(0.95); + } +` + +export default LaunchpadPage diff --git a/src/renderer/src/pages/apps/AppsPage.tsx b/src/renderer/src/pages/minapps/MinAppsPage.tsx similarity index 51% rename from src/renderer/src/pages/apps/AppsPage.tsx rename to src/renderer/src/pages/minapps/MinAppsPage.tsx index 31cb3f2392..f1c052ca73 100644 --- a/src/renderer/src/pages/apps/AppsPage.tsx +++ b/src/renderer/src/pages/minapps/MinAppsPage.tsx @@ -1,22 +1,22 @@ import { Navbar, NavbarMain } from '@renderer/components/app/Navbar' +import App from '@renderer/components/MinApp/MinApp' +import Scrollbar from '@renderer/components/Scrollbar' import { useMinapps } from '@renderer/hooks/useMinapps' +import { useNavbarPosition } from '@renderer/hooks/useSettings' import { Button, Input } from 'antd' -import { Search, SettingsIcon, X } from 'lucide-react' -import React, { FC, useEffect, useState } from 'react' +import { Search, SettingsIcon } from 'lucide-react' +import React, { FC, useState } from 'react' import { useTranslation } from 'react-i18next' -import { useLocation } from 'react-router' import styled from 'styled-components' -import App from './App' -import MiniAppSettings from './MiniappSettings/MiniAppSettings' +import MinappSettingsPopup from './MiniappSettings/MinappSettingsPopup' import NewAppButton from './NewAppButton' const AppsPage: FC = () => { const { t } = useTranslation() const [search, setSearch] = useState('') const { minapps } = useMinapps() - const [isSettingsOpen, setIsSettingsOpen] = useState(false) - const location = useLocation() + const { isTopNavbar } = useNavbarPosition() const filteredApps = search ? minapps.filter( @@ -35,10 +35,6 @@ const AppsPage: FC = () => { e.preventDefault() } - useEffect(() => { - setIsSettingsOpen(false) - }, [location.key]) - return ( @@ -60,26 +56,47 @@ const AppsPage: FC = () => { suffix={} value={search} onChange={(e) => setSearch(e.target.value)} - disabled={isSettingsOpen} /> @@ -146,10 +142,6 @@ const MiniAppSettings: FC = () => { onChange={(checked) => dispatch(setShowOpenedMinappsInSidebar(checked))} /> - - - - ) } @@ -158,6 +150,7 @@ const Container = styled.div` display: flex; flex-direction: column; flex: 1; + padding-top: 10px; ` // 修改和新增样式 diff --git a/src/renderer/src/pages/apps/NewAppButton.tsx b/src/renderer/src/pages/minapps/NewAppButton.tsx similarity index 100% rename from src/renderer/src/pages/apps/NewAppButton.tsx rename to src/renderer/src/pages/minapps/NewAppButton.tsx diff --git a/src/renderer/src/pages/settings/DisplaySettings/DisplaySettings.tsx b/src/renderer/src/pages/settings/DisplaySettings/DisplaySettings.tsx index 27453ef1cd..276447d72e 100644 --- a/src/renderer/src/pages/settings/DisplaySettings/DisplaySettings.tsx +++ b/src/renderer/src/pages/settings/DisplaySettings/DisplaySettings.tsx @@ -1,9 +1,10 @@ import { SyncOutlined } from '@ant-design/icons' import CodeEditor from '@renderer/components/CodeEditor' import { HStack } from '@renderer/components/Layout' +import TextBadge from '@renderer/components/TextBadge' import { isMac, THEME_COLOR_PRESETS } from '@renderer/config/constant' import { useTheme } from '@renderer/context/ThemeProvider' -import { useSettings } from '@renderer/hooks/useSettings' +import { useNavbarPosition, useSettings } from '@renderer/hooks/useSettings' import useUserTheme from '@renderer/hooks/useUserTheme' import { useAppDispatch } from '@renderer/store' import { @@ -68,6 +69,7 @@ const DisplaySettings: FC = () => { assistantIconType, userTheme } = useSettings() + const { navbarPosition, setNavbarPosition } = useNavbarPosition() const { theme, settedTheme } = useTheme() const { t } = useTranslation() const dispatch = useAppDispatch() @@ -216,6 +218,24 @@ const DisplaySettings: FC = () => { )} + + + {t('settings.display.navbar.title')} + + + + {t('settings.display.navbar.position')} + + + {t('settings.display.zoom.title')} @@ -286,22 +306,24 @@ const DisplaySettings: FC = () => { /> - - - {t('settings.display.sidebar.title')} - - - - - - - + {navbarPosition === 'left' && ( + + + {t('settings.display.sidebar.title')} + + + + + + + + )} {t('settings.display.custom.css')} diff --git a/src/renderer/src/pages/settings/MCPSettings/InstallNpxUv.tsx b/src/renderer/src/pages/settings/MCPSettings/InstallNpxUv.tsx index ef1bbfad3e..2f7e64bf34 100644 --- a/src/renderer/src/pages/settings/MCPSettings/InstallNpxUv.tsx +++ b/src/renderer/src/pages/settings/MCPSettings/InstallNpxUv.tsx @@ -76,7 +76,6 @@ const InstallNpxUv: FC = ({ mini = false }) => { return (