From 3468d1bdf4ebc1735bad2798e6c9388bf47dc08e Mon Sep 17 00:00:00 2001 From: Haze <709547807@qq.com> Date: Fri, 6 Feb 2026 04:49:01 +0800 Subject: [PATCH] feat(chat): native React chat page with session selector and streaming Replace the iframe-based Control UI embed with a native React implementation that communicates directly with the Gateway via gateway:rpc IPC calls and chat event streaming. New components: - ChatToolbar: session selector dropdown, refresh button, thinking toggle - ChatMessage: message bubbles with markdown (react-markdown + GFM), collapsible thinking blocks, tool use cards, image attachments - ChatInput: textarea with Enter to send, Shift+Enter for new line - message-utils: extractText/extractThinking/extractImages/extractToolUse ported from OpenClaw's message-extract.ts Rewritten chat store with: - Session management (sessions.list, switchSession) - Proper chat.history loading with raw message preservation - chat.send with idempotencyKey and run tracking - Streaming via handleChatEvent (delta/final/error/aborted) - Thinking toggle (show/hide reasoning blocks) --- src/pages/Chat/ChatInput.tsx | 85 +++++++ src/pages/Chat/ChatMessage.tsx | 249 ++++++++++++++++++++ src/pages/Chat/ChatToolbar.tsx | 81 +++++++ src/pages/Chat/index.tsx | 265 ++++++++++++--------- src/pages/Chat/message-utils.ts | 131 +++++++++++ src/stores/chat.ts | 392 +++++++++++++++++++------------- 6 files changed, 937 insertions(+), 266 deletions(-) create mode 100644 src/pages/Chat/ChatInput.tsx create mode 100644 src/pages/Chat/ChatMessage.tsx create mode 100644 src/pages/Chat/ChatToolbar.tsx create mode 100644 src/pages/Chat/message-utils.ts diff --git a/src/pages/Chat/ChatInput.tsx b/src/pages/Chat/ChatInput.tsx new file mode 100644 index 000000000..be1c4d0dd --- /dev/null +++ b/src/pages/Chat/ChatInput.tsx @@ -0,0 +1,85 @@ +/** + * Chat Input Component + * Textarea with send button. Enter to send, Shift+Enter for new line. + */ +import { useState, useRef, useEffect, useCallback } from 'react'; +import { Send, Square } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Textarea } from '@/components/ui/textarea'; + +interface ChatInputProps { + onSend: (text: string) => void; + disabled?: boolean; + sending?: boolean; +} + +export function ChatInput({ onSend, disabled = false, sending = false }: ChatInputProps) { + const [input, setInput] = useState(''); + const textareaRef = useRef(null); + + // Auto-resize textarea + useEffect(() => { + if (textareaRef.current) { + textareaRef.current.style.height = 'auto'; + textareaRef.current.style.height = `${Math.min(textareaRef.current.scrollHeight, 200)}px`; + } + }, [input]); + + const handleSend = useCallback(() => { + const trimmed = input.trim(); + if (!trimmed || disabled || sending) return; + onSend(trimmed); + setInput(''); + // Reset textarea height + if (textareaRef.current) { + textareaRef.current.style.height = 'auto'; + } + }, [input, disabled, sending, onSend]); + + const handleKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleSend(); + } + }, + [handleSend], + ); + + return ( +
+
+
+