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
103 lines
3.1 KiB
TypeScript
103 lines
3.1 KiB
TypeScript
import { useCallback, useRef } from 'react';
|
|
import { useChatStore, type Message } from '../store/chatStore';
|
|
import { createLogger } from '../lib/logger';
|
|
|
|
const log = createLogger('OptimisticMessages');
|
|
|
|
/**
|
|
* Represents a file attached to an optimistic message,
|
|
* tracking its upload lifecycle. Extends MessageFile with a status field.
|
|
*/
|
|
interface OptimisticFile {
|
|
name: string;
|
|
size: number;
|
|
status: 'uploading' | 'uploaded' | 'error';
|
|
url?: string;
|
|
}
|
|
|
|
/**
|
|
* 3-phase optimistic message merging hook (inspired by DeerFlow useThreadStream).
|
|
*
|
|
* Phase 1: Instant local echo -- creates a synthetic user message with `optimistic: true`
|
|
* Phase 2: Server confirmation -- removes optimistic message when real message arrives
|
|
* Phase 3: File status transition -- updates file status from uploading -> uploaded | error
|
|
*
|
|
* This hook provides standalone utilities for components that need fine-grained
|
|
* control over optimistic rendering outside the main chat flow.
|
|
*/
|
|
export function useOptimisticMessages() {
|
|
const optimisticIdCounter = useRef(0);
|
|
|
|
const generateOptimisticId = useCallback(() => {
|
|
optimisticIdCounter.current += 1;
|
|
return `opt-user-${Date.now()}-${optimisticIdCounter.current}`;
|
|
}, []);
|
|
|
|
/**
|
|
* Phase 1: Create and insert an optimistic user message into the store.
|
|
* Returns the optimistic ID for later correlation.
|
|
*/
|
|
const addOptimistic = useCallback((content: string, files?: File[]) => {
|
|
const id = generateOptimisticId();
|
|
const optimisticFiles: OptimisticFile[] | undefined = files?.map(f => ({
|
|
name: f.name,
|
|
size: f.size,
|
|
status: 'uploading' as const,
|
|
}));
|
|
|
|
const optimisticMessage: Message = {
|
|
id,
|
|
role: 'user',
|
|
content,
|
|
timestamp: new Date(),
|
|
optimistic: true,
|
|
// Cast through unknown because OptimisticFile extends MessageFile with status
|
|
files: optimisticFiles as Message['files'],
|
|
};
|
|
|
|
log.debug('Adding optimistic message', { id, content: content.slice(0, 50) });
|
|
|
|
useChatStore.setState(state => ({
|
|
messages: [...state.messages, optimisticMessage],
|
|
}));
|
|
|
|
return id;
|
|
}, [generateOptimisticId]);
|
|
|
|
/**
|
|
* Phase 2: Remove an optimistic message when the server confirms
|
|
* by sending back the real message.
|
|
*/
|
|
const clearOnConfirm = useCallback((optimisticId: string) => {
|
|
log.debug('Clearing optimistic message on confirm', { optimisticId });
|
|
|
|
useChatStore.setState(state => ({
|
|
messages: state.messages.filter(m => m.id !== optimisticId),
|
|
}));
|
|
}, []);
|
|
|
|
/**
|
|
* Phase 3: Transition file attachment status for an optimistic message.
|
|
*/
|
|
const updateFileStatus = useCallback((optimisticId: string, status: 'uploaded' | 'error') => {
|
|
log.debug('Updating file status', { optimisticId, status });
|
|
|
|
useChatStore.setState(state => ({
|
|
messages: state.messages.map(m => {
|
|
if (m.id === optimisticId && m.files) {
|
|
return {
|
|
...m,
|
|
files: m.files.map(f => ({
|
|
...f,
|
|
status,
|
|
})),
|
|
};
|
|
}
|
|
return m;
|
|
}),
|
|
}));
|
|
}, []);
|
|
|
|
return { addOptimistic, clearOnConfirm, updateFileStatus };
|
|
}
|