mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-29 04:20:39 +00:00
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.
101 lines
4.4 KiB
TypeScript
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>
|
|
</>
|
|
)
|
|
}
|