import * as React from "react" type MessagePartKind = | "text" | "reasoning" | "tool" | "file" | "source" | "data" | "custom" | "step-start" | "reasoning-file" | (string & {}) type MessagePartOf = TMessage extends { parts: ReadonlyArray } ? TPart : unknown type MessagePartItem = { key: string part: TPart message: TMessage index: number kind: MessagePartKind name?: string toolCallId?: string } type MessageToolPartGroup = { key: string name?: string toolCallId?: string items: Array> call?: MessagePartItem result?: MessagePartItem } type MessagePartsResult = { role?: string all: Array> protocol: Array> byKind: Record>> tools: Array> toolParts: Array> text: Array> texts: Array> reasoning: Array> files: Array> sources: Array> data: Array> custom: Array> steps: Array> reasoningFiles: Array> get: (kind: MessagePartKind) => Array> } type MessagePartsAdapter = { getParts?: (message: TMessage) => ReadonlyArray getRole?: (message: TMessage) => string | undefined getPartKey?: (part: TPart, index: number, message: TMessage) => string getPartKind?: ( part: TPart, index: number, message: TMessage ) => MessagePartKind getPartName?: ( part: TPart, index: number, message: TMessage ) => string | undefined getToolCallId?: ( part: TPart, index: number, message: TMessage ) => string | undefined isToolCall?: (part: TPart, index: number, message: TMessage) => boolean isToolResult?: (part: TPart, index: number, message: TMessage) => boolean } type CreateMessagePartsOptions = { adapter?: MessagePartsAdapter order?: | "role" | "protocol" | MessagePartKind[] | (( items: Array>, message: TMessage ) => Array>) classifyPart?: ( part: TPart, context: { message: TMessage index: number kind: MessagePartKind name?: string toolCallId?: string } ) => | { kind?: MessagePartKind name?: string toolCallId?: string } | undefined getKey?: (part: TPart, index: number, message: TMessage) => string } type MessagePartRenderProps = { item: MessagePartItem part: TPart message: TMessage index: number kind: MessagePartKind name?: string toolCallId?: string } type MessagePartComponent< TPart = unknown, TMessage = unknown, > = React.ComponentType> type MessagePartComponents = { text?: MessagePartComponent reasoning?: MessagePartComponent tool?: MessagePartComponent file?: MessagePartComponent source?: MessagePartComponent data?: MessagePartComponent custom?: MessagePartComponent stepStart?: MessagePartComponent reasoningFile?: MessagePartComponent fallback?: MessagePartComponent types?: Record> tools?: Record> dataTypes?: Record> } type MessagePartProps = { item: MessagePartItem components: MessagePartComponents } type MessagePartsProps = { parts: | MessagePartsResult | Array> components: MessagePartComponents renderPart?: (props: { item: MessagePartItem children: React.ReactNode }) => React.ReactNode } const ASSISTANT_PART_ORDER: MessagePartKind[] = [ "reasoning", "tool", "reasoning-file", "file", "text", "source", "data", "custom", "step-start", ] const USER_PART_ORDER: MessagePartKind[] = [ "file", "text", "data", "source", "custom", ] const DEFAULT_PART_ORDER: MessagePartKind[] = ["text", "data", "custom"] function createMessageParts( message: TMessage, options: CreateMessagePartsOptions> = {} ): MessagePartsResult, TMessage> { type TPart = MessagePartOf const adapter = options.adapter ?? {} const role = adapter.getRole?.(message) ?? getStructuralRole(message) const messageParts = adapter.getParts?.(message) ?? (getStructuralParts(message) as ReadonlyArray) const protocol = messageParts.map((part, index) => { const kind = adapter.getPartKind?.(part, index, message) ?? getStructuralPartKind(part) const name = adapter.getPartName?.(part, index, message) ?? getStructuralPartName(part) const toolCallId = adapter.getToolCallId?.(part, index, message) ?? getStructuralToolCallId(part) const classification = options.classifyPart?.(part, { message, index, kind, name, toolCallId, }) return { key: options.getKey?.(part, index, message) ?? adapter.getPartKey?.(part, index, message) ?? getStructuralPartKey(part, index), part, message, index, kind: classification?.kind ?? kind, name: classification?.name ?? name, toolCallId: classification?.toolCallId ?? toolCallId, } satisfies MessagePartItem }) const all = orderMessagePartItems( protocol, options.order ?? "role", role, message ) const byKind = groupMessagePartItems(all) function get(kind: MessagePartKind) { return byKind[kind] ?? [] } return { role, all, protocol, byKind, tools: groupToolPartItems(all, adapter, message), toolParts: get("tool"), text: get("text"), texts: get("text"), reasoning: get("reasoning"), files: get("file"), sources: get("source"), data: get("data"), custom: get("custom"), steps: get("step-start"), reasoningFiles: get("reasoning-file"), get, } } function MessageParts({ parts, components, renderPart, }: MessagePartsProps) { const items = Array.isArray(parts) ? parts : parts.all return ( <> {items.map((item) => { const children = ( ) return renderPart ? ( {renderPart({ item, children })} ) : ( children ) })} ) } function MessagePart({ item, components, }: MessagePartProps) { const Component = getMessagePartComponent(item, components) if (!Component) { return null } return React.createElement(Component, { item, part: item.part, message: item.message, index: item.index, kind: item.kind, name: item.name, toolCallId: item.toolCallId, }) } function getMessagePartComponent( item: MessagePartItem, components: MessagePartComponents ) { const partType = getStructuralPartType(item.part) if (partType && components.types?.[partType]) { return components.types[partType] } if (item.kind === "tool" && item.name && components.tools?.[item.name]) { return components.tools[item.name] } if (item.kind === "data" && item.name && components.dataTypes?.[item.name]) { return components.dataTypes[item.name] } if (item.kind === "text") { return components.text ?? components.fallback } if (item.kind === "reasoning") { return components.reasoning ?? components.fallback } if (item.kind === "tool") { return components.tool ?? components.fallback } if (item.kind === "file") { return components.file ?? components.fallback } if (item.kind === "source") { return components.source ?? components.fallback } if (item.kind === "data") { return components.data ?? components.fallback } if (item.kind === "custom") { return components.custom ?? components.fallback } if (item.kind === "step-start") { return components.stepStart ?? components.fallback } if (item.kind === "reasoning-file") { return components.reasoningFile ?? components.file ?? components.fallback } return components.fallback } function orderMessagePartItems( items: Array>, order: NonNullable["order"]>, role: string | undefined, message: TMessage ) { if (order === "protocol") { return items } if (typeof order === "function") { return order(items, message) } const orderedKinds = Array.isArray(order) ? order : role === "user" ? USER_PART_ORDER : role === "assistant" ? ASSISTANT_PART_ORDER : DEFAULT_PART_ORDER const orderWeight = new Map(orderedKinds.map((kind, index) => [kind, index])) return [...items].sort((a, b) => { const aWeight = orderWeight.get(a.kind) ?? Number.MAX_SAFE_INTEGER const bWeight = orderWeight.get(b.kind) ?? Number.MAX_SAFE_INTEGER return aWeight === bWeight ? a.index - b.index : aWeight - bWeight }) } function groupMessagePartItems( items: Array> ) { return items.reduce>>>( (groups, item) => { groups[item.kind] ??= [] groups[item.kind].push(item) return groups }, {} ) } function groupToolPartItems( items: Array>, adapter: MessagePartsAdapter, message: TMessage ) { const groups: MessageToolPartGroup[] = [] const groupsByKey = new Map>() for (const item of items) { if (item.kind !== "tool") { continue } const groupKey = item.toolCallId ?? item.name ?? item.key let group = groupsByKey.get(groupKey) if (!group) { group = { key: groupKey, name: item.name, toolCallId: item.toolCallId, items: [], } groupsByKey.set(groupKey, group) groups.push(group) } group.items.push(item) if (adapter.isToolResult?.(item.part, item.index, message)) { group.result = item } else if (adapter.isToolCall?.(item.part, item.index, message)) { group.call = item } else if (isStructuralToolResult(item.part)) { group.result = item } else if (!group.call) { group.call = item } } return groups } function getStructuralParts(message: unknown) { if (isRecord(message) && Array.isArray(message.parts)) { return message.parts } return [] } function getStructuralRole(message: unknown) { if (isRecord(message) && typeof message.role === "string") { return message.role } return undefined } function getStructuralPartKey(part: unknown, index: number) { const type = getStructuralPartType(part) ?? "part" const toolCallId = getStructuralToolCallId(part) if (toolCallId) { return `${type}:${toolCallId}` } if (isRecord(part)) { if (typeof part.sourceId === "string") { return `${type}:${part.sourceId}` } if (typeof part.id === "string") { return `${type}:${part.id}` } if (typeof part.url === "string") { return `${type}:${part.url}:${index}` } } return `${type}:${index}` } function getStructuralPartKind(part: unknown): MessagePartKind { const type = getStructuralPartType(part) if (!type) { return "custom" } if (type === "text") { return "text" } if (type === "reasoning" || type === "thinking") { return "reasoning" } if ( type === "file" || type === "image" || type === "audio" || type === "video" ) { return "file" } if (type === "reasoning-file") { return "reasoning-file" } if ( type === "source" || type === "source-url" || type === "source-document" || type === "citation" || type === "search-result" ) { return "source" } if ( type === "dynamic-tool" || type === "tool-call" || type === "tool-result" || type === "tool_call" || type === "tool_result" || type.startsWith("tool-") ) { return "tool" } if (type.startsWith("data-")) { return "data" } if (type === "step-start" || type === "step_start") { return "step-start" } if (type === "custom") { return "custom" } return "custom" } function getStructuralPartName(part: unknown) { const type = getStructuralPartType(part) if (!type) { return undefined } if (isRecord(part)) { if (typeof part.toolName === "string") { return part.toolName } if (typeof part.name === "string") { return part.name } if (typeof part.kind === "string") { return part.kind } } if (type.startsWith("tool-")) { return type.slice("tool-".length) } if (type.startsWith("data-")) { return type.slice("data-".length) } if (type === "source-url") { return "url" } if (type === "source-document") { return "document" } return undefined } function getStructuralToolCallId(part: unknown) { if (!isRecord(part)) { return undefined } if (typeof part.toolCallId === "string") { return part.toolCallId } if (typeof part.callId === "string") { return part.callId } return undefined } function isStructuralToolResult(part: unknown) { const type = getStructuralPartType(part) return ( type === "tool-result" || type === "tool_result" || (isRecord(part) && "output" in part) ) } function getStructuralPartType(part: unknown) { if (isRecord(part) && typeof part.type === "string") { return part.type } return undefined } function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null } export { MessagePart, MessageParts, createMessageParts, getMessagePartComponent, type CreateMessagePartsOptions, type MessagePartComponent, type MessagePartComponents, type MessagePartItem, type MessagePartKind, type MessagePartProps, type MessagePartRenderProps, type MessagePartsAdapter, type MessagePartsProps, type MessagePartsResult, type MessageToolPartGroup, }