"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 React.ChangeEvent); }, }); 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)} /> )}