import type { Message } from "@langchain/langgraph-sdk"; import { BookOpenTextIcon, FolderOpenIcon, GlobeIcon, LightbulbIcon, ListTreeIcon, NotebookPenIcon, SearchIcon, SquareTerminalIcon, WrenchIcon, } from "lucide-react"; import { useMemo, useState } from "react"; import { ChainOfThought, ChainOfThoughtContent, ChainOfThoughtHeader, ChainOfThoughtSearchResult, ChainOfThoughtSearchResults, ChainOfThoughtStep, } from "@/components/ai-elements/chain-of-thought"; import { MessageResponse } from "@/components/ai-elements/message"; import { extractReasoningContentFromMessage, findToolCallResult, } from "@/core/messages/utils"; import { useRehypeSplitWordsIntoSpans } from "@/core/rehype"; import { extractTitleFromMarkdown } from "@/core/utils/markdown"; import { cn } from "@/lib/utils"; export function MessageGroup({ className, messages, isLoading = false, }: { className?: string; messages: Message[]; isLoading?: boolean; }) { const steps = useMemo(() => convertToSteps(messages), [messages]); const rehypePlugins = useRehypeSplitWordsIntoSpans(isLoading); const [open, setOpen] = useState(false); const lastStep = steps[steps.length - 1]; const { label, icon } = describeStep(lastStep); return ( 1 ? : icon } > {open && steps.length > 1 ? ( {steps.length} steps ) : ( {label} )} {!open && steps.length > 1 && ( {steps.length - 1} more step {steps.length - 1 > 1 ? "s" : ""} )} {steps.map((step) => step.type === "reasoning" ? ( {step.reasoning ?? ""} } /> ) : ( ), )} ); } function ToolCall({ id, name, args, result, }: { id?: string; name: string; args: Record; result?: string | Record; }) { if (name === "web_search") { let label: React.ReactNode = "Search for related information"; if (typeof args.query === "string") { label = ( Search on the web for{" "} "{args.query}" ); } return ( {Array.isArray(result) && ( {result.map((item) => ( {item.title} ))} )} ); } else if (name === "web_fetch") { const url = (args as { url: string })?.url; let title = url; if (typeof result === "string") { const potentialTitle = extractTitleFromMarkdown(result); if (potentialTitle && potentialTitle.toLowerCase() !== "untitled") { title = potentialTitle; } } return ( {url && ( {title} )} ); } else if (name === "ls") { let description: string | undefined = (args as { description: string }) ?.description; if (!description) { description = "List folder"; } const path: string | undefined = (args as { path: string })?.path; return ( {path && ( {path} )} ); } else if (name === "read_file") { let description: string | undefined = (args as { description: string }) ?.description; if (!description) { description = "Read file"; } const path: string | undefined = (args as { path: string })?.path; return ( {path && ( {path} )} ); } else if (name === "write_file" || name === "str_replace") { let description: string | undefined = (args as { description: string }) ?.description; if (!description) { description = "Write file"; } const path: string | undefined = (args as { path: string })?.path; return ( {path && ( {path} )} ); } else if (name === "bash") { const description: string | undefined = (args as { description: string }) ?.description; if (!description) { return "Execute command"; } const command: string | undefined = (args as { command: string })?.command; return ( {command && ( {command} )} ); } else { const description: string | undefined = (args as { description: string }) ?.description; return ( Use "{name}" tool ) } icon={WrenchIcon} > ); } } interface GenericCoTStep { id?: string; type: T; } interface CoTReasoningStep extends GenericCoTStep<"reasoning"> { reasoning: string | null; } interface CoTToolCallStep extends GenericCoTStep<"toolCall"> { name: string; args: Record; result?: string; } type CoTStep = CoTReasoningStep | CoTToolCallStep; function convertToSteps(messages: Message[]): CoTStep[] { const steps: CoTStep[] = []; for (const message of messages) { if (message.type === "ai") { const reasoning = extractReasoningContentFromMessage(message); if (reasoning) { const step: CoTReasoningStep = { id: message.id, type: "reasoning", reasoning: extractReasoningContentFromMessage(message), }; steps.push(step); } for (const tool_call of message.tool_calls ?? []) { const step: CoTToolCallStep = { id: tool_call.id, type: "toolCall", name: tool_call.name, args: tool_call.args, }; const toolCallId = tool_call.id; if (toolCallId) { const toolCallResult = findToolCallResult(toolCallId, messages); if (toolCallResult) { try { const json = JSON.parse(toolCallResult); step.result = json; } catch { step.result = toolCallResult; } } } steps.push(step); } } } return steps; } function describeStep(step: CoTStep | undefined): { label: string; icon: React.ReactElement; } { if (!step) { return { label: "Thinking", icon: }; } if (step.type === "reasoning") { return { label: "Thinking", icon: }; } else { let label: string; let icon: React.ReactElement = ; if (step.name === "web_search") { label = `Search "${(step.args as { query: string }).query}" on web`; icon = ; } else if (step.name === "web_fetch") { label = "View web page"; icon = ; } else if (step.name === "ls") { label = "List folder"; icon = ; } else if (step.name === "read_file") { label = "Read file"; icon = ; } else if (step.name === "write_file" || step.name === "str_replace") { label = "Write file"; icon = ; } else if (step.name === "bash") { label = "Execute command"; icon = ; } else { label = `Call tool "${step.name}"`; icon = ; } if (typeof step.args.description === "string") { label = step.args.description; } return { label, icon }; } }