"use client" import { useState } from "react" import type { UIMessage } from "@ai-sdk/react" import { Streamdown } from "streamdown" import { BookOpenIcon, ChevronDownIcon, ChevronRightIcon, ClockIcon, GlobeIcon, ListIcon, Loader2, PlusIcon, SearchIcon, TerminalIcon, WrenchIcon, XCircleIcon, } from "lucide-react" import { cn } from "@lib/utils" import { isWebSearchToolName } from "@/lib/chat-web-search-tools" import { modelNames, type ModelId } from "@/lib/models" import { RelatedMemories } from "./related-memories" import { MessageActions } from "./message-actions" const TOOL_META: Record = { bash: { label: "Memory", icon: TerminalIcon }, web_search: { label: "Web search", icon: GlobeIcon }, google_search: { label: "Google search", icon: GlobeIcon }, // legacy tool names kept for existing persisted messages searchMemories: { label: "Search Memories", icon: SearchIcon }, addMemory: { label: "Add Memory", icon: PlusIcon }, fetchMemory: { label: "Fetch Memory", icon: BookOpenIcon }, scheduleTask: { label: "Schedule Task", icon: ClockIcon }, listSchedules: { label: "List Schedules", icon: ListIcon }, cancelSchedule: { label: "Cancel Schedule", icon: XCircleIcon }, } type ToolCallDisplayPart = { type: string state: string input?: unknown output?: unknown toolCallId?: string errorText?: string } type SourceUrlPart = { type: "source-url" sourceId: string url: string title?: string } function WebSourcesGroup({ sources }: { sources: SourceUrlPart[] }) { const [expanded, setExpanded] = useState(false) if (sources.length === 0) return null return (
{expanded && ( )}
) } function BashToolDisplay({ part }: { part: ToolCallDisplayPart }) { const [expanded, setExpanded] = useState(false) const isLoading = part.state === "input-streaming" || part.state === "input-available" const isDone = part.state === "output-available" const isError = part.state === "error" || part.state === "output-error" const cmd = part.input && typeof part.input === "object" && "cmd" in part.input ? String((part.input as { cmd: string }).cmd) : undefined const output = isDone && part.output && typeof part.output === "object" ? (part.output as { stdout?: string; stderr?: string; exitCode?: number }) : undefined const hasOutput = output && ((output.stdout && output.stdout.length > 0) || (output.stderr && output.stderr.length > 0)) const errorText = part.errorText const hasExpandable = hasOutput || (isError && errorText) return (
{expanded && (hasOutput || (isError && errorText)) && (
{output?.stdout && output.stdout.length > 0 && (
							{output.stdout}
						
)} {output?.stderr && output.stderr.length > 0 && (
							{output.stderr}
						
)} {isError && errorText && (
							{errorText}
						
)}
)}
) } function ToolCallDisplay({ part }: { part: ToolCallDisplayPart }) { const [expanded, setExpanded] = useState(false) const toolName = part.type.replace("tool-", "") if (toolName === "bash") { return } const meta = TOOL_META[toolName] ?? (isWebSearchToolName(toolName) ? { label: "Web search", icon: GlobeIcon } : undefined) const Icon = meta?.icon ?? WrenchIcon const label = meta?.label ?? toolName const isLoading = part.state === "input-streaming" || part.state === "input-available" const isDone = part.state === "output-available" const isError = part.state === "error" || part.state === "output-error" const errorText = part.errorText return (
{expanded && (
{part.input !== undefined && (
Input
								{typeof part.input === "string"
									? part.input
									: JSON.stringify(part.input, null, 2)}
							
)} {isDone && part.output !== undefined && (
Output
								{typeof part.output === "string"
									? part.output
									: JSON.stringify(part.output, null, 2)}
							
)} {isError && errorText && (
Error
								{errorText}
							
)}
)}
) } interface AgentMessageProps { message: UIMessage index: number messagesLength: number hoveredMessageId: string | null copiedMessageId: string | null messageFeedback: Record expandedMemories: string | null responseModel: ModelId | null onCopy: (messageId: string, text: string) => void onLike: (messageId: string) => void onDislike: (messageId: string) => void onToggleMemories: (messageId: string) => void } export function AgentMessage({ message, index, messagesLength, hoveredMessageId, copiedMessageId, messageFeedback, expandedMemories, responseModel, onCopy, onLike, onDislike, onToggleMemories, }: AgentMessageProps) { const isLastAgentMessage = index === messagesLength - 1 && message.role === "assistant" const isHovered = hoveredMessageId === message.id const messageText = message.parts .filter((part) => part.type === "text") .map((part) => part.text) .join(" ") const responseModelLabel = responseModel ? `${modelNames[responseModel].name} ${modelNames[responseModel].version}` : null return (
{message.parts.map((part, partIndex) => { if (part.type === "source-url") { if ( partIndex > 0 && message.parts[partIndex - 1]?.type === "source-url" ) { return null } const sources: SourceUrlPart[] = [] for (let j = partIndex; j < message.parts.length; j++) { const p = message.parts[j] if (!p || p.type !== "source-url") break sources.push(p as SourceUrlPart) } return ( ) } if (part.type === "source-document") { const doc = part as { type: "source-document" sourceId: string title: string filename?: string } return (
Document
{doc.title}
{doc.filename && (
{doc.filename}
)}
) } if (part.type === "text") { return (
{part.text}
) } if (part.type === "dynamic-tool") { const dt = part as { type: "dynamic-tool" toolName: string toolCallId: string state: string input?: unknown output?: unknown errorText?: string } const displayState = dt.state === "output-error" ? "error" : dt.state return ( ) } if (part.type.startsWith("tool-")) { return ( ) } return null })}
{responseModelLabel && ( {responseModelLabel} )}
) }