"use client"; import { memo, useState } from "react"; import { RefreshCw, Pencil, GitBranch, Volume2, ChevronLeft, ChevronRight, Brain, FileText, AlertTriangle, Sparkles, ChevronDown, LayoutPanelLeft, Square } from "lucide-react"; import type { MessageDTO, Part } from "@/lib/client/types"; import { Markdown } from "./markdown"; import { ToolCallCard } from "./tool-call"; import { cn, CopyButton, Badge } from "../ui"; import { fmtMs, fmtTokens, fmtUsd } from "@/lib/client/fetcher"; import { useModels, shortName } from "@/lib/client/use-models"; import { usePrefs } from "@/lib/client/prefs"; import type { ArtifactView } from "./artifact-panel"; import { typeFromLang } from "./artifact-panel"; export interface MessageActions { onRegenerate?: (id: string, modelId?: string) => void; onEdit?: (id: string, text: string) => void; onBranch?: (id: string) => void; onApprove?: (messageId: string, approve: string[], deny: string[]) => void; onSwitchSibling?: (targetId: string) => void; onOpenArtifact?: (a: ArtifactView) => void; siblings?: { ids: string[]; index: number }; } export const Message = memo(function Message({ m, actions, streaming, conversationId }: { m: MessageDTO; actions: MessageActions; streaming: boolean; conversationId: string | null }) { const isUser = m.role === "user"; const { byId } = useModels(); const prefs = usePrefs(); const [editing, setEditing] = useState(false); const [draft, setDraft] = useState(m.text); const [speaking, setSpeaking] = useState(null); const [showReason, setShowReason] = useState(false); const model = byId(m.model_id); const textParts = m.parts.filter(p => p.type === "text") as Extract[]; const reasoning = m.parts.find(p => p.type === "reasoning") as Extract | undefined; const fullText = textParts.map(p => p.text).join("\n"); const pending = m.parts.filter(p => p.type === "tool_call" && p.status === "pending_approval") as Extract[]; async function speak() { if (speaking) { speaking.pause(); setSpeaking(null); return; } const r = await fetch("/api/tts", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ text: fullText.slice(0, 4000) }) }); if (!r.ok) return; const url = URL.createObjectURL(await r.blob()); const a = new Audio(url); a.onended = () => setSpeaking(null); setSpeaking(a); void a.play(); } const openArtifact = (code: string, lang: string) => actions.onOpenArtifact?.({ title: `${lang} — ${new Date(m.created_at).toLocaleTimeString("fr-CA", { hour: "2-digit", minute: "2-digit" })}`, type: typeFromLang(lang), language: lang, content: code, conversationId }); if (isUser) { return (
{m.parts.some(p => p.type === "image" || p.type === "file") &&
{m.parts.map((p, i) => p.type === "image" ? {p.name : p.type === "file" ? {p.name}{p.pages ? {p.pages} p. : null} : null)}
} {editing ? (
{ e.preventDefault(); actions.onEdit?.(m.id, draft); setEditing(false); }} className="w-full max-w-[85%]">