1const e=`import { useEffect, useRef } from 'react'; 2import { Button } from '@/components/ui/button'; 3import { Textarea } from '@/components/ui/textarea'; 4import { Send, Loader2, Mic, MicOff, Paperclip, X, FileText } from 'lucide-react'; 5import { cn } from '@/lib/utils'; 6 7import { tx } from "@/i18n/t"; 8interface ChatInputProps { 9 input: string; 10 onInputChange: (value: string) => void; 11 onSubmit: (e?: React.FormEvent) => void; 12 onKeyDown: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void; 13 onToggleVoiceInput: () => void; 14 isLoading: boolean; 15 isListening: boolean; 16 inputRef: React.RefObject<HTMLInputElement>; 17 pendingFile?: File | null; 18 onFileSelected?: (file: File | null) => void; 19 accept?: string; 20} 21 22export function ChatInput({ 23 input, onInputChange, onSubmit, onKeyDown, onToggleVoiceInput, 24 isLoading, isListening, inputRef, pendingFile, onFileSelected, accept, 25}: ChatInputProps) { 26 const fileRef = useRef<HTMLInputElement>(null); 27 28 // Auto-grow: 3 a ~10 linhas 29 useEffect(() => { 30 const el = inputRef.current as unknown as HTMLTextAreaElement | null; 31 if (!el) return; 32 el.style.height = 'auto'; 33 el.style.height = \`\${Math.min(el.scrollHeight, 260)}px\`; 34 }, [input, inputRef]); 35 36 return ( 37 <div className="p-3 border-t shrink-0"> 38 <form onSubmit={onSubmit} className="rounded-xl border bg-background focus-within:ring-2 focus-within:ring-ring"> 39 {pendingFile && ( 40 <div className="px-3 pt-3"> 41 <span className="inline-flex items-center gap-2 rounded-md bg-muted px-2 py-1 text-xs"> 42 <FileText className="h-3 w-3" /> 43 <span className="max-w-[220px] truncate">{pendingFile.name}</span> 44 <button type="button" onClick={() => onFileSelected?.(null)} aria-label={tx("Remover anexo")}> 45 <X className="h-3 w-3" /> 46 </button> 47 </span> 48 </div> 49 )} 50 <Textarea 51 ref={inputRef as any} 52 value={input} 53 onChange={(e) => onInputChange(e.target.value)} 54 onKeyDown={onKeyDown} 55 placeholder={isListening ? 'Ouvindo...' : tx('Pergunte ao Professor Beni... (Enter envia, Shift+Enter quebra linha)')} 56 disabled={isLoading} 57 className="min-h-[84px] max-h-[260px] resize-none border-0 shadow-none focus-visible:ring-0 focus-visible:ring-offset-0" 58 rows={3} 59 /> 60 <div className="flex items-center justify-between gap-2 px-2 pb-2"> 61 <div className="flex items-center gap-1"> 62 {onFileSelected && ( 63 <> 64 <input 65 ref={fileRef} 66 type="file" 67 accept={accept} 68 className="hidden" 69 onChange={(e) => { onFileSelected(e.target.files?.[0] ?? null); e.target.value = ''; }} 70 /> 71 <Button type="button" variant="ghost" size="icon" className="h-8 w-8" disabled={isLoading} 72 onClick={() => fileRef.current?.click()} title={tx("Anexar arquivo (até 10 MB)")}> 73 <Paperclip className="h-4 w-4" /> 74 </Button> 75 </> 76 )} 77 <Button 78 type="button" 79 variant={isListening ? 'destructive' : 'ghost'} 80 size="icon" 81 onClick={onToggleVoiceInput} 82 disabled={isLoading} 83 className={cn('h-8 w-8', isListening && 'animate-pulse')} 84 title={isListening ? tx('Parar gravação') : 'Falar'} 85 > 86 {isListening ? <MicOff className="h-4 w-4" /> : <Mic className="h-4 w-4" />} 87 </Button> 88 </div> 89 <Button type="submit" size="icon" className="h-8 w-8" disabled={isLoading || (!input.trim() && !pendingFile)} title={tx("Enviar")}> 90 {isLoading ? <Loader2 className="h-4 w-4 animate-spin" /> : <Send className="h-4 w-4" />} 91 </Button> 92 </div> 93 </form> 94 </div> 95 ); 96} 97`;export{e as default};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.