import { useCallback, useEffect, useRef } from "react"; import { Platform, TextInput, type TextInputProps } from "react-native"; import { useAui, useAuiState } from "@assistant-ui/store"; import { flushTapSync } from "@assistant-ui/tap"; type TextInputInstance = React.ComponentRef; type TextInputKeyPressEvent = Parameters< NonNullable >[0]; export type ComposerInputProps = Omit< TextInputProps, "value" | "onChangeText" > & { /** * Controls how the Enter key submits messages (web only). * - "enter": Plain Enter submits (Shift+Enter for newline) * - "none": Enter inserts newline (no keyboard submission) * @default "enter" */ submitMode?: "enter" | "none"; }; const getWebTextareaMaxHeight = (el: HTMLTextAreaElement) => { const computedMaxHeight = globalThis.getComputedStyle?.(el).maxHeight; const parsedComputedMaxHeight = Number.parseFloat(computedMaxHeight ?? ""); if (Number.isFinite(parsedComputedMaxHeight)) return parsedComputedMaxHeight; const inlineMaxHeight = Number.parseFloat(el.style.maxHeight); return Number.isFinite(inlineMaxHeight) ? inlineMaxHeight : null; }; const adjustWebTextareaHeight = ( ref: React.RefObject, ) => { if (Platform.OS !== "web") return; // On web, the TextInput ref IS the DOM element const el = ref.current as unknown as HTMLTextAreaElement | null; if (!el) return; // Ensure rows=1 so scrollHeight reflects actual content if (el.rows !== 1) el.rows = 1; el.style.overflowX = "hidden"; el.style.overflowY = "hidden"; el.style.height = "auto"; const scrollHeight = el.scrollHeight; const maxHeight = getWebTextareaMaxHeight(el); const needsScrollbar = maxHeight !== null && scrollHeight > maxHeight; const nextHeight = maxHeight === null ? scrollHeight : Math.min(scrollHeight, maxHeight); el.style.overflowY = needsScrollbar ? "auto" : "hidden"; el.style.height = `${nextHeight}px`; }; export const ComposerInput = ({ submitMode = "enter", onKeyPress: onKeyPressProp, numberOfLines, style, ...props }: ComposerInputProps) => { const aui = useAui(); const text = useAuiState((s) => s.composer.text); const inputRef = useRef(null); const onChangeText = useCallback( (value: string) => { if (Platform.OS === "web") { // Keep the controlled value in sync mid-IME so react-dom does not reset the textarea to a stale value flushTapSync(() => { aui.composer.setText(value); }); return; } aui.composer.setText(value); }, [aui], ); // Auto-resize textarea on web when text changes useEffect(() => { void text; // trigger re-run when text changes adjustWebTextareaHeight(inputRef); }, [text]); const onKeyPress = useCallback( (e: TextInputKeyPressEvent) => { onKeyPressProp?.(e); if (e.isDefaultPrevented()) return; if (Platform.OS !== "web") return; if (submitMode !== "enter") return; const nativeEvent = e.nativeEvent as TextInputKeyPressEvent["nativeEvent"] & { shiftKey?: boolean; isComposing?: boolean; keyCode?: number; }; // react-native-web forwards the raw DOM keydown before its own composition guard, so re-check it here (mirrors isEventComposing) if (nativeEvent.isComposing || nativeEvent.keyCode === 229) return; if (nativeEvent.key === "Enter" && !nativeEvent.shiftKey) { (e as unknown as Event).preventDefault?.(); aui.composer.send(); } }, [aui, submitMode, onKeyPressProp], ); return ( ); };