SPB Git forge

spb/polyllm

Public
15commits 1branches 0releases
2.2 MBsize
maindefault branch
13 days agolast push
TypeScript 97.4% SQL 1% JavaScript 0.9% CSS 0.6%
6.8 KB · 121 lines tsx
Raw Blame History
1"use client";2import * as React from "react";3import Link from "next/link";4import { ArrowRight, PiggyBank, TrendingDown, TrendingUp, Minus, Info } from "lucide-react";5import { ProviderIcon } from "@/components/brand/provider-icon";6import { Tooltip } from "@/components/ui/tooltip";7import { cn, formatUsd, formatNumber } from "@/lib/utils";8import type { SavingsOpportunity, UsageProjection, UsageKpis } from "./types";910/** "Estimated monthly cost" — daily average over the observed part of the range × 30, plus the trend vs the previous period. */11export function ProjectionCard({ projection, kpis, rangeDays, className }: { projection: UsageProjection; kpis: UsageKpis; rangeDays: number; className?: string }) {12  const trend = projection.costTrendPct;13  const Icon = trend === null ? Minus : trend > 2 ? TrendingUp : trend < -2 ? TrendingDown : Minus;14  const tone = trend === null || Math.abs(trend) <= 2 ? "text-fg-muted" : trend > 0 ? "text-danger" : "text-success";15  const days = projection.observedDays;16  return (17    <section className={cn("panel flex flex-col p-4", className)} aria-label="Projected monthly cost">18      <header className="flex items-start justify-between gap-3">19        <div>20          <h3 className="text-[13px] font-semibold tracking-tight">Estimated monthly cost</h3>21          <p className="text-[11px] text-fg-subtle">If the last {days < 1 ? "hours" : `${Math.round(days)} day${Math.round(days) === 1 ? "" : "s"}`} continued for 30 days</p>22        </div>23        <Tooltip content="Daily average of the estimated cost over the observed part of this range, multiplied by 30. Based on public list prices; your provider invoice may differ.">24          <span className="text-fg-subtle">25            <Info className="size-3.5" />26          </span>27        </Tooltip>28      </header>29      <p className="mt-3 text-[32px] font-semibold leading-none tracking-tight">{formatUsd(projection.estimatedMonthlyCost)}</p>30      <dl className="mt-4 grid grid-cols-2 gap-3 text-xs">31        <div>32          <dt className="text-fg-subtle">Daily average</dt>33          <dd className="mt-0.5 font-medium">{formatUsd(projection.dailyAverageCost, { precise: projection.dailyAverageCost < 0.01 })}</dd>34        </div>35        <div>36          <dt className="text-fg-subtle">This period</dt>37          <dd className="mt-0.5 font-medium">{formatUsd(kpis.costUsd)}</dd>38        </div>39        <div className="col-span-2 flex items-center gap-1.5 border-t border-hairline pt-3">40          <Icon className={cn("size-3.5", tone)} />41          {projection.previous ? (42            <span className="text-fg-muted">43              {trend === null ? (44                <>No spend in the previous {rangeDays}-day period ({formatNumber(projection.previous.requests)} requests).</>45              ) : (46                <>47                  <span className={cn("font-medium", tone)}>48                    {trend > 0 ? "+" : ""}49                    {trend.toFixed(Math.abs(trend) >= 100 ? 0 : 1)}%50                  </span>{" "}51                  vs previous period ({formatUsd(projection.previous.costUsd)})52                </>53              )}54            </span>55          ) : (56            <span className="text-fg-muted">No previous period to compare against.</span>57          )}58        </div>59      </dl>60    </section>61  );62}6364/** Savings opportunities computed from real records + registry pricing (top 3). Each row deep-links to the model catalog. */65export function SavingsCard({ savings, className, loading }: { savings: SavingsOpportunity[]; className?: string; loading?: boolean }) {66  const total = savings.reduce((a, s) => a + s.savingsUsd, 0);67  return (68    <section className={cn("panel flex flex-col p-4", className)} aria-label="Savings opportunities">69      <header className="flex items-start justify-between gap-3">70        <div>71          <h3 className="text-[13px] font-semibold tracking-tight">Savings opportunities</h3>72          <p className="text-[11px] text-fg-subtle">Same tokens, cheaper sibling on the same provider</p>73        </div>74        <span className="flex size-8 items-center justify-center rounded-lg bg-success-soft text-success">75          <PiggyBank className="size-4" />76        </span>77      </header>78      {loading ? (79        <p className="mt-4 text-xs text-fg-subtle">Analysing…</p>80      ) : savings.length === 0 ? (81        <div className="mt-4 rounded-lg bg-bg-elevated/60 p-3 text-xs leading-5 text-fg-muted">82          Nothing to save right now. We look for a model on the same provider with the same capabilities that would have cost at least 40 % less for the traffic you actually sent — none qualifies for this period.83        </div>84      ) : (85        <>86          <p className="mt-3 text-[26px] font-semibold leading-none tracking-tight text-success">~{formatUsd(total)}</p>87          <p className="mt-1 text-xs text-fg-subtle">could have been saved in this period</p>88          <ul className="mt-4 space-y-3">89            {savings.map((s) => (90              <li key={`${s.fromModelKey}→${s.toModelKey}`} className="rounded-lg bg-bg-elevated/60 p-3">91                <p className="text-[13px] leading-5">92                  <span className="inline-flex items-center gap-1 align-middle">93                    <ProviderIcon provider={s.provider} size={13} />94                    <span className="font-medium">{s.fromLabel}</span>95                    <ArrowRight className="size-3 text-fg-subtle" />96                    <span className="font-medium">{s.toLabel}</span>97                  </span>98                </p>99                <p className="mt-1 text-xs text-fg-muted">100                  Switching eligible tasks could have saved <span className="font-medium text-success">~{formatUsd(s.savingsUsd)}</span> ({s.savingsPct.toFixed(0)} %) across {formatNumber(s.requests)} request{s.requests === 1 ? "" : "s"}.101                </p>102                <details className="mt-1.5 text-[11px] text-fg-subtle">103                  <summary className="tap cursor-pointer select-none hover:text-fg-muted">Why this sibling?</summary>104                  <p className="mt-1 leading-4">{s.rationale}</p>105                  <p className="mt-1">106                    {formatUsd(s.currentCostUsd)} at {s.fromLabel} list prices vs {formatUsd(s.alternativeCostUsd)} at {s.toLabel}.107                  </p>108                </details>109                <Link href={`/app/models?q=${encodeURIComponent(s.toLabel)}`} className="tap mt-1.5 inline-flex items-center gap-1 text-xs font-medium text-accent hover:underline">110                  Compare in the catalog <ArrowRight className="size-3" />111                </Link>112              </li>113            ))}114          </ul>115        </>116      )}117      <p className="mt-3 text-[11px] leading-4 text-fg-subtle">Estimates only — quality differs between tiers. Reasoning, vision, tools and structured output must all be supported by the sibling; context windows are checked against your average prompt.</p>118    </section>119  );120}121