import { ChevronDownIcon, ChevronUpIcon } from "@heroicons/react/20/solid";
import { Clipboard, ClipboardCheck } from "lucide-react";
import { Suspense, useEffect, useState } from "react";
import { CodeSquareIcon } from "~/assets/icons/CodeSquareIcon";
import { TextSquareIcon } from "~/assets/icons/TextSquareIcon";
import { CodeBlock } from "~/components/code/CodeBlock";
import { StreamdownRenderer } from "~/components/code/StreamdownRenderer";
import { Button, LinkButton } from "~/components/primitives/Buttons";
import { Header3 } from "~/components/primitives/Headers";
import tablerSpritePath from "~/components/primitives/tabler-sprite.svg";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "~/components/primitives/Tooltip";
import { cn } from "~/utils/cn";
import type { DisplayItem, ToolUse } from "./types";
export type PromptLink = {
slug: string;
version?: string;
path: string;
};
export function AIChatMessages({
items,
promptLink,
}: {
items: DisplayItem[];
promptLink?: PromptLink;
}) {
return (
{items.map((item, i) => {
switch (item.type) {
case "system":
return
;
case "user":
return
;
case "tool-use":
return
;
case "assistant":
return
;
}
})}
);
}
// ---------------------------------------------------------------------------
// Section header (shared across all sections)
// ---------------------------------------------------------------------------
function SectionHeader({ label, right }: { label: string; right?: React.ReactNode }) {
return (
{label}
{right &&
{right}
}
);
}
export function ChatBubble({ children }: { children: React.ReactNode }) {
return (
{children}
);
}
// ---------------------------------------------------------------------------
// System
// ---------------------------------------------------------------------------
function SystemSection({ text, promptLink }: { text: string; promptLink?: PromptLink }) {
const [expanded, setExpanded] = useState(false);
const isLong = text.length > 150;
const preview = isLong ? text.slice(0, 150) + "..." : text;
const displayText = expanded || !isLong ? text : preview;
return (
System
{promptLink && (
{promptLink.slug}
{promptLink.version ? ` v${promptLink.version}` : ""}
)}
{isLong && (
{displayText}}>
{displayText}
);
}
// ---------------------------------------------------------------------------
// User
// ---------------------------------------------------------------------------
function UserSection({ text }: { text: string }) {
return (
);
}
// ---------------------------------------------------------------------------
// Assistant response (with markdown/raw toggle)
// ---------------------------------------------------------------------------
function isJsonString(value: string): boolean {
const trimmed = value.trimStart();
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return false;
try {
JSON.parse(value);
return true;
} catch {
return false;
}
}
export function AssistantResponse({
text,
headerLabel = "Assistant",
}: {
text: string;
headerLabel?: string;
}) {
const isJson = isJsonString(text);
const [mode, setMode] = useState<"rendered" | "raw">("rendered");
const [copied, setCopied] = useState(false);
function handleCopy() {
navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
if (isJson) {
return (
);
}
return (
setMode(mode === "rendered" ? "raw" : "rendered")}
className="text-text-dimmed transition-colors duration-100 focus-custom hover:cursor-pointer hover:text-text-bright"
>
{mode === "rendered" ? (
) : (
)}
{mode === "rendered" ? "Show raw" : "Show rendered"}
{copied ? (
) : (
)}
{copied ? "Copied" : "Copy"}
}
/>
{mode === "rendered" ? (
{text}}>
{text}
) : (
)}
);
}
// ---------------------------------------------------------------------------
// Tool use (merged calls + results)
// ---------------------------------------------------------------------------
function ToolUseSection({ tools }: { tools: ToolUse[] }) {
return (
{tools.map((tool) => (
))}
);
}
type ToolTab = "input" | "output" | "details" | "agent";
export function ToolUseRow({ tool }: { tool: ToolUse }) {
const hasInput = tool.inputJson !== "{}";
const hasResult = !!tool.resultOutput;
const hasDetails = !!tool.description || !!tool.parametersJson;
const hasSubAgent = !!tool.subAgent;
const availableTabs: ToolTab[] = [
...(hasSubAgent ? (["agent"] as const) : []),
...(hasInput ? (["input"] as const) : []),
...(hasResult ? (["output"] as const) : []),
...(hasDetails ? (["details"] as const) : []),
];
const [activeTab, setActiveTab] = useState(
hasSubAgent ? "agent" : hasInput ? "input" : null
);
// Auto-select input tab when input arrives after initial render (e.g. streaming tool calls)
useEffect(() => {
if (!hasSubAgent && hasInput && activeTab === null) {
setActiveTab("input");
}
}, [hasInput, hasSubAgent]);
function handleTabClick(tab: ToolTab) {
setActiveTab(activeTab === tab ? null : tab);
}
return (
{hasSubAgent && (
)}
{tool.toolName}
{hasSubAgent && tool.subAgent?.isStreaming && (
streaming
)}
{tool.resultSummary && (
{tool.resultSummary}
)}
{availableTabs.length > 0 && (
<>
{availableTabs.map((tab) => (
))}
{activeTab === "agent" && hasSubAgent &&
}
{activeTab === "input" && hasInput && (
)}
{activeTab === "output" && hasResult && (
{isJsonString(tool.resultOutput!) ? (
) : (
{tool.resultOutput}}
>
{tool.resultOutput!}
)}
)}
{activeTab === "details" && hasDetails && (
{tool.description && (
{tool.description}
)}
{tool.parametersJson && (
Parameters schema
)}
)}
>
)}
);
}
function SubAgentContent({ parts }: { parts: any[] }) {
// Extract sub-agent run ID from injected metadata part
const runPart = parts.find((p: any) => p.type === "data-subagent-run" && p.data?.runId);
const subAgentRunId = runPart?.data?.runId as string | undefined;
return (
{subAgentRunId && (
View sub-agent run
)}
{parts.map((part: any, j: number) => {
const partType = part.type as string;
// Skip the injected metadata part — already rendered above
if (partType === "data-subagent-run") return null;
if (partType === "text" && part.text) {
return
;
}
if (partType === "step-start") {
return (
);
}
if (partType.startsWith("tool-")) {
const subToolName = partType.slice(5);
return (
);
}
if (partType === "reasoning" && part.text) {
return (
);
}
return null;
})}
);
}