"use client"; import { CheckCircle2, Circle, CircleDashed, ListTodo, PartyPopper, XCircle } from "lucide-react"; import type { FC } from "react"; import { useMemo, useState } from "react"; import { TextShimmerLoader } from "@/components/prompt-kit/loader"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { Progress } from "@/components/ui/progress"; import { cn } from "@/lib/utils"; import type { Action, ActionsConfig } from "../shared/schema"; import type { TodoStatus } from "./schema"; // ============================================================================ // Status Icon Component // ============================================================================ interface StatusIconProps { status: TodoStatus; className?: string; /** When false, in_progress items show as static (no spinner) */ isStreaming?: boolean; } const StatusIcon: FC = ({ status, className, isStreaming = true }) => { const baseClass = cn("size-4 shrink-0", className); switch (status) { case "completed": return ; case "in_progress": // Only animate the spinner if we're actively streaming // When streaming is stopped, show as a static dashed circle return ( ); case "cancelled": return ; case "pending": default: return ; } }; // ============================================================================ // Todo Item Component // ============================================================================ interface TodoItemProps { todo: { id: string; content: string; status: TodoStatus }; /** When false, in_progress items show as static (no spinner/pulse) */ isStreaming?: boolean; } const TodoItem: FC = ({ todo, isStreaming = true }) => { const isStrikethrough = todo.status === "completed" || todo.status === "cancelled"; // Only show shimmer animation if streaming and in progress const isShimmer = todo.status === "in_progress" && isStreaming; // Render the content with optional shimmer effect const renderContent = () => { if (isShimmer) { return ; } return ( {todo.content} ); }; return (
{renderContent()}
); }; // ============================================================================ // Plan Component // ============================================================================ export interface PlanProps { id: string; title: string; todos: Array<{ id: string; content: string; status: TodoStatus }>; maxVisibleTodos?: number; showProgress?: boolean; /** When false, in_progress items show as static (no spinner/pulse animations) */ isStreaming?: boolean; responseActions?: Action[] | ActionsConfig; className?: string; onResponseAction?: (actionId: string) => void; onBeforeResponseAction?: (actionId: string) => boolean; } export const Plan: FC = ({ id, title, todos, maxVisibleTodos = 4, showProgress = true, isStreaming = true, responseActions, className, onResponseAction, onBeforeResponseAction, }) => { const [isExpanded, setIsExpanded] = useState(false); // Calculate progress const progress = useMemo(() => { const completed = todos.filter((t) => t.status === "completed").length; const total = todos.filter((t) => t.status !== "cancelled").length; return { completed, total, percentage: total > 0 ? (completed / total) * 100 : 0 }; }, [todos]); const isAllComplete = progress.completed === progress.total && progress.total > 0; // Split todos for collapsible display const visibleTodos = todos.slice(0, maxVisibleTodos); const hiddenTodos = todos.slice(maxVisibleTodos); const hasHiddenTodos = hiddenTodos.length > 0; // Handle action click const handleAction = (actionId: string) => { if (onBeforeResponseAction && !onBeforeResponseAction(actionId)) { return; } onResponseAction?.(actionId); }; // Normalize actions to array const actionArray: Action[] = useMemo(() => { if (!responseActions) return []; if (Array.isArray(responseActions)) return responseActions; return [ responseActions.confirm && { ...responseActions.confirm, id: "confirm" }, responseActions.cancel && { ...responseActions.cancel, id: "cancel" }, ].filter(Boolean) as Action[]; }, [responseActions]); const TodoList: FC<{ items: typeof todos }> = ({ items }) => { return (
{items.map((todo) => ( ))}
); }; return (
{title}
{isAllComplete && (
)}
{showProgress && (
{progress.completed} of {progress.total} complete {Math.round(progress.percentage)}%
)}
{hasHiddenTodos && ( )} {actionArray.length > 0 && (
{actionArray.map((action) => ( ))}
)}
); };