"use client"; import { CheckIcon, ChevronDownIcon, RotateCwIcon } from "lucide-react"; import { AnimatePresence, motion, useReducedMotion } from "motion/react"; import { type CSSProperties, type ReactNode, useEffect, useId, useRef, useState } from "react"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; export type ReceiptStatus = "running" | "success" | "error"; export interface ToolReceiptCall { id: string; name: string; summary?: string; status: ReceiptStatus; time?: string; durationMs?: number; tokens?: number; cost?: number; args?: Record; result?: unknown; error?: string; } export interface ToolReceiptProps { call: ToolReceiptCall; defaultOpen?: boolean; open?: boolean; onOpenChange?: (open: boolean) => void; onRetry?: (id: string) => void; currency?: string; className?: string; punched?: boolean; } export interface ToolReceiptSpikeProps { calls: ToolReceiptCall[]; title?: string; newestFirst?: boolean; defaultOpenId?: string; onRetry?: (id: string) => void; currency?: string; className?: string; } const spring = { type: "spring", stiffness: 420, damping: 38, mass: 0.9 } as const; const outline: CSSProperties = { filter: "drop-shadow(0 0 0.5px color-mix(in oklab, var(--color-foreground) 30%, transparent)) drop-shadow(0 1px 1.5px color-mix(in oklab, var(--color-foreground) 7%, transparent))", }; const HOLE = { x: 16, y: 22, r: 5 }; function paperMask(punched: boolean): CSSProperties { const body = "linear-gradient(black, black) top / 100% calc(100% - 5px) no-repeat"; const teeth = "conic-gradient(from -45deg at 50% 100%, black 90deg, transparent 0) bottom / 10px 5px repeat-x"; if (!punched) return { mask: `${body}, ${teeth}` }; const hole = `radial-gradient(circle at ${HOLE.x}px ${HOLE.y}px, transparent ${HOLE.r}px, black ${HOLE.r + 0.5}px) 0 0 / 100% 100% no-repeat`; return { mask: `${hole}, ${body}, ${teeth}`, maskComposite: "intersect, add, add" } as CSSProperties; } function fmtMs(ms: number) { return ms < 1000 ? `${Math.round(ms)}ms` : `${(ms / 1000).toFixed(ms < 10_000 ? 2 : 1)}s`; } const fmtTokens = (n: number) => n.toLocaleString("en-US"); const fmtCost = (n: number, cur: string) => `${cur}${n.toFixed(4)}`; function scalar(v: unknown) { if (typeof v === "string") return v; if (v === null || v === undefined) return "—"; return String(v); } function toLines(value: unknown, prefix = ""): [string, string][] { if (value === null || typeof value !== "object") return [[prefix || "value", scalar(value)]]; if (Array.isArray(value)) { if (value.every((v) => v === null || typeof v !== "object")) return [[prefix || "items", value.map(scalar).join(", ")]]; return value.flatMap((v, i) => toLines(v, `${prefix}[${i}]`)); } return Object.entries(value).flatMap(([k, v]) => toLines(v, prefix ? `${prefix}.${k}` : k)); } function useElapsed(status: ReceiptStatus, durationMs?: number) { const [elapsed, setElapsed] = useState(0); const start = useRef(0); useEffect(() => { if (status !== "running") return; start.current = performance.now(); setElapsed(0); const t = setInterval(() => setElapsed(performance.now() - start.current), 100); return () => clearInterval(t); }, [status]); return status === "running" ? elapsed : (durationMs ?? elapsed); } function Barcode({ seed }: { seed: string }) { let h = 2166136261; const bars: number[] = []; for (let i = 0; i < 44; i++) { h ^= seed.charCodeAt(i % seed.length) + i; h = Math.imul(h, 16777619) >>> 0; bars.push((h % 3) + 1); } return ( {bars.map((w, i) => ( ))} ); } function Leader({ label, value, strong, className }: { label: ReactNode; value: ReactNode; strong?: boolean; className?: string }) { return (
{label} {value}
); } function Section({ title, lines }: { title: string; lines: [string, string][] }) { return (
{title} {lines.map(([k, v]) => ( ))}
); } const Rule = ({ double }: { double?: boolean }) => (
); export function ToolReceipt({ call, defaultOpen = false, open: openProp, onOpenChange, onRetry, currency = "$", className, punched = false, }: ToolReceiptProps) { const reduce = useReducedMotion(); const bodyId = useId(); const [openState, setOpenState] = useState(defaultOpen); const open = openProp ?? openState; const setOpen = (next: boolean) => { setOpenState(next); onOpenChange?.(next); }; const elapsed = useElapsed(call.status, call.durationMs); const running = call.status === "running"; const failed = call.status === "error"; const argLines = call.args ? toLines(call.args) : []; const resultLines = call.result !== undefined && !failed ? toLines(call.result) : []; return (
{open && ( {argLines.length > 0 &&
} {resultLines.length > 0 && ( <>
)} {failed && call.error && ( <>
Error {call.error}
)}
{call.id} {call.time && {call.time}}
)}
{failed ? ( <> {open ? "Not charged" : (call.error ?? "Failed")} {onRetry && ( )} ) : ( ) : call.cost !== undefined ? ( fmtCost(call.cost, currency) ) : ( "—" ) } /> )}
{failed && ( VOID )}
); } function PrintHead() { return ( Running {[0, 1, 2].map((i) => ( ))} ); } function StatusGlyph({ status }: { status: ReceiptStatus }) { return ( {status === "running" ? ( ) : status === "success" ? ( ) : ( )} ); } const DIGITS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; function Roll({ text, className }: { text: string; className?: string }) { const reduce = useReducedMotion(); let d = 0; let o = 0; const glyphs = [...text] .reverse() .map((c) => ({ c, digit: c >= "0" && c <= "9", key: c >= "0" && c <= "9" ? `d${d++}` : `s${o++}` })) .reverse(); return ( {text} {glyphs.map(({ c, digit, key }) => digit ? ( 0 {DIGITS.map((n) => ( {n} ))} ) : ( {c} ), )} ); } export function ToolReceiptSpike({ calls, title = "Tool calls", newestFirst = true, defaultOpenId, onRetry, currency = "$", className, }: ToolReceiptSpikeProps) { const reduce = useReducedMotion(); const ordered = newestFirst ? [...calls].reverse() : calls; const settled = calls.filter((c) => c.status === "success"); const cost = settled.reduce((s, c) => s + (c.cost ?? 0), 0); const tokens = settled.reduce((s, c) => s + (c.tokens ?? 0), 0); const ms = calls.reduce((s, c) => s + (c.status === "running" ? 0 : (c.durationMs ?? 0)), 0); const running = calls.filter((c) => c.status === "running").length; const voided = calls.filter((c) => c.status === "error").length; return (
{title} {calls.length} {calls.length === 1 ? "call" : "calls"} · {fmtMs(ms)} {running > 0 ? ` · ${running} printing` : ""} {voided > 0 ? ` · ${voided} void` : ""}
tok
    {ordered.map((call) => ( ))} {calls.length === 0 && (
  1. Waiting for the first tool call
  2. )}
); }