import type { Message } from "@langchain/langgraph-sdk"; import { BookOpenTextIcon, ChevronUp, FolderOpenIcon, GlobeIcon, LightbulbIcon, ListTodoIcon, 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 { CitationsLoadingIndicator } from "@/components/ai-elements/inline-citation"; import { MessageResponse } from "@/components/ai-elements/message"; import { Button } from "@/components/ui/button"; import { parseCitations } from "@/core/citations"; import { useI18n } from "@/core/i18n/hooks"; import { extractReasoningContentFromMessage, findToolCallResult, } from "@/core/messages/utils"; import { useRehypeSplitWordsIntoSpans } from "@/core/rehype"; import { streamdownPlugins } from "@/core/streamdown"; import { extractTitleFromMarkdown } from "@/core/utils/markdown"; import { env } from "@/env"; import { cn } from "@/lib/utils"; import { useArtifacts } from "../artifacts"; import { FlipDisplay } from "../flip-display"; import { Tooltip } from "../tooltip"; import { useThread } from "./context"; export function MessageGroup({ className, messages, isLoading = false, }: { className?: string; messages: Message[]; isLoading?: boolean; }) { const { t } = useI18n(); const [showAbove, setShowAbove] = useState( env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true", ); const [showLastThinking, setShowLastThinking] = useState( env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true", ); 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 && ( )} {lastToolCallStep && ( {showAbove && aboveLastToolCallSteps.map((step) => step.type === "reasoning" ? ( {parseCitations(step.reasoning ?? "").cleanContent} } > ) : ( ), )} {lastToolCallStep && ( )} )} {lastReasoningStep && ( <> {showLastThinking && ( {parseCitations(lastReasoningStep.reasoning ?? "").cleanContent} } > )} )} ); } function ToolCall({ id, messageId, name, args, result, isLast = false, isLoading = false, }: { id?: string; messageId?: string; name: string; args: Record; result?: string | Record; isLast?: boolean; isLoading?: boolean; }) { const { t } = useI18n(); const { setOpen, autoOpen, autoSelect, selectedArtifact, select } = useArtifacts(); const { thread } = useThread(); const threadIsLoading = thread.isLoading; // Move useMemo to top level to comply with React Hooks rules const fileContent = typeof args.content === "string" ? args.content : ""; const { citations } = useMemo(() => parseCitations(fileContent), [fileContent]); 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 === "image_search") { let label: React.ReactNode = t.toolCalls.searchForRelatedImages; if (typeof args.query === "string") { label = t.toolCalls.searchForRelatedImagesFor(args.query); } const results = ( result as { results: { source_url: string; thumbnail_url: string; image_url: string; title: string; }[]; } )?.results; return ( {Array.isArray(results) && ( {Array.isArray(results) && results.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 && ( {result as string}}> {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 && ( {result as string}}> {path} )} ); } else if (name === "read_file") { let description: string | undefined = (args as { description: string }) ?.description; if (!description) { description = t.toolCalls.readFile; } const { path } = args as { path: string; content: string }; return ( {path && ( {result as string} } > {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 (isLoading && isLast && autoOpen && autoSelect && path) { setTimeout(() => { const url = new URL( `write-file:${path}?message_id=${messageId}&tool_call_id=${id}`, ).toString(); if (selectedArtifact === url) { return; } select(url, true); setOpen(true); }, 100); } // Check if this is a markdown file with citations const isMarkdown = path?.toLowerCase().endsWith(".md") || path?.toLowerCase().endsWith(".markdown"); const hasCitationsBlock = fileContent.includes(""); const showCitationsLoading = isMarkdown && threadIsLoading && hasCitationsBlock && isLast; return ( <> { select( new URL( `write-file:${path}?message_id=${messageId}&tool_call_id=${id}`, ).toString(), ); setOpen(true); }} > {path && ( {path} )} {showCitationsLoading && (
)} ); } 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 && ( {result as string}}> )} ); } else if (name === "ask_clarification") { return ( ); } else if (name === "write_todos") { 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; }