← Files Cache StatsARCHIVED FILE

skills/cache-stats/assets/runtime/ui/cache-card.html

12.3 KB · Oct 5, 2026 · 18:30 UTC

↓ Download file

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <style>
      :root {
        color-scheme: light dark;
        --accent: #10b981;
        --accent-strong: #059669;
        --accent-soft: rgba(16, 185, 129, 0.13);
        --surface: rgba(255, 255, 255, 0.94);
        --surface-raised: rgba(248, 250, 252, 0.92);
        --text: #10231d;
        --muted: #5f6f69;
        --border: rgba(16, 35, 29, 0.12);
        --track: rgba(16, 35, 29, 0.09);
        --shadow: 0 18px 50px rgba(15, 23, 42, 0.1);
      }

      @media (prefers-color-scheme: dark) {
        :root {
          --surface: rgba(18, 26, 23, 0.96);
          --surface-raised: rgba(28, 39, 35, 0.92);
          --text: #eefbf6;
          --muted: #a5b7b0;
          --border: rgba(226, 247, 239, 0.12);
          --track: rgba(226, 247, 239, 0.1);
          --shadow: 0 18px 50px rgba(0, 0, 0, 0.26);
        }
      }

      * { box-sizing: border-box; }

      body {
        margin: 0;
        padding: 2px;
        background: transparent;
        color: var(--text);
        font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
      }

      .card {
        position: relative;
        overflow: hidden;
        max-width: 720px;
        border: 1px solid var(--border);
        border-radius: 22px;
        padding: 22px;
        background:
          radial-gradient(circle at 100% 0%, var(--accent-soft), transparent 38%),
          var(--surface);
        box-shadow: var(--shadow);
      }

      .card::before {
        content: "";
        position: absolute;
        inset: 0 auto 0 0;
        width: 4px;
        background: linear-gradient(180deg, #34d399, var(--accent-strong));
      }

      .header {
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: 16px;
        margin-bottom: 22px;
      }

      .identity { display: flex; align-items: center; gap: 12px; min-width: 0; }

      .mark {
        display: grid;
        width: 40px;
        height: 40px;
        flex: 0 0 auto;
        place-items: center;
        border-radius: 13px;
        color: white;
        background: linear-gradient(145deg, #34d399, #059669);
        box-shadow: 0 9px 22px rgba(5, 150, 105, 0.24);
      }

      .mark svg { width: 23px; height: 23px; }
      .eyebrow { margin: 0 0 2px; color: var(--muted); font-size: 12px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; }
      h1 { margin: 0; font-size: 20px; line-height: 1.2; letter-spacing: -0.025em; }

      .status {
        display: inline-flex;
        align-items: center;
        gap: 7px;
        flex: 0 0 auto;
        border: 1px solid rgba(16, 185, 129, 0.22);
        border-radius: 999px;
        padding: 7px 10px;
        color: var(--accent-strong);
        background: var(--accent-soft);
        font-size: 12px;
        font-weight: 750;
      }

      .status-dot { width: 7px; height: 7px; border-radius: 50%; background: currentColor; box-shadow: 0 0 0 4px rgba(16, 185, 129, 0.12); }

      .hero {
        display: grid;
        grid-template-columns: 132px minmax(0, 1fr);
        align-items: center;
        gap: 24px;
        margin-bottom: 20px;
      }

      .ring {
        --pct: 0;
        position: relative;
        width: 124px;
        height: 124px;
        border-radius: 50%;
        background: conic-gradient(var(--accent) calc(var(--pct) * 1%), var(--track) 0);
      }

      .ring::after {
        content: "";
        position: absolute;
        inset: 12px;
        border: 1px solid var(--border);
        border-radius: inherit;
        background: var(--surface);
      }

      .ring-copy {
        position: absolute;
        z-index: 1;
        inset: 0;
        display: grid;
        place-content: center;
        text-align: center;
      }

      .percent { font-size: 28px; font-weight: 800; line-height: 1; letter-spacing: -0.055em; }
      .percent-label { margin-top: 5px; color: var(--muted); font-size: 11px; font-weight: 650; }
      .summary { margin: 0 0 14px; font-size: 17px; font-weight: 650; line-height: 1.45; letter-spacing: -0.015em; }

      .bar { overflow: hidden; height: 8px; border-radius: 999px; background: var(--track); }
      .bar-fill { width: 0; height: 100%; border-radius: inherit; background: linear-gradient(90deg, #34d399, #059669); transition: width 520ms cubic-bezier(.2,.8,.2,1); }
      .bar-legend { display: flex; justify-content: space-between; gap: 12px; margin-top: 8px; color: var(--muted); font-size: 11px; }

      .metrics { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; }
      .metric { min-width: 0; border: 1px solid var(--border); border-radius: 15px; padding: 13px; background: var(--surface-raised); }
      .metric-label { margin-bottom: 7px; color: var(--muted); font-size: 11px; font-weight: 700; letter-spacing: 0.025em; text-transform: uppercase; }
      .metric-value { overflow: hidden; font-size: 19px; font-weight: 780; letter-spacing: -0.035em; text-overflow: ellipsis; white-space: nowrap; }
      .metric.cached .metric-value { color: var(--accent-strong); }
      .metric.write .metric-value { color: #2563eb; }

      .footer { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-top: 16px; padding-top: 15px; border-top: 1px solid var(--border); }
      .context { overflow: hidden; color: var(--muted); font-size: 12px; text-overflow: ellipsis; white-space: nowrap; }
      .note { margin: 0; max-width: 290px; color: var(--muted); font-size: 11px; line-height: 1.45; text-align: right; }

      .loading { color: var(--muted); }
      .error .status { color: #dc2626; border-color: rgba(220, 38, 38, 0.2); background: rgba(220, 38, 38, 0.08); }
      .error .status-dot { box-shadow: 0 0 0 4px rgba(220, 38, 38, 0.1); }

      @media (max-width: 520px) {
        .card { border-radius: 18px; padding: 18px; }
        .header { align-items: flex-start; }
        .hero { grid-template-columns: 1fr; justify-items: center; text-align: center; }
        .hero-copy { width: 100%; }
        .metrics { grid-template-columns: 1fr; }
        .footer { align-items: flex-start; flex-direction: column; }
        .note { max-width: none; text-align: left; }
      }
    </style>
  </head>
  <body>
    <main class="card loading" id="card" aria-live="polite">
      <header class="header">
        <div class="identity">
          <div class="mark" aria-hidden="true">
            <svg viewBox="0 0 24 24" fill="none">
              <path d="M4 15a8 8 0 1 1 16 0" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
              <path d="m12 15 4-5" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
              <circle cx="12" cy="15" r="2" fill="currentColor" />
            </svg>
          </div>
          <div>
            <p class="eyebrow">Previous response</p>
            <h1>Prompt cache</h1>
          </div>
        </div>
        <div class="status"><span class="status-dot"></span><span id="statusText">Reading usage</span></div>
      </header>

      <section class="hero">
        <div class="ring" id="ring" role="meter" aria-label="Cached input percentage" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0">
          <div class="ring-copy"><span class="percent" id="percent">—</span><span class="percent-label">input reused</span></div>
        </div>
        <div class="hero-copy">
          <p class="summary" id="summary">Reading the most recent completed response…</p>
          <div class="bar" aria-hidden="true"><div class="bar-fill" id="barFill"></div></div>
          <div class="bar-legend"><span>Cached input</span><span>Non-cached input</span></div>
        </div>
      </section>

      <section class="metrics" aria-label="Token counts">
        <article class="metric cached"><div class="metric-label">Cached input</div><div class="metric-value" id="cached">—</div></article>
        <article class="metric write"><div class="metric-label">Cache write</div><div class="metric-value" id="cacheWrite">—</div></article>
        <article class="metric"><div class="metric-label">Non-cached input</div><div class="metric-value" id="fresh">—</div></article>
        <article class="metric"><div class="metric-label">Generated output</div><div class="metric-value" id="output">—</div></article>
      </section>

      <footer class="footer">
        <div class="context" id="context">Waiting for usage data</div>
        <p class="note">Cache writes are included in non-cached input. Reuse percentage measures tokens, not cost savings. Output is newly generated.</p>
      </footer>
    </main>

    <script>
      const card = document.getElementById("card");
      const formatter = new Intl.NumberFormat();

      function setText(id, value) {
        document.getElementById(id).textContent = String(value);
      }

      function reasoningLabel(value) {
        const labels = {
          none: "None",
          low: "Low",
          medium: "Medium",
          high: "High",
          xhigh: "Extra High (xhigh)",
          max: "Max",
        };
        const key = String(value || "").toLowerCase();
        return labels[key] || (value ? String(value) : "Unspecified reasoning");
      }

      function render(payload) {
        const stats = payload && payload.structuredContent ? payload.structuredContent : payload;
        if (stats && stats.error) {
          card.classList.remove("loading");
          card.classList.add("error");
          document.getElementById("ring").style.setProperty("--pct", "0");
          document.getElementById("barFill").style.width = "0%";
          setText("statusText", "Unable to verify task");
          setText("percent", "—");
          setText("summary", String(stats.error));
          setText("cached", "—");
          setText("cacheWrite", "—");
          setText("fresh", "—");
          setText("output", "—");
          setText("context", "No cross-task usage was shown");
          return;
        }
        if (!stats || typeof stats.cachePercent !== "number") return;

        const percent = Math.max(0, Math.min(100, stats.cachePercent));
        const hit = Boolean(stats.cacheHit);
        const calls = Number(stats.modelCalls || 0);
        const ring = document.getElementById("ring");

        card.classList.remove("loading", "error");
        ring.style.setProperty("--pct", String(percent));
        ring.setAttribute("aria-valuenow", String(percent));
        document.getElementById("barFill").style.width = percent + "%";

        setText("statusText", hit ? "Cache hit" : "No cached input");
        setText("percent", percent.toFixed(1) + "%");
        setText(
          "summary",
          hit
            ? formatter.format(stats.cachedInputTokens) + " input tokens were reused from cache."
            : "No input tokens were reused from cache.",
        );
        setText("cached", formatter.format(stats.cachedInputTokens));
        setText("cacheWrite", formatter.format(stats.cacheWriteInputTokens || 0));
        setText("fresh", formatter.format(stats.freshInputTokens));
        setText("output", formatter.format(stats.outputTokens));
        const context = document.getElementById("context");
        const model = String(stats.model || "Unknown model");
        const effort = reasoningLabel(stats.reasoningEffort);
        const taskStatus = stats.taskId ? "Task verified" : "Task not verified";
        context.textContent =
          model +
          " · " +
          effort +
          " · " +
          calls +
          " model call" +
          (calls === 1 ? "" : "s") +
          " · " +
          taskStatus;
        context.title =
          model +
          " · reasoning " +
          String(stats.reasoningEffort || "unspecified") +
          " · task " +
          String(stats.taskId || "unverified");
      }

      const initialOutput = window.openai && window.openai.toolOutput;
      if (initialOutput) render(initialOutput);

      window.addEventListener(
        "message",
        (event) => {
          if (event.source !== window.parent) return;
          const message = event.data;
          if (!message || message.jsonrpc !== "2.0") return;
          if (message.method === "ui/notifications/tool-result") {
            render(message.params && message.params.structuredContent);
          }
        },
        { passive: true },
      );
    </script>
  </body>
</html>

SHA-256: 926545faa56d7100eed302f37adbcf52465882f5e5f9046f6a8ed3b18a5042b2