From 7bdd96d1bffa676d7351e4e45e86c3f93b762cd6 Mon Sep 17 00:00:00 2001 From: Jonathan Zhang Date: Sun, 20 Sep 2026 07:35:28 +0800 Subject: [PATCH 01/17] docs(readme): refresh news through v1.10.0 --- README.en.md | 13 +++++++++---- README.md | 13 +++++++++---- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/README.en.md b/README.en.md index 6fbe957..b62d231 100644 --- a/README.en.md +++ b/README.en.md @@ -32,23 +32,28 @@ Install agents, MCP tools, and Skills from the Agent Marketplace in one click. - **17 LLM Providers** — DeepSeek, MiniMax, Zhipu, Kimi, Moonshot, OpenAI, Anthropic, Google, OpenRouter, and more - **Custom Agent Portraits** — Add custom character portraits and backgrounds. Agents automatically switch expressions based on conversation context - **Voice Synthesis** — TTS voice synthesis powered by MiniMax API +- **Interject While Generating** — Send messages while the Agent is replying; interjections are queued and handled between steps without interrupting generation - **MCP & Agent Skills** — Configure custom MCP tools and Agent Skills for each Agent, including OAuth-based MCP services (Notion, GitHub) - **Agent Apps** — Install dedicated mini-apps for your Agent, interact via the app panel and receive app notifications - **Remote Access** — Built-in Cloudflare Tunnel for connecting from the mobile app ## 📢 News +- 2026-09-19 — **v1.10.0**: global Inter font and unified type scale, per-turn assistant message bubbles, redesigned provider config panel, edge-fade scroll indicators. +- 2026-09-09 — **v1.9.2**: desktop logs persisted to the user data directory for easier troubleshooting. +- 2026-09-08 — **v1.9.1**: interjections while generating, draggable session sidebar width, project adopted the MIT License. +- 2026-08-29 — **v1.9.0**: first-launch setup wizard with a preinstalled starter Agent, GLM-5.3-Flash and GLM-5.3-Highspeed model support. - 2026-08-24 — **v1.8.0**: official brand icons for providers and models, bilingual voice preview texts, unified marketplace card heights. - 2026-08-19 — **v1.7.1**: automatic retry on transient LLM connection errors, automatic fallback for invalid workspaces, message previews in the session list, GLM-5.3 support. - 2026-08-17 — **v1.7.0**: Agent Apps — a new App tab in the marketplace for one-click install of mini-apps, with an app panel and app notifications. - 2026-07-23 — **v1.6.5**: in-app update checker, macOS code signing and notarization, companion panel rebuilt as a sliding pane, abort generation support. -- 2026-07-19 — **v1.6.4**: Intel Mac support, QR scan-to-pair remote connection, Kimi K3 support, cross-fade pose transitions, redesigned thought-process timeline. -- 2026-07-04 — **v1.6.0**: merged multi-step thinking, persistent API error messages, Git Bash on Windows, one-click uv runtime download, optimistic chat loading. -- 2026-06-28 — **v1.5.0**: new Agent Marketplace for browsing and installing skills & MCP tools; unified HTTP error handling; design system rollout. -- 2026-06-23 — **v1.4.0**: redesigned desktop UI (TitleBar + dual sidebars), agent chat with multi-session management, dedicated skill/tool assignment views.
Earlier news +- 2026-07-19 — **v1.6.4**: Intel Mac support, QR scan-to-pair remote connection, Kimi K3 support, cross-fade pose transitions, redesigned thought-process timeline. +- 2026-07-04 — **v1.6.0**: merged multi-step thinking, persistent API error messages, Git Bash on Windows, one-click uv runtime download, optimistic chat loading. +- 2026-06-28 — **v1.5.0**: new Agent Marketplace for browsing and installing skills & MCP tools; unified HTTP error handling; design system rollout. +- 2026-06-23 — **v1.4.0**: redesigned desktop UI (TitleBar + dual sidebars), agent chat with multi-session management, dedicated skill/tool assignment views. - 2026-06-04 — **v1.2.3**: fixed message leaking and cross-session voice playback on session switch; improved skill path resolution and system prompt editing. - 2026-05-24 — **v1.2.1**: Windows platform support, bilingual (CN/EN) UI. - 2026-05-20 — **v1.2.0**: agent portrait & background management, companion panel animations and window drag-region fixes. diff --git a/README.md b/README.md index 5f3e5cf..258679f 100644 --- a/README.md +++ b/README.md @@ -30,23 +30,28 @@ Persona-agent 是一个开源的个人 AI Agent 聊天平台,让你能够完 - **支持 17 家 LLM 供应商** — DeepSeek、MiniMax、智谱、Kimi、月之暗面、OpenAI、Anthropic、Google、OpenRouter 等多供应商支持 - **自定义 Agent 形象界面** — 支持自定义Agent的角色立绘和背景,Agent 会根据对话自动切换表情 - **自定义 Agent 语音朗读** - 支持Minimax TTS API的语音合成回复 +- **生成中插话** — Agent 回复过程中可直接发送消息,插话进入队列在步骤间隙被响应,无需中断生成 - **支持 MCP 与 Agent Skill** — 支持自定义给每个Agent自定义MCP工具和Agent Skill,同时支持需要OAuth的MCP服务 (Notion, Github) - **Agent App** — 为 Agent 安装专属小应用,通过应用面板交互并接收应用通知 - **远程访问** — 内置 Cloudflare Tunnel,可以通过移动端App远程连接Agent ## 📢 News +- 2026-09-19 — **v1.10.0**:全局 Inter 字体与统一字号体系、对话按轮次气泡重组、供应商配置面板重设计、边缘渐隐滚动提示。 +- 2026-09-09 — **v1.9.2**:桌面端运行日志持久化到用户数据目录,问题排查有据可查。 +- 2026-09-08 — **v1.9.1**:生成中插话、会话侧边栏宽度拖拽调整、项目采用 MIT License 开源。 +- 2026-08-29 — **v1.9.0**:首次启动设置向导与预置初始 Agent、GLM-5.3-Flash 与 GLM-5.3-Highspeed 模型支持。 - 2026-08-24 — **v1.8.0**:供应商与模型官方品牌图标、语音试听文案中英双语、商城卡片高度统一。 - 2026-08-19 — **v1.7.1**:LLM 瞬时断连自动重试、无效工作空间自动回退、会话列表消息预览、GLM-5.3 支持。 - 2026-08-17 — **v1.7.0**:Agent App 体系上线 — 商城新增 App 标签页,一键安装专属小应用,支持应用面板交互与通知。 - 2026-07-23 — **v1.6.5**:应用内检查更新、macOS 安装包签名与公证、陪伴面板重构为滑出式、支持中止生成。 -- 2026-07-19 — **v1.6.4**:Intel Mac 支持、扫码配对远程连接、Kimi K3 支持、立绘切换过渡动画、思考过程时间线重设计。 -- 2026-07-04 — **v1.6.0**:思考过程合并展示、API 错误提示随会话持久化、Windows 端改用 Git Bash、uv 运行时一键下载、聊天乐观加载。 -- 2026-06-28 — **v1.5.0**:新增 Agent 市场,应用内集中浏览和安装技能与 MCP 工具;统一 HTTP 错误处理;设计系统落地。 -- 2026-06-23 — **v1.4.0**:全新桌面端 UI(TitleBar + 双侧边栏)、Agent 聊天与多会话管理、技能/工具独立分配视图。
Earlier news +- 2026-07-19 — **v1.6.4**:Intel Mac 支持、扫码配对远程连接、Kimi K3 支持、立绘切换过渡动画、思考过程时间线重设计。 +- 2026-07-04 — **v1.6.0**:思考过程合并展示、API 错误提示随会话持久化、Windows 端改用 Git Bash、uv 运行时一键下载、聊天乐观加载。 +- 2026-06-28 — **v1.5.0**:新增 Agent 市场,应用内集中浏览和安装技能与 MCP 工具;统一 HTTP 错误处理;设计系统落地。 +- 2026-06-23 — **v1.4.0**:全新桌面端 UI(TitleBar + 双侧边栏)、Agent 聊天与多会话管理、技能/工具独立分配视图。 - 2026-06-04 — **v1.2.3**:修复会话切换时消息泄漏与跨会话语音播放,改进 Skill 路径解析与系统提示词编辑体验。 - 2026-05-24 — **v1.2.1**:Windows 平台支持、中英文双语界面。 - 2026-05-20 — **v1.2.0**:Agent 立绘与背景图管理、陪伴面板动画与窗口拖拽区域全面修复。 From a251f514793f354e35f98e6cc2183b3d4fdd3ef8 Mon Sep 17 00:00:00 2001 From: Jonathan Zhang Date: Wed, 23 Sep 2026 10:26:16 +0800 Subject: [PATCH 02/17] style: sync typography and spacing with design demo across shared screens --- .../desktop/src/renderer/components/Toast.tsx | 4 +-- .../components/agent-editor/AgentEditor.tsx | 2 +- .../agent-editor/PoseImageCardList.tsx | 6 ++-- .../components/agent-editor/VoiceCard.tsx | 4 +-- .../renderer/components/chat/MessageList.tsx | 6 ++-- .../components/common/ModelSelector.tsx | 4 +-- .../components/companion/CompanionContent.tsx | 2 +- .../components/landing/LandingWizard.tsx | 12 +++---- .../marketplace/MarketplaceCard.tsx | 4 +-- .../src/renderer/components/shell/Header.tsx | 34 +++++++++++-------- .../components/shell/SessionSidebar.tsx | 16 +++++---- .../renderer/components/skills/SkillsView.tsx | 2 +- .../components/tools/AgentToolsView.tsx | 2 +- .../src/renderer/components/ui/Card.tsx | 4 +++ 14 files changed, 57 insertions(+), 45 deletions(-) diff --git a/packages/desktop/src/renderer/components/Toast.tsx b/packages/desktop/src/renderer/components/Toast.tsx index 8e54c76..3080357 100644 --- a/packages/desktop/src/renderer/components/Toast.tsx +++ b/packages/desktop/src/renderer/components/Toast.tsx @@ -40,10 +40,10 @@ const ToastItem: React.FC<{ )} > {iconMap[type]} - {message} + {message} diff --git a/packages/desktop/src/renderer/components/agent-editor/AgentEditor.tsx b/packages/desktop/src/renderer/components/agent-editor/AgentEditor.tsx index 2817fff..e96e02b 100644 --- a/packages/desktop/src/renderer/components/agent-editor/AgentEditor.tsx +++ b/packages/desktop/src/renderer/components/agent-editor/AgentEditor.tsx @@ -414,7 +414,7 @@ export const AgentEditor: React.FC = ({
-

+

{editingAgentId ? t('agentEditor.editAgent') : t('agentEditor.addAgent')} diff --git a/packages/desktop/src/renderer/components/agent-editor/PoseImageCardList.tsx b/packages/desktop/src/renderer/components/agent-editor/PoseImageCardList.tsx index 3e18585..f289a04 100644 --- a/packages/desktop/src/renderer/components/agent-editor/PoseImageCardList.tsx +++ b/packages/desktop/src/renderer/components/agent-editor/PoseImageCardList.tsx @@ -92,18 +92,18 @@ export const PoseImageCardList: React.FC = ({ }} onClick={(e) => e.stopPropagation()} onBlur={confirmRename} - className="text-micro text-white bg-background/20 rounded px-1 py-0.5 leading-tight w-full outline-none border border-white/30" + className="text-micro text-white bg-background/20 rounded px-1 py-0.5 w-full outline-none border border-white/30" /> ) : (
handleStartRename(idx)} > {img.name}
{img.name === 'default' ? ( - + {t('agentEditor.defaultPose')} ) : ( diff --git a/packages/desktop/src/renderer/components/agent-editor/VoiceCard.tsx b/packages/desktop/src/renderer/components/agent-editor/VoiceCard.tsx index 39a772c..cafae1f 100644 --- a/packages/desktop/src/renderer/components/agent-editor/VoiceCard.tsx +++ b/packages/desktop/src/renderer/components/agent-editor/VoiceCard.tsx @@ -69,7 +69,7 @@ export const VoiceCard: React.FC = ({ {voices.filter((v) => v.group === 'cloned').length > 0 && ( - + {t('agentEditor.clonedVoices')} {voices @@ -83,7 +83,7 @@ export const VoiceCard: React.FC = ({ )} - + {t('agentEditor.presetVoices')} {voices diff --git a/packages/desktop/src/renderer/components/chat/MessageList.tsx b/packages/desktop/src/renderer/components/chat/MessageList.tsx index acfeda9..d5d8cb3 100644 --- a/packages/desktop/src/renderer/components/chat/MessageList.tsx +++ b/packages/desktop/src/renderer/components/chat/MessageList.tsx @@ -74,7 +74,7 @@ const MessageItem: React.FC = ({ message, agent }) => { > {/* 头像 */} {isUser ? ( -
+
U
) : ( @@ -126,7 +126,7 @@ const MessageItem: React.FC = ({ message, agent }) => { {hasContent && ( toast.success(t('messageList.copiedToClipboard'))} onError={() => toast.error(t('messageList.failedToCopy'))} @@ -242,7 +242,7 @@ export const MessageList = React.forwardRef( if (messages.length === 0 && !isLoading) { return ( -
+

{hasAgent ? t('messageList.startConversation') diff --git a/packages/desktop/src/renderer/components/common/ModelSelector.tsx b/packages/desktop/src/renderer/components/common/ModelSelector.tsx index 1ca8eb3..109dfec 100644 --- a/packages/desktop/src/renderer/components/common/ModelSelector.tsx +++ b/packages/desktop/src/renderer/components/common/ModelSelector.tsx @@ -103,7 +103,7 @@ export const ModelSelector: React.FC = ({ if (filteredProviders.length === 0) { return (

{t('model.noModels')}
@@ -114,7 +114,7 @@ export const ModelSelector: React.FC = ({ const triggerClassName = compact ? 'h-8 w-auto max-w-[140px] border-0 bg-transparent hover:bg-muted/50 px-2.5 text-caption text-muted-foreground/60 hover:text-muted-foreground shadow-none focus:ring-0 focus:ring-offset-0 [&>svg]:hidden' - : 'w-full px-3 py-2 border border-gray-200 rounded-md text-content hover:bg-gray-50 focus:ring-blue-500'; + : 'w-full px-3 py-2 border border-border rounded-md text-body focus:ring-blue-500'; return (
diff --git a/packages/desktop/src/renderer/components/companion/CompanionContent.tsx b/packages/desktop/src/renderer/components/companion/CompanionContent.tsx index bedb5d6..521aff7 100644 --- a/packages/desktop/src/renderer/components/companion/CompanionContent.tsx +++ b/packages/desktop/src/renderer/components/companion/CompanionContent.tsx @@ -83,7 +83,7 @@ export function CompanionContent({ agentId }: CompanionContentProps) { >
-

+

{t('companion.noAppearance')}

diff --git a/packages/desktop/src/renderer/components/landing/LandingWizard.tsx b/packages/desktop/src/renderer/components/landing/LandingWizard.tsx index 15dd6ee..4e75978 100644 --- a/packages/desktop/src/renderer/components/landing/LandingWizard.tsx +++ b/packages/desktop/src/renderer/components/landing/LandingWizard.tsx @@ -291,7 +291,7 @@ export function LandingWizard({ agentId, onComplete }: LandingWizardProps) { > - + {p.name} {p.hasAuth ? ( @@ -373,7 +373,7 @@ export function LandingWizard({ agentId, onComplete }: LandingWizardProps) {

)} {KEY_URLS[provider.id] && ( -

+

{t('landing.keyHelp', { url: KEY_URLS[provider.id], })} @@ -449,7 +449,7 @@ export function LandingWizard({ agentId, onComplete }: LandingWizardProps) { {t('landing.next')}

-

+

{t('landing.p1Hint')}

@@ -550,7 +550,7 @@ export function LandingWizard({ agentId, onComplete }: LandingWizardProps) { {voices.filter((v) => v.group === 'cloned').length > 0 && ( - + {t('agentEditor.clonedVoices')} {voices @@ -563,7 +563,7 @@ export function LandingWizard({ agentId, onComplete }: LandingWizardProps) { )} - + {t('agentEditor.presetVoices')} {voices @@ -686,7 +686,7 @@ export function LandingWizard({ agentId, onComplete }: LandingWizardProps) { - + {t(`landing.${id}ConceptTitle`)} diff --git a/packages/desktop/src/renderer/components/marketplace/MarketplaceCard.tsx b/packages/desktop/src/renderer/components/marketplace/MarketplaceCard.tsx index 1a3e474..e0b0efd 100644 --- a/packages/desktop/src/renderer/components/marketplace/MarketplaceCard.tsx +++ b/packages/desktop/src/renderer/components/marketplace/MarketplaceCard.tsx @@ -67,14 +67,14 @@ export const MarketplaceCard: React.FC = ({
{item.name}
-
+
@{item.author}
{/* 两行简介(超出截断) */} -

+

{item.description}

diff --git a/packages/desktop/src/renderer/components/shell/Header.tsx b/packages/desktop/src/renderer/components/shell/Header.tsx index 2b3ea98..a410a86 100644 --- a/packages/desktop/src/renderer/components/shell/Header.tsx +++ b/packages/desktop/src/renderer/components/shell/Header.tsx @@ -12,6 +12,8 @@ import { useAgentStore } from '../../stores/agentStore'; import { useCompanionStore } from '../../stores/companionStore'; import { useVoiceStore } from '../../stores/voiceStore'; import { toast } from '../../stores/toastStore'; +import { Button } from '../ui/Button'; +import { cn } from '../../lib/utils'; interface HeaderProps { /** 当前会话是否正在生成,控制标题旁的运行指示 */ @@ -52,7 +54,7 @@ export const Header: React.FC = ({ isLoading }) => { return (
-

+

{currentSession?.title || t('header.newChat')}

{isLoading && ( @@ -70,16 +72,17 @@ export const Header: React.FC = ({ isLoading }) => {
{currentAgent && ( <> - - + )}
diff --git a/packages/desktop/src/renderer/components/shell/SessionSidebar.tsx b/packages/desktop/src/renderer/components/shell/SessionSidebar.tsx index d510712..4196784 100644 --- a/packages/desktop/src/renderer/components/shell/SessionSidebar.tsx +++ b/packages/desktop/src/renderer/components/shell/SessionSidebar.tsx @@ -45,8 +45,10 @@ const NavItem: React.FC<{ {active && ( )} - - {label} + + + {label} + ); @@ -139,7 +141,7 @@ export const SessionSidebar: React.FC = ({
)}
-
+
{currentAgent?.name || t('common.noAgentSelected')}
@@ -168,9 +170,9 @@ export const SessionSidebar: React.FC = ({ activeNav === 'chat' && ( )} - +
- + {t('sessionSidebar.chat')} @@ -205,8 +207,8 @@ export const SessionSidebar: React.FC = ({
{/* 「会话」标题行,右侧 Plus 按钮进入新对话草稿态 */}
- - + + {t('sessionSidebar.sessions')} + {index > 0 && ( +
+ )} + +
))}
diff --git a/packages/desktop/src/renderer/i18n/locales/en.json b/packages/desktop/src/renderer/i18n/locales/en.json index 263841d..18cdb4c 100644 --- a/packages/desktop/src/renderer/i18n/locales/en.json +++ b/packages/desktop/src/renderer/i18n/locales/en.json @@ -67,10 +67,15 @@ "settings.title": "Settings", "settings.loadError": "Failed to load configuration", "settings.tabs.general": "General", + "settings.tabs.generalDesc": "App preferences and general configuration", "settings.tabs.providers": "Model Providers", + "settings.tabs.providersDesc": "Manage API keys and default models", "settings.tabs.voice": "Voice Service", + "settings.tabs.voiceDesc": "Speech synthesis and voice configuration", "settings.tabs.mcp": "MCP Services", + "settings.tabs.mcpDesc": "MCP services and capability management", "settings.tabs.skills": "Skills", + "settings.tabs.skillsDesc": "Browse and uninstall installed skills", "config.basic": "Basic", "config.language": "Language", diff --git a/packages/desktop/src/renderer/i18n/locales/zh-CN.json b/packages/desktop/src/renderer/i18n/locales/zh-CN.json index 694fa19..c57ddce 100644 --- a/packages/desktop/src/renderer/i18n/locales/zh-CN.json +++ b/packages/desktop/src/renderer/i18n/locales/zh-CN.json @@ -67,10 +67,15 @@ "settings.title": "设置中心", "settings.loadError": "加载配置失败", "settings.tabs.general": "通用设置", + "settings.tabs.generalDesc": "应用偏好与基础配置", "settings.tabs.providers": "模型供应商", + "settings.tabs.providersDesc": "管理 API Key 与默认模型", "settings.tabs.voice": "语音服务", + "settings.tabs.voiceDesc": "语音合成与音色配置", "settings.tabs.mcp": "工具", + "settings.tabs.mcpDesc": "MCP 服务与功能管理", "settings.tabs.skills": "技能", + "settings.tabs.skillsDesc": "技能查看与卸载", "config.basic": "基本", "config.language": "语言", diff --git a/packages/desktop/src/renderer/stores/viewStore.ts b/packages/desktop/src/renderer/stores/viewStore.ts index c1a73e7..4672b8f 100644 --- a/packages/desktop/src/renderer/stores/viewStore.ts +++ b/packages/desktop/src/renderer/stores/viewStore.ts @@ -11,10 +11,18 @@ type ViewType = 'chat' | 'settings' | 'marketplace'; /** MainView 内部导航,在 chat 视图下按 activeNav 切换右侧内容区 */ type MainNav = 'chat' | 'agent-settings' | 'tools' | 'skills'; +/** 设置页左侧导航 tab */ +export type SettingsTab = 'general' | 'providers' | 'voice' | 'mcp' | 'skills'; + interface ViewStore { currentView: ViewType; editingAgentId: string | null; activeNav: MainNav; + /** + * 设置页当前 tab,离开设置页再回来时保持在原 tab。 + * 不进 partialize,跨应用重启回落 general。 + */ + settingsTab: SettingsTab; sessionSidebarCollapsed: boolean; /** * 会话侧边栏宽度(占 Group 的百分比,15–30)。 @@ -24,6 +32,7 @@ interface ViewStore { setView: (view: ViewType) => void; setActiveNav: (nav: MainNav) => void; + setSettingsTab: (tab: SettingsTab) => void; toggleSessionSidebar: () => void; setSessionSidebarWidth: (width: number) => void; openAgentEditor: (agentId: string | null) => void; @@ -36,11 +45,13 @@ export const useViewStore = create()( currentView: 'chat', editingAgentId: null, activeNav: 'chat', + settingsTab: 'general', sessionSidebarCollapsed: false, sessionSidebarWidth: 20, setView: (view) => set({ currentView: view }), setActiveNav: (nav) => set({ activeNav: nav }), + setSettingsTab: (tab) => set({ settingsTab: tab }), toggleSessionSidebar: () => set((s) => ({ sessionSidebarCollapsed: !s.sessionSidebarCollapsed })), setSessionSidebarWidth: (width) => set({ sessionSidebarWidth: width }), From 4e72dbbf971ea8a5ccf4ca1edc018935d6cf8b4d Mon Sep 17 00:00:00 2001 From: Jonathan Zhang Date: Wed, 23 Sep 2026 10:27:05 +0800 Subject: [PATCH 04/17] feat: show storage paths and fix language matching in general settings --- .../components/settings/ConfigForm.tsx | 118 ++++++++++-------- .../desktop/src/renderer/i18n/locales/en.json | 2 + .../src/renderer/i18n/locales/zh-CN.json | 2 + packages/desktop/src/renderer/lib/platform.ts | 14 +++ 4 files changed, 83 insertions(+), 53 deletions(-) diff --git a/packages/desktop/src/renderer/components/settings/ConfigForm.tsx b/packages/desktop/src/renderer/components/settings/ConfigForm.tsx index 9738f1c..d9c1f4c 100644 --- a/packages/desktop/src/renderer/components/settings/ConfigForm.tsx +++ b/packages/desktop/src/renderer/components/settings/ConfigForm.tsx @@ -10,16 +10,18 @@ import { useTranslation } from 'react-i18next'; import { useConfigStore } from '../../stores/configStore'; import { Switch } from '../ui/Switch'; import { Card } from '../ui/Card'; -import { SettingRow, SettingDivider } from '../common/SettingRow'; +import { SettingRow } from '../common/SettingRow'; import { EnvironmentCard } from './EnvironmentCard'; import { VersionUpdateCard } from './VersionUpdateCard'; -import { dataPath } from '../../lib/platform'; +import { configFilePath, dataPath, dataRootPath } from '../../lib/platform'; const STORAGE_PATHS = [ - { labelKey: 'config.agentDir', dir: 'agents' }, - { labelKey: 'config.skillDir', dir: 'skills' }, - { labelKey: 'config.mcpDir', dir: 'mcp' }, - { labelKey: 'config.logDir', dir: 'logs' }, + { labelKey: 'config.dataDir', path: dataRootPath() }, + { labelKey: 'config.configFile', path: configFilePath() }, + { labelKey: 'config.agentDir', path: dataPath('agents') }, + { labelKey: 'config.skillDir', path: dataPath('skills') }, + { labelKey: 'config.mcpDir', path: dataPath('mcp') }, + { labelKey: 'config.logDir', path: dataPath('logs') }, ] as const; function PathRow({ label, path }: { label: string; path: string }) { @@ -64,55 +66,65 @@ export const ConfigForm: React.FC = () => { return (
- - -
- - -
-
- - - - handleToggle('enableLogging', checked) - } - /> - + +
+ +
+ + +
+
+ + + handleToggle('enableLogging', checked) + } + /> + +
- - {STORAGE_PATHS.map((item, i) => ( - - {i > 0 && } - - - ))} + +
+ {STORAGE_PATHS.map((item) => ( + + ))} +
diff --git a/packages/desktop/src/renderer/i18n/locales/en.json b/packages/desktop/src/renderer/i18n/locales/en.json index 18cdb4c..c07cbe1 100644 --- a/packages/desktop/src/renderer/i18n/locales/en.json +++ b/packages/desktop/src/renderer/i18n/locales/en.json @@ -83,6 +83,8 @@ "config.enableLogging": "Enable Logging", "config.enableLoggingDesc": "Record runtime logs to local files", "config.storagePaths": "Storage Paths", + "config.dataDir": "Data Directory", + "config.configFile": "Config File", "config.agentDir": "Agents Directory", "config.skillDir": "Skills Directory", "config.mcpDir": "MCP Config", diff --git a/packages/desktop/src/renderer/i18n/locales/zh-CN.json b/packages/desktop/src/renderer/i18n/locales/zh-CN.json index c57ddce..75d09dd 100644 --- a/packages/desktop/src/renderer/i18n/locales/zh-CN.json +++ b/packages/desktop/src/renderer/i18n/locales/zh-CN.json @@ -83,6 +83,8 @@ "config.enableLogging": "启用日志", "config.enableLoggingDesc": "将运行日志记录到本地文件", "config.storagePaths": "存储路径", + "config.dataDir": "数据目录", + "config.configFile": "配置文件", "config.agentDir": "智能体目录", "config.skillDir": "技能目录", "config.mcpDir": "工具配置", diff --git a/packages/desktop/src/renderer/lib/platform.ts b/packages/desktop/src/renderer/lib/platform.ts index 0afb065..e7b6808 100644 --- a/packages/desktop/src/renderer/lib/platform.ts +++ b/packages/desktop/src/renderer/lib/platform.ts @@ -15,3 +15,17 @@ export const isLinux = platform === 'linux'; export function dataPath(sub: string): string { return `~/.local/share/persona-agent/${sub}/`; } + +/** + * 数据根目录路径。dataPath 固定追加尾斜杠,空子目录会产生双斜杠,故单独提供。 + */ +export function dataRootPath(): string { + return '~/.local/share/persona-agent/'; +} + +/** + * config.yaml 配置文件路径。dataPath 会给文件路径追加多余尾斜杠,故单独拼接。 + */ +export function configFilePath(): string { + return '~/.local/share/persona-agent/config/config.yaml'; +} From a0e301ab52d6709799847a685ba3b05970042b1a Mon Sep 17 00:00:00 2001 From: Jonathan Zhang Date: Wed, 23 Sep 2026 10:27:17 +0800 Subject: [PATCH 05/17] feat: add cloned voice renaming and unify api key card --- .../components/settings/ApiKeyCard.tsx | 135 ++++++++ .../components/settings/VoiceConfigPanel.tsx | 316 +++++++++++------- .../desktop/src/renderer/i18n/locales/en.json | 2 +- .../src/renderer/i18n/locales/zh-CN.json | 2 +- packages/desktop/src/renderer/lib/api.ts | 24 ++ packages/server/src/server/routers/voice.ts | 22 ++ packages/server/src/tts/voices.ts | 14 + 7 files changed, 394 insertions(+), 121 deletions(-) create mode 100644 packages/desktop/src/renderer/components/settings/ApiKeyCard.tsx diff --git a/packages/desktop/src/renderer/components/settings/ApiKeyCard.tsx b/packages/desktop/src/renderer/components/settings/ApiKeyCard.tsx new file mode 100644 index 0000000..f4c7d0e --- /dev/null +++ b/packages/desktop/src/renderer/components/settings/ApiKeyCard.tsx @@ -0,0 +1,135 @@ +/** + * @file src/renderer/components/settings/ApiKeyCard.tsx + * @description API Key 管理组件,品牌头部、文档外链、密钥输入与验证按钮 + * 语音服务与模型供应商详情共用,验证交互与状态由调用方注入 + */ + +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { CheckCircle2, ExternalLink, Loader2 } from 'lucide-react'; +import { Button } from '../ui/Button'; +import { PasswordInput } from '../ui/PasswordInput'; +import { ProviderMark } from '../common/ProviderMark'; + +interface ApiKeyCardProps { + providerId: string; + providerName: string; + desc: string; + docsUrl?: string; + apiKey: string; + onApiKeyChange: (value: string) => void; + placeholder: string; + /** 验证按钮文案,已本地化 */ + verifyLabel: string; + /** 验证中按钮文案,传入则显示文字,不传则显示转圈图标 */ + verifyingLabel?: string; + verifying: boolean; + /** 已配置标记,由调用方的服务端配置状态驱动 */ + verified: boolean; + onVerify: () => void; + /** 输入行下方的内联反馈节点,承接验证成功或失败的提示 */ + feedback?: React.ReactNode; + /** 外层卡片类,语音服务传白卡含内边距,供应商详情不传 */ + className?: string; +} + +/** + * API Key 管理组件:品牌头部 + 密钥输入行 + 外置验证按钮。 + * - 文档外链在 docsUrl 缺省时不渲染 + * - 密钥不回显由调用方保证,组件只展示传入的输入值 + * @param providerId - 供应商 id,用于查图标注册表 + * @param providerName - 供应商显示名与品牌标题 + * @param desc - 品牌描述行 + * @param docsUrl - 可选的官方文档外链 + * @param apiKey - 密钥输入值 + * @param onApiKeyChange - 密钥输入回调 + * @param placeholder - 输入框占位文案 + * @param verifyLabel - 验证按钮文案 + * @param verifyingLabel - 可选的验证中文案 + * @param verifying - 验证中状态 + * @param verified - 已配置状态,驱动绿勾标记 + * @param onVerify - 验证触发回调 + * @param feedback - 可选的内联反馈节点 + * @param className - 追加到容器根节点的样式类 + */ +export const ApiKeyCard: React.FC = ({ + providerId, + providerName, + desc, + docsUrl, + apiKey, + onApiKeyChange, + placeholder, + verifyLabel, + verifyingLabel, + verifying, + verified, + onVerify, + feedback, + className, +}) => { + const { t } = useTranslation(); + return ( +
+
+ +
+

+ {providerName} +

+

{desc}

+
+ {docsUrl && ( + + )} +
+ +
+
+ + API Key + + {verified && ( + + + {t('provider.configured')} + + )} +
+
+ onApiKeyChange(e.target.value)} + placeholder={placeholder} + className="flex-1" + /> + +
+ {feedback} +
+
+ ); +}; diff --git a/packages/desktop/src/renderer/components/settings/VoiceConfigPanel.tsx b/packages/desktop/src/renderer/components/settings/VoiceConfigPanel.tsx index 75651ac..86128f4 100644 --- a/packages/desktop/src/renderer/components/settings/VoiceConfigPanel.tsx +++ b/packages/desktop/src/renderer/components/settings/VoiceConfigPanel.tsx @@ -3,16 +3,19 @@ * @description 语音服务配置面板,管理 MiniMax TTS API Key、模型选择、语音摘要阈值和克隆音色 * 设置页面是独立 Electron 窗口,Toast 不可见,因此使用内联 UI 反馈 * 所有配置通过服务端 API 读写,不依赖本地 store + * 密钥不回显,已配置状态由服务端配置驱动,验证通过即清空输入框 */ import React, { useEffect, useState, useRef } from 'react'; import { + Check, CheckCircle, XCircle, Volume2, Trash2, Upload, Plus, + Pencil, X, Loader2, } from 'lucide-react'; @@ -25,13 +28,15 @@ import { getVoices, cloneVoice, deleteClonedVoice, + renameClonedVoice, type TtsModel, type VoiceOption, } from '../../lib/api'; import { synthesize } from '../../lib/tts'; -import { SettingRow, SettingDivider } from '../common/SettingRow'; +import { SettingRow } from '../common/SettingRow'; import { Card } from '../ui/Card'; -import { PasswordInput } from '../ui/PasswordInput'; +import { ApiKeyCard } from './ApiKeyCard'; +import { Button } from '../ui/Button'; import { Select, SelectTrigger, @@ -46,6 +51,8 @@ import { logger } from '../../lib/logger'; const VERIFY_TEXT = '测试语音功能连接'; +const MINIMAX_DOCS_URL = 'https://platform.minimaxi.com/docs/guides/quickstart'; + const ALLOWED_AUDIO_TYPES = new Set([ 'audio/mpeg', 'audio/mp4', @@ -93,6 +100,7 @@ function generateVoiceId(): string { export const VoiceConfigPanel: React.FC = () => { const { t } = useTranslation(); const [inputKey, setInputKey] = useState(''); + const [keyConfigured, setKeyConfigured] = useState(false); const [verifying, setVerifying] = useState(false); const [feedback, setFeedback] = useState(null); @@ -111,6 +119,9 @@ export const VoiceConfigPanel: React.FC = () => { const [cloneName, setCloneName] = useState(''); const [cloning, setCloning] = useState(false); + const [renamingId, setRenamingId] = useState(null); + const [renameValue, setRenameValue] = useState(''); + const fileInputRef = useRef(null); const MIN_THRESHOLD = 0; @@ -127,7 +138,7 @@ export const VoiceConfigPanel: React.FC = () => { getTtsModels(), getVoices(), ]); - setInputKey(config.apiKey); + setKeyConfigured(Boolean(config.apiKey)); setSelectedModel(config.model); setThresholdInput(String(config.summaryThreshold)); setModels(modelsData); @@ -140,7 +151,7 @@ export const VoiceConfigPanel: React.FC = () => { /** * 验证并保存 API Key: * - 用输入的 Key 调一次 synthesize 合成测试文本 - * - 验证通过:保存 Key 到服务端 + 显示成功提示 + * - 验证通过:保存 Key 到服务端 + 清空输入框 + 显示成功提示 * - 验证失败:显示错误提示,不保存 */ const handleSaveKey = async () => { @@ -160,6 +171,8 @@ export const VoiceConfigPanel: React.FC = () => { selectedModel || 'speech-2.8-hd' ); await updateTtsConfig({ apiKey: key }); + setInputKey(''); + setKeyConfigured(true); setFeedback({ type: 'success', message: t('voice.apiKeyVerified') }); } catch (err) { const message = @@ -220,6 +233,23 @@ export const VoiceConfigPanel: React.FC = () => { } }; + /** 行内重命名克隆音色,只改服务端本地配置不调 MiniMax 接口 */ + const handleRename = async (voiceId: string) => { + const name = renameValue.trim(); + if (!name) return; + try { + await renameClonedVoice(voiceId, name); + setClonedVoices((prev) => + prev.map((v) => (v.id === voiceId ? { ...v, name } : v)) + ); + setRenamingId(null); + } catch (err) { + const message = + err instanceof Error ? err.message : t('common.saveFailed'); + toast.error(message); + } + }; + const handleFileSelect = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (file) { @@ -295,99 +325,103 @@ export const VoiceConfigPanel: React.FC = () => { return (
- {/* API Key 配置 */} - - -
- setInputKey(e.target.value)} - placeholder={t('voice.enterMinimaxApiKey')} - /> - -
-
- - {feedback && ( -

- {feedback.type === 'success' ? ( - - ) : ( - - )} - {feedback.message} -

- )} -
+ {feedback.type === 'success' ? ( + + ) : ( + + )} + {feedback.message} +

+ ) + } + className="rounded-xl border border-border bg-background px-4 py-4" + /> {/* 语音参数 */} - - - + + + + + {models.map((m) => ( + + {m.name} + + ))} + + + + - - - - - {models.map((m) => ( - - {m.name} - - ))} - - - - - -
- handleThresholdChange(e.target.value)} - onBlur={handleThresholdBlur} - disabled={savingThreshold} - className="w-20 h-8 px-3 text-body text-right border border-input rounded-lg focus:outline-none focus:ring-1 focus:ring-muted-foreground" - /> - - {t('voice.characters')} - -
-
+
+ handleThresholdChange(e.target.value)} + onBlur={handleThresholdBlur} + disabled={savingThreshold} + className="w-20 h-8 px-3 text-body text-right border border-input rounded-lg focus:outline-none focus:ring-1 focus:ring-muted-foreground" + /> + + {t('voice.characters')} + +
+ +
{/* 克隆音色管理 */} setShowCloneForm(true)} - className="h-8 px-3 text-body rounded-lg border border-input text-muted-foreground hover:text-foreground hover:border-muted-foreground transition-colors flex items-center gap-1" + className="rounded-lg border-border h-8 text-body px-3" > - + {t('voice.cloneNew')} - + ) } > {showCloneForm && ( -
+
{t('voice.uploadClone')} @@ -419,7 +453,7 @@ export const VoiceConfigPanel: React.FC = () => { {t('voice.audioFile')}
-
- - +
)} {clonedVoices.length === 0 ? ( -
+
{t('voice.noClonedVoices')}
) : ( @@ -473,40 +508,83 @@ export const VoiceConfigPanel: React.FC = () => { {clonedVoices.map((v) => (
-
-
- {v.name} + {renamingId === v.id ? ( +
+ setRenameValue(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') void handleRename(v.id); + if (e.key === 'Escape') setRenamingId(null); + }} + className="rounded-lg border border-input h-7 px-2 text-body flex-1 focus:outline-none focus:ring-1 focus:ring-muted-foreground" + /> + +
-
- -
- - -
+ ) : ( +
+
+ {v.name} +
+
+ )} + + {renamingId !== v.id && ( +
+ + + +
+ )}
))}
diff --git a/packages/desktop/src/renderer/i18n/locales/en.json b/packages/desktop/src/renderer/i18n/locales/en.json index c07cbe1..441e42c 100644 --- a/packages/desktop/src/renderer/i18n/locales/en.json +++ b/packages/desktop/src/renderer/i18n/locales/en.json @@ -7,6 +7,7 @@ "common.saveSuccess": "Saved successfully", "common.saveFailed": "Save failed", "common.delete": "Delete", + "common.rename": "Rename", "common.deleteFailed": "Delete failed", "common.cancel": "Cancel", "common.save": "Save", @@ -147,7 +148,6 @@ "voice.cannotReadAudioDuration": "Cannot read audio duration, please check the file", "voice.cloneSuccess": "Voice cloned successfully", "voice.cloneFailed": "Clone failed", - "voice.minimaxTitle": "MiniMax (Text-to-Speech)", "voice.minimaxDesc": "Configure MiniMax API Key to enable voice broadcast", "voice.enterMinimaxApiKey": "Enter MiniMax API Key", "voice.verifying": "Verifying...", diff --git a/packages/desktop/src/renderer/i18n/locales/zh-CN.json b/packages/desktop/src/renderer/i18n/locales/zh-CN.json index 75d09dd..c5b61a0 100644 --- a/packages/desktop/src/renderer/i18n/locales/zh-CN.json +++ b/packages/desktop/src/renderer/i18n/locales/zh-CN.json @@ -7,6 +7,7 @@ "common.saveSuccess": "保存成功", "common.saveFailed": "保存失败", "common.delete": "删除", + "common.rename": "重命名", "common.deleteFailed": "删除失败", "common.cancel": "取消", "common.save": "保存", @@ -147,7 +148,6 @@ "voice.cannotReadAudioDuration": "无法读取音频时长,请检查文件", "voice.cloneSuccess": "音色克隆成功", "voice.cloneFailed": "克隆失败", - "voice.minimaxTitle": "MiniMax(语音合成)", "voice.minimaxDesc": "配置 MiniMax API Key 以启用语音播报功能", "voice.enterMinimaxApiKey": "输入 MiniMax API Key", "voice.verifying": "验证中...", diff --git a/packages/desktop/src/renderer/lib/api.ts b/packages/desktop/src/renderer/lib/api.ts index 9a6bec1..84de7d6 100644 --- a/packages/desktop/src/renderer/lib/api.ts +++ b/packages/desktop/src/renderer/lib/api.ts @@ -1463,3 +1463,27 @@ export async function deleteClonedVoice(voiceId: string): Promise { ); } } + +/** 重命名克隆音色,只改服务端本地配置不调 MiniMax 接口 */ +export async function renameClonedVoice( + voiceId: string, + name: string +): Promise { + const baseUrl = await getBaseUrl(); + const response = await fetch( + `${baseUrl}/api/voices/clone/${encodeURIComponent(voiceId)}`, + { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name }), + } + ); + + if (!response.ok) { + const data = await response.json().catch(() => ({})); + throw new Error( + (data as { error?: string }).error || + `Failed to rename cloned voice: ${response.status}` + ); + } +} diff --git a/packages/server/src/server/routers/voice.ts b/packages/server/src/server/routers/voice.ts index 6a4e602..acbf33e 100644 --- a/packages/server/src/server/routers/voice.ts +++ b/packages/server/src/server/routers/voice.ts @@ -4,6 +4,7 @@ * Routes: * - GET /api/voices - Get all voices (cloned first, then preset) * - POST /api/voices/clone - Clone a new voice (upload + clone + verify) + * - PUT /api/voices/clone/:voiceId - Rename a cloned voice in local config * - DELETE /api/voices/clone/:voiceId - Delete a cloned voice */ @@ -13,6 +14,7 @@ import { getAllVoices, addClonedVoice, removeClonedVoice, + renameClonedVoice, } from '../../tts/voices.js'; import { asyncHandler, getParam } from './utils.js'; import { Logger } from '../../util/logger.js'; @@ -90,6 +92,26 @@ export function createVoiceRouter(): Router { }) ); + router.put( + '/clone/:voiceId', + asyncHandler('VOICE', 'Voice rename failed', (req, res) => { + const voiceId = getParam(req.params['voiceId']); + if (!voiceId) throw new AppError(400, 'voiceId is required'); + + const name = + typeof req.body['name'] === 'string' ? req.body['name'].trim() : ''; + if (!name) throw new AppError(400, 'name is required'); + + const renamed = renameClonedVoice(voiceId, name); + if (!renamed) { + throw new AppError(404, `Cloned voice not found: ${voiceId}`); + } + + Logger.log('VOICE', `Renamed voice: ${voiceId}`); + res.json({ success: true }); + }) + ); + router.delete( '/clone/:voiceId', asyncHandler('VOICE', 'Voice delete failed', async (req, res) => { diff --git a/packages/server/src/tts/voices.ts b/packages/server/src/tts/voices.ts index 0c41565..81fd17d 100644 --- a/packages/server/src/tts/voices.ts +++ b/packages/server/src/tts/voices.ts @@ -53,3 +53,17 @@ export async function removeClonedVoice(voiceId: string): Promise { ); saveTtsConfig(config); } + +/** + * Rename a cloned voice in local config only. + * Does NOT call any MiniMax API, the clone name is local metadata. + * @returns true when the voice exists and was renamed + */ +export function renameClonedVoice(voiceId: string, name: string): boolean { + const config = loadTtsConfig(); + const voice = config.clonedVoices.find((v) => v.voice_id === voiceId); + if (!voice) return false; + voice.name = name; + saveTtsConfig(config); + return true; +} From f77ba2ad6189139ce2604ea269327167b689eba2 Mon Sep 17 00:00:00 2001 From: Jonathan Zhang Date: Wed, 23 Sep 2026 10:27:25 +0800 Subject: [PATCH 06/17] style: unify provider panel visuals with api key card --- .../settings/ProviderConfigPanel.tsx | 171 +++++++----------- .../desktop/src/renderer/i18n/locales/en.json | 1 + .../src/renderer/i18n/locales/zh-CN.json | 1 + 3 files changed, 66 insertions(+), 107 deletions(-) diff --git a/packages/desktop/src/renderer/components/settings/ProviderConfigPanel.tsx b/packages/desktop/src/renderer/components/settings/ProviderConfigPanel.tsx index 477c5f2..fd6f776 100644 --- a/packages/desktop/src/renderer/components/settings/ProviderConfigPanel.tsx +++ b/packages/desktop/src/renderer/components/settings/ProviderConfigPanel.tsx @@ -2,18 +2,17 @@ * @file src/renderer/components/settings/ProviderConfigPanel.tsx * @description 模型供应商配置面板,管理 API Key 的输入、验证、保存和删除 * 使用单张大卡片内左右分栏布局,左栏供应商列表、右栏配置详情 - * 头部提供官方文档外链,API Key 区为加粗标签行加输入行两行结构 + * 头部与密钥区由 ApiKeyCard 渲染,与语音服务面板共用 */ import React, { useState, useEffect, useMemo } from 'react'; -import { CheckCircle2, ExternalLink, Loader2 } from 'lucide-react'; +import { Loader2 } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { useProviderStore } from '../../stores/providerStore'; -import { Button } from '../ui/Button'; -import { PasswordInput } from '../ui/PasswordInput'; import { ScrollArea } from '../ui/ScrollArea'; import { StatusDot } from '../ui/StatusDot'; import { ProviderMark, ModelMark } from '../common/ProviderMark'; +import { ApiKeyCard } from './ApiKeyCard'; import { orderProviders } from '../../lib/providerOrder'; import { toast } from '../../stores/toastStore'; import { logger } from '../../lib/logger'; @@ -181,113 +180,71 @@ export const ProviderConfigPanel: React.FC = () => {
{currentProvider ? ( <> - {/* 固定区: 标题、API Key 与状态提示 */} + {/* 固定区: 品牌头部、API Key 与状态提示 */}
-
- -
-

- {currentProvider.name} -

-

- {t('provider.configDesc', { name: currentProvider.name })} -

-
- {/* 官方文档外链:方钮停靠头部右缘,地址来自后端返回的 docUrl 字段 */} - -
- -
-
- - API Key - - {isConfigured && ( - - - {t('provider.configured')} - - )} -
- {/* 验证按钮外置于输入框右侧,已配置态由标签行的绿勾表达 */} -
-
- { - setApiKey(e.target.value); - setVerifyError(null); - }} - placeholder="sk-..." - className="w-full" - /> -
- -
- {verifyError && ( -

- {verifyError} -

- )} -
+ { + setApiKey(value); + setVerifyError(null); + }} + placeholder="sk-..." + verifyLabel={t('provider.verify')} + verifying={verifyingProvider === currentProvider.id} + verified={isConfigured} + onVerify={handleVerify} + feedback={ + verifyError && ( +

+ {verifyError} +

+ ) + } + />
- {/* 模型列表: 内嵌分隔线而非独立卡片,区域独立滚动 */} - -

- {t('provider.availableModels')} -

-
- {currentProvider.models.map((model) => ( -
- - - - {model} + {/* 模型列表: 独立滚动区块,标题行带模型计数,外框容器加行 hover */} + +
+

+ {t('provider.availableModels')} +

+ + {t('provider.modelCount', { + count: currentProvider.models.length, + })} + +
+
+
+ {currentProvider.models.map((model) => ( +
+ + + + {model} + - -
- ))} +
+ ))} +
diff --git a/packages/desktop/src/renderer/i18n/locales/en.json b/packages/desktop/src/renderer/i18n/locales/en.json index 441e42c..4666df0 100644 --- a/packages/desktop/src/renderer/i18n/locales/en.json +++ b/packages/desktop/src/renderer/i18n/locales/en.json @@ -128,6 +128,7 @@ "provider.configured": "Configured", "provider.officialDocs": "Official Docs", "provider.availableModels": "Available Models", + "provider.modelCount": "{{count}} models", "provider.deleteApiKey": "Delete API Key", "provider.selectToConfigure": "Select a provider to configure", diff --git a/packages/desktop/src/renderer/i18n/locales/zh-CN.json b/packages/desktop/src/renderer/i18n/locales/zh-CN.json index c5b61a0..7cda859 100644 --- a/packages/desktop/src/renderer/i18n/locales/zh-CN.json +++ b/packages/desktop/src/renderer/i18n/locales/zh-CN.json @@ -128,6 +128,7 @@ "provider.configured": "已配置", "provider.officialDocs": "官方文档", "provider.availableModels": "可用模型", + "provider.modelCount": "{{count}} 个", "provider.deleteApiKey": "删除 API Key", "provider.selectToConfigure": "选择一个供应商进行配置", From a96a63f1f388e9c073c489b54d41d35e1f552ddd Mon Sep 17 00:00:00 2001 From: Jonathan Zhang Date: Wed, 23 Sep 2026 10:28:26 +0800 Subject: [PATCH 07/17] style: replace in-card dividers with gap spacing across setting cards Also drops two residual fixed line heights that sat inside the rewrapped cards. --- .../agent-editor/AppearanceCard.tsx | 95 ++++----- .../agent-editor/ChatConfigCard.tsx | 47 ++--- .../agent-editor/ModelConfigCard.tsx | 78 +++---- .../components/agent-editor/VoiceCard.tsx | 123 ++++++----- .../renderer/components/common/SettingRow.tsx | 11 +- .../components/settings/EnvironmentCard.tsx | 18 +- .../components/settings/VersionUpdateCard.tsx | 191 +++++++++--------- .../components/shell/ServerManagerModal.tsx | 2 - 8 files changed, 281 insertions(+), 284 deletions(-) diff --git a/packages/desktop/src/renderer/components/agent-editor/AppearanceCard.tsx b/packages/desktop/src/renderer/components/agent-editor/AppearanceCard.tsx index 6da0102..062df21 100644 --- a/packages/desktop/src/renderer/components/agent-editor/AppearanceCard.tsx +++ b/packages/desktop/src/renderer/components/agent-editor/AppearanceCard.tsx @@ -6,7 +6,6 @@ import React, { useState } from 'react'; import { VenetianMask } from 'lucide-react'; import { useTranslation } from 'react-i18next'; -import { SettingDivider } from '../common/SettingRow'; import { HoverDeleteButton } from '../ui/HoverDeleteButton'; import { ImagePreviewOverlay } from '../ui/ImagePreviewOverlay'; import { ImageAddTile } from '../ui/ImageAddTile'; @@ -47,55 +46,59 @@ export const AppearanceCard: React.FC = ({ return ( - - - - +
+
+ + +
- - {bgPreviewUrl ? ( -
-
setBgPreviewOpen(true)} - > - -
- + - {bgPreviewOpen && ( - setBgPreviewOpen(false)} - /> + {bgPreviewUrl ? ( +
+
setBgPreviewOpen(true)} + > + +
+ + {bgPreviewOpen && ( + setBgPreviewOpen(false)} + /> + )} +
+ ) : ( + )}
- ) : ( - - )} +
); }; diff --git a/packages/desktop/src/renderer/components/agent-editor/ChatConfigCard.tsx b/packages/desktop/src/renderer/components/agent-editor/ChatConfigCard.tsx index 40f4922..617ffdd 100644 --- a/packages/desktop/src/renderer/components/agent-editor/ChatConfigCard.tsx +++ b/packages/desktop/src/renderer/components/agent-editor/ChatConfigCard.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { MessageSquare } from 'lucide-react'; import { useTranslation } from 'react-i18next'; -import { SettingRow, SettingDivider } from '../common/SettingRow'; +import { SettingRow } from '../common/SettingRow'; import { Card } from '../ui/Card'; import { NumberInput } from './NumberInput'; @@ -28,28 +28,29 @@ export const ChatConfigCard: React.FC = ({ return ( - - - - - - - +
+ + + + + + +
); }; diff --git a/packages/desktop/src/renderer/components/agent-editor/ModelConfigCard.tsx b/packages/desktop/src/renderer/components/agent-editor/ModelConfigCard.tsx index 55825ee..7ed4587 100644 --- a/packages/desktop/src/renderer/components/agent-editor/ModelConfigCard.tsx +++ b/packages/desktop/src/renderer/components/agent-editor/ModelConfigCard.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { Brain } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { ModelSelector } from '../common/ModelSelector'; -import { SettingRow, SettingDivider } from '../common/SettingRow'; +import { SettingRow } from '../common/SettingRow'; import { LabelWithTooltip } from '../common/LabelWithTooltip'; import { Card } from '../ui/Card'; import { NumberInput } from './NumberInput'; @@ -41,44 +41,44 @@ export const ModelConfigCard: React.FC = ({ return ( - - - - - - - - -
- -