import type { Message } from "@langchain/langgraph-sdk"; import { BookOpenTextIcon, ChevronUp, FileTextIcon, FolderOpenIcon, GlobeIcon, LightbulbIcon, MessageCircleQuestionMarkIcon, NotebookPenIcon, SearchIcon, SquareTerminalIcon, WrenchIcon, } from "lucide-react"; import { useMemo, useState } from "react"; import { ChainOfThought, ChainOfThoughtContent, ChainOfThoughtSearchResult, ChainOfThoughtSearchResults, ChainOfThoughtStep, } from "@/components/ai-elements/chain-of-thought"; import { CodeBlock } from "@/components/ai-elements/code-block"; import { MessageResponse } from "@/components/ai-elements/message"; import { Button } from "@/components/ui/button"; import { useI18n } from "@/core/i18n/hooks"; import { extractReasoningContentFromMessage, findToolCallResult, } from "@/core/messages/utils"; import { useRehypeSplitWordsIntoSpans } from "@/core/rehype"; import { extractTitleFromMarkdown } from "@/core/utils/markdown"; import { cn } from "@/lib/utils"; import { useArtifacts } from "../artifacts"; import { FlipDisplay } from "../flip-display"; export function MessageGroup({ className, messages, isLoading = false, }: { className?: string; messages: Message[]; isLoading?: boolean; }) { const { t } = useI18n(); const [showAbove, setShowAbove] = useState(false); const [showLastThinking, setShowLastThinking] = useState(false); const steps = useMemo(() => convertToSteps(messages), [messages]); const lastToolCallStep = useMemo(() => { const filteredSteps = steps.filter((step) => step.type === "toolCall"); return filteredSteps[filteredSteps.length - 1]; }, [steps]); const aboveLastToolCallSteps = useMemo(() => { if (lastToolCallStep) { const index = steps.indexOf(lastToolCallStep); return steps.slice(0, index); } return []; }, [lastToolCallStep, steps]); const lastReasoningStep = useMemo(() => { if (lastToolCallStep) { const index = steps.indexOf(lastToolCallStep); return steps.slice(index + 1).find((step) => step.type === "reasoning"); } else { const filteredSteps = steps.filter((step) => step.type === "reasoning"); return filteredSteps[filteredSteps.length - 1]; } }, [lastToolCallStep, steps]); const rehypePlugins = useRehypeSplitWordsIntoSpans(isLoading); return ( {aboveLastToolCallSteps.length > 0 && ( )} {aboveLastToolCallSteps.length > 0 && ( {showAbove && aboveLastToolCallSteps.map((step) => step.type === "reasoning" ? ( {step.reasoning ?? ""} } > ) : ( ), )} {lastToolCallStep && ( )} )} {lastReasoningStep && ( <> {showLastThinking && ( {lastReasoningStep.reasoning ?? ""} } > )} )} ); } function ToolCall({ id, messageId, name, args, result, }: { id?: string; messageId?: string; name: string; args: Record; result?: string | Record; }) { const { t } = useI18n(); const { setOpen, autoOpen, selectedArtifact, select } = useArtifacts(); if (name === "web_search") { let label: React.ReactNode = t.toolCalls.searchForRelatedInfo; if (typeof args.query === "string") { label = t.toolCalls.searchOnWebFor(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 ( { window.open(url, "_blank"); }} > {url && ( {title} )} ); } else if (name === "ls") { let description: string | undefined = (args as { description: string }) ?.description; if (!description) { description = t.toolCalls.listFolder; } 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 = t.toolCalls.readFile; } 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 = t.toolCalls.writeFile; } const path: string | undefined = (args as { path: string })?.path; if (autoOpen && path) { setTimeout(() => { const url = new URL( `write-file:${path}?message_id=${messageId}&tool_call_id=${id}`, ).toString(); if (selectedArtifact === url) { return; } select(url); setOpen(true); }, 100); } return ( { select( new URL( `write-file:${path}?message_id=${messageId}&tool_call_id=${id}`, ).toString(), ); setOpen(true); }} > {path && ( {path} )} ); } else if (name === "bash") { const description: string | undefined = (args as { description: string }) ?.description; if (!description) { return t.toolCalls.executeCommand; } const command: string | undefined = (args as { command: string })?.command; return ( {command && ( )} ); } else if (name === "present_files") { return ( {Array.isArray((args as { filepaths: string[] }).filepaths) && (args as { filepaths: string[] }).filepaths.map( (filepath: string) => ( {filepath} ), )} ); } if (name === "ask_clarification") { return ( ); } else { const description: string | undefined = (args as { description: string }) ?.description; return ( ); } } interface GenericCoTStep { id?: string; messageId?: 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, messageId: 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, messageId: message.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; }