'use client' import * as React from 'react' import { ChatFooter } from './chat-container' import { ChatInput } from './chat-input' import { ChatAttachmentAddButton } from './chat-attachment-bar' import { ChatContextMemoryBar } from './chat-context-memory-bar' import { ChatComposerPlusMenu, ChatContextChipStrip, ChatContextPicker, } from './chat-context-picker' import { ModelDisplay } from './model-display' import { BoxArchiveIcon } from '../icons-v2-generated' import { cn } from '../../utils/cn' import type { ChatInputRef, ModelDisplayProps } from './types/component.types' import type { ChatContextItem, ChatContextPickerConfig, } from './types/context-item.types' export interface ChatComposerProps { /** Read-only archived chat → render the unarchive placeholder instead of the * input (Figma node 7361:426949). */ archived?: boolean inputRef: React.Ref onSend: (text: string) => void onStop: () => void sending: boolean placeholder: string autoFocus?: boolean slashCommands?: React.ComponentProps['slashCommands'] /** Show the (Guide-only) attachment add button to the left of the model row. */ showAttachmentButton?: boolean attachmentsCount?: number onAddFiles?: (files: FileList | File[]) => void attachmentsDisabled?: boolean /** Model + usage row props, forwarded to ``. */ model: ModelDisplayProps // ─── Entity-context picker (Figma 31:28708 / 1:5699) ────────────────────── // When `contextPicker` is set the composer renders the `+` "Assign Item" // menu, the `@`-mention trigger, the two-level picker, and the selected-item // chip strip. All selection STATE is owned by the parent (EmbeddableChat) and // flows through these props; this component is presentational. /** Host config (entity types + search resolver). Picker is inert when unset. */ contextPicker?: ChatContextPickerConfig /** Currently-selected context items (chips + ✓ state). */ selectedContextItems?: ChatContextItem[] /** Toggle an item in/out of the selection (picker rows). */ onToggleContextItem?: (item: ChatContextItem) => void /** Remove an item (chip ×). */ onRemoveContextItem?: (item: ChatContextItem) => void /** Ambient CONTEXT MEMORY (Figma 271:38656) — the entities the host collected * from the user's navigation history and sends with every message. Rendered * as a summary strip at the top of the composer card, above the assigned-item * chips. Empty/omitted → no strip. */ contextMemoryItems?: ChatContextItem[] /** Drop one entity from the context memory (dropdown row ×). */ onRemoveContextMemoryItem?: (item: ChatContextItem) => void /** Controlled picker open state. */ contextPickerOpen?: boolean /** Open the picker (the `+` menu's "Assign Item"). */ onOpenContextPicker?: () => void /** Close the picker (Escape / outside-click / pick-complete). */ onCloseContextPicker?: () => void /** Active `@`-mention query (filters the type list); null when inactive. */ mentionQuery?: string | null /** `@`-trigger callback forwarded to the input. */ onMentionQueryChange?: (query: string | null) => void /** Fires on every draft value change — threaded to `ChatInput.onValueChange` * so the host can keep `@type:id` mention tokens in sync with context chips. */ onValueChange?: (value: string) => void /** Non-destructive ghost preview shown in the empty composer (e.g. a hovered * quick-action's full prompt). Forwarded to `ChatInput.previewText`. */ previewText?: string } /** * Chat panel footer (Figma node 7363:205952): the message composer (live * `ChatInput` or the read-only archived placeholder) above the model/usage * row. No own background — inherits the panel surface so it blends seamlessly. * * With `contextPicker` set, the selected-context chip strip sits above the * input, the two-level picker popover anchors above the input, and the bottom * controls row swaps the bare attachment `+` for the `+` plus-menu. */ export function ChatComposer({ archived = false, inputRef, onSend, onStop, sending, placeholder, autoFocus, slashCommands, showAttachmentButton = false, attachmentsCount = 0, onAddFiles, attachmentsDisabled = false, model, contextPicker, selectedContextItems, onToggleContextItem, onRemoveContextItem, contextMemoryItems, onRemoveContextMemoryItem, contextPickerOpen = false, onOpenContextPicker, onCloseContextPicker, mentionQuery = null, onMentionQueryChange, onValueChange, previewText, }: ChatComposerProps) { const contextEnabled = !!contextPicker && !archived const selected = selectedContextItems ?? [] const memory = contextMemoryItems ?? [] // type → icon lookup so the chips (composer + bubble) lead with their // entity-type glyph without the parent threading an extra resolver. Keyed on // `entityTypes` (not the whole `contextPicker`) so an inline host config // doesn't rebuild the map — and the derived `resolveContextIcon` — every render. const entityTypes = contextPicker?.entityTypes const iconByType = React.useMemo(() => { const map = new Map() for (const t of entityTypes ?? []) map.set(t.type, t.icon) return map }, [entityTypes]) const resolveContextIcon = React.useCallback( (item: ChatContextItem) => iconByType.get(item.type), [iconByType], ) // The input is identical in both layouts; only `hideBorder` (context mode // lets the outer card own the chrome) and the `+` adornment differ. const inputNode = ( (contextPickerOpen ? onCloseContextPicker?.() : onOpenContextPicker?.())} open={contextPickerOpen} disabled={attachmentsDisabled} dropdown={ contextPicker ? ( onToggleContextItem?.(item)} onClose={() => onCloseContextPicker?.()} mentionQuery={mentionQuery} /> ) : undefined } /> ) : undefined } /> ) return (
{archived ? (

Unarchive the chat to continue

) : contextEnabled ? ( // Unified card (Figma 1:6073 / 271:38656): the context-memory // summary strip and the assigned-chip strip (both `bg-ods-bg`, // `border-b`) stack flush above the input; the card owns the border // / radius / focus ring. No `overflow-hidden` so the picker and the // memory popover can float above the input (whichever strip comes // first gets `rounded-t`). // `has-[[data-editor]:focus]` (not `focus-within`) so the accent // border reacts ONLY to the composer editor — not the picker's search // input or item buttons, which also live inside this card. The editor // is a `contentEditable` div (`data-editor`), not a `