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
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:
117
desktop/src/components/ai/Conversation.tsx
Normal file
117
desktop/src/components/ai/Conversation.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user