import React, { useEffect, useState } from "react"; import { apiKeyFromAuth, readAuthForModel, useConfig } from "../config.ts"; import { useModel, useAppStore } from "../state.ts"; import type { QuotaData, QuotaEntry, WeeklyEntry } from "../utils/quota.ts"; import { parseQuotaJson } from "../utils/quota.ts"; import { formatTimeUntil } from "../time.ts"; import { Span } from "paintcannon-react"; import { TerminalFlex } from "./terminal-flex.tsx"; async function fetchQuota(apiKey: string): Promise { try { const response = await fetch("https://api.synthetic.new/v2/quotas", { headers: { Authorization: `Bearer ${apiKey}`, }, }); if (!response.ok) return null; return parseQuotaJson(await response.text()) ?? null; } catch { return null; } } type QuotaRowProps = { label: string; entry: QuotaEntry; }; function formatQuotaNumber(value: number): string { if (Number.isInteger(value)) return value.toString(); return Math.floor(value * 10) / 10 + ""; } function QuotaRow({ label, entry }: QuotaRowProps) { const tickPercent = formatQuotaNumber(entry.tickPercent * 100); const showNextRegen = entry.remaining < entry.max; return ( {`${label}: ${formatQuotaNumber(entry.remaining)} / ${formatQuotaNumber(entry.max)} remaining`} {showNextRegen ? ( {`Next regen: ${tickPercent}% ${formatTimeUntil(entry.nextTickAt)}`} ) : null} ); } type WeeklyQuotaRowProps = { label: string; entry: WeeklyEntry; }; function WeeklyQuotaRow({ label, entry }: WeeklyQuotaRowProps) { const showNextRegen = entry.remainingCredits !== entry.maxCredits; return ( {`${label}: ${entry.remainingCredits} / ${entry.maxCredits} remaining`} {showNextRegen ? ( {`Next regen: ${entry.nextRegenCredits} ${formatTimeUntil(entry.nextRegenAt)}`} ) : null} ); } export const MenuQuotaIndicator = () => { const config = useConfig(); const model = useModel(); const storeQuota = useAppStore(state => state.quotaData); const [fetchedQuota, setFetchedQuota] = useState(null); // should only be used if menu is opened before the agent runs // otherwise, quota should come from the store, read from header values in the compiler useEffect(() => { if (storeQuota) return; let cancelled = false; readAuthForModel(model, config) .then(auth => { if (!auth.ok || auth.auth.type !== "apiKey") return null; return fetchQuota(apiKeyFromAuth(auth.auth)); }) .then(data => { if (!cancelled) setFetchedQuota(prev => prev ?? data); }) .catch(() => { /* ignore errors, they're out-of-place in the menu */ }); return () => { cancelled = true; }; }, [storeQuota, model, config]); const quota = storeQuota ?? fetchedQuota; if (!quota) return null; if (!quota.weeklyTokenLimit && !quota.rollingFiveHourLimit) return null; return ( Synthetic Quota {quota.weeklyTokenLimit ? ( ) : null} {quota.rollingFiveHourLimit ? ( ) : null} ); };