mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-12 22:42:13 +02:00
refactor: enhance chat and tool UI with improved streaming and error handling
- Updated NewChatPage to persist partial responses when requests are cancelled by the user. - Enhanced WriteTodosToolUI to check if the thread is running, improving the display of loading states and error handling. - Modified Plan and TodoItem components to conditionally animate in-progress items based on streaming status, providing a clearer user experience during task management.
This commit is contained in:
parent
2860b789e3
commit
5c68820c2a
3 changed files with 51 additions and 20 deletions
|
|
@ -755,7 +755,21 @@ export default function NewChatPage() {
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error && error.name === "AbortError") {
|
if (error instanceof Error && error.name === "AbortError") {
|
||||||
// Request was cancelled
|
// Request was cancelled by user - persist partial response if any content was received
|
||||||
|
const hasContent = contentParts.some(
|
||||||
|
(part) =>
|
||||||
|
(part.type === "text" && part.text.length > 0) ||
|
||||||
|
(part.type === "tool-call" && TOOLS_WITH_UI.has(part.toolName))
|
||||||
|
);
|
||||||
|
if (hasContent && currentThreadId) {
|
||||||
|
const partialContent = buildContentForPersistence();
|
||||||
|
appendMessage(currentThreadId, {
|
||||||
|
role: "assistant",
|
||||||
|
content: partialContent,
|
||||||
|
}).catch((err) =>
|
||||||
|
console.error("Failed to persist partial assistant message:", err)
|
||||||
|
);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.error("[NewChatPage] Chat error:", error);
|
console.error("[NewChatPage] Chat error:", error);
|
||||||
|
|
|
||||||
|
|
@ -30,19 +30,27 @@ import type { PlanTodo, TodoStatus } from "./schema";
|
||||||
interface StatusIconProps {
|
interface StatusIconProps {
|
||||||
status: TodoStatus;
|
status: TodoStatus;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
/** When false, in_progress items show as static (no spinner) */
|
||||||
|
isStreaming?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StatusIcon: FC<StatusIconProps> = ({ status, className }) => {
|
const StatusIcon: FC<StatusIconProps> = ({ status, className, isStreaming = true }) => {
|
||||||
const baseClass = cn("size-4 shrink-0", className);
|
const baseClass = cn("size-4 shrink-0", className);
|
||||||
|
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case "completed":
|
case "completed":
|
||||||
return <CheckCircle2 className={cn(baseClass, "text-emerald-500")} />;
|
return <CheckCircle2 className={cn(baseClass, "text-emerald-500")} />;
|
||||||
case "in_progress":
|
case "in_progress":
|
||||||
|
// Only animate the spinner if we're actively streaming
|
||||||
|
// When streaming is stopped, show as a static dashed circle
|
||||||
return (
|
return (
|
||||||
<CircleDashed
|
<CircleDashed
|
||||||
className={cn(baseClass, "text-primary animate-spin")}
|
className={cn(
|
||||||
style={{ animationDuration: "3s" }}
|
baseClass,
|
||||||
|
"text-primary",
|
||||||
|
isStreaming && "animate-spin"
|
||||||
|
)}
|
||||||
|
style={isStreaming ? { animationDuration: "3s" } : undefined}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case "cancelled":
|
case "cancelled":
|
||||||
|
|
@ -59,18 +67,21 @@ const StatusIcon: FC<StatusIconProps> = ({ status, className }) => {
|
||||||
|
|
||||||
interface TodoItemProps {
|
interface TodoItemProps {
|
||||||
todo: PlanTodo;
|
todo: PlanTodo;
|
||||||
|
/** When false, in_progress items show as static (no spinner/pulse) */
|
||||||
|
isStreaming?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TodoItem: FC<TodoItemProps> = ({ todo }) => {
|
const TodoItem: FC<TodoItemProps> = ({ todo, isStreaming = true }) => {
|
||||||
const isStrikethrough = todo.status === "completed" || todo.status === "cancelled";
|
const isStrikethrough = todo.status === "completed" || todo.status === "cancelled";
|
||||||
const isShimmer = todo.status === "in_progress";
|
// Only show pulse animation if streaming and in progress
|
||||||
|
const isShimmer = todo.status === "in_progress" && isStreaming;
|
||||||
|
|
||||||
if (todo.description) {
|
if (todo.description) {
|
||||||
return (
|
return (
|
||||||
<AccordionItem value={todo.id} className="border-0">
|
<AccordionItem value={todo.id} className="border-0">
|
||||||
<AccordionTrigger className="py-2 hover:no-underline">
|
<AccordionTrigger className="py-2 hover:no-underline">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<StatusIcon status={todo.status} />
|
<StatusIcon status={todo.status} isStreaming={isStreaming} />
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-sm text-left",
|
"text-sm text-left",
|
||||||
|
|
@ -91,7 +102,7 @@ const TodoItem: FC<TodoItemProps> = ({ todo }) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2 py-2">
|
<div className="flex items-center gap-2 py-2">
|
||||||
<StatusIcon status={todo.status} />
|
<StatusIcon status={todo.status} isStreaming={isStreaming} />
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-sm",
|
"text-sm",
|
||||||
|
|
@ -116,6 +127,8 @@ export interface PlanProps {
|
||||||
todos: PlanTodo[];
|
todos: PlanTodo[];
|
||||||
maxVisibleTodos?: number;
|
maxVisibleTodos?: number;
|
||||||
showProgress?: boolean;
|
showProgress?: boolean;
|
||||||
|
/** When false, in_progress items show as static (no spinner/pulse animations) */
|
||||||
|
isStreaming?: boolean;
|
||||||
responseActions?: Action[] | ActionsConfig;
|
responseActions?: Action[] | ActionsConfig;
|
||||||
className?: string;
|
className?: string;
|
||||||
onResponseAction?: (actionId: string) => void;
|
onResponseAction?: (actionId: string) => void;
|
||||||
|
|
@ -129,6 +142,7 @@ export const Plan: FC<PlanProps> = ({
|
||||||
todos,
|
todos,
|
||||||
maxVisibleTodos = 4,
|
maxVisibleTodos = 4,
|
||||||
showProgress = true,
|
showProgress = true,
|
||||||
|
isStreaming = true,
|
||||||
responseActions,
|
responseActions,
|
||||||
className,
|
className,
|
||||||
onResponseAction,
|
onResponseAction,
|
||||||
|
|
@ -176,7 +190,7 @@ export const Plan: FC<PlanProps> = ({
|
||||||
return (
|
return (
|
||||||
<Accordion type="single" collapsible className="w-full">
|
<Accordion type="single" collapsible className="w-full">
|
||||||
{items.map((todo) => (
|
{items.map((todo) => (
|
||||||
<TodoItem key={todo.id} todo={todo} />
|
<TodoItem key={todo.id} todo={todo} isStreaming={isStreaming} />
|
||||||
))}
|
))}
|
||||||
</Accordion>
|
</Accordion>
|
||||||
);
|
);
|
||||||
|
|
@ -185,7 +199,7 @@ export const Plan: FC<PlanProps> = ({
|
||||||
return (
|
return (
|
||||||
<div className="space-y-0">
|
<div className="space-y-0">
|
||||||
{items.map((todo) => (
|
{items.map((todo) => (
|
||||||
<TodoItem key={todo.id} todo={todo} />
|
<TodoItem key={todo.id} todo={todo} isStreaming={isStreaming} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { makeAssistantToolUI } from "@assistant-ui/react";
|
import { makeAssistantToolUI, useAssistantState } from "@assistant-ui/react";
|
||||||
import { useAtomValue, useSetAtom } from "jotai";
|
import { useAtomValue, useSetAtom } from "jotai";
|
||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
import { useEffect, useMemo } from "react";
|
import { useEffect, useMemo } from "react";
|
||||||
|
|
@ -92,6 +92,10 @@ export const WriteTodosToolUI = makeAssistantToolUI<WriteTodosArgs, WriteTodosRe
|
||||||
const updatePlanState = useSetAtom(updatePlanStateAtom);
|
const updatePlanState = useSetAtom(updatePlanStateAtom);
|
||||||
const planStates = useAtomValue(planStatesAtom);
|
const planStates = useAtomValue(planStatesAtom);
|
||||||
|
|
||||||
|
// Check if the THREAD is running (not just this tool)
|
||||||
|
// This hook subscribes to state changes, so it re-renders when thread stops
|
||||||
|
const isThreadRunning = useAssistantState(({ thread }) => thread.isRunning);
|
||||||
|
|
||||||
// Get the plan data (from result or args)
|
// Get the plan data (from result or args)
|
||||||
const planData = result || transformArgsToResult(args);
|
const planData = result || transformArgsToResult(args);
|
||||||
const rawTitle = planData?.title || args.title || "Planning Approach";
|
const rawTitle = planData?.title || args.title || "Planning Approach";
|
||||||
|
|
@ -140,7 +144,7 @@ export const WriteTodosToolUI = makeAssistantToolUI<WriteTodosArgs, WriteTodosRe
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loading state - tool is still running
|
// Loading state - tool is still running (no data yet)
|
||||||
if (status.type === "running" || status.type === "requires-action") {
|
if (status.type === "running" || status.type === "requires-action") {
|
||||||
// Try to show partial results from args while streaming
|
// Try to show partial results from args while streaming
|
||||||
const partialResult = transformArgsToResult(args);
|
const partialResult = transformArgsToResult(args);
|
||||||
|
|
@ -149,7 +153,7 @@ export const WriteTodosToolUI = makeAssistantToolUI<WriteTodosArgs, WriteTodosRe
|
||||||
return (
|
return (
|
||||||
<div className="my-4">
|
<div className="my-4">
|
||||||
<PlanErrorBoundary>
|
<PlanErrorBoundary>
|
||||||
<Plan {...plan} showProgress={true} />
|
<Plan {...plan} showProgress={true} isStreaming={isThreadRunning} />
|
||||||
</PlanErrorBoundary>
|
</PlanErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -159,17 +163,15 @@ export const WriteTodosToolUI = makeAssistantToolUI<WriteTodosArgs, WriteTodosRe
|
||||||
|
|
||||||
// Incomplete/cancelled state
|
// Incomplete/cancelled state
|
||||||
if (status.type === "incomplete") {
|
if (status.type === "incomplete") {
|
||||||
if (status.reason === "cancelled") {
|
// For cancelled or errors, try to show what we have from args or shared state
|
||||||
return null;
|
// Use isThreadRunning to determine if we should still animate
|
||||||
}
|
|
||||||
// For errors, try to show what we have from args or shared state
|
|
||||||
const fallbackResult = currentPlanState || transformArgsToResult(args);
|
const fallbackResult = currentPlanState || transformArgsToResult(args);
|
||||||
if (fallbackResult) {
|
if (fallbackResult) {
|
||||||
const plan = parseSerializablePlan(fallbackResult);
|
const plan = parseSerializablePlan(fallbackResult);
|
||||||
return (
|
return (
|
||||||
<div className="my-4">
|
<div className="my-4">
|
||||||
<PlanErrorBoundary>
|
<PlanErrorBoundary>
|
||||||
<Plan {...plan} showProgress={true} />
|
<Plan {...plan} showProgress={true} isStreaming={isThreadRunning} />
|
||||||
</PlanErrorBoundary>
|
</PlanErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -178,7 +180,8 @@ export const WriteTodosToolUI = makeAssistantToolUI<WriteTodosArgs, WriteTodosRe
|
||||||
}
|
}
|
||||||
|
|
||||||
// Success - render the plan using the LATEST shared state
|
// Success - render the plan using the LATEST shared state
|
||||||
// This way, even if our result is old, we show the latest data
|
// Use isThreadRunning to determine if we should animate in_progress items
|
||||||
|
// (LLM may still be working on tasks even though this tool call completed)
|
||||||
const planToRender = currentPlanState || result;
|
const planToRender = currentPlanState || result;
|
||||||
if (!planToRender) {
|
if (!planToRender) {
|
||||||
return <WriteTodosLoading />;
|
return <WriteTodosLoading />;
|
||||||
|
|
@ -188,7 +191,7 @@ export const WriteTodosToolUI = makeAssistantToolUI<WriteTodosArgs, WriteTodosRe
|
||||||
return (
|
return (
|
||||||
<div className="my-4">
|
<div className="my-4">
|
||||||
<PlanErrorBoundary>
|
<PlanErrorBoundary>
|
||||||
<Plan {...plan} showProgress={true} />
|
<Plan {...plan} showProgress={true} isStreaming={isThreadRunning} />
|
||||||
</PlanErrorBoundary>
|
</PlanErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue