import type { ReactNode } from 'react' import type { StreamedReply } from './useReplyStream.ts' type Part = Record & { $type: string } type Blob = { ref: { $link: string }; mimeType: string } export type MessageViewProps = { record: Record /** Text and reasoning streamed so far for a pending reply. */ streaming?: StreamedReply blobUrl: (cid: string, mimeType?: string) => string siblings?: { index: number; count: number; onPick: (index: number) => void } actions?: ReactNode } const kind = (part: Part) => part.$type.split('#')[1] function PartView({ part, blobUrl }: { part: Part; blobUrl: MessageViewProps['blobUrl'] }) { switch (kind(part)) { case 'textPart': return

{part.text as string}

case 'reasoningPart': return (
Reasoning

{(part.text as string | undefined) ?? ''}

) case 'toolCallPart': return (
Tool call: {part.tool as string}
{part.input as string}
) case 'toolResultPart': return (
{part.isError ? 'Tool error' : 'Tool result'}
{part.output as string}
) case 'sourcePart': return ( {(part.title as string | undefined) ?? (part.url as string)} ) case 'imagePart': { const image = part.image as Blob return ( {(part.alt ) } case 'filePart': { const file = part.file as Blob return ( {(part.name as string | undefined) ?? 'Attached file'} ) } default: // Skip part types this view doesn't know about. return null } } /** One message: its parts, its state, sibling controls, and actions. */ export function MessageView({ record, streaming, blobUrl, siblings, actions }: MessageViewProps) { const content = record.content as { $type: string; parts?: Part[] } const encrypted = content.$type.endsWith('#encryptedContent') const parts = content.parts ?? [] const sources = parts.filter((part) => kind(part) === 'sourcePart') return (
{record.role === 'user' ? 'You' : 'Assistant'} {siblings && siblings.count > 1 && ( {siblings.index + 1} / {siblings.count} )}
{parts .filter((part) => kind(part) !== 'sourcePart') .map((part, i) => ( // biome-ignore lint/suspicious/noArrayIndexKey: a record's parts keep their order ))} {streaming !== undefined && record.status === 'pending' && ( <> {streaming.reasoning && (
Reasoning

{streaming.reasoning}

)} {streaming.text &&

{streaming.text}

} )} {sources.length > 0 && (
    {sources.map((part) => (
  • ))}
)} {encrypted &&

This message is encrypted.

} {record.status === 'pending' && streaming === undefined &&

Thinking...

} {record.status === 'error' && (

Error{record.error ? `: ${record.error as string}` : ''}

)} {record.status === 'cancelled' &&

Stopped.

} {actions && }
) }