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,117 @@
import { useRef, useEffect, useState, createContext, useContext, useMemo, type ReactNode } from 'react';
// ---------------------------------------------------------------------------
// ConversationContext — shared state for child ai-elements components
// ---------------------------------------------------------------------------
interface ConversationContextValue {
isStreaming: boolean;
setIsStreaming: (v: boolean) => void;
messages: unknown[];
setMessages: (msgs: unknown[]) => void;
}
const ConversationContext = createContext<ConversationContextValue | null>(null);
export function useConversationContext() {
const ctx = useContext(ConversationContext);
if (!ctx) {
throw new Error('useConversationContext must be used within ConversationProvider');
}
return ctx;
}
export function ConversationProvider({ children }: { children: ReactNode }) {
const [isStreaming, setIsStreaming] = useState(false);
const [messages, setMessages] = useState<unknown[]>([]);
const value = useMemo(
() => ({ isStreaming, setIsStreaming, messages, setMessages }),
[isStreaming, messages],
);
return (
<ConversationContext.Provider value={value}>
{children}
</ConversationContext.Provider>
);
}
// ---------------------------------------------------------------------------
// Conversation container with auto-stick-to-bottom scroll behavior
// ---------------------------------------------------------------------------
/**
* Conversation container with auto-stick-to-bottom scroll behavior.
*
* Inspired by DeerFlow's use-stick-to-bottom pattern:
* - Stays pinned to bottom during streaming
* - Remembers user's scroll position when they scroll up
* - Auto-scrolls back to bottom on new content when near the bottom
*/
interface ConversationProps {
children: ReactNode;
className?: string;
}
const SCROLL_THRESHOLD = 80; // px from bottom to consider "at bottom"
export function Conversation({ children, className = '' }: ConversationProps) {
const containerRef = useRef<HTMLDivElement>(null);
const isAtBottomRef = useRef(true);
const observerRef = useRef<ResizeObserver | null>(null);
// Track whether user is near the bottom
const handleScroll = () => {
const el = containerRef.current;
if (!el) return;
const distanceFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight;
isAtBottomRef.current = distanceFromBottom < SCROLL_THRESHOLD;
};
// Auto-scroll to bottom when content changes and user is at bottom
useEffect(() => {
const el = containerRef.current;
if (!el) return;
observerRef.current = new ResizeObserver(() => {
if (isAtBottomRef.current) {
el.scrollTop = el.scrollHeight;
}
});
observerRef.current.observe(el);
return () => {
observerRef.current?.disconnect();
};
}, []);
// Also observe child list changes (new messages)
useEffect(() => {
const el = containerRef.current;
if (!el) return;
const mutationObserver = new MutationObserver(() => {
if (isAtBottomRef.current) {
el.scrollTop = el.scrollHeight;
}
});
mutationObserver.observe(el, { childList: true, subtree: true });
return () => {
mutationObserver.disconnect();
};
}, []);
return (
<div
ref={containerRef}
onScroll={handleScroll}
className={`overflow-y-auto custom-scrollbar ${className}`}
>
{children}
</div>
);
}