import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/src/components/ui/tooltip"; import { useState } from "react"; import Decimal from "decimal.js"; import { getMaxDecimals } from "@/src/features/models/utils"; interface Details { [key: string]: number | undefined; } interface BreakdownTooltipProps { details: Details | Details[]; children: React.ReactNode; isCost?: boolean; } export const BreakdownTooltip = ({ details, children, isCost = false, }: BreakdownTooltipProps) => { const [isOpen, setIsOpen] = useState(false); // Aggregate details if array is provided const aggregatedDetails = Array.isArray(details) ? details.reduce
((acc, curr) => { Object.entries(curr).forEach(([key, value]) => { acc[key] = new Decimal(acc[key] || 0) .plus(new Decimal(value || 0)) .toNumber(); }); return acc; }, {}) : details; const formatValueWithPadding = (value: number, maxDecimals: number) => { return !value ? "0" : isCost ? `$${value.toFixed(maxDecimals)}` : value.toLocaleString(); }; const maxDecimals = isCost ? Math.max( ...Object.values(aggregatedDetails).map((v) => getMaxDecimals(v)), ) : 0; return ( setIsOpen(!isOpen)} > {children}
{isCost ? "Cost breakdown" : "Usage breakdown"} {Array.isArray(details) && details.length > 0 && ( Aggregate across {details.length}{" "} {details.length === 1 ? "generation" : "generations"} )}
{/* Input Section */}
key.includes("input")} formatValue={(v) => formatValueWithPadding(v, maxDecimals)} /> {/* Output Section */}
key.includes("output")} formatValue={(v) => formatValueWithPadding(v, maxDecimals)} /> {/* Other Section */} formatValueWithPadding(v, maxDecimals)} /> {/* Total */}
{isCost ? "Total cost" : "Total usage"} {formatValueWithPadding( aggregatedDetails.total ?? 0, maxDecimals, )}
); }; interface SectionProps { title: string; details: Details; filterFn: (key: string) => boolean; formatValue: (value: number) => string; } const Section = ({ title, details, filterFn, formatValue }: SectionProps) => { const filteredEntries = Object.entries(details) .filter(([key]) => filterFn(key)) .sort(([, a], [, b]) => (b ?? 0) - (a ?? 0)); const sectionTotal = filteredEntries.reduce( (sum, [_, value]) => new Decimal(sum).plus(new Decimal(value ?? 0)).toNumber(), 0, ); return (
{title} {formatValue(sectionTotal)}
{filteredEntries.map(([key, value]) => (
{key} {formatValue(value ?? 0)}
))}
); }; interface OtherSectionProps { details: Details; isCost: boolean; formatValue: (value: number) => string; } const OtherSection = ({ details, isCost, formatValue }: OtherSectionProps) => { const otherEntries = Object.entries(details) .filter( ([key]) => !key.includes("input") && !key.includes("output") && key !== "total", ) .sort(([, a], [, b]) => (b ?? 0) - (a ?? 0)); if (otherEntries.length === 0) return null; const otherTotal = otherEntries.reduce((acc, val) => { if (typeof val[1] !== "number") return acc; return acc + (val[1] ?? 0); }, 0); return (
{isCost ? "Other cost" : "Other usage"} {formatValue(otherTotal)}
{otherEntries.map(([key, value]) => (
{key} {formatValue(value ?? 0)}
))}
); };