import { useAssistantState, useThreadViewport } from "@assistant-ui/react"; import { ChevronRightIcon } from "lucide-react"; import type { FC } from "react"; import { createContext, useCallback, useContext, useEffect, useRef, useState } from "react"; import { ChainOfThoughtItem } from "@/components/prompt-kit/chain-of-thought"; import { TextShimmerLoader } from "@/components/prompt-kit/loader"; import type { ThinkingStep } from "@/components/tool-ui/deepagent-thinking"; import { cn } from "@/lib/utils"; // Context to pass thinking steps to AssistantMessage export const ThinkingStepsContext = createContext>(new Map()); /** * Chain of thought display component - single collapsible dropdown design */ export const ThinkingStepsDisplay: FC<{ steps: ThinkingStep[]; isThreadRunning?: boolean }> = ({ steps, isThreadRunning = true, }) => { const [isOpen, setIsOpen] = useState(true); // Derive effective status for each step const getEffectiveStatus = useCallback( (step: ThinkingStep): "pending" | "in_progress" | "completed" => { if (step.status === "in_progress" && !isThreadRunning) { return "completed"; } return step.status; }, [isThreadRunning] ); // Calculate summary info const completedSteps = steps.filter((s) => getEffectiveStatus(s) === "completed").length; const inProgressStep = steps.find((s) => getEffectiveStatus(s) === "in_progress"); const allCompleted = completedSteps === steps.length && steps.length > 0 && !isThreadRunning; const isProcessing = isThreadRunning && !allCompleted; // Auto-collapse when all tasks are completed useEffect(() => { if (allCompleted) { setIsOpen(false); } }, [allCompleted]); if (steps.length === 0) return null; // Generate header text const getHeaderText = () => { if (allCompleted) { return `Reviewed ${completedSteps} ${completedSteps === 1 ? "step" : "steps"}`; } if (inProgressStep) { return inProgressStep.title; } if (isProcessing) { return `Processing ${completedSteps}/${steps.length} steps`; } return `Reviewed ${completedSteps} ${completedSteps === 1 ? "step" : "steps"}`; }; return (
{/* Main collapsible header */} {/* Collapsible content with CSS grid animation */}
{steps.map((step, index) => { const effectiveStatus = getEffectiveStatus(step); const isLast = index === steps.length - 1; return (
{/* Dot and line column */}
{/* Vertical connection line - extends to next dot */} {!isLast && (
)} {/* Step dot - on top of line */}
{effectiveStatus === "in_progress" ? ( ) : ( )}
{/* Step content */}
{/* Step title */}
{effectiveStatus === "in_progress" ? ( ) : ( step.title )}
{/* Step items (sub-content) */} {step.items && step.items.length > 0 && (
{step.items.map((item, idx) => ( {item} ))}
)}
); })}
); }; /** * Component that handles auto-scroll when thinking steps update. * Uses useThreadViewport to scroll to bottom when thinking steps change, * ensuring the user always sees the latest content during streaming. */ export const ThinkingStepsScrollHandler: FC = () => { const thinkingStepsMap = useContext(ThinkingStepsContext); const viewport = useThreadViewport(); const isRunning = useAssistantState(({ thread }) => thread.isRunning); // Track the serialized state to detect any changes const prevStateRef = useRef(""); useEffect(() => { // Only act during streaming if (!isRunning) { prevStateRef.current = ""; return; } // Serialize the thinking steps state to detect any changes // This catches new steps, status changes, and item additions let stateString = ""; thinkingStepsMap.forEach((steps, msgId) => { steps.forEach((step) => { stateString += `${msgId}:${step.id}:${step.status}:${step.items?.length || 0};`; }); }); // If state changed at all during streaming, scroll if (stateString !== prevStateRef.current && stateString !== "") { prevStateRef.current = stateString; // Multiple attempts to ensure scroll happens after DOM updates const scrollAttempt = () => { try { viewport.scrollToBottom(); } catch { // Ignore errors - viewport might not be ready } }; // Delayed attempts to handle async DOM updates requestAnimationFrame(scrollAttempt); setTimeout(scrollAttempt, 100); } }, [thinkingStepsMap, viewport, isRunning]); return null; // This component doesn't render anything };