Phase 1: streaming UI + thinking blocks in ChatPanel
- Agent streaming: real-time text deltas in chat - Thinking/reasoning: collapsible block with agent's thinking process - Tool call indicators: shows running/completed tools - Streaming bubble with pulse animation while agent generates - Message type updated with 'thinking' field
This commit is contained in:
parent
92db42113d
commit
d5eaa13b21
@ -12,6 +12,14 @@ const AUTHOR_ICON: Record<string, string> = {
|
||||
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<UploadedFile[]>([]);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [streaming, setStreaming] = useState<StreamingState | null>(null);
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const bottomRef = useRef<HTMLDivElement>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(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) {
|
||||
<span>·</span>
|
||||
<span>{new Date(msg.created_at).toLocaleString("ru-RU", { hour: "2-digit", minute: "2-digit" })}</span>
|
||||
</div>
|
||||
{msg.thinking && (
|
||||
<details className="ml-5 mb-1">
|
||||
<summary className="text-xs text-[var(--muted)] cursor-pointer hover:text-[var(--fg)]">
|
||||
💭 Размышления
|
||||
</summary>
|
||||
<div className="mt-1 text-xs text-[var(--muted)] whitespace-pre-wrap bg-white/5 rounded p-2 max-h-[200px] overflow-y-auto">
|
||||
{msg.thinking}
|
||||
</div>
|
||||
</details>
|
||||
)}
|
||||
<div className="ml-5 whitespace-pre-wrap">{renderContent(msg.content)}</div>
|
||||
{renderAttachments(msg.attachments)}
|
||||
</div>
|
||||
@ -250,9 +313,46 @@ export default function ChatPanel({ chatId, projectSlug }: Props) {
|
||||
<div className="text-center text-xs text-[var(--muted)] py-2">— начало истории —</div>
|
||||
)}
|
||||
{messages.map(renderMessage)}
|
||||
{messages.length === 0 && (
|
||||
{messages.length === 0 && !streaming && (
|
||||
<div className="text-sm text-[var(--muted)] italic py-8 text-center">Нет сообщений</div>
|
||||
)}
|
||||
|
||||
{/* Streaming indicator */}
|
||||
{streaming && (
|
||||
<div className="text-sm" style={{ backgroundColor: "rgba(0, 120, 60, 0.2)", borderRadius: 6, padding: "4px 8px" }}>
|
||||
<div className="flex items-center gap-1 text-xs text-[var(--muted)] mb-0.5">
|
||||
<span>🤖</span>
|
||||
<span className="font-medium">{streaming.agentSlug}</span>
|
||||
<span className="animate-pulse">●</span>
|
||||
</div>
|
||||
{streaming.thinking && (
|
||||
<details className="ml-5 mb-1" open>
|
||||
<summary className="text-xs text-[var(--muted)] cursor-pointer hover:text-[var(--fg)]">
|
||||
💭 Размышления
|
||||
</summary>
|
||||
<div className="mt-1 text-xs text-[var(--muted)] whitespace-pre-wrap bg-white/5 rounded p-2 max-h-[200px] overflow-y-auto">
|
||||
{streaming.thinking}
|
||||
</div>
|
||||
</details>
|
||||
)}
|
||||
{streaming.tools.length > 0 && (
|
||||
<div className="ml-5 mb-1 text-xs text-[var(--muted)]">
|
||||
{streaming.tools.map((t, i) => (
|
||||
<span key={i} className="mr-2">
|
||||
{t.status === "running" ? "🔧" : "✅"} {t.tool}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{streaming.text && (
|
||||
<div className="ml-5 whitespace-pre-wrap">{streaming.text}</div>
|
||||
)}
|
||||
{!streaming.text && streaming.tools.length === 0 && !streaming.thinking && (
|
||||
<div className="ml-5 text-[var(--muted)] italic">Думает...</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div ref={bottomRef} />
|
||||
</div>
|
||||
|
||||
|
||||
@ -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[];
|
||||
|
||||
Loading…
Reference in New Issue
Block a user