import { useState, useEffect } from 'react';
import {
LayoutDashboard,
Settings,
Network,
BrainCircuit,
Activity,
RefreshCw,
ShieldCheck,
Circle
} from 'lucide-react';
import {
AreaChart,
Area,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer
} from 'recharts';
import SettingsTab from './components/SettingsTab';
import StrategiesTab from './components/StrategiesTab';
import GraphExplorer from './components/GraphExplorer';
import MaintenanceTab from './components/MaintenanceTab';
// Types
interface StatCardProps {
title: string;
value: string | number;
icon: React.ReactNode;
trend?: string;
}
type HealthStatus = 'healthy' | 'degraded' | 'unhealthy';
interface HealthInfo {
status: HealthStatus;
components: {
graphiti: boolean;
neo4j: boolean;
ace: boolean;
};
}
function StatCard({ title, value, icon, trend }: StatCardProps) {
return (
<div className="bg-card text-card-foreground rounded-xl border shadow-sm p-6">
<div className="flex items-center justify-between space-y-0 pb-2">
<p className="text-sm font-medium text-muted-foreground">{title}</p>
{icon}
</div>
<div className="flex flex-col gap-1">
<div className="text-2xl font-bold">{value}</div>
{trend && <p className="text-xs text-muted-foreground">{trend}</p>}
</div>
</div>
);
}
function App() {
const [activeTab, setActiveTab] = useState<'overview' | 'graph' | 'strategies' | 'maintenance' | 'settings'>('overview');
const [stats, setStats] = useState<any>(null);
const [trends, setTrends] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [health, setHealth] = useState<HealthInfo | null>(null);
const fetchStats = async () => {
setLoading(true);
setError(null);
try {
const [statsRes, trendsRes, healthRes] = await Promise.all([
fetch('/api/stats'),
fetch('/api/trends?days=14'),
fetch('/api/health')
]);
if (!statsRes.ok) {
throw new Error('获取统计信息失败');
}
if (!trendsRes.ok) {
throw new Error('获取趋势数据失败');
}
if (!healthRes.ok) {
throw new Error('获取健康状态失败');
}
setStats(await statsRes.json());
setTrends(await trendsRes.json());
setHealth(await healthRes.json());
} catch (err) {
console.error(err);
setError('无法获取概览数据,请检查 Dashboard Backend / Neo4j 是否正常运行。');
// 当后端服务关闭时,清除健康状态,显示错误状态
setHealth({
status: 'unhealthy',
components: {
graphiti: false,
neo4j: false,
ace: false
}
});
setStats(null);
setTrends([]);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchStats();
}, []);
return (
<div className="flex h-screen bg-background text-foreground">
{/* Sidebar */}
<aside className="w-64 border-r bg-muted/10 flex flex-col">
<div className="p-6 border-b">
<h1 className="text-xl font-bold flex items-center gap-2">
<Network className="w-6 h-6 text-primary" />
Graphiti ACE
</h1>
</div>
<nav className="flex-1 p-4 space-y-2">
<button
onClick={() => setActiveTab('overview')}
className={`w-full flex items-center gap-3 px-4 py-2 rounded-md text-sm font-medium transition-colors
${activeTab === 'overview' ? 'bg-primary text-primary-foreground' : 'hover:bg-muted'}`}
aria-label="概览"
aria-current={activeTab === 'overview' ? 'page' : undefined}
>
<LayoutDashboard className="w-4 h-4" />
概览
</button>
<button
onClick={() => setActiveTab('graph')}
className={`w-full flex items-center gap-3 px-4 py-2 rounded-md text-sm font-medium transition-colors
${activeTab === 'graph' ? 'bg-primary text-primary-foreground' : 'hover:bg-muted'}`}
aria-label="知识图谱"
aria-current={activeTab === 'graph' ? 'page' : undefined}
>
<Network className="w-4 h-4" />
知识图谱
</button>
<button
onClick={() => setActiveTab('strategies')}
className={`w-full flex items-center gap-3 px-4 py-2 rounded-md text-sm font-medium transition-colors
${activeTab === 'strategies' ? 'bg-primary text-primary-foreground' : 'hover:bg-muted'}`}
aria-label="ACE 策略"
aria-current={activeTab === 'strategies' ? 'page' : undefined}
>
<BrainCircuit className="w-4 h-4" />
ACE 策略
</button>
<button
onClick={() => setActiveTab('maintenance')}
className={`w-full flex items-center gap-3 px-4 py-2 rounded-md text-sm font-medium transition-colors
${activeTab === 'maintenance' ? 'bg-primary text-primary-foreground' : 'hover:bg-muted'}`}
aria-label="系统维护"
aria-current={activeTab === 'maintenance' ? 'page' : undefined}
>
<ShieldCheck className="w-4 h-4" />
系统维护
</button>
<button
onClick={() => setActiveTab('settings')}
className={`w-full flex items-center gap-3 px-4 py-2 rounded-md text-sm font-medium transition-colors
${activeTab === 'settings' ? 'bg-primary text-primary-foreground' : 'hover:bg-muted'}`}
aria-label="配置管理"
aria-current={activeTab === 'settings' ? 'page' : undefined}
>
<Settings className="w-4 h-4" />
配置管理
</button>
</nav>
<div className="p-4 border-t">
<div className="text-xs text-muted-foreground text-center">
v0.1.0 Dashboard
</div>
</div>
</aside>
{/* Main Content */}
<main className="flex-1 overflow-y-auto p-8">
<header className="mb-8 flex items-center justify-between">
<h2 className="text-3xl font-bold tracking-tight">
{activeTab === 'overview' && '系统概览'}
{activeTab === 'graph' && '知识图谱探索'}
{activeTab === 'strategies' && 'ACE 策略中心'}
{activeTab === 'maintenance' && '系统维护与健康'}
{activeTab === 'settings' && '系统配置'}
</h2>
<div className="flex items-center gap-4">
{health && (
<div className="flex items-center gap-2 text-xs rounded-full border px-3 py-1 bg-muted/40">
<Circle
className={`w-3 h-3 ${
health.status === 'healthy'
? 'text-green-500'
: health.status === 'degraded'
? 'text-yellow-500'
: 'text-red-500'
}`}
fill="currentColor"
/>
<span className="text-muted-foreground">
{health.status === 'healthy' && '系统健康'}
{health.status === 'degraded' && '部分组件异常'}
{health.status === 'unhealthy' && '系统不可用'}
</span>
<span className="text-[10px] text-muted-foreground/80">
Graphiti: {health.components.graphiti ? '✅' : '❌'} | Neo4j: {health.components.neo4j ? '✅' : '❌'} | ACE: {health.components.ace ? '✅' : '❌'}
</span>
</div>
)}
<button
onClick={fetchStats}
disabled={loading}
className="p-2 rounded-full hover:bg-muted transition-colors"
>
<RefreshCw className={`w-5 h-5 ${loading ? 'animate-spin' : ''}`} />
</button>
</div>
</header>
{error && (
<div className="mb-4 rounded-md border border-red-200 bg-red-50 px-4 py-2 text-sm text-red-700">
{error}
</div>
)}
{/* Overview Tab */}
{activeTab === 'overview' && (
<div className="space-y-8">
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<StatCard
title="Episode 总数"
value={stats?.episodes?.total || 0}
icon={<Activity className="w-4 h-4 text-muted-foreground" />}
/>
<StatCard
title="节点总数"
value={stats?.nodes?.total || 0}
icon={<Network className="w-4 h-4 text-muted-foreground" />}
/>
<StatCard
title="关系总数"
value={stats?.relationships?.total || 0}
icon={<Network className="w-4 h-4 text-muted-foreground" />}
/>
<StatCard
title="当前分组"
value={stats?.group_id || 'N/A'}
icon={<Settings className="w-4 h-4 text-muted-foreground" />}
/>
</div>
{/* Placeholder for more charts */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-7">
<div className="col-span-4 rounded-xl border bg-card text-card-foreground shadow-sm p-6">
<h3 className="font-semibold mb-4">近期活动趋势</h3>
<div className="h-[200px] w-full">
{trends.length > 0 ? (
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={trends}>
<defs>
<linearGradient id="colorUsage" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#8884d8" stopOpacity={0.8}/>
<stop offset="95%" stopColor="#8884d8" stopOpacity={0}/>
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" opacity={0.1} />
<XAxis
dataKey="date"
fontSize={12}
tickFormatter={(val) => new Date(val).toLocaleDateString(undefined, {month: 'short', day: 'numeric'})}
/>
<YAxis fontSize={12} />
<Tooltip
labelFormatter={(label) => new Date(label).toLocaleDateString()}
contentStyle={{ backgroundColor: 'hsl(var(--card))', borderColor: 'hsl(var(--border))', borderRadius: '8px' }}
/>
<Area
type="monotone"
dataKey="usage_count"
name="调用次数"
stroke="#8884d8"
fillOpacity={1}
fill="url(#colorUsage)"
/>
<Area
type="monotone"
dataKey="success_count"
name="成功次数"
stroke="#82ca9d"
fillOpacity={0}
strokeWidth={2}
fill="#82ca9d"
/>
</AreaChart>
</ResponsiveContainer>
) : (
<div className="h-full flex items-center justify-center bg-muted/20 rounded-lg border border-dashed">
<span className="text-muted-foreground text-sm">暂无趋势数据,请先使用工具</span>
</div>
)}
</div>
</div>
<div className="col-span-3 rounded-xl border bg-card text-card-foreground shadow-sm p-6">
<h3 className="font-semibold mb-4">节点类型分布</h3>
<div className="space-y-2">
{stats?.nodes?.by_type && Object.entries(stats.nodes.by_type).map(([type, count]) => (
<div key={type} className="flex items-center justify-between">
<span className="text-sm">{type}</span>
<span className="font-bold">{count as number}</span>
</div>
))}
</div>
</div>
</div>
</div>
)}
{/* Other Tabs */}
{activeTab === 'graph' && <GraphExplorer />}
{activeTab === 'strategies' && <StrategiesTab />}
{activeTab === 'maintenance' && <MaintenanceTab />}
{activeTab === 'settings' && <SettingsTab />}
</main>
</div>
);
}
export default App;