From 3e55d601a1faebc2d7a52626049178c9e63eae08 Mon Sep 17 00:00:00 2001 From: thanhtunguet Date: Sat, 31 Jan 2026 21:04:34 +0700 Subject: [PATCH] feat: enhance OAuth model alias management with new UI components and localization updates --- src/components/common/ConfirmationModal.tsx | 6 +- .../ModelMappingDiagram.module.scss | 323 +++++++ .../modelAlias/ModelMappingDiagram.tsx | 902 ++++++++++++++++++ src/components/modelAlias/index.ts | 2 + src/components/ui/Modal.tsx | 4 +- src/i18n/locales/en.json | 31 + src/i18n/locales/zh-CN.json | 31 + src/pages/AuthFilesPage.module.scss | 43 + src/pages/AuthFilesPage.tsx | 376 +++++++- src/stores/useNotificationStore.ts | 3 +- 10 files changed, 1692 insertions(+), 29 deletions(-) create mode 100644 src/components/modelAlias/ModelMappingDiagram.module.scss create mode 100644 src/components/modelAlias/ModelMappingDiagram.tsx create mode 100644 src/components/modelAlias/index.ts diff --git a/src/components/common/ConfirmationModal.tsx b/src/components/common/ConfirmationModal.tsx index 9a8ca0b..6471fc2 100644 --- a/src/components/common/ConfirmationModal.tsx +++ b/src/components/common/ConfirmationModal.tsx @@ -43,7 +43,11 @@ export function ConfirmationModal() { return ( -

{message}

+ {typeof message === 'string' ? ( +

{message}

+ ) : ( +
{message}
+ )}
+ + {provider} + + {sources.length} +
+ ); + })} + + + {/* Column 2: Source Models (children per provider; hidden when provider collapsed) */} +
{ + e.preventDefault(); + e.stopPropagation(); + handleContextMenu(e, 'background'); + }} + > +
{t('oauth_model_alias.diagram_source_models')}
+ {providerNodes.flatMap(({ provider, sources }) => { + if (collapsedProviders.has(provider)) return []; + return sources.map((source) => ( +
{ + if (el) sourceRefs.current.set(source.id, el); + else sourceRefs.current.delete(source.id); + }} + className={`${styles.item} ${styles.sourceItem} ${ + draggedSource?.id === source.id ? styles.dragging : '' + } ${dropTargetSource === source.id ? styles.dropTarget : ''}`} + draggable={!!onUpdate} + onDragStart={(e) => handleDragStart(e, source)} + onDragEnd={() => { + setDraggedSource(null); + setDropTargetAlias(null); + }} + onDragOver={(e) => handleDragOverSource(e, source)} + onDragLeave={handleDragLeaveSource} + onDrop={(e) => handleDropOnSource(e, source)} + onContextMenu={(e) => { + e.preventDefault(); + e.stopPropagation(); + handleContextMenu(e, 'source', source.id); + }} + > + + {source.name} + +
0 ? 1 : 0.3 + }} + /> +
+ )); + })} +
+ +
{ + e.preventDefault(); + e.stopPropagation(); + handleContextMenu(e, 'background'); + }} + > +
{t('oauth_model_alias.diagram_aliases')}
+ {aliasNodes.map((node) => ( +
{ + if (el) aliasRefs.current.set(node.id, el); + else aliasRefs.current.delete(node.id); + }} + className={`${styles.item} ${styles.aliasItem} ${ + dropTargetAlias === node.alias ? styles.dropTarget : '' + } ${draggedAlias === node.alias ? styles.dragging : ''}`} + draggable={!!onUpdate} + onDragStart={(e) => handleDragStartAlias(e, node.alias)} + onDragEnd={() => { + setDraggedAlias(null); + setDropTargetSource(null); + }} + onDragOver={(e) => handleDragOver(e, node.alias)} + onDragLeave={handleDragLeave} + onDrop={(e) => handleDrop(e, node.alias)} + onContextMenu={(e) => { + e.preventDefault(); + e.stopPropagation(); + handleContextMenu(e, 'alias', node.alias); + }} + > +
+ + {node.alias} + + {node.sources.length} +
+ ))} +
+ + {contextMenu && + createPortal( +
e.stopPropagation()} + > + {contextMenu.type === 'background' && ( +
+ {t('oauth_model_alias.diagram_add_alias')} +
+ )} + {contextMenu.type === 'alias' && renderAliasMenu()} + {contextMenu.type === 'provider' && renderProviderMenu()} + {contextMenu.type === 'source' && renderSourceMenu()} +
, + document.body + )} + + setRenameState(null)} + title={t('oauth_model_alias.diagram_rename_alias_title')} + width={400} + footer={ + <> + + + + } + > + { + setRenameValue(e.target.value); + setRenameError(''); + }} + onKeyDown={(e) => { + if (e.key === 'Enter') handleRenameSubmit(); + }} + error={renameError} + placeholder={t('oauth_model_alias.diagram_rename_placeholder')} + autoFocus + /> + + + setAddAliasOpen(false)} + title={t('oauth_model_alias.diagram_add_alias_title')} + width={400} + footer={ + <> + + + + } + > + { + setAddAliasValue(e.target.value); + setAddAliasError(''); + }} + onKeyDown={(e) => { + if (e.key === 'Enter') handleAddAliasSubmit(); + }} + error={addAliasError} + placeholder={t('oauth_model_alias.diagram_add_placeholder')} + autoFocus + /> + + + setSettingsAlias(null)} + title={t('oauth_model_alias.diagram_settings_title', { alias: settingsAlias ?? '' })} + width={720} + footer={ + + } + > + {settingsAlias ? ( + (() => { + const node = aliasNodes.find((n) => n.alias === settingsAlias); + if (!node || node.sources.length === 0) { + return
{t('oauth_model_alias.diagram_settings_empty')}
; + } + return ( +
+ {node.sources.map((source) => { + const entry = source.aliases.find((item) => item.alias === settingsAlias); + const forkEnabled = entry?.fork === true; + return ( +
+
+ {source.name} + + {settingsAlias} +
+
+ + {t('oauth_model_alias.alias_fork_label')} + + handleToggleFork(source.provider, source.name, settingsAlias, value)} + ariaLabel={t('oauth_model_alias.alias_fork_label')} + /> + +
+
+ ); + })} +
+ ); + })() + ) : null} +
+ + setSettingsSourceId(null)} + title={t('oauth_model_alias.diagram_settings_source_title')} + width={720} + footer={ + + } + > + {settingsSourceId ? ( + (() => { + const source = resolveSourceById(settingsSourceId); + if (!source || source.aliases.length === 0) { + return
{t('oauth_model_alias.diagram_settings_empty')}
; + } + return ( +
+ {source.aliases.map((entry) => ( +
+
+ {source.name} + + {entry.alias} +
+
+ + {t('oauth_model_alias.alias_fork_label')} + + + handleToggleFork(source.provider, source.name, entry.alias, value) + } + ariaLabel={t('oauth_model_alias.alias_fork_label')} + /> + +
+
+ ))} +
+ ); + })() + ) : null} +
+
+ ); +}); diff --git a/src/components/modelAlias/index.ts b/src/components/modelAlias/index.ts new file mode 100644 index 0000000..6375f4e --- /dev/null +++ b/src/components/modelAlias/index.ts @@ -0,0 +1,2 @@ +export { ModelMappingDiagram } from './ModelMappingDiagram'; +export type { ModelMappingDiagramProps, ModelMappingDiagramRef } from './ModelMappingDiagram'; diff --git a/src/components/ui/Modal.tsx b/src/components/ui/Modal.tsx index f129c89..45eff25 100644 --- a/src/components/ui/Modal.tsx +++ b/src/components/ui/Modal.tsx @@ -8,6 +8,7 @@ interface ModalProps { onClose: () => void; footer?: ReactNode; width?: number | string; + className?: string; closeDisabled?: boolean; } @@ -39,6 +40,7 @@ export function Modal({ onClose, footer, width = 520, + className, closeDisabled = false, children }: PropsWithChildren) { @@ -110,7 +112,7 @@ export function Modal({ if (!open && !isVisible) return null; const overlayClass = `modal-overlay ${isClosing ? 'modal-overlay-closing' : 'modal-overlay-entering'}`; - const modalClass = `modal ${isClosing ? 'modal-closing' : 'modal-entering'}`; + const modalClass = `modal ${isClosing ? 'modal-closing' : 'modal-entering'}${className ? ` ${className}` : ''}`; const modalContent = (
diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 6998856..cb20b89 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -543,11 +543,42 @@ "save_failed": "Failed to update model aliases", "delete": "Delete Provider", "delete_confirm": "Delete model aliases for {{provider}}?", + "delete_link_title": "Unlink mapping", + "delete_link_confirm": "Unlink mapping from {{sourceModel}} ({{provider}}) to alias {{alias}}?", + "delete_alias_title": "Delete Alias", + "delete_alias_confirm": "Delete alias {{alias}} and unmap all associated models?", "delete_success": "Model aliases removed", "delete_failed": "Failed to delete model aliases", "no_models": "No model aliases", "model_count": "{{count}} aliases", "list_empty_all": "No model aliases yet—use “Add Alias” to create one.", + "chart_title": "All mappings overview", + "diagram_providers": "Providers", + "diagram_source_models": "Source Models", + "diagram_aliases": "Aliases", + "diagram_expand": "Expand", + "diagram_collapse": "Collapse", + "diagram_add_alias": "Add Alias", + "diagram_rename": "Rename", + "diagram_rename_alias_title": "Rename alias", + "diagram_rename_alias_label": "New alias name", + "diagram_rename_placeholder": "Enter alias name...", + "diagram_delete_link": "Unlink from {{provider}} / {{name}}", + "diagram_delete_alias": "Delete alias", + "diagram_please_enter_alias": "Please enter an alias name.", + "diagram_alias_exists": "This alias already exists.", + "diagram_add_alias_title": "Add alias", + "diagram_add_alias_label": "Alias name", + "diagram_add_placeholder": "Enter new alias name...", + "diagram_rename_btn": "Rename", + "diagram_add_btn": "Add", + "diagram_settings": "Settings", + "diagram_settings_title": "Alias settings — {{alias}}", + "diagram_settings_source_title": "Source model settings", + "diagram_settings_empty": "No mappings for this alias yet.", + "view_mode": "View mode", + "view_mode_diagram": "Diagram", + "view_mode_list": "List", "provider_required": "Please enter a provider first", "upgrade_required": "This feature requires a newer CLI Proxy API (CPA) version. Please upgrade.", "upgrade_required_title": "Please upgrade CLI Proxy API", diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index c69d536..1f9d728 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -543,11 +543,42 @@ "save_failed": "更新模型别名失败", "delete": "删除提供商", "delete_confirm": "确定要删除 {{provider}} 的模型别名吗?", + "delete_link_title": "取消链接", + "delete_link_confirm": "确定取消 {{sourceModel}}({{provider}})到别名 {{alias}} 的映射?", + "delete_alias_title": "删除别名", + "delete_alias_confirm": "确定删除别名 {{alias}} 并取消所有关联模型的映射?", "delete_success": "已删除该提供商的模型别名", "delete_failed": "删除模型别名失败", "no_models": "未配置模型别名", "model_count": "{{count}} 条别名", "list_empty_all": "暂无任何提供商的模型别名,点击“新增别名”创建。", + "chart_title": "全部映射概览", + "diagram_providers": "提供商", + "diagram_source_models": "源模型", + "diagram_aliases": "别名", + "diagram_expand": "展开", + "diagram_collapse": "收起", + "diagram_add_alias": "添加别名", + "diagram_rename": "重命名", + "diagram_rename_alias_title": "重命名别名", + "diagram_rename_alias_label": "新别名名称", + "diagram_rename_placeholder": "输入别名名称...", + "diagram_delete_link": "取消链接 {{provider}} / {{name}}", + "diagram_delete_alias": "删除别名", + "diagram_please_enter_alias": "请输入别名名称。", + "diagram_alias_exists": "该别名已存在。", + "diagram_add_alias_title": "添加别名", + "diagram_add_alias_label": "别名名称", + "diagram_add_placeholder": "输入新别名名称...", + "diagram_rename_btn": "重命名", + "diagram_add_btn": "添加", + "diagram_settings": "设置", + "diagram_settings_title": "别名设置 — {{alias}}", + "diagram_settings_source_title": "源模型设置", + "diagram_settings_empty": "该别名暂无映射。", + "view_mode": "视图模式", + "view_mode_diagram": "Diagram", + "view_mode_list": "List", "provider_required": "请先填写提供商名称", "upgrade_required": "当前 CPA 版本不支持模型别名功能,请升级 CPA 版本", "upgrade_required_title": "需要升级 CPA 版本", diff --git a/src/pages/AuthFilesPage.module.scss b/src/pages/AuthFilesPage.module.scss index 7ce60d2..6bfb681 100644 --- a/src/pages/AuthFilesPage.module.scss +++ b/src/pages/AuthFilesPage.module.scss @@ -843,6 +843,49 @@ } } +// OAuth 模型别名 - 映射概览 +.aliasChartSection { + margin-bottom: $spacing-lg; + padding-bottom: $spacing-lg; + border-bottom: 1px solid var(--border-color); +} + +.aliasChartHeader { + display: flex; + align-items: center; + justify-content: space-between; + gap: $spacing-sm; + margin-bottom: $spacing-sm; +} + +.aliasChartTitle { + margin: 0; + font-size: 14px; + font-weight: 600; + color: var(--text-secondary); +} + +.aliasChart { + width: 100%; + min-height: 120px; +} + +.cardExtraButtons { + display: flex; + gap: $spacing-sm; + align-items: center; +} + +.viewModeSwitch { + display: inline-flex; + align-items: center; + gap: $spacing-xs; + padding: 2px; + border-radius: $radius-md; + background: var(--bg-secondary); + border: 1px solid var(--border-color); +} + // OAuth 模型映射表单 .mappingRow { display: grid; diff --git a/src/pages/AuthFilesPage.tsx b/src/pages/AuthFilesPage.tsx index abe76d1..09ed9b5 100644 --- a/src/pages/AuthFilesPage.tsx +++ b/src/pages/AuthFilesPage.tsx @@ -1,5 +1,5 @@ import { useEffect, useMemo, useRef, useState, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; +import { Trans, useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import { useInterval } from '@/hooks/useInterval'; import { useHeaderRefresh } from '@/hooks/useHeaderRefresh'; @@ -10,9 +10,11 @@ import { Input } from '@/components/ui/Input'; import { Modal } from '@/components/ui/Modal'; import { EmptyState } from '@/components/ui/EmptyState'; import { ToggleSwitch } from '@/components/ui/ToggleSwitch'; +import { ModelMappingDiagram, type ModelMappingDiagramRef } from '@/components/modelAlias'; import { IconBot, IconCode, + IconChevronUp, IconDownload, IconInfo, IconTrash2, @@ -230,6 +232,10 @@ export function AuthFilesPage() { // OAuth 模型映射相关 const [modelAlias, setModelAlias] = useState>({}); const [modelAliasError, setModelAliasError] = useState<'unsupported' | null>(null); + const [allProviderModels, setAllProviderModels] = useState>( + {} + ); + const [viewMode, setViewMode] = useState<'diagram' | 'list'>('list'); const [prefixProxyEditor, setPrefixProxyEditor] = useState(null); @@ -237,11 +243,72 @@ export function AuthFilesPage() { const loadingKeyStatsRef = useRef(false); const excludedUnsupportedRef = useRef(false); const mappingsUnsupportedRef = useRef(false); + const diagramRef = useRef(null); const normalizeProviderKey = (value: string) => value.trim().toLowerCase(); const disableControls = connectionStatus !== 'connected'; + useEffect(() => { + let cancelled = false; + + const loadAllModels = async () => { + const providers = new Set(); + + Object.keys(modelAlias).forEach((provider) => { + const key = provider.trim().toLowerCase(); + if (key) providers.add(key); + }); + + files.forEach((file) => { + if (typeof file.type === 'string') { + const key = file.type.trim().toLowerCase(); + if (key) providers.add(key); + } + if (typeof file.provider === 'string') { + const key = file.provider.trim().toLowerCase(); + if (key) providers.add(key); + } + }); + + const providerList = Array.from(providers); + if (providerList.length === 0) { + if (!cancelled) setAllProviderModels({}); + return; + } + + const results = await Promise.all( + providerList.map(async (provider) => { + try { + const models = await authFilesApi.getModelDefinitions(provider); + return { provider, models }; + } catch { + return { provider, models: [] }; + } + }) + ); + + if (cancelled) return; + + const nextModels: Record = {}; + results.forEach(({ provider, models }) => { + if (models.length > 0) { + nextModels[provider] = models; + } + }); + + setAllProviderModels(nextModels); + }; + + void loadAllModels(); + + return () => { + cancelled = true; + }; + }, [files, modelAlias]); + + + useEffect(() => { const persisted = readAuthFilesUiState(); if (!persisted) return; @@ -603,7 +670,9 @@ export function AuthFilesPage() { setFiles((prev) => prev.filter((file) => isRuntimeOnlyAuthFile(file))); } else { // 删除筛选类型的文件 - const filesToDelete = files.filter((f) => f.type === filter && !isRuntimeOnlyAuthFile(f)); + const filesToDelete = files.filter( + (f) => f.type === filter && !isRuntimeOnlyAuthFile(f) + ); if (filesToDelete.length === 0) { showNotification(t('auth_files.delete_filtered_none', { type: typeLabel }), 'info'); @@ -991,6 +1060,205 @@ export function AuthFilesPage() { }); }; + const handleMappingUpdate = async (provider: string, sourceModel: string, newAlias: string) => { + if (!provider || !sourceModel || !newAlias) return; + const normalizedProvider = normalizeProviderKey(provider); + if (!normalizedProvider) return; + + const providerKey = Object.keys(modelAlias).find( + (key) => normalizeProviderKey(key) === normalizedProvider + ); + const currentMappings = (providerKey ? modelAlias[providerKey] : null) ?? []; + + const nameTrim = sourceModel.trim(); + const aliasTrim = newAlias.trim(); + const nameKey = nameTrim.toLowerCase(); + const aliasKey = aliasTrim.toLowerCase(); + + if ( + currentMappings.some( + (m) => + (m.name ?? '').trim().toLowerCase() === nameKey && + (m.alias ?? '').trim().toLowerCase() === aliasKey + ) + ) { + return; + } + + const nextMappings: OAuthModelAliasEntry[] = [ + ...currentMappings, + { name: nameTrim, alias: aliasTrim, fork: true }, + ]; + + try { + await authFilesApi.saveOauthModelAlias(normalizedProvider, nextMappings); + await loadModelAlias(); + showNotification(t('oauth_model_alias.save_success'), 'success'); + } catch (err: unknown) { + const errorMessage = err instanceof Error ? err.message : ''; + showNotification(`${t('oauth_model_alias.save_failed')}: ${errorMessage}`, 'error'); + } + }; + + const handleDeleteLink = (provider: string, sourceModel: string, alias: string) => { + const nameTrim = sourceModel.trim(); + const aliasTrim = alias.trim(); + if (!provider || !nameTrim || !aliasTrim) return; + + showConfirmation({ + title: t('oauth_model_alias.delete_link_title', { defaultValue: 'Unlink mapping' }), + message: ( + }} + /> + ), + variant: 'danger', + confirmText: t('common.confirm'), + onConfirm: async () => { + const normalizedProvider = normalizeProviderKey(provider); + const providerKey = Object.keys(modelAlias).find( + (key) => normalizeProviderKey(key) === normalizedProvider + ); + const currentMappings = (providerKey ? modelAlias[providerKey] : null) ?? []; + const nameKey = nameTrim.toLowerCase(); + const aliasKey = aliasTrim.toLowerCase(); + const nextMappings = currentMappings.filter( + (m) => + (m.name ?? '').trim().toLowerCase() !== nameKey || + (m.alias ?? '').trim().toLowerCase() !== aliasKey + ); + if (nextMappings.length === currentMappings.length) return; + + try { + if (nextMappings.length === 0) { + await authFilesApi.deleteOauthModelAlias(normalizedProvider); + } else { + await authFilesApi.saveOauthModelAlias(normalizedProvider, nextMappings); + } + await loadModelAlias(); + showNotification(t('oauth_model_alias.save_success'), 'success'); + } catch (err: unknown) { + const errorMessage = err instanceof Error ? err.message : ''; + showNotification(`${t('oauth_model_alias.save_failed')}: ${errorMessage}`, 'error'); + } + }, + }); + }; + + const handleToggleFork = async ( + provider: string, + sourceModel: string, + alias: string, + fork: boolean + ) => { + const normalizedProvider = normalizeProviderKey(provider); + if (!normalizedProvider) return; + + const providerKey = Object.keys(modelAlias).find( + (key) => normalizeProviderKey(key) === normalizedProvider + ); + const currentMappings = (providerKey ? modelAlias[providerKey] : null) ?? []; + const nameKey = sourceModel.trim().toLowerCase(); + const aliasKey = alias.trim().toLowerCase(); + let changed = false; + + const nextMappings = currentMappings.map((m) => { + const mName = (m.name ?? '').trim().toLowerCase(); + const mAlias = (m.alias ?? '').trim().toLowerCase(); + if (mName === nameKey && mAlias === aliasKey) { + changed = true; + return fork ? { ...m, fork: true } : { name: m.name, alias: m.alias }; + } + return m; + }); + + if (!changed) return; + + try { + await authFilesApi.saveOauthModelAlias(normalizedProvider, nextMappings); + await loadModelAlias(); + showNotification(t('oauth_model_alias.save_success'), 'success'); + } catch (err: unknown) { + const errorMessage = err instanceof Error ? err.message : ''; + showNotification(`${t('oauth_model_alias.save_failed')}: ${errorMessage}`, 'error'); + } + }; + + const handleRenameAlias = async (oldAlias: string, newAlias: string) => { + const oldTrim = oldAlias.trim(); + const newTrim = newAlias.trim(); + if (!oldTrim || !newTrim || oldTrim === newTrim) return; + + const oldKey = oldTrim.toLowerCase(); + const providersToUpdate = Object.entries(modelAlias).filter(([_, mappings]) => + mappings.some((m) => (m.alias ?? '').trim().toLowerCase() === oldKey) + ); + + if (providersToUpdate.length === 0) return; + + try { + await Promise.all( + providersToUpdate.map(([provider, mappings]) => { + const nextMappings = mappings.map((m) => + (m.alias ?? '').trim().toLowerCase() === oldKey ? { ...m, alias: newTrim } : m + ); + return authFilesApi.saveOauthModelAlias(provider, nextMappings); + }) + ); + await loadModelAlias(); + showNotification(t('oauth_model_alias.save_success'), 'success'); + } catch (err: unknown) { + const errorMessage = err instanceof Error ? err.message : ''; + showNotification(`${t('oauth_model_alias.save_failed')}: ${errorMessage}`, 'error'); + } + }; + + const handleDeleteAlias = (aliasName: string) => { + const aliasTrim = aliasName.trim(); + if (!aliasTrim) return; + const aliasKey = aliasTrim.toLowerCase(); + const providersToUpdate = Object.entries(modelAlias).filter(([_, mappings]) => + mappings.some((m) => (m.alias ?? '').trim().toLowerCase() === aliasKey) + ); + + if (providersToUpdate.length === 0) return; + + showConfirmation({ + title: t('oauth_model_alias.delete_alias_title', { defaultValue: 'Delete Alias' }), + message: ( + }} + /> + ), + variant: 'danger', + confirmText: t('common.confirm'), + onConfirm: async () => { + try { + await Promise.all( + providersToUpdate.map(([provider, mappings]) => { + const nextMappings = mappings.filter( + (m) => (m.alias ?? '').trim().toLowerCase() !== aliasKey + ); + if (nextMappings.length === 0) { + return authFilesApi.deleteOauthModelAlias(provider); + } + return authFilesApi.saveOauthModelAlias(provider, nextMappings); + }) + ); + await loadModelAlias(); + showNotification(t('oauth_model_alias.delete_success'), 'success'); + } catch (err: unknown) { + const errorMessage = err instanceof Error ? err.message : ''; + showNotification(`${t('oauth_model_alias.delete_failed')}: ${errorMessage}`, 'error'); + } + }, + }); + }; + // 渲染标签筛选器 const renderFilterTags = () => (
@@ -1084,22 +1352,22 @@ export function AuthFilesPage() { }; // 渲染单个认证文件卡片 - const renderFileCard = (item: AuthFileItem) => { - const fileStats = resolveAuthFileStats(item, keyStats); - const isRuntimeOnly = isRuntimeOnlyAuthFile(item); - const isAistudio = (item.type || '').toLowerCase() === 'aistudio'; - const showModelsButton = !isRuntimeOnly || isAistudio; - const typeColor = getTypeColor(item.type || 'unknown'); + const renderFileCard = (item: AuthFileItem) => { + const fileStats = resolveAuthFileStats(item, keyStats); + const isRuntimeOnly = isRuntimeOnlyAuthFile(item); + const isAistudio = (item.type || '').toLowerCase() === 'aistudio'; + const showModelsButton = !isRuntimeOnly || isAistudio; + const typeColor = getTypeColor(item.type || 'unknown'); - return ( -
-
- +
+
- +
+
+ + +
+ +
} > {modelAliasError === 'unsupported' ? ( @@ -1408,6 +1700,39 @@ export function AuthFilesPage() { title={t('oauth_model_alias.upgrade_required_title')} description={t('oauth_model_alias.upgrade_required_desc')} /> + ) : viewMode === 'diagram' ? ( + Object.keys(modelAlias).length === 0 ? ( + + ) : ( +
+
+

{t('oauth_model_alias.chart_title')}

+ +
+ +
+ ) ) : Object.keys(modelAlias).length === 0 ? ( ) : ( @@ -1625,7 +1950,6 @@ export function AuthFilesPage() {
)} -
); } diff --git a/src/stores/useNotificationStore.ts b/src/stores/useNotificationStore.ts index 17971c5..d0adb09 100644 --- a/src/stores/useNotificationStore.ts +++ b/src/stores/useNotificationStore.ts @@ -4,13 +4,14 @@ */ import { create } from 'zustand'; +import type { ReactNode } from 'react'; import type { Notification, NotificationType } from '@/types'; import { generateId } from '@/utils/helpers'; import { NOTIFICATION_DURATION_MS } from '@/utils/constants'; interface ConfirmationOptions { title?: string; - message: string; + message: ReactNode; confirmText?: string; cancelText?: string; variant?: 'danger' | 'primary' | 'secondary';