"use client"; import { ArrowUp, Plus, Square } from "lucide-react"; import { AnimatePresence, motion, useReducedMotion } from "motion/react"; import { type FormEvent, type KeyboardEvent, type ReactNode, type TextareaHTMLAttributes, useCallback, useEffect, useLayoutEffect, useRef, useState, } from "react"; import { Button } from "@/components/motion/button"; import { MorphPopover, MorphPopoverContent, MorphPopoverTrigger, } from "@/components/motion/popover-morph"; import { Select, SelectContent, SelectItem, SelectTrigger, } from "@/components/motion/select"; import { SPRING_SWAP } from "@/lib/ease"; import { cn } from "@/lib/utils"; export interface PromptModel { value: string; label: ReactNode; icon?: ReactNode; disabled?: boolean; } export interface PromptAction { value: string; label: ReactNode; description?: ReactNode; icon?: ReactNode; disabled?: boolean; } export interface PromptInputProps extends Omit< TextareaHTMLAttributes, "value" | "defaultValue" | "onChange" | "onSubmit" | "children" > { value?: string; defaultValue?: string; onValueChange?: (value: string) => void; models?: PromptModel[]; model?: string; defaultModel?: string; onModelChange?: (model: string) => void; actions?: PromptAction[]; onAction?: (action: string) => void; onSubmit?: (value: string, model?: string) => void | Promise; loading?: boolean; onStop?: () => void; minRows?: number; maxRows?: number; leadingAction?: ReactNode; className?: string; } export function PromptInput({ value, defaultValue = "", onValueChange, models = [], model, defaultModel, onModelChange, actions = [], onAction, onSubmit, loading = false, onStop, minRows = 2, maxRows = 8, leadingAction, className, disabled, placeholder = "Ask the agent to do something…", "aria-label": ariaLabel = "Prompt", onKeyDown, ...textareaProps }: PromptInputProps) { const reduce = useReducedMotion() ?? false; const textareaRef = useRef(null); const measurementRef = useRef(null); const [internalValue, setInternalValue] = useState(defaultValue); const [internalModel, setInternalModel] = useState( defaultModel ?? models[0]?.value, ); const [actionsOpen, setActionsOpen] = useState(false); const currentValue = value ?? internalValue; const currentModelValue = model ?? internalModel; const currentModel = models.find( (option) => option.value === currentModelValue, ); const canSubmit = Boolean(currentValue.trim()) && !disabled && !loading; const resizeTextarea = useCallback(() => { const textarea = textareaRef.current; const measurement = measurementRef.current; if (!textarea || !measurement || textarea.value !== currentValue) return; const lineHeight = 24; const nextHeight = Math.min( Math.max(measurement.scrollHeight, minRows * lineHeight), maxRows * lineHeight, ); const height = `${nextHeight}px`; if (textarea.style.height !== height) textarea.style.height = height; }, [currentValue, maxRows, minRows]); useLayoutEffect(() => { resizeTextarea(); }, [resizeTextarea]); useEffect(() => { const textarea = textareaRef.current; if (!textarea || typeof ResizeObserver === "undefined") return; const observer = new ResizeObserver(resizeTextarea); observer.observe(textarea); return () => observer.disconnect(); }, [resizeTextarea]); const setValue = (next: string) => { if (value === undefined) setInternalValue(next); onValueChange?.(next); }; const setModel = (next: string) => { if (model === undefined) setInternalModel(next); onModelChange?.(next); }; const submit = (event?: FormEvent) => { event?.preventDefault(); const prompt = currentValue.trim(); if (!prompt || disabled || loading) return; onSubmit?.(prompt, currentModelValue); if (value === undefined) setInternalValue(""); textareaRef.current?.focus({ preventScroll: true }); }; const handleKeyDown = (event: KeyboardEvent) => { onKeyDown?.(event); if ( event.defaultPrevented || event.key !== "Enter" || event.shiftKey || event.nativeEvent.isComposing ) { return; } event.preventDefault(); submit(); }; return (