/** * Message blocks — typed, serializable rich-content slots. * * A `ChatMessage` may carry `blocks: MessageBlock[]`. Each block is a * discriminated union member keyed by `kind`. Payloads are plain JSON * (no Date, no Float32Array, no React nodes) so blocks survive history * persistence and SSE transport. See `@docs/message-blocks.md`. */ import type { LinkPreviewData } from '../../../common/link-preview'; import type { MapBlockPayload } from '../../../common/blocks'; /** Spaciousness hint forwarded from the bubble to renderers. */ export type BlockAppearance = 'compact' | 'full'; interface BlockBase { /** Stable id — used as React key and for memo diffing. */ id: string; /** Optional caption rendered above the block. */ caption?: string; } export interface TextBlock extends BlockBase { kind: 'text'; text: string; } export interface MarkdownBlock extends BlockBase { kind: 'markdown'; markdown: string; } export interface AudioBlock extends BlockBase { kind: 'audio'; src: string; title?: string; artist?: string; cover?: string; /** `compact` → AudioPlayer variant. */ variant?: 'default' | 'compact'; } export interface VideoBlock extends BlockBase { kind: 'video'; /** Raw URL string — VideoPlayer auto-classifies YouTube/Vimeo/HLS/MP4. */ src: string; poster?: string; title?: string; aspectRatio?: number | 'auto' | 'fill'; } export interface ImageBlock extends BlockBase { kind: 'image'; src: string; alt?: string; /** When true → zoom/pan ImageViewer; else a plain ``. */ interactive?: boolean; } export interface GalleryBlock extends BlockBase { kind: 'gallery'; items: Array<{ id: string; src: string; thumbnail?: string; alt?: string }>; } /** * Map block — a chat message map. The serializable map SHAPE (center / zoom / * basemap / terrain / markers / routes / polygons) is the shared * `MapBlockPayload` (defined once in `common/blocks/types.ts`) so the chat * map and the NotionEditor `mapBlock` node can't drift. Chat adds only the * chat-specific `userLocation` opt-in on top. */ export interface MapBlock extends BlockBase, MapBlockPayload { kind: 'map'; /** * Opt in to the map's "show my location" chip (privacy-sensitive). * * This is a plain boolean OPT-IN flag, **not** coordinates — the browser * supplies the live position; we never bake lat/lng into the serialized * block. Default **off**: only set `true` when the host wants this chat * map to offer locate-me. When `true` it surfaces the toolbar locate-me * chip (forwarded to `MapContainer`'s `geolocate` prop); the user must * still grant the OS location permission, and nothing is requested until * they press the chip. Requires a secure context (HTTPS / localhost). * @default false */ userLocation?: boolean; } export interface JsonBlock extends BlockBase { kind: 'json'; data: unknown; /** JsonTree display mode. */ mode?: 'full' | 'compact' | 'inline'; } export interface MermaidBlock extends BlockBase { kind: 'mermaid'; chart: string; } export interface CodeBlock extends BlockBase { kind: 'code'; code: string; language: string; } /** * Code-diff block — wraps the `DiffViewer` tool. Provide EITHER * `oldCode` + `newCode` (the viewer runs an internal LCS diff) OR a * pre-computed unified `patch` string. `layout` pins side-by-side * (`split`) vs single-column (`unified`); when omitted it follows the * bubble appearance (compact → unified, full → split). */ export interface DiffBlock extends BlockBase { kind: 'diff'; oldCode?: string; newCode?: string; patch?: string; language?: string; layout?: 'split' | 'unified'; } /** * Link-preview (URL unfurl) card — favicon + og:image + title/description/ * site name, Notion/Telegram style. Either the metadata is already * resolved (`data`), or only `url` is given and the host's * `resolveLinkPreview` resolver fills it in lazily. See * `common/link-preview/types.ts` for the data contract (the browser cannot * fetch meta tags itself — resolution is a host concern). */ export interface LinkBlock extends BlockBase { kind: 'link'; /** The URL to preview (and link to). */ url: string; /** Pre-resolved metadata. When absent, the host resolver is used. */ data?: LinkPreviewData; } /** * Escape hatch for host-defined block types. The built-in registry * renders it as the unknown-kind fallback unless the host registers a * `custom` renderer that switches on `name`. */ export interface CustomBlock extends BlockBase { kind: 'custom'; /** Host-defined sub-type, dispatched by the host's own renderer. */ name: string; payload: unknown; } export type MessageBlock = | TextBlock | MarkdownBlock | AudioBlock | VideoBlock | ImageBlock | GalleryBlock | MapBlock | JsonBlock | MermaidBlock | CodeBlock | DiffBlock | LinkBlock | CustomBlock; /** The discriminant literal — `'audio' | 'video' | …`. */ export type MessageBlockKind = MessageBlock['kind'];