Files
hanzo.ai/components/settings/Usage.tsx
zeekay fe79d984ad Expose composer and Settings usage and expand live public activity coverage
Testing: production export, typecheck, mobile/desktop composer and paid-Settings browser gates, historical activity/count unit tests and design lint pass. Risk: organization-scoped read-only usage, labelled estimates, public repository metadata only. How to revert: revert this commit and consume app 0.2.46.
2026-10-27 14:55:30 -07:00

101 lines
4.4 KiB
TypeScript

'use client'
/** Organization usage: plan windows, actual tokens, requests, spend and recent calls.
* Every plan can inspect usage; billing-policy changes remain administrator-only.
* Both reads carry the current IAM bearer and acting organization.
*/
import { useCallback } from 'react'
import { useIam } from '@hanzo/iam/react'
import { Button, Text, XStack, YStack } from '@hanzo/ui'
import { PlanUsage } from '@hanzo/ui/product/PlanUsage'
import { UsagePanel } from '@hanzo/usage/panel'
import { scope } from '@hanzo/app/lib/session'
import { api } from '@hanzo/app/lib/api'
import { payPage } from '@hanzo/app/lib/pay'
import { say } from '@hanzo/app/lib/org'
import { setCreditsAfterAllowance, useLimits } from '@hanzo/app/lib/limits'
import { ready, usePlans } from '@/lib/api/use'
import { useStanding } from './Frame'
import { Heading, Line, Section } from './kit'
export function Usage() {
const { org, tier, tierWrong, admin } = useStanding()
const { accessToken } = useIam()
const read = useLimits(Boolean(org), org)
const { limits } = read
// A plan's name is the plan authority's, read when the page opens (lib/api).
const catalog = ready(usePlans())
const name = (slug?: string | null) => (slug && catalog?.find((p) => p.id === slug)?.name) || null
// The panel reads as whoever holds the bearer; the org this browser works in
// rides beside it, the same headers every other read here carries.
const scoped = useCallback<typeof fetch>((input, init) => {
const headers = new Headers(init?.headers)
for (const [k, v] of Object.entries(scope())) headers.set(k, v)
if (org) headers.set('X-Org-Id', org)
return fetch(input, { ...init, headers })
}, [org])
if (!org) return null
// The plan is named once, by the usage policy that reads it, and the tier's own
// plan beside it: a tier's name is the CLASS of plan (every paid rung is Pro), so
// printing it here called a Max 20x org a Pro one under its own Max 20x heading.
const rung = tier?.tier
const named = name(limits?.plan) || name(tier?.plan) || rung?.displayName || rung?.name
return (
<>
<Heading title="Usage" lede="What this organization's plan includes, and what it has spent." />
{limits ? (
<Section title="Plan usage" ruled={false}>
<PlanUsage
limits={limits}
plan={name(limits.plan) || undefined}
notice={read.notice}
onCredits={admin ? (on) => setCreditsAfterAllowance(on).then(read.reload) : undefined}
creditsHref={payPage()}
addCreditsHref={payPage('/amount/card')}
/>
</Section>
) : null}
{!limits ? <Section title="Plan usage"><Line>{read.answered ? 'Plan usage is unavailable.' : 'Reading plan usage…'}</Line><Button type="button" size="sm" variant="outline" onClick={read.reload}>Refresh plan usage</Button></Section> : null}
<Section title="Plan" ruled={Boolean(limits)} lede={named ? `On the ${named} plan.` : undefined}>
{tierWrong ? <Line tone="bad">The plan could not be read: {say(tierWrong)}</Line> : null}
{!tier && !tierWrong ? <Line>Reading the plan…</Line> : null}
{tier ? (
<YStack>
<XStack items="center" gap="$3" py="$2.5" borderBottomWidth={1} borderColor="$edge">
<Text fontSize="$3" color="$ink" flex={1}>
Agents
</Text>
<Text fontSize="$3" color="$ink">
{tier.tier.unlimitedAgents ? 'No limit' : tier.tier.maxAgents.toLocaleString('en-US')}
</Text>
</XStack>
<XStack items="center" gap="$3" py="$2.5">
<Text fontSize="$3" color="$ink" flex={1}>
Models
</Text>
<Text fontSize="$3" color="$ink" numberOfLines={2} maxW="60%" text="end">
{(tier.tier.allowedModels ?? []).includes('*') ? 'Every model' : (tier.tier.allowedModels ?? []).join(', ') || 'None'}
</Text>
</XStack>
</YStack>
) : null}
</Section>
<Section title="Tokens, requests and spend" lede="Your actual usage, by date and model, including recent requests.">
{accessToken ? (
<UsagePanel key={org} sections={{ categories: true }} baseUrl={api()} org={org} token={accessToken} fetch={scoped} upgrade={payPage('/cart')} title="" />
) : (
<Line>Sign in to see token usage and spend.</Line>
)}
</Section>
</>
)
}