feat(desktop): DeerFlow visual redesign + stream hang fix + intelligence client
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled

DeerFlow frontend visual overhaul:
- Card-style input box (white rounded card, textarea top, actions bottom)
- Dropdown mode selector (闪速/思考/Pro/Ultra with icons+descriptions)
- Colored quick-action chips (小惊喜/写作/研究/收集/学习)
- Minimal top bar (title + token count + export)
- Warm gray color system (#faf9f6 bg, #f5f4f1 sidebar, #e8e6e1 border)
- DeerFlow-style sidebar (新对话/对话/智能体 nav)
- Reasoning block, tool call chain, task progress visualization
- Streaming text, model selector, suggestion chips components
- Resizable artifact panel with drag handle
- Virtualized message list for 100+ messages

Bug fixes:
- Stream hang: GatewayClient onclose code 1000 now calls onComplete
- WebView2 textarea border: CSS !important override for UA styles
- Gateway stream event handling (response/phase/tool_call types)

Intelligence client:
- Unified client with fallback drivers (compactor/heartbeat/identity/memory/reflection)
- Gateway API types and type conversions
This commit is contained in:
iven
2026-04-01 22:03:07 +08:00
parent e3b93ff96d
commit 73ff5e8c5e
43 changed files with 4817 additions and 905 deletions

View File

@@ -0,0 +1,136 @@
import { useCallback, type ReactNode } from 'react';
import { Group, Panel, Separator } from 'react-resizable-panels';
import { X, PanelRightOpen, PanelRightClose } from 'lucide-react';
/**
* Resizable dual-panel layout for chat + artifact/detail panel.
*
* Uses react-resizable-panels v4 API:
* - Left panel: Chat area (always visible)
* - Right panel: Artifact/detail viewer (collapsible)
* - Draggable resize handle between panels
* - Persisted panel sizes via localStorage
*/
interface ResizableChatLayoutProps {
chatPanel: ReactNode;
rightPanel?: ReactNode;
rightPanelTitle?: string;
rightPanelOpen?: boolean;
onRightPanelToggle?: (open: boolean) => void;
}
const STORAGE_KEY = 'zclaw-layout-panels';
const LEFT_PANEL_ID = 'chat-panel';
const RIGHT_PANEL_ID = 'detail-panel';
function loadPanelSizes(): { left: string; right: string } {
try {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) {
const parsed = JSON.parse(stored);
if (parsed.left && parsed.right) {
return { left: parsed.left, right: parsed.right };
}
}
} catch { /* ignore */ }
return { left: '65%', right: '35%' };
}
function savePanelSizes(layout: Record<string, number>) {
try {
const left = layout[LEFT_PANEL_ID];
const right = layout[RIGHT_PANEL_ID];
if (left !== undefined && right !== undefined) {
localStorage.setItem(STORAGE_KEY, JSON.stringify({ left, right }));
}
} catch { /* ignore */ }
}
export function ResizableChatLayout({
chatPanel,
rightPanel,
rightPanelTitle = '详情',
rightPanelOpen = false,
onRightPanelToggle,
}: ResizableChatLayoutProps) {
const sizes = loadPanelSizes();
const handleToggle = useCallback(() => {
onRightPanelToggle?.(!rightPanelOpen);
}, [rightPanelOpen, onRightPanelToggle]);
if (!rightPanelOpen || !rightPanel) {
return (
<div className="flex-1 flex flex-col overflow-hidden relative">
{chatPanel}
<button
onClick={handleToggle}
className="absolute top-3 right-3 z-10 p-1.5 rounded-md bg-white/80 dark:bg-gray-800/80 border border-gray-200 dark:border-gray-700 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 hover:bg-white dark:hover:bg-gray-800 transition-colors shadow-sm"
title="打开侧面板"
>
<PanelRightOpen className="w-4 h-4" />
</button>
</div>
);
}
return (
<div className="flex-1 flex flex-col overflow-hidden">
<Group
orientation="horizontal"
onLayoutChanged={(layout) => savePanelSizes(layout)}
>
{/* Left panel: Chat */}
<Panel
id={LEFT_PANEL_ID}
defaultSize={sizes.left}
minSize="40%"
>
<div className="h-full flex flex-col relative">
{chatPanel}
<button
onClick={handleToggle}
className="absolute top-3 right-3 z-10 p-1.5 rounded-md bg-white/80 dark:bg-gray-800/80 border border-gray-200 dark:border-gray-700 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 hover:bg-white dark:hover:bg-gray-800 transition-colors shadow-sm"
title="关闭侧面板"
>
<PanelRightClose className="w-4 h-4" />
</button>
</div>
</Panel>
{/* Resize handle */}
<Separator className="w-1.5 flex items-center justify-center group cursor-col-resize hover:bg-orange-100 dark:hover:bg-orange-900/20 transition-colors">
<div className="w-0.5 h-8 rounded-full bg-gray-300 dark:bg-gray-600 group-hover:bg-orange-400 dark:group-hover:bg-orange-500 transition-colors" />
</Separator>
{/* Right panel: Artifact/Detail */}
<Panel
id={RIGHT_PANEL_ID}
defaultSize={sizes.right}
minSize="25%"
>
<div className="h-full flex flex-col bg-gray-50 dark:bg-gray-900 border-l border-gray-200 dark:border-gray-800">
{/* Panel header */}
<div className="h-12 flex items-center justify-between px-4 border-b border-gray-200 dark:border-gray-800 flex-shrink-0">
<span className="text-xs font-medium text-gray-600 dark:text-gray-400 uppercase tracking-wide">
{rightPanelTitle}
</span>
<button
onClick={handleToggle}
className="p-1 rounded text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 transition-colors"
title="关闭面板"
>
<X className="w-4 h-4" />
</button>
</div>
{/* Panel content */}
<div className="flex-1 overflow-y-auto custom-scrollbar">
{rightPanel}
</div>
</div>
</Panel>
</Group>
</div>
);
}