import { useEffect, useRef, useState, type ReactNode } from "react"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; import { ChevronDown, Download, Loader2 } from "lucide-react"; import { supabase } from "@/app/lib/supabase"; import type { AssistantEvent } from "../../shared/types"; import { FileTypeIcon } from "../../shared/FileTypeIcon"; import { RESPONSE_GLASS_SURFACE, withoutMarkdownNode } from "./messageStyles"; const THINKING_PHRASES = [ "Thinking...", "Pondering...", "Analyzing...", "Reviewing...", "Reasoning...", ]; const REASONING_COLLAPSED_MAX_LINES = 6; const REASONING_COLLAPSED_MAX_HEIGHT_REM = 9; // --------------------------------------------------------------------------- // Event block primitives // --------------------------------------------------------------------------- function EventConnector() { return (
); } export function EventBlock({ showConnector, isStreaming, dotColor = "green", children, }: { showConnector?: boolean; isStreaming?: boolean; dotColor?: "green" | "gray" | "red"; children: ReactNode; }) { const dotColorClass = dotColor === "green" ? "bg-green-400 shadow-[0_1px_3px_rgba(15,23,42,0.15),inset_0_1px_0_rgba(255,255,255,0.5)]" : dotColor === "red" ? "bg-red-400 shadow-[0_1px_3px_rgba(15,23,42,0.15),inset_0_1px_0_rgba(255,255,255,0.5)]" : "bg-gray-500 shadow-[0_1px_3px_rgba(15,23,42,0.15)]"; return (
{showConnector && } {isStreaming ? (
) : (
)}
{children}
); } // --------------------------------------------------------------------------- export function ReasoningBlock({ text, isStreaming, showConnector, }: { text: string; isStreaming: boolean; showConnector?: boolean; }) { const [isContentOpen, setIsContentOpen] = useState(false); const [isExpanded, setIsExpanded] = useState(false); const [userToggledContent, setUserToggledContent] = useState(false); const [isOverflowing, setIsOverflowing] = useState(false); const [hasMeasured, setHasMeasured] = useState(false); const [thinkingIndex, setThinkingIndex] = useState(0); const contentRef = useRef(null); useEffect(() => { if (!isStreaming) return; const interval = setInterval(() => { setThinkingIndex((i) => (i + 1) % THINKING_PHRASES.length); }, 2000); return () => clearInterval(interval); }, [isStreaming]); useEffect(() => { const el = contentRef.current; if (!el) return; const lineHeight = parseFloat(getComputedStyle(el).lineHeight) || 24; const maxHeight = lineHeight * REASONING_COLLAPSED_MAX_LINES; const nextOverflowing = el.scrollHeight > maxHeight + 2; setIsOverflowing(nextOverflowing); setHasMeasured(true); if (!userToggledContent) setIsContentOpen(isStreaming); if (!nextOverflowing) setIsExpanded(false); }, [isStreaming, text, userToggledContent]); const showContent = isContentOpen || isStreaming || !hasMeasured; const isCollapsed = isContentOpen && isOverflowing && !isExpanded; return ( {showContent && (
( ), }} > {text}
{isCollapsed && ( <>
)}
{isOverflowing && isContentOpen && isExpanded && ( )}
)} ); } export function DocReadBlock({ filename, onClick, showConnector, isStreaming, showFileIcon = true, }: { filename: string; onClick?: () => void; showConnector?: boolean; isStreaming?: boolean; showFileIcon?: boolean; }) { return (
{isStreaming ? "Reading" : "Read"} {isStreaming ? ( {showFileIcon && ( )} {filename}... ) : onClick ? ( ) : ( {showFileIcon && ( )} {filename} )}
); } export function DocFindBlock({ filename, query, totalMatches, isStreaming, showConnector, }: { filename: string; query: string; totalMatches: number; isStreaming?: boolean; showConnector?: boolean; }) { const matchSuffix = isStreaming ? "" : ` (${totalMatches} ${totalMatches === 1 ? "match" : "matches"})`; return ( 0 ? "green" : "gray"} > {isStreaming ? "Finding" : "Found"} {" "} “{query}”{matchSuffix} in {filename} {isStreaming && "..."} ); } export function DocCreatedBlock({ filename, showConnector, isStreaming, }: { filename: string; showConnector?: boolean; isStreaming?: boolean; }) { return ( {isStreaming ? "Creating" : "Created"} {" "} {isStreaming ? `${filename}...` : filename} ); } export function DocReplicatedBlock({ filename, count, showConnector, isStreaming, hasError, }: { filename: string; /** * How many consecutive replicates of this same source got collapsed * into this block. ≥ 1; only rendered when > 1. */ count: number; showConnector?: boolean; isStreaming?: boolean; hasError?: boolean; }) { const label = isStreaming ? "Replicating" : "Replicated"; const suffix = !isStreaming && count > 1 ? ` ${count} times` : isStreaming ? "..." : ""; return ( {label}{" "} {filename} {suffix} ); } export function DocDownloadBlock({ filename, download_url, onOpen, isReloading = false, versionNumber, }: { filename: string; download_url: string; onOpen?: () => void; isReloading?: boolean; versionNumber?: number | null; }) { const hasVersion = typeof versionNumber === "number" && Number.isFinite(versionNumber) && versionNumber > 0; const extMatch = filename.match(/\.(\w+)$/); const ext = extMatch ? extMatch[1].toUpperCase() : "FILE"; const rawBasename = extMatch ? filename.slice(0, -extMatch[0].length) : filename; // Strip any legacy "[Edited V3]" suffix that may still be baked into // older saved download filenames — the version is surfaced as a // separate tag now. const basename = rawBasename.replace(/\s*\[Edited V\d+\]\s*$/, "").trim(); // Only backend-relative URLs are accepted. The download fetch carries // the user's bearer token, so any absolute URL from tool output is // refused to keep the token from leaking off-origin. const API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:3001"; const isSafeHref = download_url.startsWith("/"); const href = isSafeHref ? `${API_BASE}${download_url}` : null; const [busy, setBusy] = useState(false); const handleDownload = async (e?: { stopPropagation?: () => void; preventDefault?: () => void; }) => { e?.stopPropagation?.(); e?.preventDefault?.(); if (busy || isReloading || !href) return; setBusy(true); try { const { data: { session }, } = await supabase.auth.getSession(); const token = session?.access_token; const resp = await fetch(href, { headers: token ? { Authorization: `Bearer ${token}` } : {}, }); if (!resp.ok) throw new Error(`HTTP ${resp.status}`); const blob = await resp.blob(); const blobUrl = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = blobUrl; a.download = filename; document.body.appendChild(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(blobUrl), 1000); } finally { setBusy(false); } }; const spinning = busy || isReloading; const body = (

{basename}

{hasVersion && ( V{versionNumber} )}

{ext}

); const downloadIcon = spinning ? (
) : ( ); if (onOpen) { return (
{downloadIcon}
); } if (spinning) { return (
{body} {downloadIcon}
); } return (
{downloadIcon}
); } export function WorkflowAppliedBlock({ title, showConnector, onClick, }: { title: string; showConnector?: boolean; onClick?: () => void; }) { return ( Applied Workflow{" "} {onClick ? ( ) : ( {title} )} ); } export function AskInputsBlock({ event, response, showConnector, }: { event: Extract; response?: Extract; showConnector?: boolean; }) { const responseById = new Map( response?.responses.map((item) => [item.id, item]) ?? [], ); return (

{response ? "Asked for input" : "Asking for input"}

{event.items.map((item, index) => { const itemResponse = responseById.get(item.id); const responseText = (() => { if (!itemResponse) return null; if (itemResponse.skipped) return "Skipped"; if (itemResponse.kind === "choice") { return itemResponse.answer ?? ""; } const filenames = itemResponse.filenames; return filenames.length ? filenames.join(", ") : "No documents attached"; })(); return (

{index + 1}.{" "} {item.kind === "choice" ? "Question" : "Documents"}

{item.kind === "choice" ? item.question : item.document_types.join(", ") || "Documents requested"}

{responseText !== null && (

{responseText}

)}
); })}
); } export type CourtListenerBlockItem = { caseName: string | null; citation: string | null; dateFiled?: string | null; url?: string | null; query?: string; totalMatches?: number; hasError?: boolean; }; export function CourtListenerBlock({ label, detail, isStreaming, hasError, showConnector, items, }: { label: string; detail?: string; isStreaming?: boolean; hasError?: boolean; showConnector?: boolean; items?: CourtListenerBlockItem[]; }) { const [isOpen, setIsOpen] = useState(false); const hasItems = !!items && items.length > 0; return ( {hasItems ? ( ) : ( <> {label} {detail ? {detail} : null} {isStreaming ? ... : null} )} {isOpen && hasItems && (
    {items!.map((item, idx) => { const label = [item.caseName, item.citation] .filter(Boolean) .join(", "); const primary = label || item.url || "Unknown case"; const searchText = item.query ? `Searched for "${item.query}" in ${primary}${ typeof item.totalMatches === "number" ? ` (${item.totalMatches} ${ item.totalMatches === 1 ? "match" : "matches" })` : "" }` : null; return (
  • {item.url ? ( {searchText ?? primary} ) : searchText ? ( {searchText} ) : ( {primary} )}
  • ); })}
)}
); } export function DocEditedBlock({ filename, showConnector, isStreaming, hasError, }: { filename: string; showConnector?: boolean; isStreaming?: boolean; hasError?: boolean; }) { return ( {isStreaming ? "Editing" : hasError ? "Edit failed" : "Edited"} {" "} {isStreaming ? `${filename}...` : filename} ); }