// Copyright (c) Meta Platforms, Inc. and affiliates. 'use client'; /** * @file ChatContext.tsx * @input Uses React createContext * @output Exports Chat message/list/composer/layout contexts. The composer * context (value + input-control registration) is public composition API, * re-exported from Chat/index.ts for custom inputs. * @position Shared contexts; consumed by ChatMessage, ChatComposer, * ChatComposerInput, ChatSendButton, and custom inputs. */ import { createContext, use, type RefObject } from 'react'; export type ChatMessageSender = 'user' | 'assistant' | 'system'; export type ChatDensity = 'compact' | 'balanced' | 'spacious'; export interface ChatMessageContextValue { sender: ChatMessageSender; density: ChatDensity; } export const ChatMessageContext = createContext(null); ChatMessageContext.displayName = 'ChatMessageContext'; export function useChatMessageContext(): ChatMessageContextValue | null { return use(ChatMessageContext); } export interface ChatListContextValue { density: ChatDensity; } export const ChatListContext = createContext(null); ChatListContext.displayName = 'ChatListContext'; export function useChatListContext(): ChatListContextValue | null { return use(ChatListContext); } // ============================================================================= // Composer context — shared state between ChatComposer and ChatComposerInput // ============================================================================= /** * Imperative surface the composer shell can invoke on its input slot. * * The input is one slot inside the composer body — it does not span the whole * body — so shell-level interactions like "click empty space to focus the * input" must flow shell → input. A custom input registers this control (via * `ChatComposerContextValue.inputControlRef`) so the shell can drive it * without knowing its DOM shape. Optional methods can be added over time; * inputs implement only what they support. */ export interface ChatComposerInputControl { /** Move keyboard focus into the input. */ focus: () => void; } export interface ChatComposerContextValue { value: string; onChange: (value: string) => void; onSubmit: (value: string) => void; placeholder: string; isDisabled: boolean; isStopShown: boolean; canSend: boolean; onStop?: () => void; /** * Mutable handle the input slot populates with its {@link * ChatComposerInputControl}. The shell reads `.current` to drive the input * (e.g. focus on body click). A custom input assigns * `inputControlRef.current = { focus }` on mount and clears it on unmount. * When unset, the shell falls back to focusing a `contenteditable`/`textarea` * it finds in the body. */ inputControlRef?: RefObject; } export const ChatComposerContext = createContext(null); ChatComposerContext.displayName = 'ChatComposerContext'; export function useChatComposerContext(): ChatComposerContextValue | null { return use(ChatComposerContext); } // ============================================================================= // Layout context — shared between ChatLayout and ChatMessageList // ============================================================================= export interface ChatLayoutContextValue { /** Ref to the scrollable container element that wraps the message area. */ scrollContainerRef: React.RefObject; /** Callback ref for the message list content element — layout observes it for size changes. */ contentRef: (el: HTMLElement | null) => void; } export const ChatLayoutContext = createContext(null); ChatLayoutContext.displayName = 'ChatLayoutContext'; export function useChatLayoutContext(): ChatLayoutContextValue | null { return use(ChatLayoutContext); }