From 488d420e06387de161d62622cb6e43984c1a1352 Mon Sep 17 00:00:00 2001 From: duanshuwen Date: Tue, 21 Apr 2026 19:39:36 +0800 Subject: [PATCH] feat: implement hotel staff chat model configuration API and update localization files --- src/api/{modelConfig.ts => chat.ts} | 6 +- src/api/index.ts | 2 +- src/api/types.ts | 8 +- src/i18n/locales/en/models.json | 17 +- src/i18n/locales/ja/models.json | 15 +- src/i18n/locales/zh/models.json | 15 +- .../components/ProviderEditorDialog.tsx | 167 ------ .../components/ProviderPickerDialog.tsx | 48 -- .../Models/components/ProvidersSection.tsx | 515 ++++-------------- src/pages/Models/components/provider-types.ts | 22 - src/pages/Models/index.tsx | 10 +- 11 files changed, 153 insertions(+), 672 deletions(-) rename src/api/{modelConfig.ts => chat.ts} (51%) delete mode 100644 src/pages/Models/components/ProviderEditorDialog.tsx delete mode 100644 src/pages/Models/components/ProviderPickerDialog.tsx delete mode 100644 src/pages/Models/components/provider-types.ts diff --git a/src/api/modelConfig.ts b/src/api/chat.ts similarity index 51% rename from src/api/modelConfig.ts rename to src/api/chat.ts index 578957b..c94eee4 100644 --- a/src/api/modelConfig.ts +++ b/src/api/chat.ts @@ -4,13 +4,13 @@ import request from './request'; import * as API from './types'; -/** 获取模型配置 获取模型配置 GET /chat/modelConfig */ -export function chatModelConfigUsingGet({ +/** 获取模型配置 获取模型配置 GET /hotelStaff/chat/modelConfig */ +export function hotelStaffChatModelConfigUsingGet({ options, }: { options?: { [key: string]: unknown }; }) { - return request('/chat/modelConfig', { + return request('/hotelStaff/chat/modelConfig', { method: 'GET', ...(options || {}), }); diff --git a/src/api/index.ts b/src/api/index.ts index 64faf9e..ac767e0 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -16,4 +16,4 @@ export * from './recentConversation'; export * from './conversationMessageList'; export * from './recommendedQuestionList'; export * from './chatConfig'; -export * from './modelConfig'; +export * from './chat'; diff --git a/src/api/types.ts b/src/api/types.ts index bb37897..acf1538 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -41,10 +41,6 @@ export type ChatConversationMessageListUsingPostResponses = { 200: RPageConversationMessageDTO; }; -export type ChatModelConfigUsingGetResponses = { - 200: RModelConfigDTO; -}; - export type ChatRecentConversationUsingGetResponses = { 200: RConversationDTO; }; @@ -298,6 +294,10 @@ export type EventListSearchForm = { eventStatus?: number; }; +export type HotelStaffChatModelConfigUsingGetResponses = { + 200: RModelConfigDTO; +}; + export type HotelStaffConfigChannelBindingUsingPostResponses = { 200: RBoolean; }; diff --git a/src/i18n/locales/en/models.json b/src/i18n/locales/en/models.json index 4f1899f..9f000cf 100644 --- a/src/i18n/locales/en/models.json +++ b/src/i18n/locales/en/models.json @@ -14,6 +14,7 @@ "providers": { "title": "AI Providers", "subtitle": "Manage your AI models and API keys.", + "providerName": "Provider", "add": "Add Provider", "loading": "Loading provider settings...", "defaultBadge": "Default", @@ -22,7 +23,7 @@ "setDefault": "Set Default", "edit": "Edit", "delete": "Delete", - "empty": "No providers configured yet. Click \"Add Provider\" to start.", + "empty": "No model configuration is available yet.", "confirmDelete": "Are you sure you want to delete this provider?", "notices": { "loadError": "Failed to load provider settings.", @@ -68,7 +69,9 @@ "window7d": "Last 7 Days", "window30d": "Last 30 Days", "windowAll": "All Time", - "showingRecords": "Showing {count} records", + "showingRecords": "Showing {{count}} records", + "showingRecords_one": "Showing {{count}} record", + "showingRecords_other": "Showing {{count}} records", "chart": { "total": "Total", "input": "Input", @@ -78,17 +81,17 @@ "row": { "noUsageInfo": "No usage info", "errorParsingUsage": "Error parsing usage", - "input": "Input: {count}", - "output": "Output: {count}", - "cacheRead": "Cache Read: {count}", - "cacheWrite": "Cache Write: {count}", + "input": "Input: {{count}}", + "output": "Output: {{count}}", + "cacheRead": "Cache Read: {{count}}", + "cacheWrite": "Cache Write: {{count}}", "noUsageReported": "No usage reported", "parseError": "Parse error", "viewContent": "View Content", "error": "Error" }, "pagination": { - "pageOf": "Page {page} of {total}", + "pageOf": "Page {{page}} of {{total}}", "prev": "Prev", "next": "Next" }, diff --git a/src/i18n/locales/ja/models.json b/src/i18n/locales/ja/models.json index 3e596d5..45cd18a 100644 --- a/src/i18n/locales/ja/models.json +++ b/src/i18n/locales/ja/models.json @@ -14,6 +14,7 @@ "providers": { "title": "AI プロバイダー", "subtitle": "AI モデルと API キーを管理します。", + "providerName": "Provider", "add": "プロバイダーを追加", "loading": "プロバイダー設定を読み込み中...", "defaultBadge": "デフォルト", @@ -22,7 +23,7 @@ "setDefault": "デフォルトに設定", "edit": "編集", "delete": "削除", - "empty": "プロバイダーはまだ設定されていません。「プロバイダーを追加」をクリックして開始してください。", + "empty": "モデル設定はまだ取得されていません。", "confirmDelete": "このプロバイダーを削除してもよろしいですか?", "notices": { "loadError": "プロバイダー設定の読み込みに失敗しました。", @@ -68,7 +69,7 @@ "window7d": "過去 7 日", "window30d": "過去 30 日", "windowAll": "全期間", - "showingRecords": "{count} 件を表示", + "showingRecords": "{{count}} 件を表示", "chart": { "total": "合計", "input": "入力", @@ -78,17 +79,17 @@ "row": { "noUsageInfo": "使用量情報なし", "errorParsingUsage": "使用量の解析エラー", - "input": "入力: {count}", - "output": "出力: {count}", - "cacheRead": "キャッシュ読込: {count}", - "cacheWrite": "キャッシュ書込: {count}", + "input": "入力: {{count}}", + "output": "出力: {{count}}", + "cacheRead": "キャッシュ読込: {{count}}", + "cacheWrite": "キャッシュ書込: {{count}}", "noUsageReported": "使用量が報告されていません", "parseError": "解析エラー", "viewContent": "内容を見る", "error": "エラー" }, "pagination": { - "pageOf": "{page} / {total} ページ", + "pageOf": "{{page}} / {{total}} ページ", "prev": "前へ", "next": "次へ" }, diff --git a/src/i18n/locales/zh/models.json b/src/i18n/locales/zh/models.json index 0b15d59..02b2fb9 100644 --- a/src/i18n/locales/zh/models.json +++ b/src/i18n/locales/zh/models.json @@ -14,6 +14,7 @@ "providers": { "title": "AI 服务商", "subtitle": "管理你的 AI 模型与 API Key。", + "providerName": "Provider", "add": "添加服务商", "loading": "正在加载服务商配置...", "defaultBadge": "默认", @@ -22,7 +23,7 @@ "setDefault": "设为默认", "edit": "编辑", "delete": "删除", - "empty": "暂未配置服务商。点击“添加服务商”开始。", + "empty": "暂未获取到模型配置。", "confirmDelete": "确定删除这个服务商吗?", "notices": { "loadError": "加载服务商配置失败。", @@ -68,7 +69,7 @@ "window7d": "最近 7 天", "window30d": "最近 30 天", "windowAll": "全部时间", - "showingRecords": "共显示 {count} 条记录", + "showingRecords": "共显示 {{count}} 条记录", "chart": { "total": "总计", "input": "输入", @@ -78,17 +79,17 @@ "row": { "noUsageInfo": "无用量信息", "errorParsingUsage": "用量解析错误", - "input": "输入:{count}", - "output": "输出:{count}", - "cacheRead": "缓存读取:{count}", - "cacheWrite": "缓存写入:{count}", + "input": "输入:{{count}}", + "output": "输出:{{count}}", + "cacheRead": "缓存读取:{{count}}", + "cacheWrite": "缓存写入:{{count}}", "noUsageReported": "未上报用量", "parseError": "解析失败", "viewContent": "查看内容", "error": "错误" }, "pagination": { - "pageOf": "第 {page} / {total} 页", + "pageOf": "第 {{page}} / {{total}} 页", "prev": "上一页", "next": "下一页" }, diff --git a/src/pages/Models/components/ProviderEditorDialog.tsx b/src/pages/Models/components/ProviderEditorDialog.tsx deleted file mode 100644 index a2b487a..0000000 --- a/src/pages/Models/components/ProviderEditorDialog.tsx +++ /dev/null @@ -1,167 +0,0 @@ -import { useEffect, useState } from 'react'; -import { getProviderDocsUrl, getProviderPlaceholder } from '../../../lib/providers'; -import { useLocale } from '../../../i18n'; -import { useModelsCopy } from '../copy'; -import DialogSurface from './DialogSurface'; -import type { ProviderEditorValues, ProviderListItem } from './provider-types'; - -type ProviderEditorDialogProps = { - open: boolean; - item: ProviderListItem | null; - saving: boolean; - error: string | null; - onClose: () => void; - onSave: (values: ProviderEditorValues) => void; - onSwitchProvider: () => void; -}; - -const FIELD_CLASS_NAME = [ - 'w-full rounded-[12px] border border-black/10 bg-white px-4 py-3 text-[14px] text-[#171717]', - 'outline-none transition-colors placeholder:text-[#99A0AE] focus:border-black/20', - 'dark:border-white/10 dark:bg-[#222225] dark:text-[#f3f4f6] dark:placeholder:text-gray-500 dark:focus:border-white/20', -].join(' '); - -export default function ProviderEditorDialog({ - open, - item, - saving, - error, - onClose, - onSave, - onSwitchProvider, -}: ProviderEditorDialogProps) { - const locale = useLocale(); - const t = useModelsCopy(); - const [label, setLabel] = useState(''); - const [apiKey, setApiKey] = useState(''); - const [baseUrl, setBaseUrl] = useState(''); - const [model, setModel] = useState(''); - - useEffect(() => { - if (!item) return; - - setLabel(item.account.label || item.vendor?.name || ''); - setApiKey(''); - setBaseUrl(item.account.baseUrl || item.vendor?.defaultBaseUrl || ''); - setModel(item.account.model || item.vendor?.defaultModelId || ''); - }, [item]); - - if (!item) return null; - - const provider = item.vendor; - const isNew = Boolean(item.isNew); - const showBaseUrl = Boolean(provider?.showBaseUrl); - const showModel = Boolean(provider?.showModelId); - const docsUrl = getProviderDocsUrl(provider, locale); - const apiKeyPlaceholder = getProviderPlaceholder(provider, locale) || t('models.providers.editor.apiKeyPlaceholder'); - - return ( - -
-
-
- {provider?.icon || t('models.common.ai')} -
- -
-
- {provider?.name || item.account.label} -
-
- - {docsUrl ? ( - - {t('models.providers.editor.viewDocs')} - - ) : null} -
-
-
- - {error ? ( -
- {error} -
- ) : null} - -
- - - - - {showBaseUrl ? ( - - ) : null} - - {showModel ? ( - - ) : null} -
- -
- -
-
-
- ); -} diff --git a/src/pages/Models/components/ProviderPickerDialog.tsx b/src/pages/Models/components/ProviderPickerDialog.tsx deleted file mode 100644 index 27b2f2f..0000000 --- a/src/pages/Models/components/ProviderPickerDialog.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import type { ProviderTypeInfo } from '../../../lib/providers'; -import { useModelsCopy } from '../copy'; -import DialogSurface from './DialogSurface'; - -type ProviderPickerDialogProps = { - open: boolean; - providers: ProviderTypeInfo[]; - onClose: () => void; - onSelect: (provider: ProviderTypeInfo) => void; -}; - -export default function ProviderPickerDialog({ - open, - providers, - onClose, - onSelect, -}: ProviderPickerDialogProps) { - const t = useModelsCopy(); - - return ( - -
- {providers.map((provider) => ( - - ))} -
-
- ); -} diff --git a/src/pages/Models/components/ProvidersSection.tsx b/src/pages/Models/components/ProvidersSection.tsx index 7a5af5d..a0d0d65 100644 --- a/src/pages/Models/components/ProvidersSection.tsx +++ b/src/pages/Models/components/ProvidersSection.tsx @@ -1,162 +1,85 @@ import { useEffect, useState } from 'react'; -import { - PROVIDER_TYPE_INFO, - type ProviderAccount, - type ProviderTypeInfo, - type ProviderVendorInfo, - type ProviderWithKeyInfo, -} from '../../../lib/providers'; +import { hotelStaffChatModelConfigUsingGet } from '../../../api/chat'; +import type { ModelConfigDTO } from '../../../api/types'; import { onGatewayEvent } from '../../../lib/gateway-client'; -import { hostApiFetch } from '../../../lib/host-api'; import { isRuntimeChangedGatewayEvent, runtimeEventHasTopic } from '../../../lib/runtime-events'; -import ProviderEditorDialog from './ProviderEditorDialog'; -import ProviderPickerDialog from './ProviderPickerDialog'; -import type { DisplayVendor, ProviderEditorValues, ProviderListItem } from './provider-types'; import { useModelsCopy } from '../copy'; -type NoticeState = { - tone: 'success' | 'error'; - message: string; -}; - -type ProviderSnapshot = { - accounts: ProviderAccount[]; - statuses: ProviderWithKeyInfo[]; - vendors: ProviderVendorInfo[]; - defaultAccountId: string | null; -}; - -const VISIBLE_PROVIDERS = PROVIDER_TYPE_INFO.filter((provider) => !provider.hidden); - -const ACTION_BUTTON_CLASS_NAME = [ - 'rounded-full border px-3 py-1.5 text-[12px] transition-colors', - 'border-[#E5E8EE] text-[#525866] hover:border-[#2B7FFF] hover:text-[#2B7FFF]', - 'dark:border-[#2a2a2d] dark:text-gray-300 dark:hover:border-[#3b82f6] dark:hover:text-white', -].join(' '); - function formatErrorMessage(error: unknown, fallback: string): string { if (error instanceof Error && error.message) return error.message; if (typeof error === 'string' && error) return error; return fallback; } -function hasConfiguredCredentials(account: ProviderAccount, status?: ProviderWithKeyInfo): boolean { - if (account.authMode === 'oauth_device' || account.authMode === 'oauth_browser' || account.authMode === 'local') { - return true; - } - - return status?.hasKey ?? false; +function normalizeText(value: string | undefined): string | null { + if (typeof value !== 'string') return null; + const trimmed = value.trim(); + return trimmed || null; } -function legacyStatusToAccount(status: ProviderWithKeyInfo): ProviderAccount { - return { - id: status.id, - vendorId: status.type, - label: status.name, - authMode: status.type === 'ollama' ? 'local' : 'api_key', - baseUrl: status.baseUrl, - apiProtocol: status.apiProtocol, - headers: status.headers, - model: status.model, - fallbackModels: status.fallbackModels, - fallbackAccountIds: status.fallbackProviderIds, - enabled: status.enabled, - isDefault: false, - createdAt: status.createdAt, - updatedAt: status.updatedAt, - }; +function normalizeModelConfig(response: unknown): ModelConfigDTO { + if (!response || typeof response !== 'object') { + return {}; + } + + const candidate = response as ModelConfigDTO & { data?: unknown }; + + if ( + typeof candidate.providerName === 'string' + || typeof candidate.apiKey === 'string' + || typeof candidate.baseUrl === 'string' + || typeof candidate.model === 'string' + ) { + return { + providerName: candidate.providerName, + apiKey: candidate.apiKey, + baseUrl: candidate.baseUrl, + model: candidate.model, + }; + } + + if (candidate.data && typeof candidate.data === 'object') { + return normalizeModelConfig(candidate.data); + } + + return {}; } -function buildProviderItems( - accounts: ProviderAccount[], - statuses: ProviderWithKeyInfo[], - vendors: DisplayVendor[], - defaultAccountId: string | null, -): ProviderListItem[] { - const vendorMap = new Map(); - - for (const vendor of VISIBLE_PROVIDERS) { - vendorMap.set(vendor.id, vendor); - } - - for (const vendor of vendors) { - vendorMap.set(vendor.id, vendor); - } - - const statusMap = new Map(statuses.map((status) => [status.id, status])); - - if (accounts.length > 0) { - return [...accounts] - .map((account) => ({ - account, - vendor: vendorMap.get(account.vendorId), - status: statusMap.get(account.id), - })) - .sort((left, right) => { - if (left.account.id === defaultAccountId) return -1; - if (right.account.id === defaultAccountId) return 1; - return right.account.updatedAt.localeCompare(left.account.updatedAt); - }); - } - - return statuses.map((status) => ({ - account: legacyStatusToAccount(status), - vendor: vendorMap.get(status.type), - status, - })); +function hasModelConfig(config: ModelConfigDTO | null): boolean { + return Boolean( + normalizeText(config?.providerName) + || normalizeText(config?.apiKey) + || normalizeText(config?.baseUrl) + || normalizeText(config?.model), + ); } -function buildProviderAccountId(providerId: ProviderTypeInfo['id']): string { - const suffix = Math.random().toString(36).slice(2, 8); - return `${providerId}-${Date.now().toString(36)}-${suffix}`; +function maskSecret(value: string | undefined): string | null { + const secret = normalizeText(value); + if (!secret) return null; + if (secret.includes('*')) return secret; + if (secret.length <= 4) return '*'.repeat(secret.length); + if (secret.length <= 8) return `${secret.slice(0, 2)}***${secret.slice(-2)}`; + return `${secret.slice(0, 4)}***${secret.slice(-4)}`; } export default function ProvidersSection() { const t = useModelsCopy(); const [loading, setLoading] = useState(true); - const [items, setItems] = useState([]); - const [vendors, setVendors] = useState(VISIBLE_PROVIDERS); - const [defaultAccountId, setDefaultAccountId] = useState(null); - const [notice, setNotice] = useState(null); - const [pickerOpen, setPickerOpen] = useState(false); - const [editorItem, setEditorItem] = useState(null); - const [saving, setSaving] = useState(false); - const [editorError, setEditorError] = useState(null); + const [error, setError] = useState(null); + const [modelConfig, setModelConfig] = useState(null); - async function loadProviders(showLoading = true): Promise { + async function loadModelConfig(showLoading = true): Promise { if (showLoading) { setLoading(true); } try { - const [accounts, statuses, vendorList, defaultInfo] = await Promise.all([ - hostApiFetch('/api/provider-accounts'), - hostApiFetch('/api/providers'), - hostApiFetch('/api/provider-vendors'), - hostApiFetch<{ accountId: string | null }>('/api/provider-accounts/default'), - ]); - - const nextSnapshot: ProviderSnapshot = { - accounts: Array.isArray(accounts) ? accounts : [], - statuses: Array.isArray(statuses) ? statuses : [], - vendors: Array.isArray(vendorList) ? vendorList : [], - defaultAccountId: defaultInfo?.accountId ?? null, - }; - - const nextVendors = nextSnapshot.vendors.length > 0 ? nextSnapshot.vendors : VISIBLE_PROVIDERS; - setVendors(nextVendors); - setDefaultAccountId(nextSnapshot.defaultAccountId); - setItems(buildProviderItems( - nextSnapshot.accounts, - nextSnapshot.statuses, - nextVendors, - nextSnapshot.defaultAccountId, - )); - } catch (error) { - setNotice({ - tone: 'error', - message: formatErrorMessage(error, t('models.providers.notices.loadError')), - }); + const response = await hotelStaffChatModelConfigUsingGet({}); + setModelConfig(normalizeModelConfig(response)); + setError(null); + } catch (loadError) { + setError(formatErrorMessage(loadError, t('models.providers.notices.loadError'))); } finally { if (showLoading) { setLoading(false); @@ -165,183 +88,57 @@ export default function ProvidersSection() { } useEffect(() => { - void loadProviders(); + void loadModelConfig(); }, []); useEffect(() => { return onGatewayEvent((event) => { if (!isRuntimeChangedGatewayEvent(event)) return; if (!runtimeEventHasTopic(event, 'providers', 'models')) return; - void loadProviders(false); + void loadModelConfig(false); }); }, []); - async function handleSetDefault(accountId: string): Promise { - try { - await hostApiFetch<{ success: boolean; error?: string }>('/api/provider-accounts/default', { - method: 'PUT', - body: JSON.stringify({ accountId }), - }); - await loadProviders(false); - setDefaultAccountId(accountId); - setNotice({ tone: 'success', message: t('models.providers.notices.defaultSuccess') }); - } catch (error) { - setNotice({ - tone: 'error', - message: formatErrorMessage(error, t('models.providers.notices.defaultError')), - }); - } - } + const fields = [ + { + key: 'providerName', + label: t('models.providers.providerName', undefined, 'Provider'), + value: normalizeText(modelConfig?.providerName), + }, + { + key: 'apiKey', + label: t('models.providers.editor.apiKey'), + value: maskSecret(modelConfig?.apiKey), + mono: true, + }, + { + key: 'baseUrl', + label: t('models.providers.editor.baseUrl'), + value: normalizeText(modelConfig?.baseUrl), + mono: true, + }, + { + key: 'model', + label: t('models.providers.editor.defaultModel'), + value: normalizeText(modelConfig?.model), + mono: true, + }, + ] as const; - async function handleDelete(accountId: string): Promise { - const confirmed = window.confirm(t('models.providers.confirmDelete')); - if (!confirmed) return; - - try { - await hostApiFetch<{ success: boolean; error?: string }>(`/api/provider-accounts/${encodeURIComponent(accountId)}`, { - method: 'DELETE', - }); - await loadProviders(false); - setNotice({ tone: 'success', message: t('models.providers.notices.deleteSuccess') }); - } catch (error) { - setNotice({ - tone: 'error', - message: formatErrorMessage(error, t('models.providers.notices.deleteError')), - }); - } - } - - function handleSelectProvider(provider: ProviderTypeInfo): void { - const account: ProviderAccount = { - id: buildProviderAccountId(provider.id), - vendorId: provider.id, - label: provider.name, - authMode: provider.requiresApiKey ? 'api_key' : 'local', - baseUrl: provider.defaultBaseUrl, - apiProtocol: provider.apiProtocol, - model: provider.defaultModelId, - enabled: true, - isDefault: false, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - }; - - setEditorError(null); - setPickerOpen(false); - setEditorItem({ - account, - vendor: provider, - status: undefined, - isNew: true, - }); - } - - function handleEdit(item: ProviderListItem): void { - setEditorError(null); - setEditorItem({ - ...item, - isNew: false, - }); - } - - async function handleSave(values: ProviderEditorValues): Promise { - if (!editorItem) return; - - setSaving(true); - setEditorError(null); - - const trimmedLabel = values.label.trim() || editorItem.vendor?.name || editorItem.account.label; - const trimmedApiKey = values.apiKey.trim(); - const trimmedBaseUrl = values.baseUrl.trim(); - const trimmedModel = values.model.trim(); - - try { - if (editorItem.isNew) { - const account: ProviderAccount = { - ...editorItem.account, - label: trimmedLabel, - baseUrl: trimmedBaseUrl || undefined, - model: trimmedModel || undefined, - updatedAt: new Date().toISOString(), - }; - - await hostApiFetch<{ success: boolean; error?: string }>('/api/provider-accounts', { - method: 'POST', - body: JSON.stringify({ - account, - apiKey: trimmedApiKey || undefined, - }), - }); - - if (!defaultAccountId) { - await hostApiFetch<{ success: boolean; error?: string }>('/api/provider-accounts/default', { - method: 'PUT', - body: JSON.stringify({ accountId: account.id }), - }); - } - - setNotice({ tone: 'success', message: t('models.providers.notices.saveSuccess') }); - } else { - await hostApiFetch<{ success: boolean; error?: string }>( - `/api/provider-accounts/${encodeURIComponent(editorItem.account.id)}`, - { - method: 'PUT', - body: JSON.stringify({ - updates: { - label: trimmedLabel, - baseUrl: trimmedBaseUrl || undefined, - model: trimmedModel || undefined, - enabled: true, - }, - apiKey: trimmedApiKey || undefined, - }), - }, - ); - - setNotice({ tone: 'success', message: t('models.providers.notices.saveSuccess') }); - } - - await loadProviders(false); - setEditorItem(null); - } catch (error) { - setEditorError(formatErrorMessage(error, t('models.providers.notices.saveError'))); - } finally { - setSaving(false); - } - } + const hasConfig = hasModelConfig(modelConfig); return (
-
-
-

{t('models.providers.title')}

-

- {t('models.providers.subtitle')} -

-
- - +
+

{t('models.providers.title')}

+

+ {t('models.providers.subtitle')} +

- {notice ? ( -
- {notice.message} + {error ? ( +
+ {error}
) : null} @@ -352,121 +149,37 @@ export default function ProvidersSection() {
) : null} - {!loading ? ( -
- {items.map((item) => { - const configured = hasConfiguredCredentials(item.account, item.status); - const isDefault = item.account.id === defaultAccountId; - - return ( + {!loading && hasConfig ? ( +
+
+ {fields.map((field) => (
-
-
-
- {item.vendor?.icon || t('models.common.ai')} -
- -
-
- - {item.account.label || item.vendor?.name} - - {isDefault ? ( - - {t('models.providers.defaultBadge')} - - ) : null} -
- -
- {item.account.baseUrl ? ( - {item.account.baseUrl} - ) : null} - {item.account.model ? ( - - {item.account.model} - - ) : null} - - {configured ? t('models.providers.configured') : t('models.providers.notConfigured')} - -
-
-
- -
- {!isDefault && configured ? ( - - ) : null} - - - - -
+
+ {field.label} +
+
+ {field.value || t('models.providers.notConfigured')}
- ); - })} - - {items.length === 0 ? ( -
- {t('models.providers.empty')} -
- ) : null} + ))} +
) : null} - setPickerOpen(false)} - onSelect={handleSelectProvider} - /> - - { - setSaving(false); - setEditorError(null); - setEditorItem(null); - }} - onSave={(values) => { - void handleSave(values); - }} - onSwitchProvider={() => { - setEditorItem(null); - setPickerOpen(true); - }} - /> + {!loading && !hasConfig ? ( +
+ {t('models.providers.empty')} +
+ ) : null}
); } diff --git a/src/pages/Models/components/provider-types.ts b/src/pages/Models/components/provider-types.ts deleted file mode 100644 index d39be89..0000000 --- a/src/pages/Models/components/provider-types.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { - ProviderAccount, - ProviderTypeInfo, - ProviderVendorInfo, - ProviderWithKeyInfo, -} from '../../../lib/providers'; - -export type DisplayVendor = ProviderVendorInfo | ProviderTypeInfo; - -export type ProviderListItem = { - account: ProviderAccount; - vendor?: DisplayVendor; - status?: ProviderWithKeyInfo; - isNew?: boolean; -}; - -export type ProviderEditorValues = { - label: string; - apiKey: string; - baseUrl: string; - model: string; -}; diff --git a/src/pages/Models/index.tsx b/src/pages/Models/index.tsx index fb93024..e39f11d 100644 --- a/src/pages/Models/index.tsx +++ b/src/pages/Models/index.tsx @@ -7,13 +7,13 @@ export default function ModelsPage() { return (
-
-
-
- +
+
+
+ {t('models.page.title')} - + {t('models.page.subtitle')}