diff --git a/surfsense_web/components/assistant-ui/thinking-steps.tsx b/surfsense_web/components/assistant-ui/thinking-steps.tsx index 219e06ae5..cf0c4ce52 100644 --- a/surfsense_web/components/assistant-ui/thinking-steps.tsx +++ b/surfsense_web/components/assistant-ui/thinking-steps.tsx @@ -4,9 +4,15 @@ import type { FC } from "react"; import { useCallback, useEffect, 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"; +export interface ThinkingStep { + id: string; + title: string; + items: string[]; + status: "pending" | "in_progress" | "completed"; +} + /** * Chain of thought display component - single collapsible dropdown design */ @@ -120,8 +126,8 @@ export const ThinkingStepsDisplay: FC<{ steps: ThinkingStep[]; isThreadRunning?: {step.items && step.items.length > 0 && (
- {step.items.map((item, idx) => ( - + {step.items.map((item) => ( + {item} ))} diff --git a/surfsense_web/components/tool-ui/deepagent-thinking.tsx b/surfsense_web/components/tool-ui/deepagent-thinking.tsx deleted file mode 100644 index c495c1723..000000000 --- a/surfsense_web/components/tool-ui/deepagent-thinking.tsx +++ /dev/null @@ -1,400 +0,0 @@ -"use client"; - -import type { ToolCallMessagePartProps } from "@assistant-ui/react"; -import { Brain, CheckCircle2, Loader2, Search, Sparkles } from "lucide-react"; -import type { FC, ReactNode } from "react"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { z } from "zod"; -import { - ChainOfThought, - ChainOfThoughtContent, - ChainOfThoughtItem, - ChainOfThoughtStep, - ChainOfThoughtTrigger, -} from "@/components/prompt-kit/chain-of-thought"; -import { cn } from "@/lib/utils"; - -// ============================================================================ -// Constants -// ============================================================================ - -/** Step status values */ -const STEP_STATUS = { - PENDING: "pending", - IN_PROGRESS: "in_progress", - COMPLETED: "completed", -} as const; - -/** Agent thinking status values */ -const THINKING_STATUS = { - THINKING: "thinking", - SEARCHING: "searching", - SYNTHESIZING: "synthesizing", - COMPLETED: "completed", -} as const; - -/** Keywords for icon detection */ -const STEP_KEYWORDS = { - SEARCH: ["search", "knowledge"] as const, - ANALYSIS: ["analy", "understand"] as const, -} as const; - -/** Icon size class */ -const ICON_SIZE_CLASS = "size-4" as const; - -/** Status text mapping */ -const STATUS_TEXT_MAP: Record = { - [THINKING_STATUS.SEARCHING]: "Searching knowledge base...", - [THINKING_STATUS.SYNTHESIZING]: "Synthesizing response...", - [THINKING_STATUS.THINKING]: "Thinking...", -} as const; - -// ============================================================================ -// Type Definitions -// ============================================================================ - -type StepStatus = (typeof STEP_STATUS)[keyof typeof STEP_STATUS]; -type ThinkingStatus = (typeof THINKING_STATUS)[keyof typeof THINKING_STATUS]; - -// ============================================================================ -// Zod Schemas -// ============================================================================ - -const ThinkingStepSchema = z.object({ - id: z.string(), - title: z.string(), - items: z.array(z.string()).default([]), - status: z - .enum([STEP_STATUS.PENDING, STEP_STATUS.IN_PROGRESS, STEP_STATUS.COMPLETED]) - .default(STEP_STATUS.PENDING), -}); - -const DeepAgentThinkingArgsSchema = z.object({ - query: z.string().nullish(), - context: z.string().nullish(), -}); - -const DeepAgentThinkingResultSchema = z.object({ - steps: z.array(ThinkingStepSchema).nullish(), - status: z - .enum([ - THINKING_STATUS.THINKING, - THINKING_STATUS.SEARCHING, - THINKING_STATUS.SYNTHESIZING, - THINKING_STATUS.COMPLETED, - ]) - .nullish(), - summary: z.string().nullish(), -}); - -/** Types derived from Zod schemas */ -type ThinkingStep = z.infer; -type DeepAgentThinkingArgs = z.infer; -type DeepAgentThinkingResult = z.infer; - -// ============================================================================ -// Parser Functions -// ============================================================================ - -/** Default fallback step when parsing fails */ -const DEFAULT_FALLBACK_STEP: ThinkingStep = { - id: "unknown", - title: "Processing...", - items: [], - status: STEP_STATUS.PENDING, -} as const; - -/** - * Parse and validate a single thinking step - */ -export function parseThinkingStep(data: unknown): ThinkingStep { - const result = ThinkingStepSchema.safeParse(data); - if (!result.success) { - console.warn("Invalid thinking step data:", result.error.issues); - return DEFAULT_FALLBACK_STEP; - } - return result.data; -} - -/** - * Parse and validate thinking result - */ -export function parseThinkingResult(data: unknown): DeepAgentThinkingResult { - const result = DeepAgentThinkingResultSchema.safeParse(data); - if (!result.success) { - console.warn("Invalid thinking result data:", result.error.issues); - return {}; - } - return result.data; -} - -// ============================================================================ -// Icon Utilities -// ============================================================================ - -/** - * Check if title contains any of the keywords - */ -function titleContainsKeywords(title: string, keywords: readonly string[]): boolean { - const titleLower = title.toLowerCase(); - return keywords.some((keyword) => titleLower.includes(keyword)); -} - -/** - * Get icon based on step status and title - */ -function getStepIcon(status: StepStatus, title: string): ReactNode { - if (status === STEP_STATUS.IN_PROGRESS) { - return ; - } - - if (status === STEP_STATUS.COMPLETED) { - return ; - } - - // Default icons based on step type keywords - if (titleContainsKeywords(title, STEP_KEYWORDS.SEARCH)) { - return ; - } - - if (titleContainsKeywords(title, STEP_KEYWORDS.ANALYSIS)) { - return ; - } - - return ; -} - -// ============================================================================ -// Sub-Components -// ============================================================================ - -interface ThinkingStepDisplayProps { - step: ThinkingStep; - isOpen: boolean; - onToggle: () => void; -} - -/** - * Component to display a single thinking step with controlled open state - */ -const ThinkingStepDisplay: FC = ({ step, isOpen, onToggle }) => { - const icon = useMemo(() => getStepIcon(step.status, step.title), [step.status, step.title]); - - const isInProgress = step.status === STEP_STATUS.IN_PROGRESS; - const isCompleted = step.status === STEP_STATUS.COMPLETED; - - return ( - - - {step.title} - - - {step.items.map((item, index) => ( - {item} - ))} - - - ); -}; - -interface ThinkingLoadingStateProps { - status?: ThinkingStatus | string; -} - -/** - * Loading state with animated thinking indicator - */ -const ThinkingLoadingState: FC = ({ status }) => { - const statusText = useMemo(() => { - if (status && status in STATUS_TEXT_MAP) { - return STATUS_TEXT_MAP[status]; - } - return STATUS_TEXT_MAP[THINKING_STATUS.THINKING]; - }, [status]); - - return ( -
-
- - - - - -
- {statusText} -
- ); -}; - -interface SmartChainOfThoughtProps { - steps: ThinkingStep[]; -} - -/** Type for tracking step override states */ -type StepOverrides = Record; - -/** Type for tracking step status history */ -type StepStatusHistory = Record; - -/** - * Smart chain of thought renderer with state management - */ -const SmartChainOfThought: FC = ({ steps }) => { - // Track which steps the user has manually toggled - const [manualOverrides, setManualOverrides] = useState({}); - // Track previous step statuses to detect changes - const prevStatusesRef = useRef({}); - - // Clear manual overrides when a step's status changes - useEffect(() => { - const currentStatuses: StepStatusHistory = {}; - steps.forEach((step) => { - currentStatuses[step.id] = step.status; - // If status changed, clear any manual override for this step - const prevStatus = prevStatusesRef.current[step.id]; - if (prevStatus && prevStatus !== step.status) { - setManualOverrides((prev) => { - const next = { ...prev }; - delete next[step.id]; - return next; - }); - } - }); - prevStatusesRef.current = currentStatuses; - }, [steps]); - - const getStepOpenState = useCallback( - (step: ThinkingStep): boolean => { - // If user has manually toggled, respect that - if (manualOverrides[step.id] !== undefined) { - return manualOverrides[step.id]; - } - // Auto behavior: open if in progress - if (step.status === STEP_STATUS.IN_PROGRESS) { - return true; - } - // Default: collapsed (all steps collapse when processing is done) - return false; - }, - [manualOverrides] - ); - - const handleToggle = useCallback((stepId: string, currentOpen: boolean) => { - setManualOverrides((prev) => ({ - ...prev, - [stepId]: !currentOpen, - })); - }, []); - - return ( - - {steps.map((step) => { - const isOpen = getStepOpenState(step); - return ( - handleToggle(step.id, isOpen)} - /> - ); - })} - - ); -}; - -/** - * DeepAgent Thinking Tool UI Component - * - * This component displays the agent's chain-of-thought reasoning - * when the deepagent is processing a query. It shows thinking steps - * in a collapsible, hierarchical format. - */ -export const DeepAgentThinkingToolUI = ({ result, status }: ToolCallMessagePartProps) => { - // Loading state - tool is still running - if (status.type === "running" || status.type === "requires-action") { - return ; - } - - // Incomplete/cancelled state - if (status.type === "incomplete") { - if (status.reason === "cancelled") { - return null; // Don't show anything if cancelled - } - if (status.reason === "error") { - return null; // Don't show error for thinking - it's not critical - } - } - - // No result or no steps - don't render anything - if (!result?.steps || result.steps.length === 0) { - return null; - } - - // Render the chain of thought - return ( -
- -
- ); -}; - -// ============================================================================ -// Public Components -// ============================================================================ - -export interface InlineThinkingDisplayProps { - /** The thinking steps to display */ - steps: ThinkingStep[]; - /** Whether content is currently streaming */ - isStreaming?: boolean; - /** Additional CSS class names */ - className?: string; -} - -/** - * Inline Thinking Display Component - * - * A simpler version that can be used inline with the message content - * for displaying reasoning without the full tool UI infrastructure. - */ -export const InlineThinkingDisplay: FC = ({ - steps, - isStreaming = false, - className, -}) => { - if (steps.length === 0 && !isStreaming) { - return null; - } - - return ( -
- {isStreaming && steps.length === 0 ? ( - - ) : ( - - )} -
- ); -}; - -// ============================================================================ -// Exports -// ============================================================================ - -export type { - ThinkingStep, - DeepAgentThinkingArgs, - DeepAgentThinkingResult, - StepStatus, - ThinkingStatus, -}; - -export { STEP_STATUS, THINKING_STATUS }; diff --git a/surfsense_web/components/tool-ui/index.ts b/surfsense_web/components/tool-ui/index.ts index 65c0ca497..c9ea4df41 100644 --- a/surfsense_web/components/tool-ui/index.ts +++ b/surfsense_web/components/tool-ui/index.ts @@ -16,13 +16,6 @@ export { type SerializableArticle, } from "./article"; export { Audio } from "./audio"; -export { - type DeepAgentThinkingArgs, - type DeepAgentThinkingResult, - DeepAgentThinkingToolUI, - InlineThinkingDisplay, - type ThinkingStep, -} from "./deepagent-thinking"; export { type DisplayImageArgs, DisplayImageArgsSchema,