Spaces:
Sleeping
Sleeping
| import React, { useState, useEffect } from 'react'; | |
| import { AiModel, AiRole, ApiChannel, ModelConfigManager } from '../constants'; | |
| import { | |
| Settings, | |
| Plus, | |
| Edit3, | |
| Trash2, | |
| Save, | |
| X, | |
| Download, | |
| Upload, | |
| RefreshCw, | |
| Bot, | |
| Brain, | |
| Image, | |
| Zap, | |
| AlertCircle, | |
| Check, | |
| HardDrive, | |
| Globe, | |
| Key, | |
| Eye, | |
| EyeOff, | |
| Star, | |
| StarOff, | |
| Shield, | |
| Lock | |
| } from 'lucide-react'; | |
| interface ModelConfigManagerProps { | |
| isOpen: boolean; | |
| onClose: () => void; | |
| onConfigChange: () => void; | |
| } | |
| interface EditingChannel extends Partial<ApiChannel> { | |
| isNew?: boolean; | |
| } | |
| interface EditingModel extends Partial<AiModel> { | |
| isNew?: boolean; | |
| } | |
| interface EditingRole extends Partial<AiRole> { | |
| isNew?: boolean; | |
| } | |
| const ModelConfigManagerComponent: React.FC<ModelConfigManagerProps> = ({ | |
| isOpen, | |
| onClose, | |
| onConfigChange | |
| }) => { | |
| const [activeTab, setActiveTab] = useState<'channels' | 'models' | 'roles' | 'storage'>('channels'); | |
| const [channels, setChannels] = useState<ApiChannel[]>([]); | |
| const [models, setModels] = useState<AiModel[]>([]); | |
| const [roles, setRoles] = useState<AiRole[]>([]); | |
| const [editingChannel, setEditingChannel] = useState<EditingChannel | null>(null); | |
| const [editingModel, setEditingModel] = useState<EditingModel | null>(null); | |
| const [editingRole, setEditingRole] = useState<EditingRole | null>(null); | |
| const [validationErrors, setValidationErrors] = useState<string[]>([]); | |
| const [importText, setImportText] = useState(''); | |
| const [showImport, setShowImport] = useState(false); | |
| const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null); | |
| const [storageInfo, setStorageInfo] = useState<{ used: number; available: number; channels: number; models: number; roles: number }>({ used: 0, available: 0, channels: 0, models: 0, roles: 0 }); | |
| const [showApiKeys, setShowApiKeys] = useState<Record<string, boolean>>({}); | |
| useEffect(() => { | |
| if (isOpen) { | |
| loadConfig(); | |
| updateStorageInfo(); | |
| } | |
| }, [isOpen]); | |
| const loadConfig = () => { | |
| try { | |
| setChannels(ModelConfigManager.getChannels()); | |
| setModels(ModelConfigManager.getModels()); | |
| setRoles(ModelConfigManager.getRoles()); | |
| } catch (error) { | |
| console.error('加载配置失败:', error); | |
| showMessage('error', '加载配置失败,请检查浏览器存储设置'); | |
| } | |
| }; | |
| const updateStorageInfo = () => { | |
| try { | |
| setStorageInfo(ModelConfigManager.getStorageInfo()); | |
| } catch (error) { | |
| console.error('获取存储信息失败:', error); | |
| } | |
| }; | |
| const showMessage = (type: 'success' | 'error', text: string) => { | |
| setMessage({ type, text }); | |
| setTimeout(() => setMessage(null), 3000); | |
| }; | |
| const toggleApiKeyVisibility = (channelId: string) => { | |
| setShowApiKeys(prev => ({ | |
| ...prev, | |
| [channelId]: !prev[channelId] | |
| })); | |
| }; | |
| // 获取渠道显示的API密钥内容 | |
| const getDisplayApiKey = (channel: ApiChannel): string => { | |
| if (channel.isProtected) { | |
| return '●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●● (受保护)'; | |
| } | |
| return channel.apiKey || '未设置'; | |
| }; | |
| // 检查用户是否可以修改此渠道 | |
| const canEditChannel = (channel: ApiChannel): boolean => { | |
| return channel.isCustom || !channel.isProtected; | |
| }; | |
| // ============ 渠道管理 ============ | |
| const handleSaveChannel = () => { | |
| if (!editingChannel) return; | |
| const errors = ModelConfigManager.validateChannel(editingChannel); | |
| if (errors.length > 0) { | |
| setValidationErrors(errors); | |
| return; | |
| } | |
| try { | |
| if (editingChannel.isNew) { | |
| const { id, createdAt, isCustom, isNew, ...channelData } = editingChannel; | |
| ModelConfigManager.addChannel(channelData as Omit<ApiChannel, 'id' | 'createdAt' | 'isCustom'>); | |
| showMessage('success', '渠道添加成功'); | |
| } else { | |
| ModelConfigManager.updateChannel(editingChannel.id!, editingChannel); | |
| showMessage('success', '渠道更新成功'); | |
| } | |
| loadConfig(); | |
| updateStorageInfo(); | |
| setEditingChannel(null); | |
| setValidationErrors([]); | |
| onConfigChange(); | |
| } catch (error) { | |
| showMessage('error', '保存渠道失败: ' + (error instanceof Error ? error.message : '未知错误')); | |
| } | |
| }; | |
| const handleDeleteChannel = (id: string) => { | |
| // 检查是否有模型使用此渠道 | |
| const modelsUsingChannel = models.filter(model => model.channelId === id); | |
| if (modelsUsingChannel.length > 0) { | |
| showMessage('error', `无法删除渠道:有 ${modelsUsingChannel.length} 个模型正在使用此渠道`); | |
| return; | |
| } | |
| if (window.confirm('确定要删除这个渠道吗?此操作不可撤销。')) { | |
| try { | |
| ModelConfigManager.deleteChannel(id); | |
| loadConfig(); | |
| updateStorageInfo(); | |
| showMessage('success', '渠道删除成功'); | |
| onConfigChange(); | |
| } catch (error) { | |
| showMessage('error', '删除渠道失败: ' + (error instanceof Error ? error.message : '未知错误')); | |
| } | |
| } | |
| }; | |
| const handleSetDefaultChannel = (id: string) => { | |
| try { | |
| ModelConfigManager.updateChannel(id, { isDefault: true }); | |
| loadConfig(); | |
| showMessage('success', '默认渠道设置成功'); | |
| } catch (error) { | |
| showMessage('error', '设置默认渠道失败'); | |
| } | |
| }; | |
| const handleEditChannel = (channel: ApiChannel) => { | |
| if (channel.isProtected && !channel.isCustom) { | |
| // 对于受保护的预置渠道,创建一个编辑副本,但不显示真实的API密钥 | |
| setEditingChannel({ | |
| ...channel, | |
| apiKey: '' // 不显示受保护的密钥 | |
| }); | |
| } else { | |
| setEditingChannel(channel); | |
| } | |
| }; | |
| // ============ 模型管理 ============ | |
| const handleSaveModel = () => { | |
| if (!editingModel) return; | |
| const errors = ModelConfigManager.validateModel(editingModel); | |
| if (errors.length > 0) { | |
| setValidationErrors(errors); | |
| return; | |
| } | |
| try { | |
| if (editingModel.isNew) { | |
| const { id, createdAt, isCustom, isNew, ...modelData } = editingModel; | |
| ModelConfigManager.addModel(modelData as Omit<AiModel, 'id' | 'createdAt' | 'isCustom'>); | |
| showMessage('success', '模型添加成功'); | |
| } else { | |
| ModelConfigManager.updateModel(editingModel.id!, editingModel); | |
| showMessage('success', '模型更新成功'); | |
| } | |
| loadConfig(); | |
| updateStorageInfo(); | |
| setEditingModel(null); | |
| setValidationErrors([]); | |
| onConfigChange(); | |
| } catch (error) { | |
| showMessage('error', '保存模型失败: ' + (error instanceof Error ? error.message : '未知错误')); | |
| } | |
| }; | |
| const handleDeleteModel = (id: string) => { | |
| // 检查是否有角色使用此模型 | |
| const rolesUsingModel = roles.filter(role => role.modelId === id); | |
| if (rolesUsingModel.length > 0) { | |
| showMessage('error', `无法删除模型:有 ${rolesUsingModel.length} 个角色正在使用此模型`); | |
| return; | |
| } | |
| if (window.confirm('确定要删除这个模型吗?此操作不可撤销。')) { | |
| try { | |
| ModelConfigManager.deleteModel(id); | |
| loadConfig(); | |
| updateStorageInfo(); | |
| showMessage('success', '模型删除成功'); | |
| onConfigChange(); | |
| } catch (error) { | |
| showMessage('error', '删除模型失败: ' + (error instanceof Error ? error.message : '未知错误')); | |
| } | |
| } | |
| }; | |
| // ============ 角色管理 ============ | |
| const handleSaveRole = () => { | |
| if (!editingRole) return; | |
| if (!editingRole.name?.trim() || !editingRole.systemPrompt?.trim() || !editingRole.modelId) { | |
| setValidationErrors(['角色名称、系统提示词和关联模型都不能为空']); | |
| return; | |
| } | |
| try { | |
| if (editingRole.isNew) { | |
| const { id, isNew, ...roleData } = editingRole; | |
| ModelConfigManager.addRole(roleData as Omit<AiRole, 'id'>); | |
| showMessage('success', '角色添加成功'); | |
| } else { | |
| ModelConfigManager.updateRole(editingRole.id!, editingRole); | |
| showMessage('success', '角色更新成功'); | |
| } | |
| loadConfig(); | |
| updateStorageInfo(); | |
| setEditingRole(null); | |
| setValidationErrors([]); | |
| onConfigChange(); | |
| } catch (error) { | |
| showMessage('error', '保存角色失败: ' + (error instanceof Error ? error.message : '未知错误')); | |
| } | |
| }; | |
| const handleDeleteRole = (id: string) => { | |
| if (window.confirm('确定要删除这个角色吗?此操作不可撤销。')) { | |
| try { | |
| ModelConfigManager.deleteRole(id); | |
| loadConfig(); | |
| updateStorageInfo(); | |
| showMessage('success', '角色删除成功'); | |
| onConfigChange(); | |
| } catch (error) { | |
| showMessage('error', '删除角色失败: ' + (error instanceof Error ? error.message : '未知错误')); | |
| } | |
| } | |
| }; | |
| // ============ 通用操作 ============ | |
| const handleExport = () => { | |
| try { | |
| const config = ModelConfigManager.exportConfig(); | |
| const blob = new Blob([config], { type: 'application/json' }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = `multi-mind-chat-config-${new Date().toISOString().split('T')[0]}.json`; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| URL.revokeObjectURL(url); | |
| showMessage('success', '配置导出成功'); | |
| } catch (error) { | |
| showMessage('error', '导出配置失败: ' + (error instanceof Error ? error.message : '未知错误')); | |
| } | |
| }; | |
| const handleImport = () => { | |
| if (!importText.trim()) { | |
| showMessage('error', '请输入配置内容'); | |
| return; | |
| } | |
| try { | |
| const result = ModelConfigManager.importConfig(importText); | |
| if (result.success) { | |
| loadConfig(); | |
| updateStorageInfo(); | |
| setImportText(''); | |
| setShowImport(false); | |
| showMessage('success', result.message); | |
| onConfigChange(); | |
| } else { | |
| showMessage('error', result.message); | |
| } | |
| } catch (error) { | |
| showMessage('error', '导入配置时发生错误: ' + (error instanceof Error ? error.message : '未知错误')); | |
| } | |
| }; | |
| const handleReset = () => { | |
| if (window.confirm('确定要重置为默认配置吗?这将删除所有自定义配置。')) { | |
| try { | |
| ModelConfigManager.resetToDefaults(); | |
| loadConfig(); | |
| updateStorageInfo(); | |
| showMessage('success', '已重置为默认配置'); | |
| onConfigChange(); | |
| } catch (error) { | |
| showMessage('error', '重置配置失败: ' + (error instanceof Error ? error.message : '未知错误')); | |
| } | |
| } | |
| }; | |
| const handleClearAllData = () => { | |
| if (window.confirm('警告:这将清空所有配置数据,包括渠道、模型和角色!此操作不可撤销,确定继续吗?')) { | |
| try { | |
| ModelConfigManager.clearAllData(); | |
| loadConfig(); | |
| updateStorageInfo(); | |
| showMessage('success', '所有数据已清空'); | |
| onConfigChange(); | |
| } catch (error) { | |
| showMessage('error', '清空数据失败: ' + (error instanceof Error ? error.message : '未知错误')); | |
| } | |
| } | |
| }; | |
| if (!isOpen) return null; | |
| return ( | |
| <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4"> | |
| <div className="bg-gray-800 rounded-lg w-full max-w-6xl h-[90vh] flex flex-col"> | |
| {/* Header */} | |
| <div className="flex items-center justify-between p-4 border-b border-gray-700"> | |
| <div className="flex items-center space-x-2"> | |
| <Settings size={24} className="text-sky-400" /> | |
| <h2 className="text-xl font-semibold text-white">Multi-Mind Chat 配置管理</h2> | |
| </div> | |
| <div className="flex items-center space-x-2"> | |
| <button | |
| onClick={handleExport} | |
| className="px-3 py-1 bg-green-600 hover:bg-green-700 text-white rounded text-sm flex items-center space-x-1" | |
| title="导出配置" | |
| > | |
| <Download size={16} /> | |
| <span>导出</span> | |
| </button> | |
| <button | |
| onClick={() => setShowImport(!showImport)} | |
| className="px-3 py-1 bg-blue-600 hover:bg-blue-700 text-white rounded text-sm flex items-center space-x-1" | |
| title="导入配置" | |
| > | |
| <Upload size={16} /> | |
| <span>导入</span> | |
| </button> | |
| <button | |
| onClick={handleReset} | |
| className="px-3 py-1 bg-orange-600 hover:bg-orange-700 text-white rounded text-sm flex items-center space-x-1" | |
| title="重置为默认配置" | |
| > | |
| <RefreshCw size={16} /> | |
| <span>重置</span> | |
| </button> | |
| <button | |
| onClick={onClose} | |
| className="p-2 hover:bg-gray-700 rounded text-gray-400 hover:text-white" | |
| title="关闭" | |
| > | |
| <X size={20} /> | |
| </button> | |
| </div> | |
| </div> | |
| {/* Message */} | |
| {message && ( | |
| <div className={`mx-4 mt-2 p-2 rounded text-sm flex items-center space-x-2 ${ | |
| message.type === 'success' ? 'bg-green-600 text-white' : 'bg-red-600 text-white' | |
| }`}> | |
| {message.type === 'success' ? <Check size={16} /> : <AlertCircle size={16} />} | |
| <span>{message.text}</span> | |
| </div> | |
| )} | |
| {/* Import Section */} | |
| {showImport && ( | |
| <div className="mx-4 mt-2 p-4 bg-gray-700 rounded"> | |
| <h3 className="text-white mb-2">导入配置</h3> | |
| <textarea | |
| value={importText} | |
| onChange={(e) => setImportText(e.target.value)} | |
| className="w-full h-32 bg-gray-600 text-white p-2 rounded text-sm font-mono" | |
| placeholder="粘贴配置JSON内容..." | |
| /> | |
| <div className="flex justify-end space-x-2 mt-2"> | |
| <button | |
| onClick={() => setShowImport(false)} | |
| className="px-3 py-1 bg-gray-600 hover:bg-gray-500 text-white rounded text-sm" | |
| > | |
| 取消 | |
| </button> | |
| <button | |
| onClick={handleImport} | |
| className="px-3 py-1 bg-blue-600 hover:bg-blue-700 text-white rounded text-sm" | |
| > | |
| 导入 | |
| </button> | |
| </div> | |
| </div> | |
| )} | |
| {/* Tabs */} | |
| <div className="flex border-b border-gray-700"> | |
| <button | |
| onClick={() => setActiveTab('channels')} | |
| className={`px-4 py-2 text-sm font-medium transition-colors ${ | |
| activeTab === 'channels' | |
| ? 'text-sky-400 border-b-2 border-sky-400' | |
| : 'text-gray-400 hover:text-white' | |
| }`} | |
| > | |
| <div className="flex items-center space-x-2"> | |
| <Globe size={16} /> | |
| <span>API渠道配置</span> | |
| </div> | |
| </button> | |
| <button | |
| onClick={() => setActiveTab('models')} | |
| className={`px-4 py-2 text-sm font-medium transition-colors ${ | |
| activeTab === 'models' | |
| ? 'text-sky-400 border-b-2 border-sky-400' | |
| : 'text-gray-400 hover:text-white' | |
| }`} | |
| > | |
| <div className="flex items-center space-x-2"> | |
| <Brain size={16} /> | |
| <span>AI模型配置</span> | |
| </div> | |
| </button> | |
| <button | |
| onClick={() => setActiveTab('roles')} | |
| className={`px-4 py-2 text-sm font-medium transition-colors ${ | |
| activeTab === 'roles' | |
| ? 'text-sky-400 border-b-2 border-sky-400' | |
| : 'text-gray-400 hover:text-white' | |
| }`} | |
| > | |
| <div className="flex items-center space-x-2"> | |
| <Bot size={16} /> | |
| <span>AI角色配置</span> | |
| </div> | |
| </button> | |
| <button | |
| onClick={() => setActiveTab('storage')} | |
| className={`px-4 py-2 text-sm font-medium transition-colors ${ | |
| activeTab === 'storage' | |
| ? 'text-sky-400 border-b-2 border-sky-400' | |
| : 'text-gray-400 hover:text-white' | |
| }`} | |
| > | |
| <div className="flex items-center space-x-2"> | |
| <HardDrive size={16} /> | |
| <span>存储管理</span> | |
| </div> | |
| </button> | |
| </div> | |
| {/* Content */} | |
| <div className="flex-1 overflow-hidden"> | |
| {activeTab === 'channels' ? ( | |
| <div className="h-full flex"> | |
| {/* Channels List */} | |
| <div className="w-1/2 border-r border-gray-700 overflow-y-auto"> | |
| <div className="p-4"> | |
| <div className="flex justify-between items-center mb-4"> | |
| <h3 className="text-lg font-medium text-white">API渠道列表</h3> | |
| <button | |
| onClick={() => setEditingChannel({ | |
| isNew: true, | |
| isDefault: false, | |
| isProtected: false, | |
| timeout: 30000, | |
| baseUrl: 'https://api.openai.com/v1' | |
| })} | |
| className="px-3 py-1 bg-sky-600 hover:bg-sky-700 text-white rounded text-sm flex items-center space-x-1" | |
| > | |
| <Plus size={16} /> | |
| <span>添加渠道</span> | |
| </button> | |
| </div> | |
| <div className="space-y-2"> | |
| {channels.map((channel) => ( | |
| <div key={channel.id} className="bg-gray-700 rounded p-3"> | |
| <div className="flex justify-between items-start mb-2"> | |
| <div> | |
| <div className="flex items-center space-x-2 mb-1"> | |
| <h4 className="text-white font-medium">{channel.name}</h4> | |
| {channel.isDefault && ( | |
| <Star size={16} className="text-yellow-400" title="默认渠道" /> | |
| )} | |
| {channel.isProtected && ( | |
| <Shield size={16} className="text-green-400" title="受保护配置" /> | |
| )} | |
| </div> | |
| <p className="text-gray-400 text-sm">{channel.baseUrl}</p> | |
| <div className="flex items-center space-x-2 mt-2"> | |
| <Key size={12} className="text-gray-500" /> | |
| <span className="text-gray-500 text-xs"> | |
| {channel.isProtected ? ( | |
| <span className="flex items-center space-x-1"> | |
| <Lock size={12} /> | |
| <span>受保护密钥 (已预置)</span> | |
| </span> | |
| ) : showApiKeys[channel.id] | |
| ? channel.apiKey || '未设置' | |
| : '••••••••••••••••••••••••••••••••••••••••' | |
| } | |
| </span> | |
| {!channel.isProtected && ( | |
| <button | |
| onClick={() => toggleApiKeyVisibility(channel.id)} | |
| className="text-gray-500 hover:text-gray-300" | |
| > | |
| {showApiKeys[channel.id] ? <EyeOff size={12} /> : <Eye size={12} />} | |
| </button> | |
| )} | |
| </div> | |
| {channel.description && ( | |
| <p className="text-gray-500 text-xs mt-1">{channel.description}</p> | |
| )} | |
| </div> | |
| <div className="flex space-x-1"> | |
| {!channel.isDefault && !channel.isProtected && ( | |
| <button | |
| onClick={() => handleSetDefaultChannel(channel.id)} | |
| className="p-1 text-gray-400 hover:text-yellow-400" | |
| title="设为默认" | |
| > | |
| <StarOff size={16} /> | |
| </button> | |
| )} | |
| {canEditChannel(channel) && ( | |
| <button | |
| onClick={() => handleEditChannel(channel)} | |
| className="p-1 text-gray-400 hover:text-sky-400" | |
| title="编辑" | |
| > | |
| <Edit3 size={16} /> | |
| </button> | |
| )} | |
| {channel.isCustom && ( | |
| <button | |
| onClick={() => handleDeleteChannel(channel.id)} | |
| className="p-1 text-gray-400 hover:text-red-400" | |
| title="删除" | |
| > | |
| <Trash2 size={16} /> | |
| </button> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| </div> | |
| {/* Channel Editor */} | |
| <div className="w-1/2 overflow-y-auto"> | |
| {editingChannel ? ( | |
| <div className="p-4"> | |
| <h3 className="text-lg font-medium text-white mb-4"> | |
| {editingChannel.isNew ? '添加新渠道' : '编辑渠道'} | |
| </h3> | |
| {validationErrors.length > 0 && ( | |
| <div className="mb-4 p-3 bg-red-600 rounded"> | |
| <div className="flex items-center space-x-2 mb-2"> | |
| <AlertCircle size={16} className="text-white" /> | |
| <span className="text-white font-medium">配置错误</span> | |
| </div> | |
| {validationErrors.map((error, index) => ( | |
| <p key={index} className="text-white text-sm">{error}</p> | |
| ))} | |
| </div> | |
| )} | |
| {/* 受保护渠道的提示 */} | |
| {editingChannel.isProtected && !editingChannel.isNew && ( | |
| <div className="mb-4 p-3 bg-blue-600 rounded"> | |
| <div className="flex items-center space-x-2 mb-2"> | |
| <Shield size={16} className="text-white" /> | |
| <span className="text-white font-medium">受保护配置</span> | |
| </div> | |
| <p className="text-white text-sm"> | |
| 此渠道的API密钥已预置且受到保护,您无法查看或修改密钥内容。如需使用自定义密钥,请创建新的渠道。 | |
| </p> | |
| </div> | |
| )} | |
| <div className="space-y-4"> | |
| <div> | |
| <label className="block text-gray-300 text-sm mb-1">渠道名称</label> | |
| <input | |
| type="text" | |
| value={editingChannel.name || ''} | |
| onChange={(e) => setEditingChannel({ ...editingChannel, name: e.target.value })} | |
| className="w-full bg-gray-700 text-white p-2 rounded text-sm" | |
| placeholder="例如: OpenAI 官方" | |
| /> | |
| </div> | |
| <div> | |
| <label className="block text-gray-300 text-sm mb-1">API基础URL</label> | |
| <input | |
| type="text" | |
| value={editingChannel.baseUrl || ''} | |
| onChange={(e) => setEditingChannel({ ...editingChannel, baseUrl: e.target.value })} | |
| className="w-full bg-gray-700 text-white p-2 rounded text-sm" | |
| placeholder="https://api.openai.com/v1" | |
| /> | |
| </div> | |
| <div> | |
| <label className="block text-gray-300 text-sm mb-1"> | |
| API密钥 | |
| {editingChannel.isProtected && ( | |
| <span className="ml-2 text-xs text-blue-400">(受保护,无法修改)</span> | |
| )} | |
| </label> | |
| <div className="relative"> | |
| {editingChannel.isProtected ? ( | |
| <div className="w-full bg-gray-600 text-gray-400 p-2 rounded text-sm flex items-center space-x-2"> | |
| <Lock size={16} /> | |
| <span>●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●● (受保护)</span> | |
| </div> | |
| ) : ( | |
| <input | |
| type="password" | |
| value={editingChannel.apiKey || ''} | |
| onChange={(e) => setEditingChannel({ ...editingChannel, apiKey: e.target.value })} | |
| className="w-full bg-gray-700 text-white p-2 rounded text-sm" | |
| placeholder="输入API密钥" | |
| /> | |
| )} | |
| </div> | |
| </div> | |
| <div> | |
| <label className="block text-gray-300 text-sm mb-1">描述(可选)</label> | |
| <textarea | |
| value={editingChannel.description || ''} | |
| onChange={(e) => setEditingChannel({ ...editingChannel, description: e.target.value })} | |
| className="w-full bg-gray-700 text-white p-2 rounded text-sm h-20" | |
| placeholder="渠道用途描述..." | |
| /> | |
| </div> | |
| <div> | |
| <label className="block text-gray-300 text-sm mb-1">超时时间(毫秒)</label> | |
| <input | |
| type="number" | |
| value={editingChannel.timeout || 30000} | |
| onChange={(e) => setEditingChannel({ ...editingChannel, timeout: parseInt(e.target.value) || 30000 })} | |
| className="w-full bg-gray-700 text-white p-2 rounded text-sm" | |
| min="1000" | |
| max="120000" | |
| /> | |
| </div> | |
| <div className="space-y-2"> | |
| <label className="flex items-center space-x-2"> | |
| <input | |
| type="checkbox" | |
| checked={editingChannel.isDefault || false} | |
| onChange={(e) => setEditingChannel({ ...editingChannel, isDefault: e.target.checked })} | |
| className="rounded" | |
| /> | |
| <span className="text-gray-300 text-sm">设为默认渠道</span> | |
| </label> | |
| </div> | |
| <div className="flex justify-end space-x-2 pt-4"> | |
| <button | |
| onClick={() => { | |
| setEditingChannel(null); | |
| setValidationErrors([]); | |
| }} | |
| className="px-4 py-2 bg-gray-600 hover:bg-gray-500 text-white rounded text-sm" | |
| > | |
| 取消 | |
| </button> | |
| <button | |
| onClick={handleSaveChannel} | |
| className="px-4 py-2 bg-sky-600 hover:bg-sky-700 text-white rounded text-sm flex items-center space-x-1" | |
| > | |
| <Save size={16} /> | |
| <span>保存</span> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| ) : ( | |
| <div className="p-4 h-full flex items-center justify-center"> | |
| <p className="text-gray-400">选择一个渠道进行编辑,或添加新渠道</p> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ) : activeTab === 'models' ? ( | |
| <div className="h-full flex"> | |
| {/* Models List */} | |
| <div className="w-1/2 border-r border-gray-700 overflow-y-auto"> | |
| <div className="p-4"> | |
| <div className="flex justify-between items-center mb-4"> | |
| <h3 className="text-lg font-medium text-white">模型列表</h3> | |
| <button | |
| onClick={() => setEditingModel({ | |
| isNew: true, | |
| supportsImages: true, | |
| supportsReducedCapacity: true, | |
| category: 'GPT-4系列', | |
| maxTokens: 4096, | |
| temperature: 0.7, | |
| channelId: channels.length > 0 ? channels[0].id : '' | |
| })} | |
| className="px-3 py-1 bg-sky-600 hover:bg-sky-700 text-white rounded text-sm flex items-center space-x-1" | |
| > | |
| <Plus size={16} /> | |
| <span>添加模型</span> | |
| </button> | |
| </div> | |
| <div className="space-y-2"> | |
| {models.map((model) => { | |
| const channel = channels.find(ch => ch.id === model.channelId); | |
| return ( | |
| <div key={model.id} className="bg-gray-700 rounded p-3"> | |
| <div className="flex justify-between items-start mb-2"> | |
| <div> | |
| <h4 className="text-white font-medium">{model.name}</h4> | |
| <p className="text-gray-400 text-sm">{model.apiName}</p> | |
| <p className="text-gray-500 text-xs">{model.category}</p> | |
| <p className="text-gray-500 text-xs"> | |
| 渠道: {channel?.name || '未找到渠道'} | |
| </p> | |
| </div> | |
| <div className="flex space-x-1"> | |
| <button | |
| onClick={() => setEditingModel(model)} | |
| className="p-1 text-gray-400 hover:text-sky-400" | |
| title="编辑" | |
| > | |
| <Edit3 size={16} /> | |
| </button> | |
| {model.isCustom && ( | |
| <button | |
| onClick={() => handleDeleteModel(model.id)} | |
| className="p-1 text-gray-400 hover:text-red-400" | |
| title="删除" | |
| > | |
| <Trash2 size={16} /> | |
| </button> | |
| )} | |
| </div> | |
| </div> | |
| <div className="flex space-x-2"> | |
| {model.supportsImages && ( | |
| <span className="text-xs bg-green-600 text-white px-2 py-1 rounded flex items-center space-x-1"> | |
| <Image size={12} /> | |
| <span>图像</span> | |
| </span> | |
| )} | |
| {model.supportsReducedCapacity && ( | |
| <span className="text-xs bg-blue-600 text-white px-2 py-1 rounded flex items-center space-x-1"> | |
| <Zap size={12} /> | |
| <span>优化</span> | |
| </span> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| </div> | |
| </div> | |
| {/* Model Editor */} | |
| <div className="w-1/2 overflow-y-auto"> | |
| {editingModel ? ( | |
| <div className="p-4"> | |
| <h3 className="text-lg font-medium text-white mb-4"> | |
| {editingModel.isNew ? '添加新模型' : '编辑模型'} | |
| </h3> | |
| {validationErrors.length > 0 && ( | |
| <div className="mb-4 p-3 bg-red-600 rounded"> | |
| <div className="flex items-center space-x-2 mb-2"> | |
| <AlertCircle size={16} className="text-white" /> | |
| <span className="text-white font-medium">配置错误</span> | |
| </div> | |
| {validationErrors.map((error, index) => ( | |
| <p key={index} className="text-white text-sm">{error}</p> | |
| ))} | |
| </div> | |
| )} | |
| <div className="space-y-4"> | |
| <div> | |
| <label className="block text-gray-300 text-sm mb-1">显示名称</label> | |
| <input | |
| type="text" | |
| value={editingModel.name || ''} | |
| onChange={(e) => setEditingModel({ ...editingModel, name: e.target.value })} | |
| className="w-full bg-gray-700 text-white p-2 rounded text-sm" | |
| placeholder="例如: GPT-4 Turbo" | |
| /> | |
| </div> | |
| <div> | |
| <label className="block text-gray-300 text-sm mb-1">API模型名称</label> | |
| <input | |
| type="text" | |
| value={editingModel.apiName || ''} | |
| onChange={(e) => setEditingModel({ ...editingModel, apiName: e.target.value })} | |
| className="w-full bg-gray-700 text-white p-2 rounded text-sm" | |
| placeholder="例如: gpt-4-turbo" | |
| /> | |
| </div> | |
| <div> | |
| <label className="block text-gray-300 text-sm mb-1">API渠道</label> | |
| <select | |
| value={editingModel.channelId || ''} | |
| onChange={(e) => setEditingModel({ ...editingModel, channelId: e.target.value })} | |
| className="w-full bg-gray-700 text-white p-2 rounded text-sm" | |
| > | |
| <option value="">选择渠道</option> | |
| {channels.map((channel) => ( | |
| <option key={channel.id} value={channel.id}> | |
| {channel.name} {channel.isProtected && '(受保护)'} | |
| </option> | |
| ))} | |
| </select> | |
| </div> | |
| <div> | |
| <label className="block text-gray-300 text-sm mb-1">模型类别</label> | |
| <input | |
| type="text" | |
| value={editingModel.category || ''} | |
| onChange={(e) => setEditingModel({ ...editingModel, category: e.target.value })} | |
| className="w-full bg-gray-700 text-white p-2 rounded text-sm" | |
| placeholder="例如: GPT-4系列" | |
| /> | |
| </div> | |
| <div className="grid grid-cols-2 gap-4"> | |
| <div> | |
| <label className="block text-gray-300 text-sm mb-1">最大Token数</label> | |
| <input | |
| type="number" | |
| value={editingModel.maxTokens || 4096} | |
| onChange={(e) => setEditingModel({ ...editingModel, maxTokens: parseInt(e.target.value) || 4096 })} | |
| className="w-full bg-gray-700 text-white p-2 rounded text-sm" | |
| min="1" | |
| max="32768" | |
| /> | |
| </div> | |
| <div> | |
| <label className="block text-gray-300 text-sm mb-1">默认温度</label> | |
| <input | |
| type="number" | |
| value={editingModel.temperature || 0.7} | |
| onChange={(e) => setEditingModel({ ...editingModel, temperature: parseFloat(e.target.value) || 0.7 })} | |
| className="w-full bg-gray-700 text-white p-2 rounded text-sm" | |
| min="0" | |
| max="2" | |
| step="0.1" | |
| /> | |
| </div> | |
| </div> | |
| <div className="space-y-2"> | |
| <label className="flex items-center space-x-2"> | |
| <input | |
| type="checkbox" | |
| checked={editingModel.supportsImages || false} | |
| onChange={(e) => setEditingModel({ ...editingModel, supportsImages: e.target.checked })} | |
| className="rounded" | |
| /> | |
| <span className="text-gray-300 text-sm">支持图像处理</span> | |
| </label> | |
| <label className="flex items-center space-x-2"> | |
| <input | |
| type="checkbox" | |
| checked={editingModel.supportsReducedCapacity || false} | |
| onChange={(e) => setEditingModel({ ...editingModel, supportsReducedCapacity: e.target.checked })} | |
| className="rounded" | |
| /> | |
| <span className="text-gray-300 text-sm">支持性能优化模式</span> | |
| </label> | |
| </div> | |
| <div className="flex justify-end space-x-2 pt-4"> | |
| <button | |
| onClick={() => { | |
| setEditingModel(null); | |
| setValidationErrors([]); | |
| }} | |
| className="px-4 py-2 bg-gray-600 hover:bg-gray-500 text-white rounded text-sm" | |
| > | |
| 取消 | |
| </button> | |
| <button | |
| onClick={handleSaveModel} | |
| className="px-4 py-2 bg-sky-600 hover:bg-sky-700 text-white rounded text-sm flex items-center space-x-1" | |
| > | |
| <Save size={16} /> | |
| <span>保存</span> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| ) : ( | |
| <div className="p-4 h-full flex items-center justify-center"> | |
| <p className="text-gray-400">选择一个模型进行编辑,或添加新模型</p> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ) : activeTab === 'roles' ? ( | |
| <div className="h-full flex"> | |
| {/* Roles List */} | |
| <div className="w-1/2 border-r border-gray-700 overflow-y-auto"> | |
| <div className="p-4"> | |
| <div className="flex justify-between items-center mb-4"> | |
| <h3 className="text-lg font-medium text-white">角色列表</h3> | |
| <button | |
| onClick={() => setEditingRole({ | |
| isNew: true, | |
| isActive: true, | |
| modelId: models.length > 0 ? models[0].id : '' | |
| })} | |
| className="px-3 py-1 bg-sky-600 hover:bg-sky-700 text-white rounded text-sm flex items-center space-x-1" | |
| > | |
| <Plus size={16} /> | |
| <span>添加角色</span> | |
| </button> | |
| </div> | |
| <div className="space-y-2"> | |
| {roles.map((role) => { | |
| const associatedModel = models.find(m => m.id === role.modelId); | |
| const associatedChannel = associatedModel ? channels.find(ch => ch.id === associatedModel.channelId) : null; | |
| return ( | |
| <div key={role.id} className="bg-gray-700 rounded p-3"> | |
| <div className="flex justify-between items-start mb-2"> | |
| <div> | |
| <h4 className="text-white font-medium flex items-center space-x-2"> | |
| <span>{role.name}</span> | |
| {role.isActive && ( | |
| <span className="text-xs bg-green-600 text-white px-2 py-1 rounded">活跃</span> | |
| )} | |
| </h4> | |
| <p className="text-gray-400 text-sm"> | |
| 模型: {associatedModel?.name || '未找到模型'} | |
| </p> | |
| <p className="text-gray-500 text-xs"> | |
| 渠道: {associatedChannel?.name || '未找到渠道'} | |
| {associatedChannel?.isProtected && ( | |
| <span className="ml-1 text-green-400">(受保护)</span> | |
| )} | |
| </p> | |
| </div> | |
| <div className="flex space-x-1"> | |
| <button | |
| onClick={() => setEditingRole(role)} | |
| className="p-1 text-gray-400 hover:text-sky-400" | |
| title="编辑" | |
| > | |
| <Edit3 size={16} /> | |
| </button> | |
| <button | |
| onClick={() => handleDeleteRole(role.id)} | |
| className="p-1 text-gray-400 hover:text-red-400" | |
| title="删除" | |
| > | |
| <Trash2 size={16} /> | |
| </button> | |
| </div> | |
| </div> | |
| <p className="text-gray-500 text-xs line-clamp-2"> | |
| {role.systemPrompt} | |
| </p> | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| </div> | |
| </div> | |
| {/* Role Editor */} | |
| <div className="w-1/2 overflow-y-auto"> | |
| {editingRole ? ( | |
| <div className="p-4"> | |
| <h3 className="text-lg font-medium text-white mb-4"> | |
| {editingRole.isNew ? '添加新角色' : '编辑角色'} | |
| </h3> | |
| {validationErrors.length > 0 && ( | |
| <div className="mb-4 p-3 bg-red-600 rounded"> | |
| <div className="flex items-center space-x-2 mb-2"> | |
| <AlertCircle size={16} className="text-white" /> | |
| <span className="text-white font-medium">配置错误</span> | |
| </div> | |
| {validationErrors.map((error, index) => ( | |
| <p key={index} className="text-white text-sm">{error}</p> | |
| ))} | |
| </div> | |
| )} | |
| <div className="space-y-4"> | |
| <div> | |
| <label className="block text-gray-300 text-sm mb-1">角色名称</label> | |
| <input | |
| type="text" | |
| value={editingRole.name || ''} | |
| onChange={(e) => setEditingRole({ ...editingRole, name: e.target.value })} | |
| className="w-full bg-gray-700 text-white p-2 rounded text-sm" | |
| placeholder="例如: 分析师" | |
| /> | |
| </div> | |
| <div> | |
| <label className="block text-gray-300 text-sm mb-1">关联模型</label> | |
| <select | |
| value={editingRole.modelId || ''} | |
| onChange={(e) => setEditingRole({ ...editingRole, modelId: e.target.value })} | |
| className="w-full bg-gray-700 text-white p-2 rounded text-sm" | |
| > | |
| <option value="">选择模型</option> | |
| {models.map((model) => { | |
| const channel = channels.find(ch => ch.id === model.channelId); | |
| return ( | |
| <option key={model.id} value={model.id}> | |
| {model.name} ({channel?.name || '未知渠道'}) | |
| {channel?.isProtected && ' - 受保护'} | |
| </option> | |
| ); | |
| })} | |
| </select> | |
| </div> | |
| <div> | |
| <label className="block text-gray-300 text-sm mb-1">系统提示词</label> | |
| <textarea | |
| value={editingRole.systemPrompt || ''} | |
| onChange={(e) => setEditingRole({ ...editingRole, systemPrompt: e.target.value })} | |
| className="w-full bg-gray-700 text-white p-2 rounded text-sm h-32" | |
| placeholder="定义AI角色的行为和特性..." | |
| /> | |
| </div> | |
| <div> | |
| <label className="flex items-center space-x-2"> | |
| <input | |
| type="checkbox" | |
| checked={editingRole.isActive || false} | |
| onChange={(e) => setEditingRole({ ...editingRole, isActive: e.target.checked })} | |
| className="rounded" | |
| /> | |
| <span className="text-gray-300 text-sm">激活此角色</span> | |
| </label> | |
| </div> | |
| <div className="flex justify-end space-x-2 pt-4"> | |
| <button | |
| onClick={() => { | |
| setEditingRole(null); | |
| setValidationErrors([]); | |
| }} | |
| className="px-4 py-2 bg-gray-600 hover:bg-gray-500 text-white rounded text-sm" | |
| > | |
| 取消 | |
| </button> | |
| <button | |
| onClick={handleSaveRole} | |
| className="px-4 py-2 bg-sky-600 hover:bg-sky-700 text-white rounded text-sm flex items-center space-x-1" | |
| > | |
| <Save size={16} /> | |
| <span>保存</span> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| ) : ( | |
| <div className="p-4 h-full flex items-center justify-center"> | |
| <p className="text-gray-400">选择一个角色进行编辑,或添加新角色</p> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ) : ( | |
| // Storage Management Tab | |
| <div className="p-6"> | |
| <h3 className="text-lg font-medium text-white mb-6">存储管理</h3> | |
| <div className="grid grid-cols-1 md:grid-cols-2 gap-6"> | |
| <div className="bg-gray-700 rounded-lg p-4"> | |
| <h4 className="text-white font-medium mb-3">存储使用情况</h4> | |
| <div className="space-y-2"> | |
| <div className="flex justify-between text-sm"> | |
| <span className="text-gray-300">已使用:</span> | |
| <span className="text-white">{(storageInfo.used / 1024).toFixed(2)} KB</span> | |
| </div> | |
| <div className="flex justify-between text-sm"> | |
| <span className="text-gray-300">可用:</span> | |
| <span className="text-white">{(storageInfo.available / 1024).toFixed(2)} KB</span> | |
| </div> | |
| <div className="w-full bg-gray-600 rounded-full h-2 mt-2"> | |
| <div | |
| className="bg-sky-500 h-2 rounded-full transition-all duration-300" | |
| style={{ width: `${Math.min((storageInfo.used / (storageInfo.used + storageInfo.available)) * 100, 100)}%` }} | |
| ></div> | |
| </div> | |
| </div> | |
| </div> | |
| <div className="bg-gray-700 rounded-lg p-4"> | |
| <h4 className="text-white font-medium mb-3">配置统计</h4> | |
| <div className="space-y-2"> | |
| <div className="flex justify-between text-sm"> | |
| <span className="text-gray-300">API渠道:</span> | |
| <span className="text-white">{storageInfo.channels}</span> | |
| </div> | |
| <div className="flex justify-between text-sm"> | |
| <span className="text-gray-300">模型数量:</span> | |
| <span className="text-white">{storageInfo.models}</span> | |
| </div> | |
| <div className="flex justify-between text-sm"> | |
| <span className="text-gray-300">角色数量:</span> | |
| <span className="text-white">{storageInfo.roles}</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div className="mt-6 bg-gray-700 rounded-lg p-4"> | |
| <h4 className="text-white font-medium mb-3">数据管理</h4> | |
| <div className="space-y-3"> | |
| <p className="text-gray-300 text-sm"> | |
| 所有配置数据存储在浏览器的localStorage中。清除浏览器数据可能会导致配置丢失。建议定期导出配置进行备份。 | |
| </p> | |
| <p className="text-blue-300 text-sm"> | |
| 注意:受保护的预置API密钥不会包含在导出的配置中,以确保安全性。 | |
| </p> | |
| <div className="flex space-x-3"> | |
| <button | |
| onClick={handleReset} | |
| className="px-4 py-2 bg-orange-600 hover:bg-orange-700 text-white rounded text-sm" | |
| > | |
| 重置为默认配置 | |
| </button> | |
| <button | |
| onClick={handleClearAllData} | |
| className="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded text-sm" | |
| > | |
| 清空所有数据 | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| export default ModelConfigManagerComponent; |