feat(freebuff): enhance Freebucks handling and pricing logic across services
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { formatResetTime, getRemainingPercentage } from "./utils";
|
||||
import { formatFreebucksPrice, formatResetTime, getRemainingPercentage } from "./utils";
|
||||
|
||||
const PAGE_SIZE = 10;
|
||||
|
||||
@@ -168,9 +168,20 @@ export default function QuotaTable({
|
||||
{/* Name */}
|
||||
<div className="flex w-36 min-w-0 items-center gap-1.5">
|
||||
<span className="text-[10px] shrink-0">{colors.emoji}</span>
|
||||
<span className={`${nameText} font-medium text-text-primary truncate`}>
|
||||
{quota.name}
|
||||
</span>
|
||||
<div className="min-w-0">
|
||||
<div className={`${nameText} font-medium text-text-primary truncate`}>
|
||||
{quota.name}
|
||||
</div>
|
||||
{quota.price !== undefined && (
|
||||
<div
|
||||
className="text-[9px] leading-tight text-text-muted truncate"
|
||||
title={quota.priceNote || ""}
|
||||
>
|
||||
{formatFreebucksPrice(quota.price)}
|
||||
{quota.priceNote ? ` · ${quota.priceNote}` : ""}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Progress + used/total */}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
parseQuotaData,
|
||||
calculatePercentage,
|
||||
filterQuotasByVisibility,
|
||||
formatFreebucksHeader,
|
||||
getHiddenQuotaRows,
|
||||
getQuotaVisibilityKey,
|
||||
getConnectionLabel,
|
||||
@@ -1237,6 +1238,11 @@ export default function ProviderLimits() {
|
||||
</div>
|
||||
|
||||
<div className="px-2 py-1.5">
|
||||
{quota?.raw?.freebucks && !error && !isLoading && (
|
||||
<div className="mb-1.5 rounded-md bg-black/[0.03] px-2 py-1.5 text-[10px] leading-relaxed text-text-muted dark:bg-white/[0.03]">
|
||||
{formatFreebucksHeader(quota.raw.freebucks)}
|
||||
</div>
|
||||
)}
|
||||
{isLoading ? (
|
||||
<div className="text-center py-5 text-text-muted">
|
||||
<span className="material-symbols-outlined text-[28px] animate-spin">
|
||||
|
||||
@@ -258,6 +258,49 @@ export function formatResetTime(date) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Freebucks account header line — mirrors upstream freebucksHeaderLine:
|
||||
* "10/25 Freebucks daily · resets in 4h 12m · 20 in wallet · $6.50 monthly usage left".
|
||||
* Wallet shown only when non-empty; monthly only when the server sent it
|
||||
* (older servers omit it — showing $0 would read as "nothing left").
|
||||
* @param {{balance: number|null, daily: {limit:number,spent:number,remaining:number,resetAt:string|null}, wallet:{balance:number}, monthly?:{remainingUsd:number,limitUsd:number|null,resetAt:string|null}}} freebucks
|
||||
* @returns {string|null}
|
||||
*/
|
||||
export function formatFreebucksHeader(freebucks) {
|
||||
if (!freebucks?.daily) return null;
|
||||
const parts = [
|
||||
`${Math.max(0, Math.round(freebucks.daily.remaining))}/${Math.max(0, Math.round(freebucks.daily.limit))} Freebucks daily`,
|
||||
];
|
||||
const countdown = formatResetTime(freebucks.daily.resetAt);
|
||||
if (countdown !== "-") parts.push(`resets in ${countdown}`);
|
||||
if (Number(freebucks.wallet?.balance) > 0) {
|
||||
parts.push(`${Math.max(0, Math.round(freebucks.wallet.balance))} in wallet`);
|
||||
}
|
||||
if (freebucks.monthly && Number.isFinite(Number(freebucks.monthly.remainingUsd))) {
|
||||
parts.push(`${formatFreebucksUsd(freebucks.monthly.remainingUsd)} monthly usage left`);
|
||||
}
|
||||
return parts.join(" · ");
|
||||
}
|
||||
|
||||
/**
|
||||
* "$25", "$4.20", "$0" — whole dollars until the figure is small enough that
|
||||
* the cents are the story. Mirrors upstream formatAllowanceUsd.
|
||||
*/
|
||||
export function formatFreebucksUsd(usd) {
|
||||
const safe = Math.max(0, Number(usd));
|
||||
if (safe >= 10) return `$${Math.round(safe)}`;
|
||||
if (safe >= 1) return `$${safe.toFixed(1).replace(/\.0$/, "")}`;
|
||||
return `$${safe.toFixed(2)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* "15 Freebucks/hr" — a bare number would read as dollars; the unit is the
|
||||
* hour, not the message.
|
||||
*/
|
||||
export function formatFreebucksPrice(price) {
|
||||
return `${Math.max(0, Math.round(Number(price) || 0))} Freebucks/hr`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Tailwind color class based on percentage
|
||||
* @param {number} percentage - Remaining percentage (0-100)
|
||||
@@ -545,6 +588,8 @@ export function parseQuotaData(provider, data) {
|
||||
case "freebuff":
|
||||
// Session quotas keyed by model id — label rows with the friendly
|
||||
// displayName (from the registry) and keep modelKey for ordering.
|
||||
// Metered rows carry the live Freebucks price (price) + promo tagline
|
||||
// (priceNote), both server-authoritative.
|
||||
if (data.quotas) {
|
||||
Object.entries(data.quotas).forEach(([modelKey, quota]) => {
|
||||
normalizedQuotas.push({
|
||||
@@ -554,6 +599,8 @@ export function parseQuotaData(provider, data) {
|
||||
total: quota.total || 0,
|
||||
resetAt: quota.resetAt || null,
|
||||
recurring: quota.recurring !== false,
|
||||
price: quota.price,
|
||||
priceNote: quota.priceNote,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -287,9 +287,15 @@ export async function markAccountUnavailable(connectionId, status, errorText, pr
|
||||
} else if (resetsAtMs && resetsAtMs > Date.now()) {
|
||||
shouldFallback = true;
|
||||
// Antigravity quota API provides exact per-model resetAt. Do not truncate it.
|
||||
cooldownMs = resolveProviderId(provider) === "antigravity"
|
||||
// Freebucks exhaustion is likewise a hard stop until the daily Pacific
|
||||
// reset (up to ~24h) — skip the account for the day rather than re-poke it
|
||||
// every 30 min; guard at 26h so a bad server value can't lock forever.
|
||||
const providerId = resolveProviderId(provider);
|
||||
cooldownMs = providerId === "antigravity"
|
||||
? resetsAtMs - Date.now()
|
||||
: Math.min(resetsAtMs - Date.now(), MAX_RATE_LIMIT_COOLDOWN_MS);
|
||||
: providerId === "freebuff"
|
||||
? Math.min(resetsAtMs - Date.now(), 26 * 60 * 60 * 1000)
|
||||
: Math.min(resetsAtMs - Date.now(), MAX_RATE_LIMIT_COOLDOWN_MS);
|
||||
newBackoffLevel = 0;
|
||||
} else {
|
||||
({ shouldFallback, cooldownMs, newBackoffLevel } = checkFallbackError(status, errorText, backoffLevel));
|
||||
|
||||
Reference in New Issue
Block a user