diff --git a/frontend/web/src/App.tsx b/frontend/web/src/App.tsx index 61a26ac..086ece4 100644 --- a/frontend/web/src/App.tsx +++ b/frontend/web/src/App.tsx @@ -2,7 +2,12 @@ import { Routes, Route, Navigate } from 'react-router-dom' import { useAuthStore } from './store/authStore' import AppLayout from './components/layout/AppLayout' import ChatPage from './pages/chat/ChatPage' +import AdminLayout from './components/admin/AdminLayout' import DashboardPage from './pages/admin/DashboardPage' +import ServicesPage from './pages/admin/ServicesPage' +import IoTManagementPage from './pages/admin/IoTManagementPage' +import PluginsPage from './pages/admin/PluginsPage' +import MemoryPage from './pages/admin/MemoryPage' import LoginPage from './pages/LoginPage' export default function App() { @@ -23,8 +28,14 @@ export default function App() { } /> } /> } /> - } /> - } /> + }> + } /> + } /> + } /> + } /> + } /> + } /> + ) diff --git a/frontend/web/src/components/admin/AdminLayout.tsx b/frontend/web/src/components/admin/AdminLayout.tsx new file mode 100644 index 0000000..3177a37 --- /dev/null +++ b/frontend/web/src/components/admin/AdminLayout.tsx @@ -0,0 +1,45 @@ +import { Outlet, NavLink } from 'react-router-dom' + +const navItems = [ + { to: '/admin/dashboard', label: '仪表盘', icon: '◉' }, + { to: '/admin/services', label: '服务管理', icon: '⚙' }, + { to: '/admin/iot', label: 'IoT 设备', icon: '⌂' }, + { to: '/admin/plugins', label: '插件管理', icon: '◇' }, + { to: '/admin/memory', label: '记忆管理', icon: '♡' }, +] + +export default function AdminLayout() { + return ( +
+ {/* Sidebar */} + + + {/* Content area */} +
+ +
+
+ ) +} diff --git a/frontend/web/src/pages/admin/DashboardPage.tsx b/frontend/web/src/pages/admin/DashboardPage.tsx index 124f69e..7e3221d 100644 --- a/frontend/web/src/pages/admin/DashboardPage.tsx +++ b/frontend/web/src/pages/admin/DashboardPage.tsx @@ -2,44 +2,90 @@ import { useEffect, useState } from 'react' import { api } from '../../api/client' export default function DashboardPage() { - const [data, setData] = useState | null>(null) + const [data, setData] = useState(null) useEffect(() => { - api.admin.dashboard().then(r => setData(r.dashboard)) + api.admin.dashboard().then(r => setData(r.dashboard)).catch(() => {}) }, []) + const services = data?.services || {} + const serviceCount = Object.values(services).filter(Boolean).length + const totalServices = Object.keys(services).length || 3 + return ( -
-

管理面板

+
+

仪表盘

- {/* Stats grid */} -
- - - - + {/* Stats */} +
+ + + +
- {/* Service status */} -
- {['ai_core', 'memory', 'voice'].map(name => ( -
- {name} - -
- ))} -
+ {/* Service Status */} +
+
+ {['ai_core', 'memory', 'voice'].map(name => ( +
+
+ + {name} +
+ {services[name] ? '运行中' : '未启动'} +
+ ))} +
+
+ + {/* Quick Links */} +
+
+ + + + +
+
) } -function StatCard({ label, value, accent }: { label: string; value: string; accent: string }) { +function StatCard({ label, value, sub, color }: { label:string; value:string; sub:string; color:string }) { return (

{label}

-

{value}

+

{value}

+ {sub &&

{sub}

}
) } + +function Section({ title, children }: { title:string; children:React.ReactNode }) { + return ( +
+

{title}

+ {children} +
+ ) +} + +import { Link } from 'react-router-dom' + +function QuickLink({ to, label, desc }: { to:string; label:string; desc:string }) { + return ( + +

{label}

+

{desc}

+ + ) +} + +function formatUptime(s: number) { + if (s < 60) return `${s}s` + if (s < 3600) return `${Math.floor(s/60)}m` + return `${Math.floor(s/3600)}h${Math.floor((s%3600)/60)}m` +} diff --git a/frontend/web/src/pages/admin/IoTManagementPage.tsx b/frontend/web/src/pages/admin/IoTManagementPage.tsx new file mode 100644 index 0000000..1ce18bf --- /dev/null +++ b/frontend/web/src/pages/admin/IoTManagementPage.tsx @@ -0,0 +1,59 @@ +import { useEffect, useState } from 'react' +import type { IotDevice } from '../../types/chat' + +const mockDevices: IotDevice[] = [ + { id: 'light-livingroom', name: '客厅灯', type: 'light', state: { power: false, brightness: 80 }, online: true }, + { id: 'light-bedroom', name: '卧室灯', type: 'light', state: { power: false, brightness: 60 }, online: true }, + { id: 'ac-livingroom', name: '客厅空调', type: 'ac', state: { power: false, temperature: 26, mode: 'cool' }, online: true }, + { id: 'ac-bedroom', name: '卧室空调', type: 'ac', state: { power: false, temperature: 25, mode: 'heat' }, online: true }, + { id: 'curtain-livingroom', name: '客厅窗帘', type: 'curtain', state: { power: false, position: 0 }, online: true }, + { id: 'sensor-temperature', name: '温度传感器', type: 'sensor', state: { temperature: 25.0 }, online: true }, + { id: 'sensor-humidity', name: '湿度传感器', type: 'sensor', state: { humidity: 55.0 }, online: true }, + { id: 'lock-door', name: '智能门锁', type: 'lock', state: { locked: true, battery: 85 }, online: true }, +] + +export default function IoTManagementPage() { + const [devices, setDevices] = useState(mockDevices) + + const toggle = (id: string) => { + setDevices(prev => prev.map(d => { + if (d.id !== id) return d + const newState = { ...d.state, power: !(d.state.power as boolean) } + return { ...d, state: newState } + })) + } + + return ( +
+

IoT 设备管理

+

8 个模拟智能家居设备(测试环境)

+ +
+ {devices.map(d => { + const isOn = d.type !== 'sensor' && d.state.power === true + return ( +
+
+

{d.name}

+

+ {d.type === 'light' && `亮度: ${d.state.brightness}%`} + {d.type === 'ac' && `${d.state.temperature}°C ${d.state.mode}`} + {d.type === 'curtain' && `位置: ${d.state.position}%`} + {d.type === 'sensor' && `${JSON.stringify(d.state)}`} + {d.type === 'lock' && `电量: ${d.state.battery}%`} +

+
+ {d.type !== 'sensor' && ( + + )} +
+ ) + })} +
+
+ ) +} diff --git a/frontend/web/src/pages/admin/MemoryPage.tsx b/frontend/web/src/pages/admin/MemoryPage.tsx new file mode 100644 index 0000000..4272bc9 --- /dev/null +++ b/frontend/web/src/pages/admin/MemoryPage.tsx @@ -0,0 +1,101 @@ +import { useState, useCallback } from 'react' +import { api, getToken } from '../../api/client' + +interface MemoryEntry { + id: string; user_id: string; content: string; summary: string; + keywords: string[]; category: string; priority: number; +} + +export default function MemoryPage() { + const [memories, setMemories] = useState([]) + const [search, setSearch] = useState('') + const [loading, setLoading] = useState(false) + const [newContent, setNewContent] = useState('') + + const fetchMemories = useCallback(async (q?: string) => { + setLoading(true) + try { + const token = getToken() + const path = q ? `/api/v1/memory/search?q=${encodeURIComponent(q)}` : '/api/v1/memory' + const res = await fetch(path, { headers: { Authorization: `Bearer ${token}` } }) + const data = await res.json() + setMemories(Array.isArray(data?.data) ? data.data : []) + } catch { } finally { setLoading(false) } + }, []) + + const handleAdd = async () => { + if (!newContent.trim()) return + const token = getToken() + await fetch('/api/v1/memory', { + method: 'POST', + headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, + body: JSON.stringify({ content: newContent, category: 'fact', priority: 0.5 }), + }) + setNewContent('') + fetchMemories(search || undefined) + } + + const handleDelete = async (id: string) => { + const token = getToken() + await fetch(`/api/v1/memory/${id}`, { method: 'DELETE', headers: { Authorization: `Bearer ${token}` } }) + fetchMemories(search || undefined) + } + + return ( +
+

记忆管理

+ + {/* Search */} +
+ setSearch(e.target.value)} + onKeyDown={e => { if (e.key === 'Enter') fetchMemories(search) }} + placeholder="搜索记忆..." + className="flex-1 bg-[#000] border border-[#404040] focus:border-[#277AF7] rounded-[14px] px-4 py-2.5 text-sm text-[#F2F2F2] placeholder:text-white/30 outline-none" + /> + + +
+ + {/* Add */} +
+ setNewContent(e.target.value)} + placeholder="添加新记忆..." + className="flex-1 bg-[#000] border border-[#404040] focus:border-[#277AF7] rounded-[14px] px-4 py-2.5 text-sm text-[#F2F2F2] placeholder:text-white/30 outline-none" + /> + +
+ + {/* List */} + {loading ? ( +

加载中...

+ ) : memories.length === 0 ? ( +

暂无记忆

+ ) : ( +
+ {memories.map(m => ( +
+
+

{m.content}

+
+ {m.category} + {m.keywords?.slice(0, 3).map((kw, i) => ( + {kw} + ))} +
+
+ +
+ ))} +
+ )} +
+ ) +} diff --git a/frontend/web/src/pages/admin/PluginsPage.tsx b/frontend/web/src/pages/admin/PluginsPage.tsx new file mode 100644 index 0000000..32c0f7b --- /dev/null +++ b/frontend/web/src/pages/admin/PluginsPage.tsx @@ -0,0 +1,39 @@ +import { useEffect, useState } from 'react' +import { api } from '../../api/client' + +const builtInPlugins = [ + { name: 'iot-mock', desc: '8 个模拟智能家居设备', type: 'IoT' }, + { name: 'calculator', desc: '数学表达式求值', type: '工具' }, + { name: 'datetime', desc: '日期时间计算', type: '工具' }, + { name: 'crypto', desc: '哈希/加解密', type: '工具' }, + { name: 'random', desc: '随机数生成', type: '工具' }, + { name: 'text', desc: '文本处理(统计/翻转)', type: '工具' }, + { name: 'json', desc: 'JSON 解析/格式化', type: '工具' }, + { name: 'markdown', desc: 'Markdown 渲染', type: '工具' }, + { name: 'file', desc: '文件操作', type: '工具' }, + { name: 'http', desc: 'HTTP 请求', type: '工具' }, + { name: 'web-fetch', desc: '网页内容抓取', type: '工具' }, +] + +export default function PluginsPage() { + return ( +
+

插件管理

+

已加载 {builtInPlugins.length} 个插件。工具插件为 LLM 提供可调用能力。

+ +
+ {builtInPlugins.map(p => ( +
+
+

{p.name}

+

{p.desc}

+
+ {p.type} +
+ ))} +
+
+ ) +} diff --git a/frontend/web/src/pages/admin/ServicesPage.tsx b/frontend/web/src/pages/admin/ServicesPage.tsx new file mode 100644 index 0000000..32cd17f --- /dev/null +++ b/frontend/web/src/pages/admin/ServicesPage.tsx @@ -0,0 +1,46 @@ +import { useEffect, useState } from 'react' +import { api } from '../../api/client' + +const builtInServices = [ + { id: 'ai_core', name: 'AI 核心', desc: 'LLM 编排引擎', port: '8081' }, + { id: 'memory', name: '记忆服务', desc: 'pgvector 语义检索', port: '8091' }, + { id: 'voice', name: '语音服务', desc: 'STT / TTS', port: '8093' }, + { id: 'gateway', name: '网关', desc: 'API + WebSocket 入口', port: '8080' }, +] + +export default function ServicesPage() { + const [services, setServices] = useState>({}) + + useEffect(() => { + api.admin.dashboard().then(r => { + setServices(r.dashboard?.services as Record || {}) + }) + }, []) + + return ( +
+

服务管理

+

单二进制模式 — 所有服务编译在同一进程。服务不可单独启停。

+ +
+ {builtInServices.map(svc => { + const alive = services[svc.id] ?? false + return ( +
+
+ +
+

{svc.name}

+

{svc.desc} · 端口 {svc.port}

+
+
+ + {alive ? '运行中' : '未启动'} + +
+ ) + })} +
+
+ ) +}