import React, { useEffect, useRef, useState } from "react"; import { Alert, Box, Button, LinearProgress, Skeleton, Typography } from "@mui/material"; import { PageSection } from "../layout/PageLayout"; import { api } from "../api"; import { useI18n } from "../i18n/I18nProvider"; import { checkSessionCookie, getSessionContext, isSessionInvalidated } from "../sessionBoundary"; type Usage = { currentMonth: { calls: number; estimatedTokens: number }; plan: string; monthlyCallLimit: number; monthlyTokenLimit: number; storageUsedBytes: number; storageLimitBytes: number; }; type BillingStatus = { enabled: boolean; canCheckout: boolean; canManage: boolean }; function formatBytes(bytes: number, locale: string) { if (bytes < 1_000_000) return `${Math.round(bytes / 1_000).toLocaleString(locale)} KB`; const options = { minimumFractionDigits: 1, maximumFractionDigits: 1 }; if (bytes < 1_000_000_000) return `${(bytes / 1_000_000).toLocaleString(locale, options)} MB`; return `${(bytes / 1_000_000_000).toLocaleString(locale, options)} GB`; } export default function AiUsageCard() { const { t, language } = useI18n(); const locale = language === "nb" ? "nb-NO" : "en"; const [usage, setUsage] = useState(null); const [billing, setBilling] = useState(null); const [billingBusy, setBillingBusy] = useState(false); const [billingFailed, setBillingFailed] = useState(false); const [failed, setFailed] = useState(false); const [billingReadFailed, setBillingReadFailed] = useState(false); const [usageAttempt, setUsageAttempt] = useState(0); const [billingAttempt, setBillingAttempt] = useState(0); const owner = useRef({ mounted: false, generation: 0, billingBusy: false }); const body = useRef(null); useEffect(() => { const lifetime = owner.current; lifetime.mounted = true; lifetime.generation += 1; return () => { lifetime.mounted = false; lifetime.generation += 1; }; }, []); useEffect(() => { let active = true; const context = getSessionContext(); const current = () => active && !isSessionInvalidated() && getSessionContext() === context; api.get("/ai/usage") .then((response) => { if (current()) setUsage(response.data); }) .catch(() => { if (current()) setFailed(true); }); return () => { active = false; }; }, [usageAttempt]); useEffect(() => { let active = true; const context = getSessionContext(); const current = () => active && !isSessionInvalidated() && getSessionContext() === context; api.get("/billing/status") .then((response) => { if (current()) setBilling(response.data); }) .catch(() => { if (current()) setBillingReadFailed(true); }); return () => { active = false; }; }, [billingAttempt]); const openBilling = async (path: "checkout" | "portal") => { checkSessionCookie(); const lifetime = owner.current; if (!lifetime.mounted || lifetime.billingBusy || isSessionInvalidated()) return; const generation = lifetime.generation; const context = getSessionContext(); const active = () => lifetime.mounted && lifetime.generation === generation && !isSessionInvalidated() && getSessionContext() === context; lifetime.billingBusy = true; setBillingBusy(true); setBillingFailed(false); try { const response = await api.post<{ url: string }>(`/billing/${path}`); checkSessionCookie(); if (!active()) return; window.location.assign(response.data.url); } catch { checkSessionCookie(); if (!active()) return; setBillingFailed(true); setBillingBusy(false); lifetime.billingBusy = false; } }; const callsPercent = usage && usage.monthlyCallLimit > 0 ? Math.min(100, usage.currentMonth.calls / usage.monthlyCallLimit * 100) : 0; const tokensPercent = usage && usage.monthlyTokenLimit > 0 ? Math.min(100, usage.currentMonth.estimatedTokens / usage.monthlyTokenLimit * 100) : 0; const storagePercent = usage && usage.storageLimitBytes > 0 ? Math.min(100, usage.storageUsedBytes / usage.storageLimitBytes * 100) : 0; return ( {t("settingsUsagePlan", { plan: usage.plan })} : undefined}> {failed ? { if (body.current?.contains(document.activeElement)) body.current.focus(); setFailed(false); setUsageAttempt(value => value + 1); }}>{t("retry")}}>{t("settingsUsageUnavailable")} : !usage ? : null} {usage ? <> {usage.monthlyCallLimit === 0 ? {t("settingsUsageNoAi")} : <> {t("settingsUsageGenerations", { used: usage.currentMonth.calls.toLocaleString(locale), limit: usage.monthlyCallLimit.toLocaleString(locale) })} {t("settingsUsageTokens", { used: usage.currentMonth.estimatedTokens.toLocaleString(locale), limit: usage.monthlyTokenLimit.toLocaleString(locale) })} } {t("settingsUsageStorage", { used: formatBytes(usage.storageUsedBytes, locale), limit: formatBytes(usage.storageLimitBytes, locale) })} {t("settingsUsageReset")} : null} {billingReadFailed ? { if (body.current?.contains(document.activeElement)) body.current.focus(); setBillingReadFailed(false); setBillingAttempt(value => value + 1); }}>{t("retry")}}>{t("settingsBillingUnavailable")} : !billing ? : null} {billing?.canCheckout ? : null} {billing?.canManage ? : null} {billingFailed ? {t("settingsBillingUnavailable")} : null} {usage?.plan === "free" && billing?.enabled === false ? ( {t("settingsBillingNotConfigured")} ) : null} ); }