From ff72c007c03ff47de21a4d0bf52a1ff1fb35cd89 Mon Sep 17 00:00:00 2001 From: beyondkmp Date: Thu, 17 Jul 2025 13:15:29 +0800 Subject: [PATCH 001/173] 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 002/173] 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 003/173] 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 (