"use client"; import { useEffect, useRef, useState, type KeyboardEvent } from "react"; import { ArrowUp, Square, Paperclip, Mic, MicOff, Wrench, X, FileText, Image as ImageIcon, SlidersHorizontal } from "lucide-react"; import { ModelPicker } from "./model-picker"; import type { AttachmentDTO, RoutingMode } from "@/lib/client/types"; import { usePrefs, setPrefs } from "@/lib/client/prefs"; import { api } from "@/lib/client/fetcher"; import { cn, toast, Toggle } from "../ui"; import { useModels } from "@/lib/client/use-models"; export interface ComposerSubmit { text: string; attachments: AttachmentDTO[]; modelId: string | null; routingMode: RoutingMode; effort?: "low" | "medium" | "high" | "xhigh" | "max"; toolsEnabled: boolean } export function Composer({ onSubmit, onStop, streaming, conversationId, projectId, placeholder, initialModel, autoFocus = true }: { onSubmit: (s: ComposerSubmit) => void; onStop: () => void; streaming: boolean; conversationId: string | null; projectId?: string | null; placeholder?: string; initialModel?: string | null; autoFocus?: boolean }) { const prefs = usePrefs(); const { chat, defaultModel, byId } = useModels(); const [text, setText] = useState(""); const [atts, setAtts] = useState([]); const [uploading, setUploading] = useState(false); const [rec, setRec] = useState(null); const [transcribing, setTranscribing] = useState(false); const [opts, setOpts] = useState(false); const ta = useRef(null); const fileRef = useRef(null); const modelId = prefs.modelId && byId(prefs.modelId)?.available ? prefs.modelId : (initialModel && byId(initialModel)?.available ? initialModel : defaultModel && byId(defaultModel)?.available ? defaultModel : chat[0]?.id ?? null); const model = byId(modelId); const supportsEffort = model && (/^(gpt-5|gpt-6|o\d)/.test(model.id) && !/chat-latest$/.test(model.id) || /-4-6|-4-7|-4-8|opus-5|sonnet-5|haiku-5|fable/.test(model.id)); useEffect(() => { const el = ta.current; if (!el) return; el.style.height = "0px"; el.style.height = Math.min(el.scrollHeight, 260) + "px"; }, [text]); useEffect(() => { if (autoFocus && window.innerWidth > 768) ta.current?.focus(); }, [autoFocus, conversationId]); function submit() { if (streaming) return; if (!text.trim() && !atts.length) return; if (prefs.routingMode === "manual" && !modelId) { toast("Choisis un modèle", "error"); return; } onSubmit({ text, attachments: atts, modelId, routingMode: prefs.routingMode, effort: prefs.effort, toolsEnabled: prefs.toolsEnabled }); setText(""); setAtts([]); } function onKey(e: KeyboardEvent) { if (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing && prefs.sendWithEnter && window.innerWidth > 768) { e.preventDefault(); submit(); } else if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) { e.preventDefault(); submit(); } } async function upload(files: FileList | File[]) { const list = Array.from(files); if (!list.length) return; setUploading(true); try { const fd = new FormData(); for (const f of list) fd.append("file", f); if (conversationId) fd.append("conversationId", conversationId); if (projectId) fd.append("projectId", projectId); const r = await api<{ attachments: AttachmentDTO[] }>("/api/attachments", { method: "POST", body: fd }); setAtts(a => [...a, ...r.attachments]); } catch (e) { toast((e as Error).message, "error"); } finally { setUploading(false); } } async function toggleMic() { if (rec) { rec.stop(); return; } try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); const mime = MediaRecorder.isTypeSupported("audio/webm") ? "audio/webm" : "audio/mp4"; const r = new MediaRecorder(stream, { mimeType: mime }); const chunks: Blob[] = []; const t0 = Date.now(); r.ondataavailable = e => chunks.push(e.data); r.onstop = async () => { stream.getTracks().forEach(t => t.stop()); setRec(null); setTranscribing(true); try { const fd = new FormData(); fd.append("audio", new File(chunks, `voice.${mime.includes("webm") ? "webm" : "m4a"}`, { type: mime })); fd.append("durationSec", String((Date.now() - t0) / 1000)); const res = await api<{ text: string }>("/api/stt", { method: "POST", body: fd }); setText(t => (t ? t + " " : "") + res.text); ta.current?.focus(); } catch (e) { toast((e as Error).message, "error"); } finally { setTranscribing(false); } }; r.start(); setRec(r); } catch { toast("Micro indisponible", "error"); } } return (
e.preventDefault()} onDrop={e => { e.preventDefault(); void upload(e.dataTransfer.files); }} className={cn("rounded-2xl border bg-surface shadow-soft transition-colors focus-within:border-border-strong", streaming ? "border-border" : "border-border")}> {atts.length > 0 &&
{atts.map(a => {a.mime.startsWith("image/") ? : }{a.filename}{a.pages ? {a.pages} p. : null})}
}