import { useEffect, useRef, useState, type ChangeEvent, type DragEvent, type KeyboardEvent } from 'react'; import { ArrowUp, Brain, Mic, MicOff, Paperclip, Square, X, FileText } from 'lucide-react'; import { api } from '@/lib/api'; import type { UploadedFile } from '@/lib/types'; import { cn } from '@/lib/cn'; import { fmtBytes } from '@/lib/format'; import { useUI } from '@/stores/ui'; interface SpeechRecognitionLike { start(): void; stop(): void; lang: string; interimResults: boolean; continuous: boolean; onresult: ((e: { results: ArrayLike> }) => void) | null; onend: (() => void) | null } declare global { interface Window { webkitSpeechRecognition?: new () => SpeechRecognitionLike; SpeechRecognition?: new () => SpeechRecognitionLike } } export function Composer({ conversationId, streaming, onSend, onStop, deep, onToggleDeep, course, onCourseChange, courses, disabled }: { conversationId: string | null; streaming: boolean; onSend: (text: string, attachments: string[]) => void; onStop: () => void; deep: boolean; onToggleDeep: () => void; course: string; onCourseChange: (c: string) => void; courses: string[]; disabled?: boolean; }) { const [text, setText] = useState(''); const [files, setFiles] = useState([]); const [uploading, setUploading] = useState(false); const [drag, setDrag] = useState(false); const [error, setError] = useState(null); const [listening, setListening] = useState(false); const taRef = useRef(null); const fileRef = useRef(null); const recRef = useRef(null); const speechOk = typeof window !== 'undefined' && !!(window.SpeechRecognition || window.webkitSpeechRecognition); const draft = useUI((st) => st.draft); const setDraft = useUI((st) => st.setDraft); useEffect(() => { if (draft) { setText(draft); setDraft(''); setTimeout(() => { taRef.current?.focus(); taRef.current?.setSelectionRange(taRef.current.value.length, taRef.current.value.length); }, 50); } }, [draft, setDraft]); useEffect(() => { const ta = taRef.current; if (!ta) return; ta.style.height = 'auto'; ta.style.height = Math.min(ta.scrollHeight, 200) + 'px'; }, [text]); const upload = async (list: FileList | File[]) => { setError(null); setUploading(true); try { for (const f of Array.from(list)) { const fd = new FormData(); fd.append('file', f); if (conversationId) fd.append('conversation_id', conversationId); const up = await api('/files', { method: 'POST', body: fd }); setFiles((prev) => [...prev, up]); } } catch (e) { setError(e instanceof Error ? e.message : 'Téléversement impossible.'); } finally { setUploading(false); } }; const send = () => { const t = text.trim(); if (!t || streaming || disabled) return; onSend(t, files.map((f) => f.file_id)); setText(''); setFiles([]); }; const onKey = (e: KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) { e.preventDefault(); send(); } }; const onDrop = (e: DragEvent) => { e.preventDefault(); setDrag(false); if (e.dataTransfer.files?.length) void upload(e.dataTransfer.files); }; const toggleMic = () => { if (listening) { recRef.current?.stop(); setListening(false); return; } const Ctor = window.SpeechRecognition || window.webkitSpeechRecognition; if (!Ctor) return; const rec = new Ctor(); rec.lang = 'fr-CA'; rec.interimResults = false; rec.continuous = true; rec.onresult = (ev) => { const parts: string[] = []; for (let i = 0; i < ev.results.length; i++) parts.push(ev.results[i][0].transcript); setText((t) => (t ? t + ' ' : '') + parts.join(' ').trim()); }; rec.onend = () => setListening(false); recRef.current = rec; rec.start(); setListening(true); }; return (
{ e.preventDefault(); setDrag(true); }} onDragLeave={() => setDrag(false)} onDrop={onDrop} className={cn('mx-auto max-w-[900px] rounded-[22px] border bg-white shadow-float transition-colors focus-within:border-uqo-blue/60', drag ? 'border-uqo-green ring-2 ring-uqo-green/30' : 'border-neutral-line')} > {(files.length > 0 || error) && (
{files.map((f) => ( {f.filename} {fmtBytes(f.size)} ))} {error && {error}}
)}