"use client" import React, { useEffect, useRef, useState } from "react" import { AnimatePresence, motion } from "framer-motion" import { ArrowUp, Info, Loader2, Mic, Paperclip, Square } from "lucide-react" import { omit } from "remeda" import { cn } from "@/lib/utils" import { useAudioRecording } from "@/hooks/use-audio-recording" import { useAutosizeTextArea } from "@/hooks/use-autosize-textarea" import { AudioVisualizer } from "@/components/ui/audio-visualizer" import { Button } from "@/components/ui/button" import { FilePreview } from "@/components/ui/file-preview" import { InterruptPrompt } from "@/components/chat-playground/interrupt-prompt" interface MessageInputBaseProps extends React.TextareaHTMLAttributes { value: string submitOnEnter?: boolean stop?: () => void isGenerating: boolean enableInterrupt?: boolean transcribeAudio?: (blob: Blob) => Promise } interface MessageInputWithoutAttachmentProps extends MessageInputBaseProps { allowAttachments?: false } interface MessageInputWithAttachmentsProps extends MessageInputBaseProps { allowAttachments: true files: File[] | null setFiles: React.Dispatch> } type MessageInputProps = | MessageInputWithoutAttachmentProps | MessageInputWithAttachmentsProps export function MessageInput({ placeholder = "Ask AI...", className, onKeyDown: onKeyDownProp, submitOnEnter = true, stop, isGenerating, enableInterrupt = true, transcribeAudio, ...props }: MessageInputProps) { const [isDragging, setIsDragging] = useState(false) const [showInterruptPrompt, setShowInterruptPrompt] = useState(false) const { isListening, isSpeechSupported, isRecording, isTranscribing, audioStream, toggleListening, stopRecording, } = useAudioRecording({ transcribeAudio, onTranscriptionComplete: (text) => { props.onChange?.({ target: { value: text } } as any) }, }) useEffect(() => { if (!isGenerating) { setShowInterruptPrompt(false) } }, [isGenerating]) const addFiles = (files: File[] | null) => { if (props.allowAttachments) { props.setFiles((currentFiles) => { if (currentFiles === null) { return files } if (files === null) { return currentFiles } return [...currentFiles, ...files] }) } } const onDragOver = (event: React.DragEvent) => { if (props.allowAttachments !== true) return event.preventDefault() setIsDragging(true) } const onDragLeave = (event: React.DragEvent) => { if (props.allowAttachments !== true) return event.preventDefault() setIsDragging(false) } const onDrop = (event: React.DragEvent) => { setIsDragging(false) if (props.allowAttachments !== true) return event.preventDefault() const dataTransfer = event.dataTransfer if (dataTransfer.files.length) { addFiles(Array.from(dataTransfer.files)) } } const onPaste = (event: React.ClipboardEvent) => { const items = event.clipboardData?.items if (!items) return const text = event.clipboardData.getData("text") if (text && text.length > 500 && props.allowAttachments) { event.preventDefault() const blob = new Blob([text], { type: "text/plain" }) const file = new File([blob], "Pasted text", { type: "text/plain", lastModified: Date.now(), }) addFiles([file]) return } const files = Array.from(items) .map((item) => item.getAsFile()) .filter((file) => file !== null) if (props.allowAttachments && files.length > 0) { addFiles(files) } } const onKeyDown = (event: React.KeyboardEvent) => { if (submitOnEnter && event.key === "Enter" && !event.shiftKey) { event.preventDefault() if (isGenerating && stop && enableInterrupt) { if (showInterruptPrompt) { stop() setShowInterruptPrompt(false) event.currentTarget.form?.requestSubmit() } else if ( props.value || (props.allowAttachments && props.files?.length) ) { setShowInterruptPrompt(true) return } } event.currentTarget.form?.requestSubmit() } onKeyDownProp?.(event) } const textAreaRef = useRef(null) const [textAreaHeight, setTextAreaHeight] = useState(0) useEffect(() => { if (textAreaRef.current) { setTextAreaHeight(textAreaRef.current.offsetHeight) } }, [props.value]) const showFileList = props.allowAttachments && props.files && props.files.length > 0 useAutosizeTextArea({ ref: textAreaRef, maxHeight: 240, borderWidth: 1, dependencies: [props.value, showFileList], }) return (
{enableInterrupt && ( setShowInterruptPrompt(false)} /> )}