diff --git a/src/components/ChatPanel.tsx b/src/components/ChatPanel.tsx index 92cc785..0fcd093 100644 --- a/src/components/ChatPanel.tsx +++ b/src/components/ChatPanel.tsx @@ -12,6 +12,14 @@ const AUTHOR_ICON: Record = { system: "⚙️", }; +/** Streaming state for agent that is currently generating */ +interface StreamingState { + agentSlug: string; + text: string; + thinking: string; + tools: Array<{ tool: string; status: string }>; +} + interface Props { chatId: string; projectSlug?: string; @@ -35,6 +43,7 @@ export default function ChatPanel({ chatId, projectSlug }: Props) { const [hasMore, setHasMore] = useState(true); const [pendingFiles, setPendingFiles] = useState([]); const [uploading, setUploading] = useState(false); + const [streaming, setStreaming] = useState(null); const scrollRef = useRef(null); const bottomRef = useRef(null); const fileInputRef = useRef(null); @@ -62,10 +71,54 @@ export default function ChatPanel({ chatId, projectSlug }: Props) { if (prev.some((m) => m.id === msg.id)) return prev; return [...prev, msg]; }); + // Clear streaming when final message arrives + setStreaming(null); }); return () => { unsub?.(); }; }, [chatId]); + // WS: agent streaming events + useEffect(() => { + const unsubStart = wsClient.on("agent.stream.start", (data: any) => { + if (data.chat_id !== chatId) return; + setStreaming({ agentSlug: data.agent_slug || "agent", text: "", thinking: "", tools: [] }); + }); + const unsubDelta = wsClient.on("agent.stream.delta", (data: any) => { + if (data.chat_id !== chatId) return; + setStreaming((prev) => { + if (!prev) return { agentSlug: data.agent_slug || "agent", text: "", thinking: "", tools: [] }; + if (data.block_type === "thinking") { + return { ...prev, thinking: prev.thinking + (data.text || "") }; + } + return { ...prev, text: prev.text + (data.text || "") }; + }); + }); + const unsubTool = wsClient.on("agent.stream.tool", (data: any) => { + if (data.chat_id !== chatId) return; + setStreaming((prev) => { + if (!prev) return null; + const tools = [...prev.tools]; + const existing = tools.findIndex((t) => t.tool === data.tool); + if (existing >= 0) { + tools[existing] = { tool: data.tool, status: data.status }; + } else { + tools.push({ tool: data.tool, status: data.status }); + } + return { ...prev, tools }; + }); + }); + const unsubEnd = wsClient.on("agent.stream.end", (data: any) => { + if (data.chat_id !== chatId) return; + // Don't clear immediately — wait for message.new with final content + }); + return () => { + unsubStart?.(); + unsubDelta?.(); + unsubTool?.(); + unsubEnd?.(); + }; + }, [chatId]); + // Auto-scroll on new messages (only if near bottom) useEffect(() => { if (isInitial.current) { @@ -230,6 +283,16 @@ export default function ChatPanel({ chatId, projectSlug }: Props) { · {new Date(msg.created_at).toLocaleString("ru-RU", { hour: "2-digit", minute: "2-digit" })} + {msg.thinking && ( +
+ + 💭 Размышления + +
+ {msg.thinking} +
+
+ )}
{renderContent(msg.content)}
{renderAttachments(msg.attachments)} @@ -250,9 +313,46 @@ export default function ChatPanel({ chatId, projectSlug }: Props) {
— начало истории —
)} {messages.map(renderMessage)} - {messages.length === 0 && ( + {messages.length === 0 && !streaming && (
Нет сообщений
)} + + {/* Streaming indicator */} + {streaming && ( +
+
+ 🤖 + {streaming.agentSlug} + +
+ {streaming.thinking && ( +
+ + 💭 Размышления + +
+ {streaming.thinking} +
+
+ )} + {streaming.tools.length > 0 && ( +
+ {streaming.tools.map((t, i) => ( + + {t.status === "running" ? "🔧" : "✅"} {t.tool} + + ))} +
+ )} + {streaming.text && ( +
{streaming.text}
+ )} + {!streaming.text && streaming.tools.length === 0 && !streaming.thinking && ( +
Думает...
+ )} +
+ )} +
diff --git a/src/lib/api.ts b/src/lib/api.ts index 8e0fc73..c225e73 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -146,6 +146,7 @@ export interface Message { author_id: string | null; author: MemberBrief | null; content: string; + thinking: string | null; mentions: string[]; voice_url: string | null; attachments: Attachment[];