import type { ClientModel, EffectiveSessionControl } from "@opengeni/sdk"; import { LayoutGroup, motion } from "motion/react"; import { useImperativeHandle, type ClipboardEvent, type ComponentProps, type ReactNode, type Ref, } from "react"; import type { SlashCommand } from "../commands/types"; import type { ComposerState } from "../hooks/use-composer"; import type { UseFileAttachmentsResult } from "../hooks/use-file-attachments"; import type { SlashCommandContext } from "../hooks/use-slash-commands"; import { OPEN_WORKSTREAM_CONTROL_EVENT } from "../workstream-control-event"; import { Actions, AlternateSendButton, AttachButton, Attachments, CommandPalette, Confirmation, Controls, Footer, Frame, Help, Hint, Input, ModelPicker, PauseButton, PausedState, RestoredResources, Root, SendButton, Status, StopButton, Surface, useChatComposerController, type ChatComposerMessages, type ComposerControlLinks, type ComposerRunControl, type ResponsiveBasis, } from "./composer"; import { ComposerTranscriptionControl, type ComposerTranscriptionControlProps, } from "./composer-transcription-control"; import { TimelineAnnotationsChip } from "./timeline-annotations"; export { OPEN_WORKSTREAM_CONTROL_EVENT }; export type ChatComposerProps = { composer: ComposerState; /** Replace only the normal footer, inside the shared composer context. * Confirmation, drafts, annotations and attachments remain native. */ footer?: ReactNode; focusRef?: Ref<{ focusInput: () => void }>; inputProps?: ComponentProps & { [attribute: `data-${string}`]: string | number | boolean | undefined; }; surfaceClassName?: string; /** * Measurement surface for responsive chrome. Defaults to the historical * viewport breakpoints; opt into `container` for narrow embedded panels. */ responsiveBasis?: ResponsiveBasis | undefined; /** Canonical workstream control, separate from lifecycle status. */ effectiveControl?: EffectiveSessionControl | null | undefined; /** Waiting prompts already ahead of a normal Send. */ queuedAheadCount?: number | undefined; /** Whether broader Workspace Resume is authorized for this viewer. */ canControlWorkspace?: boolean | undefined; /** Optional host routes used to navigate from effective Pause blockers. */ controlLinks?: ComposerControlLinks | undefined; placeholder?: string | undefined; disabled?: boolean | undefined; autoFocus?: boolean | undefined; /** Replaces the default keyboard hint under the field. */ hint?: string | undefined; /** App controls ahead of attach (e.g. mobile “+” overflow). */ controlsLeading?: ReactNode | undefined; /** App controls in the footer row, replacing the hint. */ controlsStart?: ReactNode | undefined; /** App actions beside Pause/Send, ordered before the built-in actions. */ actionsStart?: ReactNode | undefined; /** Extra classes for the built-in attach control (e.g. `max-sm:hidden`). */ attachButtonClassName?: string | undefined; /** Provider-neutral speech capability. Provider configuration stays in workspace settings. */ transcription?: ComposerTranscriptionControlProps | undefined; /** Extra classes on the transcription control. */ transcriptionClassName?: string | undefined; /** Soft-hide dictate while realtime voice is active (animated collapse). */ transcriptionSuppressed?: boolean | undefined; /** Content rendered above the textarea, inside the field chrome. */ header?: ReactNode | undefined; /** Paste hook composed with the attachment paste path. */ onPaste?: ((event: ClipboardEvent) => void) | undefined; /** Opt-in file attachment state, typically from `useFileAttachments`. */ attachments?: UseFileAttachmentsResult | undefined; /** Opt-in model picker choices. */ models?: ClientModel[] | undefined; selectedModel?: string | undefined; onSelectModel?: ((modelId: string) => void) | undefined; className?: string | undefined; commands?: readonly SlashCommand[] | undefined; commandContext?: SlashCommandContext | undefined; onClearView?: (() => void) | undefined; /** Partial overrides for all composer-owned visible and accessible copy. */ messages?: Partial | undefined; /** * Run control: `pause` (default) shows the workstream Pause control and * paused strip; `stop` shows Stop only while `running`, and the next message * continues a stopped conversation; `none` shows neither. */ runControl?: ComposerRunControl | undefined; /** A response is in progress (drives the Stop control). */ running?: boolean | undefined; }; /** * Batteries-included chat composer. This preset is assembled exclusively from * the public controller and compound primitives exported by the composer * subpath, so custom and default layouts share one behavioral implementation. */ export function ChatComposer({ composer, responsiveBasis, effectiveControl, queuedAheadCount, canControlWorkspace, controlLinks, placeholder, disabled, autoFocus, hint, controlsLeading, controlsStart, actionsStart, attachButtonClassName, transcription, transcriptionClassName, transcriptionSuppressed = false, header, onPaste, attachments, models, selectedModel, onSelectModel, className, commands, commandContext, onClearView, messages, footer, focusRef, inputProps, surfaceClassName, runControl, running, }: ChatComposerProps) { const controller = useChatComposerController({ delivery: composer, draft: composer, control: composer, effectiveControl, queuedAheadCount, canControlWorkspace, controlLinks, disabled, attachments, commands, commandContext, onClearView, onPaste, messages, runControl, running, }); const hasControls = Boolean( attachments || models || controlsLeading || controlsStart || transcription, ); const stackActions = hasControls && Boolean(actionsStart); useImperativeHandle(focusRef, () => ({ focusInput: controller.focusInput }), [ controller.focusInput, ]); return ( <> {header} {composer.annotations && composer.annotations.length > 0 && composer.updateAnnotation && composer.removeAnnotation ? (
) : null} {controller.confirmState ? ( ) : footer !== undefined ? ( footer ) : (
{hasControls ? ( {controlsLeading} {transcription ? ( ) : null} {models ? ( ) : null} {controlsStart ? ( {controlsStart} ) : null} ) : ( {hint} )} {actionsStart}
)}
); }