Compare commits
3 Commits
bf080eed88
...
e059442d53
| Author | SHA1 | Date | |
|---|---|---|---|
| e059442d53 | |||
| 67f96fff03 | |||
| 4498a3f666 |
@ -4,6 +4,7 @@ import { Send, Bot, User, Loader2, Sparkles, Plus, Trash2 } from "lucide-react";
|
|||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
import remarkGfm from "remark-gfm";
|
import remarkGfm from "remark-gfm";
|
||||||
import { useAIChat } from "@/features/binary/hooks/useAIChat";
|
import { useAIChat } from "@/features/binary/hooks/useAIChat";
|
||||||
|
import { analyzingMessages } from "@/features/binary/analyzingMessages";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
@ -19,6 +20,46 @@ const quickPrompts = [
|
|||||||
"Summarize the binary",
|
"Summarize the binary",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const remarkPlugins = [remarkGfm];
|
||||||
|
|
||||||
|
const markdownContainerClass =
|
||||||
|
"[&_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";
|
||||||
|
|
||||||
|
const markdownComponents: ComponentPropsWithoutRef<typeof ReactMarkdown>["components"] = {
|
||||||
|
code({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: ComponentPropsWithoutRef<"code"> & {
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
const match = /language-(\w+)/.exec(className || "");
|
||||||
|
const isInline = !match;
|
||||||
|
const codeString = String(children).replace(/\n$/, "");
|
||||||
|
|
||||||
|
if (isInline) {
|
||||||
|
return (
|
||||||
|
<code className="bg-muted-foreground/20 rounded px-1 py-0.5 text-xs break-all" {...props}>
|
||||||
|
{children}
|
||||||
|
</code>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<pre className="bg-muted-foreground/10 max-w-full overflow-x-auto rounded-lg p-3 text-xs">
|
||||||
|
<code className="break-all whitespace-pre-wrap">{codeString}</code>
|
||||||
|
</pre>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
pre({ children }) {
|
||||||
|
return <pre className="my-2 overflow-x-auto rounded-lg">{children}</pre>;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function normalizeMarkdown(content: string) {
|
||||||
|
return content.replace(/\r\n/g, "\n").replace(/([^\n])\n([^\n])/g, "$1 \n$2");
|
||||||
|
}
|
||||||
|
|
||||||
export function AIChatPanel() {
|
export function AIChatPanel() {
|
||||||
const {
|
const {
|
||||||
binary,
|
binary,
|
||||||
@ -29,6 +70,8 @@ export function AIChatPanel() {
|
|||||||
isLoadingMessages,
|
isLoadingMessages,
|
||||||
isStreaming,
|
isStreaming,
|
||||||
streamingContent,
|
streamingContent,
|
||||||
|
toolStatus,
|
||||||
|
isToolRunning,
|
||||||
streamError,
|
streamError,
|
||||||
isCreatingSession,
|
isCreatingSession,
|
||||||
createSession,
|
createSession,
|
||||||
@ -43,6 +86,21 @@ export function AIChatPanel() {
|
|||||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
const [userScrolledUp, setUserScrolledUp] = useState(false);
|
const [userScrolledUp, setUserScrolledUp] = useState(false);
|
||||||
|
const [analyzingIdx, setAnalyzingIdx] = useState(() =>
|
||||||
|
Math.floor(Math.random() * analyzingMessages.length)
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isStreaming || streamingContent) return;
|
||||||
|
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
setAnalyzingIdx(Math.floor(Math.random() * analyzingMessages.length));
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [isStreaming, streamingContent]);
|
||||||
|
|
||||||
|
const analyzingMessage = analyzingMessages[analyzingIdx];
|
||||||
|
|
||||||
// Track manual scroll to respect user reading position
|
// Track manual scroll to respect user reading position
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -215,8 +273,6 @@ export function AIChatPanel() {
|
|||||||
<>
|
<>
|
||||||
{messages.map((message, idx) => {
|
{messages.map((message, idx) => {
|
||||||
const isUser = message.role === "USER";
|
const isUser = message.role === "USER";
|
||||||
const isLastMessage = idx === messages.length - 1;
|
|
||||||
const isStreamingMessage = isLastMessage && isStreaming && !message.id;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -245,67 +301,13 @@ export function AIChatPanel() {
|
|||||||
>
|
>
|
||||||
{isUser ? (
|
{isUser ? (
|
||||||
<div className="break-words whitespace-pre-wrap">{message.content}</div>
|
<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">
|
<div className={markdownContainerClass}>
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
remarkPlugins={[remarkGfm]}
|
remarkPlugins={remarkPlugins}
|
||||||
components={{
|
components={markdownComponents}
|
||||||
code({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: ComponentPropsWithoutRef<"code"> & {
|
|
||||||
className?: string;
|
|
||||||
}) {
|
|
||||||
const match = /language-(\w+)/.exec(className || "");
|
|
||||||
const isInline = !match;
|
|
||||||
const codeString = String(children).replace(/\n$/, "");
|
|
||||||
|
|
||||||
if (isInline) {
|
|
||||||
return (
|
|
||||||
<code
|
|
||||||
className="bg-muted-foreground/20 rounded px-1 py-0.5 text-xs break-all"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</code>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<pre className="bg-muted-foreground/10 max-w-full overflow-x-auto rounded-lg p-3 text-xs">
|
|
||||||
<code className="break-all whitespace-pre-wrap">
|
|
||||||
{codeString}
|
|
||||||
</code>
|
|
||||||
</pre>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
pre({ children }) {
|
|
||||||
return (
|
|
||||||
<pre className="my-2 overflow-x-auto rounded-lg">
|
|
||||||
{children}
|
|
||||||
</pre>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{(message.content ?? "")
|
{normalizeMarkdown(message.content ?? "")}
|
||||||
.replace(/\r\n/g, "\n")
|
|
||||||
.replace(/([^\n])\n([^\n])/g, "$1 \n$2")}
|
|
||||||
</ReactMarkdown>
|
</ReactMarkdown>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -323,6 +325,42 @@ 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">
|
||||||
|
{streamingContent ? (
|
||||||
|
<div className={markdownContainerClass}>
|
||||||
|
<ReactMarkdown
|
||||||
|
remarkPlugins={remarkPlugins}
|
||||||
|
components={markdownComponents}
|
||||||
|
>
|
||||||
|
{normalizeMarkdown(streamingContent)}
|
||||||
|
</ReactMarkdown>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-muted-foreground flex items-center gap-2">
|
||||||
|
<Loader2 className="text-primary h-4 w-4 animate-spin" />
|
||||||
|
<span>{analyzingMessage}...</span>
|
||||||
|
</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 ref={messagesEndRef} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
52
src/features/binary/analyzingMessages.ts
Normal file
52
src/features/binary/analyzingMessages.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
export const analyzingMessages = [
|
||||||
|
"Analyzing",
|
||||||
|
"Examining",
|
||||||
|
"Decompiling",
|
||||||
|
"Decoding",
|
||||||
|
"Mapping",
|
||||||
|
"Tracing",
|
||||||
|
"Extracting",
|
||||||
|
"Processing",
|
||||||
|
"Verifying",
|
||||||
|
"Comparing",
|
||||||
|
"Indexing",
|
||||||
|
"Scanning",
|
||||||
|
"Interpreting",
|
||||||
|
"Rebuilding",
|
||||||
|
"Inspecting",
|
||||||
|
"Classifying",
|
||||||
|
"Resolving",
|
||||||
|
"Checking",
|
||||||
|
"Searching",
|
||||||
|
"Navigating",
|
||||||
|
"Cross-referencing",
|
||||||
|
"Validating",
|
||||||
|
"Cataloging",
|
||||||
|
"Filtering",
|
||||||
|
"Grouping",
|
||||||
|
"Deobfuscating",
|
||||||
|
"Unpacking",
|
||||||
|
"Loading",
|
||||||
|
"Parsing",
|
||||||
|
"Translating",
|
||||||
|
"Optimizing",
|
||||||
|
"Symbolizing",
|
||||||
|
"Linking",
|
||||||
|
"Disassembling",
|
||||||
|
"Reconstructing",
|
||||||
|
"Profiling",
|
||||||
|
"Fuzzing",
|
||||||
|
"Auditing",
|
||||||
|
"Monitoring",
|
||||||
|
"Computing",
|
||||||
|
"Iterating",
|
||||||
|
"Deserializing",
|
||||||
|
"Normalizing",
|
||||||
|
"Hashing",
|
||||||
|
"Enumerating",
|
||||||
|
"Unwinding",
|
||||||
|
"Patching",
|
||||||
|
"Diffing",
|
||||||
|
"Emulating",
|
||||||
|
"Tainting",
|
||||||
|
];
|
||||||
@ -10,6 +10,46 @@ import {
|
|||||||
} from "@/api/generated/chat/chat.ts";
|
} from "@/api/generated/chat/chat.ts";
|
||||||
import type { SessionResponse, MessageResponse } from "@/api/generated/api.schemas.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() {
|
export function useAIChat() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const binaryId = params.binaryId!;
|
const binaryId = params.binaryId!;
|
||||||
@ -20,7 +60,11 @@ export function useAIChat() {
|
|||||||
const [streamingContent, setStreamingContent] = useState("");
|
const [streamingContent, setStreamingContent] = useState("");
|
||||||
const [isStreaming, setIsStreaming] = useState(false);
|
const [isStreaming, setIsStreaming] = useState(false);
|
||||||
const [streamError, setStreamError] = useState<string | null>(null);
|
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 [pendingUserMessage, setPendingUserMessage] = useState<MessageResponse | null>(null);
|
||||||
|
const [optimisticAssistantMessage, setOptimisticAssistantMessage] =
|
||||||
|
useState<MessageResponse | null>(null);
|
||||||
|
|
||||||
const { data: binary } = useGetBinary(binaryId, {
|
const { data: binary } = useGetBinary(binaryId, {
|
||||||
query: { enabled: !!binaryId },
|
query: { enabled: !!binaryId },
|
||||||
@ -89,6 +133,9 @@ export function useAIChat() {
|
|||||||
|
|
||||||
setIsStreaming(true);
|
setIsStreaming(true);
|
||||||
setStreamingContent("");
|
setStreamingContent("");
|
||||||
|
setToolStatus(null);
|
||||||
|
setIsToolRunning(false);
|
||||||
|
setOptimisticAssistantMessage(null);
|
||||||
setStreamError(null);
|
setStreamError(null);
|
||||||
setPendingUserMessage({
|
setPendingUserMessage({
|
||||||
role: "USER",
|
role: "USER",
|
||||||
@ -137,23 +184,53 @@ export function useAIChat() {
|
|||||||
if (!line.startsWith("data:")) continue;
|
if (!line.startsWith("data:")) continue;
|
||||||
const event = JSON.parse(line.slice(5).trim());
|
const event = JSON.parse(line.slice(5).trim());
|
||||||
|
|
||||||
if (event.type === "chunk") {
|
const sseEvent = event as SSEEvent;
|
||||||
fullContent += event.content;
|
switch (sseEvent.type) {
|
||||||
setStreamingContent(fullContent);
|
case "chunk":
|
||||||
} else if (event.type === "done") {
|
fullContent += sseEvent.content;
|
||||||
setIsStreaming(false);
|
setStreamingContent(fullContent);
|
||||||
setStreamingContent("");
|
setToolStatus(null);
|
||||||
setPendingUserMessage(null);
|
setIsToolRunning(false);
|
||||||
queryClient.invalidateQueries({
|
break;
|
||||||
queryKey: [`/chat/sessions/${activeSessionId}/messages`],
|
case "tool_call_start":
|
||||||
});
|
setToolStatus(`Calling ${sseEvent.tool}...`);
|
||||||
queryClient.invalidateQueries({
|
setIsToolRunning(true);
|
||||||
queryKey: [`/binaries/${binaryId}/chat/sessions`],
|
break;
|
||||||
});
|
case "tool_call_end":
|
||||||
} else if (event.type === "error") {
|
setToolStatus(
|
||||||
setIsStreaming(false);
|
`${sseEvent.tool} completed (${sseEvent.durationMs}ms): ${sseEvent.resultSummary}`
|
||||||
setPendingUserMessage(null);
|
);
|
||||||
setStreamError(event.message);
|
setIsToolRunning(false);
|
||||||
|
break;
|
||||||
|
case "thinking":
|
||||||
|
setToolStatus(sseEvent.message);
|
||||||
|
setIsToolRunning(true);
|
||||||
|
break;
|
||||||
|
case "done":
|
||||||
|
setOptimisticAssistantMessage({
|
||||||
|
role: "ASSISTANT",
|
||||||
|
content: fullContent,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
setIsStreaming(false);
|
||||||
|
setStreamingContent("");
|
||||||
|
setToolStatus(null);
|
||||||
|
setIsToolRunning(false);
|
||||||
|
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);
|
||||||
|
setOptimisticAssistantMessage(null);
|
||||||
|
setStreamError(sseEvent.message);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -164,23 +241,53 @@ export function useAIChat() {
|
|||||||
if (!line.startsWith("data:")) continue;
|
if (!line.startsWith("data:")) continue;
|
||||||
const event = JSON.parse(line.slice(5).trim());
|
const event = JSON.parse(line.slice(5).trim());
|
||||||
|
|
||||||
if (event.type === "chunk") {
|
const sseEvent = event as SSEEvent;
|
||||||
fullContent += event.content;
|
switch (sseEvent.type) {
|
||||||
setStreamingContent(fullContent);
|
case "chunk":
|
||||||
} else if (event.type === "done") {
|
fullContent += sseEvent.content;
|
||||||
setIsStreaming(false);
|
setStreamingContent(fullContent);
|
||||||
setStreamingContent("");
|
setToolStatus(null);
|
||||||
setPendingUserMessage(null);
|
setIsToolRunning(false);
|
||||||
queryClient.invalidateQueries({
|
break;
|
||||||
queryKey: [`/chat/sessions/${activeSessionId}/messages`],
|
case "tool_call_start":
|
||||||
});
|
setToolStatus(`Calling ${sseEvent.tool}...`);
|
||||||
queryClient.invalidateQueries({
|
setIsToolRunning(true);
|
||||||
queryKey: [`/binaries/${binaryId}/chat/sessions`],
|
break;
|
||||||
});
|
case "tool_call_end":
|
||||||
} else if (event.type === "error") {
|
setToolStatus(
|
||||||
setIsStreaming(false);
|
`${sseEvent.tool} completed (${sseEvent.durationMs}ms): ${sseEvent.resultSummary}`
|
||||||
setPendingUserMessage(null);
|
);
|
||||||
setStreamError(event.message);
|
setIsToolRunning(false);
|
||||||
|
break;
|
||||||
|
case "thinking":
|
||||||
|
setToolStatus(sseEvent.message);
|
||||||
|
setIsToolRunning(true);
|
||||||
|
break;
|
||||||
|
case "done":
|
||||||
|
setOptimisticAssistantMessage({
|
||||||
|
role: "ASSISTANT",
|
||||||
|
content: fullContent,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
setIsStreaming(false);
|
||||||
|
setStreamingContent("");
|
||||||
|
setToolStatus(null);
|
||||||
|
setIsToolRunning(false);
|
||||||
|
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);
|
||||||
|
setOptimisticAssistantMessage(null);
|
||||||
|
setStreamError(sseEvent.message);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -190,7 +297,10 @@ export function useAIChat() {
|
|||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
if (err instanceof Error && err.name !== "AbortError") {
|
if (err instanceof Error && err.name !== "AbortError") {
|
||||||
setIsStreaming(false);
|
setIsStreaming(false);
|
||||||
|
setToolStatus(null);
|
||||||
|
setIsToolRunning(false);
|
||||||
setPendingUserMessage(null);
|
setPendingUserMessage(null);
|
||||||
|
setOptimisticAssistantMessage(null);
|
||||||
setStreamError(err.message || "An unexpected error occurred");
|
setStreamError(err.message || "An unexpected error occurred");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -201,17 +311,28 @@ export function useAIChat() {
|
|||||||
const cancelStream = useCallback(() => {
|
const cancelStream = useCallback(() => {
|
||||||
abortRef.current?.abort();
|
abortRef.current?.abort();
|
||||||
setIsStreaming(false);
|
setIsStreaming(false);
|
||||||
|
setToolStatus(null);
|
||||||
|
setIsToolRunning(false);
|
||||||
setPendingUserMessage(null);
|
setPendingUserMessage(null);
|
||||||
|
setOptimisticAssistantMessage(null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const messages: MessageResponse[] = [...historicalMessages];
|
const messages: MessageResponse[] = [...historicalMessages];
|
||||||
if (isStreaming && pendingUserMessage) {
|
|
||||||
|
if (
|
||||||
|
pendingUserMessage &&
|
||||||
|
!historicalMessages.some((m) => m.role === "USER" && m.content === pendingUserMessage.content)
|
||||||
|
) {
|
||||||
messages.push(pendingUserMessage);
|
messages.push(pendingUserMessage);
|
||||||
messages.push({
|
}
|
||||||
role: "ASSISTANT",
|
|
||||||
content: streamingContent,
|
if (
|
||||||
createdAt: new Date().toISOString(),
|
optimisticAssistantMessage &&
|
||||||
});
|
!historicalMessages.some(
|
||||||
|
(m) => m.role === "ASSISTANT" && m.content === optimisticAssistantMessage.content
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
messages.push(optimisticAssistantMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
const userFriendlyError = streamError
|
const userFriendlyError = streamError
|
||||||
@ -231,6 +352,8 @@ export function useAIChat() {
|
|||||||
isLoadingMessages: messagesQuery.isLoading,
|
isLoadingMessages: messagesQuery.isLoading,
|
||||||
isStreaming,
|
isStreaming,
|
||||||
streamingContent,
|
streamingContent,
|
||||||
|
toolStatus,
|
||||||
|
isToolRunning,
|
||||||
streamError: userFriendlyError,
|
streamError: userFriendlyError,
|
||||||
isCreatingSession: createSessionMutation.isPending,
|
isCreatingSession: createSessionMutation.isPending,
|
||||||
createSession,
|
createSession,
|
||||||
|
|||||||
@ -28,6 +28,7 @@
|
|||||||
--color-input: var(--input);
|
--color-input: var(--input);
|
||||||
--color-border: var(--border);
|
--color-border: var(--border);
|
||||||
--color-destructive: var(--destructive);
|
--color-destructive: var(--destructive);
|
||||||
|
--color-success: var(--success);
|
||||||
--color-accent-foreground: var(--accent-foreground);
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
--color-accent: var(--accent);
|
--color-accent: var(--accent);
|
||||||
--color-muted-foreground: var(--muted-foreground);
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user