import React, { useRef, useEffect, useState } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { Send, Plus, Paperclip, Mic, FileText, Radio, Search, X } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { Button } from '../../ui/button'; import { Popover, PopoverContent, PopoverTrigger } from '../../ui/popover'; import { toast } from 'sonner'; /** * Supported special action types for the chat input. * - 'document': Triggers document generation workflow. * - 'podcast': Triggers audio podcast generation. * - 'search': Triggers a knowledge base search. */ export type ActionType = 'document' | 'podcast' | 'search' | null; /** * Props for the ModernChatInput component. */ interface ModernChatInputProps { /** Current input text value */ value: string; /** Callback fired on every keystroke */ onChange: (value: string) => void; /** Callback fired when the user submits (via Enter or click) */ onSubmit: (action?: ActionType) => void; /** Input placeholder text */ placeholder?: string; /** Whether the input is disabled */ disabled?: boolean; /** Callback for file attachment button */ onFileUpload?: () => void; /** Callback for audio upload button */ onAudioUpload?: () => void; /** Callback fired when a voice recording is finished and transcribed */ onVoiceRecording?: (transcript: string) => void; /** Whether the input is being used in a full-page view (affects layout) */ isFullPage?: boolean; /** Whether to show audio input (voice recording). @default true */ enableAudioInput?: boolean; /** Whether to show the file attachment button. @default true */ enableFileAttachment?: boolean; /** Whether to show the "Create document" action. @default true */ enableDocumentCreation?: boolean; /** Whether to show the "Generate podcast" action. @default true */ enablePodcastGeneration?: boolean; /** Whether to show the "Search" action. @default true */ enableSearch?: boolean; } /** * Modern floating chat input with rich actions and voice recording. * * @description * An advanced text input that supports multi-line typing, quick action chips * (Document, Podcast, Search), file attachments, and simulated voice-to-text recording. * * @ai-rules * 1. Use the `onSubmit` callback to handle both text messages and special actions. * 2. This component manages its own height dynamically based on content (up to 100px). * 3. Voice recording is simulated — it returns a random string to the `onVoiceRecording` callback. */ export function ModernChatInput({ value, onChange, onSubmit, placeholder, disabled = false, onFileUpload, onAudioUpload, onVoiceRecording, isFullPage = false, enableAudioInput = true, enableFileAttachment = true, enableDocumentCreation = true, enablePodcastGeneration = true, enableSearch = true, }: ModernChatInputProps) { const { t } = useTranslation(); const textareaRef = useRef(null); const [isFocused, setIsFocused] = useState(false); const [textareaHeight, setTextareaHeight] = useState(20); const [selectedAction, setSelectedAction] = useState(null); const [isPopoverOpen, setIsPopoverOpen] = useState(false); const [isRecording, setIsRecording] = useState(false); const [recordingTime, setRecordingTime] = useState(0); const recordingIntervalRef = useRef(null); // Resolve placeholder: caller prop takes precedence, then i18n key const resolvedPlaceholder = placeholder ?? t('assistant.inputPlaceholder'); const adjustHeight = () => { const textarea = textareaRef.current; if (textarea) { // Reset to minimum height first textarea.style.height = '20px'; // Calculate new height based on content const scrollHeight = textarea.scrollHeight; const newHeight = Math.min(Math.max(scrollHeight, 20), 100); // Apply the new height smoothly setTextareaHeight(newHeight); textarea.style.height = newHeight + 'px'; } }; // Initialize with proper height on mount useEffect(() => { const textarea = textareaRef.current; if (textarea) { // Force initial height without content check textarea.style.height = '20px'; setTextareaHeight(20); // If there's initial content, adjust immediately if (value) { setTimeout(adjustHeight, 0); } } }, []); // Empty dependency array - only runs on mount // Adjust height when value changes useEffect(() => { if (value === '') { // Reset to minimum when empty const textarea = textareaRef.current; if (textarea) { textarea.style.height = '20px'; setTextareaHeight(20); } } else { // Adjust for content const timeoutId = setTimeout(adjustHeight, 0); return () => clearTimeout(timeoutId); } }, [value]); const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); if (value.trim() && !disabled) { onSubmit(selectedAction); setSelectedAction(null); } } }; const handleActionSelect = (action: ActionType) => { setSelectedAction(action); setIsPopoverOpen(false); }; const handleRemoveAction = () => { setSelectedAction(null); }; const handleVoiceRecording = () => { if (isRecording) { // Stop recording if (recordingIntervalRef.current) { clearInterval(recordingIntervalRef.current); recordingIntervalRef.current = null; } // Simulate audio transcription — demo strings keyed by i18n const transcricoes = [ t('assistant.voiceTranscriptions.salesData'), t('assistant.voiceTranscriptions.performanceReport'), t('assistant.voiceTranscriptions.marketTrends'), t('assistant.voiceTranscriptions.customerSatisfaction'), t('assistant.voiceTranscriptions.teamProductivity'), t('assistant.voiceTranscriptions.keyMetrics'), t('assistant.voiceTranscriptions.digitalMarketing'), t('assistant.voiceTranscriptions.socialEngagement'), ]; const transcricaoAleatoria = transcricoes[Math.floor(Math.random() * transcricoes.length)]; // Call the callback with the transcription if (onVoiceRecording) { onVoiceRecording(transcricaoAleatoria); } setIsRecording(false); setRecordingTime(0); } else { // Start recording setIsRecording(true); setRecordingTime(0); toast.info(t('assistant.recordingStarted'), { description: t('assistant.recordingDescriptionFull'), duration: 3000, }); recordingIntervalRef.current = setInterval(() => { setRecordingTime(prev => prev + 1); }, 1000); } }; // Automatically stop recording after 60 seconds useEffect(() => { if (recordingTime >= 60 && isRecording) { handleVoiceRecording(); } }, [recordingTime, isRecording]); // Clear interval on unmount useEffect(() => { return () => { if (recordingIntervalRef.current) { clearInterval(recordingIntervalRef.current); } }; }, []); const getActionInfo = (action: ActionType) => { switch (action) { case 'document': return { label: t('assistant.actions.createDocument'), icon: FileText, color: 'bg-[var(--chart-4)]', }; case 'podcast': return { label: t('assistant.actions.generatePodcast'), icon: Radio, color: 'bg-[var(--chart-1)]', }; case 'search': return { label: t('assistant.actions.search'), icon: Search, color: 'bg-[var(--chart-2)]' }; default: return null; } }; const handleFocus = () => setIsFocused(true); const handleBlur = () => setIsFocused(false); const hasContent = value.trim().length > 0; return (
{/* Recording Indicator */} {isRecording && (
{/* Pulsing Dot */} {/* Audio Waves Animation */}
{[0, 1, 2, 3, 4].map(i => ( ))}
{t('assistant.recordingAudio')}
{Math.floor(recordingTime / 60)}: {(recordingTime % 60).toString().padStart(2, '0')}
)}
{/* Modern Input Container */}
{/* Action Chip */} {selectedAction && ( {(() => { const actionInfo = getActionInfo(selectedAction); if (!actionInfo) return null; const Icon = actionInfo.icon; return (
{actionInfo.label}
); })()}
)}
{/* Text Input - Full Width */}