Spaces:
Sleeping
Sleeping
| import React, { useState, useEffect } from 'react'; | |
| import { api } from '../../services/api'; | |
| import { SystemConfig, OpenRouterModelConfig, DoubaoModelConfig } from '../../types'; | |
| import { Bot, CheckCircle, X, BarChart2, Brain, Activity, ShieldCheck, Power, Key, Trash2, Plus, Save, Layers, ArrowUp, ArrowDown, Server } from 'lucide-react'; | |
| import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, PieChart, Pie, Cell, Legend } from 'recharts'; | |
| import { Toast, ToastState } from '../Toast'; | |
| const PIE_COLORS = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899', '#06b6d4']; | |
| const DEFAULT_OR_MODELS = [ | |
| { id: 'qwen/qwen3-coder:free', name: 'Qwen 3 Coder', isCustom: false }, | |
| { id: 'openai/gpt-oss-120b:free', name: 'GPT OSS 120B', isCustom: false }, | |
| { id: 'qwen/qwen3-235b-a22b:free', name: 'Qwen 3 235B', isCustom: false }, | |
| { id: 'tngtech/deepseek-r1t-chimera:free', name: 'DeepSeek R1T', isCustom: false } | |
| ]; | |
| const ALL_PROVIDERS = ['GEMINI', 'OPENROUTER', 'DOUBAO', 'GEMMA']; | |
| export const AdminPanel: React.FC = () => { | |
| const [systemConfig, setSystemConfig] = useState<SystemConfig | null>(null); | |
| const [toast, setToast] = useState<ToastState>({ show: false, message: '', type: 'success' }); | |
| // Stats | |
| const [detailedStats, setDetailedStats] = useState<{ | |
| totalCalls: number; | |
| todayCount: number; | |
| dailyTrend: {date: string, count: number}[]; | |
| modelDistribution: {name: string, value: number}[]; | |
| } | null>(null); | |
| // Key Management | |
| const [geminiKeys, setGeminiKeys] = useState<string[]>([]); | |
| const [openRouterKeys, setOpenRouterKeys] = useState<string[]>([]); | |
| const [doubaoKeys, setDoubaoKeys] = useState<string[]>([]); // New Doubao State | |
| const [newGeminiKey, setNewGeminiKey] = useState(''); | |
| const [newOpenRouterKey, setNewOpenRouterKey] = useState(''); | |
| const [newDoubaoKey, setNewDoubaoKey] = useState(''); // New Doubao Input | |
| // Model Management - OpenRouter | |
| const [orModels, setOrModels] = useState<OpenRouterModelConfig[]>([]); | |
| const [newModelId, setNewModelId] = useState(''); | |
| const [newModelName, setNewModelName] = useState(''); | |
| const [newModelApiUrl, setNewModelApiUrl] = useState(''); | |
| // Model Management - Doubao | |
| const [doubaoModels, setDoubaoModels] = useState<DoubaoModelConfig[]>([]); | |
| const [newDoubaoModelId, setNewDoubaoModelId] = useState(''); // Model ID (e.g. doubao-pro) | |
| const [newDoubaoEndpointId, setNewDoubaoEndpointId] = useState(''); // Endpoint ID (e.g. ep-xxxx) | |
| const [newDoubaoModelName, setNewDoubaoModelName] = useState(''); | |
| // Provider Priority | |
| const [providerOrder, setProviderOrder] = useState<string[]>(ALL_PROVIDERS); | |
| useEffect(() => { | |
| loadData(); | |
| }, []); | |
| const loadData = async () => { | |
| try { | |
| const cfg = await api.config.get(); | |
| setSystemConfig(cfg); | |
| if (cfg.apiKeys) { | |
| setGeminiKeys(cfg.apiKeys.gemini || []); | |
| setOpenRouterKeys(cfg.apiKeys.openrouter || []); | |
| setDoubaoKeys(cfg.apiKeys.doubao || []); // Load Doubao keys | |
| } | |
| setOrModels(cfg.openRouterModels && cfg.openRouterModels.length > 0 ? cfg.openRouterModels : DEFAULT_OR_MODELS); | |
| // Load Doubao Models | |
| setDoubaoModels(cfg.doubaoModels || []); | |
| // Fix: Merge saved order with any new providers (like DOUBAO) that might be missing from old configs | |
| let currentOrder = cfg.aiProviderOrder || []; | |
| if (currentOrder.length === 0) { | |
| currentOrder = [...ALL_PROVIDERS]; | |
| } else { | |
| // Check if any standard provider is missing from the saved config and append it | |
| ALL_PROVIDERS.forEach(p => { | |
| if (!currentOrder.includes(p)) { | |
| currentOrder.push(p); | |
| } | |
| }); | |
| } | |
| setProviderOrder(currentOrder); | |
| const stats = await api.ai.getStats(); | |
| setDetailedStats(stats); | |
| } catch (e) { | |
| console.error("Failed to load admin data", e); | |
| } | |
| }; | |
| const toggleSystemAI = async () => { | |
| if (!systemConfig) return; | |
| try { | |
| const newVal = !systemConfig.enableAI; | |
| await api.config.save({ ...systemConfig, enableAI: newVal }); | |
| setSystemConfig({ ...systemConfig, enableAI: newVal }); | |
| setToast({ show: true, message: `AI 服务已${newVal ? '开启' : '关闭'}`, type: 'success' }); | |
| } catch (e) { | |
| setToast({ show: true, message: '操作失败', type: 'error' }); | |
| } | |
| }; | |
| const handleAddKey = (type: 'gemini' | 'openrouter' | 'doubao') => { | |
| const keyMap = { gemini: newGeminiKey, openrouter: newOpenRouterKey, doubao: newDoubaoKey }; | |
| const key = keyMap[type].trim(); | |
| if (!key) return; | |
| if (type === 'gemini') { setGeminiKeys([...geminiKeys, key]); setNewGeminiKey(''); } | |
| else if (type === 'openrouter') { setOpenRouterKeys([...openRouterKeys, key]); setNewOpenRouterKey(''); } | |
| else if (type === 'doubao') { setDoubaoKeys([...doubaoKeys, key]); setNewDoubaoKey(''); } | |
| }; | |
| const removeKey = (type: 'gemini' | 'openrouter' | 'doubao', index: number) => { | |
| if (type === 'gemini') setGeminiKeys(geminiKeys.filter((_, i) => i !== index)); | |
| else if (type === 'openrouter') setOpenRouterKeys(openRouterKeys.filter((_, i) => i !== index)); | |
| else if (type === 'doubao') setDoubaoKeys(doubaoKeys.filter((_, i) => i !== index)); | |
| }; | |
| // --- Model Management Functions --- | |
| // OpenRouter | |
| const handleAddModel = () => { | |
| if (!newModelId.trim()) return; | |
| setOrModels([...orModels, { | |
| id: newModelId.trim(), | |
| name: newModelName.trim() || newModelId.trim(), | |
| apiUrl: newModelApiUrl.trim() || undefined, | |
| isCustom: true | |
| }]); | |
| setNewModelId(''); setNewModelName(''); setNewModelApiUrl(''); | |
| }; | |
| const handleRemoveModel = (idx: number) => setOrModels(orModels.filter((_, i) => i !== idx)); | |
| const handleMoveModel = (idx: number, direction: -1 | 1) => { | |
| const newArr = [...orModels]; const targetIdx = idx + direction; | |
| if (targetIdx < 0 || targetIdx >= newArr.length) return; | |
| [newArr[idx], newArr[targetIdx]] = [newArr[targetIdx], newArr[idx]]; | |
| setOrModels(newArr); | |
| }; | |
| // Doubao | |
| const handleAddDoubaoModel = () => { | |
| if (!newDoubaoEndpointId.trim()) return; // Endpoint ID is crucial | |
| setDoubaoModels([...doubaoModels, { | |
| modelId: newDoubaoModelId.trim() || newDoubaoEndpointId.trim(), | |
| endpointId: newDoubaoEndpointId.trim(), | |
| name: newDoubaoModelName.trim() || newDoubaoModelId.trim() || 'Doubao Model' | |
| }]); | |
| setNewDoubaoModelId(''); setNewDoubaoEndpointId(''); setNewDoubaoModelName(''); | |
| }; | |
| const handleRemoveDoubaoModel = (idx: number) => setDoubaoModels(doubaoModels.filter((_, i) => i !== idx)); | |
| const handleMoveDoubaoModel = (idx: number, direction: -1 | 1) => { | |
| const newArr = [...doubaoModels]; const targetIdx = idx + direction; | |
| if (targetIdx < 0 || targetIdx >= newArr.length) return; | |
| [newArr[idx], newArr[targetIdx]] = [newArr[targetIdx], newArr[idx]]; | |
| setDoubaoModels(newArr); | |
| }; | |
| const handleMoveProviderOrder = (idx: number, direction: -1 | 1) => { | |
| const newArr = [...providerOrder]; const targetIdx = idx + direction; | |
| if (targetIdx < 0 || targetIdx >= newArr.length) return; | |
| [newArr[idx], newArr[targetIdx]] = [newArr[targetIdx], newArr[idx]]; | |
| setProviderOrder(newArr); | |
| }; | |
| const saveApiKeys = async () => { | |
| if (!systemConfig) return; | |
| try { | |
| await api.config.save({ | |
| ...systemConfig, | |
| apiKeys: { gemini: geminiKeys, openrouter: openRouterKeys, doubao: doubaoKeys }, | |
| openRouterModels: orModels, | |
| doubaoModels: doubaoModels, // NEW | |
| aiProviderOrder: providerOrder | |
| }); | |
| await api.ai.resetPool(); | |
| setToast({ show: true, message: 'API 配置及模型列表已保存', type: 'success' }); | |
| } catch (e) { setToast({ show: true, message: '保存失败', type: 'error' }); } | |
| }; | |
| return ( | |
| <div className="p-6 md:p-10 max-w-5xl mx-auto space-y-8 animate-in fade-in pb-20 overflow-y-auto h-full custom-scrollbar"> | |
| {toast.show && <Toast message={toast.message} type={toast.type} onClose={()=>setToast({...toast, show: false})}/>} | |
| <div className="flex items-center gap-4 border-b pb-6"> | |
| <div className="p-3 bg-blue-100 rounded-2xl text-blue-600"> | |
| <Bot size={32} /> | |
| </div> | |
| <div> | |
| <h1 className="text-2xl font-bold text-gray-800">AI 智能助教管理后台</h1> | |
| <p className="text-gray-500">监控 AI 服务状态与用量,管理密钥池。</p> | |
| </div> | |
| </div> | |
| <div className="grid grid-cols-1 md:grid-cols-2 gap-6"> | |
| {/* STATS PANEL */} | |
| <div className="bg-white p-6 rounded-xl border border-gray-100 shadow-sm md:col-span-2"> | |
| <h3 className="font-bold text-gray-800 mb-6 flex items-center"><Activity className="mr-2 text-purple-500"/> 调用数据分析</h3> | |
| <div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8"> | |
| <div className="bg-purple-50 p-4 rounded-xl text-center"> | |
| <div className="text-3xl font-black text-purple-600">{detailedStats?.totalCalls || 0}</div> | |
| <div className="text-xs text-purple-700 font-bold uppercase mt-1">历史累计调用</div> | |
| </div> | |
| <div className="bg-blue-50 p-4 rounded-xl text-center"> | |
| <div className="text-3xl font-black text-blue-600">{detailedStats?.todayCount || 0}</div> | |
| <div className="text-xs text-blue-700 font-bold uppercase mt-1">今日调用次数</div> | |
| </div> | |
| <div className="md:col-span-2 bg-gray-50 p-4 rounded-xl flex items-center justify-center text-gray-500 text-sm"> | |
| <span className="mr-2 font-bold">当前状态:</span> | |
| {systemConfig?.enableAI ? <span className="text-green-600 font-bold flex items-center"><CheckCircle size={14} className="mr-1"/> 服务运行中</span> : <span className="text-red-500 font-bold flex items-center"><X size={14} className="mr-1"/> 服务已暂停</span>} | |
| </div> | |
| </div> | |
| <div className="grid grid-cols-1 md:grid-cols-2 gap-8"> | |
| <div className="h-64"> | |
| <h4 className="text-sm font-bold text-gray-600 mb-4 flex items-center"><BarChart2 size={16} className="mr-2"/> 近7日调用趋势</h4> | |
| <ResponsiveContainer width="100%" height="100%"> | |
| <BarChart data={detailedStats?.dailyTrend || []}><CartesianGrid strokeDasharray="3 3" vertical={false}/><XAxis dataKey="date" tick={{fontSize: 10}} tickFormatter={(val) => val.slice(5)}/><YAxis allowDecimals={false}/><Tooltip cursor={{fill: 'transparent'}} contentStyle={{borderRadius: '8px', border: 'none', boxShadow: '0 4px 12px rgba(0,0,0,0.1)'}}/><Bar dataKey="count" fill="#8b5cf6" radius={[4, 4, 0, 0]} name="调用次数" /></BarChart> | |
| </ResponsiveContainer> | |
| </div> | |
| <div className="h-64"> | |
| <h4 className="text-sm font-bold text-gray-600 mb-4 flex items-center"><Brain size={16} className="mr-2"/> 模型使用分布</h4> | |
| <ResponsiveContainer width="100%" height="100%"> | |
| <PieChart><Pie data={detailedStats?.modelDistribution || []} cx="50%" cy="50%" innerRadius={60} outerRadius={80} paddingAngle={5} dataKey="value">{(detailedStats?.modelDistribution || []).map((entry, index) => (<Cell key={`cell-${index}`} fill={PIE_COLORS[index % PIE_COLORS.length]} />))}</Pie><Tooltip /><Legend layout="vertical" verticalAlign="middle" align="right" wrapperStyle={{fontSize: '10px'}}/></PieChart> | |
| </ResponsiveContainer> | |
| </div> | |
| </div> | |
| </div> | |
| <div className="bg-white p-6 rounded-xl border border-gray-100 shadow-sm"> | |
| <h3 className="font-bold text-gray-800 mb-4 flex items-center"><ShieldCheck className="mr-2 text-green-500"/> 服务控制</h3> | |
| <div className="flex items-center justify-between bg-gray-50 p-4 rounded-xl"> | |
| <div><p className="font-bold text-gray-700">AI 服务总开关</p><p className="text-xs text-gray-500">关闭后所有用户将无法使用 AI 功能</p></div> | |
| <button onClick={toggleSystemAI} className={`flex items-center gap-2 px-4 py-2 rounded-lg font-bold transition-colors ${systemConfig?.enableAI ? 'bg-green-100 text-green-700 hover:bg-green-200' : 'bg-red-100 text-red-700 hover:bg-red-200'}`}><Power size={18}/> {systemConfig?.enableAI ? '运行中' : '已停止'}</button> | |
| </div> | |
| </div> | |
| </div> | |
| <div className="bg-white p-6 rounded-xl border border-gray-100 shadow-sm"> | |
| <div className="flex justify-between items-center mb-6"><h3 className="font-bold text-gray-800 flex items-center"><Key className="mr-2 text-amber-500"/> 多线路密钥池配置</h3><button onClick={saveApiKeys} className="bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-bold flex items-center gap-2 hover:bg-blue-700 shadow-sm"><Save size={16}/> 保存所有配置</button></div> | |
| <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8"> | |
| {/* Gemini Keys */} | |
| <div className="bg-gray-50/50 p-3 rounded-xl border border-gray-100"> | |
| <div className="flex items-center justify-between mb-2"><label className="text-sm font-bold text-gray-700">Google Gemini / Gemma</label><span className="text-xs bg-blue-100 text-blue-700 px-2 py-0.5 rounded-full">{geminiKeys.length} 个</span></div> | |
| <p className="text-[10px] text-gray-400 mb-3">当额度耗尽时自动切换。</p> | |
| <div className="space-y-2 mb-3 max-h-32 overflow-y-auto custom-scrollbar">{geminiKeys.map((k, idx) => (<div key={idx} className="flex gap-2 items-center bg-white p-2 rounded border border-gray-200"><div className="flex-1 font-mono text-xs text-gray-600 truncate">{k.substring(0, 8)}...{k.substring(k.length - 6)}</div><button onClick={() => removeKey('gemini', idx)} className="text-gray-400 hover:text-red-500"><Trash2 size={14}/></button></div>))}</div> | |
| <div className="flex gap-2"><input className="flex-1 border border-gray-300 rounded px-2 py-1 text-xs outline-none focus:ring-1 focus:ring-blue-500" placeholder="输入 Gemini API Key" value={newGeminiKey} onChange={e => setNewGeminiKey(e.target.value)}/><button onClick={() => handleAddKey('gemini')} className="bg-white hover:bg-gray-100 text-gray-600 px-2 py-1 rounded border border-gray-300"><Plus size={14}/></button></div> | |
| </div> | |
| {/* Doubao Keys */} | |
| <div className="bg-gray-50/50 p-3 rounded-xl border border-gray-100"> | |
| <div className="flex items-center justify-between mb-2"><label className="text-sm font-bold text-gray-700">Doubao (豆包/火山引擎)</label><span className="text-xs bg-green-100 text-green-700 px-2 py-0.5 rounded-full">{doubaoKeys.length} 个</span></div> | |
| <p className="text-[10px] text-gray-400 mb-3">使用原生 Axios 调用。</p> | |
| <div className="space-y-2 mb-3 max-h-32 overflow-y-auto custom-scrollbar">{doubaoKeys.map((k, idx) => (<div key={idx} className="flex gap-2 items-center bg-white p-2 rounded border border-gray-200"><div className="flex-1 font-mono text-xs text-gray-600 truncate">{k.substring(0, 8)}...{k.substring(k.length - 6)}</div><button onClick={() => removeKey('doubao', idx)} className="text-gray-400 hover:text-red-500"><Trash2 size={14}/></button></div>))}</div> | |
| <div className="flex gap-2"><input className="flex-1 border border-gray-300 rounded px-2 py-1 text-xs outline-none focus:ring-1 focus:ring-green-500" placeholder="输入 Doubao API Key" value={newDoubaoKey} onChange={e => setNewDoubaoKey(e.target.value)}/><button onClick={() => handleAddKey('doubao')} className="bg-white hover:bg-gray-100 text-gray-600 px-2 py-1 rounded border border-gray-300"><Plus size={14}/></button></div> | |
| </div> | |
| {/* OpenRouter Keys */} | |
| <div className="bg-gray-50/50 p-3 rounded-xl border border-gray-100"> | |
| <div className="flex items-center justify-between mb-2"><label className="text-sm font-bold text-gray-700">OpenRouter (通用)</label><span className="text-xs bg-purple-100 text-purple-700 px-2 py-0.5 rounded-full">{openRouterKeys.length} 个</span></div> | |
| <p className="text-[10px] text-gray-400 mb-3">备用线路,支持自定义模型。</p> | |
| <div className="space-y-2 mb-3 max-h-32 overflow-y-auto custom-scrollbar">{openRouterKeys.map((k, idx) => (<div key={idx} className="flex gap-2 items-center bg-white p-2 rounded border border-gray-200"><div className="flex-1 font-mono text-xs text-gray-600 truncate">{k.substring(0, 8)}...{k.substring(k.length - 6)}</div><button onClick={() => removeKey('openrouter', idx)} className="text-gray-400 hover:text-red-500"><Trash2 size={14}/></button></div>))}</div> | |
| <div className="flex gap-2"><input className="flex-1 border border-gray-300 rounded px-2 py-1 text-xs outline-none focus:ring-1 focus:ring-purple-500" placeholder="输入 OpenRouter Key" value={newOpenRouterKey} onChange={e => setNewOpenRouterKey(e.target.value)}/><button onClick={() => handleAddKey('openrouter')} className="bg-white hover:bg-gray-100 text-gray-600 px-2 py-1 rounded border border-gray-300"><Plus size={14}/></button></div> | |
| </div> | |
| </div> | |
| {/* Provider Order Management */} | |
| <div className="mt-8 border-t border-gray-100 pt-6"> | |
| <div className="flex justify-between items-center mb-4"><h4 className="font-bold text-gray-700 text-sm flex items-center"><Layers className="mr-2" size={16}/> 大模型调用优先级</h4></div> | |
| <div className="bg-amber-50 p-4 rounded-lg border border-amber-100 mb-4"> | |
| <p className="text-xs text-amber-800">系统将按照以下顺序尝试调用大模型。如果前一个服务商额度耗尽或报错,会自动切换到下一个。</p> | |
| </div> | |
| <div className="space-y-2 max-w-md"> | |
| {providerOrder.map((provider, idx) => ( | |
| <div key={provider} className="flex items-center gap-3 bg-white p-3 rounded-lg border border-gray-200 shadow-sm"> | |
| <div className="bg-gray-100 text-gray-500 w-6 h-6 flex items-center justify-center rounded-full text-xs font-bold">{idx + 1}</div> | |
| <div className="flex-1 font-bold text-gray-700">{provider}</div> | |
| <div className="flex gap-1"> | |
| <button onClick={() => handleMoveProviderOrder(idx, -1)} disabled={idx === 0} className="p-1 hover:bg-gray-100 rounded text-gray-400 hover:text-blue-500 disabled:opacity-30"><ArrowUp size={16}/></button> | |
| <button onClick={() => handleMoveProviderOrder(idx, 1)} disabled={idx === providerOrder.length - 1} className="p-1 hover:bg-gray-100 rounded text-gray-400 hover:text-blue-500 disabled:opacity-30"><ArrowDown size={16}/></button> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| {/* DOUBAO MODEL MANAGEMENT */} | |
| <div className="mt-8 border-t border-gray-100 pt-6"> | |
| <div className="flex justify-between items-center mb-4"><h4 className="font-bold text-gray-700 text-sm flex items-center"><Server className="mr-2" size={16}/> 豆包/火山引擎模型管理</h4></div> | |
| <div className="bg-blue-50 border border-blue-100 p-3 rounded-lg mb-4 text-xs text-blue-700"> | |
| <p className="font-bold mb-1">配置说明:</p> | |
| <ul className="list-disc list-inside space-y-1"> | |
| <li><b>接入点 ID (Endpoint ID):</b> 必填。API 调用核心凭证,格式如 <code>ep-2024060401xxxx-xxxxx</code>。</li> | |
| <li><b>模型 ID (Model ID):</b> 选填。模型本身的名称(如 <code>doubao-pro-32k</code>),用于记录统计。</li> | |
| </ul> | |
| </div> | |
| <div className="space-y-2 mb-4 bg-gray-50 p-3 rounded-lg border border-gray-200"> | |
| {doubaoModels.length === 0 ? <div className="text-center text-gray-400 text-xs py-2">暂无配置模型,请在下方添加。</div> : doubaoModels.map((m, idx) => ( | |
| <div key={idx} className="flex items-center gap-2 bg-white p-2 rounded border border-gray-100 shadow-sm"> | |
| <div className="flex flex-col gap-0.5 px-1"> | |
| <button onClick={()=>handleMoveDoubaoModel(idx, -1)} className="text-gray-400 hover:text-blue-500 disabled:opacity-30" disabled={idx===0}><ArrowUp size={12}/></button> | |
| <button onClick={()=>handleMoveDoubaoModel(idx, 1)} className="text-gray-400 hover:text-blue-500 disabled:opacity-30" disabled={idx===doubaoModels.length-1}><ArrowDown size={12}/></button> | |
| </div> | |
| <div className="flex-1 min-w-0"> | |
| <div className="text-sm font-bold text-gray-800">{m.name || 'Doubao Model'}</div> | |
| <div className="flex flex-col md:flex-row gap-2 mt-1"> | |
| <div className="text-xs text-blue-600 bg-blue-50 px-1.5 py-0.5 rounded font-mono truncate" title="Endpoint ID">EP: {m.endpointId}</div> | |
| <div className="text-xs text-gray-500 bg-gray-100 px-1.5 py-0.5 rounded font-mono truncate" title="Model ID">ID: {m.modelId || '-'}</div> | |
| </div> | |
| </div> | |
| <div className="flex items-center gap-2"> | |
| {idx === 0 && <span className="text-[10px] bg-green-50 text-green-600 px-2 py-0.5 rounded font-bold">默认</span>} | |
| <button onClick={() => handleRemoveDoubaoModel(idx)} className="p-1.5 rounded text-gray-400 hover:text-red-500 hover:bg-red-50"><Trash2 size={16}/></button> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| <div className="grid grid-cols-1 md:grid-cols-3 gap-2 items-end bg-gray-50 p-3 rounded-lg border border-gray-200"> | |
| <div> | |
| <label className="text-xs text-gray-500 mb-1 block">接入点 ID (Endpoint ID) *</label> | |
| <input className="w-full border border-gray-300 rounded px-2 py-1.5 text-sm" value={newDoubaoEndpointId} onChange={e=>setNewDoubaoEndpointId(e.target.value)} placeholder="如: ep-2024xxxx-xxxxx"/> | |
| </div> | |
| <div> | |
| <label className="text-xs text-gray-500 mb-1 block">模型 ID (Model ID)</label> | |
| <input className="w-full border border-gray-300 rounded px-2 py-1.5 text-sm" value={newDoubaoModelId} onChange={e=>setNewDoubaoModelId(e.target.value)} placeholder="如: doubao-pro-32k"/> | |
| </div> | |
| <div className="flex gap-2"> | |
| <div className="flex-1"> | |
| <label className="text-xs text-gray-500 mb-1 block">显示名称</label> | |
| <input className="w-full border border-gray-300 rounded px-2 py-1.5 text-sm" value={newDoubaoModelName} onChange={e=>setNewDoubaoModelName(e.target.value)} placeholder="如: 豆包 Pro"/> | |
| </div> | |
| <button onClick={handleAddDoubaoModel} className="bg-indigo-600 text-white px-4 py-1.5 rounded text-sm hover:bg-indigo-700 h-9 mb-[1px]">添加</button> | |
| </div> | |
| </div> | |
| </div> | |
| {/* OPENROUTER MODEL MANAGEMENT */} | |
| <div className="mt-8 border-t border-gray-100 pt-6"> | |
| <div className="flex justify-between items-center mb-4"><h4 className="font-bold text-gray-700 text-sm">OpenAI 格式大模型列表管理</h4></div> | |
| <div className="space-y-2 mb-4 bg-gray-50 p-3 rounded-lg border border-gray-200">{orModels.map((m, idx) => (<div key={idx} className="flex items-center gap-2 bg-white p-2 rounded border border-gray-100 shadow-sm"><div className="flex flex-col gap-0.5 px-1"><button onClick={()=>handleMoveModel(idx, -1)} className="text-gray-400 hover:text-blue-500 disabled:opacity-30" disabled={idx===0}><ArrowUp size={12}/></button><button onClick={()=>handleMoveModel(idx, 1)} className="text-gray-400 hover:text-blue-500 disabled:opacity-30" disabled={idx===orModels.length-1}><ArrowDown size={12}/></button></div><div className="flex-1 min-w-0"><div className="text-sm font-bold text-gray-800">{m.name || m.id}</div><div className="text-xs text-gray-400 font-mono truncate" title={m.id}>ID: {m.id}</div>{m.apiUrl && <div className="text-[10px] text-blue-500 truncate" title={m.apiUrl}>API: {m.apiUrl}</div>}</div><div className="flex items-center gap-2">{m.isCustom ? (<span className="text-[10px] bg-blue-50 text-blue-600 px-2 py-0.5 rounded">自定义</span>) : (<span className="text-[10px] bg-gray-100 text-gray-500 px-2 py-0.5 rounded">内置</span>)}<button onClick={() => handleRemoveModel(idx)} className={`p-1.5 rounded transition-colors ${m.isCustom ? 'text-gray-400 hover:text-red-500 hover:bg-red-50' : 'text-gray-200 cursor-not-allowed'}`} disabled={!m.isCustom}><Trash2 size={16}/></button></div></div>))}</div> | |
| <div className="flex flex-col md:flex-row gap-2 items-end bg-gray-50 p-3 rounded-lg border border-gray-200"> | |
| <div className="flex-1 w-full"><label className="text-xs text-gray-500 mb-1 block">模型 ID *</label><input className="w-full border border-gray-300 rounded px-2 py-1.5 text-sm" value={newModelId} onChange={e=>setNewModelId(e.target.value)} placeholder="如: gpt-4o"/></div> | |
| <div className="flex-1 w-full"><label className="text-xs text-gray-500 mb-1 block">显示名称</label><input className="w-full border border-gray-300 rounded px-2 py-1.5 text-sm" value={newModelName} onChange={e=>setNewModelName(e.target.value)} placeholder="如: GPT-4o"/></div> | |
| <div className="flex-[1.5] w-full"><label className="text-xs text-gray-500 mb-1 block">API URL (选填, 默认 OpenRouter)</label><input className="w-full border border-gray-300 rounded px-2 py-1.5 text-sm" value={newModelApiUrl} onChange={e=>setNewModelApiUrl(e.target.value)} placeholder="https://api.openai.com/v1"/></div> | |
| <button onClick={handleAddModel} className="bg-indigo-600 text-white px-4 py-1.5 rounded text-sm hover:bg-indigo-700 h-9 w-full md:w-auto">添加</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| }; |