feat: 添加亮色/暗色主题切换功能

- 在 index.css 中新增 html.light 亮色主题 CSS 变量和过渡动画
- 在 App.tsx Header 添加 Sun/Moon 主题切换按钮
- 主题偏好通过 localStorage 持久化,默认暗色主题
- 将所有组件中的硬编码 Tailwind 颜色值转换为 CSS 变量引用
- 状态色(emerald/amber/red/violet)保持不变,两种主题均适用
This commit is contained in:
ooodc 2026-06-04 21:52:45 +08:00
parent d1d4998a26
commit bd13cffe14
9 changed files with 373 additions and 252 deletions

View File

@ -1,5 +1,5 @@
import { useCallback, useEffect, useMemo, useRef } from 'react'
import { Zap, Cpu, MessageSquare, ArrowLeft, Bot, Clock } from 'lucide-react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Zap, Cpu, MessageSquare, ArrowLeft, Bot, Clock, Sun, Moon } from 'lucide-react'
import { ChatContainer } from './components/Chat/ChatContainer'
import { TopicList } from './components/Sidebar/TopicList'
import { SessionInfo } from './components/Sidebar/SessionInfo'
@ -59,6 +59,32 @@ function App() {
onMessage: handleServerMessage,
})
// ---- 主题状态 ----
const [theme, setTheme] = useState<'dark' | 'light'>(() => {
const saved = localStorage.getItem('picobot-theme')
return saved === 'light' ? 'light' : 'dark'
})
useEffect(() => {
const root = document.documentElement
if (theme === 'light') {
root.classList.add('light')
} else {
root.classList.remove('light')
}
localStorage.setItem('picobot-theme', theme)
// 切换时启用平滑过渡
root.classList.add('theme-transitioning')
const timer = setTimeout(() => {
root.classList.remove('theme-transitioning')
}, 350)
return () => clearTimeout(timer)
}, [theme])
// ---- WebSocket 初始化 ----
// 连接建立后自动加载 Session
useEffect(() => {
if (isConnected && status === 'connected') {
@ -261,23 +287,33 @@ function App() {
const toolMessages = messages
return (
<div className="flex h-screen flex-col bg-[#0a0a0f] text-white overflow-hidden">
<div className="flex h-screen flex-col bg-[var(--bg-primary)] text-[var(--text-primary)] overflow-hidden">
{/* Header */}
<header className="flex items-center justify-between border-b border-white/8 bg-[#12121a]/80 backdrop-blur-md px-6 py-4">
<header className="flex items-center justify-between border-b border-[var(--border-color)] bg-[var(--bg-secondary)]/80 backdrop-blur-md px-6 py-4">
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<Zap className="h-6 w-6 text-[#00f0ff]" />
<Zap className="h-6 w-6 text-[var(--accent-cyan)]" />
<h1 className="text-xl font-bold tracking-tight">
<span className="text-white">Pico</span>
<span className="text-[#00f0ff] glow-text">Bot</span>
<span className="text-[var(--text-primary)]">Pico</span>
<span className="text-[var(--accent-cyan)] glow-text">Bot</span>
</h1>
</div>
<div className="h-4 w-px bg-white/20" />
<div className="h-4 w-px bg-[var(--divider-color)]" />
<ConnectionStatus status={status} />
{/* 主题切换按钮 */}
<div className="h-4 w-px bg-[var(--divider-color)]" />
<button
onClick={() => setTheme(prev => prev === 'dark' ? 'light' : 'dark')}
className="flex h-8 w-8 items-center justify-center rounded-lg text-[var(--text-muted)] hover:text-[var(--accent-cyan)] hover:bg-[var(--overlay-hover)] transition-all"
title={theme === 'dark' ? '切换到亮色主题' : '切换到暗色主题'}
aria-label={theme === 'dark' ? 'Switch to light theme' : 'Switch to dark theme'}
>
{theme === 'dark' ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
</button>
</div>
<div className="flex items-center gap-4 text-sm text-zinc-400">
<div className="flex items-center gap-4 text-sm text-[var(--text-secondary)]">
<div className="flex items-center gap-2">
<Cpu className="h-4 w-4 text-[#00f0ff]" />
<Cpu className="h-4 w-4 text-[var(--accent-cyan)]" />
<span>AI Ready</span>
</div>
{session && (
@ -294,21 +330,21 @@ function App() {
{/* Main Content */}
<div className="flex flex-1 overflow-hidden">
{/* Left Sidebar */}
<div className={`w-72 shrink-0 border-r border-white/8 bg-[#12121a]/50 flex flex-col ${subAgentView || schedulerView ? 'opacity-50 pointer-events-none' : ''}`}>
<div className={`w-72 shrink-0 border-r border-[var(--border-color)] bg-[var(--bg-secondary)]/50 flex flex-col ${subAgentView || schedulerView ? 'opacity-50 pointer-events-none' : ''}`}>
<SessionInfo
session={session}
connectionId={connectionId}
/>
<div className="border-b border-white/8" />
<div className="border-b border-[var(--border-color)]" />
{/* Tab 栏 */}
<div className="flex border-b border-white/8">
<div className="flex border-b border-[var(--border-color)]">
<button
onClick={() => setSidebarTab('topics')}
className={`flex-1 py-2.5 text-sm font-medium text-center transition-colors ${
sidebarTab === 'topics'
? 'text-[#00f0ff] border-b-2 border-[#00f0ff]'
: 'text-zinc-500 hover:text-zinc-300'
? 'text-[var(--accent-cyan)] border-b-2 border-[var(--accent-cyan)]'
: 'text-[var(--text-muted)] hover:text-[var(--text-secondary)]'
}`}
>
@ -317,8 +353,8 @@ function App() {
onClick={() => setSidebarTab('scheduler')}
className={`flex-1 py-2.5 text-sm font-medium text-center transition-colors ${
sidebarTab === 'scheduler'
? 'text-[#00f0ff] border-b-2 border-[#00f0ff]'
: 'text-zinc-500 hover:text-zinc-300'
? 'text-[var(--accent-cyan)] border-b-2 border-[var(--accent-cyan)]'
: 'text-[var(--text-muted)] hover:text-[var(--text-secondary)]'
}`}
>
@ -348,60 +384,60 @@ function App() {
</div>
{/* Center - Chat */}
<div className="flex-1 min-w-0 bg-[#0a0a0f] flex flex-col">
<div className="flex-1 min-w-0 bg-[var(--bg-primary)] flex flex-col">
{/* Scheduler job view back bar */}
{schedulerView && (
<div className="shrink-0 border-b border-white/8 bg-[#12121a]/80 px-4 py-2 flex items-center gap-4">
<div className="shrink-0 border-b border-[var(--border-color)] bg-[var(--bg-secondary)]/80 px-4 py-2 flex items-center gap-4">
<button
onClick={handleExitSchedulerJobView}
className="flex items-center gap-1.5 text-sm text-[#00f0ff] hover:text-[#00f0ff]/80 transition-colors"
className="flex items-center gap-1.5 text-sm text-[var(--accent-cyan)] hover:text-[var(--accent-cyan)]/80 transition-colors"
>
<ArrowLeft className="h-4 w-4" />
<span></span>
</button>
<div className="h-4 w-px bg-white/20" />
<div className="flex items-center gap-1.5 text-sm text-zinc-300">
<div className="h-4 w-px bg-[var(--divider-color)]" />
<div className="flex items-center gap-1.5 text-sm text-[var(--text-secondary)]">
<Clock className="h-4 w-4 text-amber-400" />
<span className="text-zinc-500">:</span>
<span className="text-white font-medium font-mono text-xs truncate max-w-[250px]">{schedulerView.description}</span>
<span className="text-[var(--text-muted)]">:</span>
<span className="text-[var(--text-primary)] font-medium font-mono text-xs truncate max-w-[250px]">{schedulerView.description}</span>
</div>
<div className="h-4 w-px bg-white/20" />
<div className="h-4 w-px bg-[var(--divider-color)]" />
<div className="flex items-center gap-1.5 text-sm">
<span className="text-zinc-500">:</span>
<span className="text-zinc-300">{schedulerView.channel}</span>
<span className="text-[var(--text-muted)]">:</span>
<span className="text-[var(--text-secondary)]">{schedulerView.channel}</span>
</div>
</div>
)}
{/* Sub-agent back bar */}
{subAgentView && (
<div className="shrink-0 border-b border-white/8 bg-[#12121a]/80 px-4 py-2 flex items-center gap-4">
<div className="shrink-0 border-b border-[var(--border-color)] bg-[var(--bg-secondary)]/80 px-4 py-2 flex items-center gap-4">
<button
onClick={handleExitSubAgentView}
className="flex items-center gap-1.5 text-sm text-[#00f0ff] hover:text-[#00f0ff]/80 transition-colors"
className="flex items-center gap-1.5 text-sm text-[var(--accent-cyan)] hover:text-[var(--accent-cyan)]/80 transition-colors"
>
<ArrowLeft className="h-4 w-4" />
<span></span>
</button>
<div className="h-4 w-px bg-white/20" />
<div className="flex items-center gap-1.5 text-sm text-zinc-300">
<div className="h-4 w-px bg-[var(--divider-color)]" />
<div className="flex items-center gap-1.5 text-sm text-[var(--text-secondary)]">
<Bot className="h-4 w-4 text-violet-400" />
<span className="text-zinc-500">:</span>
<span className="text-white font-medium">{subAgentView.description}</span>
<span className="text-[var(--text-muted)]">:</span>
<span className="text-[var(--text-primary)] font-medium">{subAgentView.description}</span>
</div>
<div className="h-4 w-px bg-white/20" />
<div className="h-4 w-px bg-[var(--divider-color)]" />
<div className="flex items-center gap-1.5 text-sm">
<span className="text-zinc-500">:</span>
<span className="text-zinc-300">{subAgentView.subagentType || '...'}</span>
<span className="text-[var(--text-muted)]">:</span>
<span className="text-[var(--text-secondary)]">{subAgentView.subagentType || '...'}</span>
</div>
<div className="h-4 w-px bg-white/20" />
<div className="h-4 w-px bg-[var(--divider-color)]" />
<div className="flex items-center gap-1.5 text-sm">
<span className="text-zinc-500">:</span>
<span className="text-[var(--text-muted)]">:</span>
<span className={`font-medium ${
subAgentView.status === 'completed' ? 'text-emerald-400' :
subAgentView.status === 'failed' ? 'text-red-400' :
subAgentView.status === 'timeout' ? 'text-amber-400' :
subAgentView.status === 'running' ? 'text-amber-400' :
'text-zinc-400'
'text-[var(--text-secondary)]'
}`}>
{subAgentView.status === 'completed' ? '已完成' :
subAgentView.status === 'failed' ? '失败' :
@ -431,7 +467,7 @@ function App() {
</div>
{/* Right Sidebar - Tool Panel */}
<div className="w-80 shrink-0 border-l border-white/8 bg-[#12121a]/50">
<div className="w-80 shrink-0 border-l border-[var(--border-color)] bg-[var(--bg-secondary)]/50">
<ToolPanel messages={toolMessages} />
</div>
</div>

View File

@ -125,20 +125,20 @@ function AttachmentCard({ attachment }: { attachment: Attachment }) {
return (
<div
onClick={canDownload ? handleDownload : undefined}
className={`flex items-center gap-2 rounded-lg bg-white/5 border border-white/10 px-3 py-2 text-xs transition-colors group ${
canDownload ? 'hover:bg-white/10 hover:border-[#00f0ff]/30 cursor-pointer' : ''
className={`flex items-center gap-2 rounded-lg bg-[var(--overlay-hover)] border border-[var(--border-color)] px-3 py-2 text-xs transition-colors group ${
canDownload ? 'hover:bg-[var(--overlay-subtle)] hover:border-[var(--accent-cyan)]/30 cursor-pointer' : ''
}`}
title={canDownload ? `下载 ${fileName}` : attachment.path}
>
<span className={`text-zinc-400 transition-colors ${canDownload ? 'group-hover:text-[#00f0ff]' : ''}`}>
<span className={`text-[var(--text-secondary)] transition-colors ${canDownload ? 'group-hover:text-[var(--accent-cyan)]' : ''}`}>
{getAttachmentIcon(attachment.media_type)}
</span>
<span className={`text-zinc-300 truncate max-w-[200px] transition-colors ${canDownload ? 'group-hover:text-white' : ''}`} title={attachment.path}>
<span className={`text-[var(--text-secondary)] truncate max-w-[200px] transition-colors ${canDownload ? 'group-hover:text-[var(--text-primary)]' : ''}`} title={attachment.path}>
{fileName}
</span>
<span className="text-zinc-600 ml-auto shrink-0">{attachment.media_type}</span>
<span className="text-[var(--text-muted)] ml-auto shrink-0">{attachment.media_type}</span>
{canDownload && (
<Download className="h-3 w-3 text-zinc-600 group-hover:text-[#00f0ff] transition-colors" />
<Download className="h-3 w-3 text-[var(--text-muted)] group-hover:text-[var(--accent-cyan)] transition-colors" />
)}
</div>
)
@ -162,13 +162,13 @@ function CopyButton({ text, className = '' }: { text: string; className?: string
return (
<button
onClick={handleCopy}
className={`opacity-0 group-hover:opacity-100 transition-opacity p-0.5 rounded hover:bg-white/10 flex-shrink-0 ${className}`}
className={`opacity-0 group-hover:opacity-100 transition-opacity p-0.5 rounded hover:bg-[var(--overlay-subtle)] flex-shrink-0 ${className}`}
title="复制"
>
{copied ? (
<Check className="h-3 w-3 text-emerald-400" />
) : (
<Copy className="h-3 w-3 text-zinc-500" />
<Copy className="h-3 w-3 text-[var(--text-muted)]" />
)}
</button>
)
@ -281,7 +281,7 @@ export function MessageBubble({ message, onNavigateToSubAgent }: MessageBubblePr
</div>
<div className="max-w-[80%] min-w-0">
<div className="flex items-center gap-2 mb-1">
<span className="text-xs font-medium text-zinc-400">{message.toolName || 'Tool'}</span>
<span className="text-xs font-medium text-[var(--text-secondary)]">{message.toolName || 'Tool'}</span>
{isTaskTool && (
<span className="text-xs px-1.5 py-0.5 rounded bg-violet-500/10 text-violet-400">
·{subagentType}
@ -292,11 +292,11 @@ export function MessageBubble({ message, onNavigateToSubAgent }: MessageBubblePr
</span>
)}
<span className="text-xs text-zinc-600">{formatTime(message.timestamp)}</span>
<span className="text-xs text-[var(--text-muted)]">{formatTime(message.timestamp)}</span>
</div>
<div
onClick={() => setToolExpanded(!toolExpanded)}
className={`cursor-pointer rounded-xl border bg-[#1a1a25]/60 w-full transition-all duration-500 hover:bg-[#1a1a25]/80 group ${
className={`cursor-pointer rounded-xl border bg-[var(--bg-tertiary)]/60 w-full transition-all duration-500 hover:bg-[var(--bg-tertiary)]/80 group ${
taskResult ? taskStatusConfig[taskResult.status].borderColor : statusConfig.fullBorder
}`}
>
@ -305,7 +305,7 @@ export function MessageBubble({ message, onNavigateToSubAgent }: MessageBubblePr
<span className={`inline-block h-2 w-2 rounded-full flex-shrink-0 transition-colors duration-500 ${
taskResult ? taskStatusConfig[taskResult.status].dot : statusConfig.dot
}`} />
<span className="text-sm font-medium text-zinc-300 truncate">
<span className="text-sm font-medium text-[var(--text-secondary)] truncate">
{isTaskTool ? (taskDescription || '子智能体任务') : (message.toolName || 'Tool')}
</span>
<span className={`flex-shrink-0 transition-all duration-300 ${
@ -318,16 +318,16 @@ export function MessageBubble({ message, onNavigateToSubAgent }: MessageBubblePr
)}
</span>
{status === 'result' && message.durationMs != null && (
<span className="text-xs text-zinc-600 flex-shrink-0 tabular-nums ml-1">
<span className="text-xs text-[var(--text-muted)] flex-shrink-0 tabular-nums ml-1">
{formatDuration(message.durationMs)}
</span>
)}
{hasResult && <CopyButton text={taskResult ? taskResult.output : displayContent} />}
<span className="ml-auto flex-shrink-0">
{toolExpanded ? (
<ChevronDown className="h-3.5 w-3.5 text-zinc-500" />
<ChevronDown className="h-3.5 w-3.5 text-[var(--text-muted)]" />
) : (
<ChevronRight className="h-3.5 w-3.5 text-zinc-500" />
<ChevronRight className="h-3.5 w-3.5 text-[var(--text-muted)]" />
)}
</span>
</div>
@ -336,7 +336,7 @@ export function MessageBubble({ message, onNavigateToSubAgent }: MessageBubblePr
{!toolExpanded && (
<>
{hasArgs && argsPreview && !taskResult && (
<div className="px-3 pb-1 text-xs text-zinc-500 font-mono line-clamp-3"
<div className="px-3 pb-1 text-xs text-[var(--text-muted)] font-mono line-clamp-3"
style={{
display: '-webkit-box',
WebkitLineClamp: 3,
@ -347,7 +347,7 @@ export function MessageBubble({ message, onNavigateToSubAgent }: MessageBubblePr
</div>
)}
{taskResult && taskResult.summary && (
<div className="px-3 pb-1 text-xs text-zinc-400 line-clamp-2">
<div className="px-3 pb-1 text-xs text-[var(--text-secondary)] line-clamp-2">
{taskResult.summary}
</div>
)}
@ -358,7 +358,7 @@ export function MessageBubble({ message, onNavigateToSubAgent }: MessageBubblePr
e.stopPropagation()
onNavigateToSubAgent?.(taskResult.task_id, taskDescription || '子智能体任务')
}}
className="text-xs text-[#00f0ff] hover:text-[#00f0ff]/80 hover:underline transition-colors flex items-center gap-1"
className="text-xs text-[var(--accent-cyan)] hover:text-[var(--accent-cyan)]/80 hover:underline transition-colors flex items-center gap-1"
>
<span></span>
<span></span>
@ -372,7 +372,7 @@ export function MessageBubble({ message, onNavigateToSubAgent }: MessageBubblePr
e.stopPropagation()
onNavigateToSubAgent?.(message.subagentTaskId!, taskDescription || '子智能体任务')
}}
className="text-xs text-[#00f0ff] hover:text-[#00f0ff]/80 hover:underline transition-colors flex items-center gap-1"
className="text-xs text-[var(--accent-cyan)] hover:text-[var(--accent-cyan)]/80 hover:underline transition-colors flex items-center gap-1"
>
<span></span>
<span></span>
@ -380,7 +380,7 @@ export function MessageBubble({ message, onNavigateToSubAgent }: MessageBubblePr
</div>
)}
{isTaskTool && !taskResult && !message.subagentTaskId && (
<div className="px-3 pb-2 text-xs text-zinc-500">
<div className="px-3 pb-2 text-xs text-[var(--text-muted)]">
...
</div>
)}
@ -389,13 +389,13 @@ export function MessageBubble({ message, onNavigateToSubAgent }: MessageBubblePr
{/* Expanded */}
{toolExpanded && (
<div className="border-t border-white/8 px-3 py-2 space-y-2">
<div className="border-t border-[var(--border-color)] px-3 py-2 space-y-2">
{taskResult ? (
<>
{taskPrompt && (
<div>
<div className="text-xs font-medium text-zinc-500 mb-1"></div>
<pre className="text-xs text-zinc-400 font-mono whitespace-pre-wrap bg-black/20 rounded-lg p-2 overflow-x-auto max-h-32 overflow-y-auto">
<div className="text-xs font-medium text-[var(--text-muted)] mb-1"></div>
<pre className="text-xs text-[var(--text-secondary)] font-mono whitespace-pre-wrap bg-[var(--overlay-dim)] rounded-lg p-2 overflow-x-auto max-h-32 overflow-y-auto">
{taskPrompt}
</pre>
</div>
@ -406,19 +406,19 @@ export function MessageBubble({ message, onNavigateToSubAgent }: MessageBubblePr
taskResult.status === 'failed' ? 'bg-red-500/10 border border-red-500/30' :
'bg-amber-500/10 border border-amber-500/30'
}`}>
<div className="text-xs font-medium text-zinc-500 mb-0.5"></div>
<div className="text-sm text-zinc-300">{taskResult.summary}</div>
<div className="text-xs font-medium text-[var(--text-muted)] mb-0.5"></div>
<div className="text-sm text-[var(--text-secondary)]">{taskResult.summary}</div>
</div>
)}
<div>
<div className="text-xs font-medium text-zinc-500 mb-1"></div>
<div className="markdown-content text-sm leading-relaxed bg-black/20 rounded-lg p-3 max-h-96 overflow-y-auto">
<div className="text-xs font-medium text-[var(--text-muted)] mb-1"></div>
<div className="markdown-content text-sm leading-relaxed bg-[var(--overlay-dim)] rounded-lg p-3 max-h-96 overflow-y-auto">
<ReactMarkdown remarkPlugins={[remarkGfm]}>
{taskResult.output}
</ReactMarkdown>
</div>
</div>
<div className="text-xs text-zinc-600 font-mono select-all">
<div className="text-xs text-[var(--text-muted)] font-mono select-all">
task_id: {taskResult.task_id}
</div>
<button
@ -426,7 +426,7 @@ export function MessageBubble({ message, onNavigateToSubAgent }: MessageBubblePr
e.stopPropagation()
onNavigateToSubAgent?.(taskResult.task_id, taskDescription || '子智能体任务')
}}
className="text-xs text-[#00f0ff] hover:text-[#00f0ff]/80 hover:underline transition-colors flex items-center gap-1"
className="text-xs text-[var(--accent-cyan)] hover:text-[var(--accent-cyan)]/80 hover:underline transition-colors flex items-center gap-1"
>
<span></span>
<span></span>
@ -436,16 +436,16 @@ export function MessageBubble({ message, onNavigateToSubAgent }: MessageBubblePr
<>
{hasArgs && (
<div>
<div className="text-xs font-medium text-zinc-500 mb-1"></div>
<pre className="text-xs text-zinc-400 font-mono whitespace-pre-wrap bg-black/20 rounded-lg p-2 overflow-x-auto">
<div className="text-xs font-medium text-[var(--text-muted)] mb-1"></div>
<pre className="text-xs text-[var(--text-secondary)] font-mono whitespace-pre-wrap bg-[var(--overlay-dim)] rounded-lg p-2 overflow-x-auto">
{JSON.stringify(message.arguments, null, 2)}
</pre>
</div>
)}
{hasResult && (
<div>
<div className="text-xs font-medium text-zinc-500 mb-1"></div>
<pre className="text-xs text-zinc-400 font-mono whitespace-pre-wrap bg-black/20 rounded-lg p-2 overflow-x-auto max-h-48 overflow-y-auto">
<div className="text-xs font-medium text-[var(--text-muted)] mb-1"></div>
<pre className="text-xs text-[var(--text-secondary)] font-mono whitespace-pre-wrap bg-[var(--overlay-dim)] rounded-lg p-2 overflow-x-auto max-h-48 overflow-y-auto">
{formatJSON(displayContent)}
</pre>
</div>
@ -456,7 +456,7 @@ export function MessageBubble({ message, onNavigateToSubAgent }: MessageBubblePr
e.stopPropagation()
onNavigateToSubAgent?.(message.subagentTaskId!, taskDescription || '子智能体任务')
}}
className="text-xs text-[#00f0ff] hover:text-[#00f0ff]/80 hover:underline transition-colors flex items-center gap-1"
className="text-xs text-[var(--accent-cyan)] hover:text-[var(--accent-cyan)]/80 hover:underline transition-colors flex items-center gap-1"
>
<span></span>
<span></span>
@ -485,26 +485,26 @@ export function MessageBubble({ message, onNavigateToSubAgent }: MessageBubblePr
const getContainerStyles = () => {
if (isUser) {
return 'bg-gradient-to-br from-[#00f0ff]/20 to-[#3b82f6]/20 border-[#00f0ff]/30 text-white'
return 'bg-gradient-to-br from-[var(--accent-cyan)]/20 to-[var(--accent-blue)]/20 border-[var(--accent-cyan)]/30 text-[var(--text-primary)]'
}
if (isTool) {
if (message.type === 'tool_call') return 'bg-amber-500/10 border-amber-500/30 text-amber-100'
if (message.type === 'tool_result') return 'bg-emerald-500/10 border-emerald-500/30 text-emerald-100'
if (message.type === 'tool_pending') return 'bg-orange-500/10 border-orange-500/30 text-orange-100'
return 'bg-zinc-800/50 border-zinc-700 text-zinc-300'
return 'bg-[var(--bg-hover)] border-[var(--border-color)] text-[var(--text-secondary)]'
}
return 'bg-[#1a1a25] border-white/10 text-white'
return 'bg-[var(--bg-tertiary)] border-[var(--border-color)] text-[var(--text-primary)]'
}
const getAvatarStyles = () => {
if (isUser) return 'bg-gradient-to-br from-[#00f0ff] to-[#3b82f6]'
if (isUser) return 'bg-gradient-to-br from-[var(--accent-cyan)] to-[var(--accent-blue)]'
if (isTool) {
if (message.type === 'tool_call') return 'bg-amber-500'
if (message.type === 'tool_result') return 'bg-emerald-500'
if (message.type === 'tool_pending') return 'bg-orange-500'
return 'bg-zinc-700'
}
return 'bg-gradient-to-br from-[#8b5cf6] to-[#ec4899]'
return 'bg-gradient-to-br from-[var(--accent-purple)] to-[#ec4899]'
}
return (
@ -516,10 +516,10 @@ export function MessageBubble({ message, onNavigateToSubAgent }: MessageBubblePr
</div>
<div className={`max-w-[80%] ${isUser ? 'text-right' : 'text-left'}`}>
<div className="flex items-center gap-2 mb-1">
<span className={`text-xs font-medium ${isUser ? 'text-[#00f0ff]' : 'text-zinc-400'}`}>
<span className={`text-xs font-medium ${isUser ? 'text-[var(--accent-cyan)]' : 'text-[var(--text-secondary)]'}`}>
{isUser ? 'You' : isTool ? message.toolName || 'Tool' : 'Assistant'}
</span>
<span className="text-xs text-zinc-600">{formatTime(message.timestamp)}</span>
<span className="text-xs text-[var(--text-muted)]">{formatTime(message.timestamp)}</span>
<CopyButton text={message.content} />
</div>
<div
@ -546,7 +546,7 @@ export function MessageBubble({ message, onNavigateToSubAgent }: MessageBubblePr
if (isInline) {
return (
<code
className="bg-black/30 px-1.5 py-0.5 rounded text-[#00f0ff] font-mono text-xs"
className="bg-[var(--overlay-code)] px-1.5 py-0.5 rounded text-[var(--accent-cyan)] font-mono text-xs"
{...props}
>
{children}
@ -554,7 +554,7 @@ export function MessageBubble({ message, onNavigateToSubAgent }: MessageBubblePr
)
}
return (
<pre className="bg-black/40 rounded-lg p-3 overflow-x-auto my-2">
<pre className="bg-[var(--overlay-dim-heavy)] rounded-lg p-3 overflow-x-auto my-2">
<code className={`${className} font-mono text-xs`} {...props}>
{children}
</code>
@ -563,13 +563,13 @@ export function MessageBubble({ message, onNavigateToSubAgent }: MessageBubblePr
},
// 标题样式
h1: ({ children }) => (
<h1 className="text-xl font-bold text-white mb-2 mt-4">{children}</h1>
<h1 className="text-xl font-bold text-[var(--text-primary)] mb-2 mt-4">{children}</h1>
),
h2: ({ children }) => (
<h2 className="text-lg font-bold text-white mb-2 mt-3">{children}</h2>
<h2 className="text-lg font-bold text-[var(--text-primary)] mb-2 mt-3">{children}</h2>
),
h3: ({ children }) => (
<h3 className="text-base font-bold text-white mb-1 mt-2">{children}</h3>
<h3 className="text-base font-bold text-[var(--text-primary)] mb-1 mt-2">{children}</h3>
),
// 段落
p: ({ children }) => <p className="mb-2 last:mb-0">{children}</p>,
@ -582,7 +582,7 @@ export function MessageBubble({ message, onNavigateToSubAgent }: MessageBubblePr
href={href}
target="_blank"
rel="noopener noreferrer"
className="text-[#00f0ff] hover:underline"
className="text-[var(--accent-cyan)] hover:underline"
>
{children}
</a>
@ -591,24 +591,24 @@ export function MessageBubble({ message, onNavigateToSubAgent }: MessageBubblePr
table: ({ children }) => (
<table className="w-full border-collapse mb-2 text-xs">{children}</table>
),
thead: ({ children }) => <thead className="bg-white/10">{children}</thead>,
thead: ({ children }) => <thead className="bg-[var(--overlay-subtle)]">{children}</thead>,
th: ({ children }) => (
<th className="border border-white/10 px-2 py-1 text-left font-semibold">{children}</th>
<th className="border border-[var(--border-color)] px-2 py-1 text-left font-semibold">{children}</th>
),
td: ({ children }) => (
<td className="border border-white/10 px-2 py-1">{children}</td>
<td className="border border-[var(--border-color)] px-2 py-1">{children}</td>
),
// 引用块
blockquote: ({ children }) => (
<blockquote className="border-l-2 border-[#00f0ff]/50 pl-3 my-2 text-zinc-400">
<blockquote className="border-l-2 border-[var(--accent-cyan)]/50 pl-3 my-2 text-[var(--text-secondary)]">
{children}
</blockquote>
),
// 分隔线
hr: () => <hr className="border-white/10 my-3" />,
hr: () => <hr className="border-[var(--border-color)] my-3" />,
// 加粗和斜体
strong: ({ children }) => <strong className="font-bold text-white">{children}</strong>,
em: ({ children }) => <em className="italic text-zinc-300">{children}</em>,
strong: ({ children }) => <strong className="font-bold text-[var(--text-primary)]">{children}</strong>,
em: ({ children }) => <em className="italic text-[var(--text-secondary)]">{children}</em>,
}}
>
{message.content}

View File

@ -251,22 +251,22 @@ export function MessageInput({
// 只读模式:显示提示占位符
if (isReadOnly) {
return (
<div className="border-t border-white/8 bg-[#12121a]/80 backdrop-blur-md p-4">
<div className="border-t border-[var(--border-color)] bg-[var(--bg-secondary)]/80 backdrop-blur-md p-4">
<div className="max-w-5xl mx-auto">
<div className="rounded-xl border border-zinc-500/20 bg-[#1a1a25]/50 px-4 py-5 text-center">
<div className="rounded-xl border border-[var(--border-color)] bg-[var(--bg-tertiary)]/50 px-4 py-5 text-center">
<div className="flex flex-col items-center gap-2">
<div className="flex items-center gap-2 text-zinc-400">
<div className="flex items-center gap-2 text-[var(--text-secondary)]">
<Eye className="h-5 w-5" />
<span className="font-medium"></span>
</div>
<p className="text-sm text-zinc-500">
<p className="text-sm text-[var(--text-muted)]">
{channelName ? (
<>{channelName} </>
) : (
'当前通道仅支持查看历史消息'
)}
</p>
<p className="text-xs text-zinc-600">
<p className="text-xs text-[var(--text-muted)]">
WebSocket
</p>
</div>
@ -277,7 +277,7 @@ export function MessageInput({
}
return (
<div className="border-t border-white/8 bg-[#12121a]/80 backdrop-blur-md p-4">
<div className="border-t border-[var(--border-color)] bg-[var(--bg-secondary)]/80 backdrop-blur-md p-4">
<div className="max-w-5xl mx-auto">
{/* 错误提示 */}
{error && (
@ -292,7 +292,7 @@ export function MessageInput({
{attachments.map((att, index) => (
<div
key={index}
className="flex items-center gap-2 rounded-lg border border-white/10 bg-[#1a1a25] px-2 py-1.5 text-sm"
className="flex items-center gap-2 rounded-lg border border-[var(--border-color)] bg-[var(--bg-tertiary)] px-2 py-1.5 text-sm"
>
{att.preview ? (
<img
@ -301,16 +301,16 @@ export function MessageInput({
className="h-8 w-8 rounded object-cover"
/>
) : (
<div className="h-8 w-8 rounded bg-zinc-700/50 flex items-center justify-center text-zinc-400">
<div className="h-8 w-8 rounded bg-[var(--bg-hover)] flex items-center justify-center text-[var(--text-secondary)]">
{getAttachmentIcon(att.attachment.media_type)}
</div>
)}
<span className="text-zinc-300 max-w-[120px] truncate">
<span className="text-[var(--text-secondary)] max-w-[120px] truncate">
{att.attachment.file_name}
</span>
<button
onClick={() => handleRemoveAttachment(index)}
className="text-zinc-500 hover:text-red-400 transition-colors"
className="text-[var(--text-muted)] hover:text-red-400 transition-colors"
>
<X className="h-4 w-4" />
</button>
@ -321,7 +321,7 @@ export function MessageInput({
{/* 输入区域 */}
<div
className={`flex gap-3 items-center relative ${isDragging ? 'ring-2 ring-[#00f0ff]/50 rounded-xl' : ''}`}
className={`flex gap-3 items-center relative ${isDragging ? 'ring-2 ring-[var(--accent-cyan)]/50 rounded-xl' : ''}`}
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
onDragOver={handleDragOver}
@ -329,8 +329,8 @@ export function MessageInput({
>
{/* 拖拽提示 */}
{isDragging && (
<div className="absolute inset-0 rounded-xl bg-[#00f0ff]/10 border-2 border-[#00f0ff]/50 flex items-center justify-center z-10">
<div className="text-[#00f0ff] text-sm font-medium">
<div className="absolute inset-0 rounded-xl bg-[var(--accent-cyan)]/10 border-2 border-[var(--accent-cyan)]/50 flex items-center justify-center z-10">
<div className="text-[var(--accent-cyan)] text-sm font-medium">
</div>
</div>
@ -340,7 +340,7 @@ export function MessageInput({
<button
onClick={handleAttachClick}
disabled={disabled}
className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl border border-white/10 bg-[#1a1a25] text-zinc-400 hover:text-white hover:border-white/20 disabled:opacity-50 transition-all"
className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl border border-[var(--border-color)] bg-[var(--bg-tertiary)] text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:border-[var(--border-color)] disabled:opacity-50 transition-all"
>
<Paperclip className="h-5 w-5" />
</button>
@ -363,9 +363,9 @@ export function MessageInput({
placeholder={placeholder}
disabled={disabled}
rows={1}
className="w-full resize-none rounded-xl border border-white/10 bg-[#1a1a25] px-4 py-3 pr-12 text-sm text-white placeholder:text-zinc-500 focus:border-[#00f0ff]/50 focus:outline-none focus:ring-1 focus:ring-[#00f0ff]/20 disabled:opacity-50 transition-all self-center scrollbar-hide"
className="w-full resize-none rounded-xl border border-[var(--border-color)] bg-[var(--bg-tertiary)] px-4 py-3 pr-12 text-sm text-[var(--text-primary)] placeholder:text-[var(--text-muted)] focus:border-[var(--accent-cyan)]/50 focus:outline-none focus:ring-1 focus:ring-[var(--focus-ring)] disabled:opacity-50 transition-all self-center scrollbar-hide"
/>
<Sparkles className="absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-zinc-600 pointer-events-none" />
<Sparkles className="absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-[var(--text-muted)] pointer-events-none" />
</div>
{/* 发送/停止按钮 */}
@ -381,7 +381,7 @@ export function MessageInput({
<button
onClick={handleSend}
disabled={disabled || (!content.trim() && attachments.length === 0)}
className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-gradient-to-r from-[#00f0ff] to-[#3b82f6] text-white shadow-lg shadow-[#00f0ff]/20 hover:shadow-xl hover:shadow-[#00f0ff]/30 hover:scale-105 disabled:opacity-50 disabled:hover:scale-100 disabled:cursor-not-allowed transition-all"
className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-gradient-to-r from-[var(--accent-cyan)] to-[var(--accent-blue)] text-white shadow-lg shadow-[var(--accent-cyan)]/20 hover:shadow-xl hover:shadow-[var(--accent-cyan)]/30 hover:scale-105 disabled:opacity-50 disabled:hover:scale-100 disabled:cursor-not-allowed transition-all"
>
{disabled ? (
<Loader2 className="h-4 w-4 animate-spin" />
@ -393,10 +393,10 @@ export function MessageInput({
</div>
{/* 提示 */}
<div className="mt-2 text-center text-xs text-zinc-500">
<div className="mt-2 text-center text-xs text-[var(--text-muted)]">
Enter Shift+Enter · / · 50MB
</div>
</div>
</div>
)
}
}

View File

@ -131,14 +131,14 @@ export function MessageList({ messages, onNavigateToSubAgent }: MessageListProps
return (
<div className="flex h-full items-center justify-center">
<div className="text-center animate-fade-in">
<div className="mb-6 inline-flex h-20 w-20 items-center justify-center rounded-full bg-gradient-to-br from-[#00f0ff]/20 to-[#3b82f6]/20 shadow-2xl shadow-[#00f0ff]/20">
<Sparkles className="h-10 w-10 text-[#00f0ff]" />
<div className="mb-6 inline-flex h-20 w-20 items-center justify-center rounded-full bg-gradient-to-br from-[var(--accent-cyan)]/20 to-[var(--accent-blue)]/20 shadow-2xl shadow-[var(--accent-cyan)]/20">
<Sparkles className="h-10 w-10 text-[var(--accent-cyan)]" />
</div>
<h2 className="mb-2 text-2xl font-bold text-white"></h2>
<p className="text-zinc-500"> AI </p>
<div className="mt-8 flex items-center justify-center gap-4 text-sm text-zinc-600">
<span className="px-3 py-1 rounded-full bg-zinc-800/50 border border-zinc-700">/new </span>
<span className="px-3 py-1 rounded-full bg-zinc-800/50 border border-zinc-700">/list </span>
<h2 className="mb-2 text-2xl font-bold text-[var(--text-primary)]"></h2>
<p className="text-[var(--text-muted)]"> AI </p>
<div className="mt-8 flex items-center justify-center gap-4 text-sm text-[var(--text-muted)]">
<span className="px-3 py-1 rounded-full bg-[var(--bg-hover)] border border-[var(--border-color)]">/new </span>
<span className="px-3 py-1 rounded-full bg-[var(--bg-hover)] border border-[var(--border-color)]">/list </span>
</div>
</div>
</div>
@ -168,11 +168,11 @@ export function MessageList({ messages, onNavigateToSubAgent }: MessageListProps
onClick={scrollToTop}
className="pointer-events-auto group relative flex items-center justify-center
w-9 h-9 rounded-full
bg-[#1a1a25]/80 backdrop-blur-md
border border-white/[0.06]
text-zinc-500 hover:text-[#00f0ff]
hover:border-[#00f0ff]/30
hover:shadow-[0_0_20px_rgba(0,240,255,0.12)]
bg-[var(--bg-tertiary)]/80 backdrop-blur-md
border border-[var(--border-color)]
text-[var(--text-muted)] hover:text-[var(--accent-cyan)]
hover:border-[var(--accent-cyan)]/30
hover:shadow-[0_0_20px_var(--shadow-glow-sm)]
transition-all duration-300 ease-out
animate-fade-in"
aria-label="回到顶部"
@ -187,11 +187,11 @@ export function MessageList({ messages, onNavigateToSubAgent }: MessageListProps
onClick={() => scrollToBottom('smooth')}
className="pointer-events-auto group relative flex items-center justify-center
w-9 h-9 rounded-full
bg-[#1a1a25]/80 backdrop-blur-md
border border-white/[0.06]
text-zinc-500 hover:text-[#00f0ff]
hover:border-[#00f0ff]/30
hover:shadow-[0_0_20px_rgba(0,240,255,0.12)]
bg-[var(--bg-tertiary)]/80 backdrop-blur-md
border border-[var(--border-color)]
text-[var(--text-muted)] hover:text-[var(--accent-cyan)]
hover:border-[var(--accent-cyan)]/30
hover:shadow-[0_0_20px_var(--shadow-glow-sm)]
transition-all duration-300 ease-out
animate-fade-in"
aria-label="回到底部"
@ -201,8 +201,8 @@ export function MessageList({ messages, onNavigateToSubAgent }: MessageListProps
{/* 未读消息指示点 */}
{hasNewMessage && (
<span className="absolute -top-0.5 -right-0.5 flex h-2.5 w-2.5">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-[#00f0ff]/60"></span>
<span className="relative inline-flex h-2.5 w-2.5 rounded-full bg-[#00f0ff]"></span>
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-[var(--accent-cyan)]/60"></span>
<span className="relative inline-flex h-2.5 w-2.5 rounded-full bg-[var(--accent-cyan)]"></span>
</span>
)}
</button>

View File

@ -128,11 +128,11 @@ export function ToolPanel({ messages }: ToolPanelProps) {
return (
<div className="flex h-full flex-col">
<style>{animStyles}</style>
<div className="border-b border-white/8 p-4 font-semibold text-white flex items-center gap-2">
<Terminal className="h-4 w-4 text-[#00f0ff]" />
<div className="border-b border-[var(--border-color)] p-4 font-semibold text-[var(--text-primary)] flex items-center gap-2">
<Terminal className="h-4 w-4 text-[var(--accent-cyan)]" />
</div>
<div className="flex flex-1 items-center justify-center p-4 text-sm text-zinc-500">
<div className="flex flex-1 items-center justify-center p-4 text-sm text-[var(--text-muted)]">
<div className="text-center">
<Terminal className="h-10 w-10 mx-auto mb-3 opacity-30" />
@ -145,10 +145,10 @@ export function ToolPanel({ messages }: ToolPanelProps) {
return (
<div className="flex h-full flex-col">
<style>{animStyles}</style>
<div className="border-b border-white/8 p-4 font-semibold text-white flex items-center gap-2">
<Terminal className="h-4 w-4 text-[#00f0ff]" />
<div className="border-b border-[var(--border-color)] p-4 font-semibold text-[var(--text-primary)] flex items-center gap-2">
<Terminal className="h-4 w-4 text-[var(--accent-cyan)]" />
<span className="ml-auto text-xs px-2 py-0.5 rounded-full bg-[#00f0ff]/10 text-[#00f0ff]">
<span className="ml-auto text-xs px-2 py-0.5 rounded-full bg-[var(--accent-cyan)]/10 text-[var(--accent-cyan)]">
{toolCalls.length}
</span>
</div>
@ -167,31 +167,31 @@ export function ToolPanel({ messages }: ToolPanelProps) {
return (
<div
key={tool.toolCallId}
className={`rounded-xl border bg-[#1a1a25]/50 text-sm overflow-hidden tool-card transition-colors duration-500 ${config.borderClass}`}
className={`rounded-xl border bg-[var(--bg-tertiary)]/50 text-sm overflow-hidden tool-card transition-colors duration-500 ${config.borderClass}`}
>
<button
onClick={() => toggleExpand(tool.toolCallId)}
className="flex w-full items-center justify-between px-3 py-2.5 hover:bg-white/5 transition-colors"
className="flex w-full items-center justify-between px-3 py-2.5 hover:bg-[var(--overlay-hover)] transition-colors"
>
<div className="flex items-center gap-2 min-w-0">
<span className={`tool-status-icon ${tool.status === 'calling' && !hasResult ? 'animate-pulse' : ''}`}>
<StatusIcon className={`h-3.5 w-3.5 transition-colors duration-500 ${config.iconColor}`} />
</span>
<span className="font-medium text-zinc-300 truncate">{tool.toolName}</span>
<span className="font-medium text-[var(--text-secondary)] truncate">{tool.toolName}</span>
<span className={`text-xs flex-shrink-0 transition-colors duration-500 ${config.labelClass}`}>
{config.label}
</span>
{tool.status === 'result' && tool.durationMs != null && (
<span className="text-xs text-zinc-600 flex-shrink-0 tabular-nums ml-1">
<span className="text-xs text-[var(--text-muted)] flex-shrink-0 tabular-nums ml-1">
{formatDuration(tool.durationMs)}
</span>
)}
</div>
<span className="flex-shrink-0 ml-2">
{isExpanded ? (
<ChevronDown className="h-4 w-4 text-zinc-500" />
<ChevronDown className="h-4 w-4 text-[var(--text-muted)]" />
) : (
<ChevronRight className="h-4 w-4 text-zinc-500" />
<ChevronRight className="h-4 w-4 text-[var(--text-muted)]" />
)}
</span>
</button>
@ -200,7 +200,7 @@ export function ToolPanel({ messages }: ToolPanelProps) {
{hasResult && (
<div className="px-3 pb-2">
<div
className={`rounded-lg bg-black/30 px-2.5 py-2 text-xs text-zinc-400 font-mono cursor-pointer hover:bg-black/40 transition-colors ${
className={`rounded-lg bg-[var(--overlay-dim-strong)] px-2.5 py-2 text-xs text-[var(--text-secondary)] font-mono cursor-pointer hover:bg-[var(--overlay-dim-heavy)] transition-colors ${
isExpanded ? '' : 'line-clamp-2'
}`}
onClick={() => toggleExpand(tool.toolCallId)}
@ -212,7 +212,7 @@ export function ToolPanel({ messages }: ToolPanelProps) {
)}
</div>
{!isExpanded && hasMore && (
<div className="text-xs text-zinc-500 mt-1 px-1">
<div className="text-xs text-[var(--text-muted)] mt-1 px-1">
({displayContent.split('\n').length} )
</div>
)}
@ -221,9 +221,9 @@ export function ToolPanel({ messages }: ToolPanelProps) {
{/* 展开区域:参数 */}
{isExpanded && tool.arguments ? (
<div className="border-t border-white/8 px-3 py-2 bg-black/20">
<div className="text-xs font-medium text-zinc-500 mb-1">:</div>
<pre className="rounded-lg bg-black/40 p-2 text-xs overflow-x-auto text-zinc-400 font-mono whitespace-pre-wrap break-all">
<div className="border-t border-[var(--border-color)] px-3 py-2 bg-[var(--overlay-dim)]">
<div className="text-xs font-medium text-[var(--text-muted)] mb-1">:</div>
<pre className="rounded-lg bg-[var(--overlay-dim-heavy)] p-2 text-xs overflow-x-auto text-[var(--text-secondary)] font-mono whitespace-pre-wrap break-all">
{JSON.stringify(tool.arguments, null, 2)}
</pre>
</div>

View File

@ -68,7 +68,7 @@ function lastStatusIcon(lastStatus: string | undefined) {
case 'error':
return <X className="h-3 w-3 text-red-400" />
default:
return <Minus className="h-3 w-3 text-zinc-500" />
return <Minus className="h-3 w-3 text-[var(--text-muted)]" />
}
}
@ -76,17 +76,17 @@ export function SchedulerJobList({ jobs, onRefresh, onViewJob, sessionId }: Sche
return (
<div className="flex h-full flex-col">
{/* Header */}
<div className="flex items-center justify-between border-b border-white/8 px-3 py-2.5">
<h2 className="font-semibold text-white flex items-center gap-2 text-sm">
<Clock className="h-4 w-4 text-[#00f0ff]" />
<div className="flex items-center justify-between border-b border-[var(--border-color)] px-3 py-2.5">
<h2 className="font-semibold text-[var(--text-primary)] flex items-center gap-2 text-sm">
<Clock className="h-4 w-4 text-[var(--accent-cyan)]" />
{jobs.length > 0 && (
<span className="text-xs text-zinc-500">({jobs.length})</span>
<span className="text-xs text-[var(--text-muted)]">({jobs.length})</span>
)}
</h2>
<button
onClick={onRefresh}
className="flex items-center gap-1 rounded-lg px-2 py-1 text-xs text-zinc-400 hover:text-white hover:bg-white/10 transition-all"
className="flex items-center gap-1 rounded-lg px-2 py-1 text-xs text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--overlay-subtle)] transition-all"
>
<RefreshCw className="h-3.5 w-3.5" />
@ -96,12 +96,12 @@ export function SchedulerJobList({ jobs, onRefresh, onViewJob, sessionId }: Sche
{/* Job List */}
<div className="flex-1 overflow-y-auto p-2">
{!sessionId ? (
<div className="p-4 text-center text-sm text-zinc-500">
<div className="p-4 text-center text-sm text-[var(--text-muted)]">
<Clock className="h-8 w-8 mx-auto mb-2 opacity-50" />
<p>...</p>
</div>
) : jobs.length === 0 ? (
<div className="p-4 text-center text-sm text-zinc-500">
<div className="p-4 text-center text-sm text-[var(--text-muted)]">
<Clock className="h-8 w-8 mx-auto mb-2 opacity-50" />
<p></p>
</div>
@ -121,23 +121,23 @@ export function SchedulerJobList({ jobs, onRefresh, onViewJob, sessionId }: Sche
disabled={!hasLookup}
className={`w-full rounded-lg px-3 py-2.5 text-left transition-all border ${
hasLookup
? 'hover:bg-white/5 border-transparent hover:border-white/10 cursor-pointer'
? 'hover:bg-[var(--overlay-hover)] border-transparent hover:border-[var(--border-color)] cursor-pointer'
: 'border-transparent cursor-default opacity-70'
}`}
>
{/* Row 1: ID + kind + enabled */}
<div className="flex items-center justify-between mb-1">
<div className="flex items-center gap-1.5 min-w-0 flex-1">
<span className="text-xs text-zinc-500 font-mono truncate max-w-[120px]">
<span className="text-xs text-[var(--text-muted)] font-mono truncate max-w-[120px]">
{job.id}
</span>
<span className="text-xs px-1.5 py-0.5 rounded bg-white/10 text-zinc-300 shrink-0">
<span className="text-xs px-1.5 py-0.5 rounded bg-[var(--overlay-subtle)] text-[var(--text-secondary)] shrink-0">
{kindLabel(job.kind)}
</span>
</div>
<div className="flex items-center gap-1.5 shrink-0">
<span className={`inline-block h-1.5 w-1.5 rounded-full ${job.enabled ? 'bg-emerald-400' : 'bg-zinc-600'}`} />
{hasLookup && <ChevronRight className="h-3.5 w-3.5 text-zinc-600" />}
{hasLookup && <ChevronRight className="h-3.5 w-3.5 text-[var(--text-muted)]" />}
</div>
</div>
@ -147,11 +147,11 @@ export function SchedulerJobList({ jobs, onRefresh, onViewJob, sessionId }: Sche
{badge.pulse && <span className="inline-block h-1.5 w-1.5 rounded-full bg-amber-400 animate-pulse" />}
{badge.label}
</span>
<span className="inline-flex items-center gap-0.5 text-xs text-zinc-500">
<span className="inline-flex items-center gap-0.5 text-xs text-[var(--text-muted)]">
{lastStatusIcon(job.last_status)}
<span className={
job.last_status === 'error' ? 'text-red-400' :
job.last_status === 'ok' ? 'text-emerald-400' : 'text-zinc-500'
job.last_status === 'ok' ? 'text-emerald-400' : 'text-[var(--text-muted)]'
}>
{job.last_status === 'ok' ? '正常' :
job.last_status === 'error' ? '异常' :
@ -168,17 +168,17 @@ export function SchedulerJobList({ jobs, onRefresh, onViewJob, sessionId }: Sche
)}
{/* Row 4: Run count + schedule */}
<div className="flex items-center justify-between text-xs text-zinc-500 mb-1">
<div className="flex items-center justify-between text-xs text-[var(--text-muted)] mb-1">
<span>
: {job.run_count}{job.max_runs ? `/${job.max_runs}` : ''}
</span>
<span className="truncate max-w-[120px] text-zinc-600">
<span className="truncate max-w-[120px] text-[var(--text-muted)]">
{scheduleDescription(job.schedule)}
</span>
</div>
{/* Row 5: Times */}
<div className="flex items-center justify-between text-xs text-zinc-600">
<div className="flex items-center justify-between text-xs text-[var(--text-muted)]">
<span>: {formatTime(job.last_fired_at)}</span>
<span>: {formatTime(job.next_fire_at)}</span>
</div>

View File

@ -10,36 +10,36 @@ export function SessionInfo({ session, connectionId }: SessionInfoProps) {
return (
<div className="p-4">
<div className="flex items-center gap-2 mb-3">
<Wifi className="h-4 w-4 text-[#00f0ff]" />
<span className="text-sm font-medium text-white">WebSocket</span>
<Wifi className="h-4 w-4 text-[var(--accent-cyan)]" />
<span className="text-sm font-medium text-[var(--text-primary)]">WebSocket</span>
<span className="text-xs px-1.5 py-0.5 rounded bg-emerald-500/20 text-emerald-400">
线
</span>
</div>
<div className="rounded-xl border border-white/10 bg-[#1a1a25]/80 p-3">
<div className="rounded-xl border border-[var(--border-color)] bg-[var(--bg-tertiary)]/80 p-3">
<div className="flex items-center gap-2 mb-2">
<FolderOpen className="h-4 w-4 text-zinc-400" />
<span className="text-xs text-zinc-500 uppercase tracking-wider"></span>
<FolderOpen className="h-4 w-4 text-[var(--text-secondary)]" />
<span className="text-xs text-[var(--text-muted)] uppercase tracking-wider"></span>
</div>
{session ? (
<div className="space-y-1">
<p className="text-sm font-medium text-white truncate">
<p className="text-sm font-medium text-[var(--text-primary)] truncate">
{session.title}
</p>
<div className="flex items-center gap-1 text-xs text-zinc-500">
<div className="flex items-center gap-1 text-xs text-[var(--text-muted)]">
<Hash className="h-3 w-3" />
<span className="font-mono">{session.id.slice(0, 8)}...</span>
</div>
</div>
) : (
<p className="text-sm text-zinc-500">...</p>
<p className="text-sm text-[var(--text-muted)]">...</p>
)}
{connectionId && (
<div className="mt-2 pt-2 border-t border-white/10">
<p className="text-xs text-zinc-600 font-mono">
<div className="mt-2 pt-2 border-t border-[var(--border-color)]">
<p className="text-xs text-[var(--text-muted)] font-mono">
conn: {connectionId.slice(0, 8)}...
</p>
</div>

View File

@ -39,12 +39,12 @@ export function TopicList({
return (
<div className="flex h-full flex-col">
{/* Header */}
<div className="flex items-center justify-between border-b border-white/8 px-4 py-3">
<h2 className="font-semibold text-white flex items-center gap-2 text-sm">
<Layers className="h-4 w-4 text-[#00f0ff]" />
<div className="flex items-center justify-between border-b border-[var(--border-color)] px-4 py-3">
<h2 className="font-semibold text-[var(--text-primary)] flex items-center gap-2 text-sm">
<Layers className="h-4 w-4 text-[var(--accent-cyan)]" />
{topics.length > 0 && (
<span className="text-xs text-zinc-500">({topics.length})</span>
<span className="text-xs text-[var(--text-muted)]">({topics.length})</span>
)}
</h2>
<button
@ -52,8 +52,8 @@ export function TopicList({
disabled={isReadOnly || !sessionId}
className={`flex items-center gap-1 rounded-lg px-3 py-1.5 text-sm transition-all ${
isReadOnly || !sessionId
? 'bg-zinc-500/10 text-zinc-500 cursor-not-allowed'
: 'bg-[#00f0ff]/10 text-[#00f0ff] hover:bg-[#00f0ff]/20 border border-[#00f0ff]/30'
? 'bg-[var(--overlay-subtle)] text-[var(--text-muted)] cursor-not-allowed'
: 'bg-[var(--accent-cyan)]/10 text-[var(--accent-cyan)] hover:bg-[var(--accent-cyan)]/20 border border-[var(--accent-cyan)]/30'
}`}
>
<Plus className="h-4 w-4" />
@ -63,21 +63,21 @@ export function TopicList({
{/* Session 标题 */}
{sessionTitle && (
<div className="px-4 py-2 border-b border-white/8 bg-[#00f0ff]/5">
<p className="text-xs text-zinc-500 mb-1"></p>
<p className="text-sm text-zinc-300 font-medium truncate">{sessionTitle}</p>
<div className="px-4 py-2 border-b border-[var(--border-color)] bg-[var(--accent-cyan)]/5">
<p className="text-xs text-[var(--text-muted)] mb-1"></p>
<p className="text-sm text-[var(--text-secondary)] font-medium truncate">{sessionTitle}</p>
</div>
)}
{/* Topics 列表 */}
<div className="flex-1 overflow-y-auto p-3">
{!sessionId ? (
<div className="p-4 text-center text-sm text-zinc-500">
<div className="p-4 text-center text-sm text-[var(--text-muted)]">
<MessageSquare className="h-8 w-8 mx-auto mb-2 opacity-50" />
<p>...</p>
</div>
) : topics.length === 0 ? (
<div className="p-4 text-center text-sm text-zinc-500">
<div className="p-4 text-center text-sm text-[var(--text-muted)]">
<MessageSquare className="h-8 w-8 mx-auto mb-2 opacity-50" />
<p></p>
<p className="text-xs mt-1">"新建"</p>
@ -90,33 +90,33 @@ export function TopicList({
onClick={() => onSwitchTopic(topic.id)}
className={`w-full rounded-xl px-3 py-3 text-left text-sm transition-all ${
topic.id === currentTopicId
? 'bg-gradient-to-r from-[#00f0ff]/20 to-transparent border border-[#00f0ff]/30'
: 'hover:bg-white/5 border border-transparent'
? 'bg-gradient-to-r from-[var(--accent-cyan)]/20 to-transparent border border-[var(--accent-cyan)]/30'
: 'hover:bg-[var(--overlay-hover)] border border-transparent'
}`}
>
<div className="flex items-start gap-3">
<span className="mt-0.5 text-xs text-zinc-500 font-mono w-4">
<span className="mt-0.5 text-xs text-[var(--text-muted)] font-mono w-4">
{index + 1}
</span>
<div className="min-w-0 flex-1">
<div className={`truncate font-medium ${
topic.id === currentTopicId ? 'text-[#00f0ff]' : 'text-zinc-300'
topic.id === currentTopicId ? 'text-[var(--accent-cyan)]' : 'text-[var(--text-secondary)]'
}`}>
{topic.description || topic.title}
</div>
<div className="flex items-center gap-3 mt-1.5">
<span className="text-xs text-zinc-500 flex items-center gap-1">
<span className="text-xs text-[var(--text-muted)] flex items-center gap-1">
<Hash className="h-3 w-3" />
{topic.message_count}
</span>
<span className="text-xs text-zinc-600 flex items-center gap-1">
<span className="text-xs text-[var(--text-muted)] flex items-center gap-1">
<Clock className="h-3 w-3" />
{formatTime(topic.updated_at)}
</span>
</div>
</div>
{topic.id === currentTopicId && (
<span className="inline-block h-2 w-2 rounded-full bg-[#00f0ff] shadow-lg shadow-[#00f0ff]/50 mt-1.5" />
<span className="inline-block h-2 w-2 rounded-full bg-[var(--accent-cyan)] shadow-lg shadow-[var(--shadow-glow-soft)] mt-1.5" />
)}
</div>
</button>

View File

@ -1,6 +1,8 @@
@import "tailwindcss";
/* PicoBot Theme - Tech Dark */
/* ============================================
PicoBot Theme Dark (default)
============================================ */
:root {
/* Core colors */
--bg-primary: #0a0a0f;
@ -24,9 +26,104 @@
/* Border */
--border-color: rgba(255, 255, 255, 0.08);
--border-accent: rgba(0, 240, 255, 0.3);
/* Overlay — for hover / subtle surface effects */
--overlay-hover: rgba(255, 255, 255, 0.05);
--overlay-subtle: rgba(255, 255, 255, 0.10);
--overlay-medium: rgba(255, 255, 255, 0.20);
--overlay-dim: rgba(0, 0, 0, 0.20);
--overlay-dim-strong: rgba(0, 0, 0, 0.30);
--overlay-dim-heavy: rgba(0, 0, 0, 0.40);
--overlay-code: rgba(0, 0, 0, 0.30);
/* Shadows */
--shadow-glow-sm: rgba(0, 240, 255, 0.12);
--shadow-glow: rgba(0, 240, 255, 0.20);
--shadow-glow-strong: rgba(0, 240, 255, 0.30);
--shadow-glow-soft: rgba(0, 240, 255, 0.50);
/* Focus ring */
--focus-ring: rgba(0, 240, 255, 0.20);
/* Selection / code highlight */
--selection-bg: rgba(0, 240, 255, 0.30);
/* Divider */
--divider-color: rgba(255, 255, 255, 0.20);
color-scheme: dark;
}
/* Custom scrollbar */
/* ============================================
PicoBot Theme Light
============================================ */
html.light {
/* Backgrounds */
--bg-primary: #f5f5f7;
--bg-secondary: #ffffff;
--bg-tertiary: #ebecf0;
--bg-hover: #dfe0e5;
/* Accents — slightly darker for readability on white */
--accent-cyan: #008899;
--accent-blue: #2563eb;
--accent-purple: #7c3aed;
--accent-green: #059669;
--accent-amber: #d97706;
/* Text */
--text-primary: #1a1a2e;
--text-secondary: #52525b;
--text-muted: #a1a1aa;
--text-accent: var(--accent-cyan);
/* Borders */
--border-color: rgba(0, 0, 0, 0.08);
--border-accent: rgba(0, 136, 153, 0.30);
/* Overlays */
--overlay-hover: rgba(0, 0, 0, 0.04);
--overlay-subtle: rgba(0, 0, 0, 0.06);
--overlay-medium: rgba(0, 0, 0, 0.10);
--overlay-dim: rgba(0, 0, 0, 0.04);
--overlay-dim-strong: rgba(0, 0, 0, 0.06);
--overlay-dim-heavy: rgba(0, 0, 0, 0.08);
--overlay-code: rgba(0, 0, 0, 0.06);
/* Shadows — softer */
--shadow-glow-sm: rgba(0, 136, 153, 0.10);
--shadow-glow: rgba(0, 136, 153, 0.15);
--shadow-glow-strong: rgba(0, 136, 153, 0.22);
--shadow-glow-soft: rgba(0, 136, 153, 0.35);
/* Focus ring */
--focus-ring: rgba(0, 136, 153, 0.20);
/* Selection / code highlight */
--selection-bg: rgba(0, 136, 153, 0.20);
/* Divider */
--divider-color: rgba(0, 0, 0, 0.10);
color-scheme: light;
}
/* ============================================
Smooth theme transition
============================================ */
html.theme-transitioning *,
html.theme-transitioning *::before,
html.theme-transitioning *::after {
transition: background-color 0.3s ease,
color 0.3s ease,
border-color 0.3s ease,
box-shadow 0.3s ease,
text-shadow 0.3s ease !important;
}
/* ============================================
Custom scrollbar
============================================ */
::-webkit-scrollbar {
width: 6px;
height: 6px;
@ -55,7 +152,9 @@
display: none;
}
/* Base styles */
/* ============================================
Base styles
============================================ */
* {
box-sizing: border-box;
}
@ -71,20 +170,26 @@ body {
line-height: 1.6;
}
/* Custom selection */
/* ============================================
Custom selection
============================================ */
::selection {
background: rgba(0, 240, 255, 0.3);
background: var(--selection-bg);
color: var(--text-primary);
}
/* Glowing text effect */
/* ============================================
Glowing text effect
============================================ */
.glow-text {
text-shadow: 0 0 10px rgba(0, 240, 255, 0.5),
0 0 20px rgba(0, 240, 255, 0.3),
0 0 30px rgba(0, 240, 255, 0.1);
text-shadow: 0 0 10px var(--shadow-glow-soft),
0 0 20px var(--shadow-glow-strong),
0 0 30px var(--shadow-glow-sm);
}
/* Gradient border */
/* ============================================
Gradient border
============================================ */
.gradient-border {
position: relative;
}
@ -106,7 +211,9 @@ body {
mask-composite: exclude;
}
/* Animations */
/* ============================================
Animations
============================================ */
@keyframes pulse-glow {
0%, 100% {
box-shadow: 0 0 5px var(--accent-cyan),
@ -132,63 +239,35 @@ body {
}
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes typing-dot {
0%, 60%, 100% {
transform: translateY(0);
}
30% {
transform: translateY(-4px);
}
0%, 60%, 100% { transform: translateY(0); }
30% { transform: translateY(-4px); }
}
@keyframes scale-in {
0% {
opacity: 0;
transform: scale(0.5);
}
50% {
transform: scale(1.1);
}
100% {
opacity: 1;
transform: scale(1);
}
0% { opacity: 0; transform: scale(0.5); }
50% { transform: scale(1.1); }
100% { opacity: 1; transform: scale(1); }
}
.animate-slide-in {
animation: slide-in 0.3s ease-out;
}
.animate-fade-in {
animation: fade-in 0.2s ease-out;
}
.animate-scale-in {
animation: scale-in 0.3s ease-out;
}
.animate-slide-in { animation: slide-in 0.3s ease-out; }
.animate-fade-in { animation: fade-in 0.2s ease-out; }
.animate-scale-in { animation: scale-in 0.3s ease-out; }
.typing-indicator span {
animation: typing-dot 1.4s infinite;
display: inline-block;
}
.typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
.typing-indicator span:nth-child(3) { animation-delay: 0.4s; }
.typing-indicator span:nth-child(2) {
animation-delay: 0.2s;
}
.typing-indicator span:nth-child(3) {
animation-delay: 0.4s;
}
/* Code block styling */
/* ============================================
Code block styling
============================================ */
pre {
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
@ -202,15 +281,19 @@ code {
font-size: 0.9em;
}
/* Focus styles */
/* ============================================
Focus styles
============================================ */
input:focus,
textarea:focus {
outline: none;
border-color: var(--accent-cyan);
box-shadow: 0 0 0 2px rgba(0, 240, 255, 0.2);
box-shadow: 0 0 0 2px var(--focus-ring);
}
/* Button hover effects */
/* ============================================
Button hover effects
============================================ */
button {
transition: all 0.2s ease;
}
@ -223,7 +306,9 @@ button:active:not(:disabled) {
transform: translateY(0);
}
/* Link styles */
/* ============================================
Link styles
============================================ */
a {
color: var(--accent-cyan);
text-decoration: none;