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> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <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> <title>Cyrene — 昔涟</title>
</head> </head>
<body class="bg-gray-950 text-gray-100"> <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 ( return (
<div className="flex h-full"> <div className="flex h-full">
{/* Sidebar */} {/* Sidebar */}
<nav className="w-52 bg-[#000] border-r border-[#393939] shrink-0 flex flex-col"> <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-[#393939]"> <div className="p-4 border-b border-[var(--mx-divider)]">
<h2 className="text-sm font-semibold text-white/80">管理面板</h2> <h2 className="text-sm font-semibold text-[var(--mx-text-secondary)]">管理面板</h2>
</div> </div>
<div className="flex-1 p-2 space-y-0.5"> <div className="flex-1 p-2 space-y-0.5">
{navItems.map(item => ( {navItems.map(item => (
@@ -30,8 +30,8 @@ export default function AdminLayout() {
className={({ isActive }) => className={({ isActive }) =>
`flex items-center gap-3 px-3 py-2.5 rounded-[14px] text-sm transition-colors ${ `flex items-center gap-3 px-3 py-2.5 rounded-[14px] text-sm transition-colors ${
isActive isActive
? 'bg-[#277AF7]/15 text-[#4788FF] font-medium' ? 'bg-[var(--mx-primary)]/15 text-[var(--mx-tertiary-text)] font-medium'
: 'text-white/50 hover:text-white/80 hover:bg-white/5' : '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> </nav>
{/* Content area */} {/* Content area */}
<div className="flex-1 overflow-auto bg-[#242424]"> <div className="flex-1 overflow-auto bg-[var(--mx-bg)]">
<Outlet /> <Outlet />
</div> </div>
</div> </div>
@@ -40,10 +40,10 @@ export default function ChatContainer() {
<div className="max-w-2xl mx-auto px-4 py-6"> <div className="max-w-2xl mx-auto px-4 py-6">
{messages.length === 0 ? ( {messages.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full text-center space-y-2 pt-32"> <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"> <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">
<span className="text-2xl font-bold text-white">C</span> <img src="/images/cyrene-avatar.png" alt="Cyrene" className="w-full h-full object-cover" />
</div> </div>
<p className="text-white/50 text-sm">开始新对话吧~</p> <p className="text-[var(--mx-text-dim)] text-sm">开始新对话吧~</p>
</div> </div>
) : ( ) : (
<MessageList messages={messages} /> <MessageList messages={messages} />
@@ -53,7 +53,7 @@ export default function ChatContainer() {
</div> </div>
{/* Input bar */} {/* 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"> <div className="max-w-2xl mx-auto px-4 py-3">
<ChatInput value={inputText} onChange={setInput} onSend={handleSend} disabled={isStreaming} /> <ChatInput value={inputText} onChange={setInput} onSend={handleSend} disabled={isStreaming} />
</div> </div>
@@ -14,7 +14,7 @@ export default function ChatInput({ value, onChange, onSend, disabled }: ChatInp
} }
return ( 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 <input
type="text" type="text"
value={value} value={value}
@@ -22,12 +22,12 @@ export default function ChatInput({ value, onChange, onSend, disabled }: ChatInp
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
disabled={disabled} disabled={disabled}
placeholder="输入消息..." 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 <button
onClick={onSend} onClick={onSend}
disabled={disabled || !value.trim()} 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"> <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"/> <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) { if (isAction) {
return ( return (
<div className="flex justify-center py-1"> <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} {message.content}
</span> </span>
</div> </div>
@@ -29,7 +29,7 @@ function MessageBubble({ message }: { message: Message }) {
if (isSystem) { if (isSystem) {
return ( return (
<div className="flex justify-center py-1"> <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> </div>
) )
} }
@@ -38,17 +38,17 @@ function MessageBubble({ message }: { message: Message }) {
<div className={`flex ${isUser ? 'justify-end' : 'justify-start'}`}> <div className={`flex ${isUser ? 'justify-end' : 'justify-start'}`}>
{/* Avatar for AI */} {/* Avatar for AI */}
{!isUser && ( {!isUser && (
<div className="w-8 h-8 rounded-xl bg-[#277AF7] flex items-center justify-center shrink-0 mr-2 mt-1"> <div className="w-8 h-8 rounded-xl bg-[var(--mx-primary)] flex items-center justify-center shrink-0 mr-2 mt-1">
<span className="text-xs font-bold text-white">C</span> <img src="/images/cyrene-avatar.png" alt="Cyrene" className="w-full h-full object-cover" />
</div> </div>
)} )}
<div className={`max-w-[75%] px-4 py-2.5 text-sm leading-relaxed ${ <div className={`max-w-[75%] px-4 py-2.5 text-sm leading-relaxed ${
isUser isUser
? 'bg-[#277AF7] text-white rounded-[18px] rounded-br-[6px]' ? 'bg-[var(--mx-primary)] text-white rounded-[18px] rounded-br-[6px]'
: isEmpty : isEmpty
? 'bg-transparent text-transparent' ? '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 ? ( {isEmpty ? (
<span className="flex gap-1.5 py-1"> <span className="flex gap-1.5 py-1">
@@ -1,41 +1,51 @@
import { ReactNode } from 'react' import { ReactNode } from 'react'
import { Link, useLocation } from 'react-router-dom' import { Link, useLocation } from 'react-router-dom'
import { useAuthStore } from '../../store/authStore' import { useAuthStore } from '../../store/authStore'
import { useThemeStore } from '../../store/themeStore'
export default function AppLayout({ children }: { children: ReactNode }) { export default function AppLayout({ children }: { children: ReactNode }) {
const { username, logout } = useAuthStore() const { username, logout } = useAuthStore()
const { theme, toggle } = useThemeStore()
const location = useLocation() const location = useLocation()
return ( 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 */}
<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"> <Link to="/chat" className="flex items-center gap-2">
<div className="w-7 h-7 rounded-lg bg-[#277AF7] flex items-center justify-center"> <div className="w-7 h-7 rounded-lg overflow-hidden flex items-center justify-center" style={{ background: 'var(--mx-primary)' }}>
<span className="text-sm font-bold text-white">C</span> <img src="/images/cyrene-avatar.png" alt="Cyrene" className="w-full h-full object-cover" />
</div> </div>
<span className="font-semibold text-base">Cyrene</span> <span className="font-semibold text-base">Cyrene</span>
</Link> </Link>
<nav className="flex items-center gap-1"> <nav className="flex items-center gap-1">
<Link to="/chat" <Link to="/chat"
className={`px-3 py-1.5 rounded-full text-sm transition-colors ${ className="px-3 py-1.5 rounded-full text-sm transition-colors"
location.pathname.startsWith('/chat') style={{
? 'bg-[#277AF7]/15 text-[#4788FF]' background: location.pathname.startsWith('/chat') ? 'color-mix(in srgb, var(--mx-primary) 15%, transparent)' : 'transparent',
: 'text-white/50 hover:text-white/80' color: location.pathname.startsWith('/chat') ? 'var(--mx-tertiary-text)' : 'var(--mx-text-dim)',
}`} }}
>聊天</Link> >聊天</Link>
<Link to="/admin/dashboard" <Link to="/admin/dashboard"
className={`px-3 py-1.5 rounded-full text-sm transition-colors ${ className="px-3 py-1.5 rounded-full text-sm transition-colors"
location.pathname.startsWith('/admin') style={{
? 'bg-[#277AF7]/15 text-[#4788FF]' background: location.pathname.startsWith('/admin') ? 'color-mix(in srgb, var(--mx-primary) 15%, transparent)' : 'transparent',
: 'text-white/50 hover:text-white/80' color: location.pathname.startsWith('/admin') ? 'var(--mx-tertiary-text)' : 'var(--mx-text-dim)',
}`} }}
>管理</Link> >管理</Link>
<div className="w-px h-4 bg-[#393939] mx-1" /> <div className="w-px h-4 mx-1" style={{ background: 'var(--mx-divider)' }} />
<span className="text-xs text-white/40">{username}</span> {/* 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} <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> >退出</button>
</nav> </nav>
</header> </header>
@@ -16,10 +16,10 @@ export default function Sidebar() {
} }
return ( 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"> <div className="p-3">
<button onClick={handleNewChat} <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> </button>
@@ -27,11 +27,11 @@ export default function Sidebar() {
<div className="flex-1 overflow-auto px-2 pb-2"> <div className="flex-1 overflow-auto px-2 pb-2">
{currentSessionId ? ( {currentSessionId ? (
<div className="px-3 py-2 bg-[#242424] rounded-[14px]"> <div className="px-3 py-2 bg-[var(--mx-bg)] rounded-[14px]">
<p className="text-xs text-white/40 truncate">{currentSessionId.slice(0, 12)}...</p> <p className="text-xs text-[var(--mx-text-dim)] truncate">{currentSessionId.slice(0, 12)}...</p>
</div> </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> </div>
</aside> </aside>
+100 -51
View File
@@ -1,33 +1,46 @@
@import "tailwindcss"; @import "tailwindcss";
@theme { /* =============================================
/* Miuix Dark Color Scheme — Xiaomi HyperOS */ Miuix HyperOS Design Tokens
--color-mx-primary: #277AF7; 日间 (Light) + 夜间 (Dark)
--color-mx-primary-hover: #3482FF; ============================================= */
--color-mx-primary-container: #338FE4;
--color-mx-on-primary: #FFFFFF;
--color-mx-bg: #242424; /* 夜间模式 (默认) */
--color-mx-bg-elevated: #000000; :root {
--color-mx-bg-high: #2D2D2D; --mx-primary: #3482FF;
--mx-primary-hover: #277AF7;
--mx-primary-container: #338FE4;
--mx-on-primary: #FFFFFF;
--color-mx-surface: #000000; --mx-bg: #242424;
--color-mx-surface-variant: #242424; --mx-bg-elevated: #000000;
--color-mx-surface-high: #2D2D2D; --mx-bg-high: #2D2D2D;
--color-mx-text: #F2F2F2; --mx-surface: #000000;
--color-mx-text-secondary: rgba(255,255,255,0.8); --mx-surface-variant: #242424;
--color-mx-text-dim: rgba(255,255,255,0.5); --mx-surface-high: #2D2D2D;
--color-mx-text-disabled: #666666;
--color-mx-outline: #404040; --mx-text: #F2F2F2;
--color-mx-divider: #393939; --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; --mx-outline: #404040;
--color-mx-error-container: #2E0603; --mx-divider: #393939;
--color-mx-tertiary: #2B3B54; --mx-error: #F12522;
--color-mx-tertiary-text: #4788FF; --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-card: 18px;
--radius-mx-button: 9999px; --radius-mx-button: 9999px;
@@ -37,64 +50,100 @@
--font-mx: -apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue", sans-serif; --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 { body {
margin: 0; margin: 0;
background: var(--color-mx-bg); background: var(--mx-bg);
color: var(--color-mx-text); color: var(--mx-text);
font-family: var(--font-mx); font-family: var(--font-mx);
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
} }
::-webkit-scrollbar { width: 4px; } ::-webkit-scrollbar { width: 4px; }
::-webkit-scrollbar-track { background: transparent; } ::-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 { *:focus-visible {
outline: 2px solid var(--color-mx-primary); outline: 2px solid var(--mx-primary);
outline-offset: 2px; outline-offset: 2px;
border-radius: var(--radius-mx-button); border-radius: var(--radius-mx-button);
} }
/* Glass effect for header */ /* =============================================
Utility classes (using CSS variables)
============================================= */
.mx-glass { .mx-glass {
background: rgba(36, 36, 36, 0.85); background: rgba(36,36,36,0.85);
backdrop-filter: blur(20px) saturate(1.5); backdrop-filter: blur(20px) saturate(1.5);
-webkit-backdrop-filter: blur(20px) saturate(1.5); -webkit-backdrop-filter: blur(20px) saturate(1.5);
} }
[data-theme="light"] .mx-glass {
/* Smooth transitions */ background: rgba(255,255,255,0.85);
.mx-transition {
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
} }
/* Card surface */
.mx-card { .mx-card {
background: var(--color-mx-surface); background: var(--mx-surface);
border-radius: var(--radius-mx-card); 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-primary {
.mx-btn { background: var(--mx-primary);
background: var(--color-mx-primary); color: var(--mx-on-primary);
color: var(--color-mx-on-primary);
border-radius: var(--radius-mx-button); border-radius: var(--radius-mx-button);
font-weight: 500; font-weight: 500;
transition: all 0.2s ease;
} }
.mx-btn:hover { background: var(--color-mx-primary-hover); } .mx-btn-primary:hover { background: var(--mx-primary-hover); }
.mx-btn:active { transform: scale(0.97); } .mx-btn-primary:active { transform: scale(0.97); }
.mx-btn:disabled { background: #253E64; color: #677993; }
/* Input base */
.mx-input { .mx-input {
background: var(--color-mx-surface); background: var(--mx-surface);
border: 0.5px solid var(--color-mx-outline); border: 0.5px solid var(--mx-outline);
border-radius: var(--radius-mx-input); border-radius: var(--radius-mx-input);
color: var(--color-mx-text); color: var(--mx-text);
outline: none; outline: none;
transition: border-color 0.2s;
} }
.mx-input:focus { border-color: var(--color-mx-primary); } .mx-input:focus { border-color: var(--mx-primary); }
.mx-input::placeholder { color: var(--color-mx-text-dim); } .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 { BrowserRouter } from 'react-router-dom'
import App from './App' import App from './App'
import './index.css' import './index.css'
import './store/themeStore' // Initialize theme (light mode default, follows system)
createRoot(document.getElementById('root')!).render( createRoot(document.getElementById('root')!).render(
<StrictMode> <StrictMode>
+9 -9
View File
@@ -25,20 +25,20 @@ export default function LoginPage() {
} }
return ( 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"> <form onSubmit={handleSubmit} className="w-full max-w-sm space-y-5">
{/* Logo area */} {/* Logo area */}
<div className="text-center space-y-2 mb-8"> <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"> <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">
<span className="text-3xl font-bold text-white">C</span> <img src="/images/cyrene-avatar.png" alt="Cyrene" className="w-full h-full object-cover" />
</div> </div>
<h1 className="text-2xl font-semibold text-[#F2F2F2]">Cyrene</h1> <h1 className="text-2xl font-semibold text-[var(--mx-text)]">Cyrene</h1>
<p className="text-sm text-white/50">LLM 智能体平台</p> <p className="text-sm text-[var(--mx-text-dim)]">LLM 智能体平台</p>
</div> </div>
{/* Error */} {/* Error */}
{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} {error}
</div> </div>
)} )}
@@ -49,19 +49,19 @@ export default function LoginPage() {
type="text" value={username} type="text" value={username}
onChange={e => setUsername(e.target.value)} onChange={e => setUsername(e.target.value)}
placeholder="用户名" 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 <input
type="password" value={password} type="password" value={password}
onChange={e => setPassword(e.target.value)} onChange={e => setPassword(e.target.value)}
placeholder="密码" 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> </div>
{/* Submit */} {/* Submit */}
<button type="submit" disabled={loading} <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 ? '登录中...' : '登录'} {loading ? '登录中...' : '登录'}
</button> </button>
+8 -8
View File
@@ -9,26 +9,26 @@ export default function BridgePage() {
return ( return (
<div className="p-6 space-y-4"> <div className="p-6 space-y-4">
<h2 className="text-lg font-semibold">平台桥接</h2> <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"> <div className="space-y-2">
{adapters.map(a => ( {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 justify-between mb-1">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<span className="w-2.5 h-2.5 rounded-full bg-[#F2F2F2]/30" /> <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> </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> </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> </div>
<div className="bg-[#000] border border-[#404040] rounded-[18px] p-4 mt-4"> <div className="bg-[var(--mx-surface)] border border-[var(--mx-outline)] rounded-[18px] p-4 mt-4">
<h3 className="text-sm font-medium text-[#F2F2F2] mb-2">群聊行为规则</h3> <h3 className="text-sm font-medium text-[var(--mx-text)] mb-2">群聊行为规则</h3>
<ul className="text-xs text-white/40 space-y-1 list-disc list-inside"> <ul className="text-xs text-[var(--mx-text-dim)] space-y-1 list-disc list-inside">
<li>消息最大 500 字,超出截断</li> <li>消息最大 500 字,超出截断</li>
<li>最短发送间隔 3 秒</li> <li>最短发送间隔 3 秒</li>
<li>每轮最多 2 条消息</li> <li>每轮最多 2 条消息</li>
+16 -16
View File
@@ -18,22 +18,22 @@ export default function DashboardPage() {
{/* Stats */} {/* Stats */}
<div className="grid grid-cols-4 gap-3"> <div className="grid grid-cols-4 gap-3">
<StatCard label="服务" value={`${serviceCount}/${totalServices}`} sub="运行中/总数" color="#4788FF" /> <StatCard label="服务" value={`${serviceCount}/${totalServices}`} sub="运行中/总数" color='var(--mx-tertiary-text)' />
<StatCard label="插件" value="11" sub="已加载" color="#277AF7" /> <StatCard label="插件" value="11" sub="已加载" color='var(--mx-primary)' />
<StatCard label="运行时间" value={data?.uptime_sec ? formatUptime(data.uptime_sec) : '...'} sub="since start" color="#F2F2F2" /> <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="rgba(255,255,255,0.5)" /> <StatCard label="版本" value="2.0.0-dev" sub="" color="var(--mx-text-dim)" />
</div> </div>
{/* Service Status */} {/* Service Status */}
<Section title="服务状态"> <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 => ( {['ai_core', 'memory', 'voice'].map(name => (
<div key={name} className="flex items-center justify-between px-4 py-3"> <div key={name} className="flex items-center justify-between px-4 py-3">
<div className="flex items-center gap-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={`w-2.5 h-2.5 rounded-full ${services[name] ? 'bg-[var(--mx-tertiary-text)]' : 'bg-[var(--mx-outline)]'}`} />
<span className="text-sm text-[#F2F2F2]">{name}</span> <span className="text-sm text-[var(--mx-text)]">{name}</span>
</div> </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>
))} ))}
</div> </div>
@@ -54,10 +54,10 @@ export default function DashboardPage() {
function StatCard({ label, value, sub, color }: { label:string; value:string; sub:string; color:string }) { function StatCard({ label, value, sub, color }: { label:string; value:string; sub:string; color:string }) {
return ( return (
<div className="bg-[#000] border border-[#404040] rounded-[18px] px-4 py-3"> <div className="bg-[var(--mx-surface)] border border-[var(--mx-outline)] rounded-[18px] px-4 py-3">
<p className="text-xs text-white/40 mb-1">{label}</p> <p className="text-xs text-[var(--mx-text-dim)] mb-1">{label}</p>
<p className="text-xl font-bold" style={{color}}>{value}</p> <p className="text-xl font-bold" style={{ color }}>{value}</p>
{sub && <p className="text-xs text-white/30 mt-0.5">{sub}</p>} {sub && <p className="text-xs text-[var(--mx-text-dim)] mt-0.5">{sub}</p>}
</div> </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 }) { function Section({ title, children }: { title:string; children:React.ReactNode }) {
return ( return (
<div> <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} {children}
</div> </div>
) )
@@ -76,10 +76,10 @@ import { Link } from 'react-router-dom'
function QuickLink({ to, label, desc }: { to:string; label:string; desc:string }) { function QuickLink({ to, label, desc }: { to:string; label:string; desc:string }) {
return ( return (
<Link to={to} <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-sm text-[var(--mx-text)] font-medium">{label}</p>
<p className="text-xs text-white/30 mt-1">{desc}</p> <p className="text-xs text-[var(--mx-text-dim)] mt-1">{desc}</p>
</Link> </Link>
) )
} }
@@ -26,16 +26,16 @@ export default function IoTManagementPage() {
return ( return (
<div className="p-6 space-y-4"> <div className="p-6 space-y-4">
<h2 className="text-lg font-semibold">IoT 设备管理</h2> <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"> <div className="space-y-2">
{devices.map(d => { {devices.map(d => {
const isOn = d.type !== 'sensor' && d.state.power === true const isOn = d.type !== 'sensor' && d.state.power === true
return ( 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> <div>
<p className="text-sm text-[#F2F2F2]">{d.name}</p> <p className="text-sm text-[var(--mx-text)]">{d.name}</p>
<p className="text-xs text-white/30"> <p className="text-xs text-[var(--mx-text-dim)]">
{d.type === 'light' && `亮度: ${d.state.brightness}%`} {d.type === 'light' && `亮度: ${d.state.brightness}%`}
{d.type === 'ac' && `${d.state.temperature}°C ${d.state.mode}`} {d.type === 'ac' && `${d.state.temperature}°C ${d.state.mode}`}
{d.type === 'curtain' && `位置: ${d.state.position}%`} {d.type === 'curtain' && `位置: ${d.state.position}%`}
@@ -45,7 +45,7 @@ export default function IoTManagementPage() {
</div> </div>
{d.type !== 'sensor' && ( {d.type !== 'sensor' && (
<button onClick={() => toggle(d.id)} <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'}`} /> <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> </button>
+14 -14
View File
@@ -34,13 +34,13 @@ export default function LLMCallsPage() {
</div> </div>
{/* Calls table */} {/* 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 ? ( {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"> <table className="w-full text-sm">
<thead> <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> <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> </thead>
<tbody> <tbody>
{calls.map((c, i) => ( {calls.map((c, i) => (
<tr key={c.id || i} className="border-b border-[#393939]/50"> <tr key={c.id || i} className="border-b border-[var(--mx-divider)]/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-[var(--mx-text-dim)] 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-[var(--mx-text)]">{c.model || '-'}</td>
<td className="px-4 py-2.5 text-white/50 text-xs"> <td className="px-4 py-2.5 text-[var(--mx-text-dim)] text-xs">
<span className="px-1.5 py-0.5 rounded bg-[#2B3B54]/50 text-[#4788FF]">{c.purpose || '-'}</span> <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>
<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-[var(--mx-text-dim)] 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.prompt_tokens||0)+(c.completion_tokens||0)}</td>
<td className="px-4 py-2.5 text-center"> <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 ? '✓' : '✗'} {c.success ? '✓' : '✗'}
</span> </span>
</td> </td>
@@ -76,9 +76,9 @@ export default function LLMCallsPage() {
function MiniStat({ label, value }: { label:string; value:string|number }) { function MiniStat({ label, value }: { label:string; value:string|number }) {
return ( return (
<div className="bg-[#000] border border-[#404040] rounded-[18px] px-4 py-3"> <div className="bg-[var(--mx-surface)] border border-[var(--mx-outline)] rounded-[18px] px-4 py-3">
<p className="text-xs text-white/40 mb-1">{label}</p> <p className="text-xs text-[var(--mx-text-dim)] mb-1">{label}</p>
<p className="text-lg font-bold text-[#F2F2F2]">{value}</p> <p className="text-lg font-bold text-[var(--mx-text)]">{value}</p>
</div> </div>
) )
} }
+12 -12
View File
@@ -52,10 +52,10 @@ export default function MemoryPage() {
onChange={e => setSearch(e.target.value)} onChange={e => setSearch(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter') fetchMemories(search) }} onKeyDown={e => { if (e.key === 'Enter') fetchMemories(search) }}
placeholder="搜索记忆..." 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={() => 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-[#000] border border-[#404040] text-white/50 text-sm 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> </div>
{/* Add */} {/* Add */}
@@ -64,33 +64,33 @@ export default function MemoryPage() {
type="text" value={newContent} type="text" value={newContent}
onChange={e => setNewContent(e.target.value)} onChange={e => setNewContent(e.target.value)}
placeholder="添加新记忆..." 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()} <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> >添加</button>
</div> </div>
{/* List */} {/* List */}
{loading ? ( {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 ? ( ) : 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"> <div className="space-y-2">
{memories.map(m => ( {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"> <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"> <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) => ( {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>
</div> </div>
<button onClick={() => handleDelete(m.id)} <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> >删除</button>
</div> </div>
))} ))}
+26 -26
View File
@@ -43,11 +43,11 @@ export default function ModelsPage() {
<h2 className="text-lg font-semibold">模型配置</h2> <h2 className="text-lg font-semibold">模型配置</h2>
{/* Tabs */} {/* 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 => ( {(['providers','models','routing'] as const).map(t => (
<button key={t} onClick={() => setTab(t)} <button key={t} onClick={() => setTab(t)}
className={`px-4 py-1.5 rounded-full text-sm transition-colors ${ 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> >{t === 'providers' ? '提供商' : t === 'models' ? '模型' : '路由'}</button>
))} ))}
@@ -57,12 +57,12 @@ export default function ModelsPage() {
{tab === 'providers' && ( {tab === 'providers' && (
<div className="space-y-2"> <div className="space-y-2">
{providers.map(p => ( {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> <div>
<p className="text-sm text-[#F2F2F2]">{p.name}</p> <p className="text-sm text-[var(--mx-text)]">{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-xs text-[var(--mx-text-dim)]">{p.base_url} · timeout {p.timeout_sec}s · retries {p.max_retries}</p>
</div> </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> </div>
))} ))}
<AddProviderForm onSave={(name, body) => save('providers', name, body)} /> <AddProviderForm onSave={(name, body) => save('providers', name, body)} />
@@ -72,13 +72,13 @@ export default function ModelsPage() {
{tab === 'models' && ( {tab === 'models' && (
<div className="space-y-2"> <div className="space-y-2">
{models.map(m => ( {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> <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-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-white/30">{m.id} · {m.provider} · {m.description}</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-[#2B3B54]/50 text-[#4788FF]">{t}</span>)}</div> <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> </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> </div>
))} ))}
<AddModelForm providers={providers} onSave={(name, body) => save('models', name, body)} /> <AddModelForm providers={providers} onSave={(name, body) => save('models', name, body)} />
@@ -88,12 +88,12 @@ export default function ModelsPage() {
{tab === 'routing' && ( {tab === 'routing' && (
<div className="space-y-2"> <div className="space-y-2">
{routing.map(r => ( {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> <div>
<p className="text-sm text-[#F2F2F2]">{r.purpose}</p> <p className="text-sm text-[var(--mx-text)]">{r.purpose}</p>
<p className="text-xs text-white/30">fallback: {r.fallback_chain?.join(' → ') || 'none'} {r.required ? '(required)' : ''}</p> <p className="text-xs text-[var(--mx-text-dim)]">fallback: {r.fallback_chain?.join(' → ') || 'none'} {r.required ? '(required)' : ''}</p>
</div> </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> </div>
))} ))}
<AddRoutingForm onSave={(name, body) => save('routing', name, body)} /> <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('') } } const save = () => { if(name&&url) { onSave(name, {base_url:url, timeout_sec:120, max_retries:3}); setName(''); setUrl('') } }
return ( return (
<div className="flex gap-2"> <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={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-[#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-[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-[#277AF7] text-white text-sm rounded-full font-medium">添加</button> <button onClick={save} className="px-4 py-2 bg-[var(--mx-primary)] text-white text-sm rounded-full font-medium">添加</button>
</div> </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('') } } const save = () => { if(id&&name) { onSave(id, {name, provider:prov, description:desc, tags:[], params:{}, enabled:true}); setId(''); setName('') } }
return ( return (
<div className="flex flex-wrap gap-2"> <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={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-[#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-[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-[#000] border border-[#404040] rounded-[14px] px-3 py-2 text-sm text-[#F2F2F2] 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>)} {providers.map(p => <option key={p.name}>{p.name}</option>)}
</select> </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" /> <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-[#277AF7] text-white text-sm rounded-full font-medium">添加</button> <button onClick={save} className="px-4 py-2 bg-[var(--mx-primary)] text-white text-sm rounded-full font-medium">添加</button>
</div> </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('') } } const save = () => { if(purpose) { onSave(purpose, {fallback_chain: chain.split(',').map(s=>s.trim()).filter(Boolean), required:true}); setPurpose(''); setChain('') } }
return ( return (
<div className="flex gap-2"> <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={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-[#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-[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-[#277AF7] text-white text-sm rounded-full font-medium">添加</button> <button onClick={save} className="px-4 py-2 bg-[var(--mx-primary)] text-white text-sm rounded-full font-medium">添加</button>
</div> </div>
) )
} }
@@ -37,21 +37,21 @@ export default function PerformancePage() {
{/* Memory chart */} {/* Memory chart */}
<div> <div>
<h3 className="text-sm font-medium text-white/60 mb-3">内存使用趋势 (最近 {history.length} 次采样)</h3> <h3 className="text-sm font-medium text-[var(--mx-text-secondary)] mb-3">内存使用趋势 (最近 {history.length} 次采样)</h3>
<div className="bg-[#000] border border-[#404040] rounded-[18px] p-4"> <div className="bg-[var(--mx-surface)] border border-[var(--mx-outline)] rounded-[18px] p-4">
<div className="flex items-end gap-1 h-32"> <div className="flex items-end gap-1 h-32">
{history.slice(-60).map((h: any, i: number) => { {history.slice(-60).map((h: any, i: number) => {
const pct = maxAlloc > 0 ? ((h.alloc_mb || 0) / maxAlloc) * 100 : 0 const pct = maxAlloc > 0 ? ((h.alloc_mb || 0) / maxAlloc) * 100 : 0
return ( return (
<div key={i} <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)}%` }} style={{ height: `${Math.max(2, pct)}%` }}
title={`${(h.alloc_mb||0).toFixed(1)}MB`} title={`${(h.alloc_mb||0).toFixed(1)}MB`}
/> />
) )
})} })}
</div> </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[0]?.timestamp?.slice(11, 19) || '--'}</span>
<span>{history[history.length-1]?.timestamp?.slice(11, 19) || '--'}</span> <span>{history[history.length-1]?.timestamp?.slice(11, 19) || '--'}</span>
</div> </div>
@@ -60,8 +60,8 @@ export default function PerformancePage() {
{/* GC chart */} {/* GC chart */}
<div> <div>
<h3 className="text-sm font-medium text-white/60 mb-3">GC 次数趋势</h3> <h3 className="text-sm font-medium text-[var(--mx-text-secondary)] mb-3">GC 次数趋势</h3>
<div className="bg-[#000] border border-[#404040] rounded-[18px] p-4"> <div className="bg-[var(--mx-surface)] border border-[var(--mx-outline)] rounded-[18px] p-4">
<div className="flex items-end gap-1 h-24"> <div className="flex items-end gap-1 h-24">
{history.slice(-60).map((h: any, i: number) => { {history.slice(-60).map((h: any, i: number) => {
const maxGC = Math.max(1, ...history.map((x: any) => x.num_gc || 0)) 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 }) { function Stat({ label, value, sub }: { label:string; value:string|number; sub:string }) {
return ( return (
<div className="bg-[#000] border border-[#404040] rounded-[18px] px-4 py-3"> <div className="bg-[var(--mx-surface)] border border-[var(--mx-outline)] rounded-[18px] px-4 py-3">
<p className="text-xs text-white/40 mb-1">{label}</p> <p className="text-xs text-[var(--mx-text-dim)] mb-1">{label}</p>
<p className="text-lg font-bold text-[#F2F2F2]">{value}</p> <p className="text-lg font-bold text-[var(--mx-text)]">{value}</p>
<p className="text-xs text-white/20 mt-0.5">{sub}</p> <p className="text-xs text-[var(--mx-text-disabled)] mt-0.5">{sub}</p>
</div> </div>
) )
} }
+5 -5
View File
@@ -19,17 +19,17 @@ export default function PluginsPage() {
return ( return (
<div className="p-6 space-y-4"> <div className="p-6 space-y-4">
<h2 className="text-lg font-semibold">插件管理</h2> <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"> <div className="grid grid-cols-2 gap-2">
{builtInPlugins.map(p => ( {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> <div>
<p className="text-sm text-[#F2F2F2]">{p.name}</p> <p className="text-sm text-[var(--mx-text)]">{p.name}</p>
<p className="text-xs text-white/30">{p.desc}</p> <p className="text-xs text-[var(--mx-text-dim)]">{p.desc}</p>
</div> </div>
<span className={`text-xs px-2 py-0.5 rounded-full ${ <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> }`}>{p.type}</span>
</div> </div>
))} ))}
@@ -20,21 +20,21 @@ export default function ServicesPage() {
return ( return (
<div className="p-6 space-y-4"> <div className="p-6 space-y-4">
<h2 className="text-lg font-semibold">服务管理</h2> <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"> <div className="space-y-2">
{builtInServices.map(svc => { {builtInServices.map(svc => {
const alive = services[svc.id] ?? false const alive = services[svc.id] ?? false
return ( 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"> <div className="flex items-center gap-3">
<span className={`w-2.5 h-2.5 rounded-full ${alive ? 'bg-[#4788FF]' : 'bg-[#404040]'}`} /> <span className={`w-2.5 h-2.5 rounded-full ${alive ? 'bg-[#4788FF]' : 'bg-[#404040]'}`} />
<div> <div>
<p className="text-sm text-[#F2F2F2]">{svc.name}</p> <p className="text-sm text-[var(--mx-text)]">{svc.name}</p>
<p className="text-xs text-white/30">{svc.desc} · 端口 {svc.port}</p> <p className="text-xs text-[var(--mx-text-dim)]">{svc.desc} · 端口 {svc.port}</p>
</div> </div>
</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 ? '运行中' : '未启动'} {alive ? '运行中' : '未启动'}
</span> </span>
</div> </div>
+8 -8
View File
@@ -41,10 +41,10 @@ export default function SystemPage() {
{/* Plugins list */} {/* Plugins list */}
<div> <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"> <div className="flex flex-wrap gap-2">
{plugins.map((p, i) => ( {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} {p.name}
</span> </span>
))} ))}
@@ -53,13 +53,13 @@ export default function SystemPage() {
{/* Sessions */} {/* Sessions */}
<div> <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 ? ( {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"> <div className="space-y-1">
{sessions.map((s: any, i) => ( {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 || '-'} {s.session_id || s.id || '-'}
</div> </div>
))} ))}
@@ -72,9 +72,9 @@ export default function SystemPage() {
function StatRow({ label, value }: { label:string; value:string|number }) { function StatRow({ label, value }: { label:string; value:string|number }) {
return ( return (
<div className="bg-[#000] border border-[#404040] rounded-[18px] px-4 py-3"> <div className="bg-[var(--mx-surface)] border border-[var(--mx-outline)] rounded-[18px] px-4 py-3">
<p className="text-xs text-white/40 mb-1">{label}</p> <p className="text-xs text-[var(--mx-text-dim)] mb-1">{label}</p>
<p className="text-lg font-bold text-[#F2F2F2]">{value}</p> <p className="text-lg font-bold text-[var(--mx-text)]">{value}</p>
</div> </div>
) )
} }
+8 -8
View File
@@ -28,16 +28,16 @@ export default function TracePage() {
return ( return (
<div className="p-6 space-y-4"> <div className="p-6 space-y-4">
<h2 className="text-lg font-semibold">全链路追踪</h2> <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 ? ( {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"> <div className="space-y-1.5">
{events.map((ev, i) => ( {events.map((ev, i) => (
<div key={ev.id || i} <div key={ev.id || i}
className={`bg-[#000] border rounded-[14px] px-4 py-3 flex items-center gap-3 ${ className={`bg-[var(--mx-surface)] border rounded-[14px] px-4 py-3 flex items-center gap-3 ${
ev.status === 'error' ? 'border-[#F12522]/30' : 'border-[#404040]' ev.status === 'error' ? 'border-[var(--mx-error)]/30' : 'border-[var(--mx-outline)]'
}`} }`}
> >
{/* Hop icon */} {/* Hop icon */}
@@ -46,19 +46,19 @@ export default function TracePage() {
{/* Content */} {/* Content */}
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2"> <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 && ( {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> </div>
{ev.detail && ( {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> </div>
{/* Status */} {/* Status */}
<div className="flex items-center gap-3 shrink-0"> <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) : ''} {ev.timestamp ? ev.timestamp.slice(11, 19) : ''}
</span> </span>
<span className={`w-2 h-2 rounded-full ${ <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 })
}
})
}