import { useState, useEffect } from 'react';
import { Save, Database, Key, Users } from 'lucide-react';
interface Config {
neo4j: {
uri: string;
username: string;
password?: string;
database: string;
} | null;
api: {
provider: string;
api_key?: string;
base_url?: string;
model?: string;
} | null;
group_id: string;
}
export default function SettingsTab() {
const [config, setConfig] = useState<Config | null>(null);
const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false);
const [message, setMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null);
useEffect(() => {
fetchConfig();
}, []);
const fetchConfig = async () => {
setLoading(true);
setMessage(null);
try {
const res = await fetch('/api/config');
if (!res.ok) throw new Error('加载配置失败');
const data = await res.json();
setConfig(data);
} catch (err) {
setMessage({ type: 'error', text: '加载配置失败' });
} finally {
setLoading(false);
}
};
// 确保 neo4j 和 api 有默认值
const neo4j = config?.neo4j || { uri: 'bolt://localhost:7687', username: 'neo4j', password: '', database: 'neo4j' };
const api = config?.api || { provider: 'openai', api_key: '', base_url: '', model: '' };
const handleSave = async (section: 'neo4j' | 'api' | 'group') => {
if (!config) return;
setSaving(true);
setMessage(null);
const payload: any = {};
if (section === 'neo4j') payload.neo4j = neo4j;
if (section === 'api') payload.api = api;
if (section === 'group') payload.group_id = config.group_id;
try {
const res = await fetch('/api/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (!res.ok) {
const text = await res.text();
throw new Error(text || 'Failed to save');
}
setMessage({ type: 'success', text: '配置保存成功' });
// 保存成功后重新拉取配置,确保前端与后端状态一致
await fetchConfig();
} catch (err) {
setMessage({ type: 'error', text: '配置保存失败' });
} finally {
setSaving(false);
}
};
if (loading) return <div>正在加载配置...</div>;
if (!config) return <div>加载配置失败</div>;
return (
<div className="space-y-8 max-w-4xl">
{message && (
<div className={`p-4 rounded-md ${message.type === 'success' ? 'bg-green-50 text-green-700 border border-green-200' : 'bg-red-50 text-red-700 border border-red-200'}`}>
{message.text}
</div>
)}
{/* Neo4j Config */}
<div className="border rounded-xl p-6 bg-card shadow-sm">
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
<Database className="w-5 h-5" />
Neo4j 数据库
</h3>
<div className="grid gap-6">
<div className="space-y-2">
<label className="text-sm font-medium">URI</label>
<input
className="flex h-10 w-full rounded-md border bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
value={neo4j.uri}
onChange={e => setConfig({...config!, neo4j: {...neo4j, uri: e.target.value}})}
/>
</div>
<div className="grid grid-cols-2 gap-6">
<div className="space-y-2">
<label className="text-sm font-medium">用户名</label>
<input
className="flex h-10 w-full rounded-md border bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
value={neo4j.username}
onChange={e => setConfig({...config!, neo4j: {...neo4j, username: e.target.value}})}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">密码 (保持默认 ****** 则不修改)</label>
<input
type="password"
className="flex h-10 w-full rounded-md border bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
value={neo4j.password || ''}
onChange={e => setConfig({...config!, neo4j: {...neo4j, password: e.target.value}})}
placeholder="******"
/>
</div>
</div>
<div className="pt-2">
<button
onClick={() => handleSave('neo4j')}
disabled={saving}
className="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2 w-fit shadow-sm"
>
<Save className="w-4 h-4 mr-2" />
保存 Neo4j 设置
</button>
</div>
</div>
</div>
{/* API Config */}
<div className="border rounded-xl p-6 bg-card shadow-sm">
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
<Key className="w-5 h-5" />
AI 模型 API
</h3>
<div className="grid gap-6">
<div className="grid grid-cols-2 gap-6">
<div className="space-y-2">
<label className="text-sm font-medium">提供商</label>
<select
className="flex h-10 w-full rounded-md border bg-background px-3 py-2 text-sm ring-offset-background focus:outline-none focus:ring-2 focus:ring-ring"
value={api.provider}
onChange={e => setConfig({...config!, api: {...api, provider: e.target.value}})}
>
<option value="openai">OpenAI / DeepSeek / Compatible</option>
<option value="anthropic">Anthropic</option>
<option value="azure">Azure OpenAI</option>
</select>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">模型名称</label>
<input
className="flex h-10 w-full rounded-md border bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
value={api.model || ''}
onChange={e => setConfig({...config!, api: {...api, model: e.target.value}})}
placeholder="gpt-4, deepseek-chat, etc."
/>
<p className="text-xs text-slate-500">必须填写服务商提供的准确 Model ID (如 deepseek-chat, gpt-4o)。</p>
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Base URL (可选)</label>
<input
className="flex h-10 w-full rounded-md border bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
value={api.base_url || ''}
onChange={e => setConfig({...config!, api: {...api, base_url: e.target.value}})}
placeholder="https://api.deepseek.com (默认使用官方 API)"
/>
<p className="text-xs text-slate-500">用于配置 DeepSeek 或其他 OpenAI 兼容接口的地址。</p>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">API Key (保持默认 ****** 则不修改)</label>
<input
type="password"
className="flex h-10 w-full rounded-md border bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
value={api.api_key || ''}
onChange={e => setConfig({...config!, api: {...api, api_key: e.target.value}})}
placeholder="******"
/>
</div>
<div className="pt-2">
<button
onClick={() => handleSave('api')}
disabled={saving}
className="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2 w-fit shadow-sm"
>
<Save className="w-4 h-4 mr-2" />
保存 API 设置
</button>
</div>
</div>
</div>
{/* Group ID */}
<div className="border rounded-xl p-6 bg-card shadow-sm">
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
<Users className="w-5 h-5" />
项目上下文
</h3>
<div className="grid gap-4">
<div className="grid gap-2">
<label className="text-sm font-medium">分组 ID (Group ID)</label>
<div className="flex gap-4">
<input
className="flex h-10 w-full rounded-md border bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
value={config.group_id}
onChange={e => setConfig({...config!, group_id: e.target.value})}
/>
<button
onClick={() => handleSave('group')}
disabled={saving}
className="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2 whitespace-nowrap shrink-0"
>
更新分组
</button>
</div>
<p className="text-xs text-muted-foreground space-y-1">
<span className="block">• <strong>ACE 策略</strong>:仅加载该分组下的学习策略。</span>
<span className="block">• <strong>外部工具</strong>:CLI 和 MCP Server 将默认使用此分组。</span>
<span className="block">• <strong>图谱浏览</strong>:当前 Dashboard 支持全局浏览,不受此设置限制。</span>
</p>
</div>
</div>
</div>
</div>
);
}