cacode's picture
Upload 74 files
7c15d35 verified
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import { toast } from 'react-hot-toast';
import { Settings, Save, RefreshCw, Cpu, Brain } from 'lucide-react';
const ConfigManager = ({ adminToken }) => {
const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false);
const [formData, setFormData] = useState({
POLISH_MODEL: '',
POLISH_API_KEY: '',
POLISH_BASE_URL: '',
ENHANCE_MODEL: '',
ENHANCE_API_KEY: '',
ENHANCE_BASE_URL: '',
EMOTION_MODEL: '',
EMOTION_API_KEY: '',
EMOTION_BASE_URL: '',
MAX_CONCURRENT_USERS: '',
HISTORY_COMPRESSION_THRESHOLD: '',
COMPRESSION_MODEL: '',
COMPRESSION_API_KEY: '',
COMPRESSION_BASE_URL: '',
DEFAULT_USAGE_LIMIT: '',
SEGMENT_SKIP_THRESHOLD: '',
MAX_UPLOAD_FILE_SIZE_MB: '',
THINKING_MODE_ENABLED: true,
THINKING_MODE_EFFORT: 'high'
});
useEffect(() => {
fetchConfig();
}, []);
const fetchConfig = async () => {
setLoading(true);
try {
const response = await axios.get('/api/admin/config', {
headers: { Authorization: `Bearer ${adminToken}` }
});
// 填充表单,直接使用返回的值
setFormData({
POLISH_MODEL: response.data.polish.model || '',
POLISH_API_KEY: response.data.polish.api_key || '',
POLISH_BASE_URL: response.data.polish.base_url || '',
ENHANCE_MODEL: response.data.enhance.model || '',
ENHANCE_API_KEY: response.data.enhance.api_key || '',
ENHANCE_BASE_URL: response.data.enhance.base_url || '',
EMOTION_MODEL: response.data.emotion?.model || '',
EMOTION_API_KEY: response.data.emotion?.api_key || '',
EMOTION_BASE_URL: response.data.emotion?.base_url || '',
MAX_CONCURRENT_USERS: response.data.system.max_concurrent_users?.toString() || '',
HISTORY_COMPRESSION_THRESHOLD: response.data.system.history_compression_threshold?.toString() || '',
COMPRESSION_MODEL: response.data.system.compression_model || '',
COMPRESSION_API_KEY: response.data.compression?.api_key || '',
COMPRESSION_BASE_URL: response.data.compression?.base_url || '',
DEFAULT_USAGE_LIMIT: response.data.system.default_usage_limit?.toString() || '',
SEGMENT_SKIP_THRESHOLD: response.data.system.segment_skip_threshold?.toString() || '',
MAX_UPLOAD_FILE_SIZE_MB: response.data.system.max_upload_file_size_mb?.toString() || '',
THINKING_MODE_ENABLED: response.data.thinking?.enabled ?? true,
THINKING_MODE_EFFORT: response.data.thinking?.effort || 'high'
});
} catch (error) {
toast.error('获取配置失败');
} finally {
setLoading(false);
}
};
const handleSave = async () => {
setSaving(true);
try {
// 只发送已修改的非空值
const updates = {};
Object.keys(formData).forEach(key => {
const value = formData[key];
// 布尔值需要转换为字符串
if (typeof value === 'boolean') {
updates[key] = value.toString();
} else if (typeof value === 'string' && value.trim()) {
updates[key] = value.trim();
}
});
const response = await axios.post('/api/admin/config', updates, {
headers: { Authorization: `Bearer ${adminToken}` }
});
toast.success(response.data.message);
fetchConfig();
} catch (error) {
toast.error(error.response?.data?.detail || '保存配置失败');
} finally {
setSaving(false);
}
};
if (loading) {
return (
<div className="flex items-center justify-center py-12">
<div className="w-8 h-8 border-4 border-blue-600 border-t-transparent rounded-full animate-spin" />
</div>
);
}
return (
<div className="space-y-6">
{/* 润色模型配置 */}
<div className="bg-white rounded-2xl shadow-ios p-6">
<div className="flex items-center gap-3 mb-6">
<div className="w-10 h-10 bg-teal-50 rounded-xl flex items-center justify-center">
<Cpu className="w-5 h-5 text-teal-600" />
</div>
<h3 className="text-lg font-bold text-gray-900">润色模型配置</h3>
</div>
<div className="space-y-5">
<div>
<label className="block text-sm font-medium text-gray-500 mb-2">
模型名称
</label>
<input
type="text"
value={formData.POLISH_MODEL}
onChange={(e) => setFormData({...formData, POLISH_MODEL: e.target.value})}
placeholder="gemini-2.5-pro"
className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl focus:bg-white focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all text-sm"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-500 mb-2">
API Key (可选)
</label>
<input
type="text"
value={formData.POLISH_API_KEY}
onChange={(e) => setFormData({...formData, POLISH_API_KEY: e.target.value})}
placeholder="留空使用默认 OpenAI Key"
className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl focus:bg-white focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all text-sm font-mono"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-500 mb-2">
Base URL
</label>
<input
type="text"
value={formData.POLISH_BASE_URL}
onChange={(e) => setFormData({...formData, POLISH_BASE_URL: e.target.value})}
placeholder="http://localhost:8317/v1"
className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl focus:bg-white focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all text-sm"
/>
</div>
</div>
</div>
{/* 增强模型配置 */}
<div className="bg-white rounded-2xl shadow-ios p-6">
<div className="flex items-center gap-3 mb-6">
<div className="w-10 h-10 bg-cyan-50 rounded-xl flex items-center justify-center">
<Cpu className="w-5 h-5 text-cyan-600" />
</div>
<h3 className="text-lg font-bold text-gray-900">论文增强模型配置</h3>
</div>
<div className="space-y-5">
<div>
<label className="block text-sm font-medium text-gray-500 mb-2">
模型名称
</label>
<input
type="text"
value={formData.ENHANCE_MODEL}
onChange={(e) => setFormData({...formData, ENHANCE_MODEL: e.target.value})}
placeholder="gemini-2.5-pro"
className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl focus:bg-white focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all text-sm"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-500 mb-2">
API Key (可选)
</label>
<input
type="text"
value={formData.ENHANCE_API_KEY}
onChange={(e) => setFormData({...formData, ENHANCE_API_KEY: e.target.value})}
placeholder="留空使用默认 OpenAI Key"
className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl focus:bg-white focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all text-sm font-mono"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-500 mb-2">
Base URL
</label>
<input
type="text"
value={formData.ENHANCE_BASE_URL}
onChange={(e) => setFormData({...formData, ENHANCE_BASE_URL: e.target.value})}
placeholder="http://localhost:8317/v1"
className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl focus:bg-white focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all text-sm"
/>
</div>
</div>
</div>
{/* 感情文章润色模型配置 */}
<div className="bg-white rounded-2xl shadow-ios p-6">
<div className="flex items-center gap-3 mb-6">
<div className="w-10 h-10 bg-rose-50 rounded-xl flex items-center justify-center">
<Cpu className="w-5 h-5 text-rose-600" />
</div>
<h3 className="text-lg font-bold text-gray-900">感情文章润色模型配置</h3>
</div>
<div className="space-y-5">
<div>
<label className="block text-sm font-medium text-gray-500 mb-2">
模型名称
</label>
<input
type="text"
value={formData.EMOTION_MODEL}
onChange={(e) => setFormData({...formData, EMOTION_MODEL: e.target.value})}
placeholder="gemini-2.5-pro"
className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl focus:bg-white focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all text-sm"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-500 mb-2">
API Key (可选)
</label>
<input
type="text"
value={formData.EMOTION_API_KEY}
onChange={(e) => setFormData({...formData, EMOTION_API_KEY: e.target.value})}
placeholder="留空使用默认 OpenAI Key"
className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl focus:bg-white focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all text-sm font-mono"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-500 mb-2">
Base URL
</label>
<input
type="text"
value={formData.EMOTION_BASE_URL}
onChange={(e) => setFormData({...formData, EMOTION_BASE_URL: e.target.value})}
placeholder="http://localhost:8317/v1"
className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl focus:bg-white focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all text-sm"
/>
</div>
</div>
</div>
{/* 思考模式配置 */}
<div className="bg-white rounded-2xl shadow-ios p-6">
<div className="flex items-center gap-3 mb-6">
<div className="w-10 h-10 bg-blue-50 rounded-xl flex items-center justify-center">
<Brain className="w-5 h-5 text-blue-600" />
</div>
<h3 className="text-lg font-bold text-gray-900">思考模式配置</h3>
</div>
<div className="space-y-5">
{/* 启用开关 */}
<div className="flex items-center justify-between">
<div>
<label className="block text-sm font-medium text-gray-700">
启用思考模式
</label>
<p className="text-xs text-gray-400 mt-1">
开启后模型会进行深度推理,可能增加响应时间和 token 消耗
</p>
</div>
<button
type="button"
onClick={() => setFormData({
...formData,
THINKING_MODE_ENABLED: !formData.THINKING_MODE_ENABLED
})}
className={`relative w-12 h-7 rounded-full transition-colors duration-200 ${
formData.THINKING_MODE_ENABLED
? 'bg-blue-600'
: 'bg-gray-200'
}`}
>
<span className={`absolute top-0.5 left-0.5 w-6 h-6 bg-white rounded-full shadow transition-transform ${
formData.THINKING_MODE_ENABLED
? 'translate-x-5'
: 'translate-x-0'
}`} />
</button>
</div>
{/* 思考强度选择器 */}
<div>
<label className="block text-sm font-medium text-gray-500 mb-2">
思考强度
</label>
<select
value={formData.THINKING_MODE_EFFORT}
onChange={(e) => setFormData({...formData, THINKING_MODE_EFFORT: e.target.value})}
disabled={!formData.THINKING_MODE_ENABLED}
className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl focus:bg-white focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all text-sm disabled:opacity-50 disabled:cursor-not-allowed"
>
<option value="none">无推理 (最低延迟)</option>
<option value="low">轻度推理</option>
<option value="medium">中度推理</option>
<option value="high">深度推理 (推荐)</option>
<option value="xhigh">极深推理 (仅部分模型支持)</option>
</select>
<p className="mt-1.5 text-xs text-gray-400">
更高的强度会增加推理 token 消耗和响应时间,但可能获得更好的结果
</p>
</div>
</div>
</div>
{/* 系统配置 */}
<div className="bg-white rounded-2xl shadow-ios p-6">
<div className="flex items-center gap-3 mb-6">
<div className="w-10 h-10 bg-orange-50 rounded-xl flex items-center justify-center">
<Settings className="w-5 h-5 text-orange-600" />
</div>
<h3 className="text-lg font-bold text-gray-900">系统配置</h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-medium text-gray-500 mb-2">
最大并发用户数
</label>
<input
type="number"
value={formData.MAX_CONCURRENT_USERS}
onChange={(e) => setFormData({...formData, MAX_CONCURRENT_USERS: e.target.value})}
placeholder="5"
className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl focus:bg-white focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all text-sm"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-500 mb-2">
历史压缩阈值(字符)
</label>
<input
type="number"
value={formData.HISTORY_COMPRESSION_THRESHOLD}
onChange={(e) => setFormData({...formData, HISTORY_COMPRESSION_THRESHOLD: e.target.value})}
placeholder="5000"
className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl focus:bg-white focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all text-sm"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-500 mb-2">
压缩模型
</label>
<input
type="text"
value={formData.COMPRESSION_MODEL}
onChange={(e) => setFormData({...formData, COMPRESSION_MODEL: e.target.value})}
placeholder="gemini-2.5-pro"
className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl focus:bg-white focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all text-sm"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-500 mb-2">
压缩 API Key (可选)
</label>
<input
type="text"
value={formData.COMPRESSION_API_KEY}
onChange={(e) => setFormData({...formData, COMPRESSION_API_KEY: e.target.value})}
placeholder="留空使用默认 OpenAI Key"
className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl focus:bg-white focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all text-sm font-mono"
/>
</div>
<div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-500 mb-2">
压缩 Base URL
</label>
<input
type="text"
value={formData.COMPRESSION_BASE_URL}
onChange={(e) => setFormData({...formData, COMPRESSION_BASE_URL: e.target.value})}
placeholder="http://localhost:8317/v1"
className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl focus:bg-white focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all text-sm"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-500 mb-2">
默认使用次数限制
</label>
<input
type="number"
value={formData.DEFAULT_USAGE_LIMIT}
onChange={(e) => setFormData({...formData, DEFAULT_USAGE_LIMIT: e.target.value})}
placeholder="1"
className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl focus:bg-white focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all text-sm"
/>
<p className="mt-1.5 text-xs text-gray-400">新用户的默认使用次数限制</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-500 mb-2">
段落跳过阈值(字符)
</label>
<input
type="number"
value={formData.SEGMENT_SKIP_THRESHOLD}
onChange={(e) => setFormData({...formData, SEGMENT_SKIP_THRESHOLD: e.target.value})}
placeholder="15"
className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl focus:bg-white focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all text-sm"
/>
<p className="mt-1.5 text-xs text-gray-400">小于此字数的段落将被识别为标题并跳过</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-500 mb-2">
Word 排版文件大小限制 (MB)
</label>
<input
type="number"
value={formData.MAX_UPLOAD_FILE_SIZE_MB}
onChange={(e) => setFormData({...formData, MAX_UPLOAD_FILE_SIZE_MB: e.target.value})}
placeholder="0"
min="0"
className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl focus:bg-white focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all text-sm"
/>
<p className="mt-1.5 text-xs text-gray-400">0 表示无限制</p>
</div>
</div>
</div>
{/* 操作按钮 */}
<div className="flex gap-4">
<button
onClick={fetchConfig}
disabled={loading}
className="flex items-center gap-2 px-6 py-3 bg-white border border-gray-200 hover:bg-gray-50 disabled:bg-gray-50 text-gray-700 rounded-xl transition-all active:scale-[0.98] font-medium shadow-sm"
>
<RefreshCw className={`w-5 h-5 ${loading ? 'animate-spin' : ''}`} />
刷新
</button>
<button
onClick={handleSave}
disabled={saving}
className="flex-1 flex items-center justify-center gap-2 px-6 py-3 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-300 text-white rounded-xl transition-all active:scale-[0.98] font-semibold shadow-sm"
>
{saving ? (
<>
<div className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
保存中...
</>
) : (
<>
<Save className="w-5 h-5" />
保存配置
</>
)}
</button>
</div>
<div className="bg-green-50/50 border border-green-100 rounded-xl p-4">
<p className="text-sm font-medium text-green-800 flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-green-500"></span>
配置修改后会立即生效,无需重启服务!
</p>
</div>
</div>
);
};
export default ConfigManager;