feat(ui): add light theme with system auto-switch + Cyrene avatar

- Miuix day/night dual theme CSS variables
- themeStore: light default, follows prefers-color-scheme
- Header: sun/moon toggle button
- All components migrated to CSS variables
- Cyrene avatar image replaces 'C' logo everywhere
- Favicon added
This commit is contained in:
Aska committed 2026-08-12 17:42:18 +08:00
1 parent 78c5f0f1b0
commit 5faa955bce
24 files changed
+337 -218

No files matched your search

+1
View File
@@ -3,6 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/png" href="/images/cyrene-avatar.png" />
<title>Cyrene — 昔涟</title>
</head>
<body class="bg-gray-950 text-gray-100">
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

@@ -18,9 +18,9 @@ export default function AdminLayout() {
return (
<div className="flex h-full">
{/* Sidebar */}
<nav className="w-52 bg-[#000] border-r border-[#393939] shrink-0 flex flex-col">
<div className="p-4 border-b border-[#393939]">
<h2 className="text-sm font-semibold text-white/80">管理面板</h2>
<nav className="w-52 bg-[var(--mx-surface)] border-r border-[var(--mx-divider)] shrink-0 flex flex-col">
<div className="p-4 border-b border-[var(--mx-divider)]">
<h2 className="text-sm font-semibold text-[var(--mx-text-secondary)]">管理面板</h2>
</div>
<div className="flex-1 p-2 space-y-0.5">
{navItems.map(item => (
@@ -30,8 +30,8 @@ export default function AdminLayout() {
className={({ isActive }) =>
`flex items-center gap-3 px-3 py-2.5 rounded-[14px] text-sm transition-colors ${
isActive
? 'bg-[#277AF7]/15 text-[#4788FF] font-medium'
: 'text-white/50 hover:text-white/80 hover:bg-white/5'
? 'bg-[var(--mx-primary)]/15 text-[var(--mx-tertiary-text)] font-medium'
: 'text-[var(--mx-text-dim)] hover:text-[var(--mx-text-secondary)] hover:bg-[color-mix(in srgb, var(--mx-primary) 5%, transparent)]'
}`
}
>
@@ -43,7 +43,7 @@ export default function AdminLayout() {
</nav>
{/* Content area */}
<div className="flex-1 overflow-auto bg-[#242424]">
<div className="flex-1 overflow-auto bg-[var(--mx-bg)]">
<Outlet />
</div>
</div>
@@ -40,10 +40,10 @@ export default function ChatContainer() {
<div className="max-w-2xl mx-auto px-4 py-6">
{messages.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full text-center space-y-2 pt-32">
<div className="w-16 h-16 rounded-[20px] bg-[#277AF7] flex items-center justify-center shadow-lg shadow-[#277AF7]/20 mb-2">
<span className="text-2xl font-bold text-white">C</span>
<div className="w-16 h-16 rounded-[20px] bg-[var(--mx-primary)] flex items-center justify-center shadow-lg shadow-[var(--mx-primary)]/20 mb-2">
<img src="/images/cyrene-avatar.png" alt="Cyrene" className="w-full h-full object-cover" />
</div>
<p className="text-white/50 text-sm">开始新对话吧~</p>
<p className="text-[var(--mx-text-dim)] text-sm">开始新对话吧~</p>
</div>
) : (
<MessageList messages={messages} />
@@ -53,7 +53,7 @@ export default function ChatContainer() {
</div>
{/* Input bar */}
<div className="shrink-0 border-t border-[#393939] bg-[#000]">
<div className="shrink-0 border-t border-[var(--mx-divider)] bg-[var(--mx-surface)]">
<div className="max-w-2xl mx-auto px-4 py-3">
<ChatInput value={inputText} onChange={setInput} onSend={handleSend} disabled={isStreaming} />
</div>
@@ -14,7 +14,7 @@ export default function ChatInput({ value, onChange, onSend, disabled }: ChatInp
}
return (
<div className="flex items-end gap-2 bg-[#000] rounded-[20px] border border-[#404040] focus-within:border-[#277AF7] transition-colors p-1.5">
<div className="flex items-end gap-2 bg-[var(--mx-surface)] rounded-[20px] border border-[var(--mx-outline)] focus-within:border-[var(--mx-primary)] transition-colors p-1.5">
<input
type="text"
value={value}
@@ -22,12 +22,12 @@ export default function ChatInput({ value, onChange, onSend, disabled }: ChatInp
onKeyDown={handleKeyDown}
disabled={disabled}
placeholder="输入消息..."
className="flex-1 bg-transparent px-3 py-1.5 text-sm text-[#F2F2F2] placeholder:text-white/30 outline-none disabled:opacity-40"
className="flex-1 bg-transparent px-3 py-1.5 text-sm text-[var(--mx-text)] placeholder:text-[var(--mx-text-dim)] outline-none disabled:opacity-40"
/>
<button
onClick={onSend}
disabled={disabled || !value.trim()}
className="w-9 h-9 bg-[#277AF7] hover:bg-[#3482FF] active:scale-90 disabled:bg-[#253E64] text-white rounded-full flex items-center justify-center shrink-0 transition-all duration-200"
className="w-9 h-9 bg-[var(--mx-primary)] hover:bg-[var(--mx-primary-hover)] active:scale-90 disabled:bg-[#253E64] text-white rounded-full flex items-center justify-center shrink-0 transition-all duration-200"
>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M2 8L14 2L8 14L6.5 9.5L2 8Z" fill="currentColor" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
@@ -19,7 +19,7 @@ function MessageBubble({ message }: { message: Message }) {
if (isAction) {
return (
<div className="flex justify-center py-1">
<span className="text-xs text-[#4788FF] bg-[#2B3B54]/80 px-3 py-1 rounded-full">
<span className="text-xs text-[var(--mx-tertiary-text)] bg-[var(--mx-badge-blue)]/80 px-3 py-1 rounded-full">
{message.content}
</span>
</div>
@@ -29,7 +29,7 @@ function MessageBubble({ message }: { message: Message }) {
if (isSystem) {
return (
<div className="flex justify-center py-1">
<span className="text-xs text-[#ffdad6] bg-[#2E0603]/80 px-3 py-1 rounded-full">{message.content}</span>
<span className="text-xs text-[var(--mx-error-text)] bg-[var(--mx-error-container)]/80 px-3 py-1 rounded-full">{message.content}</span>
</div>
)
}
@@ -38,17 +38,17 @@ function MessageBubble({ message }: { message: Message }) {
<div className={`flex ${isUser ? 'justify-end' : 'justify-start'}`}>
{/* Avatar for AI */}
{!isUser && (
<div className="w-8 h-8 rounded-xl bg-[#277AF7] flex items-center justify-center shrink-0 mr-2 mt-1">
<span className="text-xs font-bold text-white">C</span>
<div className="w-8 h-8 rounded-xl bg-[var(--mx-primary)] flex items-center justify-center shrink-0 mr-2 mt-1">
<img src="/images/cyrene-avatar.png" alt="Cyrene" className="w-full h-full object-cover" />
</div>
)}
<div className={`max-w-[75%] px-4 py-2.5 text-sm leading-relaxed ${
isUser
? 'bg-[#277AF7] text-white rounded-[18px] rounded-br-[6px]'
? 'bg-[var(--mx-primary)] text-white rounded-[18px] rounded-br-[6px]'
: isEmpty
? 'bg-transparent text-transparent'
: 'bg-[#000] text-[#F2F2F2] rounded-[18px] rounded-bl-[6px] border border-[#404040]'
: 'bg-[var(--mx-surface)] text-[var(--mx-text)] rounded-[18px] rounded-bl-[6px] border border-[var(--mx-outline)]'
}`}>
{isEmpty ? (
<span className="flex gap-1.5 py-1">
@@ -1,41 +1,51 @@
import { ReactNode } from 'react'
import { Link, useLocation } from 'react-router-dom'
import { useAuthStore } from '../../store/authStore'
import { useThemeStore } from '../../store/themeStore'
export default function AppLayout({ children }: { children: ReactNode }) {
const { username, logout } = useAuthStore()
const { theme, toggle } = useThemeStore()
const location = useLocation()
return (
<div className="h-screen flex flex-col bg-[#242424] text-[#F2F2F2]">
<div className="h-screen flex flex-col text-[var(--mx-text)]" style={{ background: 'var(--mx-bg)' }}>
{/* Header */}
<header className="h-14 shrink-0 flex items-center justify-between px-4 border-b border-[#393939] bg-[#000]">
<header className="h-14 shrink-0 flex items-center justify-between px-4 border-b mx-glass" style={{ borderColor: 'var(--mx-divider)', background: 'var(--mx-surface)' }}>
<Link to="/chat" className="flex items-center gap-2">
<div className="w-7 h-7 rounded-lg bg-[#277AF7] flex items-center justify-center">
<span className="text-sm font-bold text-white">C</span>
<div className="w-7 h-7 rounded-lg overflow-hidden flex items-center justify-center" style={{ background: 'var(--mx-primary)' }}>
<img src="/images/cyrene-avatar.png" alt="Cyrene" className="w-full h-full object-cover" />
</div>
<span className="font-semibold text-base">Cyrene</span>
</Link>
<nav className="flex items-center gap-1">
<Link to="/chat"
className={`px-3 py-1.5 rounded-full text-sm transition-colors ${
location.pathname.startsWith('/chat')
? 'bg-[#277AF7]/15 text-[#4788FF]'
: 'text-white/50 hover:text-white/80'
}`}
className="px-3 py-1.5 rounded-full text-sm transition-colors"
style={{
background: location.pathname.startsWith('/chat') ? 'color-mix(in srgb, var(--mx-primary) 15%, transparent)' : 'transparent',
color: location.pathname.startsWith('/chat') ? 'var(--mx-tertiary-text)' : 'var(--mx-text-dim)',
}}
>聊天</Link>
<Link to="/admin/dashboard"
className={`px-3 py-1.5 rounded-full text-sm transition-colors ${
location.pathname.startsWith('/admin')
? 'bg-[#277AF7]/15 text-[#4788FF]'
: 'text-white/50 hover:text-white/80'
}`}
className="px-3 py-1.5 rounded-full text-sm transition-colors"
style={{
background: location.pathname.startsWith('/admin') ? 'color-mix(in srgb, var(--mx-primary) 15%, transparent)' : 'transparent',
color: location.pathname.startsWith('/admin') ? 'var(--mx-tertiary-text)' : 'var(--mx-text-dim)',
}}
>管理</Link>
<div className="w-px h-4 bg-[#393939] mx-1" />
<span className="text-xs text-white/40">{username}</span>
<div className="w-px h-4 mx-1" style={{ background: 'var(--mx-divider)' }} />
{/* Theme toggle */}
<button onClick={toggle}
className="w-8 h-8 rounded-full flex items-center justify-center text-lg transition-colors hover:opacity-80"
title={theme === 'dark' ? '切换日间模式' : '切换夜间模式'}
>
{theme === 'dark' ? '☀️' : '🌙'}
</button>
<span className="text-xs" style={{ color: 'var(--mx-text-dim)' }}>{username}</span>
<button onClick={logout}
className="px-3 py-1.5 rounded-full text-sm text-white/40 hover:text-[#F12522] transition-colors"
className="px-3 py-1.5 rounded-full text-sm transition-colors"
style={{ color: 'var(--mx-text-dim)' }}
>退出</button>
</nav>
</header>
@@ -16,10 +16,10 @@ export default function Sidebar() {
}
return (
<aside className="w-60 bg-[#000] border-r border-[#393939] shrink-0 flex flex-col">
<aside className="w-60 bg-[var(--mx-surface)] border-r border-[var(--mx-divider)] shrink-0 flex flex-col">
<div className="p-3">
<button onClick={handleNewChat}
className="w-full py-2.5 bg-[#277AF7] hover:bg-[#3482FF] active:scale-[0.98] text-white font-medium text-sm rounded-full transition-all duration-200"
className="w-full py-2.5 bg-[var(--mx-primary)] hover:bg-[var(--mx-primary-hover)] active:scale-[0.98] text-white font-medium text-sm rounded-full transition-all duration-200"
>
+ 新对话
</button>
@@ -27,11 +27,11 @@ export default function Sidebar() {
<div className="flex-1 overflow-auto px-2 pb-2">
{currentSessionId ? (
<div className="px-3 py-2 bg-[#242424] rounded-[14px]">
<p className="text-xs text-white/40 truncate">{currentSessionId.slice(0, 12)}...</p>
<div className="px-3 py-2 bg-[var(--mx-bg)] rounded-[14px]">
<p className="text-xs text-[var(--mx-text-dim)] truncate">{currentSessionId.slice(0, 12)}...</p>
</div>
) : (
<p className="px-3 py-2 text-xs text-white/30 text-center">暂无对话</p>
<p className="px-3 py-2 text-xs text-[var(--mx-text-dim)] text-center">暂无对话</p>
)}
</div>
</aside>
+100 -51
View File
@@ -1,33 +1,46 @@
@import "tailwindcss";
@theme {
/* Miuix Dark Color Scheme — Xiaomi HyperOS */
--color-mx-primary: #277AF7;
--color-mx-primary-hover: #3482FF;
--color-mx-primary-container: #338FE4;
--color-mx-on-primary: #FFFFFF;
/* =============================================
Miuix HyperOS Design Tokens
日间 (Light) + 夜间 (Dark)
============================================= */
--color-mx-bg: #242424;
--color-mx-bg-elevated: #000000;
--color-mx-bg-high: #2D2D2D;
/* 夜间模式 (默认) */
:root {
--mx-primary: #3482FF;
--mx-primary-hover: #277AF7;
--mx-primary-container: #338FE4;
--mx-on-primary: #FFFFFF;
--color-mx-surface: #000000;
--color-mx-surface-variant: #242424;
--color-mx-surface-high: #2D2D2D;
--mx-bg: #242424;
--mx-bg-elevated: #000000;
--mx-bg-high: #2D2D2D;
--color-mx-text: #F2F2F2;
--color-mx-text-secondary: rgba(255,255,255,0.8);
--color-mx-text-dim: rgba(255,255,255,0.5);
--color-mx-text-disabled: #666666;
--mx-surface: #000000;
--mx-surface-variant: #242424;
--mx-surface-high: #2D2D2D;
--color-mx-outline: #404040;
--color-mx-divider: #393939;
--mx-text: #F2F2F2;
--mx-text-secondary: rgba(255,255,255,0.8);
--mx-text-dim: rgba(255,255,255,0.5);
--mx-text-disabled: #666666;
--color-mx-error: #F12522;
--color-mx-error-container: #2E0603;
--mx-outline: #404040;
--mx-divider: #393939;
--color-mx-tertiary: #2B3B54;
--color-mx-tertiary-text: #4788FF;
--mx-error: #F12522;
--mx-error-container: #2E0603;
--mx-error-text: #ffdad6;
--mx-tertiary: #2B3B54;
--mx-tertiary-text: #4788FF;
--mx-badge-blue: #2B3B54;
--mx-switch-off: #404040;
--mx-switch-on: #3482FF;
--mx-stat-value: #F2F2F2;
--radius-mx-card: 18px;
--radius-mx-button: 9999px;
@@ -37,64 +50,100 @@
--font-mx: -apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue", sans-serif;
}
/* 日间模式 */
[data-theme="light"] {
--mx-primary: #3482FF;
--mx-primary-hover: #277AF7;
--mx-primary-container: #5D9BFF;
--mx-on-primary: #FFFFFF;
--mx-bg: #FFFFFF;
--mx-bg-elevated: #F7F7F7;
--mx-bg-high: #E8E8E8;
--mx-surface: #FFFFFF;
--mx-surface-variant: #F7F7F7;
--mx-surface-high: #E8E8E8;
--mx-text: #000000;
--mx-text-secondary: rgba(0,0,0,0.7);
--mx-text-dim: rgba(0,0,0,0.4);
--mx-text-disabled: #B2B2B2;
--mx-outline: #D9D9D9;
--mx-divider: #E0E0E0;
--mx-error: #E94634;
--mx-error-container: #FDF6F4;
--mx-error-text: #410002;
--mx-tertiary: #EAF2FF;
--mx-tertiary-text: #3482FF;
--mx-badge-blue: #EAF2FF;
--mx-switch-off: #D9D9D9;
--mx-switch-on: #3482FF;
--mx-stat-value: #000000;
}
/* =============================================
Base styles
============================================= */
body {
margin: 0;
background: var(--color-mx-bg);
color: var(--color-mx-text);
background: var(--mx-bg);
color: var(--mx-text);
font-family: var(--font-mx);
-webkit-font-smoothing: antialiased;
}
::-webkit-scrollbar { width: 4px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--color-mx-outline); border-radius: 2px; }
::-webkit-scrollbar-thumb { background: var(--mx-outline); border-radius: 2px; }
/* Smooth focus ring */
*:focus-visible {
outline: 2px solid var(--color-mx-primary);
outline: 2px solid var(--mx-primary);
outline-offset: 2px;
border-radius: var(--radius-mx-button);
}
/* Glass effect for header */
/* =============================================
Utility classes (using CSS variables)
============================================= */
.mx-glass {
background: rgba(36, 36, 36, 0.85);
background: rgba(36,36,36,0.85);
backdrop-filter: blur(20px) saturate(1.5);
-webkit-backdrop-filter: blur(20px) saturate(1.5);
}
/* Smooth transitions */
.mx-transition {
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
[data-theme="light"] .mx-glass {
background: rgba(255,255,255,0.85);
}
/* Card surface */
.mx-card {
background: var(--color-mx-surface);
background: var(--mx-surface);
border-radius: var(--radius-mx-card);
border: 0.5px solid var(--color-mx-outline);
border: 0.5px solid var(--mx-outline);
}
/* Button base */
.mx-btn {
background: var(--color-mx-primary);
color: var(--color-mx-on-primary);
.mx-btn-primary {
background: var(--mx-primary);
color: var(--mx-on-primary);
border-radius: var(--radius-mx-button);
font-weight: 500;
transition: all 0.2s ease;
}
.mx-btn:hover { background: var(--color-mx-primary-hover); }
.mx-btn:active { transform: scale(0.97); }
.mx-btn:disabled { background: #253E64; color: #677993; }
.mx-btn-primary:hover { background: var(--mx-primary-hover); }
.mx-btn-primary:active { transform: scale(0.97); }
/* Input base */
.mx-input {
background: var(--color-mx-surface);
border: 0.5px solid var(--color-mx-outline);
background: var(--mx-surface);
border: 0.5px solid var(--mx-outline);
border-radius: var(--radius-mx-input);
color: var(--color-mx-text);
color: var(--mx-text);
outline: none;
transition: border-color 0.2s;
}
.mx-input:focus { border-color: var(--color-mx-primary); }
.mx-input::placeholder { color: var(--color-mx-text-dim); }
.mx-input:focus { border-color: var(--mx-primary); }
.mx-input::placeholder { color: var(--mx-text-dim); }
+1
View File
@@ -3,6 +3,7 @@ import { createRoot } from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
import App from './App'
import './index.css'
import './store/themeStore' // Initialize theme (light mode default, follows system)
createRoot(document.getElementById('root')!).render(
<StrictMode>
+9 -9
View File
@@ -25,20 +25,20 @@ export default function LoginPage() {
}
return (
<div className="min-h-screen flex items-center justify-center bg-[#242424] p-4">
<div className="min-h-screen flex items-center justify-center bg-[var(--mx-bg)] p-4">
<form onSubmit={handleSubmit} className="w-full max-w-sm space-y-5">
{/* Logo area */}
<div className="text-center space-y-2 mb-8">
<div className="mx-auto w-20 h-20 rounded-[22px] bg-[#277AF7] flex items-center justify-center shadow-lg shadow-[#277AF7]/20">
<span className="text-3xl font-bold text-white">C</span>
<div className="mx-auto w-20 h-20 rounded-[22px] bg-[var(--mx-primary)] flex items-center justify-center shadow-lg shadow-[var(--mx-primary)]/20">
<img src="/images/cyrene-avatar.png" alt="Cyrene" className="w-full h-full object-cover" />
</div>
<h1 className="text-2xl font-semibold text-[#F2F2F2]">Cyrene</h1>
<p className="text-sm text-white/50">LLM 智能体平台</p>
<h1 className="text-2xl font-semibold text-[var(--mx-text)]">Cyrene</h1>
<p className="text-sm text-[var(--mx-text-dim)]">LLM 智能体平台</p>
</div>
{/* Error */}
{error && (
<div className="bg-[#2E0603] border border-[#F12522]/30 rounded-[14px] px-4 py-3 text-sm text-[#ffdad6] text-center">
<div className="bg-[var(--mx-error-container)] border border-[var(--mx-error)]/30 rounded-[14px] px-4 py-3 text-sm text-[var(--mx-error-text)] text-center">
{error}
</div>
)}
@@ -49,19 +49,19 @@ export default function LoginPage() {
type="text" value={username}
onChange={e => setUsername(e.target.value)}
placeholder="用户名"
className="w-full bg-[#000] border border-[#404040] focus:border-[#277AF7] rounded-[14px] px-4 py-3 text-[#F2F2F2] placeholder:text-white/50 outline-none transition-colors"
className="w-full bg-[var(--mx-surface)] border border-[var(--mx-outline)] focus:border-[var(--mx-primary)] rounded-[14px] px-4 py-3 text-[var(--mx-text)] placeholder:text-[var(--mx-text-dim)] outline-none transition-colors"
/>
<input
type="password" value={password}
onChange={e => setPassword(e.target.value)}
placeholder="密码"
className="w-full bg-[#000] border border-[#404040] focus:border-[#277AF7] rounded-[14px] px-4 py-3 text-[#F2F2F2] placeholder:text-white/50 outline-none transition-colors"
className="w-full bg-[var(--mx-surface)] border border-[var(--mx-outline)] focus:border-[var(--mx-primary)] rounded-[14px] px-4 py-3 text-[var(--mx-text)] placeholder:text-[var(--mx-text-dim)] outline-none transition-colors"
/>
</div>
{/* Submit */}
<button type="submit" disabled={loading}
className="w-full bg-[#277AF7] hover:bg-[#3482FF] active:scale-[0.97] disabled:bg-[#253E64] disabled:text-[#677993] text-white font-medium py-3 rounded-full transition-all duration-200"
className="w-full bg-[var(--mx-primary)] hover:bg-[var(--mx-primary-hover)] active:scale-[0.97] disabled:bg-[#253E64] disabled:text-[#677993] text-white font-medium py-3 rounded-full transition-all duration-200"
>
{loading ? '登录中...' : '登录'}
</button>
+8 -8
View File
@@ -9,26 +9,26 @@ export default function BridgePage() {
return (
<div className="p-6 space-y-4">
<h2 className="text-lg font-semibold">平台桥接</h2>
<p className="text-xs text-white/40">多平台消息桥接 — QQ (OBv11) / Telegram / Discord / Webhook。通过环境变量配置启用。</p>
<p className="text-xs text-[var(--mx-text-dim)]">多平台消息桥接 — QQ (OBv11) / Telegram / Discord / Webhook。通过环境变量配置启用。</p>
<div className="space-y-2">
{adapters.map(a => (
<div key={a.name} className="bg-[#000] border border-[#404040] rounded-[18px] px-4 py-3">
<div key={a.name} className="bg-[var(--mx-surface)] border border-[var(--mx-outline)] rounded-[18px] px-4 py-3">
<div className="flex items-center justify-between mb-1">
<div className="flex items-center gap-3">
<span className="w-2.5 h-2.5 rounded-full bg-[#F2F2F2]/30" />
<span className="text-sm text-[#F2F2F2]">{a.name}</span>
<span className="text-sm text-[var(--mx-text)]">{a.name}</span>
</div>
<span className="text-xs px-2 py-0.5 rounded-full bg-[#2B3B54]/50 text-[#4788FF]">{a.platform}</span>
<span className="text-xs px-2 py-0.5 rounded-full bg-[var(--mx-badge-blue)]/50 text-[var(--mx-tertiary-text)]">{a.platform}</span>
</div>
<p className="text-xs text-white/30 pl-7">{a.desc}</p>
<p className="text-xs text-[var(--mx-text-dim)] pl-7">{a.desc}</p>
</div>
))}
</div>
<div className="bg-[#000] border border-[#404040] rounded-[18px] p-4 mt-4">
<h3 className="text-sm font-medium text-[#F2F2F2] mb-2">群聊行为规则</h3>
<ul className="text-xs text-white/40 space-y-1 list-disc list-inside">
<div className="bg-[var(--mx-surface)] border border-[var(--mx-outline)] rounded-[18px] p-4 mt-4">
<h3 className="text-sm font-medium text-[var(--mx-text)] mb-2">群聊行为规则</h3>
<ul className="text-xs text-[var(--mx-text-dim)] space-y-1 list-disc list-inside">
<li>消息最大 500 字,超出截断</li>
<li>最短发送间隔 3 秒</li>
<li>每轮最多 2 条消息</li>
+16 -16
View File
@@ -18,22 +18,22 @@ export default function DashboardPage() {
{/* Stats */}
<div className="grid grid-cols-4 gap-3">
<StatCard label="服务" value={`${serviceCount}/${totalServices}`} sub="运行中/总数" color="#4788FF" />
<StatCard label="插件" value="11" sub="已加载" color="#277AF7" />
<StatCard label="运行时间" value={data?.uptime_sec ? formatUptime(data.uptime_sec) : '...'} sub="since start" color="#F2F2F2" />
<StatCard label="版本" value="2.0.0-dev" sub="" color="rgba(255,255,255,0.5)" />
<StatCard label="服务" value={`${serviceCount}/${totalServices}`} sub="运行中/总数" color='var(--mx-tertiary-text)' />
<StatCard label="插件" value="11" sub="已加载" color='var(--mx-primary)' />
<StatCard label="运行时间" value={data?.uptime_sec ? formatUptime(data.uptime_sec) : '...'} sub="since start" color='var(--mx-stat-value)' />
<StatCard label="版本" value="2.0.0-dev" sub="" color="var(--mx-text-dim)" />
</div>
{/* Service Status */}
<Section title="服务状态">
<div className="bg-[#000] rounded-[18px] border border-[#404040] divide-y divide-[#393939]">
<div className="bg-[var(--mx-surface)] rounded-[18px] border border-[var(--mx-outline)] divide-y divide-[var(--mx-divider)]">
{['ai_core', 'memory', 'voice'].map(name => (
<div key={name} className="flex items-center justify-between px-4 py-3">
<div className="flex items-center gap-3">
<span className={`w-2.5 h-2.5 rounded-full ${services[name] ? 'bg-[#4788FF]' : 'bg-[#404040]'}`} />
<span className="text-sm text-[#F2F2F2]">{name}</span>
<span className={`w-2.5 h-2.5 rounded-full ${services[name] ? 'bg-[var(--mx-tertiary-text)]' : 'bg-[var(--mx-outline)]'}`} />
<span className="text-sm text-[var(--mx-text)]">{name}</span>
</div>
<span className="text-xs text-white/40">{services[name] ? '运行中' : '未启动'}</span>
<span className="text-xs text-[var(--mx-text-dim)]">{services[name] ? '运行中' : '未启动'}</span>
</div>
))}
</div>
@@ -54,10 +54,10 @@ export default function DashboardPage() {
function StatCard({ label, value, sub, color }: { label:string; value:string; sub:string; color:string }) {
return (
<div className="bg-[#000] border border-[#404040] rounded-[18px] px-4 py-3">
<p className="text-xs text-white/40 mb-1">{label}</p>
<p className="text-xl font-bold" style={{color}}>{value}</p>
{sub && <p className="text-xs text-white/30 mt-0.5">{sub}</p>}
<div className="bg-[var(--mx-surface)] border border-[var(--mx-outline)] rounded-[18px] px-4 py-3">
<p className="text-xs text-[var(--mx-text-dim)] mb-1">{label}</p>
<p className="text-xl font-bold" style={{ color }}>{value}</p>
{sub && <p className="text-xs text-[var(--mx-text-dim)] mt-0.5">{sub}</p>}
</div>
)
}
@@ -65,7 +65,7 @@ function StatCard({ label, value, sub, color }: { label:string; value:string; su
function Section({ title, children }: { title:string; children:React.ReactNode }) {
return (
<div>
<h3 className="text-sm font-medium text-white/60 mb-3">{title}</h3>
<h3 className="text-sm font-medium text-[var(--mx-text-secondary)] mb-3">{title}</h3>
{children}
</div>
)
@@ -76,10 +76,10 @@ import { Link } from 'react-router-dom'
function QuickLink({ to, label, desc }: { to:string; label:string; desc:string }) {
return (
<Link to={to}
className="block bg-[#000] border border-[#404040] rounded-[18px] px-4 py-3 hover:border-[#277AF7]/50 transition-colors"
className="block bg-[var(--mx-surface)] border border-[var(--mx-outline)] rounded-[18px] px-4 py-3 hover:border-[var(--mx-primary)]/50 transition-colors"
>
<p className="text-sm text-[#F2F2F2] font-medium">{label}</p>
<p className="text-xs text-white/30 mt-1">{desc}</p>
<p className="text-sm text-[var(--mx-text)] font-medium">{label}</p>
<p className="text-xs text-[var(--mx-text-dim)] mt-1">{desc}</p>
</Link>
)
}
@@ -26,16 +26,16 @@ export default function IoTManagementPage() {
return (
<div className="p-6 space-y-4">
<h2 className="text-lg font-semibold">IoT 设备管理</h2>
<p className="text-xs text-white/40">8 个模拟智能家居设备(测试环境)</p>
<p className="text-xs text-[var(--mx-text-dim)]">8 个模拟智能家居设备(测试环境)</p>
<div className="space-y-2">
{devices.map(d => {
const isOn = d.type !== 'sensor' && d.state.power === true
return (
<div key={d.id} className="bg-[#000] border border-[#404040] rounded-[18px] px-4 py-3 flex items-center justify-between">
<div key={d.id} className="bg-[var(--mx-surface)] border border-[var(--mx-outline)] rounded-[18px] px-4 py-3 flex items-center justify-between">
<div>
<p className="text-sm text-[#F2F2F2]">{d.name}</p>
<p className="text-xs text-white/30">
<p className="text-sm text-[var(--mx-text)]">{d.name}</p>
<p className="text-xs text-[var(--mx-text-dim)]">
{d.type === 'light' && `亮度: ${d.state.brightness}%`}
{d.type === 'ac' && `${d.state.temperature}°C ${d.state.mode}`}
{d.type === 'curtain' && `位置: ${d.state.position}%`}
@@ -45,7 +45,7 @@ export default function IoTManagementPage() {
</div>
{d.type !== 'sensor' && (
<button onClick={() => toggle(d.id)}
className={`w-12 h-7 rounded-full relative transition-colors ${isOn ? 'bg-[#277AF7]' : 'bg-[#404040]'}`}
className={`w-12 h-7 rounded-full relative transition-colors ${isOn ? 'bg-[var(--mx-primary)]' : 'bg-[#404040]'}`}
>
<div className={`absolute top-0.5 w-6 h-6 rounded-full bg-white transition-transform ${isOn ? 'translate-x-6' : 'translate-x-0.5'}`} />
</button>
+14 -14
View File
@@ -34,13 +34,13 @@ export default function LLMCallsPage() {
</div>
{/* Calls table */}
<div className="bg-[#000] rounded-[18px] border border-[#404040] overflow-hidden">
<div className="bg-[var(--mx-surface)] rounded-[18px] border border-[var(--mx-outline)] overflow-hidden">
{calls.length === 0 ? (
<div className="p-8 text-center text-white/30 text-sm">暂无 LLM 调用记录</div>
<div className="p-8 text-center text-[var(--mx-text-dim)] text-sm">暂无 LLM 调用记录</div>
) : (
<table className="w-full text-sm">
<thead>
<tr className="border-b border-[#393939] text-white/40 text-xs">
<tr className="border-b border-[var(--mx-divider)] text-[var(--mx-text-dim)] text-xs">
<th className="text-left px-4 py-2 font-normal">时间</th>
<th className="text-left px-4 py-2 font-normal">模型</th>
<th className="text-left px-4 py-2 font-normal">用途</th>
@@ -51,16 +51,16 @@ export default function LLMCallsPage() {
</thead>
<tbody>
{calls.map((c, i) => (
<tr key={c.id || i} className="border-b border-[#393939]/50">
<td className="px-4 py-2.5 text-white/50 text-xs font-mono">{c.time?.slice(11,19) || '-'}</td>
<td className="px-4 py-2.5 text-[#F2F2F2]">{c.model || '-'}</td>
<td className="px-4 py-2.5 text-white/50 text-xs">
<span className="px-1.5 py-0.5 rounded bg-[#2B3B54]/50 text-[#4788FF]">{c.purpose || '-'}</span>
<tr key={c.id || i} className="border-b border-[var(--mx-divider)]/50">
<td className="px-4 py-2.5 text-[var(--mx-text-dim)] text-xs font-mono">{c.time?.slice(11,19) || '-'}</td>
<td className="px-4 py-2.5 text-[var(--mx-text)]">{c.model || '-'}</td>
<td className="px-4 py-2.5 text-[var(--mx-text-dim)] text-xs">
<span className="px-1.5 py-0.5 rounded bg-[var(--mx-badge-blue)]/50 text-[var(--mx-tertiary-text)]">{c.purpose || '-'}</span>
</td>
<td className="px-4 py-2.5 text-white/50 text-xs text-right">{c.duration_ms}ms</td>
<td className="px-4 py-2.5 text-white/50 text-xs text-right">{(c.prompt_tokens||0)+(c.completion_tokens||0)}</td>
<td className="px-4 py-2.5 text-[var(--mx-text-dim)] text-xs text-right">{c.duration_ms}ms</td>
<td className="px-4 py-2.5 text-[var(--mx-text-dim)] text-xs text-right">{(c.prompt_tokens||0)+(c.completion_tokens||0)}</td>
<td className="px-4 py-2.5 text-center">
<span className={`text-xs ${c.success ? 'text-[#4788FF]' : 'text-[#F12522]'}`}>
<span className={`text-xs ${c.success ? 'text-[var(--mx-tertiary-text)]' : 'text-[var(--mx-error)]'}`}>
{c.success ? '✓' : '✗'}
</span>
</td>
@@ -76,9 +76,9 @@ export default function LLMCallsPage() {
function MiniStat({ label, value }: { label:string; value:string|number }) {
return (
<div className="bg-[#000] border border-[#404040] rounded-[18px] px-4 py-3">
<p className="text-xs text-white/40 mb-1">{label}</p>
<p className="text-lg font-bold text-[#F2F2F2]">{value}</p>
<div className="bg-[var(--mx-surface)] border border-[var(--mx-outline)] rounded-[18px] px-4 py-3">
<p className="text-xs text-[var(--mx-text-dim)] mb-1">{label}</p>
<p className="text-lg font-bold text-[var(--mx-text)]">{value}</p>
</div>
)
}
+12 -12
View File
@@ -52,10 +52,10 @@ export default function MemoryPage() {
onChange={e => 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"
className="flex-1 bg-[var(--mx-surface)] border border-[var(--mx-outline)] focus:border-[var(--mx-primary)] rounded-[14px] px-4 py-2.5 text-sm text-[var(--mx-text)] placeholder:text-[var(--mx-text-dim)] outline-none"
/>
<button onClick={() => fetchMemories(search)} className="px-4 py-2.5 bg-[#277AF7] hover:bg-[#3482FF] text-white text-sm font-medium rounded-full">搜索</button>
<button onClick={() => { setSearch(''); fetchMemories() }} className="px-4 py-2.5 bg-[#000] border border-[#404040] text-white/50 text-sm rounded-full">全部</button>
<button onClick={() => fetchMemories(search)} className="px-4 py-2.5 bg-[var(--mx-primary)] hover:bg-[var(--mx-primary-hover)] text-white text-sm font-medium rounded-full">搜索</button>
<button onClick={() => { setSearch(''); fetchMemories() }} className="px-4 py-2.5 bg-[var(--mx-surface)] border border-[var(--mx-outline)] text-[var(--mx-text-dim)] text-sm rounded-full">全部</button>
</div>
{/* Add */}
@@ -64,33 +64,33 @@ export default function MemoryPage() {
type="text" value={newContent}
onChange={e => 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"
className="flex-1 bg-[var(--mx-surface)] border border-[var(--mx-outline)] focus:border-[var(--mx-primary)] rounded-[14px] px-4 py-2.5 text-sm text-[var(--mx-text)] placeholder:text-[var(--mx-text-dim)] outline-none"
/>
<button onClick={handleAdd} disabled={!newContent.trim()}
className="px-4 py-2.5 bg-[#277AF7] hover:bg-[#3482FF] disabled:bg-[#253E64] text-white text-sm font-medium rounded-full"
className="px-4 py-2.5 bg-[var(--mx-primary)] hover:bg-[var(--mx-primary-hover)] disabled:bg-[#253E64] text-white text-sm font-medium rounded-full"
>添加</button>
</div>
{/* List */}
{loading ? (
<p className="text-center text-white/30 py-8 text-sm">加载中...</p>
<p className="text-center text-[var(--mx-text-dim)] py-8 text-sm">加载中...</p>
) : memories.length === 0 ? (
<p className="text-center text-white/30 py-8 text-sm">暂无记忆</p>
<p className="text-center text-[var(--mx-text-dim)] py-8 text-sm">暂无记忆</p>
) : (
<div className="space-y-2">
{memories.map(m => (
<div key={m.id} className="bg-[#000] border border-[#404040] rounded-[18px] px-4 py-3 flex items-start justify-between">
<div key={m.id} className="bg-[var(--mx-surface)] border border-[var(--mx-outline)] rounded-[18px] px-4 py-3 flex items-start justify-between">
<div className="flex-1 min-w-0">
<p className="text-sm text-[#F2F2F2] truncate">{m.content}</p>
<p className="text-sm text-[var(--mx-text)] truncate">{m.content}</p>
<div className="flex items-center gap-2 mt-1">
<span className="text-xs px-1.5 py-0.5 rounded bg-[#2B3B54]/50 text-[#4788FF]">{m.category}</span>
<span className="text-xs px-1.5 py-0.5 rounded bg-[var(--mx-badge-blue)]/50 text-[var(--mx-tertiary-text)]">{m.category}</span>
{m.keywords?.slice(0, 3).map((kw, i) => (
<span key={i} className="text-xs text-white/30">{kw}</span>
<span key={i} className="text-xs text-[var(--mx-text-dim)]">{kw}</span>
))}
</div>
</div>
<button onClick={() => handleDelete(m.id)}
className="text-xs text-[#F12522]/70 hover:text-[#F12522] shrink-0 ml-3 mt-0.5"
className="text-xs text-[var(--mx-error)]/70 hover:text-[var(--mx-error)] shrink-0 ml-3 mt-0.5"
>删除</button>
</div>
))}
+26 -26
View File
@@ -43,11 +43,11 @@ export default function ModelsPage() {
<h2 className="text-lg font-semibold">模型配置</h2>
{/* Tabs */}
<div className="flex gap-1 bg-[#000] rounded-full p-1 w-fit border border-[#404040]">
<div className="flex gap-1 bg-[var(--mx-surface)] rounded-full p-1 w-fit border border-[var(--mx-outline)]">
{(['providers','models','routing'] as const).map(t => (
<button key={t} onClick={() => setTab(t)}
className={`px-4 py-1.5 rounded-full text-sm transition-colors ${
tab === t ? 'bg-[#277AF7] text-white' : 'text-white/40 hover:text-white/70'
tab === t ? 'bg-[var(--mx-primary)] text-white' : 'text-[var(--mx-text-dim)] hover:text-[var(--mx-text-secondary)]'
}`}
>{t === 'providers' ? '提供商' : t === 'models' ? '模型' : '路由'}</button>
))}
@@ -57,12 +57,12 @@ export default function ModelsPage() {
{tab === 'providers' && (
<div className="space-y-2">
{providers.map(p => (
<div key={p.name} className="bg-[#000] border border-[#404040] rounded-[18px] px-4 py-3 flex items-center justify-between">
<div key={p.name} className="bg-[var(--mx-surface)] border border-[var(--mx-outline)] rounded-[18px] px-4 py-3 flex items-center justify-between">
<div>
<p className="text-sm text-[#F2F2F2]">{p.name}</p>
<p className="text-xs text-white/30">{p.base_url} · timeout {p.timeout_sec}s · retries {p.max_retries}</p>
<p className="text-sm text-[var(--mx-text)]">{p.name}</p>
<p className="text-xs text-[var(--mx-text-dim)]">{p.base_url} · timeout {p.timeout_sec}s · retries {p.max_retries}</p>
</div>
<button onClick={() => remove('providers', p.name)} className="text-xs text-[#F12522]/60 hover:text-[#F12522]">删除</button>
<button onClick={() => remove('providers', p.name)} className="text-xs text-[var(--mx-error)]/60 hover:text-[var(--mx-error)]">删除</button>
</div>
))}
<AddProviderForm onSave={(name, body) => save('providers', name, body)} />
@@ -72,13 +72,13 @@ export default function ModelsPage() {
{tab === 'models' && (
<div className="space-y-2">
{models.map(m => (
<div key={m.id} className="bg-[#000] border border-[#404040] rounded-[18px] px-4 py-3 flex items-center justify-between">
<div key={m.id} className="bg-[var(--mx-surface)] border border-[var(--mx-outline)] rounded-[18px] px-4 py-3 flex items-center justify-between">
<div>
<p className="text-sm text-[#F2F2F2]">{m.name} <span className={`ml-2 text-xs ${m.enabled ? 'text-[#4788FF]' : 'text-white/30'}`}>{m.enabled ? '启用' : '禁用'}</span></p>
<p className="text-xs text-white/30">{m.id} · {m.provider} · {m.description}</p>
<div className="flex gap-1 mt-1">{(m.tags||[]).map(t => <span key={t} className="text-xs px-1.5 py-0.5 rounded bg-[#2B3B54]/50 text-[#4788FF]">{t}</span>)}</div>
<p className="text-sm text-[var(--mx-text)]">{m.name} <span className={`ml-2 text-xs ${m.enabled ? 'text-[var(--mx-tertiary-text)]' : 'text-[var(--mx-text-dim)]'}`}>{m.enabled ? '启用' : '禁用'}</span></p>
<p className="text-xs text-[var(--mx-text-dim)]">{m.id} · {m.provider} · {m.description}</p>
<div className="flex gap-1 mt-1">{(m.tags||[]).map(t => <span key={t} className="text-xs px-1.5 py-0.5 rounded bg-[var(--mx-badge-blue)]/50 text-[var(--mx-tertiary-text)]">{t}</span>)}</div>
</div>
<button onClick={() => remove('models', m.id)} className="text-xs text-[#F12522]/60 hover:text-[#F12522]">删除</button>
<button onClick={() => remove('models', m.id)} className="text-xs text-[var(--mx-error)]/60 hover:text-[var(--mx-error)]">删除</button>
</div>
))}
<AddModelForm providers={providers} onSave={(name, body) => save('models', name, body)} />
@@ -88,12 +88,12 @@ export default function ModelsPage() {
{tab === 'routing' && (
<div className="space-y-2">
{routing.map(r => (
<div key={r.purpose} className="bg-[#000] border border-[#404040] rounded-[18px] px-4 py-3 flex items-center justify-between">
<div key={r.purpose} className="bg-[var(--mx-surface)] border border-[var(--mx-outline)] rounded-[18px] px-4 py-3 flex items-center justify-between">
<div>
<p className="text-sm text-[#F2F2F2]">{r.purpose}</p>
<p className="text-xs text-white/30">fallback: {r.fallback_chain?.join(' → ') || 'none'} {r.required ? '(required)' : ''}</p>
<p className="text-sm text-[var(--mx-text)]">{r.purpose}</p>
<p className="text-xs text-[var(--mx-text-dim)]">fallback: {r.fallback_chain?.join(' → ') || 'none'} {r.required ? '(required)' : ''}</p>
</div>
<button onClick={() => remove('routing', r.purpose)} className="text-xs text-[#F12522]/60 hover:text-[#F12522]">删除</button>
<button onClick={() => remove('routing', r.purpose)} className="text-xs text-[var(--mx-error)]/60 hover:text-[var(--mx-error)]">删除</button>
</div>
))}
<AddRoutingForm onSave={(name, body) => save('routing', name, body)} />
@@ -109,9 +109,9 @@ function AddProviderForm({ onSave }: { onSave: (name:string, body:any) => void }
const save = () => { if(name&&url) { onSave(name, {base_url:url, timeout_sec:120, max_retries:3}); setName(''); setUrl('') } }
return (
<div className="flex gap-2">
<input value={name} onChange={e=>setName(e.target.value)} placeholder="名称" className="flex-1 bg-[#000] border border-[#404040] rounded-[14px] px-3 py-2 text-sm text-[#F2F2F2] placeholder:text-white/30 outline-none" />
<input value={url} onChange={e=>setUrl(e.target.value)} placeholder="Base URL" className="flex-1 bg-[#000] border border-[#404040] rounded-[14px] px-3 py-2 text-sm text-[#F2F2F2] placeholder:text-white/30 outline-none" />
<button onClick={save} className="px-4 py-2 bg-[#277AF7] text-white text-sm rounded-full font-medium">添加</button>
<input value={name} onChange={e=>setName(e.target.value)} placeholder="名称" className="flex-1 bg-[var(--mx-surface)] border border-[var(--mx-outline)] rounded-[14px] px-3 py-2 text-sm text-[var(--mx-text)] placeholder:text-[var(--mx-text-dim)] outline-none" />
<input value={url} onChange={e=>setUrl(e.target.value)} placeholder="Base URL" className="flex-1 bg-[var(--mx-surface)] border border-[var(--mx-outline)] rounded-[14px] px-3 py-2 text-sm text-[var(--mx-text)] placeholder:text-[var(--mx-text-dim)] outline-none" />
<button onClick={save} className="px-4 py-2 bg-[var(--mx-primary)] text-white text-sm rounded-full font-medium">添加</button>
</div>
)
}
@@ -122,13 +122,13 @@ function AddModelForm({ providers, onSave }: { providers:Provider[]; onSave: (na
const save = () => { if(id&&name) { onSave(id, {name, provider:prov, description:desc, tags:[], params:{}, enabled:true}); setId(''); setName('') } }
return (
<div className="flex flex-wrap gap-2">
<input value={id} onChange={e=>setId(e.target.value)} placeholder="ID" className="bg-[#000] border border-[#404040] rounded-[14px] px-3 py-2 text-sm w-32 text-[#F2F2F2] placeholder:text-white/30 outline-none" />
<input value={name} onChange={e=>setName(e.target.value)} placeholder="模型名" className="bg-[#000] border border-[#404040] rounded-[14px] px-3 py-2 text-sm w-32 text-[#F2F2F2] placeholder:text-white/30 outline-none" />
<select value={prov} onChange={e=>setProv(e.target.value)} className="bg-[#000] border border-[#404040] rounded-[14px] px-3 py-2 text-sm text-[#F2F2F2] outline-none">
<input value={id} onChange={e=>setId(e.target.value)} placeholder="ID" className="bg-[var(--mx-surface)] border border-[var(--mx-outline)] rounded-[14px] px-3 py-2 text-sm w-32 text-[var(--mx-text)] placeholder:text-[var(--mx-text-dim)] outline-none" />
<input value={name} onChange={e=>setName(e.target.value)} placeholder="模型名" className="bg-[var(--mx-surface)] border border-[var(--mx-outline)] rounded-[14px] px-3 py-2 text-sm w-32 text-[var(--mx-text)] placeholder:text-[var(--mx-text-dim)] outline-none" />
<select value={prov} onChange={e=>setProv(e.target.value)} className="bg-[var(--mx-surface)] border border-[var(--mx-outline)] rounded-[14px] px-3 py-2 text-sm text-[var(--mx-text)] outline-none">
{providers.map(p => <option key={p.name}>{p.name}</option>)}
</select>
<input value={desc} onChange={e=>setDesc(e.target.value)} placeholder="描述" className="bg-[#000] border border-[#404040] rounded-[14px] px-3 py-2 text-sm flex-1 text-[#F2F2F2] placeholder:text-white/30 outline-none" />
<button onClick={save} className="px-4 py-2 bg-[#277AF7] text-white text-sm rounded-full font-medium">添加</button>
<input value={desc} onChange={e=>setDesc(e.target.value)} placeholder="描述" className="bg-[var(--mx-surface)] border border-[var(--mx-outline)] rounded-[14px] px-3 py-2 text-sm flex-1 text-[var(--mx-text)] placeholder:text-[var(--mx-text-dim)] outline-none" />
<button onClick={save} className="px-4 py-2 bg-[var(--mx-primary)] text-white text-sm rounded-full font-medium">添加</button>
</div>
)
}
@@ -139,9 +139,9 @@ function AddRoutingForm({ onSave }: { onSave: (name:string, body:any) => void })
const save = () => { if(purpose) { onSave(purpose, {fallback_chain: chain.split(',').map(s=>s.trim()).filter(Boolean), required:true}); setPurpose(''); setChain('') } }
return (
<div className="flex gap-2">
<input value={purpose} onChange={e=>setPurpose(e.target.value)} placeholder="用途 (chat/intent/thinking)" className="flex-1 bg-[#000] border border-[#404040] rounded-[14px] px-3 py-2 text-sm text-[#F2F2F2] placeholder:text-white/30 outline-none" />
<input value={chain} onChange={e=>setChain(e.target.value)} placeholder="fallback chain (逗号分隔)" className="flex-2 bg-[#000] border border-[#404040] rounded-[14px] px-3 py-2 text-sm text-[#F2F2F2] placeholder:text-white/30 outline-none" />
<button onClick={save} className="px-4 py-2 bg-[#277AF7] text-white text-sm rounded-full font-medium">添加</button>
<input value={purpose} onChange={e=>setPurpose(e.target.value)} placeholder="用途 (chat/intent/thinking)" className="flex-1 bg-[var(--mx-surface)] border border-[var(--mx-outline)] rounded-[14px] px-3 py-2 text-sm text-[var(--mx-text)] placeholder:text-[var(--mx-text-dim)] outline-none" />
<input value={chain} onChange={e=>setChain(e.target.value)} placeholder="fallback chain (逗号分隔)" className="flex-2 bg-[var(--mx-surface)] border border-[var(--mx-outline)] rounded-[14px] px-3 py-2 text-sm text-[var(--mx-text)] placeholder:text-[var(--mx-text-dim)] outline-none" />
<button onClick={save} className="px-4 py-2 bg-[var(--mx-primary)] text-white text-sm rounded-full font-medium">添加</button>
</div>
)
}
@@ -37,21 +37,21 @@ export default function PerformancePage() {
{/* Memory chart */}
<div>
<h3 className="text-sm font-medium text-white/60 mb-3">内存使用趋势 (最近 {history.length} 次采样)</h3>
<div className="bg-[#000] border border-[#404040] rounded-[18px] p-4">
<h3 className="text-sm font-medium text-[var(--mx-text-secondary)] mb-3">内存使用趋势 (最近 {history.length} 次采样)</h3>
<div className="bg-[var(--mx-surface)] border border-[var(--mx-outline)] rounded-[18px] p-4">
<div className="flex items-end gap-1 h-32">
{history.slice(-60).map((h: any, i: number) => {
const pct = maxAlloc > 0 ? ((h.alloc_mb || 0) / maxAlloc) * 100 : 0
return (
<div key={i}
className="flex-1 bg-[#277AF7]/60 hover:bg-[#277AF7] rounded-t-sm transition-colors"
className="flex-1 bg-[var(--mx-primary)]/60 hover:bg-[var(--mx-primary)] rounded-t-sm transition-colors"
style={{ height: `${Math.max(2, pct)}%` }}
title={`${(h.alloc_mb||0).toFixed(1)}MB`}
/>
)
})}
</div>
<div className="flex justify-between mt-2 text-xs text-white/30">
<div className="flex justify-between mt-2 text-xs text-[var(--mx-text-dim)]">
<span>{history[0]?.timestamp?.slice(11, 19) || '--'}</span>
<span>{history[history.length-1]?.timestamp?.slice(11, 19) || '--'}</span>
</div>
@@ -60,8 +60,8 @@ export default function PerformancePage() {
{/* GC chart */}
<div>
<h3 className="text-sm font-medium text-white/60 mb-3">GC 次数趋势</h3>
<div className="bg-[#000] border border-[#404040] rounded-[18px] p-4">
<h3 className="text-sm font-medium text-[var(--mx-text-secondary)] mb-3">GC 次数趋势</h3>
<div className="bg-[var(--mx-surface)] border border-[var(--mx-outline)] rounded-[18px] p-4">
<div className="flex items-end gap-1 h-24">
{history.slice(-60).map((h: any, i: number) => {
const maxGC = Math.max(1, ...history.map((x: any) => x.num_gc || 0))
@@ -83,10 +83,10 @@ export default function PerformancePage() {
function Stat({ label, value, sub }: { label:string; value:string|number; sub:string }) {
return (
<div className="bg-[#000] border border-[#404040] rounded-[18px] px-4 py-3">
<p className="text-xs text-white/40 mb-1">{label}</p>
<p className="text-lg font-bold text-[#F2F2F2]">{value}</p>
<p className="text-xs text-white/20 mt-0.5">{sub}</p>
<div className="bg-[var(--mx-surface)] border border-[var(--mx-outline)] rounded-[18px] px-4 py-3">
<p className="text-xs text-[var(--mx-text-dim)] mb-1">{label}</p>
<p className="text-lg font-bold text-[var(--mx-text)]">{value}</p>
<p className="text-xs text-[var(--mx-text-disabled)] mt-0.5">{sub}</p>
</div>
)
}
+5 -5
View File
@@ -19,17 +19,17 @@ export default function PluginsPage() {
return (
<div className="p-6 space-y-4">
<h2 className="text-lg font-semibold">插件管理</h2>
<p className="text-xs text-white/40">已加载 {builtInPlugins.length} 个插件。工具插件为 LLM 提供可调用能力。</p>
<p className="text-xs text-[var(--mx-text-dim)]">已加载 {builtInPlugins.length} 个插件。工具插件为 LLM 提供可调用能力。</p>
<div className="grid grid-cols-2 gap-2">
{builtInPlugins.map(p => (
<div key={p.name} className="bg-[#000] border border-[#404040] rounded-[18px] px-4 py-3 flex items-center justify-between">
<div key={p.name} className="bg-[var(--mx-surface)] border border-[var(--mx-outline)] rounded-[18px] px-4 py-3 flex items-center justify-between">
<div>
<p className="text-sm text-[#F2F2F2]">{p.name}</p>
<p className="text-xs text-white/30">{p.desc}</p>
<p className="text-sm text-[var(--mx-text)]">{p.name}</p>
<p className="text-xs text-[var(--mx-text-dim)]">{p.desc}</p>
</div>
<span className={`text-xs px-2 py-0.5 rounded-full ${
p.type === 'IoT' ? 'bg-[#2B3B54] text-[#4788FF]' : 'bg-[#2B3B54]/50 text-white/50'
p.type === 'IoT' ? 'bg-[var(--mx-badge-blue)] text-[var(--mx-tertiary-text)]' : 'bg-[var(--mx-badge-blue)]/50 text-[var(--mx-text-dim)]'
}`}>{p.type}</span>
</div>
))}
@@ -20,21 +20,21 @@ export default function ServicesPage() {
return (
<div className="p-6 space-y-4">
<h2 className="text-lg font-semibold">服务管理</h2>
<p className="text-xs text-white/40">单二进制模式 — 所有服务编译在同一进程。服务不可单独启停。</p>
<p className="text-xs text-[var(--mx-text-dim)]">单二进制模式 — 所有服务编译在同一进程。服务不可单独启停。</p>
<div className="space-y-2">
{builtInServices.map(svc => {
const alive = services[svc.id] ?? false
return (
<div key={svc.id} className="bg-[#000] border border-[#404040] rounded-[18px] px-4 py-3 flex items-center justify-between">
<div key={svc.id} className="bg-[var(--mx-surface)] border border-[var(--mx-outline)] rounded-[18px] px-4 py-3 flex items-center justify-between">
<div className="flex items-center gap-3">
<span className={`w-2.5 h-2.5 rounded-full ${alive ? 'bg-[#4788FF]' : 'bg-[#404040]'}`} />
<div>
<p className="text-sm text-[#F2F2F2]">{svc.name}</p>
<p className="text-xs text-white/30">{svc.desc} · 端口 {svc.port}</p>
<p className="text-sm text-[var(--mx-text)]">{svc.name}</p>
<p className="text-xs text-[var(--mx-text-dim)]">{svc.desc} · 端口 {svc.port}</p>
</div>
</div>
<span className={`text-xs ${alive ? 'text-[#4788FF]' : 'text-white/30'}`}>
<span className={`text-xs ${alive ? 'text-[var(--mx-tertiary-text)]' : 'text-[var(--mx-text-dim)]'}`}>
{alive ? '运行中' : '未启动'}
</span>
</div>
+8 -8
View File
@@ -41,10 +41,10 @@ export default function SystemPage() {
{/* Plugins list */}
<div>
<h3 className="text-sm font-medium text-white/60 mb-3">已加载插件 ({plugins.length})</h3>
<h3 className="text-sm font-medium text-[var(--mx-text-secondary)] mb-3">已加载插件 ({plugins.length})</h3>
<div className="flex flex-wrap gap-2">
{plugins.map((p, i) => (
<span key={i} className="px-3 py-1.5 bg-[#000] border border-[#404040] rounded-full text-xs text-[#F2F2F2]">
<span key={i} className="px-3 py-1.5 bg-[var(--mx-surface)] border border-[var(--mx-outline)] rounded-full text-xs text-[var(--mx-text)]">
{p.name}
</span>
))}
@@ -53,13 +53,13 @@ export default function SystemPage() {
{/* Sessions */}
<div>
<h3 className="text-sm font-medium text-white/60 mb-3">活跃会话 ({sessions.length})</h3>
<h3 className="text-sm font-medium text-[var(--mx-text-secondary)] mb-3">活跃会话 ({sessions.length})</h3>
{sessions.length === 0 ? (
<p className="text-xs text-white/30">暂无活跃会话</p>
<p className="text-xs text-[var(--mx-text-dim)]">暂无活跃会话</p>
) : (
<div className="space-y-1">
{sessions.map((s: any, i) => (
<div key={i} className="bg-[#000] border border-[#404040] rounded-[14px] px-4 py-2 text-xs text-white/50">
<div key={i} className="bg-[var(--mx-surface)] border border-[var(--mx-outline)] rounded-[14px] px-4 py-2 text-xs text-[var(--mx-text-dim)]">
{s.session_id || s.id || '-'}
</div>
))}
@@ -72,9 +72,9 @@ export default function SystemPage() {
function StatRow({ label, value }: { label:string; value:string|number }) {
return (
<div className="bg-[#000] border border-[#404040] rounded-[18px] px-4 py-3">
<p className="text-xs text-white/40 mb-1">{label}</p>
<p className="text-lg font-bold text-[#F2F2F2]">{value}</p>
<div className="bg-[var(--mx-surface)] border border-[var(--mx-outline)] rounded-[18px] px-4 py-3">
<p className="text-xs text-[var(--mx-text-dim)] mb-1">{label}</p>
<p className="text-lg font-bold text-[var(--mx-text)]">{value}</p>
</div>
)
}
+8 -8
View File
@@ -28,16 +28,16 @@ export default function TracePage() {
return (
<div className="p-6 space-y-4">
<h2 className="text-lg font-semibold">全链路追踪</h2>
<p className="text-xs text-white/40">最近 {events.length} 条追踪事件</p>
<p className="text-xs text-[var(--mx-text-dim)]">最近 {events.length} 条追踪事件</p>
{events.length === 0 ? (
<div className="p-8 text-center text-white/30 text-sm">暂无追踪数据 — 发送消息后刷新</div>
<div className="p-8 text-center text-[var(--mx-text-dim)] text-sm">暂无追踪数据 — 发送消息后刷新</div>
) : (
<div className="space-y-1.5">
{events.map((ev, i) => (
<div key={ev.id || i}
className={`bg-[#000] border rounded-[14px] px-4 py-3 flex items-center gap-3 ${
ev.status === 'error' ? 'border-[#F12522]/30' : 'border-[#404040]'
className={`bg-[var(--mx-surface)] border rounded-[14px] px-4 py-3 flex items-center gap-3 ${
ev.status === 'error' ? 'border-[var(--mx-error)]/30' : 'border-[var(--mx-outline)]'
}`}
>
{/* Hop icon */}
@@ -46,19 +46,19 @@ export default function TracePage() {
{/* Content */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="text-sm text-[#F2F2F2]">{ev.label || ev.hop}</span>
<span className="text-sm text-[var(--mx-text)]">{ev.label || ev.hop}</span>
{ev.duration_ms > 0 && (
<span className="text-xs text-white/30">{ev.duration_ms}ms</span>
<span className="text-xs text-[var(--mx-text-dim)]">{ev.duration_ms}ms</span>
)}
</div>
{ev.detail && (
<p className="text-xs text-white/30 truncate mt-0.5">{ev.detail}</p>
<p className="text-xs text-[var(--mx-text-dim)] truncate mt-0.5">{ev.detail}</p>
)}
</div>
{/* Status */}
<div className="flex items-center gap-3 shrink-0">
<span className={`text-xs font-mono text-white/30`}>
<span className={`text-xs font-mono text-[var(--mx-text-dim)]`}>
{ev.timestamp ? ev.timestamp.slice(11, 19) : ''}
</span>
<span className={`w-2 h-2 rounded-full ${
+58
View File
@@ -0,0 +1,58 @@
import { create } from 'zustand'
type Theme = 'light' | 'dark'
function getSystemTheme(): Theme {
if (typeof window === 'undefined') return 'light'
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
}
function getStoredTheme(): Theme | null {
try {
const stored = localStorage.getItem('cyrene_theme')
if (stored === 'light' || stored === 'dark') return stored
} catch {}
return null
}
interface ThemeState {
theme: Theme
setTheme: (t: Theme) => void
toggle: () => void
}
export const useThemeStore = create<ThemeState>((set, get) => ({
theme: getStoredTheme() || 'light',
setTheme: (t: Theme) => {
document.documentElement.setAttribute('data-theme', t)
localStorage.setItem('cyrene_theme', t)
set({ theme: t })
},
toggle: () => {
const next = get().theme === 'dark' ? 'light' : 'dark'
get().setTheme(next)
},
}))
// Initialize theme on app start
if (typeof document !== 'undefined') {
const stored = getStoredTheme()
if (stored) {
document.documentElement.setAttribute('data-theme', stored)
} else {
// Follow system preference
const sys = getSystemTheme()
document.documentElement.setAttribute('data-theme', sys)
}
// Listen for system theme changes
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
if (!getStoredTheme()) {
const sys = e.matches ? 'dark' : 'light'
document.documentElement.setAttribute('data-theme', sys)
useThemeStore.setState({ theme: sys })
}
})
}