From 5e942ca9458d45997aecd18c94320251a5dee334 Mon Sep 17 00:00:00 2001 From: linshen <32978552+linshenkx@users.noreply.github.com> Date: Thu, 14 May 2026 21:18:43 +0800 Subject: [PATCH] feat: polish provider controls and themed UI - Add provider pill selection and normalized provider display copy - Adopt themed tooltips, confirmation dialogs, and Naive UI surface styles - Enable Prompt Garden thumbnail previews through shared image preview components --- .../src/services/image/adapters/modelscope.ts | 1 + .../src/services/image/adapters/openrouter.ts | 1 + .../src/services/image/adapters/seedream.ts | 11 +- .../services/image/adapters/siliconflow.ts | 1 + .../llm/adapters/cloudflare-adapter.ts | 4 +- .../llm/adapters/dashscope-adapter.ts | 10 +- .../llm/adapters/modelscope-adapter.ts | 4 +- .../llm/adapters/openai-compatible-adapter.ts | 2 +- .../llm/adapters/openrouter-adapter.ts | 4 +- packages/core/src/services/model/converter.ts | 2 +- packages/core/src/services/model/defaults.ts | 2 +- .../core/src/services/model/model-utils.ts | 2 +- .../unit/image/modelscope-adapter.test.ts | 1 + .../unit/image/openrouter-adapter.test.ts | 1 + .../tests/unit/image/seedream-adapter.test.ts | 3 +- .../unit/image/siliconflow-adapter.test.ts | 1 + .../tests/unit/llm/openai-adapter.test.ts | 4 +- .../core/tests/unit/model/defaults.test.ts | 2 +- .../core/tests/unit/model/model-utils.test.ts | 2 +- .../ui/src/components/CategoryManager.vue | 5 +- packages/ui/src/components/DataManager.vue | 73 +++-- .../components/FavoriteLibraryWorkspace.vue | 74 ++--- .../ui/src/components/FavoriteListItem.vue | 17 +- .../src/components/FunctionModelManager.vue | 9 +- packages/ui/src/components/HistoryDrawer.vue | 32 +- .../ui/src/components/ImageModelEditModal.vue | 64 ++-- .../ui/src/components/ImageModelManager.vue | 35 ++- .../src/components/ImageModelQuickSwitch.vue | 10 +- .../ui/src/components/InputWithSelect.vue | 41 ++- packages/ui/src/components/MainLayout.vue | 20 +- .../ui/src/components/MarkdownRenderer.vue | 28 +- .../src/components/ModelParameterEditor.vue | 4 +- packages/ui/src/components/OutputDisplay.vue | 53 ++-- packages/ui/src/components/PromptPanel.vue | 45 +-- .../ui/src/components/ProviderPillSelect.vue | 294 ++++++++++++++++++ .../SaveTestResultExampleButton.vue | 38 ++- packages/ui/src/components/TagManager.vue | 13 +- .../ui/src/components/TemplateManager.vue | 26 +- packages/ui/src/components/TextDiff.vue | 19 +- .../ui/src/components/TextModelEditModal.vue | 90 +++--- packages/ui/src/components/TextModelList.vue | 7 +- .../ui/src/components/TextModelManager.vue | 23 +- .../src/components/TextModelQuickSwitch.vue | 13 +- packages/ui/src/components/Toast.vue | 28 +- .../app-layout/AppHeaderActions.vue | 10 +- .../app-layout/PromptOptimizerApp.vue | 66 ++-- .../basic-mode/BasicSystemWorkspace.vue | 40 ++- .../basic-mode/BasicUserWorkspace.vue | 40 ++- .../common/PromptGardenInspirationPopover.vue | 21 +- .../src/components/common/ThemedTooltip.vue | 72 ++++- .../context-mode/ContextSystemWorkspace.vue | 39 ++- .../context-mode/ContextUserWorkspace.vue | 40 ++- .../evaluation/CompareRoleBadge.vue | 24 +- .../evaluation/CompareRoleConfigDialog.vue | 22 +- .../components/evaluation/EvaluationPanel.vue | 20 +- .../evaluation/FeedbackAnalyzeButton.vue | 46 ++- .../evaluation/FocusAnalyzeButton.vue | 67 ++-- .../src/components/evaluation/InlineDiff.vue | 19 +- .../image-mode/ImageImage2ImageWorkspace.vue | 27 +- .../image-mode/ImageMultiImageWorkspace.vue | 44 ++- .../image-mode/ImageText2ImageWorkspace.vue | 26 +- .../components/media/safe-preview-toolbar.ts | 8 +- .../src/components/tool/ToolManagerModal.vue | 15 +- .../variable/VariableManagerModal.vue | 38 ++- .../ui/src/components/xml/XmlTreeNode.vue | 88 ++++-- packages/ui/src/composables/ui/index.ts | 1 + .../ui/src/composables/ui/useConfirmDialog.ts | 78 +++++ .../workspaces/useWorkspaceModelSelection.ts | 12 +- .../useWorkspaceTextModelSelection.ts | 8 +- packages/ui/src/i18n/locales/en-US/models.ts | 4 +- packages/ui/src/i18n/locales/zh-CN/models.ts | 4 +- packages/ui/src/i18n/locales/zh-TW/models.ts | 4 +- packages/ui/src/utils/data-transformer.ts | 28 +- packages/ui/src/utils/provider-display.ts | 54 ++++ .../PromptGardenInspirationPopover.spec.ts | 86 +++++ .../components/ProviderPillSelect.spec.ts | 92 ++++++ .../unit/i18n/runtime-english-guards.spec.ts | 2 +- .../image/app-preview-image-adoption.spec.ts | 2 + .../tests/unit/utils/provider-display.spec.ts | 28 ++ 79 files changed, 1584 insertions(+), 710 deletions(-) create mode 100644 packages/ui/src/components/ProviderPillSelect.vue create mode 100644 packages/ui/src/composables/ui/useConfirmDialog.ts create mode 100644 packages/ui/src/utils/provider-display.ts create mode 100644 packages/ui/tests/unit/components/ProviderPillSelect.spec.ts create mode 100644 packages/ui/tests/unit/utils/provider-display.spec.ts diff --git a/packages/core/src/services/image/adapters/modelscope.ts b/packages/core/src/services/image/adapters/modelscope.ts index a04befc6..69063212 100644 --- a/packages/core/src/services/image/adapters/modelscope.ts +++ b/packages/core/src/services/image/adapters/modelscope.ts @@ -42,6 +42,7 @@ export class ModelScopeImageAdapter extends AbstractImageProviderAdapter { requiresApiKey: true, defaultBaseURL: 'https://api-inference.modelscope.cn/v1', supportsDynamicModels: false, + apiKeyUrl: 'https://modelscope.cn/my/myaccesstoken', connectionSchema: { required: ['apiKey'], optional: ['baseURL'], diff --git a/packages/core/src/services/image/adapters/openrouter.ts b/packages/core/src/services/image/adapters/openrouter.ts index 9802bc44..a4e6da4a 100644 --- a/packages/core/src/services/image/adapters/openrouter.ts +++ b/packages/core/src/services/image/adapters/openrouter.ts @@ -25,6 +25,7 @@ export class OpenRouterImageAdapter extends AbstractImageProviderAdapter { requiresApiKey: true, defaultBaseURL: 'https://openrouter.ai/api/v1', supportsDynamicModels: true, + apiKeyUrl: 'https://openrouter.ai/settings/keys', connectionSchema: { required: ['apiKey'], optional: ['baseURL'], diff --git a/packages/core/src/services/image/adapters/seedream.ts b/packages/core/src/services/image/adapters/seedream.ts index 7af7dcbb..ebaaff15 100644 --- a/packages/core/src/services/image/adapters/seedream.ts +++ b/packages/core/src/services/image/adapters/seedream.ts @@ -31,7 +31,7 @@ const SEEDREAM_MODEL_SPECS: readonly SeedreamModelSpec[] = [ { id: SEEDREAM_40_MODEL_ID, name: 'Doubao Seedream 4.0', - description: 'Volcano Ark Doubao Seedream 4.0 high-quality image generation model', + description: 'Doubao Seedream 4.0 high-quality image generation model', capabilities: { text2image: true, image2image: true, @@ -80,7 +80,7 @@ const SEEDREAM_MODEL_SPECS: readonly SeedreamModelSpec[] = [ { id: SEEDREAM_45_MODEL_ID, name: 'Doubao Seedream 4.5', - description: 'Volcano Ark Doubao Seedream 4.5 high-quality image generation model', + description: 'Doubao Seedream 4.5 high-quality image generation model', capabilities: { text2image: true, image2image: true, @@ -129,7 +129,7 @@ const SEEDREAM_MODEL_SPECS: readonly SeedreamModelSpec[] = [ { id: SEEDREAM_50_LITE_MODEL_ID, name: 'Doubao Seedream 5.0 Lite', - description: 'Volcano Ark Doubao Seedream 5.0 Lite high-quality image generation model', + description: 'Doubao Seedream 5.0 Lite high-quality image generation model', capabilities: { text2image: true, image2image: true, @@ -179,11 +179,12 @@ export class SeedreamImageAdapter extends AbstractImageProviderAdapter { getProvider(): ImageProvider { return { id: 'seedream', - name: 'Seedream (Volcano Ark)', - description: 'Volcano Ark Seedream image generation models', + name: 'Seedream', + description: 'Seedream image generation models', requiresApiKey: true, defaultBaseURL: 'https://ark.cn-beijing.volces.com/api/v3', supportsDynamicModels: false, // 不支持动态获取 + apiKeyUrl: 'https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey', connectionSchema: { required: ['apiKey'], optional: ['baseURL'], diff --git a/packages/core/src/services/image/adapters/siliconflow.ts b/packages/core/src/services/image/adapters/siliconflow.ts index aff5f3c1..875fab29 100644 --- a/packages/core/src/services/image/adapters/siliconflow.ts +++ b/packages/core/src/services/image/adapters/siliconflow.ts @@ -23,6 +23,7 @@ export class SiliconFlowImageAdapter extends AbstractImageProviderAdapter { requiresApiKey: true, defaultBaseURL: 'https://api.siliconflow.cn/v1', supportsDynamicModels: false, + apiKeyUrl: 'https://cloud.siliconflow.cn/account/ak', connectionSchema: { required: ['apiKey'], optional: ['baseURL'], diff --git a/packages/core/src/services/llm/adapters/cloudflare-adapter.ts b/packages/core/src/services/llm/adapters/cloudflare-adapter.ts index bdfc0cfe..b22369d5 100644 --- a/packages/core/src/services/llm/adapters/cloudflare-adapter.ts +++ b/packages/core/src/services/llm/adapters/cloudflare-adapter.ts @@ -27,7 +27,7 @@ const CLOUDFLARE_STATIC_MODELS: ModelOverride[] = [ { id: '@cf/qwen/qwen3-30b-a3b-fp8', name: 'Qwen3 30B A3B FP8', - description: 'Cloudflare Workers AI 默认推荐文本模型,成本较低,支持工具调用和推理输出。', + description: 'Default recommended Cloudflare Workers AI text model with tool calling and reasoning output support', capabilities: { supportsTools: true, supportsReasoning: true, @@ -41,7 +41,7 @@ export class CloudflareAdapter extends OpenAIAdapter { return { id: 'cloudflare', name: 'Cloudflare', - description: 'Cloudflare Workers AI 文本模型,支持动态检索可用模型,默认推荐 Qwen3。', + description: 'Cloudflare Workers AI text models with dynamic model discovery', corsRestricted: true, requiresApiKey: true, defaultBaseURL: 'https://api.cloudflare.com/client/v4/accounts/{accountId}/ai/v1', diff --git a/packages/core/src/services/llm/adapters/dashscope-adapter.ts b/packages/core/src/services/llm/adapters/dashscope-adapter.ts index de22cb34..79e39a33 100644 --- a/packages/core/src/services/llm/adapters/dashscope-adapter.ts +++ b/packages/core/src/services/llm/adapters/dashscope-adapter.ts @@ -16,7 +16,7 @@ const DASHSCOPE_STATIC_MODELS: ModelOverride[] = [ { id: 'qwen3.5-27b', name: 'Qwen3.5-27B', - description: '通义千问3.5 27B 模型,默认用于阿里百炼文本与视觉理解场景', + description: 'Qwen3.5 27B model for DashScope text and vision understanding scenarios', capabilities: { supportsTools: true, supportsReasoning: true, @@ -26,7 +26,7 @@ const DASHSCOPE_STATIC_MODELS: ModelOverride[] = [ { id: 'qwen-plus', name: 'Qwen Plus', - description: '通义千问高性能模型,适合复杂任务,支持超长上下文', + description: 'High-performance Qwen model for complex tasks with long-context support', capabilities: { supportsTools: true, supportsReasoning: false, @@ -36,7 +36,7 @@ const DASHSCOPE_STATIC_MODELS: ModelOverride[] = [ { id: 'qwen-turbo', name: 'Qwen Turbo', - description: '通义千问快速模型,支持超长上下文(1M tokens)', + description: 'Fast Qwen model with long-context support up to 1M tokens', capabilities: { supportsTools: true, supportsReasoning: false, @@ -46,7 +46,7 @@ const DASHSCOPE_STATIC_MODELS: ModelOverride[] = [ { id: 'qwen-flash', name: 'Qwen Flash', - description: '通义千问极速模型,响应快速,适合简单任务', + description: 'Lightweight Qwen model optimized for fast responses and simple tasks', capabilities: { supportsTools: true, supportsReasoning: false, @@ -67,7 +67,7 @@ export class DashScopeAdapter extends OpenAIAdapter { return { id: 'dashscope', name: 'DashScope', - description: '阿里云百炼大模型服务平台,提供通义千问系列模型,支持 OpenAI 兼容的 Chat Completions 与 Responses 接口', + description: 'Alibaba Cloud Model Studio provider for Qwen models with OpenAI-compatible Chat Completions and Responses APIs', requiresApiKey: true, defaultBaseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1', supportsDynamicModels: true, diff --git a/packages/core/src/services/llm/adapters/modelscope-adapter.ts b/packages/core/src/services/llm/adapters/modelscope-adapter.ts index 68f53f1f..602899c5 100644 --- a/packages/core/src/services/llm/adapters/modelscope-adapter.ts +++ b/packages/core/src/services/llm/adapters/modelscope-adapter.ts @@ -17,7 +17,7 @@ const MODELSCOPE_STATIC_MODELS: ModelOverride[] = [ { id: 'Qwen/Qwen3-Coder-480B-A35B-Instruct', name: 'Qwen3-Coder-480B-A35B-Instruct', - description: '通义千问 Qwen/Qwen3-Coder-480B-A35B-Instruct,专为代码生成和理解优化', + description: 'Qwen3-Coder model optimized for code generation and understanding', capabilities: { supportsTools: false, // 未验证 ModelScope 的工具调用兼容性 supportsReasoning: false, @@ -43,7 +43,7 @@ export class ModelScopeAdapter extends OpenAIAdapter { return { id: 'modelscope', name: 'ModelScope', - description: '阿里云魔搭社区 API 推理服务,每天免费 2000 次调用', + description: 'ModelScope API-Inference service for community-hosted open models', requiresApiKey: true, defaultBaseURL: 'https://api-inference.modelscope.cn/v1', supportsDynamicModels: true, diff --git a/packages/core/src/services/llm/adapters/openai-compatible-adapter.ts b/packages/core/src/services/llm/adapters/openai-compatible-adapter.ts index 22db1792..ea89e45d 100644 --- a/packages/core/src/services/llm/adapters/openai-compatible-adapter.ts +++ b/packages/core/src/services/llm/adapters/openai-compatible-adapter.ts @@ -17,7 +17,7 @@ export class OpenAICompatibleAdapter extends OpenAIAdapter { public getProvider(): TextProvider { return { id: 'openai-compatible', - name: 'Custom API (OpenAI Compatible)', + name: 'OpenAI Compatible (Custom)', description: 'Custom endpoints that implement OpenAI Chat Completions or Responses APIs', requiresApiKey: false, defaultBaseURL: 'http://localhost:11434/v1', diff --git a/packages/core/src/services/llm/adapters/openrouter-adapter.ts b/packages/core/src/services/llm/adapters/openrouter-adapter.ts index c6ba60ab..78321f1c 100644 --- a/packages/core/src/services/llm/adapters/openrouter-adapter.ts +++ b/packages/core/src/services/llm/adapters/openrouter-adapter.ts @@ -13,7 +13,7 @@ const OPENROUTER_STATIC_MODELS: ModelOverride[] = [ { id: 'google/gemma-3-27b-it:free', name: 'Gemma 3 27B IT (Free)', - description: 'Google Gemma 3 27B 免费模型,通过 OpenRouter 访问', + description: 'Free Google Gemma 3 27B model served through OpenRouter', capabilities: { supportsTools: true, supportsReasoning: false, @@ -27,7 +27,7 @@ export class OpenRouterAdapter extends OpenAIAdapter { return { id: 'openrouter', name: 'OpenRouter', - description: 'OpenRouter 聚合多种 AI 模型的 OpenAI 兼容 API', + description: 'OpenAI-compatible gateway for accessing models from many providers', requiresApiKey: true, defaultBaseURL: 'https://openrouter.ai/api/v1', supportsDynamicModels: true, diff --git a/packages/core/src/services/model/converter.ts b/packages/core/src/services/model/converter.ts index ea8b06bb..1729056a 100644 --- a/packages/core/src/services/model/converter.ts +++ b/packages/core/src/services/model/converter.ts @@ -283,7 +283,7 @@ function createProviderMeta(providerId: string, legacy: ModelConfig): TextProvid } else if (providerId === 'openai-compatible') { return { id: 'openai-compatible', - name: 'Custom API (OpenAI Compatible)', + name: 'OpenAI Compatible (Custom)', description: 'Custom endpoints that implement OpenAI Chat Completions or Responses APIs', requiresApiKey: false, defaultBaseURL: legacy.baseURL || 'http://localhost:11434/v1', diff --git a/packages/core/src/services/model/defaults.ts b/packages/core/src/services/model/defaults.ts index 96b614cb..29f5f91b 100644 --- a/packages/core/src/services/model/defaults.ts +++ b/packages/core/src/services/model/defaults.ts @@ -131,7 +131,7 @@ export function getDefaultTextModels(registry?: ITextAdapterRegistry): Record { expect(provider.requiresApiKey).toBe(true) expect(provider.defaultBaseURL).toBe('https://api-inference.modelscope.cn/v1') expect(provider.supportsDynamicModels).toBe(false) + expect(provider.apiKeyUrl).toBe('https://modelscope.cn/my/myaccesstoken') expect(provider.connectionSchema?.required).toContain('apiKey') expect(provider.connectionSchema?.optional).toEqual(expect.arrayContaining(['baseURL'])) }) diff --git a/packages/core/tests/unit/image/openrouter-adapter.test.ts b/packages/core/tests/unit/image/openrouter-adapter.test.ts index 88bf7930..ea03371e 100644 --- a/packages/core/tests/unit/image/openrouter-adapter.test.ts +++ b/packages/core/tests/unit/image/openrouter-adapter.test.ts @@ -24,6 +24,7 @@ describe('OpenRouterImageAdapter', () => { expect(provider.requiresApiKey).toBe(true) expect(provider.supportsDynamicModels).toBe(true) expect(provider.defaultBaseURL).toBe('https://openrouter.ai/api/v1') + expect(provider.apiKeyUrl).toBe('https://openrouter.ai/settings/keys') }) it('should have correct connection schema', () => { diff --git a/packages/core/tests/unit/image/seedream-adapter.test.ts b/packages/core/tests/unit/image/seedream-adapter.test.ts index bcb20e3a..2bb8e529 100644 --- a/packages/core/tests/unit/image/seedream-adapter.test.ts +++ b/packages/core/tests/unit/image/seedream-adapter.test.ts @@ -24,10 +24,11 @@ describe('SeedreamImageAdapter', () => { const provider = adapter.getProvider() expect(provider.id).toBe('seedream') - expect(provider.name).toContain('Seedream') + expect(provider.name).toBe('Seedream') expect(provider.requiresApiKey).toBe(true) expect(provider.defaultBaseURL).toBe('https://ark.cn-beijing.volces.com/api/v3') expect(provider.supportsDynamicModels).toBe(false) + expect(provider.apiKeyUrl).toBe('https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey') expect(provider.connectionSchema?.required).toContain('apiKey') expect(provider.connectionSchema?.optional).toEqual(expect.arrayContaining(['baseURL'])) }) diff --git a/packages/core/tests/unit/image/siliconflow-adapter.test.ts b/packages/core/tests/unit/image/siliconflow-adapter.test.ts index d0b074c5..2be19649 100644 --- a/packages/core/tests/unit/image/siliconflow-adapter.test.ts +++ b/packages/core/tests/unit/image/siliconflow-adapter.test.ts @@ -24,6 +24,7 @@ describe('SiliconFlowImageAdapter', () => { expect(provider.requiresApiKey).toBe(true) expect(provider.defaultBaseURL).toBe('https://api.siliconflow.cn/v1') expect(provider.supportsDynamicModels).toBe(false) + expect(provider.apiKeyUrl).toBe('https://cloud.siliconflow.cn/account/ak') expect(provider.connectionSchema?.required).toContain('apiKey') expect(provider.connectionSchema?.optional).toEqual(expect.arrayContaining(['baseURL'])) }) diff --git a/packages/core/tests/unit/llm/openai-adapter.test.ts b/packages/core/tests/unit/llm/openai-adapter.test.ts index adad6c9f..3ebbe27f 100644 --- a/packages/core/tests/unit/llm/openai-adapter.test.ts +++ b/packages/core/tests/unit/llm/openai-adapter.test.ts @@ -389,7 +389,7 @@ describe('OpenAIAdapter', () => { const compatibleConfig: TextModelConfig = { ...mockConfig, id: 'openai-compatible', - name: 'Custom API (OpenAI Compatible)', + name: 'OpenAI Compatible (Custom)', providerMeta: openAICompatibleAdapter.getProvider(), modelMeta: openAICompatibleAdapter.buildDefaultModel('custom-model'), connectionConfig: { @@ -443,7 +443,7 @@ describe('OpenAIAdapter', () => { const compatibleConfig: TextModelConfig = { ...mockConfig, id: 'openai-compatible', - name: 'Custom API (OpenAI Compatible)', + name: 'OpenAI Compatible (Custom)', providerMeta: openAICompatibleAdapter.getProvider(), modelMeta: openAICompatibleAdapter.buildDefaultModel('custom-model'), connectionConfig: { diff --git a/packages/core/tests/unit/model/defaults.test.ts b/packages/core/tests/unit/model/defaults.test.ts index 6fee9f6e..5634216a 100644 --- a/packages/core/tests/unit/model/defaults.test.ts +++ b/packages/core/tests/unit/model/defaults.test.ts @@ -144,7 +144,7 @@ describe('model defaults provider env mapping', () => { expect(models.custom).toBeDefined() expect(models.custom.providerMeta.id).toBe('openai-compatible') - expect(models.custom.providerMeta.name).toBe('Custom API (OpenAI Compatible)') + expect(models.custom.providerMeta.name).toBe('OpenAI Compatible (Custom)') expect(models.custom.enabled).toBe(true) expect(models.custom.connectionConfig.apiKey).toBe('') expect(models.custom.connectionConfig.requestStyle).toBe('chat_completions') diff --git a/packages/core/tests/unit/model/model-utils.test.ts b/packages/core/tests/unit/model/model-utils.test.ts index 092cd7bb..39c989a0 100644 --- a/packages/core/tests/unit/model/model-utils.test.ts +++ b/packages/core/tests/unit/model/model-utils.test.ts @@ -65,7 +65,7 @@ describe('generateTextModelConfig', () => { const config = generateTextModelConfig(baseEnvConfig) expect(config.providerMeta.id).toBe('openai-compatible') - expect(config.providerMeta.name).toBe('Custom API (OpenAI Compatible)') + expect(config.providerMeta.name).toBe('OpenAI Compatible (Custom)') expect(config.connectionConfig.requestStyle).toBe('chat_completions') expect(config.modelMeta.providerId).toBe('openai-compatible') }) diff --git a/packages/ui/src/components/CategoryManager.vue b/packages/ui/src/components/CategoryManager.vue index 4e5d874b..653e63d5 100644 --- a/packages/ui/src/components/CategoryManager.vue +++ b/packages/ui/src/components/CategoryManager.vue @@ -525,7 +525,8 @@ watch(() => services?.value?.favoriteManager, (favoriteManager) => { } .toolbar { - @apply p-4 border-b border-gray-200 dark:border-gray-700; + @apply p-4; + border-bottom: 1px solid var(--n-border-color); } .tree-container { @@ -545,6 +546,6 @@ watch(() => services?.value?.favoriteManager, (favoriteManager) => { } :deep(.n-tree-node-content:hover) { - @apply bg-gray-50 dark:bg-gray-800; + background: var(--n-hover-color); } diff --git a/packages/ui/src/components/DataManager.vue b/packages/ui/src/components/DataManager.vue index c7646fb6..1e3fe8f1 100644 --- a/packages/ui/src/components/DataManager.vue +++ b/packages/ui/src/components/DataManager.vue @@ -178,10 +178,9 @@ -
- {{ $t('dataManager.import.settingsTitle') }} - -
+ + +
{{ $t('dataManager.import.scopeTitle') }} @@ -232,8 +231,9 @@
-
-
+ + + {{ $t('dataManager.warning') }} @@ -376,8 +376,8 @@ 'remote-step--collapsed': !isCloudflareR2StepExpanded('account'), }" > - +
{{ $t('dataManager.remote.accountId') }} @@ -418,8 +418,8 @@ 'remote-step--collapsed': !isCloudflareR2StepExpanded('bucket'), }" > - +
{{ $t('dataManager.remote.bucket') }} @@ -477,8 +477,8 @@ 'remote-step--collapsed': !isCloudflareR2StepExpanded('credentials'), }" > - +
@@ -770,11 +770,13 @@ import { useI18n } from 'vue-i18n' import { NModal, NText, NButton, NUpload, NUploadDragger, NIcon, NCheckbox, NRadio, NRadioGroup, NSelect, NInput, NProgress, NTag, + NCollapse, NCollapseItem, type UploadFileInfo, } from 'naive-ui' import { isRunningInElectron } from '@prompt-optimizer/core' import { ChevronDown, Clipboard, Download, ExternalLink, Folder, Refresh, Trash, Upload } from '@vicons/tabler' import { useToast } from '../composables/ui/useToast' +import { useConfirmDialog } from '../composables/ui/useConfirmDialog' import type { AppServices } from '../types/services' import { DEFAULT_DATA_MANAGER_PACKAGE_SECTIONS, @@ -840,6 +842,7 @@ const emit = defineEmits() const { t } = useI18n() const toast = useToast() +const confirmDialog = useConfirmDialog() const isDesktopRuntime = isRunningInElectron() const remoteRuntime: RemoteBackupRuntime = isDesktopRuntime ? 'desktop' : 'web' const DATA_MANAGER_MODAL_MAX_WIDTH = '1200px' @@ -1641,10 +1644,15 @@ const handleRemoteCleanup = async () => { return } - const confirmed = window.confirm(t('dataManager.remote.cleanupConfirm', { - count: analysis.candidates.length, - size: formatFileSize(analysis.totalCandidateBytes), - })) + const confirmed = await confirmDialog.warning({ + title: t('common.warning'), + content: t('dataManager.remote.cleanupConfirm', { + count: analysis.candidates.length, + size: formatFileSize(analysis.totalCandidateBytes), + }), + positiveText: t('common.confirm'), + negativeText: t('common.cancel'), + }) if (!confirmed) return const cleanup = await cleanupRemoteSnapshotAssets(objectStore, (event) => @@ -2264,18 +2272,21 @@ const getStorageItemDetail = (item: Pick) } .remote-step-header { + min-width: 0; + width: 100%; + padding: 0; + color: inherit; + text-align: left; +} + +.remote-step-header :deep(.n-button__content) { display: grid; grid-template-columns: 24px minmax(0, 1fr) 18px; gap: 8px; align-items: start; - min-width: 0; width: 100%; - padding: 0; - border: 0; - color: inherit; + min-width: 0; text-align: left; - background: transparent; - cursor: pointer; } .remote-step-toggle { @@ -2461,16 +2472,18 @@ const getStorageItemDetail = (item: Pick) } .local-import-settings { - padding: 8px 10px; + padding: 0 2px; border-radius: 8px; background: var(--n-color); } -.local-import-settings > summary { - cursor: pointer; +.local-import-settings :deep(.n-collapse-item__header) { + padding: 8px 10px; font-size: 12px; - color: var(--n-text-color-3); - user-select: none; +} + +.local-import-settings :deep(.n-collapse-item__content-inner) { + padding: 0 10px 10px; } .local-import-settings-body { diff --git a/packages/ui/src/components/FavoriteLibraryWorkspace.vue b/packages/ui/src/components/FavoriteLibraryWorkspace.vue index 07843731..99a803e4 100644 --- a/packages/ui/src/components/FavoriteLibraryWorkspace.vue +++ b/packages/ui/src/components/FavoriteLibraryWorkspace.vue @@ -277,6 +277,7 @@ import { useI18n } from 'vue-i18n' import type { FavoriteCategory, FavoritePrompt } from '@prompt-optimizer/core' import { useFavoriteInitializer } from '../composables/storage/useFavoriteInitializer' +import { useConfirmDialog } from '../composables/ui/useConfirmDialog' import { useToast } from '../composables/ui/useToast' import type { AppServices } from '../types/services' import { getI18nErrorMessage } from '../utils/error' @@ -349,6 +350,7 @@ const emit = defineEmits<{ const services = inject | null>('services', null) const message = useToast() +const confirmDialog = useConfirmDialog() let ensuredDefaultCategoryManager: NonNullable | null = null @@ -900,28 +902,29 @@ const incrementFavoriteUseCount = async (id: string) => { bumpUseCountLocally(id) } -const handleDeleteFavorite = (favorite: FavoritePrompt) => { +const handleDeleteFavorite = async (favorite: FavoritePrompt) => { const deletingOpenFavorite = selectedFavorite.value?.id === favorite.id || taskFavorite.value?.id === favorite.id - const confirmed = typeof window === 'undefined' - ? true - : window.confirm(t('favorites.manager.actions.deleteConfirm', { title: favorite.title })) + const confirmed = await confirmDialog.warning({ + title: t('common.warning'), + content: t('favorites.manager.actions.deleteConfirm', { title: favorite.title }), + positiveText: t('common.confirm'), + negativeText: t('common.cancel'), + }) if (!confirmed) return - ;(async () => { - try { - const servicesValue = services?.value - if (servicesValue?.favoriteManager) { - await servicesValue.favoriteManager.deleteFavorite(favorite.id) - message.success(t('favorites.manager.actions.deleteSuccess')) - await loadFavorites() - } else { - message.warning(t('favorites.manager.messages.unavailable')) - } - } catch (error) { - message.error(buildErrorMessage(t('favorites.manager.actions.deleteFailed'), error)) + try { + const servicesValue = services?.value + if (servicesValue?.favoriteManager) { + await servicesValue.favoriteManager.deleteFavorite(favorite.id) + message.success(t('favorites.manager.actions.deleteSuccess')) + await loadFavorites() + } else { + message.warning(t('favorites.manager.messages.unavailable')) } - })() + } catch (error) { + message.error(buildErrorMessage(t('favorites.manager.actions.deleteFailed'), error)) + } if (selectedFavorite.value?.id === favorite.id) { selectedFavorite.value = null @@ -965,7 +968,7 @@ const handleUseFavorite = async ( } } -const handleActionMenuSelect = (key: string) => { +const handleActionMenuSelect = async (key: string) => { switch (key) { case 'manageTags': tagManagerVisible.value = true @@ -977,29 +980,30 @@ const handleActionMenuSelect = (key: string) => { handleExportFavorites() break case 'clear': { - const confirmed = typeof window === 'undefined' - ? true - : window.confirm(t('favorites.manager.actions.clearConfirm')) + const confirmed = await confirmDialog.warning({ + title: t('common.warning'), + content: t('favorites.manager.actions.clearConfirm'), + positiveText: t('common.confirm'), + negativeText: t('common.cancel'), + }) if (!confirmed) { break } - ;(async () => { - try { - const servicesValue = services?.value - if (servicesValue?.favoriteManager) { - const allIds = favorites.value.map((favorite) => favorite.id) - await servicesValue.favoriteManager.deleteFavorites(allIds) - message.success(t('favorites.manager.actions.clearSuccess')) - await loadFavorites() - } else { - message.warning(t('favorites.manager.messages.unavailable')) - } - } catch (error) { - message.error(buildErrorMessage(t('favorites.manager.actions.clearFailed'), error)) + try { + const servicesValue = services?.value + if (servicesValue?.favoriteManager) { + const allIds = favorites.value.map((favorite) => favorite.id) + await servicesValue.favoriteManager.deleteFavorites(allIds) + message.success(t('favorites.manager.actions.clearSuccess')) + await loadFavorites() + } else { + message.warning(t('favorites.manager.messages.unavailable')) } - })() + } catch (error) { + message.error(buildErrorMessage(t('favorites.manager.actions.clearFailed'), error)) + } break } } diff --git a/packages/ui/src/components/FavoriteListItem.vue b/packages/ui/src/components/FavoriteListItem.vue index fc691a53..79e2c7b5 100644 --- a/packages/ui/src/components/FavoriteListItem.vue +++ b/packages/ui/src/components/FavoriteListItem.vue @@ -236,7 +236,10 @@ const handleActionSelect = (key: string) => { .favorite-list-item { @apply transition-colors duration-200; - @apply hover:bg-gray-50 dark:hover:bg-gray-800; +} + +.favorite-list-item:hover { + background: var(--n-hover-color); } .item-checkbox { @@ -252,7 +255,8 @@ const handleActionSelect = (key: string) => { } .title-text { - @apply font-medium text-gray-900 dark:text-gray-100; + @apply font-medium; + color: var(--n-text-color); } .item-actions { @@ -264,7 +268,8 @@ const handleActionSelect = (key: string) => { } .content-preview { - @apply text-sm text-gray-700 dark:text-gray-300 mb-1; + @apply text-sm mb-1; + color: var(--n-text-color-2); line-height: 1.5; max-height: 3em; overflow: hidden; @@ -274,7 +279,8 @@ const handleActionSelect = (key: string) => { } .description-text { - @apply text-xs text-gray-500 dark:text-gray-400; + @apply text-xs; + color: var(--n-text-color-3); max-height: 2em; overflow: hidden; display: -webkit-box; @@ -283,7 +289,8 @@ const handleActionSelect = (key: string) => { } .item-footer { - @apply flex justify-between items-center mt-3 pt-3 border-t border-gray-100 dark:border-gray-800; + @apply flex justify-between items-center mt-3 pt-3 border-t; + border-color: var(--n-border-color); } .footer-left { diff --git a/packages/ui/src/components/FunctionModelManager.vue b/packages/ui/src/components/FunctionModelManager.vue index a8fa0b88..db943c94 100644 --- a/packages/ui/src/components/FunctionModelManager.vue +++ b/packages/ui/src/components/FunctionModelManager.vue @@ -99,6 +99,7 @@ import { NCard, NSpace, NTag, NText } from 'naive-ui' import SelectWithConfig from './SelectWithConfig.vue' import { useFunctionModelManager } from '../composables/model/useFunctionModelManager' import { DataTransformer, OptionAccessors } from '../utils/data-transformer' +import { getProviderDisplayName, getTextModelConfigDisplayName } from '../utils/provider-display' import type { AppServices } from '../types/services' import type { ModelSelectOption } from '../types/select-options' @@ -138,7 +139,7 @@ const findModelInfo = (modelKey: string) => { const option = evaluationModelOptions.value.find(opt => opt.value === modelKey) if (!option?.raw) return null return { - provider: option.raw.providerMeta?.name || null, + provider: getProviderDisplayName(option.raw.providerMeta, t, ''), model: option.raw.modelMeta?.id || null, } } @@ -172,8 +173,10 @@ const refreshModels = async () => { await ensureInitializedIfSupported(manager) const enabledModels = await manager.getEnabledModels() - evaluationModelOptions.value = DataTransformer.modelsToSelectOptions(enabledModels) - imageRecognitionModelOptions.value = DataTransformer.modelsToSelectOptions(enabledModels) + const getProviderName = (model: ModelSelectOption['raw']) => getProviderDisplayName(model.providerMeta, t) + const getModelName = (model: ModelSelectOption['raw']) => getTextModelConfigDisplayName(model, t) + evaluationModelOptions.value = DataTransformer.modelsToSelectOptions(enabledModels, { getProviderName, getModelName }) + imageRecognitionModelOptions.value = DataTransformer.modelsToSelectOptions(enabledModels, { getProviderName, getModelName }) } catch (error) { console.error('[FunctionModelManager] Failed to refresh models:', error) evaluationModelOptions.value = [] diff --git a/packages/ui/src/components/HistoryDrawer.vue b/packages/ui/src/components/HistoryDrawer.vue index 92afd588..f143b250 100644 --- a/packages/ui/src/components/HistoryDrawer.vue +++ b/packages/ui/src/components/HistoryDrawer.vue @@ -227,6 +227,7 @@ import { NDivider, NCollapse, NCollapseItem, NEmpty, NInput } from 'naive-ui' import type { PromptRecord, PromptRecordChain } from '@prompt-optimizer/core' +import { useConfirmDialog } from '../composables/ui/useConfirmDialog' import { useToast } from '../composables/ui/useToast' import SourceAssetBadge from './source/SourceAssetBadge.vue' import { extractHistorySourceBinding } from '../utils/history-source-binding' @@ -256,6 +257,7 @@ const emit = defineEmits<{ // eslint-disable-next-line @typescript-eslint/no-unused-vars const _toast = useToast() +const confirmDialog = useConfirmDialog() const expandedVersions = ref>({}) const searchQuery = ref('') @@ -298,10 +300,16 @@ const _toggleVersion = (recordId: string) => { // 清空历史记录 const handleClear = async () => { - if (confirm(t('history.confirmClear'))) { - emit('clear') - // 不需要强制刷新,因为现在使用props.history - } + const confirmed = await confirmDialog.warning({ + title: t('common.warning'), + content: t('history.confirmClear'), + positiveText: t('common.confirm'), + negativeText: t('common.cancel'), + }) + if (!confirmed) return + + emit('clear') + // 不需要强制刷新,因为现在使用props.history } // 监听显示状态变化 @@ -370,11 +378,17 @@ const getFunctionModeLabel = (recordType: string) => { } // 添加删除单条记录的方法 -const deleteChain = (chainId: string) => { - if (confirm(t('history.confirmDeleteChain'))) { - emit('deleteChain', chainId) - // 不需要强制刷新,因为现在使用props.history - } +const deleteChain = async (chainId: string) => { + const confirmed = await confirmDialog.warning({ + title: t('common.warning'), + content: t('history.confirmDeleteChain'), + positiveText: t('common.confirm'), + negativeText: t('common.cancel'), + }) + if (!confirmed) return + + emit('deleteChain', chainId) + // 不需要强制刷新,因为现在使用props.history } diff --git a/packages/ui/src/components/ImageModelEditModal.vue b/packages/ui/src/components/ImageModelEditModal.vue index 8000928a..e50e71ec 100644 --- a/packages/ui/src/components/ImageModelEditModal.vue +++ b/packages/ui/src/components/ImageModelEditModal.vue @@ -25,13 +25,14 @@ {{ t('image.provider.section') }} - @@ -114,29 +115,30 @@ @update:value="handleModelChange" /> - - - {{ refreshButtonTooltip }} - + + + + + @@ -208,7 +210,7 @@ width="32" height="32" object-fit="cover" - :style="{ borderRadius: '4px', border: '1px solid #d9d9d9' }" + :style="{ borderRadius: '4px', border: '1px solid var(--n-border-color)' }" :preview-disabled="false" :alt="t('image.connection.testImagePreview')" /> @@ -237,15 +239,17 @@ import { computed, watch, nextTick, h } from 'vue' import { useI18n } from 'vue-i18n' import { NModal, NSpace, NInput, NInputNumber, - NCheckbox, NSelect, NButton, NTag, NTooltip, NText, + NCheckbox, NSelect, NButton, NTag, NText, NDivider, NH4, NForm, NFormItem, useDialog } from 'naive-ui' import { useImageModelManager } from '../composables/model/useImageModelManager' import { useToast } from '../composables/ui/useToast' import { isRunningInElectron, type ImageModelConfig } from '@prompt-optimizer/core' import ModelAdvancedSection from './ModelAdvancedSection.vue' +import ProviderPillSelect from './ProviderPillSelect.vue' import ExternalLinkIcon from './icons/ExternalLinkIcon.vue' import AppPreviewImage from './media/AppPreviewImage.vue' +import ThemedTooltip from './common/ThemedTooltip.vue' const { t } = useI18n() @@ -344,6 +348,10 @@ const providerOptions = computed(() => })) ) +const providerLabelOverrides = computed(() => ({ + 'openai-compatible': t('modelManager.provider.openaiCompatibleCustomLabel') +})) + const modelOptions = computed(() => models.value.map(m => ({ label: m.id, diff --git a/packages/ui/src/components/ImageModelManager.vue b/packages/ui/src/components/ImageModelManager.vue index 48b062a4..1d1b6b0b 100644 --- a/packages/ui/src/components/ImageModelManager.vue +++ b/packages/ui/src/components/ImageModelManager.vue @@ -38,7 +38,7 @@ - {{ config.provider?.name || config.providerId }} + {{ providerDisplayName(config) }} {{ config.model?.name || config.modelId }} @@ -89,7 +89,7 @@ width="24" height="24" object-fit="cover" - :style="{ borderRadius: '4px', border: '1px solid #d9d9d9' }" + :style="{ borderRadius: '4px', border: '1px solid var(--n-border-color)' }" :preview-disabled="false" :alt="t('image.connection.testImagePreview')" /> @@ -170,14 +170,17 @@ import { NSpace, NCard, NText, NTag, NButton, NEmpty, useDialog } from 'naive-ui' import { useImageModelManager } from '../composables/model/useImageModelManager' +import { useConfirmDialog } from '../composables/ui/useConfirmDialog' import { useToast } from '../composables/ui/useToast' import { getI18nErrorMessage } from '../utils/error' import { isRunningInElectron, type IImageService, type ImageModel } from '@prompt-optimizer/core' +import { getProviderDisplayName } from '../utils/provider-display' import AppPreviewImage from './media/AppPreviewImage.vue' const { t } = useI18n() const toast = useToast() const dialog = useDialog() +const confirmDialog = useConfirmDialog() const isElectronEnv = isRunningInElectron() // 定义事件 @@ -211,6 +214,8 @@ const testResults = ref>({}) const isTestingConnectionFor = (configId: string) => !!testingConnections.value[configId] +const providerDisplayName = (config: { provider?: { id?: string; name?: string }; providerId?: string }) => + getProviderDisplayName(config.provider || { id: config.providerId }, t) // 辅助函数:根据模型能力选择测试类型 const selectTestType = (model: ImageModel): 'text2image' | 'image2image' => { @@ -326,7 +331,7 @@ const testConnection = async (configId: string) => { if (config?.provider?.corsRestricted) { dialog.warning({ title: t('modelManager.corsRestrictedTag'), - content: () => h('div', { style: 'white-space: pre-line;' }, t('modelManager.corsRestrictedConfirm', { provider: config.provider.name || config.providerId })), + content: () => h('div', { style: 'white-space: pre-line;' }, t('modelManager.corsRestrictedConfirm', { provider: providerDisplayName(config) })), positiveText: t('common.confirm'), negativeText: t('common.cancel'), // Don't block dialog close while the async test runs. @@ -353,15 +358,21 @@ const toggleConfig = async (config: { id: string; enabled: boolean }) => { } const deleteConfig = async (configId: string) => { - if (confirm(t('modelManager.deleteConfirm'))) { - try { - await deleteConfigFromManager(configId) - await loadConfigs() - toast.success(t('modelManager.deleteSuccess')) - } catch (error) { - console.error('[ImageModelManager] Failed to delete model:', error) - toast.error(t('modelManager.deleteFailed', { error: getI18nErrorMessage(error, t('common.error')) })) - } + const confirmed = await confirmDialog.warning({ + title: t('common.warning'), + content: t('modelManager.deleteConfirm'), + positiveText: t('common.confirm'), + negativeText: t('common.cancel'), + }) + if (!confirmed) return + + try { + await deleteConfigFromManager(configId) + await loadConfigs() + toast.success(t('modelManager.deleteSuccess')) + } catch (error) { + console.error('[ImageModelManager] Failed to delete model:', error) + toast.error(t('modelManager.deleteFailed', { error: getI18nErrorMessage(error, t('common.error')) })) } } diff --git a/packages/ui/src/components/ImageModelQuickSwitch.vue b/packages/ui/src/components/ImageModelQuickSwitch.vue index 4284ae59..46948a1f 100644 --- a/packages/ui/src/components/ImageModelQuickSwitch.vue +++ b/packages/ui/src/components/ImageModelQuickSwitch.vue @@ -264,8 +264,8 @@ watch( overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - background: rgba(120, 113, 108, 0.1); - color: #5f5548; + background: var(--n-tag-color, var(--n-color-embedded)); + color: var(--n-text-color-2); transition: background-color 0.15s ease, box-shadow 0.15s ease, @@ -285,9 +285,9 @@ watch( .image-model-quick-switch__model--clickable:hover, .image-model-quick-switch__model--clickable:focus-visible { - background: rgba(120, 113, 108, 0.16); - color: #3f372d; - box-shadow: inset 0 0 0 1px rgba(120, 113, 108, 0.2); + background: var(--n-hover-color); + color: var(--n-text-color); + box-shadow: inset 0 0 0 1px var(--n-border-color); } .image-model-quick-switch__popover { diff --git a/packages/ui/src/components/InputWithSelect.vue b/packages/ui/src/components/InputWithSelect.vue index 712586a9..75196f9c 100644 --- a/packages/ui/src/components/InputWithSelect.vue +++ b/packages/ui/src/components/InputWithSelect.vue @@ -40,7 +40,7 @@
+ class="select-hint absolute right-12 top-0 bottom-0 min-w-[120px] flex items-center pointer-events-none"> {{ hintText }}
@@ -55,17 +55,20 @@ -
{{ option.label }} -
+
@@ -170,15 +173,8 @@ const selectOption = (option) => { // Close dropdown when clicking outside const handleClickOutside = (event) => { - console.log('Click detected', { - isOpen: isOpen.value, - isComponent: componentRef.value && componentRef.value.contains(event.target), - target: event.target - }); - // 只有在下拉菜单打开且点击的是组件外部时才关闭下拉菜单 if (isOpen.value && componentRef.value && !componentRef.value.contains(event.target)) { - console.log('Closing dropdown'); isOpen.value = false; searchText.value = ''; } @@ -215,4 +211,23 @@ onUnmounted(() => { } /* Pure Naive UI implementation - dropdown and hover styles handled by Naive UI */ - \ No newline at end of file +.select-hint { + font-size: 12px; + color: var(--n-text-color-3); +} + +.select-option { + justify-content: flex-start; + color: var(--n-text-color-2); +} + +.select-option:hover, +.select-option--selected { + background: var(--n-hover-color); + color: var(--n-text-color); +} + +.select-option--selected { + box-shadow: inset 2px 0 0 var(--n-primary-color); +} + diff --git a/packages/ui/src/components/MainLayout.vue b/packages/ui/src/components/MainLayout.vue index 155d10d8..f841ef58 100644 --- a/packages/ui/src/components/MainLayout.vue +++ b/packages/ui/src/components/MainLayout.vue @@ -14,8 +14,8 @@ - +
@@ -72,7 +72,7 @@ import { computed, ref, onMounted, onUnmounted } from 'vue' import { useI18n } from 'vue-i18n' -import { NLayout, NLayoutHeader, NLayoutContent, NFlex, NText } from 'naive-ui' +import { NButton, NLayout, NLayoutHeader, NLayoutContent, NFlex, NText } from 'naive-ui' import ToastUI from './Toast.vue' import logoImage from '../assets/logo.png' import AppPreviewImage from './media/AppPreviewImage.vue' @@ -162,14 +162,10 @@ const openBrandWebsite = async () => { } .brand-link { - display: inline-flex; align-items: center; padding: 6px 10px 6px 6px; - border: 0; border-radius: 12px; - background: transparent; color: inherit; - cursor: pointer; transition: background-color 0.2s ease-in-out, box-shadow 0.2s ease-in-out, @@ -177,7 +173,7 @@ const openBrandWebsite = async () => { } .brand-link:hover { - background: color-mix(in srgb, var(--primary-color, #18a058) 10%, transparent); + background: color-mix(in srgb, var(--n-primary-color) 10%, transparent); transform: translateY(-1px); } @@ -191,8 +187,8 @@ const openBrandWebsite = async () => { .brand-link:focus-visible { outline: none; - background: color-mix(in srgb, var(--primary-color, #18a058) 14%, transparent); - box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary-color, #18a058) 28%, transparent); + background: color-mix(in srgb, var(--n-primary-color) 14%, transparent); + box-shadow: 0 0 0 2px color-mix(in srgb, var(--n-primary-color) 28%, transparent); } /* Logo样式优化 */ @@ -216,7 +212,7 @@ const openBrandWebsite = async () => { align-items: center; margin-left: 16px; padding-left: 16px; - border-left: 1px solid var(--border-color, rgba(239, 239, 245, 0.6)); + border-left: 1px solid var(--n-border-color); min-height: 32px; } diff --git a/packages/ui/src/components/MarkdownRenderer.vue b/packages/ui/src/components/MarkdownRenderer.vue index 820a6d5a..673b048d 100644 --- a/packages/ui/src/components/MarkdownRenderer.vue +++ b/packages/ui/src/components/MarkdownRenderer.vue @@ -364,7 +364,7 @@ onMounted(renderMarkdown); font-weight: 600; padding-bottom: 0.1em; color: inherit; - border-bottom: 1px solid rgba(0, 0, 0, 0.1); + border-bottom: 1px solid var(--n-divider-color, var(--n-border-color)); } .markdown-content h3 { @@ -421,8 +421,8 @@ onMounted(renderMarkdown); overflow: auto; margin-bottom: 0.1em; position: relative; - background: rgba(0, 0, 0, 0.02); - border: 1px solid rgba(0, 0, 0, 0.1); + background: var(--n-color-embedded, color-mix(in srgb, var(--n-color) 92%, var(--n-text-color) 8%)); + border: 1px solid var(--n-border-color); /* 添加滚动条样式 */ scrollbar-width: thin; /* Firefox */ -ms-overflow-style: none; /* IE and Edge */ @@ -434,7 +434,7 @@ onMounted(renderMarkdown); } .markdown-content pre::-webkit-scrollbar-thumb { - background-color: rgba(0, 0, 0, 0.1); + background-color: var(--n-scrollbar-color, var(--n-border-color)); border-radius: 3px; } @@ -459,8 +459,8 @@ onMounted(renderMarkdown); "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; border-bottom-left-radius: 4px; user-select: none; - background-color: #18a058; - color: white; + background-color: var(--n-primary-color); + color: var(--n-text-color-primary); } .markdown-content code { @@ -471,9 +471,9 @@ onMounted(renderMarkdown); margin: 0.3em; border-radius: 6px; white-space: pre; - background-color: rgba(0, 0, 0, 0.02); + background-color: var(--n-color-embedded, color-mix(in srgb, var(--n-color) 92%, var(--n-text-color) 8%)); color: inherit; - border: 1px solid rgba(0, 0, 0, 0.1); + border: 1px solid var(--n-border-color); } /* 引用样式 */ @@ -482,8 +482,8 @@ onMounted(renderMarkdown); margin: var(--md-spacing-sm); border-left-width: 0.25em; border-left-style: solid; - border-left-color: #18a058; - background-color: rgba(0, 0, 0, 0.02); + border-left-color: var(--n-primary-color); + background-color: var(--n-color-embedded, color-mix(in srgb, var(--n-color) 92%, var(--n-text-color) 8%)); color: inherit; } @@ -494,19 +494,19 @@ onMounted(renderMarkdown); margin: var(--md-spacing-sm); overflow: auto; font-size: 0.9em; - border: 1px solid rgba(0, 0, 0, 0.1); + border: 1px solid var(--n-border-color); } .markdown-content table th, .markdown-content table td { line-height: 1.5; padding: 0.3em 0.5em; - border: 1px solid rgba(0, 0, 0, 0.1); + border: 1px solid var(--n-border-color); color: inherit; } .markdown-content table th { - background-color: rgba(0, 0, 0, 0.02); + background-color: var(--n-color-embedded, color-mix(in srgb, var(--n-color) 92%, var(--n-text-color) 8%)); font-weight: 600; } @@ -544,7 +544,7 @@ onMounted(renderMarkdown); .markdown-content a { text-decoration: none; transition: color 0.2s ease; /* 平滑颜色变化 */ - color: #18a058; + color: var(--n-primary-color); } .markdown-content a:hover { diff --git a/packages/ui/src/components/ModelParameterEditor.vue b/packages/ui/src/components/ModelParameterEditor.vue index d0b30871..15e2d022 100644 --- a/packages/ui/src/components/ModelParameterEditor.vue +++ b/packages/ui/src/components/ModelParameterEditor.vue @@ -152,7 +152,7 @@ fontSize: '12px', color: paramOverrides[entry.key] !== null && typeof paramOverrides[entry.key] === 'object' ? undefined - : 'var(--n-color-error, #d03050)' + : 'var(--n-error-color)' }" > {{ @@ -308,7 +308,7 @@ fontSize: '12px', color: paramOverrides[entry.key] !== null && typeof paramOverrides[entry.key] === 'object' ? undefined - : 'var(--n-color-error, #d03050)' + : 'var(--n-error-color)' }" > {{ diff --git a/packages/ui/src/components/OutputDisplay.vue b/packages/ui/src/components/OutputDisplay.vue index 6a46db89..ba9d6d84 100644 --- a/packages/ui/src/components/OutputDisplay.vue +++ b/packages/ui/src/components/OutputDisplay.vue @@ -169,35 +169,43 @@ defineExpose({ forceRefreshContent, forceExitEditing }); @reference "../styles/index.css"; .output-display { - @apply flex flex-col h-full border border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800 relative; + @apply flex flex-col h-full rounded-lg relative; + border: 1px solid var(--n-border-color); + background: var(--n-card-color, var(--n-color)); + color: var(--n-text-color); } /* 悬浮工具栏样式 */ .floating-toolbar { - @apply absolute top-2 right-2 z-10 flex items-center gap-2 px-3 py-2 rounded-lg shadow-lg; - background: rgba(255, 255, 255, 0.8); + @apply absolute top-2 right-2 z-10 flex items-center gap-2 px-3 py-2 rounded-lg; + background: color-mix(in srgb, var(--n-popover-color, var(--n-color)) 88%, transparent); backdrop-filter: blur(8px); - border: 1px solid rgba(0, 0, 0, 0.1); + border: 1px solid var(--n-border-color); + box-shadow: var(--n-box-shadow-2, 0 8px 20px color-mix(in srgb, var(--n-text-color) 12%, transparent)); } /* 全屏模式下的悬浮复制按钮 */ .floating-copy-btn { - @apply absolute top-2 right-2 z-10 flex items-center gap-2 px-3 py-2 rounded-lg shadow-lg; - background: rgba(255, 255, 255, 0.8); + @apply absolute top-2 right-2 z-10 flex items-center gap-2 px-3 py-2 rounded-lg; + background: color-mix(in srgb, var(--n-popover-color, var(--n-color)) 88%, transparent); backdrop-filter: blur(8px); - border: 1px solid rgba(0, 0, 0, 0.1); -} - -.dark .floating-toolbar, -.dark .floating-copy-btn { - background: rgba(31, 41, 55, 0.8); - border: 1px solid rgba(255, 255, 255, 0.1); + border: 1px solid var(--n-border-color); + box-shadow: var(--n-box-shadow-2, 0 8px 20px color-mix(in srgb, var(--n-text-color) 12%, transparent)); } .toolbar-btn { @apply flex items-center gap-1 px-2 py-1 text-sm rounded transition-colors; - @apply hover:bg-gray-100 dark:hover:bg-gray-700; - @apply focus:outline-hidden focus:ring-2 focus:ring-blue-500/50; + color: var(--n-text-color-2); +} + +.toolbar-btn:hover { + background: var(--n-hover-color); + color: var(--n-text-color); +} + +.toolbar-btn:focus-visible { + outline: none; + box-shadow: 0 0 0 2px var(--n-primary-color-suppl); } /* 悬浮工具栏动画 */ @@ -225,7 +233,11 @@ defineExpose({ forceRefreshContent, forceExitEditing }); } .reasoning-header { - @apply flex justify-between items-center cursor-pointer p-2 rounded hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors; + @apply flex justify-between items-center cursor-pointer p-2 rounded transition-colors; +} + +.reasoning-header:hover { + background: var(--n-hover-color); } .reasoning-toggle { @@ -273,7 +285,8 @@ defineExpose({ forceRefreshContent, forceExitEditing }); .loading-placeholder, .empty-placeholder { - @apply flex items-center justify-center h-full text-gray-500 text-sm italic; + @apply flex items-center justify-center h-full text-sm italic; + color: var(--n-text-color-3); } .edit-actions { @@ -285,11 +298,13 @@ defineExpose({ forceRefreshContent, forceExitEditing }); } .output-display--editable .output-display__content { - @apply border border-dashed border-gray-300 dark:border-gray-600; + @apply border border-dashed; + border-color: var(--n-border-color); } .output-display--editing .output-display__content { - @apply border-solid border-blue-500; + @apply border-solid; + border-color: var(--n-primary-color); } /* 隐藏滚动条但保持可滚动 */ diff --git a/packages/ui/src/components/PromptPanel.vue b/packages/ui/src/components/PromptPanel.vue index c9daf91c..02369bab 100644 --- a/packages/ui/src/components/PromptPanel.vue +++ b/packages/ui/src/components/PromptPanel.vue @@ -49,24 +49,21 @@ V{{ version.version }} - - - {{ t("prompt.originalVersionTooltip") }} - + + + {{ t("prompt.originalVersion") }} + + @@ -304,13 +301,15 @@ + + diff --git a/packages/ui/src/components/SaveTestResultExampleButton.vue b/packages/ui/src/components/SaveTestResultExampleButton.vue index 5d243e11..8b458b28 100644 --- a/packages/ui/src/components/SaveTestResultExampleButton.vue +++ b/packages/ui/src/components/SaveTestResultExampleButton.vue @@ -1,28 +1,25 @@ diff --git a/packages/ui/src/components/app-layout/PromptOptimizerApp.vue b/packages/ui/src/components/app-layout/PromptOptimizerApp.vue index 63ffd5f4..243d9d6d 100644 --- a/packages/ui/src/components/app-layout/PromptOptimizerApp.vue +++ b/packages/ui/src/components/app-layout/PromptOptimizerApp.vue @@ -17,15 +17,15 @@ :hljs="hljsInstance" >
-
-

{{ t("log.info.initializing") }}

+ + {{ t("log.info.initializing") }}
-

{{ t("toast.error.appInitFailed") }}

+
-
-

{{ t("log.info.initializing") }}

+ + {{ t("log.info.initializing") }}