import { For, Show } from 'solid-js' import { ThinkingPart } from './thinking-part' import type { JSX } from 'solid-js' import type { UIMessage } from '../types' export interface ToolCallRenderProps { id: string name: string arguments: string state: string approval?: any output?: any } /** @deprecated Use `createChatUI()` Message instead. Deprecated in 0.8.0. Removed in 1.0.0. */ export interface ChatMessageProps { /** The message to render */ message: UIMessage /** Base CSS class name */ class?: string /** Additional class for user messages */ userClass?: string /** Additional class for assistant messages */ assistantClass?: string /** Custom renderer for text parts */ textPartRenderer?: (props: { content: string }) => JSX.Element /** Custom renderer for thinking parts */ thinkingPartRenderer?: (props: { content: string isComplete?: boolean }) => JSX.Element /** Named tool renderers - use the tool name as the key */ toolsRenderer?: Record JSX.Element> /** Default tool renderer when tool name not found in toolsRenderer */ defaultToolRenderer?: (props: ToolCallRenderProps) => JSX.Element /** Custom renderer for tool result parts */ toolResultRenderer?: (props: { toolCallId: string content: string state: string }) => JSX.Element } /** * @deprecated Use `createChatUI()` Message instead. Deprecated in 0.8.0. Removed in 1.0.0. * * Message component - renders a single message with all its parts * * This component natively understands TanStack AI's parts-based message format: * - thinking parts: rendered as collapsible thinking/reasoning sections (auto-collapses when complete) * - text parts: rendered as content * - tool-call parts: rendered with state, approvals, etc. * - tool-result parts: rendered with results * * @example Basic usage * ```tsx * * ``` * * @example With role-based styling * ```tsx * * ``` * * @example With custom thinking renderer * ```tsx * ( *
* Thinking... *
{content}
*
* )} * /> * ``` * * @example With named tool renderers * ```tsx * , * weatherLookup: ({ id, arguments: args }) => , * }} * defaultToolRenderer={() => null} * /> * ``` */ export function ChatMessage(props: ChatMessageProps) { // Combine classes based on role const roleClass = () => props.message.role === 'user' ? (props.userClass ?? '') : (props.assistantClass ?? '') const combinedClass = () => [props.class ?? '', roleClass()].filter(Boolean).join(' ') return (
{(part, index) => { // Check if thinking is complete (if there's a text part after this thinking part) const isThinkingComplete = () => part.type === 'thinking' && props.message.parts .slice(index() + 1) .some((p) => p.type === 'text') return ( ) }}
) } function MessagePart(props: { part: any isThinkingComplete?: boolean textPartRenderer?: ChatMessageProps['textPartRenderer'] thinkingPartRenderer?: ChatMessageProps['thinkingPartRenderer'] toolsRenderer?: ChatMessageProps['toolsRenderer'] defaultToolRenderer?: ChatMessageProps['defaultToolRenderer'] toolResultRenderer?: ChatMessageProps['toolResultRenderer'] }) { // Text part if (props.part.type === 'text') { if (props.textPartRenderer) { return <>{props.textPartRenderer({ content: props.part.content })} } return (
{props.part.content}
) } // Thinking part if (props.part.type === 'thinking') { if (props.thinkingPartRenderer) { return ( <> {props.thinkingPartRenderer({ content: props.part.content, isComplete: props.isThinkingComplete, })} ) } return ( ) } // Tool call part if (props.part.type === 'tool-call') { const toolProps: ToolCallRenderProps = { id: props.part.id, name: props.part.name, arguments: props.part.arguments, state: props.part.state, approval: props.part.approval, output: props.part.output, } // Check if there's a specific renderer for this tool if (props.toolsRenderer?.[props.part.name]) { return <>{props.toolsRenderer[props.part.name]?.(toolProps)} } // Use default tool renderer if provided if (props.defaultToolRenderer) { return <>{props.defaultToolRenderer(toolProps)} } // Fallback to built-in default renderer return (
{props.part.name} {props.part.state}
{props.part.arguments}
{props.part.approval.approved !== undefined ? props.part.approval.approved ? '✓ Approved' : '✗ Denied' : '⏳ Awaiting approval...'}
{JSON.stringify(props.part.output, null, 2)}
) } // Tool result part if (props.part.type === 'tool-result') { if (props.toolResultRenderer) { return ( <> {props.toolResultRenderer({ toolCallId: props.part.toolCallId, content: props.part.content, state: props.part.state, })} ) } return (
{props.part.content}
) } return null }