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 (
-
);
})}
)}
);
}
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}
);
}