Files
qihuaiflow/frontend/src/pages/admin/Models.tsx
T

681 lines
35 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect, useCallback, useRef } from 'react';
import { adminAPI } from '@/api';
import type { ModelProvider, AIModel, ProtocolDef, FetchableModel } from '@/types';
const SVGIcon: React.FC<{ children: React.ReactNode; size?: number }> = ({ children, size = 16 }) => (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
{children}
</svg>
);
const formatNumber = (n: number) => {
if (n >= 100000000) return (n / 100000000).toFixed(1) + '亿';
if (n >= 10000) return (n / 10000).toFixed(1) + '万';
return n.toLocaleString();
};
const capabilityColors: Record<string, string> = {
text: '#5B5FED', thinking: '#8B5CF6', vision: '#219B5E', tool: '#3B82F6',
image: '#D9383A', audio: '#E8922C', asr: '#0EA5E9', ocr: '#F59E0B',
embedding: '#10B981', rerank: '#EC4899', video: '#EF4444',
};
const capabilityLabels: Record<string, string> = {
text: '文本对话', thinking: '推理思考', vision: '视觉理解', tool: '工具调用',
image: '图像生成', audio: '语音合成', asr: '语音识别', ocr: 'OCR识别',
embedding: '嵌入向量', rerank: '重排序', video: '视频生成',
};
// Test result type
interface TestResult {
modelId: string;
success: boolean;
message: string;
latency_ms?: number;
}
// Model editing type
interface EditingModel {
id?: string;
name: string;
provider_id: string;
api_key: string;
endpoint: string;
capabilities: string[];
timeout: number;
status: string;
price_per_million_tokens: number;
is_enabled: boolean;
}
const Models: React.FC = () => {
const [providers, setProviders] = useState<ModelProvider[]>([]);
const [models, setModels] = useState<AIModel[]>([]);
const [protocols, setProtocols] = useState<ProtocolDef[]>([]);
const [expandedProvider, setExpandedProvider] = useState<string | null>(null);
const [fetchedModels, setFetchedModels] = useState<FetchableModel[]>([]);
const [fetchSource, setFetchSource] = useState<string>('');
const [fetchMessage, setFetchMessage] = useState<string>('');
const [selectedModelIds, setSelectedModelIds] = useState<Set<string>>(new Set());
const [showAddProvider, setShowAddProvider] = useState(false);
const [editingProvider, setEditingProvider] = useState<ModelProvider | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
// 能力默认模型(Auto 自动模式)
const [capabilities, setCapabilities] = useState<any[]>([]);
const [capabilityDefaults, setCapabilityDefaults] = useState<Record<string, string>>({});
const [capabilityModels, setCapabilityModels] = useState<any[]>([]);
const [savingCaps, setSavingCaps] = useState<Set<string>>(new Set());
// Model test state
const [testingModelId, setTestingModelId] = useState<string | null>(null);
const [testResult, setTestResult] = useState<TestResult | null>(null);
const testTimer = useRef<number | null>(null);
// 测试反馈自动消失(6 秒后)
useEffect(() => {
return () => { if (testTimer.current) clearTimeout(testTimer.current); };
}, []);
// Model editor state
const [showModelEditor, setShowModelEditor] = useState(false);
const [editingModel, setEditingModel] = useState<EditingModel | null>(null);
const [modelSaving, setModelSaving] = useState(false);
const [providerForm, setProviderForm] = useState({
name: '', protocol: 'openai_compatible', api_key: '', api_base_url: '',
});
const loadData = useCallback(async () => {
try {
setLoading(true);
const [provRes, modelsRes, protoRes, capRes] = await Promise.all([
adminAPI.getProviders(),
adminAPI.getAdminModels({ page_size: 100 }),
adminAPI.getProtocols(),
adminAPI.getCapabilityDefaults(),
]);
setProviders(Array.isArray(provRes) ? provRes : (provRes.data || []));
setProtocols(Array.isArray(protoRes) ? protoRes : (protoRes.data || []));
const modelData = modelsRes.data || modelsRes;
const rawModels = Array.isArray(modelData) ? modelData : (modelData?.items || []);
const mappedModels = (Array.isArray(rawModels) ? rawModels : []).map((m: any) => ({
...m,
providerId: m.providerId || m.provider_id,
apiKey: m.apiKey || m.api_key,
pricePerMillionTokens: m.pricePerMillionTokens ?? m.price_per_million_tokens ?? 0,
isEnabled: m.isEnabled ?? m.is_enabled ?? true,
isDefault: m.isDefault ?? m.is_default ?? false,
totalCalls: m.totalCalls ?? m.total_calls ?? 0,
totalTokens: m.totalTokens ?? m.total_tokens ?? 0,
}));
setModels(mappedModels);
// 能力默认模型
const cd = (capRes.data || capRes) as any;
setCapabilities(cd.capabilities || []);
setCapabilityDefaults(cd.defaults || {});
setCapabilityModels(cd.models || []);
} catch (err: any) {
setError('加载数据失败:' + (err.message || err.detail || JSON.stringify(err)));
} finally {
setLoading(false);
}
}, []);
useEffect(() => { loadData(); }, [loadData]);
// Test model connection (single model only)
const handleTestConnection = async (modelId: string) => {
setTestingModelId(modelId);
setTestResult(null);
if (testTimer.current) { clearTimeout(testTimer.current); testTimer.current = null; }
try {
const result = await adminAPI.testModelConnection(modelId) as any;
const r = result.success !== undefined ? result : (result.data || result);
setTestResult({ modelId, success: !!r.success, message: r.message || '', latency_ms: r.latency_ms });
} catch (err: any) {
setTestResult({ modelId, success: false, message: err.message || '测试失败' });
} finally {
setTestingModelId(null);
// 6 秒后自动收起反馈
testTimer.current = window.setTimeout(() => setTestResult(null), 6000);
}
};
// Set default model
const handleSetDefault = async (modelId: string) => {
try {
await adminAPI.setDefaultModel(modelId);
await loadData();
} catch (err: any) {
setError(err.message || '设置失败');
}
};
// Provider operations
const handleAddProvider = async (e: React.FormEvent) => {
e.preventDefault();
if (!providerForm.name.trim()) return;
try {
setError(null);
await adminAPI.addProvider({
name: providerForm.name,
protocol: providerForm.protocol,
api_key: (providerForm as any).api_key || providerForm.api_key,
api_base_url: (providerForm as any).api_base_url || providerForm.api_base_url || undefined,
});
setShowAddProvider(false);
setProviderForm({ name: '', protocol: 'openai_compatible', api_key: '', api_base_url: '' });
await loadData();
} catch (err: any) {
setError(err.message || '添加供应商失败');
}
};
const handleUpdateProvider = async (e: React.FormEvent) => {
e.preventDefault();
if (!editingProvider || !providerForm.name.trim()) return;
try {
setError(null);
await adminAPI.updateProvider(editingProvider.id, {
name: providerForm.name, protocol: providerForm.protocol,
api_key: (providerForm as any).api_key || providerForm.api_key,
api_base_url: (providerForm as any).api_base_url || providerForm.api_base_url || undefined,
});
setEditingProvider(null);
setProviderForm({ name: '', protocol: 'openai_compatible', api_key: '', api_base_url: '' });
await loadData();
} catch (err: any) {
setError(err.message || '更新供应商失败');
}
};
const handleDeleteProvider = async (id: string) => {
if (!confirm('确定删除该供应商?')) return;
try {
await adminAPI.deleteProvider(id);
await loadData();
} catch (err: any) {
setError(err.message || '删除失败');
}
};
// Fetch models from provider API
const handleFetchModels = async (providerId: string) => {
try {
const result = await adminAPI.fetchProviderModels(providerId) as any;
setFetchedModels(result.models || result.data?.models || []);
setFetchSource(result.source || result.data?.source || 'unknown');
setFetchMessage(result.message || result.data?.message || '');
setExpandedProvider(providerId);
setSelectedModelIds(new Set());
} catch (err: any) {
setError(err.message || '获取模型列表失败');
}
};
// Add selected models
const handleAddModels = async (providerId: string) => {
if (selectedModelIds.size === 0) return;
try {
await adminAPI.addModels(providerId, Array.from(selectedModelIds));
setExpandedProvider(null);
setSelectedModelIds(new Set());
setFetchedModels([]);
await loadData();
} catch (err: any) {
setError(err.message || '添加模型失败');
}
};
// Toggle model selection
const toggleModelSelection = (id: string) => {
const newSet = new Set(selectedModelIds);
if (newSet.has(id)) newSet.delete(id);
else newSet.add(id);
setSelectedModelIds(newSet);
};
// Open model editor
const openModelEditor = (model?: AIModel, providerId?: string) => {
if (model) {
setEditingModel({
id: model.id, name: model.name,
provider_id: (model as any).provider_id || model.providerId || providerId || '',
api_key: (model as any).api_key || model.apiKey || '',
endpoint: model.endpoint || '',
capabilities: model.capabilities || ['text'],
timeout: model.timeout || 60,
status: model.status || 'paid',
price_per_million_tokens: model.pricePerMillionTokens ?? 0,
is_enabled: model.isEnabled ?? true,
});
} else {
setEditingModel({
name: '', provider_id: providerId || '',
api_key: '', endpoint: '',
capabilities: ['text'], timeout: 60,
status: 'paid', price_per_million_tokens: 0, is_enabled: true,
});
}
setShowModelEditor(true);
};
// Save model
const handleSaveModel = async (e: React.FormEvent) => {
e.preventDefault();
if (!editingModel || !editingModel.name.trim()) return;
setModelSaving(true);
try {
if (editingModel.id) {
await adminAPI.updateAdminModel(editingModel.id, editingModel as any);
} else {
await adminAPI.createAdminModel(editingModel as any);
}
setShowModelEditor(false);
await loadData();
} catch (err: any) {
setError(err.message || '保存失败');
} finally {
setModelSaving(false);
}
};
// Delete model
const handleDeleteModel = async (modelId: string) => {
if (!confirm('确定删除该模型?')) return;
try {
await adminAPI.deleteAdminModel(modelId);
await loadData();
} catch (err: any) {
setError(err.message || '删除失败');
}
};
// 设置/清除某个能力的默认模型(Auto 自动模式使用)
const handleSetCapabilityDefault = async (capId: string, modelId: string | null) => {
try {
setSavingCaps(prev => new Set(prev).add(capId));
const res = await adminAPI.setCapabilityDefault(capId, modelId) as any;
setCapabilityDefaults((res && res.defaults) || { ...capabilityDefaults, [capId]: modelId || undefined } as Record<string, string>);
} catch (err: any) {
setError(err.message || '设置失败');
} finally {
setSavingCaps(prev => {
const n = new Set(prev);
n.delete(capId);
return n;
});
}
};
// Toggle capability
const toggleCapability = (cap: string) => {
if (!editingModel) return;
const caps = editingModel.capabilities || [];
if (caps.includes(cap)) {
editingModel.capabilities = caps.filter(c => c !== cap);
} else {
editingModel.capabilities = [...caps, cap];
}
setEditingModel({ ...editingModel });
};
// Group models by provider
const getProviderModels = (providerId: string) =>
models.filter(m => ((m as any).provider_id || m.providerId) === providerId);
const getProviderName = (providerId: string) => {
const p = providers.find(p => p.id === providerId);
return p ? p.name : providerId;
};
return (
<div style={{ padding: '24px', maxWidth: '1200px', margin: '0 auto' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '24px' }}>
<div>
<h2 style={{ margin: 0, fontSize: '20px', fontWeight: 600 }}>大模型配置</h2>
<p style={{ margin: '4px 0 0', color: '#666', fontSize: '13px' }}>
管理 AI 模型供应商、模型配置和连接测试
</p>
</div>
<button onClick={() => { setShowAddProvider(true); setEditingProvider(null); setProviderForm({ name: '', protocol: 'openai_compatible', api_key: '', api_base_url: '' }); }}
style={btnStyle('#5B5FED')}>
+ 添加供应商
</button>
</div>
{error && (
<div style={{ ...alertStyle('#FEE2E2', '#DC2626'), marginBottom: '16px' }}>
{error} <button onClick={() => setError(null)} style={{ float: 'right', background: 'none', border: 'none', cursor: 'pointer', fontSize: '16px' }}>×</button>
</div>
)}
{loading ? (
<div style={{ textAlign: 'center', padding: '60px', color: '#999' }}>加载中...</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
{providers.map(provider => {
const provModels = getProviderModels(provider.id);
const isExpanded = expandedProvider === provider.id;
return (
<div key={provider.id} style={{ background: '#fff', borderRadius: '8px', border: '1px solid #E5E7EB', overflow: 'hidden' }}>
{/* Provider Header */}
<div style={{ padding: '16px 20px', background: '#F9FAFB', borderBottom: '1px solid #E5E7EB', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<span style={{ fontWeight: 600, fontSize: '15px' }}>{provider.name}</span>
<span style={{ fontSize: '11px', padding: '2px 8px', background: '#EEF2FF', color: '#5B5FED', borderRadius: '4px' }}>
{provider.protocol}
</span>
{provModels.filter(m => m.isDefault).length > 0 && (
<span style={{ fontSize: '11px', padding: '2px 8px', background: '#DCFCE7', color: '#16A34A', borderRadius: '4px' }}>默认</span>
)}
</div>
<div style={{ fontSize: '12px', color: '#666', marginTop: '4px' }}>
API: {(provider as any).api_base_url || provider.apiBaseUrl || '未配置'}
{(provider as any).api_key || provider.apiKey ? ' ✓ 已配置Key' : ' ✗ 未配置Key'}
</div>
</div>
<div style={{ display: 'flex', gap: '8px' }}>
<button onClick={() => handleFetchModels(provider.id)} style={btnStyle('#6B7280', 'small')}>获取模型</button>
<button onClick={() => { setEditingProvider(provider); setProviderForm({ name: provider.name, protocol: provider.protocol,
api_base_url: (provider as any).api_base_url || provider.apiBaseUrl || '',
api_key: (provider as any).api_key || provider.apiKey || '' }); setShowAddProvider(true); }} style={btnStyle('#6B7280', 'small')}>编辑</button>
<button onClick={() => handleDeleteProvider(provider.id)} style={btnStyle('#EF4444', 'small')}>删除</button>
<button onClick={() => openModelEditor(undefined, provider.id)} style={btnStyle('#5B5FED', 'small')}>+ 添加模型</button>
</div>
</div>
{/* Model Fetch Panel */}
{isExpanded && (
<div style={{ padding: '16px 20px', background: '#FFFBEB', borderBottom: '1px solid #FDE68A' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px' }}>
<span style={{ fontSize: '13px', color: '#92400E' }}>
{fetchMessage || `找到 ${fetchedModels.length} 个模型(来源: ${fetchSource})`}
</span>
{fetchedModels.length > 0 && (
<button onClick={() => handleAddModels(provider.id)} style={btnStyle('#5B5FED', 'small')} disabled={selectedModelIds.size === 0}>
添加选中 ({selectedModelIds.size})
</button>
)}
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
{fetchedModels.map(m => (
<label key={m.id} style={{ display: 'flex', alignItems: 'center', gap: '4px', fontSize: '13px', cursor: 'pointer', padding: '4px 8px', background: selectedModelIds.has(m.id) ? '#EDE9FE' : '#fff', border: '1px solid #D1D5DB', borderRadius: '4px' }}>
<input type="checkbox" checked={selectedModelIds.has(m.id)} onChange={() => toggleModelSelection(m.id)} />
{m.name || m.id}
</label>
))}
</div>
</div>
)}
{/* Models List */}
<div style={{ padding: '0' }}>
{provModels.length === 0 ? (
<div style={{ padding: '24px', textAlign: 'center', color: '#999', fontSize: '13px' }}>
暂无模型,点击"获取模型"或"添加模型"添加
</div>
) : (
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr style={{ background: '#F3F4F6' }}>
<th style={thStyle}>模型</th>
<th style={thStyle}>能力</th>
<th style={thStyle}>状态</th>
<th style={thStyle}>价格</th>
<th style={thStyle}>调用量</th>
<th style={thStyle}>操作</th>
</tr>
</thead>
<tbody>
{provModels.map(model => (
<tr key={model.id} style={{ borderBottom: '1px solid #F3F4F6' }}>
<td style={tdStyle}>
<div style={{ fontWeight: 500 }}>{model.name}</div>
{model.isDefault && <span style={{ fontSize: '11px', color: '#16A34A' }}>⭐ 默认模型</span>}
</td>
<td style={tdStyle}>
<div style={{ display: 'flex', gap: '4px', flexWrap: 'wrap' }}>
{(model.capabilities || []).map(cap => (
<span key={cap} style={{ fontSize: '11px', padding: '2px 6px', background: capabilityColors[cap] + '22', color: capabilityColors[cap], borderRadius: '3px' }}>
{capabilityLabels[cap] || cap}
</span>
))}
</div>
</td>
<td style={tdStyle}>
<span style={{ fontSize: '12px', padding: '2px 8px', background: model.isEnabled ? '#DCFCE7' : '#FEE2E2', color: model.isEnabled ? '#16A34A' : '#DC2626', borderRadius: '4px' }}>
{model.isEnabled ? '启用' : '禁用'}
</span>
</td>
<td style={tdStyle}>
<span style={{ fontSize: '12px', color: '#666' }}>
¥{model.pricePerMillionTokens?.toFixed(4) || '0.0000'}/M
</span>
</td>
<td style={tdStyle}>
<span style={{ fontSize: '12px', color: '#666' }}>
{formatNumber(model.totalCalls || 0)}
</span>
</td>
<td style={tdStyle}>
<div style={{ display: 'flex', gap: '4px', flexWrap: 'wrap' }}>
{/* Test Connection Button */}
<button onClick={() => handleTestConnection(model.id)} disabled={testingModelId === model.id}
style={{ ...btnStyle(testingModelId === model.id ? '#9CA3AF' : '#059669', 'small') }}>
{testingModelId === model.id ? '测试中...' : '连接测试'}
</button>
{/* Test Result Popover (only for the tested model) */}
{testResult && testResult.modelId === model.id && (
<div style={{ position: 'relative' }}>
<span style={{ fontSize: '11px', padding: '4px 8px', background: testResult.success ? '#DCFCE7' : '#FEE2E2', color: testResult.success ? '#16A34A' : '#DC2626', borderRadius: '4px', display: 'inline-block' }}>
{testResult.success ? '✓ 成功' : '✗ 失败'}
{testResult.latency_ms && ` (${testResult.latency_ms}ms)`}
</span>
<div style={{ position: 'absolute', top: '100%', left: 0, zIndex: 10, background: '#fff', border: '1px solid #E5E7EB', borderRadius: '6px', padding: '8px 12px', fontSize: '12px', color: '#333', minWidth: '200px', boxShadow: '0 4px 12px rgba(0,0,0,0.1)', marginTop: '4px' }}>
{testResult.message}
</div>
<button onClick={() => setTestResult(null)} style={{ marginLeft: '8px', fontSize: '11px', color: '#999', background: 'none', border: 'none', cursor: 'pointer' }}>×</button>
</div>
)}
{!model.isDefault && model.isEnabled && (
<button onClick={() => handleSetDefault(model.id)} style={btnStyle('#5B5FED', 'small')}>设为默认</button>
)}
<button onClick={() => openModelEditor(model as AIModel)} style={btnStyle('#6B7280', 'small')}>编辑</button>
<button onClick={() => handleDeleteModel(model.id)} style={btnStyle('#EF4444', 'small')}>删除</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
</div>
);
})}
{providers.length === 0 && (
<div style={{ textAlign: 'center', padding: '60px', background: '#fff', borderRadius: '8px', border: '1px solid #E5E7EB' }}>
<div style={{ fontSize: '48px', marginBottom: '16px' }}>🤖</div>
<p style={{ color: '#666', marginBottom: '16px' }}>暂无模型供应商,点击上方按钮添加</p>
</div>
)}
</div>
)}
{/* ===== 能力默认模型(Auto 自动模式) ===== */}
{capabilities.length > 0 && (
<div style={{ background: '#fff', borderRadius: '8px', border: '1px solid #E5E7EB', padding: '20px', marginTop: '16px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: '4px', flexWrap: 'wrap', gap: '8px' }}>
<h3 style={{ margin: 0, fontSize: '16px', fontWeight: 600 }}>能力默认模型</h3>
<span style={{ fontSize: '12px', color: '#888' }}>
为每项能力指定默认模型,前台「Auto 自动」将按对话意图自动匹配
</span>
</div>
<p style={{ margin: '0 0 16px', fontSize: '13px', color: '#999' }}>
选择「Auto 自动」时,系统依据用户输入(如含「画图 / 视频」)自动选用对应能力的默认模型;文本类能力在未单独配置时回退到全局默认模型。
</p>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))', gap: '12px' }}>
{capabilities.map((cap: any) => {
const capModels = capabilityModels.filter(
(m: any) => (m.capabilities || []).includes(cap.id) || capabilityDefaults[cap.id] === m.id
);
const cur = capabilityDefaults[cap.id] || '';
const saving = savingCaps.has(cap.id);
const dotColor = cap.color || capabilityColors[cap.id] || '#6B7280';
return (
<div key={cap.id} style={{ border: '1px solid #EEF0F4', borderRadius: '8px', padding: '12px 14px', background: '#FBFCFE' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '6px' }}>
<span style={{ width: '10px', height: '10px', borderRadius: '3px', background: dotColor, flexShrink: 0 }} />
<span style={{ fontWeight: 600, fontSize: '14px' }}>{cap.label}</span>
<span style={{ fontSize: '11px', color: '#bbb', marginLeft: 'auto' }}>{cap.id}</span>
</div>
<div style={{ fontSize: '12px', color: '#8a8a8a', marginBottom: '10px', minHeight: '32px' }}>{cap.desc}</div>
<select
value={cur}
disabled={saving}
onChange={e => handleSetCapabilityDefault(cap.id, e.target.value || null)}
style={{ width: '100%', padding: '7px 10px', border: '1px solid #D1D5DB', borderRadius: '6px', fontSize: '13px', boxSizing: 'border-box', background: '#fff', color: '#333' }}
>
<option value="">— 未设置(回退全局默认)—</option>
{capModels.map((m: any) => (
<option key={m.id} value={m.id}>
{m.display_name || m.name}{m.provider_name ? `(${m.provider_name})` : ''}
</option>
))}
</select>
</div>
);
})}
</div>
</div>
)}
{/* Provider Modal */}
{showAddProvider && (
<div style={modalOverlay}>
<div style={modalContent}>
<h3 style={{ marginTop: 0 }}>{editingProvider ? '编辑供应商' : '添加供应商'}</h3>
<form onSubmit={editingProvider ? handleUpdateProvider : handleAddProvider}>
<div style={formGroup}>
<label style={labelStyle}>供应商名称 *</label>
<input value={providerForm.name} onChange={e => setProviderForm({ ...providerForm, name: e.target.value })} placeholder="如:SiliconFlow" style={inputStyle} required />
</div>
<div style={formGroup}>
<label style={labelStyle}>API 协议</label>
<select value={providerForm.protocol} onChange={e => setProviderForm({ ...providerForm, protocol: e.target.value })} style={inputStyle}>
{protocols.map(p => <option key={p.id} value={p.id}>{p.name} ({p.id})</option>)}
</select>
</div>
<div style={formGroup}>
<label style={labelStyle}>API Base URL</label>
<input value={providerForm.api_base_url} onChange={e => setProviderForm({ ...providerForm, api_base_url: e.target.value })} placeholder="https://api.siliconflow.cn" style={inputStyle} />
</div>
<div style={formGroup}>
<label style={labelStyle}>API Key</label>
<input type="password" value={providerForm.api_key} onChange={e => setProviderForm({ ...providerForm, api_key: e.target.value })} placeholder="sk-..." style={inputStyle} />
</div>
<div style={{ display: 'flex', gap: '8px', justifyContent: 'flex-end' }}>
<button type="button" onClick={() => { setShowAddProvider(false); setEditingProvider(null); }} style={btnStyle('#6B7280')}>取消</button>
<button type="submit" style={btnStyle('#5B5FED')}>{editingProvider ? '保存' : '添加'}</button>
</div>
</form>
</div>
</div>
)}
{/* Model Editor Modal */}
{showModelEditor && editingModel && (
<div style={modalOverlay}>
<div style={{ ...modalContent, maxWidth: '600px' }}>
<h3 style={{ marginTop: 0 }}>{editingModel.id ? '编辑模型' : '添加模型'}</h3>
<form onSubmit={handleSaveModel}>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px' }}>
<div style={formGroup}>
<label style={labelStyle}>模型名称 *</label>
<input value={editingModel.name} onChange={e => setEditingModel({ ...editingModel, name: e.target.value })} placeholder="如:DeepSeek-V4-Flash" style={inputStyle} required />
</div>
<div style={formGroup}>
<label style={labelStyle}>供应商</label>
<select value={editingModel.provider_id} onChange={e => setEditingModel({ ...editingModel, provider_id: e.target.value })} style={inputStyle}>
{providers.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
</select>
</div>
</div>
<div style={formGroup}>
<label style={labelStyle}>API Key</label>
<input type="password" value={editingModel.api_key} onChange={e => setEditingModel({ ...editingModel, api_key: e.target.value })} placeholder="sk-... (留空则使用供应商的Key)" style={inputStyle} />
</div>
<div style={formGroup}>
<label style={labelStyle}>API Endpoint</label>
<input value={editingModel.endpoint} onChange={e => setEditingModel({ ...editingModel, endpoint: e.target.value })} placeholder="https://api.siliconflow.cn/v1/chat/completions" style={inputStyle} />
</div>
<div style={formGroup}>
<label style={labelStyle}>模型能力 (可多选)</label>
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
{Object.entries(capabilityLabels).map(([key, label]) => (
<label key={key} style={{ display: 'flex', alignItems: 'center', gap: '4px', cursor: 'pointer', padding: '4px 10px', background: (editingModel.capabilities || []).includes(key) ? capabilityColors[key] + '22' : '#F3F4F6', border: `1px solid ${(editingModel.capabilities || []).includes(key) ? capabilityColors[key] : '#D1D5DB'}`, borderRadius: '4px', fontSize: '13px' }}>
<input type="checkbox" checked={(editingModel.capabilities || []).includes(key)} onChange={() => toggleCapability(key)} style={{ display: 'none' }} />
<span style={{ color: capabilityColors[key] }}>●</span> {label}
</label>
))}
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '16px' }}>
<div style={formGroup}>
<label style={labelStyle}>超时 (秒)</label>
<input type="number" value={editingModel.timeout} onChange={e => setEditingModel({ ...editingModel, timeout: parseInt(e.target.value) || 60 })} style={inputStyle} />
</div>
<div style={formGroup}>
<label style={labelStyle}>计费状态</label>
<select value={editingModel.status} onChange={e => setEditingModel({ ...editingModel, status: e.target.value })} style={inputStyle}>
<option value="free">免费</option>
<option value="paid">付费</option>
</select>
</div>
<div style={formGroup}>
<label style={labelStyle}>价格 (¥/M tokens)</label>
<input type="number" step="0.0001" value={editingModel.price_per_million_tokens} onChange={e => setEditingModel({ ...editingModel, price_per_million_tokens: parseFloat(e.target.value) || 0 })} style={inputStyle} />
</div>
</div>
<div style={formGroup}>
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
<input type="checkbox" checked={editingModel.is_enabled} onChange={e => setEditingModel({ ...editingModel, is_enabled: e.target.checked })} />
启用该模型
</label>
</div>
<div style={{ display: 'flex', gap: '8px', justifyContent: 'flex-end' }}>
<button type="button" onClick={() => setShowModelEditor(false)} style={btnStyle('#6B7280')}>取消</button>
<button type="submit" disabled={modelSaving} style={btnStyle('#5B5FED')}>{modelSaving ? '保存中...' : '保存'}</button>
</div>
</form>
</div>
</div>
)}
</div>
);
};
// Styles
const btnStyle = (color: string, size: 'normal' | 'small' = 'normal'): React.CSSProperties => ({
background: color, color: '#fff', border: 'none', borderRadius: '6px', cursor: 'pointer',
fontSize: size === 'small' ? '12px' : '14px', padding: size === 'small' ? '4px 10px' : '8px 16px',
fontWeight: 500, transition: 'opacity 0.2s',
});
const thStyle: React.CSSProperties = { padding: '10px 16px', textAlign: 'left', fontSize: '12px', color: '#6B7280', fontWeight: 600 };
const tdStyle: React.CSSProperties = { padding: '12px 16px', fontSize: '13px', verticalAlign: 'middle' };
const formGroup: React.CSSProperties = { marginBottom: '16px' };
const labelStyle: React.CSSProperties = { display: 'block', marginBottom: '6px', fontSize: '13px', fontWeight: 500, color: '#374151' };
const inputStyle: React.CSSProperties = { width: '100%', padding: '8px 12px', border: '1px solid #D1D5DB', borderRadius: '6px', fontSize: '13px', boxSizing: 'border-box' };
const alertStyle = (bg: string, color: string): React.CSSProperties => ({ background: bg, color, padding: '10px 16px', borderRadius: '6px', fontSize: '13px' });
const modalOverlay: React.CSSProperties = { position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.5)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000 };
const modalContent: React.CSSProperties = { background: '#fff', borderRadius: '12px', padding: '24px', width: '480px', maxWidth: '90vw', maxHeight: '90vh', overflow: 'auto', boxSizing: 'border-box' };
export default Models;