"use client"; import { useEffect, useMemo, useRef, useState, useCallback } from "react"; import { useRouter } from "next/navigation"; import useSWR, { mutate as gmutate } from "swr"; import { ArrowDown, Download, Pin, FolderKanban, AlertTriangle, Sparkles, Columns2, Telescope, FileText, Code2, Lightbulb, GraduationCap } from "lucide-react"; import { useChat } from "@/lib/client/use-chat"; import { Composer, type ComposerSubmit } from "./composer"; import { Message } from "./message"; import { ArtifactPanel, type ArtifactView } from "./artifact-panel"; import { api, fetcher } from "@/lib/client/fetcher"; import type { ProjectDTO } from "@/lib/client/types"; import { cn, IconButton, toast } from "../ui"; import { setPrefs } from "@/lib/client/prefs"; import { useShell } from "../app-shell"; const SUGGESTIONS = [ { icon: FileText, title: "Analyser un document", text: "Résume ce document et relève les points qui méritent une vérification.", hint: "glisse un PDF, DOCX ou XLSX" }, { icon: Code2, title: "Écrire du code", text: "Écris une fonction TypeScript qui…", hint: "aperçu HTML/React dans le panneau d'artefacts" }, { icon: GraduationCap, title: "Évaluation immobilière", text: "Explique la méthode du coût pour un immeuble à revenus et donne un exemple chiffré au Québec.", hint: "contexte UQO" }, { icon: Lightbulb, title: "Recherche sourcée", text: "Cherche sur le web les derniers chiffres sur … et cite tes sources.", hint: "outil web_search" }, ]; export function ChatView({ conversationId, projectId }: { conversationId: string | null; projectId?: string | null }) { const chat = useChat(conversationId); const router = useRouter(); const { openPalette } = useShell(); const isDraft = !chat.conversationId; const { data: projData } = useSWR<{ projects: ProjectDTO[] }>("/api/projects", fetcher); const [artifact, setArtifact] = useState(null); const scroller = useRef(null); const [stuck, setStuck] = useState(true); const effectiveProject = chat.conversation?.project_id ?? projectId ?? null; const project = projData?.projects.find(p => p.id === effectiveProject); // auto-scroll while streaming when the user is near the bottom useEffect(() => { const el = scroller.current; if (!el || !stuck) return; el.scrollTop = el.scrollHeight; }, [chat.messages, stuck]); const onScroll = useCallback(() => { const el = scroller.current; if (!el) return; setStuck(el.scrollHeight - el.scrollTop - el.clientHeight < 80); }, []); useEffect(() => { if (location.hash && chat.messages.length) { const t = document.getElementById(location.hash.slice(1)); if (t) { t.scrollIntoView({ block: "center" }); t.classList.add("ring-2", "ring-accent/40", "rounded-xl"); setTimeout(() => t.classList.remove("ring-2", "ring-accent/40"), 2500); } } }, [chat.messages.length]); const siblingsFor = useMemo(() => { const map = new Map(); for (const m of chat.messages) { const sibs = chat.allMessages.filter(x => (x.parent_id ?? null) === (m.parent_id ?? null) && x.role === m.role).sort((a, b) => a.created_at.localeCompare(b.created_at)); if (sibs.length > 1) map.set(m.id, { ids: sibs.map(x => x.id), index: sibs.findIndex(x => x.id === m.id) }); } return map; }, [chat.allMessages, chat.messages]); async function submit(s: ComposerSubmit) { setStuck(true); await chat.send(s.text, { modelId: s.routingMode === "manual" ? s.modelId ?? undefined : s.modelId ?? undefined, routingMode: s.routingMode, settings: s.effort ? { effort: s.effort } : undefined, toolsEnabled: s.toolsEnabled, projectId: effectiveProject }, s.attachments); } async function branch(messageId: string) { if (!chat.conversationId) return; const r = await api<{ conversation: { id: string } }>(`/api/conversations/${chat.conversationId}/branch`, { method: "POST", json: { messageId } }); void gmutate("/api/conversations"); toast("Branche créée", "ok"); router.push(`/c/${r.conversation.id}`); } const lastAssistantId = [...chat.messages].reverse().find(m => m.role === "assistant")?.id; return (
{/* header */}
{chat.conversation?.pinned && }

{chat.conversation?.title ?? (chat.conversationId ? "…" : "Nouvelle conversation")}

{project &&
{project.name}
}
{chat.conversation && <> {chat.conversation.message_count} msgs · {Number(chat.conversation.total_cost_usd).toFixed(3)} $ window.open(`/api/conversations/${chat.conversationId}/export?format=md`)}> router.push(`/compare?prompt=${encodeURIComponent([...chat.messages].reverse().find(m => m.role === "user")?.text ?? "")}`)}> }
{/* messages */}
{chat.loading && !chat.messages.length &&
{[0, 1, 2].map(i =>
)}
} {chat.error && !chat.messages.length &&
{chat.error}
} {isDraft && !chat.messages.length && !chat.loading && (

Que veux-tu explorer ?

Tous les modèles OpenAI et Anthropic vérifiés, une seule interface privée.{project ? ` Projet « ${project.name} ».` : ""}

{SUGGESTIONS.map(s => )}
)} {chat.messages.map(m => { const sib = siblingsFor.get(m.id); return chat.regenerate(id) : undefined, onEdit: m.role === "user" ? (id, text) => chat.edit(id, text) : undefined, onBranch: chat.conversationId ? branch : undefined, onApprove: (mid, a, d) => chat.approve(mid, a, d), onOpenArtifact: setArtifact, siblings: sib, onSwitchSibling: sib ? (t) => chat.switchBranch(t) : undefined, }} />; })} {chat.warning &&
{chat.warning}
}
{!stuck && }
{artifact &&
setArtifact(null)} />
} {artifact &&
setArtifact(null)} />
}
); } export function usePersistModel() { return setPrefs; }