From f6b37652fdc9d6b526c6fff20910d9aae8f342f1 Mon Sep 17 00:00:00 2001 From: Kuroome Date: Wed, 1 Jul 2026 17:56:56 +0800 Subject: [PATCH 1/2] feat:redesign-model-cards - add model-id-copy-button - redo model-cards - complete some model provider's logos and sections --- apps/web/src/assets/icons/bytedance.svg | 1 + apps/web/src/assets/icons/glm-dark.svg | 3 + apps/web/src/assets/icons/glm-light.svg | 3 + apps/web/src/assets/icons/glm.svg | 1 - apps/web/src/assets/icons/hunyuan.svg | 1 + apps/web/src/assets/icons/longcat.svg | 1 + apps/web/src/assets/icons/stepfun.svg | 1 + apps/web/src/assets/icons/volcengine.svg | 1 + apps/web/src/assets/icons/wenxin.svg | 1 + apps/web/src/assets/icons/xiaomimimo-dark.svg | 1 + .../web/src/assets/icons/xiaomimimo-light.svg | 1 + .../features/system/SystemPage.module.scss | 99 ++++++++++++++--- apps/web/src/features/system/SystemPage.tsx | 105 ++++++++++++++---- apps/web/src/utils/models.ts | 18 ++- 14 files changed, 197 insertions(+), 40 deletions(-) create mode 100644 apps/web/src/assets/icons/bytedance.svg create mode 100644 apps/web/src/assets/icons/glm-dark.svg create mode 100644 apps/web/src/assets/icons/glm-light.svg delete mode 100644 apps/web/src/assets/icons/glm.svg create mode 100644 apps/web/src/assets/icons/hunyuan.svg create mode 100644 apps/web/src/assets/icons/longcat.svg create mode 100644 apps/web/src/assets/icons/stepfun.svg create mode 100644 apps/web/src/assets/icons/volcengine.svg create mode 100644 apps/web/src/assets/icons/wenxin.svg create mode 100644 apps/web/src/assets/icons/xiaomimimo-dark.svg create mode 100644 apps/web/src/assets/icons/xiaomimimo-light.svg diff --git a/apps/web/src/assets/icons/bytedance.svg b/apps/web/src/assets/icons/bytedance.svg new file mode 100644 index 00000000..6921097d --- /dev/null +++ b/apps/web/src/assets/icons/bytedance.svg @@ -0,0 +1 @@ +ByteDance \ No newline at end of file diff --git a/apps/web/src/assets/icons/glm-dark.svg b/apps/web/src/assets/icons/glm-dark.svg new file mode 100644 index 00000000..84b66599 --- /dev/null +++ b/apps/web/src/assets/icons/glm-dark.svg @@ -0,0 +1,3 @@ + diff --git a/apps/web/src/assets/icons/glm-light.svg b/apps/web/src/assets/icons/glm-light.svg new file mode 100644 index 00000000..f819e163 --- /dev/null +++ b/apps/web/src/assets/icons/glm-light.svg @@ -0,0 +1,3 @@ + diff --git a/apps/web/src/assets/icons/glm.svg b/apps/web/src/assets/icons/glm.svg deleted file mode 100644 index 0c6e61ce..00000000 --- a/apps/web/src/assets/icons/glm.svg +++ /dev/null @@ -1 +0,0 @@ -Zhipu \ No newline at end of file diff --git a/apps/web/src/assets/icons/hunyuan.svg b/apps/web/src/assets/icons/hunyuan.svg new file mode 100644 index 00000000..42edd6cd --- /dev/null +++ b/apps/web/src/assets/icons/hunyuan.svg @@ -0,0 +1 @@ +Hunyuan \ No newline at end of file diff --git a/apps/web/src/assets/icons/longcat.svg b/apps/web/src/assets/icons/longcat.svg new file mode 100644 index 00000000..fde6c8a3 --- /dev/null +++ b/apps/web/src/assets/icons/longcat.svg @@ -0,0 +1 @@ +LongCat \ No newline at end of file diff --git a/apps/web/src/assets/icons/stepfun.svg b/apps/web/src/assets/icons/stepfun.svg new file mode 100644 index 00000000..920e8a60 --- /dev/null +++ b/apps/web/src/assets/icons/stepfun.svg @@ -0,0 +1 @@ +Stepfun \ No newline at end of file diff --git a/apps/web/src/assets/icons/volcengine.svg b/apps/web/src/assets/icons/volcengine.svg new file mode 100644 index 00000000..ecf6d75e --- /dev/null +++ b/apps/web/src/assets/icons/volcengine.svg @@ -0,0 +1 @@ +Volcengine \ No newline at end of file diff --git a/apps/web/src/assets/icons/wenxin.svg b/apps/web/src/assets/icons/wenxin.svg new file mode 100644 index 00000000..d4a87792 --- /dev/null +++ b/apps/web/src/assets/icons/wenxin.svg @@ -0,0 +1 @@ +Wenxin \ No newline at end of file diff --git a/apps/web/src/assets/icons/xiaomimimo-dark.svg b/apps/web/src/assets/icons/xiaomimimo-dark.svg new file mode 100644 index 00000000..9785e2de --- /dev/null +++ b/apps/web/src/assets/icons/xiaomimimo-dark.svg @@ -0,0 +1 @@ +XiaomiMiMo \ No newline at end of file diff --git a/apps/web/src/assets/icons/xiaomimimo-light.svg b/apps/web/src/assets/icons/xiaomimimo-light.svg new file mode 100644 index 00000000..50193d3c --- /dev/null +++ b/apps/web/src/assets/icons/xiaomimimo-light.svg @@ -0,0 +1 @@ +XiaomiMiMo \ No newline at end of file diff --git a/apps/web/src/features/system/SystemPage.module.scss b/apps/web/src/features/system/SystemPage.module.scss index c005a570..2f2f7363 100644 --- a/apps/web/src/features/system/SystemPage.module.scss +++ b/apps/web/src/features/system/SystemPage.module.scss @@ -76,41 +76,98 @@ } } -.modelTags { - display: flex; - flex-wrap: wrap; - flex: 0 0 100%; - gap: $spacing-sm; +.modelsGrid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + gap: $spacing-md; +} + +.modelCategoryCard { + border: 1px solid var(--border-subtle); + border-radius: $radius-lg; + background-color: var(--surface-subtle); + overflow: hidden; + transition: border-color 0.2s ease; + + &:hover { + border-color: var(--primary-color); + } } -.groupTitle { +.modelCategoryHeader { display: flex; align-items: center; gap: $spacing-sm; + padding: $spacing-md $spacing-lg; + border-bottom: 1px solid var(--border-subtle); } -.groupIcon { - width: 18px; - height: 18px; +.modelCategoryIcon { + width: 28px; + height: 28px; flex-shrink: 0; } +.modelCategoryName { + font-size: 14px; + font-weight: 600; + color: var(--text-primary); +} + +.modelCategoryCount { + margin-left: auto; + font-size: 12px; + color: var(--text-secondary); + background-color: var(--surface-subtle-hover); + padding: 2px 8px; + border-radius: $radius-full; +} + +.modelTags { + display: flex; + flex-wrap: wrap; + gap: $spacing-xs; + padding: $spacing-md $spacing-lg; +} + .modelTag { display: inline-flex; align-items: center; - gap: $spacing-xs; - padding: 4px 10px; - border-radius: $radius-full; + gap: 6px; + padding: 5px 12px; + border-radius: $radius-md; border: 1px solid var(--border-subtle); - background-color: var(--surface-subtle); + background-color: var(--bg); font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; + color: inherit; + cursor: pointer; + transition: + background-color 0.15s ease, + border-color 0.15s ease, + box-shadow 0.15s ease; + + &:hover { + background-color: var(--surface-subtle-hover); + border-color: var(--primary-color); + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06); + } + + &:active { + transform: translateY(1px); + } +} + +.modelTagCopied { + border-color: var(--success-color); + background-color: oklch(from var(--success-color) l c h / 0.08); } .modelName { color: var(--text-primary); font-weight: 600; + font-size: 13px; } .modelAlias { @@ -118,6 +175,22 @@ font-size: 12px; } +.modelCopyIcon { + display: inline-flex; + align-items: center; + color: var(--text-secondary); + transition: color 0.15s ease; + margin-left: 2px; +} + +.modelTag:hover .modelCopyIcon { + color: var(--text-primary); +} + +.modelTagCopied .modelCopyIcon { + color: var(--success-color); +} + .versionCheck { display: flex; flex-direction: column; diff --git a/apps/web/src/features/system/SystemPage.tsx b/apps/web/src/features/system/SystemPage.tsx index 43e39784..20cd64d0 100644 --- a/apps/web/src/features/system/SystemPage.tsx +++ b/apps/web/src/features/system/SystemPage.tsx @@ -2,7 +2,14 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Card } from '@/components/ui/Card'; import { Button } from '@/components/ui/Button'; -import { IconGithub, IconBookOpen, IconExternalLink, IconCode } from '@/components/ui/icons'; +import { + IconGithub, + IconBookOpen, + IconExternalLink, + IconCode, + IconCopy, + IconCheck, +} from '@/components/ui/icons'; import { useAuthStore, useConfigStore, @@ -12,6 +19,7 @@ import { } from '@/stores'; import { apiKeysApi } from '@/services/api/apiKeys'; import { classifyModels } from '@/utils/models'; +import { copyToClipboard } from '@/utils/clipboard'; import { STORAGE_KEY_AUTH, STORAGE_KEY_QUOTA_CACHE } from '@/utils/constants'; import iconGemini from '@/assets/icons/gemini.svg'; import iconClaude from '@/assets/icons/claude.svg'; @@ -20,11 +28,21 @@ import iconOpenaiDark from '@/assets/icons/openai-dark.svg'; import iconQwen from '@/assets/icons/qwen.svg'; import iconKimiLight from '@/assets/icons/kimi-light.svg'; import iconKimiDark from '@/assets/icons/kimi-dark.svg'; -import iconGlm from '@/assets/icons/glm.svg'; +import iconGlmLight from '@/assets/icons/glm-light.svg'; +import iconGlmDark from '@/assets/icons/glm-dark.svg'; import iconGrok from '@/assets/icons/grok.svg'; import iconGrokDark from '@/assets/icons/grok-dark.svg'; import iconDeepseek from '@/assets/icons/deepseek.svg'; import iconMinimax from '@/assets/icons/minimax.svg'; +import iconMimoLight from '@/assets/icons/xiaomimimo-light.svg'; +import iconMimoDark from '@/assets/icons/xiaomimimo-dark.svg'; +import iconHunyuan from '@/assets/icons/hunyuan.svg'; +import iconBytedance from '@/assets/icons/bytedance.svg'; +import iconVolcengine from '@/assets/icons/volcengine.svg'; +import iconStepfun from '@/assets/icons/stepfun.svg'; +import iconWenxin from '@/assets/icons/wenxin.svg'; +import iconLongcat from '@/assets/icons/longcat.svg'; + import styles from './SystemPage.module.scss'; const MODEL_CATEGORY_ICONS: Record = { @@ -33,10 +51,17 @@ const MODEL_CATEGORY_ICONS: Record(); + const [copiedModelName, setCopiedModelName] = useState(null); + const copiedResetTimer = useRef | null>(null); + const apiKeysCache = useRef([]); const otherLabel = useMemo( @@ -71,6 +99,29 @@ export function SystemPage() { return resolvedTheme === 'dark' ? iconEntry.dark : iconEntry.light; }; + const handleCopyModelName = async (name: string) => { + const copied = await copyToClipboard(name); + showNotification( + t(copied ? 'notification.link_copied' : 'notification.copy_failed'), + copied ? 'success' : 'error' + ); + if (!copied) return; + if (copiedResetTimer.current) { + clearTimeout(copiedResetTimer.current); + } + setCopiedModelName(name); + copiedResetTimer.current = setTimeout(() => { + setCopiedModelName(null); + copiedResetTimer.current = null; + }, 1500); + }; + + useEffect(() => { + return () => { + if (copiedResetTimer.current) clearTimeout(copiedResetTimer.current); + }; + }, []); + const normalizeApiKeyList = (input: unknown): string[] => { if (!Array.isArray(input)) return []; const seen = new Set(); @@ -277,31 +328,41 @@ export function SystemPage() { ) : models.length === 0 ? (
{t('system_info.models_empty')}
) : ( -
+
{groupedModels.map((group) => { const iconSrc = getIconForCategory(group.id); return ( -
-
-
- {iconSrc && } - {group.label} -
-
+
+
+ {iconSrc && ( + + )} + {group.label} + {t('system_info.models_count', { count: group.items.length })} -
+
- {group.items.map((model) => ( - - {model.name} - {model.alias && {model.alias}} - - ))} + {group.items.map((model) => { + const isCopied = copiedModelName === model.name; + return ( + + ); + })}
); diff --git a/apps/web/src/utils/models.ts b/apps/web/src/utils/models.ts index 5049fa0e..9bc3c976 100644 --- a/apps/web/src/utils/models.ts +++ b/apps/web/src/utils/models.ts @@ -15,10 +15,17 @@ const MODEL_CATEGORIES = [ { id: 'gemini', label: 'Gemini', patterns: [/gemini/i, /\bgai\b/i] }, { id: 'kimi', label: 'Kimi', patterns: [/kimi/i] }, { id: 'qwen', label: 'Qwen', patterns: [/qwen/i] }, - { id: 'glm', label: 'GLM', patterns: [/glm/i, /chatglm/i] }, + { id: 'glm', label: 'ZHIPU', patterns: [/glm/i, /chatglm/i] }, { id: 'grok', label: 'Grok', patterns: [/grok/i] }, { id: 'deepseek', label: 'DeepSeek', patterns: [/deepseek/i] }, - { id: 'minimax', label: 'MiniMax', patterns: [/minimax/i, /abab/i] } + { id: 'minimax', label: 'MiniMax', patterns: [/minimax/i, /abab/i] }, + { id: 'mimo', label: 'Mimo', patterns: [/mimo/i] }, + { id: 'hunyuan', label: 'HunYuan', patterns: [/hunyuan/i] }, + { id: 'bytedance', label: 'ByteDance', patterns: [/seed/i, /doubao/i] }, + { id: 'volcengine', label: 'VolcEngine', patterns: [/ark/i] }, + { id: 'longcat', label: 'LongCat', patterns: [/longcat/i] }, + { id: 'stepfun', label: 'StepFun', patterns: [/stepfun/i] }, + { id: 'wenxin', label: 'WenXin', patterns: [/wenxin/i] }, ]; const matchCategory = (text: string) => { @@ -90,11 +97,14 @@ export interface ModelGroup { items: ModelInfo[]; } -export function classifyModels(models: ModelInfo[] = [], { otherLabel = 'Other' } = {}): ModelGroup[] { +export function classifyModels( + models: ModelInfo[] = [], + { otherLabel = 'Other' } = {} +): ModelGroup[] { const groups: ModelGroup[] = MODEL_CATEGORIES.map((category) => ({ id: category.id, label: category.label, - items: [] + items: [], })); const otherGroup: ModelGroup = { id: 'other', label: otherLabel, items: [] }; From c1200cf77cd9f02205b084c77fca6b151178a6bb Mon Sep 17 00:00:00 2001 From: Kuroome Date: Wed, 1 Jul 2026 18:35:10 +0800 Subject: [PATCH 2/2] fix(frontend): address model-cards pr review feedback - Tighten ByteDance/VolcEngine regex patterns with word boundaries - Use dedicated model_id_copied translation key (add to 4 locales) - Add aria-label to model copy button for accessibility - Replace oklch() with color-mix for codebase consistency --- apps/web/src/features/system/SystemPage.module.scss | 2 +- apps/web/src/features/system/SystemPage.tsx | 3 ++- apps/web/src/i18n/locales/en.json | 3 ++- apps/web/src/i18n/locales/ru.json | 3 ++- apps/web/src/i18n/locales/zh-CN.json | 3 ++- apps/web/src/i18n/locales/zh-TW.json | 3 ++- apps/web/src/utils/models.ts | 4 ++-- 7 files changed, 13 insertions(+), 8 deletions(-) diff --git a/apps/web/src/features/system/SystemPage.module.scss b/apps/web/src/features/system/SystemPage.module.scss index 2f2f7363..26a5ea71 100644 --- a/apps/web/src/features/system/SystemPage.module.scss +++ b/apps/web/src/features/system/SystemPage.module.scss @@ -161,7 +161,7 @@ .modelTagCopied { border-color: var(--success-color); - background-color: oklch(from var(--success-color) l c h / 0.08); + background: color-mix(in srgb, var(--success-color) 8%, var(--bg)); } .modelName { diff --git a/apps/web/src/features/system/SystemPage.tsx b/apps/web/src/features/system/SystemPage.tsx index 20cd64d0..c418836f 100644 --- a/apps/web/src/features/system/SystemPage.tsx +++ b/apps/web/src/features/system/SystemPage.tsx @@ -102,7 +102,7 @@ export function SystemPage() { const handleCopyModelName = async (name: string) => { const copied = await copyToClipboard(name); showNotification( - t(copied ? 'notification.link_copied' : 'notification.copy_failed'), + t(copied ? 'notification.model_id_copied' : 'notification.copy_failed'), copied ? 'success' : 'error' ); if (!copied) return; @@ -351,6 +351,7 @@ export function SystemPage() { type="button" className={`${styles.modelTag}${isCopied ? ` ${styles.modelTagCopied}` : ''}`} title={model.description || model.name} + aria-label={t('common.copy') + ' ' + model.name} onClick={() => handleCopyModelName(model.name)} > {model.name} diff --git a/apps/web/src/i18n/locales/en.json b/apps/web/src/i18n/locales/en.json index a4991466..93919b85 100644 --- a/apps/web/src/i18n/locales/en.json +++ b/apps/web/src/i18n/locales/en.json @@ -3297,7 +3297,8 @@ "claude_api_key": "Claude API key", "commercial_mode_restart_required": "Commercial mode setting changed. Please restart the service for it to take effect", "copy_failed": "Copy failed", - "link_copied": "Link copied to clipboard" + "link_copied": "Link copied to clipboard", + "model_id_copied": "Model ID copied" }, "language": { "switch": "Language", diff --git a/apps/web/src/i18n/locales/ru.json b/apps/web/src/i18n/locales/ru.json index 90a39d9c..4f34010c 100644 --- a/apps/web/src/i18n/locales/ru.json +++ b/apps/web/src/i18n/locales/ru.json @@ -3293,7 +3293,8 @@ "claude_api_key": "API-ключ Claude", "commercial_mode_restart_required": "Режим коммерческого использования изменён. Перезапустите сервис, чтобы применить изменения", "copy_failed": "Не удалось скопировать", - "link_copied": "Ссылка скопирована в буфер обмена" + "link_copied": "Ссылка скопирована в буфер обмена", + "model_id_copied": "ID модели скопирован" }, "language": { "switch": "Язык", diff --git a/apps/web/src/i18n/locales/zh-CN.json b/apps/web/src/i18n/locales/zh-CN.json index 3929f6f8..03f7fcc2 100644 --- a/apps/web/src/i18n/locales/zh-CN.json +++ b/apps/web/src/i18n/locales/zh-CN.json @@ -3243,7 +3243,8 @@ "claude_api_key": "Claude API密钥", "commercial_mode_restart_required": "商业模式开关已变更,请重启服务后生效", "copy_failed": "复制失败", - "link_copied": "已复制" + "link_copied": "已复制", + "model_id_copied": "模型 ID 已复制" }, "language": { "switch": "语言", diff --git a/apps/web/src/i18n/locales/zh-TW.json b/apps/web/src/i18n/locales/zh-TW.json index 64cd6a90..fb170d81 100644 --- a/apps/web/src/i18n/locales/zh-TW.json +++ b/apps/web/src/i18n/locales/zh-TW.json @@ -3268,7 +3268,8 @@ "claude_api_key": "Claude API 金鑰", "commercial_mode_restart_required": "商業模式開關已變更,請重啟服務後生效", "copy_failed": "複製失敗", - "link_copied": "已複製" + "link_copied": "已複製", + "model_id_copied": "模型 ID 已複製" }, "language": { "switch": "語言", diff --git a/apps/web/src/utils/models.ts b/apps/web/src/utils/models.ts index 9bc3c976..c1b27c06 100644 --- a/apps/web/src/utils/models.ts +++ b/apps/web/src/utils/models.ts @@ -21,8 +21,8 @@ const MODEL_CATEGORIES = [ { id: 'minimax', label: 'MiniMax', patterns: [/minimax/i, /abab/i] }, { id: 'mimo', label: 'Mimo', patterns: [/mimo/i] }, { id: 'hunyuan', label: 'HunYuan', patterns: [/hunyuan/i] }, - { id: 'bytedance', label: 'ByteDance', patterns: [/seed/i, /doubao/i] }, - { id: 'volcengine', label: 'VolcEngine', patterns: [/ark/i] }, + { id: 'bytedance', label: 'ByteDance', patterns: [/\bseed\b/i, /doubao/i] }, + { id: 'volcengine', label: 'VolcEngine', patterns: [/\bark\b/i] }, { id: 'longcat', label: 'LongCat', patterns: [/longcat/i] }, { id: 'stepfun', label: 'StepFun', patterns: [/stepfun/i] }, { id: 'wenxin', label: 'WenXin', patterns: [/wenxin/i] },