feat(web): 对齐 deepseek-harness 设计语言,重构前端视觉与三栏布局
- 设计 token 重映射为 DeepSeek 调色板(深浅双主题),正文改无衬线字体栈,全面去霓虹化 - 移除顶部 Header,控件迁入左栏(Logo/连接/通道/会话/tabs/底部操作区),左右栏支持拖拽调宽并持久化,折叠为图标 rail - 对话区对齐 DeepSeek 聊天风:hero/docked 状态机、736px 居中列、用户右对齐柔和气泡、助手无气泡平铺、composer 卡片与反差发送键;子智能体点击导航完整保留 - 全功能区同步换肤:侧栏列表、右栏面板、ConfigPage、弹窗、选择器等 - fix: 修复 virtual-core 3.17.x 陈旧行高导致消息行重叠(宽度变化/滚动停止后强制 resizeItem 重测) - fix: 修复 ConfigPage/Modal 引用不存在的 fadeIn/scaleIn keyframes 致入场动画失效
This commit is contained in:
parent
4c21add43b
commit
eaee29841d
361
web/src/App.tsx
361
web/src/App.tsx
@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState, type MouseEvent as ReactMouseEvent } from 'react';
|
||||
import {
|
||||
Zap,
|
||||
ArrowLeft,
|
||||
@ -11,6 +11,8 @@ import {
|
||||
Brain,
|
||||
Settings as SettingsIcon,
|
||||
ChevronRight,
|
||||
MessageSquare,
|
||||
Calendar,
|
||||
} from 'lucide-react';
|
||||
import { ChatContainer } from './components/Chat/ChatContainer';
|
||||
import { TopicList } from './components/Sidebar/TopicList';
|
||||
@ -200,6 +202,75 @@ function App() {
|
||||
// 设置弹窗关闭计数器:每次关闭时递增,用于通知 ExpertSelector 刷新已选专家状态
|
||||
const [settingsClosedTick, setSettingsClosedTick] = useState(0);
|
||||
|
||||
// ---- 三栏列宽(拖拽调宽,对齐 harness AppFrame)----
|
||||
const [sidebarWidth, setSidebarWidth] = useState(() => {
|
||||
const parsed = parseInt(localStorage.getItem('picobot-sidebar-width') ?? '', 10);
|
||||
return Number.isFinite(parsed) ? Math.min(Math.max(parsed, 216), 400) : 288;
|
||||
});
|
||||
const [rightPanelWidth, setRightPanelWidth] = useState(() => {
|
||||
const parsed = parseInt(localStorage.getItem('picobot-right-panel-width') ?? '', 10);
|
||||
return Number.isFinite(parsed) ? Math.min(Math.max(parsed, 240), 420) : 320;
|
||||
});
|
||||
|
||||
// 拖拽期间禁用 width transition,避免面板"追鼠标"延迟感
|
||||
const [isResizing, setIsResizing] = useState(false);
|
||||
|
||||
const startSidebarDrag = useCallback(
|
||||
(e: ReactMouseEvent) => {
|
||||
e.preventDefault();
|
||||
const startX = e.clientX;
|
||||
const startWidth = sidebarWidth;
|
||||
setIsResizing(true);
|
||||
const onMove = (ev: MouseEvent) => {
|
||||
setSidebarWidth(Math.min(Math.max(startWidth + ev.clientX - startX, 216), 400));
|
||||
};
|
||||
const onUp = (ev: MouseEvent) => {
|
||||
document.removeEventListener('mousemove', onMove);
|
||||
document.removeEventListener('mouseup', onUp);
|
||||
document.body.style.cursor = '';
|
||||
document.body.style.userSelect = '';
|
||||
setIsResizing(false);
|
||||
localStorage.setItem(
|
||||
'picobot-sidebar-width',
|
||||
String(Math.min(Math.max(startWidth + ev.clientX - startX, 216), 400)),
|
||||
);
|
||||
};
|
||||
document.addEventListener('mousemove', onMove);
|
||||
document.addEventListener('mouseup', onUp);
|
||||
document.body.style.cursor = 'col-resize';
|
||||
document.body.style.userSelect = 'none';
|
||||
},
|
||||
[sidebarWidth],
|
||||
);
|
||||
|
||||
const startRightPanelDrag = useCallback(
|
||||
(e: ReactMouseEvent) => {
|
||||
e.preventDefault();
|
||||
const startX = e.clientX;
|
||||
const startWidth = rightPanelWidth;
|
||||
setIsResizing(true);
|
||||
const onMove = (ev: MouseEvent) => {
|
||||
setRightPanelWidth(Math.min(Math.max(startWidth - (ev.clientX - startX), 240), 420));
|
||||
};
|
||||
const onUp = (ev: MouseEvent) => {
|
||||
document.removeEventListener('mousemove', onMove);
|
||||
document.removeEventListener('mouseup', onUp);
|
||||
document.body.style.cursor = '';
|
||||
document.body.style.userSelect = '';
|
||||
setIsResizing(false);
|
||||
localStorage.setItem(
|
||||
'picobot-right-panel-width',
|
||||
String(Math.min(Math.max(startWidth - (ev.clientX - startX), 240), 420)),
|
||||
);
|
||||
};
|
||||
document.addEventListener('mousemove', onMove);
|
||||
document.addEventListener('mouseup', onUp);
|
||||
document.body.style.cursor = 'col-resize';
|
||||
document.body.style.userSelect = 'none';
|
||||
},
|
||||
[rightPanelWidth],
|
||||
);
|
||||
|
||||
const handleSaveConnection = useCallback((host: string, port: number) => {
|
||||
setGatewaySettings({ host, port });
|
||||
}, []);
|
||||
@ -615,59 +686,112 @@ function App() {
|
||||
}, [schedulerView, subAgentView, selectedTopic]);
|
||||
|
||||
return (
|
||||
<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-[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-[var(--accent-cyan)]" />
|
||||
<h1 className="text-xl font-bold tracking-tight">
|
||||
<div className="flex h-screen overflow-hidden bg-[var(--bg-primary)] text-[var(--text-primary)]">
|
||||
{/* 左栏 — harness 风格 shell(Logo/连接/通道/会话/列表/底部操作区) */}
|
||||
<aside
|
||||
className={`relative shrink-0 border-r border-[var(--border-color)] bg-[var(--bg-secondary)] flex flex-col overflow-hidden ${subAgentView || schedulerView ? 'opacity-50 pointer-events-none' : ''}`}
|
||||
style={{
|
||||
width: sidebarCollapsed ? 56 : sidebarWidth,
|
||||
transition: isResizing ? 'none' : 'width 200ms ease-out',
|
||||
willChange: 'width',
|
||||
}}
|
||||
>
|
||||
{/* Logo 行 */}
|
||||
<div className="flex h-12 shrink-0 items-center gap-2 px-3">
|
||||
{sidebarCollapsed ? (
|
||||
<button
|
||||
onClick={toggleSidebar}
|
||||
className="flex h-8 w-8 items-center justify-center rounded-lg hover:bg-[var(--overlay-hover)] transition-colors"
|
||||
title="展开侧栏"
|
||||
aria-label="Expand sidebar"
|
||||
>
|
||||
<Zap className="h-5 w-5 text-[var(--accent-cyan)]" />
|
||||
</button>
|
||||
) : (
|
||||
<>
|
||||
<Zap className="h-5 w-5 shrink-0 text-[var(--accent-cyan)]" />
|
||||
<h1 className="min-w-0 flex-1 truncate text-[15px] font-semibold tracking-tight">
|
||||
<span className="text-[var(--text-primary)]">Pico</span>
|
||||
<span className="text-[var(--accent-cyan)] glow-text">Bot</span>
|
||||
<span className="text-[var(--accent-cyan)]">Bot</span>
|
||||
</h1>
|
||||
{appVersion && (
|
||||
<span className="text-xs font-mono text-[var(--text-muted)] select-none">
|
||||
<span className="shrink-0 text-xs text-[var(--text-muted)] select-none">
|
||||
v{appVersion}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<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'}
|
||||
onClick={toggleSidebar}
|
||||
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg text-[var(--text-muted)] hover:bg-[var(--overlay-hover)] hover:text-[var(--text-primary)] transition-colors"
|
||||
title="收起侧栏"
|
||||
aria-label="Collapse sidebar"
|
||||
>
|
||||
{theme === 'dark' ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
|
||||
<PanelLeftClose className="h-4 w-4" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{sidebarCollapsed ? (
|
||||
/* 折叠 rail */
|
||||
<div className="flex flex-1 flex-col items-center gap-1 px-1 pt-1">
|
||||
<button
|
||||
onClick={() => {
|
||||
setSidebarTab('topics');
|
||||
setSidebarCollapsed(false);
|
||||
localStorage.setItem('picobot-sidebar-collapsed', 'false');
|
||||
}}
|
||||
className={`flex h-8 w-8 items-center justify-center rounded-lg transition-colors ${
|
||||
sidebarTab === 'topics'
|
||||
? 'text-[var(--accent-cyan)] bg-[var(--overlay-subtle)]'
|
||||
: 'text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--overlay-hover)]'
|
||||
}`}
|
||||
title="话题"
|
||||
>
|
||||
<MessageSquare className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowThinking((prev) => !prev)}
|
||||
className={`flex h-8 w-8 items-center justify-center rounded-lg transition-all ${
|
||||
showThinking
|
||||
? 'text-purple-400 hover:text-purple-300 bg-purple-500/10 hover:bg-purple-500/20'
|
||||
: 'text-[var(--text-muted)] hover:text-[var(--text-secondary)] hover:bg-[var(--overlay-hover)]'
|
||||
onClick={() => {
|
||||
setSidebarTab('scheduler');
|
||||
setSidebarCollapsed(false);
|
||||
localStorage.setItem('picobot-sidebar-collapsed', 'false');
|
||||
}}
|
||||
className={`flex h-8 w-8 items-center justify-center rounded-lg transition-colors ${
|
||||
sidebarTab === 'scheduler'
|
||||
? 'text-[var(--accent-cyan)] bg-[var(--overlay-subtle)]'
|
||||
: 'text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--overlay-hover)]'
|
||||
}`}
|
||||
title={showThinking ? '隐藏思考过程' : '显示思考过程'}
|
||||
aria-label="Toggle thinking display"
|
||||
title="定时任务"
|
||||
>
|
||||
<Brain className="h-4 w-4" />
|
||||
<Calendar className="h-4 w-4" />
|
||||
</button>
|
||||
<div className="flex-1" />
|
||||
<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(--text-primary)] hover:bg-[var(--overlay-hover)] transition-colors"
|
||||
title={theme === 'dark' ? '切换到亮色主题' : '切换到暗色主题'}
|
||||
>
|
||||
{theme === 'dark' ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setConfigInitialTab('providers');
|
||||
setConfigPageOpen(true);
|
||||
}}
|
||||
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"
|
||||
className="flex h-8 w-8 items-center justify-center rounded-lg text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--overlay-hover)] transition-colors"
|
||||
title="系统配置"
|
||||
aria-label="System config"
|
||||
>
|
||||
<SettingsIcon className="h-4 w-4" />
|
||||
</button>
|
||||
<div className="pb-3 pt-2">
|
||||
<ConnectionStatus status={status} compact />
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-sm text-[var(--text-secondary)]">
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="shrink-0 px-3 pb-2">
|
||||
<ConnectionStatus status={status} />
|
||||
</div>
|
||||
<div className="flex shrink-0 flex-col gap-1.5 px-3 pb-2 text-sm text-[var(--text-secondary)]">
|
||||
<ChannelSelector
|
||||
channels={channels}
|
||||
selectedChannel={selectedChannel}
|
||||
@ -679,101 +803,33 @@ function App() {
|
||||
onSelectSession={handleSelectSession}
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="flex flex-1 overflow-hidden relative">
|
||||
{/* Left Sidebar — smooth width animation via will-change + overflow-hidden */}
|
||||
<div
|
||||
className={`shrink-0 border-r border-[var(--border-color)] bg-[var(--bg-secondary)]/50 flex flex-col overflow-hidden ${sidebarCollapsed ? 'w-11' : 'w-72'} ${subAgentView || schedulerView ? 'opacity-50 pointer-events-none' : ''}`}
|
||||
style={{ transition: 'width 200ms ease-out', willChange: 'width' }}
|
||||
>
|
||||
{/* Tab 栏 + collapse toggle */}
|
||||
<div
|
||||
className="flex border-b border-[var(--border-color)]"
|
||||
style={{ minWidth: sidebarCollapsed ? 0 : '288px' }}
|
||||
>
|
||||
{sidebarCollapsed ? (
|
||||
/* 收起态:紧凑竖排 tab 按钮 */
|
||||
<div className="flex flex-col w-full">
|
||||
<button
|
||||
onClick={() => {
|
||||
setSidebarTab('topics');
|
||||
setSidebarCollapsed(false);
|
||||
localStorage.setItem('picobot-sidebar-collapsed', 'false');
|
||||
}}
|
||||
className={`py-2 text-xs font-medium text-center transition-colors ${
|
||||
sidebarTab === 'topics'
|
||||
? 'text-[var(--accent-cyan)] bg-[var(--accent-cyan)]/10'
|
||||
: 'text-[var(--text-muted)] hover:text-[var(--text-secondary)] hover:bg-[var(--overlay-hover)]'
|
||||
}`}
|
||||
title="话题"
|
||||
>
|
||||
话<br />题
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setSidebarTab('scheduler');
|
||||
setSidebarCollapsed(false);
|
||||
localStorage.setItem('picobot-sidebar-collapsed', 'false');
|
||||
}}
|
||||
className={`py-2 text-xs font-medium text-center transition-colors ${
|
||||
sidebarTab === 'scheduler'
|
||||
? 'text-[var(--accent-cyan)] bg-[var(--accent-cyan)]/10'
|
||||
: 'text-[var(--text-muted)] hover:text-[var(--text-secondary)] hover:bg-[var(--overlay-hover)]'
|
||||
}`}
|
||||
title="定时任务"
|
||||
>
|
||||
定<br />时
|
||||
</button>
|
||||
{/* 展开按钮 */}
|
||||
<button
|
||||
onClick={toggleSidebar}
|
||||
className="flex items-center justify-center py-2 text-[var(--text-muted)] hover:text-[var(--accent-cyan)] hover:bg-[var(--overlay-hover)] transition-colors"
|
||||
title="展开侧栏"
|
||||
>
|
||||
<PanelLeftOpen className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Tab 栏 */}
|
||||
<div className="flex shrink-0 border-b border-[var(--border-color)] px-3">
|
||||
<button
|
||||
onClick={() => setSidebarTab('topics')}
|
||||
className={`flex-1 py-2.5 text-sm font-medium text-center transition-colors whitespace-nowrap ${
|
||||
className={`flex-1 border-b-2 py-2 text-center text-sm transition-colors whitespace-nowrap ${
|
||||
sidebarTab === 'topics'
|
||||
? 'text-[var(--accent-cyan)] border-b-2 border-[var(--accent-cyan)]'
|
||||
: 'text-[var(--text-muted)] hover:text-[var(--text-secondary)]'
|
||||
? 'border-[var(--accent-cyan)] font-medium text-[var(--text-primary)]'
|
||||
: 'border-transparent text-[var(--text-muted)] hover:text-[var(--text-secondary)]'
|
||||
}`}
|
||||
>
|
||||
话题
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setSidebarTab('scheduler')}
|
||||
className={`flex-1 py-2.5 text-sm font-medium text-center transition-colors whitespace-nowrap ${
|
||||
className={`flex-1 border-b-2 py-2 text-center text-sm transition-colors whitespace-nowrap ${
|
||||
sidebarTab === 'scheduler'
|
||||
? 'text-[var(--accent-cyan)] border-b-2 border-[var(--accent-cyan)]'
|
||||
: 'text-[var(--text-muted)] hover:text-[var(--text-secondary)]'
|
||||
? 'border-[var(--accent-cyan)] font-medium text-[var(--text-primary)]'
|
||||
: 'border-transparent text-[var(--text-muted)] hover:text-[var(--text-secondary)]'
|
||||
}`}
|
||||
>
|
||||
定时任务
|
||||
</button>
|
||||
{/* 收起按钮 */}
|
||||
<button
|
||||
onClick={toggleSidebar}
|
||||
className="flex items-center justify-center px-2 text-[var(--text-muted)] hover:text-[var(--accent-cyan)] hover:bg-[var(--overlay-hover)] transition-colors shrink-0"
|
||||
title="收起侧栏"
|
||||
>
|
||||
<PanelLeftClose className="h-4 w-4" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="flex-1 overflow-hidden"
|
||||
style={{ minWidth: sidebarCollapsed ? 0 : '288px' }}
|
||||
>
|
||||
{sidebarCollapsed ? null : sidebarTab === 'topics' ? (
|
||||
<div className="flex-1 overflow-hidden">
|
||||
{sidebarTab === 'topics' ? (
|
||||
<TopicList
|
||||
sessionId={sessionId}
|
||||
topics={topics}
|
||||
@ -794,10 +850,58 @@ function App() {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Center - Chat */}
|
||||
<div className="flex-1 min-w-0 bg-[var(--bg-primary)] flex flex-col">
|
||||
{/* 底部操作区:思考 / 主题 / 配置 */}
|
||||
<div className="flex shrink-0 items-center gap-1 border-t border-[var(--border-color)] px-3 py-2">
|
||||
<button
|
||||
onClick={() => setShowThinking((prev) => !prev)}
|
||||
className={`flex h-8 w-8 items-center justify-center rounded-lg transition-colors ${
|
||||
showThinking
|
||||
? 'text-[var(--accent-purple)] bg-[var(--accent-purple)]/10'
|
||||
: 'text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--overlay-hover)]'
|
||||
}`}
|
||||
title={showThinking ? '隐藏思考过程' : '显示思考过程'}
|
||||
aria-label="Toggle thinking display"
|
||||
>
|
||||
<Brain className="h-4 w-4" />
|
||||
</button>
|
||||
<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(--text-primary)] hover:bg-[var(--overlay-hover)] transition-colors"
|
||||
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 className="flex-1" />
|
||||
<button
|
||||
onClick={() => {
|
||||
setConfigInitialTab('providers');
|
||||
setConfigPageOpen(true);
|
||||
}}
|
||||
className="flex h-8 w-8 items-center justify-center rounded-lg text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--overlay-hover)] transition-colors"
|
||||
title="系统配置"
|
||||
aria-label="System config"
|
||||
>
|
||||
<SettingsIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</aside>
|
||||
|
||||
{/* 左栏拖拽调宽手柄 */}
|
||||
{!sidebarCollapsed && (
|
||||
<div
|
||||
onMouseDown={startSidebarDrag}
|
||||
className="w-1.5 shrink-0 cursor-col-resize transition-colors hover:bg-[var(--accent-cyan)]/40 active:bg-[var(--accent-cyan)]/60"
|
||||
role="separator"
|
||||
aria-orientation="vertical"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 中栏 — 对话区 */}
|
||||
<div className="flex min-w-0 flex-1 flex-col bg-[var(--bg-primary)]">
|
||||
{/* Scheduler job view back bar */}
|
||||
{schedulerView && (
|
||||
<div className="shrink-0 border-b border-[var(--border-color)] bg-[var(--bg-secondary)]/80 px-4 py-2 flex items-center gap-4">
|
||||
@ -810,7 +914,7 @@ function App() {
|
||||
</button>
|
||||
<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" />
|
||||
<Clock className="h-4 w-4 text-[var(--accent-amber)]" />
|
||||
<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}
|
||||
@ -861,13 +965,13 @@ function App() {
|
||||
: level.status;
|
||||
const statusColor =
|
||||
level.status === 'completed'
|
||||
? 'text-emerald-400'
|
||||
? 'text-[var(--accent-green)]'
|
||||
: level.status === 'failed'
|
||||
? 'text-red-400'
|
||||
? 'text-[rgb(242,90,90)]'
|
||||
: level.status === 'timeout'
|
||||
? 'text-amber-400'
|
||||
? 'text-[var(--accent-amber)]'
|
||||
: level.status === 'running'
|
||||
? 'text-amber-400'
|
||||
? 'text-[var(--accent-amber)]'
|
||||
: 'text-[var(--text-secondary)]';
|
||||
return (
|
||||
<div key={level.taskId} className="flex items-center gap-2 min-w-0">
|
||||
@ -935,10 +1039,24 @@ function App() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Sidebar - Todo / Memory / Skill Panel (collapsible, tabbed) */}
|
||||
{/* 右栏拖拽调宽手柄 */}
|
||||
{!rightSidebarCollapsed && (
|
||||
<div
|
||||
className={`shrink-0 border-l border-[var(--border-color)] bg-[var(--bg-secondary)]/50 flex flex-col overflow-hidden ${rightSidebarCollapsed ? 'w-11' : 'w-80'}`}
|
||||
style={{ transition: 'width 200ms ease-out', willChange: 'width' }}
|
||||
onMouseDown={startRightPanelDrag}
|
||||
className="w-1.5 shrink-0 cursor-col-resize transition-colors hover:bg-[var(--accent-cyan)]/40 active:bg-[var(--accent-cyan)]/60"
|
||||
role="separator"
|
||||
aria-orientation="vertical"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 右栏 — Token 统计 / 待办 / 记忆 / 技能(可折叠) */}
|
||||
<div
|
||||
className="relative shrink-0 border-l border-[var(--border-color)] bg-[var(--bg-secondary)] flex flex-col overflow-hidden"
|
||||
style={{
|
||||
width: rightSidebarCollapsed ? 44 : rightPanelWidth,
|
||||
transition: isResizing ? 'none' : 'width 200ms ease-out',
|
||||
willChange: 'width',
|
||||
}}
|
||||
>
|
||||
{rightSidebarCollapsed ? (
|
||||
// 折叠态:窄条 + 展开按钮
|
||||
@ -950,7 +1068,7 @@ function App() {
|
||||
<PanelLeftOpen className="h-4 w-4 rotate-180" />
|
||||
</button>
|
||||
) : (
|
||||
<div className="w-80 h-full flex flex-col">
|
||||
<div className="flex h-full min-w-0 flex-col">
|
||||
{/* Token 用量面板(主代理/子代理共用,按视图切换数据源) */}
|
||||
<TopicTokenStatsPanel
|
||||
tokenStats={subAgentView ? subAgentView.tokenStats : currentTopic?.tokenStats}
|
||||
@ -1020,7 +1138,6 @@ function App() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 系统配置页面 */}
|
||||
{configPageOpen && (
|
||||
|
||||
@ -3,6 +3,7 @@ import { MessageList } from './MessageList';
|
||||
import { MessageInput } from './MessageInput';
|
||||
import { ExpertSelector } from './ExpertSelector';
|
||||
import { ModelSelector } from './ModelSelector';
|
||||
import { Zap } from 'lucide-react';
|
||||
import type { ChatMessage, Attachment } from '../../types/protocol';
|
||||
|
||||
interface ChatContainerProps {
|
||||
@ -49,18 +50,8 @@ export function ChatContainer({
|
||||
description: string;
|
||||
} | null>(null);
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col relative">
|
||||
<div className="flex-1 overflow-hidden relative">
|
||||
<MessageList
|
||||
messages={messages}
|
||||
onNavigateToSubAgent={onNavigateToSubAgent}
|
||||
showThinking={showThinking}
|
||||
viewKey={viewKey}
|
||||
highlightedMessageId={highlightedMessageId}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-1 px-4 pt-2 max-w-5xl mx-auto w-full">
|
||||
const selectors = (
|
||||
<div className="flex flex-wrap items-center gap-1 px-3 pt-2">
|
||||
<ExpertSelector
|
||||
sessionId={sessionId ?? null}
|
||||
onManageExperts={onOpenSettings}
|
||||
@ -69,6 +60,9 @@ export function ChatContainer({
|
||||
/>
|
||||
<ModelSelector sessionId={sessionId ?? null} settingsClosedTick={settingsClosedTick} />
|
||||
</div>
|
||||
);
|
||||
|
||||
const input = (
|
||||
<MessageInput
|
||||
onSend={onSendMessage}
|
||||
onStop={onStop}
|
||||
@ -79,6 +73,49 @@ export function ChatContainer({
|
||||
selectedExpert={selectedExpert}
|
||||
topicId={topicId}
|
||||
/>
|
||||
);
|
||||
|
||||
/* Hero 空状态:空话题 + 非加载 + 可写。
|
||||
注意:composer 保持在组件树中的固定位置(不随分支 remount),
|
||||
避免空→非空切换时丢失未发送草稿。 */
|
||||
const isHero = messages.length === 0 && !isLoading && !isReadOnly;
|
||||
|
||||
return (
|
||||
<div className="relative flex h-full w-full flex-col">
|
||||
{isHero && (
|
||||
<div
|
||||
className="pointer-events-none absolute inset-0"
|
||||
style={{
|
||||
background:
|
||||
'radial-gradient(closest-side at 50% 42%, color-mix(in srgb, var(--accent-cyan) 9%, transparent), transparent)',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{isHero ? (
|
||||
<div className="relative flex flex-1 flex-col items-center justify-center px-6 pb-6">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-2xl border border-[var(--border-color)] bg-[var(--bg-tertiary)]">
|
||||
<Zap className="h-6 w-6 text-[var(--accent-cyan)]" />
|
||||
</div>
|
||||
<h1 className="mt-4 text-[26px] font-semibold text-[var(--text-primary)]">PicoBot</h1>
|
||||
<p className="mt-1 text-sm text-[var(--text-muted)]">有什么可以帮你的?</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="relative flex-1 overflow-hidden">
|
||||
<MessageList
|
||||
messages={messages}
|
||||
onNavigateToSubAgent={onNavigateToSubAgent}
|
||||
showThinking={showThinking}
|
||||
viewKey={viewKey}
|
||||
highlightedMessageId={highlightedMessageId}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="relative shrink-0 px-6 pb-4">
|
||||
<div className={`mx-auto w-full ${isHero ? 'max-w-[680px]' : 'max-w-[736px]'}`}>
|
||||
{selectors}
|
||||
{input}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -256,7 +256,7 @@ export function ExpertSelector({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{error && <span className="text-xs text-red-400 truncate">{error}</span>}
|
||||
{error && <span className="text-xs text-[rgb(242,90,90)] truncate">{error}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,28 +1,23 @@
|
||||
import { useState, useEffect, memo } from 'react';
|
||||
import {
|
||||
User,
|
||||
Bot,
|
||||
Wrench,
|
||||
CheckCircle,
|
||||
AlertCircle,
|
||||
Terminal,
|
||||
File,
|
||||
Image,
|
||||
FileText,
|
||||
Music,
|
||||
Video,
|
||||
Download,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Copy,
|
||||
Check,
|
||||
Loader2,
|
||||
XCircle,
|
||||
ChevronDown,
|
||||
Clock,
|
||||
Loader,
|
||||
X,
|
||||
Loader2,
|
||||
XCircle,
|
||||
Copy,
|
||||
Check,
|
||||
Brain,
|
||||
Maximize2,
|
||||
Download,
|
||||
File,
|
||||
FileText,
|
||||
Image,
|
||||
Music,
|
||||
Video,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
@ -289,7 +284,7 @@ function CopyButton({ text, className = '' }: { text: string; className?: string
|
||||
title="复制"
|
||||
>
|
||||
{copied ? (
|
||||
<Check className="h-3 w-3 text-emerald-400" />
|
||||
<Check className="h-3 w-3 text-[var(--accent-green)]" />
|
||||
) : (
|
||||
<Copy className="h-3 w-3 text-[var(--text-muted)]" />
|
||||
)}
|
||||
@ -385,25 +380,25 @@ export const MessageBubble = memo(function MessageBubble({
|
||||
|
||||
const statusConfig = {
|
||||
calling: {
|
||||
dot: 'bg-amber-400 animate-pulse',
|
||||
fullBorder: 'border-amber-500/30',
|
||||
iconColor: 'text-amber-400',
|
||||
avatarBg: 'bg-amber-500/20',
|
||||
avatarIcon: 'text-amber-400',
|
||||
dot: 'bg-[var(--accent-cyan)] animate-pulse',
|
||||
fullBorder: 'border-[var(--accent-cyan)]/30',
|
||||
iconColor: 'text-[var(--accent-cyan)]',
|
||||
avatarBg: 'bg-[var(--accent-cyan)]/10',
|
||||
avatarIcon: 'text-[var(--accent-cyan)]',
|
||||
},
|
||||
result: {
|
||||
dot: 'bg-emerald-400',
|
||||
fullBorder: 'border-emerald-500/30',
|
||||
iconColor: 'text-emerald-400',
|
||||
avatarBg: 'bg-emerald-500/20',
|
||||
avatarIcon: 'text-emerald-400',
|
||||
dot: 'bg-[var(--accent-green)]',
|
||||
fullBorder: 'border-[var(--accent-green)]/30',
|
||||
iconColor: 'text-[var(--accent-green)]',
|
||||
avatarBg: 'bg-[var(--accent-green)]/10',
|
||||
avatarIcon: 'text-[var(--accent-green)]',
|
||||
},
|
||||
pending: {
|
||||
dot: 'bg-orange-400 animate-pulse',
|
||||
fullBorder: 'border-orange-500/30',
|
||||
iconColor: 'text-orange-400',
|
||||
avatarBg: 'bg-orange-500/20',
|
||||
avatarIcon: 'text-orange-400',
|
||||
dot: 'bg-[var(--accent-amber)] animate-pulse',
|
||||
fullBorder: 'border-[var(--accent-amber)]/30',
|
||||
iconColor: 'text-[var(--accent-amber)]',
|
||||
avatarBg: 'bg-[var(--accent-amber)]/10',
|
||||
avatarIcon: 'text-[var(--accent-amber)]',
|
||||
},
|
||||
}[status];
|
||||
|
||||
@ -450,35 +445,39 @@ export const MessageBubble = memo(function MessageBubble({
|
||||
// - interrupted: 子代理因服务器重启被中断
|
||||
const taskStatusConfig: Record<string, { dot: string; borderColor: string; iconColor: string }> = {
|
||||
running: {
|
||||
dot: 'bg-amber-400 animate-pulse',
|
||||
borderColor: 'border-amber-500/40',
|
||||
iconColor: 'text-amber-400',
|
||||
dot: 'bg-[var(--accent-amber)] animate-pulse',
|
||||
borderColor: 'border-[var(--accent-amber)]/40',
|
||||
iconColor: 'text-[var(--accent-amber)]',
|
||||
},
|
||||
success: {
|
||||
dot: 'bg-emerald-400',
|
||||
borderColor: 'border-emerald-500/40',
|
||||
iconColor: 'text-emerald-400',
|
||||
dot: 'bg-[var(--accent-green)]',
|
||||
borderColor: 'border-[var(--accent-green)]/40',
|
||||
iconColor: 'text-[var(--accent-green)]',
|
||||
},
|
||||
completed: {
|
||||
dot: 'bg-emerald-400',
|
||||
borderColor: 'border-emerald-500/40',
|
||||
iconColor: 'text-emerald-400',
|
||||
dot: 'bg-[var(--accent-green)]',
|
||||
borderColor: 'border-[var(--accent-green)]/40',
|
||||
iconColor: 'text-[var(--accent-green)]',
|
||||
},
|
||||
failed: {
|
||||
dot: 'bg-[rgb(242,90,90)]',
|
||||
borderColor: 'border-[rgb(242,90,90)]/40',
|
||||
iconColor: 'text-[rgb(242,90,90)]',
|
||||
},
|
||||
failed: { dot: 'bg-red-400', borderColor: 'border-red-500/40', iconColor: 'text-red-400' },
|
||||
timeout: {
|
||||
dot: 'bg-amber-400',
|
||||
borderColor: 'border-amber-500/40',
|
||||
iconColor: 'text-amber-400',
|
||||
dot: 'bg-[var(--accent-amber)]',
|
||||
borderColor: 'border-[var(--accent-amber)]/40',
|
||||
iconColor: 'text-[var(--accent-amber)]',
|
||||
},
|
||||
cancelled: {
|
||||
dot: 'bg-zinc-400',
|
||||
borderColor: 'border-zinc-500/40',
|
||||
iconColor: 'text-zinc-400',
|
||||
dot: 'bg-[var(--text-muted)]',
|
||||
borderColor: 'border-[var(--border-color)]',
|
||||
iconColor: 'text-[var(--text-muted)]',
|
||||
},
|
||||
interrupted: {
|
||||
dot: 'bg-orange-400',
|
||||
borderColor: 'border-orange-500/40',
|
||||
iconColor: 'text-orange-400',
|
||||
dot: 'bg-[var(--accent-amber)]',
|
||||
borderColor: 'border-[var(--accent-amber)]/40',
|
||||
iconColor: 'text-[var(--accent-amber)]',
|
||||
},
|
||||
};
|
||||
|
||||
@ -486,35 +485,10 @@ export const MessageBubble = memo(function MessageBubble({
|
||||
const taskStyle = taskResult ? (taskStatusConfig[taskResult.status] ?? taskStatusConfig.failed) : null;
|
||||
|
||||
return (
|
||||
<div data-message-id={message.id} className="flex gap-3 animate-slide-in">
|
||||
<div
|
||||
className={`flex h-7 w-7 shrink-0 items-center justify-center rounded-full mt-0.5 ${
|
||||
isTaskTool ? 'bg-violet-500/20' : statusConfig.avatarBg
|
||||
}`}
|
||||
>
|
||||
{isTaskTool ? (
|
||||
<Bot className="h-3.5 w-3.5 text-violet-400" />
|
||||
) : (
|
||||
<Terminal className={`h-3.5 w-3.5 ${statusConfig.avatarIcon}`} />
|
||||
)}
|
||||
</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-[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}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-xs text-[var(--text-muted)]">
|
||||
{formatTime(message.timestamp)}
|
||||
</span>
|
||||
</div>
|
||||
<div data-message-id={message.id} className="animate-slide-in">
|
||||
<div
|
||||
onClick={() => setToolExpanded(!toolExpanded)}
|
||||
className={`cursor-pointer rounded-xl border bg-[var(--bg-tertiary)]/60 w-full transition-all duration-500 hover:bg-[var(--bg-tertiary)]/80 group ${
|
||||
className={`group w-full cursor-pointer rounded-xl border bg-[var(--bg-secondary)]/60 transition-colors duration-300 hover:bg-[var(--bg-secondary)] ${
|
||||
taskStyle ? taskStyle.borderColor : statusConfig.fullBorder
|
||||
}`}
|
||||
>
|
||||
@ -660,10 +634,10 @@ export const MessageBubble = memo(function MessageBubble({
|
||||
<div
|
||||
className={`rounded-lg px-3 py-2 ${
|
||||
taskResult.status === 'success'
|
||||
? 'bg-emerald-500/10 border border-emerald-500/30'
|
||||
? 'bg-[var(--accent-green)]/10 border border-[var(--accent-green)]/30'
|
||||
: taskResult.status === 'failed'
|
||||
? 'bg-red-500/10 border border-red-500/30'
|
||||
: 'bg-amber-500/10 border border-amber-500/30'
|
||||
? 'bg-[rgb(242,90,90)]/10 border border-[rgb(242,90,90)]/30'
|
||||
: 'bg-[var(--accent-amber)]/10 border border-[var(--accent-amber)]/30'
|
||||
}`}
|
||||
>
|
||||
<div className="text-xs font-medium text-[var(--text-muted)] mb-0.5">
|
||||
@ -743,6 +717,14 @@ export const MessageBubble = memo(function MessageBubble({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-1 flex items-center gap-2 px-1">
|
||||
<span className="text-xs text-[var(--text-muted)]">{message.toolName || 'Tool'}</span>
|
||||
{isTaskTool && (
|
||||
<span className="rounded bg-[var(--accent-purple)]/10 px-1.5 py-0.5 text-xs text-[var(--accent-purple)]">
|
||||
子智能体·{subagentType}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-xs text-[var(--text-muted)]">{formatTime(message.timestamp)}</span>
|
||||
</div>
|
||||
{lightboxElement}
|
||||
{showDetailModal && (
|
||||
@ -776,75 +758,38 @@ export const MessageBubble = memo(function MessageBubble({
|
||||
return null;
|
||||
}
|
||||
|
||||
const getIcon = () => {
|
||||
if (isUser) return <User className="h-4 w-4" />;
|
||||
if (isTool) {
|
||||
if (message.type === 'tool_call') return <Terminal className="h-4 w-4" />;
|
||||
if (message.type === 'tool_result') return <CheckCircle className="h-4 w-4" />;
|
||||
if (message.type === 'tool_pending') return <AlertCircle className="h-4 w-4" />;
|
||||
return <Wrench className="h-4 w-4" />;
|
||||
}
|
||||
return <Bot className="h-4 w-4" />;
|
||||
};
|
||||
|
||||
const getContainerStyles = () => {
|
||||
if (isUser) {
|
||||
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-[var(--bg-hover)] border-[var(--border-color)] text-[var(--text-secondary)]';
|
||||
}
|
||||
return 'bg-[var(--bg-tertiary)] border-[var(--border-color)] text-[var(--text-primary)]';
|
||||
};
|
||||
|
||||
const getAvatarStyles = () => {
|
||||
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-[var(--accent-purple)] to-[#ec4899]';
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
data-message-id={message.id}
|
||||
className={`flex gap-4 ${isUser ? 'flex-row-reverse' : 'flex-row'} animate-slide-in group`}
|
||||
className={`animate-slide-in group ${isUser ? 'flex flex-col items-end' : ''}`}
|
||||
>
|
||||
<div
|
||||
className={`flex h-8 w-8 shrink-0 items-center justify-center rounded-full ${getAvatarStyles()} shadow-lg`}
|
||||
>
|
||||
{getIcon()}
|
||||
</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-[var(--accent-cyan)]' : 'text-[var(--text-secondary)]'}`}
|
||||
>
|
||||
{isUser ? 'You' : isTool ? message.toolName || 'Tool' : 'Assistant'}
|
||||
{!isUser && (
|
||||
<div className="mb-1 flex items-center gap-2">
|
||||
{isTool && message.toolName ? (
|
||||
<span className="text-xs font-medium text-[var(--text-secondary)]">
|
||||
{message.toolName}
|
||||
</span>
|
||||
) : null}
|
||||
<span className="text-xs text-[var(--text-muted)] opacity-0 transition-opacity group-hover:opacity-100">
|
||||
{formatTime(message.timestamp)}
|
||||
</span>
|
||||
<span className="text-xs text-[var(--text-muted)]">{formatTime(message.timestamp)}</span>
|
||||
<CopyButton text={message.content} />
|
||||
</div>
|
||||
<div
|
||||
className={`inline-block rounded-2xl border px-5 py-3 text-left shadow-lg ${getContainerStyles()}`}
|
||||
>
|
||||
{isTool && message.toolName && (
|
||||
<div className="mb-2 text-xs font-semibold opacity-70 flex items-center gap-1">
|
||||
<Terminal className="h-3 w-3" />
|
||||
{message.toolName}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={
|
||||
isUser
|
||||
? 'max-w-[min(525px,82%)] rounded-[22px] bg-[var(--bubble-user)] px-4 py-2.5 text-left'
|
||||
: 'w-full min-w-0'
|
||||
}
|
||||
>
|
||||
{isUser ? (
|
||||
// 用户消息保持纯文本
|
||||
<div className="whitespace-pre-wrap text-sm leading-relaxed">{message.content}</div>
|
||||
// 用户消息保持纯文本(右对齐柔和气泡)
|
||||
<div className="whitespace-pre-wrap text-[15px] leading-6 text-[var(--text-primary)]">
|
||||
{message.content}
|
||||
</div>
|
||||
) : (
|
||||
// 模型思考内容(仅助手消息,非工具消息)
|
||||
<>
|
||||
@ -855,7 +800,7 @@ export const MessageBubble = memo(function MessageBubble({
|
||||
)}
|
||||
{/* AI 和工具消息使用 Markdown 渲染 */}
|
||||
{message.content.trim() && (
|
||||
<div className="markdown-content text-sm leading-relaxed">
|
||||
<div className="markdown-content text-[15px] leading-6">
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
@ -992,7 +937,14 @@ export const MessageBubble = memo(function MessageBubble({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isUser && (
|
||||
<div className="mt-1 flex items-center gap-2 px-1">
|
||||
<span className="text-xs text-[var(--text-muted)] opacity-0 transition-opacity group-hover:opacity-100">
|
||||
{formatTime(message.timestamp)}
|
||||
</span>
|
||||
<CopyButton text={message.content} />
|
||||
</div>
|
||||
)}
|
||||
{lightboxElement}
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -2,7 +2,6 @@ import {
|
||||
Send,
|
||||
Loader2,
|
||||
Square,
|
||||
Sparkles,
|
||||
Eye,
|
||||
Paperclip,
|
||||
X,
|
||||
@ -284,9 +283,8 @@ export function MessageInput({
|
||||
// 只读模式:显示提示占位符
|
||||
if (isReadOnly) {
|
||||
return (
|
||||
<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-[var(--border-color)] bg-[var(--bg-tertiary)]/50 px-4 py-5 text-center">
|
||||
<div className="pt-2">
|
||||
<div className="rounded-2xl border border-[var(--border-color)] bg-[var(--surface-input)] px-4 py-5 text-center shadow-sm">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<div className="flex items-center gap-2 text-[var(--text-secondary)]">
|
||||
<Eye className="h-5 w-5" />
|
||||
@ -303,16 +301,14 @@ export function MessageInput({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<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="pt-2">
|
||||
{/* 错误提示 */}
|
||||
{error && (
|
||||
<div className="mb-2 rounded-lg bg-red-500/10 border border-red-500/20 px-3 py-2 text-sm text-red-400">
|
||||
<div className="mb-2 rounded-lg bg-[rgb(242,90,90)]/10 border border-[rgb(242,90,90)]/25 px-3 py-2 text-sm text-[rgb(242,90,90)]">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
@ -341,7 +337,7 @@ export function MessageInput({
|
||||
</span>
|
||||
<button
|
||||
onClick={() => handleRemoveAttachment(index)}
|
||||
className="text-[var(--text-muted)] hover:text-red-400 transition-colors"
|
||||
className="text-[var(--text-muted)] hover:text-[rgb(242,90,90)] transition-colors"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
@ -350,9 +346,13 @@ export function MessageInput({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 输入区域 */}
|
||||
{/* Composer 卡片 */}
|
||||
<div
|
||||
className={`flex gap-3 items-center relative ${isDragging ? 'ring-2 ring-[var(--accent-cyan)]/50 rounded-xl' : ''}`}
|
||||
className={`relative rounded-2xl border bg-[var(--surface-input)] shadow-sm transition-colors ${
|
||||
isDragging
|
||||
? 'border-[var(--border-accent)]'
|
||||
: 'border-[var(--border-color)] focus-within:border-[var(--border-accent)]'
|
||||
}`}
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDragOver={handleDragOver}
|
||||
@ -360,29 +360,11 @@ export function MessageInput({
|
||||
>
|
||||
{/* 拖拽提示 */}
|
||||
{isDragging && (
|
||||
<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 className="absolute inset-0 z-10 flex items-center justify-center rounded-2xl border border-[var(--border-accent)] bg-[var(--accent-cyan)]/10">
|
||||
<div className="text-sm font-medium text-[var(--accent-cyan)]">拖放文件到这里</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 附件按钮 */}
|
||||
<button
|
||||
onClick={handleAttachClick}
|
||||
disabled={disabled}
|
||||
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>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
multiple
|
||||
className="hidden"
|
||||
onChange={(e) => handleFileSelect(e.target.files)}
|
||||
/>
|
||||
|
||||
{/* 输入框 */}
|
||||
<div className="flex-1 relative flex items-center">
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
value={content}
|
||||
@ -392,40 +374,58 @@ export function MessageInput({
|
||||
placeholder={effectivePlaceholder}
|
||||
disabled={disabled}
|
||||
rows={1}
|
||||
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"
|
||||
className="composer-area w-full resize-none bg-transparent px-4 pt-3 pb-1 text-[15px] leading-6 text-[var(--text-primary)] placeholder:text-[var(--text-muted)] disabled:opacity-50 scrollbar-hide"
|
||||
/>
|
||||
<Sparkles className="absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-[var(--text-muted)] pointer-events-none" />
|
||||
</div>
|
||||
|
||||
{/* 发送/停止按钮 */}
|
||||
{/* 底部操作行 */}
|
||||
<div className="flex items-center gap-1 px-2 pb-2">
|
||||
<button
|
||||
onClick={handleAttachClick}
|
||||
disabled={disabled}
|
||||
title="添加附件"
|
||||
className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg text-[var(--text-muted)] hover:bg-[var(--overlay-hover)] hover:text-[var(--text-primary)] disabled:opacity-50 transition-colors"
|
||||
>
|
||||
<Paperclip className="h-4 w-4" />
|
||||
</button>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
multiple
|
||||
className="hidden"
|
||||
onChange={(e) => handleFileSelect(e.target.files)}
|
||||
/>
|
||||
<div className="flex-1" />
|
||||
{isLoading && onStop ? (
|
||||
<button
|
||||
onClick={onStop}
|
||||
title="停止执行"
|
||||
className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-red-500/10 border border-red-500/20 text-red-400 hover:bg-red-500/20 hover:text-red-300 transition-all"
|
||||
aria-label="Stop generating"
|
||||
className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg border border-[var(--border-color)] text-[var(--text-muted)] hover:bg-[var(--overlay-hover)] hover:text-[rgb(242,90,90)] transition-colors"
|
||||
>
|
||||
<Square className="h-4 w-4" />
|
||||
<Square className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
) : (
|
||||
<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-[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"
|
||||
title="发送"
|
||||
aria-label="Send message"
|
||||
className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-[var(--btn-contrast)] text-[var(--btn-contrast-fg)] hover:opacity-85 disabled:opacity-35 disabled:cursor-not-allowed transition-opacity"
|
||||
>
|
||||
{disabled ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
) : (
|
||||
<Send className="h-4 w-4" />
|
||||
<Send className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 提示 */}
|
||||
<div className="mt-2 text-center text-xs text-[var(--text-muted)]">
|
||||
按 Enter 发送,Shift+Enter 换行 · 支持拖拽/粘贴文件 · 最大 50MB
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -182,25 +182,57 @@ export function MessageList({
|
||||
});
|
||||
}, [highlightedMessageId, virtualizer]);
|
||||
|
||||
// ---- 行高强制重测(修复 tanstack virtual-core 3.17.x 陈旧高度导致行重叠)----
|
||||
// 3.17.x 在滚动状态会跳过同步测量、对缓冲区外的行跳过 RO 更新并复用缓存高度;
|
||||
// 滚动停止后或对话列宽度变化(拖拽调宽/折叠侧栏)时,换行行可能保留 120px 估值导致行重叠。
|
||||
// 在这两个时机绕过门控,直接对已渲染行调用 resizeItem 强制纠正。
|
||||
const remeasureRows = useCallback(() => {
|
||||
const container = containerRef.current;
|
||||
if (!container) return;
|
||||
for (const vi of virtualizer.getVirtualItems()) {
|
||||
const node = container.querySelector<HTMLElement>(`[data-index="${vi.index}"]`);
|
||||
if (node) {
|
||||
const h = Math.round(node.getBoundingClientRect().height);
|
||||
if (h > 0) virtualizer.resizeItem(vi.index, h);
|
||||
}
|
||||
}
|
||||
}, [virtualizer]);
|
||||
|
||||
// 宽度变化后强制重测(含一次 rAF 补测同提交内的重 stamped 行)
|
||||
useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
if (!container || typeof ResizeObserver === 'undefined') return;
|
||||
let lastWidth = container.clientWidth;
|
||||
const ro = new ResizeObserver(() => {
|
||||
const w = container.clientWidth;
|
||||
if (w === lastWidth) return;
|
||||
lastWidth = w;
|
||||
remeasureRows();
|
||||
requestAnimationFrame(remeasureRows);
|
||||
});
|
||||
ro.observe(container);
|
||||
return () => ro.disconnect();
|
||||
}, [virtualizer, remeasureRows]);
|
||||
|
||||
// 滚动停止后补测一次(滚动期间被门控跳过的行在此纠正)
|
||||
const isScrolling = virtualizer.isScrolling;
|
||||
useEffect(() => {
|
||||
if (isScrolling) return;
|
||||
const id = requestAnimationFrame(remeasureRows);
|
||||
return () => cancelAnimationFrame(id);
|
||||
}, [isScrolling, remeasureRows]);
|
||||
|
||||
// ---- empty state ----
|
||||
|
||||
if (messages.length === 0) {
|
||||
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-[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-[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 className="mb-6 inline-flex h-16 w-16 items-center justify-center rounded-2xl border border-[var(--border-color)] bg-[var(--bg-tertiary)]">
|
||||
<Sparkles className="h-8 w-8 text-[var(--accent-cyan)]" />
|
||||
</div>
|
||||
<h2 className="mb-2 text-xl font-semibold text-[var(--text-primary)]">开始新的对话</h2>
|
||||
<p className="text-sm text-[var(--text-muted)]">在下方输入消息开始与 AI 助手聊天</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -237,12 +269,14 @@ export function MessageList({
|
||||
transform: `translateY(${vi.start}px)`,
|
||||
}}
|
||||
>
|
||||
<div className="mx-auto w-full max-w-[736px]">
|
||||
<MessageBubble
|
||||
message={message}
|
||||
onNavigateToSubAgent={onNavigateToSubAgent}
|
||||
showThinking={showThinking}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
@ -256,11 +290,9 @@ export function MessageList({
|
||||
onClick={scrollToTop}
|
||||
className="flex items-center gap-1.5 px-3 py-2 rounded-full
|
||||
bg-[var(--bg-tertiary)]/90 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
|
||||
border border-[var(--border-color)] shadow-sm
|
||||
text-[var(--text-muted)] hover:text-[var(--text-primary)]
|
||||
transition-colors duration-200 ease-out
|
||||
animate-fade-in"
|
||||
aria-label="回到顶部"
|
||||
>
|
||||
@ -273,11 +305,9 @@ export function MessageList({
|
||||
onClick={() => scrollToBottom('smooth')}
|
||||
className="flex items-center gap-2 px-4 py-2 rounded-full
|
||||
bg-[var(--bg-tertiary)]/90 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
|
||||
border border-[var(--border-color)] shadow-sm
|
||||
text-[var(--text-muted)] hover:text-[var(--text-primary)]
|
||||
transition-colors duration-200 ease-out
|
||||
animate-fade-in"
|
||||
aria-label="回到底部"
|
||||
>
|
||||
|
||||
@ -229,7 +229,7 @@ export function ModelSelector({
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{error && <div className="text-xs text-red-400 truncate">{error}</div>}
|
||||
{error && <div className="text-xs text-[rgb(242,90,90)] truncate">{error}</div>}
|
||||
|
||||
<div className="flex items-center justify-between gap-2 pt-1">
|
||||
<button
|
||||
@ -260,7 +260,7 @@ export function ModelSelector({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{error && <span className="text-xs text-red-400 truncate">{error}</span>}
|
||||
{error && <span className="text-xs text-[rgb(242,90,90)] truncate">{error}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -59,7 +59,7 @@ export function ToolDetailModal({
|
||||
: status === 'result'
|
||||
? 'var(--accent-green)'
|
||||
: status === 'pending'
|
||||
? '#f59e0b'
|
||||
? 'var(--accent-amber)'
|
||||
: 'var(--text-muted)';
|
||||
|
||||
return (
|
||||
@ -81,9 +81,9 @@ export function ToolDetailModal({
|
||||
<span
|
||||
className="px-2.5 py-1 rounded-full text-sm font-medium"
|
||||
style={{
|
||||
backgroundColor: `${statusColor}15`,
|
||||
backgroundColor: `color-mix(in srgb, ${statusColor} 12%, transparent)`,
|
||||
color: statusColor,
|
||||
border: `1px solid ${statusColor}30`,
|
||||
border: `1px solid color-mix(in srgb, ${statusColor} 25%, transparent)`,
|
||||
}}
|
||||
>
|
||||
{statusLabel}
|
||||
|
||||
@ -3,34 +3,40 @@ import type { ConnectionStatus } from '../types/protocol';
|
||||
|
||||
interface ConnectionStatusProps {
|
||||
status: ConnectionStatus;
|
||||
/** rail 紧凑模式:仅显示图标 */
|
||||
compact?: boolean;
|
||||
}
|
||||
|
||||
export function ConnectionStatus({ status }: ConnectionStatusProps) {
|
||||
export function ConnectionStatus({ status, compact = false }: ConnectionStatusProps) {
|
||||
const getStatusConfig = () => {
|
||||
switch (status) {
|
||||
case 'connecting':
|
||||
return {
|
||||
icon: <Loader2 className="h-3 w-3 animate-spin" />,
|
||||
icon: <Loader2 className={`animate-spin ${compact ? 'h-4 w-4' : 'h-3 w-3'}`} />,
|
||||
text: '连接中',
|
||||
className: 'text-amber-400 bg-amber-400/10 border-amber-400/30',
|
||||
className:
|
||||
'text-[var(--accent-amber)] bg-[var(--accent-amber)]/10 border-[var(--accent-amber)]/30',
|
||||
};
|
||||
case 'connected':
|
||||
return {
|
||||
icon: <Wifi className="h-3 w-3" />,
|
||||
icon: <Wifi className={compact ? 'h-4 w-4' : 'h-3 w-3'} />,
|
||||
text: '已连接',
|
||||
className: 'text-emerald-400 bg-emerald-400/10 border-emerald-400/30',
|
||||
className:
|
||||
'text-[var(--accent-green)] bg-[var(--accent-green)]/10 border-[var(--accent-green)]/30',
|
||||
};
|
||||
case 'disconnected':
|
||||
return {
|
||||
icon: <WifiOff className="h-3 w-3" />,
|
||||
icon: <WifiOff className={compact ? 'h-4 w-4' : 'h-3 w-3'} />,
|
||||
text: '已断开',
|
||||
className: 'text-zinc-400 bg-zinc-400/10 border-zinc-400/30',
|
||||
className:
|
||||
'text-[var(--text-muted)] bg-[var(--overlay-dim)] border-[var(--border-color)]',
|
||||
};
|
||||
case 'error':
|
||||
return {
|
||||
icon: <WifiOff className="h-3 w-3" />,
|
||||
icon: <WifiOff className={compact ? 'h-4 w-4' : 'h-3 w-3'} />,
|
||||
text: '连接错误',
|
||||
className: 'text-red-400 bg-red-400/10 border-red-400/30',
|
||||
className:
|
||||
'text-[rgb(242,90,90)] bg-[rgb(242,90,90)]/10 border-[rgb(242,90,90)]/30',
|
||||
};
|
||||
}
|
||||
};
|
||||
@ -39,9 +45,21 @@ export function ConnectionStatus({ status }: ConnectionStatusProps) {
|
||||
|
||||
if (!config) return null;
|
||||
|
||||
if (compact) {
|
||||
return (
|
||||
<div
|
||||
className={`flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs border ${config.className}`}
|
||||
className={`flex h-8 w-8 items-center justify-center rounded-lg border ${config.className}`}
|
||||
title={config.text}
|
||||
aria-label={config.text}
|
||||
>
|
||||
{config.icon}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`inline-flex w-fit items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs ${config.className}`}
|
||||
>
|
||||
{config.icon}
|
||||
<span>{config.text}</span>
|
||||
|
||||
@ -110,11 +110,11 @@ export function ChannelSelector({
|
||||
ref={triggerRef}
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className={`
|
||||
group flex items-center gap-2 rounded-lg border px-3 py-1.5 text-sm w-44 justify-between
|
||||
transition-all duration-200 ease-out
|
||||
group flex w-full items-center gap-2 rounded-xl border px-3 py-2 text-sm justify-between
|
||||
transition-colors duration-200 ease-out
|
||||
${
|
||||
isOpen
|
||||
? 'border-[var(--accent-cyan)]/40 bg-[var(--overlay-subtle)] shadow-[0_0_12px_var(--shadow-glow-sm)]'
|
||||
? 'border-[var(--border-accent)] bg-[var(--overlay-subtle)]'
|
||||
: 'border-[var(--border-color)] hover:border-[var(--border-accent)] hover:bg-[var(--overlay-hover)]'
|
||||
}
|
||||
`}
|
||||
@ -128,7 +128,7 @@ export function ChannelSelector({
|
||||
</span>
|
||||
|
||||
{/* Channel name */}
|
||||
<span className="flex-1 min-w-0 text-[var(--text-primary)] font-medium text-xs tracking-wide truncate text-center">
|
||||
<span className="flex-1 min-w-0 text-[var(--text-primary)] text-[13px] truncate text-left">
|
||||
{selected?.name || '选择通道'}
|
||||
</span>
|
||||
|
||||
@ -137,9 +137,7 @@ export function ChannelSelector({
|
||||
{selected ? (
|
||||
<span
|
||||
className={`h-1.5 w-1.5 rounded-full ${
|
||||
selected.isWritable
|
||||
? 'bg-emerald-400 shadow-[0_0_6px_rgba(52,211,153,0.5)]'
|
||||
: 'bg-zinc-500'
|
||||
selected.isWritable ? 'bg-[var(--accent-green)]' : 'bg-[var(--text-muted)]'
|
||||
}`}
|
||||
/>
|
||||
) : (
|
||||
@ -203,7 +201,7 @@ export function ChannelSelector({
|
||||
className={`
|
||||
absolute left-0 top-1/2 -translate-y-1/2 w-0.5 h-5 rounded-r-full
|
||||
transition-all duration-200
|
||||
${isActive ? 'bg-[var(--accent-cyan)] shadow-[0_0_8px_var(--accent-cyan)]' : 'bg-transparent'}
|
||||
${isActive ? 'bg-[var(--accent-cyan)]' : 'bg-transparent'}
|
||||
`}
|
||||
/>
|
||||
|
||||
@ -243,10 +241,9 @@ export function ChannelSelector({
|
||||
transition-all duration-200
|
||||
${
|
||||
channel.isWritable
|
||||
? 'bg-emerald-400/10 text-emerald-400'
|
||||
: 'bg-zinc-500/10 text-zinc-500'
|
||||
? 'bg-[var(--accent-green)]/10 text-[var(--accent-green)]'
|
||||
: 'bg-[var(--overlay-dim)] text-[var(--text-muted)]'
|
||||
}
|
||||
${isActive && channel.isWritable ? 'bg-emerald-400/15' : ''}
|
||||
`}
|
||||
>
|
||||
{channel.isWritable ? (
|
||||
|
||||
@ -80,19 +80,19 @@ export function SessionSelector({
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
disabled={sessions.length <= 1}
|
||||
className={`
|
||||
group flex items-center gap-2 rounded-lg border px-3 py-1.5 text-sm w-44 justify-between
|
||||
transition-all duration-200 ease-out
|
||||
group flex w-full items-center gap-2 rounded-xl border px-3 py-2 text-sm justify-between
|
||||
transition-colors duration-200 ease-out
|
||||
${
|
||||
sessions.length <= 1
|
||||
? 'border-[var(--border-color)] cursor-default'
|
||||
: isOpen
|
||||
? 'border-[var(--accent-cyan)]/40 bg-[var(--overlay-subtle)] shadow-[0_0_12px_var(--shadow-glow-sm)]'
|
||||
? 'border-[var(--border-accent)] bg-[var(--overlay-subtle)]'
|
||||
: 'border-[var(--border-color)] hover:border-[var(--border-accent)] hover:bg-[var(--overlay-hover)] cursor-pointer'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<MessageSquare className="h-3.5 w-3.5 flex-shrink-0 text-[var(--accent-cyan)]" />
|
||||
<span className="flex-1 min-w-0 text-[var(--text-primary)] font-medium text-xs tracking-wide truncate text-center">
|
||||
<span className="flex-1 min-w-0 text-[var(--text-primary)] text-[13px] truncate text-left">
|
||||
{selected?.title || '无会话'}
|
||||
</span>
|
||||
{sessions.length > 1 ? (
|
||||
@ -151,7 +151,7 @@ export function SessionSelector({
|
||||
className={`
|
||||
absolute left-0 top-1/2 -translate-y-1/2 w-0.5 h-5 rounded-r-full
|
||||
transition-all duration-200
|
||||
${isActive ? 'bg-[var(--accent-cyan)] shadow-[0_0_8px_var(--accent-cyan)]' : 'bg-transparent'}
|
||||
${isActive ? 'bg-[var(--accent-cyan)]' : 'bg-transparent'}
|
||||
`}
|
||||
/>
|
||||
<MessageSquare
|
||||
|
||||
@ -42,44 +42,44 @@ const NS: Record<string, NamespaceConfig> = {
|
||||
user: {
|
||||
label: '用户记忆',
|
||||
icon: User,
|
||||
accent: 'text-cyan-400',
|
||||
accentBorder: 'border-cyan-400/40',
|
||||
accent: 'text-[var(--accent-cyan)]',
|
||||
accentBorder: 'border-[var(--accent-cyan)]/40',
|
||||
},
|
||||
semantic: {
|
||||
label: '语义记忆',
|
||||
icon: Library,
|
||||
accent: 'text-amber-400',
|
||||
accentBorder: 'border-amber-400/40',
|
||||
accent: 'text-[var(--accent-amber)]',
|
||||
accentBorder: 'border-[var(--accent-amber)]/40',
|
||||
},
|
||||
episodic: {
|
||||
label: '情景记忆',
|
||||
icon: History,
|
||||
accent: 'text-purple-400',
|
||||
accentBorder: 'border-purple-400/40',
|
||||
accent: 'text-[var(--accent-purple)]',
|
||||
accentBorder: 'border-[var(--accent-purple)]/40',
|
||||
},
|
||||
skill: {
|
||||
label: '技能记忆',
|
||||
icon: Cpu,
|
||||
accent: 'text-green-400',
|
||||
accentBorder: 'border-green-400/40',
|
||||
accent: 'text-[var(--accent-green)]',
|
||||
accentBorder: 'border-[var(--accent-green)]/40',
|
||||
},
|
||||
environment: {
|
||||
label: '环境记忆',
|
||||
icon: Globe,
|
||||
accent: 'text-sky-400',
|
||||
accentBorder: 'border-sky-400/40',
|
||||
accent: 'text-[var(--accent-blue)]',
|
||||
accentBorder: 'border-[var(--accent-blue)]/40',
|
||||
},
|
||||
reflection: {
|
||||
label: '反思记忆',
|
||||
icon: Star,
|
||||
accent: 'text-rose-400',
|
||||
accentBorder: 'border-rose-400/40',
|
||||
accent: 'text-[rgb(242,90,90)]',
|
||||
accentBorder: 'border-[rgb(242,90,90)]/40',
|
||||
},
|
||||
other: {
|
||||
label: '其他',
|
||||
icon: Package,
|
||||
accent: 'text-stone-400',
|
||||
accentBorder: 'border-stone-400/40',
|
||||
accent: 'text-[var(--text-muted)]',
|
||||
accentBorder: 'border-[var(--border-color)]',
|
||||
},
|
||||
};
|
||||
|
||||
@ -159,7 +159,7 @@ function MemoryCard({
|
||||
</button>
|
||||
<button
|
||||
onClick={handleDelete}
|
||||
className={`p-1 rounded hover:bg-red-500/10 ${confirmDelete ? 'text-red-400' : 'text-[var(--text-muted)]'} hover:text-red-400 transition-colors`}
|
||||
className={`p-1 rounded hover:bg-[rgb(242,90,90)]/10 ${confirmDelete ? 'text-[rgb(242,90,90)]' : 'text-[var(--text-muted)]'} hover:text-[rgb(242,90,90)] transition-colors`}
|
||||
title={confirmDelete ? '再次点击确认删除' : '删除'}
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
|
||||
@ -24,12 +24,12 @@ interface SourceConfig {
|
||||
}
|
||||
|
||||
const SOURCE_CONFIG: Record<string, SourceConfig> = {
|
||||
user: { label: '用户技能', icon: User, accent: 'text-cyan-400' },
|
||||
useragent: { label: '用户 Agent', icon: User, accent: 'text-cyan-400' },
|
||||
useropenclaw: { label: '用户 OpenClaw', icon: User, accent: 'text-cyan-400' },
|
||||
project: { label: '项目技能', icon: Folder, accent: 'text-amber-400' },
|
||||
projectagent: { label: '项目 Agent', icon: Folder, accent: 'text-amber-400' },
|
||||
projectopenclaw: { label: '项目 OpenClaw', icon: Folder, accent: 'text-amber-400' },
|
||||
user: { label: '用户技能', icon: User, accent: 'text-[var(--accent-cyan)]' },
|
||||
useragent: { label: '用户 Agent', icon: User, accent: 'text-[var(--accent-cyan)]' },
|
||||
useropenclaw: { label: '用户 OpenClaw', icon: User, accent: 'text-[var(--accent-cyan)]' },
|
||||
project: { label: '项目技能', icon: Folder, accent: 'text-[var(--accent-amber)]' },
|
||||
projectagent: { label: '项目 Agent', icon: Folder, accent: 'text-[var(--accent-amber)]' },
|
||||
projectopenclaw: { label: '项目 OpenClaw', icon: Folder, accent: 'text-[var(--accent-amber)]' },
|
||||
};
|
||||
|
||||
function sourceConfig(source: string): SourceConfig {
|
||||
|
||||
@ -18,9 +18,9 @@ interface StatusCfg {
|
||||
}
|
||||
|
||||
const STATUS: Record<string, StatusCfg> = {
|
||||
in_progress: { label: '进行中', color: 'text-amber-400', dot: 'bg-amber-400' },
|
||||
in_progress: { label: '进行中', color: 'text-[var(--accent-amber)]', dot: 'bg-[var(--accent-amber)]' },
|
||||
pending: { label: '待处理', color: 'text-slate-400', dot: 'bg-slate-500' },
|
||||
completed: { label: '已完成', color: 'text-emerald-400', dot: 'bg-emerald-400' },
|
||||
completed: { label: '已完成', color: 'text-[var(--accent-green)]', dot: 'bg-[var(--accent-green)]' },
|
||||
cancelled: { label: '已取消', color: 'text-slate-500', dot: 'bg-slate-600' },
|
||||
};
|
||||
|
||||
@ -45,8 +45,8 @@ function groupTodos(todos: TodoItemSummary[]): Map<string, TodoItemSummary[]> {
|
||||
function PulseDot() {
|
||||
return (
|
||||
<span className="relative flex h-1.5 w-1.5 shrink-0">
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-amber-400 opacity-75" />
|
||||
<span className="relative inline-flex rounded-full h-1.5 w-1.5 bg-amber-400" />
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-[var(--accent-amber)] opacity-75" />
|
||||
<span className="relative inline-flex rounded-full h-1.5 w-1.5 bg-[var(--accent-amber)]" />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@ -107,29 +107,29 @@ export function ToolPanel({ messages }: ToolPanelProps) {
|
||||
case 'calling':
|
||||
return {
|
||||
icon: Play,
|
||||
iconColor: 'text-amber-400',
|
||||
bgClass: 'bg-amber-400',
|
||||
borderClass: 'border-amber-500/30',
|
||||
iconColor: 'text-[var(--accent-amber)]',
|
||||
bgClass: 'bg-[var(--accent-amber)]',
|
||||
borderClass: 'border-[var(--accent-amber)]/30',
|
||||
label: '执行中',
|
||||
labelClass: 'text-amber-400',
|
||||
labelClass: 'text-[var(--accent-amber)]',
|
||||
};
|
||||
case 'result':
|
||||
return {
|
||||
icon: Check,
|
||||
iconColor: 'text-emerald-400',
|
||||
bgClass: 'bg-emerald-400',
|
||||
borderClass: 'border-emerald-500/30',
|
||||
iconColor: 'text-[var(--accent-green)]',
|
||||
bgClass: 'bg-[var(--accent-green)]',
|
||||
borderClass: 'border-[var(--accent-green)]/30',
|
||||
label: '已完成',
|
||||
labelClass: 'text-emerald-400',
|
||||
labelClass: 'text-[var(--accent-green)]',
|
||||
};
|
||||
case 'pending':
|
||||
return {
|
||||
icon: AlertTriangle,
|
||||
iconColor: 'text-orange-400',
|
||||
bgClass: 'bg-orange-400',
|
||||
borderClass: 'border-orange-500/30',
|
||||
iconColor: 'text-[var(--accent-amber)]',
|
||||
bgClass: 'bg-[var(--accent-amber)]',
|
||||
borderClass: 'border-[var(--accent-amber)]/30',
|
||||
label: '待确认',
|
||||
labelClass: 'text-orange-400',
|
||||
labelClass: 'text-[var(--accent-amber)]',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@ -77,10 +77,10 @@ export function TopicTokenStatsPanel({ tokenStats }: TopicTokenStatsPanelProps)
|
||||
<div
|
||||
className={`h-full rounded-full transition-all ${
|
||||
pct >= 80
|
||||
? 'bg-red-400'
|
||||
? 'bg-[rgb(242,90,90)]'
|
||||
: pct >= 50
|
||||
? 'bg-amber-400'
|
||||
: 'bg-emerald-400'
|
||||
? 'bg-[var(--accent-amber)]'
|
||||
: 'bg-[var(--accent-green)]'
|
||||
}`}
|
||||
style={{ width: `${pct}%` }}
|
||||
/>
|
||||
|
||||
@ -175,7 +175,7 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
||||
if (!config)
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm">
|
||||
<div className="text-red-400 text-center space-y-3">
|
||||
<div className="text-[rgb(242,90,90)] text-center space-y-3">
|
||||
<AlertTriangle className="h-10 w-10 mx-auto" />
|
||||
<p>{error || '加载失败'}</p>
|
||||
<button
|
||||
@ -227,9 +227,9 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm animate-[fadeIn_0.15s_ease-out]">
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm animate-fade-in">
|
||||
<div
|
||||
className="relative flex flex-col w-[92vw] max-w-4xl h-[85vh] rounded-2xl border border-[var(--border-color)] bg-[var(--bg-primary)] shadow-2xl overflow-hidden animate-[scaleIn_0.2s_ease-out]"
|
||||
className="relative flex flex-col w-[92vw] max-w-4xl h-[85vh] rounded-2xl border border-[var(--border-color)] bg-[var(--bg-primary)] shadow-2xl overflow-hidden animate-scale-in"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Header */}
|
||||
@ -237,7 +237,7 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
||||
<Settings className="h-5 w-5 text-[var(--accent-cyan)]" />
|
||||
<span className="text-lg font-semibold text-[var(--text-primary)]">系统配置</span>
|
||||
{dirty && (
|
||||
<span className="text-xs text-amber-400 bg-amber-500/10 px-2 py-0.5 rounded-full">
|
||||
<span className="text-xs text-[var(--accent-amber)] bg-[var(--accent-amber)]/10 px-2 py-0.5 rounded-full">
|
||||
未保存
|
||||
</span>
|
||||
)}
|
||||
@ -267,9 +267,9 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className={`w-full flex items-center gap-2.5 px-4 py-2.5 text-sm transition-all relative ${
|
||||
className={`w-full flex items-center gap-2.5 px-4 py-2.5 text-sm rounded-lg transition-colors relative ${
|
||||
active
|
||||
? 'text-[var(--accent-cyan)] bg-[var(--accent-cyan)]/5'
|
||||
? 'text-[var(--text-primary)] bg-[var(--overlay-subtle)] font-medium'
|
||||
: 'text-[var(--text-muted)] hover:text-[var(--text-secondary)] hover:bg-[var(--overlay-hover)]'
|
||||
}`}
|
||||
>
|
||||
@ -293,7 +293,7 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
||||
|
||||
{/* Footer */}
|
||||
<div className="shrink-0 px-6 py-3 border-t border-[var(--border-color)] bg-[var(--bg-secondary)]/80 backdrop-blur-md flex items-center gap-3">
|
||||
{error && <span className="text-sm text-red-400 truncate max-w-xs">{error}</span>}
|
||||
{error && <span className="text-sm text-[rgb(242,90,90)] truncate max-w-xs">{error}</span>}
|
||||
<div className="flex-1" />
|
||||
<button
|
||||
onClick={handleClose}
|
||||
@ -304,7 +304,7 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={saving || restarting || !dirty}
|
||||
className="flex items-center gap-2 px-5 py-2 rounded-lg text-sm font-medium text-white bg-[var(--accent-cyan)]/20 border border-[var(--accent-cyan)]/30 hover:bg-[var(--accent-cyan)]/30 hover:border-[var(--accent-cyan)]/50 transition-all disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
className="flex items-center gap-2 px-5 py-2 rounded-xl text-sm font-medium text-white bg-[var(--accent-cyan)] hover:opacity-90 transition-opacity disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
>
|
||||
{saving ? <Loader2 className="h-4 w-4 animate-spin" /> : <Save className="h-4 w-4" />}
|
||||
{saving ? '保存中...' : '保存配置'}
|
||||
@ -322,7 +322,7 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
||||
|
||||
{/* Toast */}
|
||||
{toast && (
|
||||
<div className="absolute top-20 left-1/2 -translate-x-1/2 z-10 flex items-center gap-2 px-5 py-3 rounded-xl bg-emerald-500/15 border border-emerald-500/30 text-emerald-400 text-sm shadow-lg backdrop-blur-md animate-[fadeIn_0.2s_ease-out]">
|
||||
<div className="absolute top-20 left-1/2 -translate-x-1/2 z-10 flex items-center gap-2 px-5 py-3 rounded-xl bg-[var(--accent-green)]/15 border border-[var(--accent-green)]/30 text-[var(--accent-green)] text-sm shadow-lg backdrop-blur-md animate-fade-in">
|
||||
{restarting ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
@ -335,7 +335,7 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
||||
{/* Restart confirmation dialog */}
|
||||
{restartDialogMode && (
|
||||
<div className="absolute inset-0 z-20 flex items-center justify-center bg-black/50 backdrop-blur-sm rounded-2xl">
|
||||
<div className="bg-[var(--bg-secondary)] border border-[var(--border-color)] rounded-xl p-6 max-w-sm mx-4 shadow-2xl animate-[scaleIn_0.2s_ease-out]">
|
||||
<div className="bg-[var(--bg-secondary)] border border-[var(--border-color)] rounded-xl p-6 max-w-sm mx-4 shadow-2xl animate-scale-in">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="flex items-center justify-center w-10 h-10 rounded-full bg-[var(--accent-cyan)]/10">
|
||||
<RefreshCw className="h-5 w-5 text-[var(--accent-cyan)]" />
|
||||
@ -362,7 +362,7 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
||||
</button>
|
||||
<button
|
||||
onClick={handleRestart}
|
||||
className="flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium text-white bg-[var(--accent-cyan)]/20 border border-[var(--accent-cyan)]/30 hover:bg-[var(--accent-cyan)]/30 hover:border-[var(--accent-cyan)]/50 transition-all"
|
||||
className="flex items-center gap-2 px-4 py-2 rounded-xl text-sm font-medium text-white bg-[var(--accent-cyan)] hover:opacity-90 transition-opacity"
|
||||
>
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
立即重启
|
||||
|
||||
@ -68,7 +68,7 @@ export const TAB_GROUPS: TabGroup[] = [
|
||||
export const TABS: TabDef[] = TAB_GROUPS.flatMap((g) => g.tabs);
|
||||
|
||||
export const inputCls =
|
||||
'w-full px-3 py-2 rounded-lg bg-[var(--bg-tertiary)] border border-[var(--border-color)] text-[var(--text-primary)] text-sm placeholder:text-[var(--text-muted)] focus:outline-none focus:border-[var(--accent-cyan)] focus:ring-1 focus:ring-[var(--focus-ring)] transition-colors';
|
||||
'w-full px-3 py-2 rounded-xl bg-[var(--surface-input)] border border-[var(--border-color)] text-[var(--text-primary)] text-sm placeholder:text-[var(--text-muted)] focus:outline-none focus:border-[var(--border-accent)] focus:ring-2 focus:ring-[var(--focus-ring)] transition-colors';
|
||||
export const selectCls = inputCls;
|
||||
|
||||
export const TIMEZONE_OPTIONS: { value: string; label: string }[] = [
|
||||
|
||||
@ -145,7 +145,7 @@ export function ExpertModal({ draft, onClose, onSaved, setToast }: Props) {
|
||||
onClick={onClose}
|
||||
>
|
||||
<div
|
||||
className="bg-[var(--bg-secondary)] border border-[var(--border-color)] rounded-xl w-[90%] max-w-3xl mx-4 shadow-2xl animate-[scaleIn_0.2s_ease-out] max-h-[90%] flex flex-col overflow-hidden"
|
||||
className="bg-[var(--bg-secondary)] border border-[var(--border-color)] rounded-xl w-[90%] max-w-3xl mx-4 shadow-2xl animate-scale-in max-h-[90%] flex flex-col overflow-hidden"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<ModalHeader
|
||||
|
||||
@ -173,7 +173,7 @@ export function SubagentModal({ draft, onClose, onSaved, setToast }: Props) {
|
||||
onClick={onClose}
|
||||
>
|
||||
<div
|
||||
className="bg-[var(--bg-secondary)] border border-[var(--border-color)] rounded-xl w-[90%] max-w-3xl mx-4 shadow-2xl animate-[scaleIn_0.2s_ease-out] max-h-[90%] flex flex-col overflow-hidden"
|
||||
className="bg-[var(--bg-secondary)] border border-[var(--border-color)] rounded-xl w-[90%] max-w-3xl mx-4 shadow-2xl animate-scale-in max-h-[90%] flex flex-col overflow-hidden"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<ModalHeader
|
||||
|
||||
@ -30,7 +30,7 @@ export function AddButton({ label, onClick }: { label: string; onClick: () => vo
|
||||
export function ErrorBanner({ children }: { children: ReactNode }) {
|
||||
if (!children) return null;
|
||||
return (
|
||||
<div className="text-sm text-red-400 bg-red-500/10 border border-red-500/20 rounded-lg px-3 py-2">
|
||||
<div className="text-sm text-[rgb(242,90,90)] bg-[rgb(242,90,90)]/10 border border-[rgb(242,90,90)]/25 rounded-lg px-3 py-2">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -138,7 +138,7 @@ export function ExpertsTab({ config, update, setToast }: Props) {
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(expert.name)}
|
||||
className="p-1 rounded text-[var(--text-muted)] hover:text-red-400 hover:bg-red-500/10 transition-colors"
|
||||
className="p-1 rounded text-[var(--text-muted)] hover:text-[rgb(242,90,90)] hover:bg-[rgb(242,90,90)]/10 transition-colors"
|
||||
title="删除"
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
|
||||
@ -84,7 +84,7 @@ export function McpTab({ config, update, setToast }: Props) {
|
||||
</span>
|
||||
</div>
|
||||
{mcpStatus.failed_servers > 0 && (
|
||||
<span className="text-red-400">{mcpStatus.failed_servers} 失败</span>
|
||||
<span className="text-[rgb(242,90,90)]">{mcpStatus.failed_servers} 失败</span>
|
||||
)}
|
||||
<span className="text-[var(--text-muted)]">{mcpStatus.total_tools} 个工具</span>
|
||||
<button
|
||||
@ -111,10 +111,10 @@ export function McpTab({ config, update, setToast }: Props) {
|
||||
</span>
|
||||
) : st.error ? (
|
||||
<span
|
||||
className="inline-flex items-center gap-1 text-xs text-red-400"
|
||||
className="inline-flex items-center gap-1 text-xs text-[rgb(242,90,90)]"
|
||||
title={st.error}
|
||||
>
|
||||
<span className="w-2 h-2 rounded-full bg-red-400" /> 错误
|
||||
<span className="w-2 h-2 rounded-full bg-[rgb(242,90,90)]" /> 错误
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-flex items-center gap-1 text-xs text-gray-400">
|
||||
|
||||
@ -52,8 +52,8 @@ function ToolTag({
|
||||
if (!tools || tools.length === 0) return null;
|
||||
const tagCls =
|
||||
tone === 'allow'
|
||||
? 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400'
|
||||
: 'bg-rose-500/10 text-rose-600 dark:text-rose-400';
|
||||
? 'bg-[var(--accent-green)]/10 text-[var(--accent-green)]'
|
||||
: 'bg-[rgb(242,90,90)]/10 text-[rgb(242,90,90)]';
|
||||
return (
|
||||
<div className="flex items-center gap-1 flex-wrap mt-1">
|
||||
<span className="text-[10px] text-[var(--text-muted)]">{label}:</span>
|
||||
@ -235,7 +235,7 @@ export function SubagentsTab({ config, update, setToast }: Props) {
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(subagent.name)}
|
||||
className="p-1 rounded text-[var(--text-muted)] hover:text-red-400 hover:bg-red-500/10 transition-colors"
|
||||
className="p-1 rounded text-[var(--text-muted)] hover:text-[rgb(242,90,90)] hover:bg-[rgb(242,90,90)]/10 transition-colors"
|
||||
title="删除"
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
|
||||
@ -401,7 +401,7 @@ export function MapEntryHeader({
|
||||
<div className="flex-1" />
|
||||
<button
|
||||
onClick={onDelete}
|
||||
className="p-1 rounded text-red-400/60 hover:text-red-400 hover:bg-red-500/10 transition-colors"
|
||||
className="p-1 rounded text-[rgb(242,90,90)]/60 hover:text-[rgb(242,90,90)] hover:bg-[rgb(242,90,90)]/10 transition-colors"
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
|
||||
@ -23,31 +23,31 @@ function stateBadge(state: string): { label: string; color: string; pulse: boole
|
||||
case 'running':
|
||||
return {
|
||||
label: '执行中',
|
||||
color: 'bg-amber-500/20 text-amber-400 border-amber-500/30',
|
||||
color: 'bg-[var(--accent-amber)]/15 text-[var(--accent-amber)] border-[var(--accent-amber)]/30',
|
||||
pulse: true,
|
||||
};
|
||||
case 'scheduled':
|
||||
return {
|
||||
label: '已调度',
|
||||
color: 'bg-amber-500/15 text-amber-300 border-amber-500/20',
|
||||
color: 'bg-[var(--accent-amber)]/10 text-[var(--accent-amber)] border-[var(--accent-amber)]/20',
|
||||
pulse: false,
|
||||
};
|
||||
case 'paused':
|
||||
return {
|
||||
label: '已暂停',
|
||||
color: 'bg-zinc-500/20 text-zinc-400 border-zinc-500/30',
|
||||
color: 'bg-[var(--overlay-dim)] text-[var(--text-muted)] border-[var(--border-color)]',
|
||||
pulse: false,
|
||||
};
|
||||
case 'completed':
|
||||
return {
|
||||
label: '已完成',
|
||||
color: 'bg-emerald-500/20 text-emerald-400 border-emerald-500/30',
|
||||
color: 'bg-[var(--accent-green)]/15 text-[var(--accent-green)] border-[var(--accent-green)]/30',
|
||||
pulse: false,
|
||||
};
|
||||
default:
|
||||
return {
|
||||
label: state,
|
||||
color: 'bg-zinc-500/20 text-zinc-400 border-zinc-500/30',
|
||||
color: 'bg-[var(--overlay-dim)] text-[var(--text-muted)] border-[var(--border-color)]',
|
||||
pulse: false,
|
||||
};
|
||||
}
|
||||
@ -84,9 +84,9 @@ function scheduleDescription(schedule: unknown): string {
|
||||
function lastStatusIcon(lastStatus: string | undefined) {
|
||||
switch (lastStatus) {
|
||||
case 'ok':
|
||||
return <Check className="h-3 w-3 text-emerald-400" />;
|
||||
return <Check className="h-3 w-3 text-[var(--accent-green)]" />;
|
||||
case 'error':
|
||||
return <X className="h-3 w-3 text-red-400" />;
|
||||
return <X className="h-3 w-3 text-[rgb(242,90,90)]" />;
|
||||
default:
|
||||
return <Minus className="h-3 w-3 text-[var(--text-muted)]" />;
|
||||
}
|
||||
@ -157,7 +157,7 @@ export function SchedulerJobList({ jobs, onRefresh, onViewJob, sessionId }: Sche
|
||||
</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'}`}
|
||||
className={`inline-block h-1.5 w-1.5 rounded-full ${job.enabled ? 'bg-[var(--accent-green)]' : 'bg-[var(--text-muted)]'}`}
|
||||
/>
|
||||
{hasLookup && (
|
||||
<ChevronRight className="h-3.5 w-3.5 text-[var(--text-muted)]" />
|
||||
@ -171,7 +171,7 @@ export function SchedulerJobList({ jobs, onRefresh, onViewJob, sessionId }: Sche
|
||||
className={`inline-flex items-center gap-1 text-xs px-1.5 py-0.5 rounded border ${badge.color}`}
|
||||
>
|
||||
{badge.pulse && (
|
||||
<span className="inline-block h-1.5 w-1.5 rounded-full bg-amber-400 animate-pulse" />
|
||||
<span className="inline-block h-1.5 w-1.5 rounded-full bg-[var(--accent-amber)] animate-pulse" />
|
||||
)}
|
||||
{badge.label}
|
||||
</span>
|
||||
@ -180,9 +180,9 @@ export function SchedulerJobList({ jobs, onRefresh, onViewJob, sessionId }: Sche
|
||||
<span
|
||||
className={
|
||||
job.last_status === 'error'
|
||||
? 'text-red-400'
|
||||
? 'text-[rgb(242,90,90)]'
|
||||
: job.last_status === 'ok'
|
||||
? 'text-emerald-400'
|
||||
? 'text-[var(--accent-green)]'
|
||||
: 'text-[var(--text-muted)]'
|
||||
}
|
||||
>
|
||||
@ -199,7 +199,7 @@ export function SchedulerJobList({ jobs, onRefresh, onViewJob, sessionId }: Sche
|
||||
|
||||
{/* Row 3: Error message (if any) */}
|
||||
{job.last_error && (
|
||||
<div className="mb-1.5 text-xs text-red-400/80 truncate bg-red-500/10 rounded px-1.5 py-0.5">
|
||||
<div className="mb-1.5 text-xs text-[rgb(242,90,90)] truncate bg-[rgb(242,90,90)]/10 rounded px-1.5 py-0.5">
|
||||
{job.last_error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -12,7 +12,7 @@ export function SessionInfo({ session, connectionId }: SessionInfoProps) {
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<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 className="text-xs px-1.5 py-0.5 rounded bg-[var(--accent-green)]/15 text-[var(--accent-green)]">
|
||||
在线
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@ -158,10 +158,10 @@ export function TopicList({
|
||||
<button
|
||||
onClick={onCreateTopic}
|
||||
disabled={!sessionId}
|
||||
className={`flex items-center gap-1 rounded-lg px-3 py-1.5 text-sm transition-all ${
|
||||
className={`flex items-center gap-1 rounded-lg px-3 py-1.5 text-sm transition-colors ${
|
||||
!sessionId
|
||||
? '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'
|
||||
: 'bg-[var(--accent-cyan)] text-white hover:opacity-90'
|
||||
}`}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
@ -215,7 +215,7 @@ export function TopicList({
|
||||
<button
|
||||
type="submit"
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
className="flex items-center justify-center h-6 w-6 rounded-md bg-emerald-500/20 text-emerald-400 hover:bg-emerald-500/30 transition-colors shrink-0"
|
||||
className="flex items-center justify-center h-6 w-6 rounded-md bg-[var(--accent-green)]/15 text-[var(--accent-green)] hover:bg-[var(--accent-green)]/25 transition-colors shrink-0"
|
||||
title="确认 (Enter)"
|
||||
>
|
||||
<Check className="h-3.5 w-3.5" />
|
||||
@ -224,7 +224,7 @@ export function TopicList({
|
||||
type="button"
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={cancelEdit}
|
||||
className="flex items-center justify-center h-6 w-6 rounded-md bg-zinc-500/20 text-zinc-400 hover:bg-zinc-500/30 transition-colors shrink-0"
|
||||
className="flex items-center justify-center h-6 w-6 rounded-md bg-[var(--overlay-subtle)] text-[var(--text-muted)] hover:bg-[var(--overlay-medium)] transition-colors shrink-0"
|
||||
title="取消 (Esc)"
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
@ -234,21 +234,21 @@ export function TopicList({
|
||||
<>
|
||||
<button
|
||||
onClick={() => onSwitchTopic(topic.id)}
|
||||
className={`w-full rounded-xl pl-3 pr-8 py-3 text-left text-sm transition-all ${
|
||||
className={`w-full rounded-xl pl-3 pr-8 py-3 text-left text-sm transition-colors ${
|
||||
topic.id === currentTopicId
|
||||
? 'bg-gradient-to-r from-[var(--accent-cyan)]/20 to-transparent border border-[var(--accent-cyan)]/30'
|
||||
? 'bg-[var(--overlay-subtle)] border border-[var(--border-color)]'
|
||||
: 'hover:bg-[var(--overlay-hover)] border border-transparent'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="mt-0.5 text-xs text-[var(--text-muted)] font-mono w-4">
|
||||
<span className="mt-0.5 text-xs text-[var(--text-muted)] w-4">
|
||||
{currentPage * pageSize + index + 1}
|
||||
</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div
|
||||
className={`truncate font-medium ${
|
||||
topic.id === currentTopicId
|
||||
? 'text-[var(--accent-cyan)]'
|
||||
? 'text-[var(--text-primary)]'
|
||||
: 'text-[var(--text-secondary)]'
|
||||
}`}
|
||||
>
|
||||
@ -266,7 +266,7 @@ export function TopicList({
|
||||
</div>
|
||||
</div>
|
||||
{topic.id === currentTopicId && (
|
||||
<span className="inline-block h-2 w-2 rounded-full bg-[var(--accent-cyan)] shadow-lg shadow-[var(--shadow-glow-soft)] mt-1.5" />
|
||||
<span className="inline-block h-2 w-2 rounded-full bg-[var(--accent-cyan)] mt-1.5" />
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
@ -275,14 +275,16 @@ export function TopicList({
|
||||
<div className="absolute top-2.5 right-2.5">
|
||||
{confirmDeleteId === topic.id ? (
|
||||
<span className="flex items-center gap-1.5 rounded-lg bg-[var(--bg-tertiary)] border border-[var(--border-color)] px-2 py-1 shadow-lg animate-scale-in">
|
||||
<span className="text-xs text-red-400 whitespace-nowrap">确认删除?</span>
|
||||
<span className="text-xs text-[rgb(242,90,90)] whitespace-nowrap">
|
||||
确认删除?
|
||||
</span>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDeleteTopic(topic.id);
|
||||
setConfirmDeleteId(null);
|
||||
}}
|
||||
className="flex items-center justify-center h-5 w-5 rounded bg-emerald-500/20 text-emerald-400 hover:bg-emerald-500/30 transition-colors"
|
||||
className="flex items-center justify-center h-5 w-5 rounded bg-[var(--accent-green)]/15 text-[var(--accent-green)] hover:bg-[var(--accent-green)]/25 transition-colors"
|
||||
title="确认"
|
||||
>
|
||||
<Check className="h-3 w-3" />
|
||||
@ -292,7 +294,7 @@ export function TopicList({
|
||||
e.stopPropagation();
|
||||
setConfirmDeleteId(null);
|
||||
}}
|
||||
className="flex items-center justify-center h-5 w-5 rounded bg-zinc-500/20 text-zinc-400 hover:bg-zinc-500/30 transition-colors"
|
||||
className="flex items-center justify-center h-5 w-5 rounded bg-[var(--overlay-subtle)] text-[var(--text-muted)] hover:bg-[var(--overlay-medium)] transition-colors"
|
||||
title="取消"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
@ -305,7 +307,7 @@ export function TopicList({
|
||||
e.stopPropagation();
|
||||
startEdit(topic);
|
||||
}}
|
||||
className="flex items-center justify-center h-6 w-6 rounded-md text-[var(--text-muted)] hover:text-[var(--accent-cyan)] hover:bg-[var(--accent-cyan)]/10 transition-colors"
|
||||
className="flex items-center justify-center h-6 w-6 rounded-md text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--overlay-hover)] transition-colors"
|
||||
title="重命名话题"
|
||||
>
|
||||
<Edit2 className="h-3.5 w-3.5" />
|
||||
@ -315,7 +317,7 @@ export function TopicList({
|
||||
e.stopPropagation();
|
||||
setConfirmDeleteId(topic.id);
|
||||
}}
|
||||
className="flex items-center justify-center h-6 w-6 rounded-md text-[var(--text-muted)] hover:text-red-400 hover:bg-red-500/10 transition-colors"
|
||||
className="flex items-center justify-center h-6 w-6 rounded-md text-[var(--text-muted)] hover:text-[rgb(242,90,90)] hover:bg-[rgb(242,90,90)]/10 transition-colors"
|
||||
title="删除话题"
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
|
||||
@ -2,54 +2,62 @@
|
||||
|
||||
/* ============================================
|
||||
PicoBot Theme — Dark (default)
|
||||
对齐 deepseek-harness 设计语言
|
||||
(--accent-cyan 语义为品牌主色 DeepSeek blue)
|
||||
============================================ */
|
||||
:root {
|
||||
/* Core colors */
|
||||
--bg-primary: #0a0a0f;
|
||||
--bg-secondary: #12121a;
|
||||
--bg-tertiary: #1a1a25;
|
||||
--bg-hover: #252535;
|
||||
--bg-primary: rgb(35, 35, 36);
|
||||
--bg-secondary: rgb(27, 27, 28);
|
||||
--bg-tertiary: rgb(44, 44, 46);
|
||||
--bg-hover: rgba(255, 255, 255, 0.08);
|
||||
|
||||
/* Accent colors */
|
||||
--accent-cyan: #00f0ff;
|
||||
--accent-blue: #3b82f6;
|
||||
--accent-purple: #8b5cf6;
|
||||
--accent-green: #10b981;
|
||||
--accent-amber: #f59e0b;
|
||||
--accent-cyan: rgb(103, 158, 254);
|
||||
--accent-blue: rgb(96, 165, 250);
|
||||
--accent-purple: rgb(183, 200, 254);
|
||||
--accent-green: rgb(78, 209, 126);
|
||||
--accent-amber: rgb(247, 173, 49);
|
||||
|
||||
/* Text colors */
|
||||
--text-primary: #ffffff;
|
||||
--text-secondary: #a1a1aa;
|
||||
--text-muted: #71717a;
|
||||
--text-primary: rgb(249, 250, 251);
|
||||
--text-secondary: rgb(207, 211, 214);
|
||||
--text-muted: rgb(151, 157, 166);
|
||||
--text-accent: var(--accent-cyan);
|
||||
|
||||
/* Border */
|
||||
--border-color: rgba(255, 255, 255, 0.08);
|
||||
--border-accent: rgba(0, 240, 255, 0.3);
|
||||
--border-color: rgba(255, 255, 255, 0.12);
|
||||
--border-accent: rgba(103, 158, 254, 0.4);
|
||||
|
||||
/* Overlay — for hover / subtle surface effects */
|
||||
--overlay-hover: rgba(255, 255, 255, 0.05);
|
||||
--overlay-subtle: rgba(255, 255, 255, 0.1);
|
||||
--overlay-medium: rgba(255, 255, 255, 0.2);
|
||||
--overlay-hover: rgba(255, 255, 255, 0.08);
|
||||
--overlay-subtle: rgba(255, 255, 255, 0.14);
|
||||
--overlay-medium: rgba(255, 255, 255, 0.24);
|
||||
--overlay-dim: rgba(0, 0, 0, 0.2);
|
||||
--overlay-dim-strong: rgba(0, 0, 0, 0.3);
|
||||
--overlay-dim-heavy: rgba(0, 0, 0, 0.4);
|
||||
--overlay-code: rgba(0, 0, 0, 0.3);
|
||||
|
||||
/* Shadows */
|
||||
--shadow-glow-sm: rgba(0, 240, 255, 0.12);
|
||||
--shadow-glow: rgba(0, 240, 255, 0.2);
|
||||
--shadow-glow-strong: rgba(0, 240, 255, 0.3);
|
||||
--shadow-glow-soft: rgba(0, 240, 255, 0.5);
|
||||
--shadow-glow-sm: rgba(103, 158, 254, 0.1);
|
||||
--shadow-glow: rgba(103, 158, 254, 0.16);
|
||||
--shadow-glow-strong: rgba(103, 158, 254, 0.24);
|
||||
--shadow-glow-soft: rgba(103, 158, 254, 0.32);
|
||||
|
||||
/* Focus ring */
|
||||
--focus-ring: rgba(0, 240, 255, 0.2);
|
||||
--focus-ring: rgba(103, 158, 254, 0.3);
|
||||
|
||||
/* Selection / code highlight */
|
||||
--selection-bg: rgba(0, 240, 255, 0.3);
|
||||
--selection-bg: rgba(103, 158, 254, 0.28);
|
||||
|
||||
/* Divider */
|
||||
--divider-color: rgba(255, 255, 255, 0.2);
|
||||
--divider-color: rgba(255, 255, 255, 0.08);
|
||||
|
||||
/* Chat composer / user bubble surfaces */
|
||||
--bubble-user: rgb(44, 44, 46);
|
||||
--surface-input: rgb(44, 44, 46);
|
||||
--btn-contrast: rgb(249, 250, 251);
|
||||
--btn-contrast-fg: rgb(27, 27, 28);
|
||||
|
||||
color-scheme: dark;
|
||||
}
|
||||
@ -59,51 +67,57 @@
|
||||
============================================ */
|
||||
html.light {
|
||||
/* Backgrounds */
|
||||
--bg-primary: #f5f5f7;
|
||||
--bg-secondary: #ffffff;
|
||||
--bg-tertiary: #ebecf0;
|
||||
--bg-hover: #dfe0e5;
|
||||
--bg-primary: rgb(255, 255, 255);
|
||||
--bg-secondary: rgb(249, 250, 251);
|
||||
--bg-tertiary: rgb(241, 243, 245);
|
||||
--bg-hover: rgba(38, 49, 72, 0.06);
|
||||
|
||||
/* Accents — slightly darker for readability on white */
|
||||
--accent-cyan: #008899;
|
||||
--accent-blue: #2563eb;
|
||||
--accent-purple: #7c3aed;
|
||||
--accent-green: #059669;
|
||||
--accent-amber: #d97706;
|
||||
/* Accents */
|
||||
--accent-cyan: rgb(65, 118, 230);
|
||||
--accent-blue: rgb(59, 130, 246);
|
||||
--accent-purple: rgb(65, 118, 230);
|
||||
--accent-green: rgb(34, 197, 94);
|
||||
--accent-amber: rgb(245, 158, 11);
|
||||
|
||||
/* Text */
|
||||
--text-primary: #1a1a2e;
|
||||
--text-secondary: #52525b;
|
||||
--text-muted: #a1a1aa;
|
||||
--text-primary: rgb(15, 17, 21);
|
||||
--text-secondary: rgb(97, 102, 107);
|
||||
--text-muted: rgb(151, 157, 166);
|
||||
--text-accent: var(--accent-cyan);
|
||||
|
||||
/* Borders */
|
||||
--border-color: rgba(0, 0, 0, 0.08);
|
||||
--border-accent: rgba(0, 136, 153, 0.3);
|
||||
--border-color: rgba(0, 0, 0, 0.1);
|
||||
--border-accent: rgba(65, 118, 230, 0.4);
|
||||
|
||||
/* Overlays */
|
||||
--overlay-hover: rgba(0, 0, 0, 0.04);
|
||||
--overlay-subtle: rgba(0, 0, 0, 0.06);
|
||||
--overlay-medium: rgba(0, 0, 0, 0.1);
|
||||
--overlay-hover: rgba(38, 49, 72, 0.06);
|
||||
--overlay-subtle: rgba(38, 49, 72, 0.1);
|
||||
--overlay-medium: rgba(38, 49, 72, 0.16);
|
||||
--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.1);
|
||||
--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);
|
||||
--shadow-glow-sm: rgba(65, 118, 230, 0.08);
|
||||
--shadow-glow: rgba(65, 118, 230, 0.12);
|
||||
--shadow-glow-strong: rgba(65, 118, 230, 0.18);
|
||||
--shadow-glow-soft: rgba(65, 118, 230, 0.25);
|
||||
|
||||
/* Focus ring */
|
||||
--focus-ring: rgba(0, 136, 153, 0.2);
|
||||
--focus-ring: rgba(65, 118, 230, 0.25);
|
||||
|
||||
/* Selection / code highlight */
|
||||
--selection-bg: rgba(0, 136, 153, 0.2);
|
||||
--selection-bg: rgba(65, 118, 230, 0.2);
|
||||
|
||||
/* Divider */
|
||||
--divider-color: rgba(0, 0, 0, 0.1);
|
||||
--divider-color: rgba(0, 0, 0, 0.08);
|
||||
|
||||
/* Chat composer / user bubble surfaces */
|
||||
--bubble-user: rgb(237, 243, 254);
|
||||
--surface-input: rgb(255, 255, 255);
|
||||
--btn-contrast: rgb(67, 69, 74);
|
||||
--btn-contrast-fg: rgb(255, 255, 255);
|
||||
|
||||
color-scheme: light;
|
||||
}
|
||||
@ -122,6 +136,11 @@ html.theme-transitioning *::after {
|
||||
text-shadow 0.3s ease !important;
|
||||
}
|
||||
|
||||
/* 虚拟列表行使用内联 transform 定位,主题过渡期间禁止对其做 transition,避免行位移被动画化 */
|
||||
html.theme-transitioning [data-index] {
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Custom scrollbar
|
||||
============================================ */
|
||||
@ -135,12 +154,20 @@ html.theme-transitioning *::after {
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--bg-tertiary);
|
||||
background: rgb(101, 103, 107);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--accent-blue);
|
||||
background: rgb(129, 133, 140);
|
||||
}
|
||||
|
||||
html.light ::-webkit-scrollbar-thumb {
|
||||
background: rgb(229, 229, 229);
|
||||
}
|
||||
|
||||
html.light ::-webkit-scrollbar-thumb:hover {
|
||||
background: rgb(212, 212, 212);
|
||||
}
|
||||
|
||||
/* Hide scrollbar utility */
|
||||
@ -163,12 +190,15 @@ html.theme-transitioning *::after {
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: 'SF Mono', 'Fira Code', 'JetBrains Mono', monospace;
|
||||
font-family:
|
||||
-apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Segoe UI', Roboto,
|
||||
'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
background-color: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
line-height: 1.6;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
@ -180,13 +210,10 @@ body {
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Glowing text effect
|
||||
Glowing text effect(DeepSeek 风格下仅保留品牌色,无发光)
|
||||
============================================ */
|
||||
.glow-text {
|
||||
text-shadow:
|
||||
0 0 10px var(--shadow-glow-soft),
|
||||
0 0 20px var(--shadow-glow-strong),
|
||||
0 0 30px var(--shadow-glow-sm);
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
@ -202,7 +229,7 @@ body {
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
padding: 1px;
|
||||
background: linear-gradient(135deg, var(--accent-cyan), var(--accent-purple));
|
||||
background: var(--accent-cyan);
|
||||
-webkit-mask:
|
||||
linear-gradient(#fff 0 0) content-box,
|
||||
linear-gradient(#fff 0 0);
|
||||
@ -219,16 +246,10 @@ body {
|
||||
@keyframes pulse-glow {
|
||||
0%,
|
||||
100% {
|
||||
box-shadow:
|
||||
0 0 5px var(--accent-cyan),
|
||||
0 0 10px var(--accent-cyan),
|
||||
0 0 20px var(--accent-cyan);
|
||||
box-shadow: 0 0 0 1px var(--shadow-glow);
|
||||
}
|
||||
50% {
|
||||
box-shadow:
|
||||
0 0 10px var(--accent-cyan),
|
||||
0 0 20px var(--accent-cyan),
|
||||
0 0 40px var(--accent-cyan);
|
||||
box-shadow: 0 0 0 3px var(--shadow-glow);
|
||||
}
|
||||
}
|
||||
|
||||
@ -330,7 +351,7 @@ body {
|
||||
background-color: transparent;
|
||||
}
|
||||
50% {
|
||||
background-color: rgba(0, 240, 255, 0.15);
|
||||
background-color: color-mix(in srgb, var(--accent-cyan) 15%, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
@ -405,9 +426,9 @@ body {
|
||||
Code block styling
|
||||
============================================ */
|
||||
pre {
|
||||
background: var(--bg-tertiary);
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
@ -427,6 +448,12 @@ textarea:focus {
|
||||
box-shadow: 0 0 0 2px var(--focus-ring);
|
||||
}
|
||||
|
||||
/* Composer 内的无边框 textarea:聚焦高亮由外层卡片 focus-within 提供 */
|
||||
textarea.composer-area:focus {
|
||||
border-color: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Button hover effects
|
||||
============================================ */
|
||||
@ -434,14 +461,6 @@ button {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
button:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
button:active:not(:disabled) {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Link styles
|
||||
============================================ */
|
||||
@ -452,5 +471,5 @@ a {
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-shadow: 0 0 8px var(--accent-cyan);
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
@ -17,7 +17,7 @@ export function contextOccupancyPct(stats: TopicTokenStats): number | null {
|
||||
|
||||
/** 根据占用率返回颜色 class */
|
||||
export function occupancyColor(pct: number): string {
|
||||
if (pct >= 80) return 'text-red-400';
|
||||
if (pct >= 50) return 'text-amber-400';
|
||||
return 'text-emerald-400';
|
||||
if (pct >= 80) return 'text-[rgb(242,90,90)]';
|
||||
if (pct >= 50) return 'text-[var(--accent-amber)]';
|
||||
return 'text-[var(--accent-green)]';
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user