feat(chat): enhance streaming status and tool call events
This commit is contained in:
parent
bf080eed88
commit
4498a3f666
@ -29,6 +29,8 @@ export function AIChatPanel() {
|
||||
isLoadingMessages,
|
||||
isStreaming,
|
||||
streamingContent,
|
||||
toolStatus,
|
||||
isToolRunning,
|
||||
streamError,
|
||||
isCreatingSession,
|
||||
createSession,
|
||||
@ -215,8 +217,6 @@ export function AIChatPanel() {
|
||||
<>
|
||||
{messages.map((message, idx) => {
|
||||
const isUser = message.role === "USER";
|
||||
const isLastMessage = idx === messages.length - 1;
|
||||
const isStreamingMessage = isLastMessage && isStreaming && !message.id;
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -245,20 +245,6 @@ export function AIChatPanel() {
|
||||
>
|
||||
{isUser ? (
|
||||
<div className="break-words whitespace-pre-wrap">{message.content}</div>
|
||||
) : isStreamingMessage ? (
|
||||
<div className="break-words whitespace-pre-wrap">
|
||||
{streamingContent ? (
|
||||
<>
|
||||
{streamingContent}
|
||||
<span className="ml-0.5 inline-block h-4 w-1 animate-pulse bg-current align-middle" />
|
||||
</>
|
||||
) : (
|
||||
<div className="text-muted-foreground flex items-center gap-2">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
<span>Analyzing...</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="[&_th]:border-border [&_td]:border-border max-w-none break-words [&_ol]:my-2 [&_p]:my-2 [&_p:first-child]:mt-0 [&_p:last-child]:mb-0 [&_pre]:my-2 [&_table]:w-full [&_table]:border-collapse [&_td]:border [&_td]:px-2 [&_td]:py-1 [&_th]:border [&_th]:px-2 [&_th]:py-1 [&_th]:text-left [&_ul]:my-2">
|
||||
<ReactMarkdown
|
||||
@ -323,6 +309,40 @@ export function AIChatPanel() {
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
{isStreaming && (
|
||||
<>
|
||||
<div className="mb-4 flex gap-3">
|
||||
<div className="bg-muted flex h-8 w-8 shrink-0 items-center justify-center rounded-full">
|
||||
<Bot className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="flex max-w-[85%] min-w-0 flex-col items-start gap-1">
|
||||
<div className="bg-muted overflow-hidden rounded-lg px-3 py-2 text-sm">
|
||||
<div className="break-words whitespace-pre-wrap">
|
||||
{streamingContent ? (
|
||||
<>
|
||||
{streamingContent}
|
||||
<span className="ml-0.5 inline-block h-4 w-1 animate-pulse bg-current align-middle" />
|
||||
</>
|
||||
) : (
|
||||
<div className="text-muted-foreground flex items-center gap-2">
|
||||
<Loader2 className="text-primary h-4 w-4 animate-spin" />
|
||||
<span>Analyzing...</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{toolStatus && (
|
||||
<div className="text-muted-foreground bg-muted border-l-primary my-1 flex items-center gap-2 rounded-lg border-l-[3px] px-3 py-2 text-sm">
|
||||
{isToolRunning && (
|
||||
<Loader2 className="text-primary h-3.5 w-3.5 shrink-0 animate-spin" />
|
||||
)}
|
||||
<span>{toolStatus}</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -10,6 +10,46 @@ import {
|
||||
} from "@/api/generated/chat/chat.ts";
|
||||
import type { SessionResponse, MessageResponse } from "@/api/generated/api.schemas.ts";
|
||||
|
||||
interface ChunkEvent {
|
||||
type: "chunk";
|
||||
content: string;
|
||||
}
|
||||
|
||||
interface ToolCallStartEvent {
|
||||
type: "tool_call_start";
|
||||
tool: string;
|
||||
}
|
||||
|
||||
interface ToolCallEndEvent {
|
||||
type: "tool_call_end";
|
||||
tool: string;
|
||||
durationMs: number;
|
||||
resultSummary: string;
|
||||
}
|
||||
|
||||
interface ThinkingEvent {
|
||||
type: "thinking";
|
||||
message: string;
|
||||
}
|
||||
|
||||
interface DoneEvent {
|
||||
type: "done";
|
||||
sessionId: string;
|
||||
}
|
||||
|
||||
interface ErrorEvent {
|
||||
type: "error";
|
||||
message: string;
|
||||
}
|
||||
|
||||
type SSEEvent =
|
||||
| ChunkEvent
|
||||
| ToolCallStartEvent
|
||||
| ToolCallEndEvent
|
||||
| ThinkingEvent
|
||||
| DoneEvent
|
||||
| ErrorEvent;
|
||||
|
||||
export function useAIChat() {
|
||||
const params = useParams();
|
||||
const binaryId = params.binaryId!;
|
||||
@ -20,6 +60,8 @@ export function useAIChat() {
|
||||
const [streamingContent, setStreamingContent] = useState("");
|
||||
const [isStreaming, setIsStreaming] = useState(false);
|
||||
const [streamError, setStreamError] = useState<string | null>(null);
|
||||
const [toolStatus, setToolStatus] = useState<string | null>(null);
|
||||
const [isToolRunning, setIsToolRunning] = useState(false);
|
||||
const [pendingUserMessage, setPendingUserMessage] = useState<MessageResponse | null>(null);
|
||||
|
||||
const { data: binary } = useGetBinary(binaryId, {
|
||||
@ -89,6 +131,8 @@ export function useAIChat() {
|
||||
|
||||
setIsStreaming(true);
|
||||
setStreamingContent("");
|
||||
setToolStatus(null);
|
||||
setIsToolRunning(false);
|
||||
setStreamError(null);
|
||||
setPendingUserMessage({
|
||||
role: "USER",
|
||||
@ -137,23 +181,48 @@ export function useAIChat() {
|
||||
if (!line.startsWith("data:")) continue;
|
||||
const event = JSON.parse(line.slice(5).trim());
|
||||
|
||||
if (event.type === "chunk") {
|
||||
fullContent += event.content;
|
||||
setStreamingContent(fullContent);
|
||||
} else if (event.type === "done") {
|
||||
setIsStreaming(false);
|
||||
setStreamingContent("");
|
||||
setPendingUserMessage(null);
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [`/chat/sessions/${activeSessionId}/messages`],
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [`/binaries/${binaryId}/chat/sessions`],
|
||||
});
|
||||
} else if (event.type === "error") {
|
||||
setIsStreaming(false);
|
||||
setPendingUserMessage(null);
|
||||
setStreamError(event.message);
|
||||
const sseEvent = event as SSEEvent;
|
||||
switch (sseEvent.type) {
|
||||
case "chunk":
|
||||
fullContent += sseEvent.content;
|
||||
setStreamingContent(fullContent);
|
||||
setToolStatus(null);
|
||||
setIsToolRunning(false);
|
||||
break;
|
||||
case "tool_call_start":
|
||||
setToolStatus(`Calling ${sseEvent.tool}...`);
|
||||
setIsToolRunning(true);
|
||||
break;
|
||||
case "tool_call_end":
|
||||
setToolStatus(
|
||||
`${sseEvent.tool} completed (${sseEvent.durationMs}ms): ${sseEvent.resultSummary}`
|
||||
);
|
||||
setIsToolRunning(false);
|
||||
break;
|
||||
case "thinking":
|
||||
setToolStatus(sseEvent.message);
|
||||
setIsToolRunning(true);
|
||||
break;
|
||||
case "done":
|
||||
setIsStreaming(false);
|
||||
setStreamingContent("");
|
||||
setToolStatus(null);
|
||||
setIsToolRunning(false);
|
||||
setPendingUserMessage(null);
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [`/chat/sessions/${activeSessionId}/messages`],
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [`/binaries/${binaryId}/chat/sessions`],
|
||||
});
|
||||
break;
|
||||
case "error":
|
||||
setIsStreaming(false);
|
||||
setToolStatus(null);
|
||||
setIsToolRunning(false);
|
||||
setPendingUserMessage(null);
|
||||
setStreamError(sseEvent.message);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -164,23 +233,48 @@ export function useAIChat() {
|
||||
if (!line.startsWith("data:")) continue;
|
||||
const event = JSON.parse(line.slice(5).trim());
|
||||
|
||||
if (event.type === "chunk") {
|
||||
fullContent += event.content;
|
||||
setStreamingContent(fullContent);
|
||||
} else if (event.type === "done") {
|
||||
setIsStreaming(false);
|
||||
setStreamingContent("");
|
||||
setPendingUserMessage(null);
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [`/chat/sessions/${activeSessionId}/messages`],
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [`/binaries/${binaryId}/chat/sessions`],
|
||||
});
|
||||
} else if (event.type === "error") {
|
||||
setIsStreaming(false);
|
||||
setPendingUserMessage(null);
|
||||
setStreamError(event.message);
|
||||
const sseEvent = event as SSEEvent;
|
||||
switch (sseEvent.type) {
|
||||
case "chunk":
|
||||
fullContent += sseEvent.content;
|
||||
setStreamingContent(fullContent);
|
||||
setToolStatus(null);
|
||||
setIsToolRunning(false);
|
||||
break;
|
||||
case "tool_call_start":
|
||||
setToolStatus(`Calling ${sseEvent.tool}...`);
|
||||
setIsToolRunning(true);
|
||||
break;
|
||||
case "tool_call_end":
|
||||
setToolStatus(
|
||||
`${sseEvent.tool} completed (${sseEvent.durationMs}ms): ${sseEvent.resultSummary}`
|
||||
);
|
||||
setIsToolRunning(false);
|
||||
break;
|
||||
case "thinking":
|
||||
setToolStatus(sseEvent.message);
|
||||
setIsToolRunning(true);
|
||||
break;
|
||||
case "done":
|
||||
setIsStreaming(false);
|
||||
setStreamingContent("");
|
||||
setToolStatus(null);
|
||||
setIsToolRunning(false);
|
||||
setPendingUserMessage(null);
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [`/chat/sessions/${activeSessionId}/messages`],
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [`/binaries/${binaryId}/chat/sessions`],
|
||||
});
|
||||
break;
|
||||
case "error":
|
||||
setIsStreaming(false);
|
||||
setToolStatus(null);
|
||||
setIsToolRunning(false);
|
||||
setPendingUserMessage(null);
|
||||
setStreamError(sseEvent.message);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -190,6 +284,8 @@ export function useAIChat() {
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof Error && err.name !== "AbortError") {
|
||||
setIsStreaming(false);
|
||||
setToolStatus(null);
|
||||
setIsToolRunning(false);
|
||||
setPendingUserMessage(null);
|
||||
setStreamError(err.message || "An unexpected error occurred");
|
||||
}
|
||||
@ -201,17 +297,14 @@ export function useAIChat() {
|
||||
const cancelStream = useCallback(() => {
|
||||
abortRef.current?.abort();
|
||||
setIsStreaming(false);
|
||||
setToolStatus(null);
|
||||
setIsToolRunning(false);
|
||||
setPendingUserMessage(null);
|
||||
}, []);
|
||||
|
||||
const messages: MessageResponse[] = [...historicalMessages];
|
||||
if (isStreaming && pendingUserMessage) {
|
||||
messages.push(pendingUserMessage);
|
||||
messages.push({
|
||||
role: "ASSISTANT",
|
||||
content: streamingContent,
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
const userFriendlyError = streamError
|
||||
@ -231,6 +324,8 @@ export function useAIChat() {
|
||||
isLoadingMessages: messagesQuery.isLoading,
|
||||
isStreaming,
|
||||
streamingContent,
|
||||
toolStatus,
|
||||
isToolRunning,
|
||||
streamError: userFriendlyError,
|
||||
isCreatingSession: createSessionMutation.isPending,
|
||||
createSession,
|
||||
|
||||
@ -28,6 +28,7 @@
|
||||
--color-input: var(--input);
|
||||
--color-border: var(--border);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-success: var(--success);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user