@hanzo/build 0.2.52 → 0.2.53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +103 -49
- package/lib/api/billing.d.ts +39 -0
- package/lib/api/billing.js +41 -0
- package/lib/api/coding.d.ts +4 -1
- package/lib/api/coding.js +2 -0
- package/lib/api/environment.d.ts +12 -0
- package/lib/api/environment.js +39 -1
- package/lib/api/github.d.ts +24 -4
- package/lib/api/github.js +60 -16
- package/lib/api/limits.d.ts +69 -0
- package/lib/api/limits.js +86 -0
- package/lib/api/links.d.ts +42 -0
- package/lib/api/links.js +65 -0
- package/lib/api/pref.d.ts +5 -3
- package/lib/api/pref.js +0 -3
- package/lib/api/turn.d.ts +10 -1
- package/lib/api/turn.js +16 -4
- package/lib/api/work.d.ts +75 -38
- package/lib/api/work.js +132 -44
- package/lib/builder.d.ts +0 -10
- package/lib/builder.js +12 -52
- package/lib/choice.d.ts +21 -0
- package/lib/choice.js +40 -1
- package/lib/customize/agents.d.ts +1 -1
- package/lib/customize/agents.js +21 -19
- package/lib/customize/connectors.d.ts +1 -1
- package/lib/customize/connectors.js +80 -31
- package/lib/customize/index.d.ts +2 -0
- package/lib/customize/index.js +23 -19
- package/lib/customize/load.d.ts +9 -0
- package/lib/customize/load.js +22 -0
- package/lib/customize/plugins.d.ts +1 -1
- package/lib/customize/plugins.js +21 -19
- package/lib/customize/say.d.ts +1 -0
- package/lib/customize/say.js +38 -0
- package/lib/customize/skills.d.ts +1 -1
- package/lib/customize/skills.js +23 -17
- package/lib/customize/ui.d.ts +28 -11
- package/lib/customize/ui.js +29 -21
- package/lib/data.d.ts +13 -0
- package/lib/data.js +70 -0
- package/lib/desk.d.ts +6 -1
- package/lib/desk.js +43 -19
- package/lib/environment.d.ts +11 -3
- package/lib/environment.js +53 -13
- package/lib/foot.js +10 -6
- package/lib/forge.js +139 -71
- package/lib/index.d.ts +28 -2
- package/lib/index.js +26 -2
- package/lib/landing.d.ts +0 -10
- package/lib/landing.js +70 -38
- package/lib/merge.d.ts +223 -0
- package/lib/merge.js +380 -0
- package/lib/meter.d.ts +31 -0
- package/lib/meter.js +45 -0
- package/lib/mind.d.ts +60 -0
- package/lib/mind.js +187 -0
- package/lib/nav.d.ts +11 -0
- package/lib/nav.js +75 -0
- package/lib/panel.d.ts +45 -0
- package/lib/panel.js +115 -0
- package/lib/pick.d.ts +8 -0
- package/lib/pick.js +18 -1
- package/lib/plan.d.ts +78 -0
- package/lib/plan.js +133 -0
- package/lib/project.js +16 -6
- package/lib/prompt.d.ts +75 -0
- package/lib/prompt.js +125 -0
- package/lib/prose.js +14 -10
- package/lib/reply.d.ts +36 -0
- package/lib/reply.js +124 -0
- package/lib/route.d.ts +16 -0
- package/lib/route.js +40 -0
- package/lib/run.js +87 -52
- package/lib/section.d.ts +1 -1
- package/lib/section.js +7 -11
- package/lib/settings/billing.js +16 -9
- package/lib/settings/code.js +15 -12
- package/lib/settings/credits.d.ts +9 -0
- package/lib/settings/credits.js +110 -0
- package/lib/settings/plan.d.ts +17 -0
- package/lib/settings/plan.js +108 -0
- package/lib/settings/usage.js +11 -119
- package/lib/shelf.js +3 -3
- package/lib/standing.d.ts +44 -0
- package/lib/standing.js +64 -0
- package/lib/sync.js +22 -8
- package/lib/tabs.d.ts +53 -0
- package/lib/tabs.js +197 -0
- package/lib/tools.d.ts +2 -9
- package/lib/tools.js +20 -25
- package/lib/transcript.d.ts +10 -1
- package/lib/transcript.js +25 -10
- package/lib/voice.d.ts +2 -13
- package/lib/voice.js +7 -73
- package/package.json +20 -3
package/lib/settings/code.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
|
-
* Code: where and how a new run starts — the
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* Code: where and how a new run starts — the mode and the place, saved to the
|
|
4
|
+
* person's own settings for anyone whose New has no choice of its own kept yet
|
|
5
|
+
* — and the model and effort, which are the one choice Chat and Dev share
|
|
6
|
+
* (mind.tsx): changed here, changed in both composers. Beside them, the
|
|
7
|
+
* codebases' environments and the organization's machines, each on its own page.
|
|
7
8
|
*/
|
|
8
9
|
import { YStack } from '@hanzo/gui';
|
|
9
10
|
import { ChevronRight } from '@hanzogui/lucide-icons-2';
|
|
@@ -11,11 +12,12 @@ import { ChipSelect } from '@hanzo/ui/product';
|
|
|
11
12
|
import { ModelPicker } from '@hanzo/ui/models';
|
|
12
13
|
import { useState } from 'react';
|
|
13
14
|
import { unhonoured } from '../api/coding.js';
|
|
14
|
-
import { ENSO
|
|
15
|
+
import { ENSO } from '../api/models.js';
|
|
15
16
|
import { SANDBOX } from '../api/places.js';
|
|
16
|
-
import { usePlaces
|
|
17
|
+
import { usePlaces } from '../data.js';
|
|
17
18
|
import { useHost, useTarget } from '../host.js';
|
|
18
|
-
import {
|
|
19
|
+
import { MODES } from '../landing.js';
|
|
20
|
+
import { EFFORTS, useCatalog, useChoice } from '../mind.js';
|
|
19
21
|
import { usePrefs } from '../prefs.js';
|
|
20
22
|
import { path } from '../route.js';
|
|
21
23
|
import { Card, Group, Heading, Note, Row, Soft } from './ui.js';
|
|
@@ -24,13 +26,14 @@ export function Code() {
|
|
|
24
26
|
const t = useTarget();
|
|
25
27
|
const signed = Boolean(host.person);
|
|
26
28
|
const { prefs, save } = usePrefs();
|
|
27
|
-
const catalog =
|
|
29
|
+
const catalog = useCatalog(t);
|
|
30
|
+
const [choice, choose] = useChoice();
|
|
28
31
|
const places = usePlaces(t, signed);
|
|
29
32
|
const [note, setNote] = useState('');
|
|
30
33
|
if (!signed) {
|
|
31
34
|
return (_jsxs(YStack, { gap: "$5", children: [_jsx(Heading, { title: "Code" }), _jsx(Soft, { children: "Sign in to set how your runs start." })] }));
|
|
32
35
|
}
|
|
33
|
-
const now = {
|
|
36
|
+
const now = { mode: 'build', place: '', ...prefs.code };
|
|
34
37
|
const keep = (change) => {
|
|
35
38
|
setNote('');
|
|
36
39
|
save({ code: { ...prefs.code, ...change } }).catch((e) => setNote(e.message));
|
|
@@ -45,11 +48,11 @@ export function Code() {
|
|
|
45
48
|
// The sandbox is always listed, so a place not found is a machine that is not: it reads as its id.
|
|
46
49
|
const place = where.find((w) => w.place === now.place) ?? { id: now.place, label: now.place, place: now.place };
|
|
47
50
|
// A default is never premium: a premium model is picked for one run, in New.
|
|
48
|
-
const offered = catalog.
|
|
51
|
+
const offered = catalog.models.filter((m) => m.class !== 'premium');
|
|
49
52
|
const go = (section) => host.go(path({ kind: 'settings', section }));
|
|
50
|
-
const effort = EFFORTS.find((e) => e.id ===
|
|
53
|
+
const effort = EFFORTS.find((e) => e.id === choice.effort);
|
|
51
54
|
const mode = MODES.find((m) => m.id === now.mode);
|
|
52
|
-
return (_jsxs(YStack, { gap: "$6", children: [_jsx(Heading, { title: "Code", detail: "How a new run starts.
|
|
55
|
+
return (_jsxs(YStack, { gap: "$6", children: [_jsx(Heading, { title: "Code", detail: "How a new run starts. The model and effort are the ones Chat uses too." }), _jsxs(Group, { title: "Defaults", children: [_jsxs(Card, { children: [_jsx(Row, { first: true, title: "Model", detail: "Enso picks one for each step; any other model runs them all.", trailing: _jsx(ModelPicker, { size: "sm", name: "Default model", models: offered, scope: "chat", value: choice.model || ENSO, onChange: (id) => choose({ model: id }), loading: catalog.loading, error: catalog.error }) }), _jsx(Row, { title: "Effort", detail: "How long the agent thinks before it acts.", trailing: _jsx(ChipSelect, { name: "Default effort", label: effort.label, chosen: effort, items: EFFORTS, onChange: (e) => choose({ effort: e.id }), placement: "bottom-end", width: 180 }) }), _jsx(Row, { title: "Mode", detail: mode.hint, trailing: _jsx(ChipSelect, { name: "Default mode", label: mode.label, chosen: mode, items: MODES, onChange: (m) => keep({ mode: m.id }), placement: "bottom-end", width: 260 }) }), _jsx(Row, { title: "Where it runs", detail: "The Hanzo sandbox, or one of your organization\u2019s machines.", trailing: _jsx(ChipSelect, { name: "Default place", label: place.label, chosen: place, items: where, onChange: (w) => keep({ place: w.place }), placeholder: "Search machines\u2026", placement: "bottom-end", loading: places.loading, error: places.error?.message ?? null }) })] }), _jsx(Note, { children: unhonoured(now.mode, now.place) })] }), _jsx(Group, { title: "Workspace", children: _jsxs(Card, { children: [_jsx(Row, { first: true, title: "Environments", detail: "What a run installs and starts in each codebase, and the secrets it exports.", trailing: _jsx(Link, { label: "Environments", onPress: () => go('environments') }) }), _jsx(Row, { title: "Machines", detail: "Your organization\u2019s own computers that take runs.", trailing: _jsx(Link, { label: "Machines", onPress: () => go('machines') }) })] }) }), _jsx(Note, { children: note })] }));
|
|
53
56
|
}
|
|
54
57
|
function Link({ label, onPress }) {
|
|
55
58
|
return (_jsx(YStack, { render: "button", "aria-label": `Open ${label}`, p: "$1", rounded: "$2", hoverStyle: { bg: '$hover' }, onPress: onPress, children: _jsx(ChevronRight, { size: 16 }) }));
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Target } from '../api/call.ts';
|
|
2
|
+
/** What the credits section says first, on every surface. */
|
|
3
|
+
export declare const SEPARATE = "Credits are separate from your plan\u2019s usage allowance. They pay for usage past it when you choose to continue with credits, and for all usage when there is no plan.";
|
|
4
|
+
export declare function Credits({ target: t, admin, add, onCard }: {
|
|
5
|
+
target: Target;
|
|
6
|
+
admin: boolean;
|
|
7
|
+
add?: string;
|
|
8
|
+
onCard?: () => void;
|
|
9
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Credits, as the one view every Billing page draws: Dev's Settings, the
|
|
4
|
+
* rooms' Settings and hanzo.ai's organization settings.
|
|
5
|
+
*
|
|
6
|
+
* CREDITS ARE NOT THE PLAN. A plan includes usage, measured in shares on Usage;
|
|
7
|
+
* credits are money — the prepaid balance bought with a card and the credit
|
|
8
|
+
* Hanzo granted — and they pay for usage past the plan when the payer chose to
|
|
9
|
+
* continue with credits, and for everything when there is no plan. So the
|
|
10
|
+
* balance, the grants, top-ups, auto-reload, this month's spend and the monthly
|
|
11
|
+
* limit on it live here, and nowhere a plan is named.
|
|
12
|
+
*
|
|
13
|
+
* Money moves two ways, the host's choice: `add` names a page that takes it (the
|
|
14
|
+
* pay site); without one a saved card is charged here (POST /v1/billing/topup)
|
|
15
|
+
* and a person with no card is sent to add one (`onCard`).
|
|
16
|
+
*/
|
|
17
|
+
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
18
|
+
import { Button, Input, Switch } from '@hanzo/ui';
|
|
19
|
+
import { useEffect, useState } from 'react';
|
|
20
|
+
import { arm, balance, caps, cents, chosen, credit, grants, label, lift, limit, methods, money, monthly, recharge, spend, topup, } from '../api/billing.js';
|
|
21
|
+
import { useRead } from '../data.js';
|
|
22
|
+
import { Card, day, Group, Note, Row, Soft } from './ui.js';
|
|
23
|
+
/** What the credits section says first, on every surface. */
|
|
24
|
+
export const SEPARATE = 'Credits are separate from your plan’s usage allowance. They pay for usage past it when you choose to continue with credits, and for all usage when there is no plan.';
|
|
25
|
+
/** A money figure as read: the amount, or why there is none. */
|
|
26
|
+
function Figure({ read }) {
|
|
27
|
+
return (_jsx(SizableText, { size: "$2", color: read.error ? '$soft' : '$ink', numberOfLines: 1, maxW: 200, children: read.error ? 'Unavailable' : read.loading ? '…' : money(read.value) }));
|
|
28
|
+
}
|
|
29
|
+
/** A thin bar of a whole, red once a limit is used up. */
|
|
30
|
+
function Bar({ part, whole, limit }) {
|
|
31
|
+
const pct = Math.min(100, Math.max(0, (part / whole) * 100));
|
|
32
|
+
return (_jsx(YStack, { height: 6, rounded: 999, bg: "$hover", overflow: "hidden", role: "progressbar", "aria-valuenow": Math.round(pct), "aria-valuemin": 0, "aria-valuemax": 100, children: _jsx(YStack, { height: 6, width: `${pct}%`, rounded: 999, bg: limit && pct >= 100 ? '$red10' : '$ink' }) }));
|
|
33
|
+
}
|
|
34
|
+
function Meter({ title, used, of, says, first, limit = true }) {
|
|
35
|
+
return (_jsxs(YStack, { gap: "$2", px: "$3", py: "$3", borderTopWidth: first ? 0 : 1, borderColor: "$borderColor", children: [_jsxs(XStack, { items: "baseline", justify: "space-between", flexWrap: "wrap", columnGap: "$3", rowGap: "$1", children: [_jsx(SizableText, { size: "$2", color: "$ink", children: title }), _jsx(SizableText, { size: "$1", color: "$soft", children: says })] }), _jsx(Bar, { part: used, whole: of, limit: limit })] }));
|
|
36
|
+
}
|
|
37
|
+
/** `Resets Oct 1, 2026`, or '' for none. */
|
|
38
|
+
const resets = (iso) => (day(iso) ? `Resets ${day(iso)}` : '');
|
|
39
|
+
export function Credits({ target: t, admin, add, onCard }) {
|
|
40
|
+
const left = useRead(() => balance(t), 0, [t]);
|
|
41
|
+
const granted = useRead(() => credit(t), 0, [t]);
|
|
42
|
+
const rows = useRead(() => grants(t), [], [t]);
|
|
43
|
+
const spent = useRead(() => spend(t), null, [t]);
|
|
44
|
+
const rule = useRead(() => recharge(t), null, [t]);
|
|
45
|
+
const ceilings = useRead(() => caps(t), [], [t]);
|
|
46
|
+
const cards = useRead(add ? null : () => methods(t), [], [t, add]);
|
|
47
|
+
const [buying, setBuying] = useState(false);
|
|
48
|
+
const [amount, setAmount] = useState('25');
|
|
49
|
+
const [ceiling, setCeiling] = useState('');
|
|
50
|
+
const [below, setBelow] = useState('');
|
|
51
|
+
const [reload, setReload] = useState('');
|
|
52
|
+
const [working, setWorking] = useState(false);
|
|
53
|
+
const [note, setNote] = useState('');
|
|
54
|
+
// The rule's figures are what the form starts from, once read.
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (!rule.value)
|
|
57
|
+
return;
|
|
58
|
+
setBelow(rule.value.threshold ? String(rule.value.threshold / 100) : '');
|
|
59
|
+
setReload(rule.value.amount ? String(rule.value.amount / 100) : '');
|
|
60
|
+
}, [rule.value]);
|
|
61
|
+
const act = async (what, done, after) => {
|
|
62
|
+
setWorking(true);
|
|
63
|
+
setNote('');
|
|
64
|
+
try {
|
|
65
|
+
await what();
|
|
66
|
+
after();
|
|
67
|
+
setNote(done);
|
|
68
|
+
}
|
|
69
|
+
catch (e) {
|
|
70
|
+
setNote(e.message);
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
setWorking(false);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
const card = chosen(cards.value);
|
|
77
|
+
const adding = cents(amount);
|
|
78
|
+
const cap = monthly(ceilings.value);
|
|
79
|
+
const others = ceilings.value.filter((c) => c !== cap);
|
|
80
|
+
const s = spent.value;
|
|
81
|
+
const top = Math.max(0, ...(s?.categories ?? []).map((c) => c.cents));
|
|
82
|
+
const live = rows.value.filter((g) => g.active && g.remaining > 0);
|
|
83
|
+
const buy = () => {
|
|
84
|
+
if (!adding)
|
|
85
|
+
return setNote('Enter an amount in dollars');
|
|
86
|
+
void act(() => topup(t, adding, card.id), `${money(adding)} is added to the balance`, () => {
|
|
87
|
+
setBuying(false);
|
|
88
|
+
left.reload();
|
|
89
|
+
});
|
|
90
|
+
};
|
|
91
|
+
const setLimit = () => {
|
|
92
|
+
const c = cents(ceiling);
|
|
93
|
+
if (!c)
|
|
94
|
+
return setNote('Enter a monthly limit in dollars');
|
|
95
|
+
void act(() => limit(t, ceilings.value, c), `Spend stops at ${money(c)} a month`, () => {
|
|
96
|
+
setCeiling('');
|
|
97
|
+
ceilings.reload();
|
|
98
|
+
});
|
|
99
|
+
};
|
|
100
|
+
const saveRule = (enabled) => {
|
|
101
|
+
const threshold = below.trim() === '' || below.trim() === '0' ? 0 : cents(below);
|
|
102
|
+
const each = cents(reload);
|
|
103
|
+
if (enabled && (threshold === null || !each))
|
|
104
|
+
return setNote('Enter the balance to reload below and the amount to add, in dollars');
|
|
105
|
+
void act(() => arm(t, { enabled, threshold: threshold ?? 0, amount: each ?? rule.value?.amount ?? 0 }), enabled ? `Adds ${money(each ?? 0)} whenever the balance falls below ${money(threshold ?? 0)}` : 'Auto-reload is off', rule.reload);
|
|
106
|
+
};
|
|
107
|
+
return (_jsxs(YStack, { gap: "$6", "data-slot": "credits", children: [_jsxs(Group, { title: "Credits", detail: SEPARATE, action: add ? (_jsx(Button, { size: "sm", variant: "outline", onPress: () => window.open(add, '_blank', 'noopener'), children: "Add funds" })) : buying || !admin ? undefined : (_jsx(Button, { size: "sm", variant: "outline", onPress: () => setBuying(true), children: "Buy more" })), children: [_jsxs(Card, { children: [_jsx(Row, { first: true, title: "Prepaid balance", detail: "Bought with a card", trailing: _jsx(Figure, { read: left }) }), _jsx(Row, { title: "Granted credit", detail: "Given by Hanzo; some models take only prepaid", trailing: _jsx(Figure, { read: granted }) }), _jsx(Row, { title: "Spent this month", detail: s && !s.known ? 'The ledger did not answer, so this is not a measurement' : undefined, trailing: _jsx(Figure, { read: { ...spent, value: s?.total ?? 0 } }) }), buying ? (_jsx(YStack, { gap: "$2", px: "$3", py: "$3", borderTopWidth: 1, borderColor: "$borderColor", children: card ? (_jsxs(_Fragment, { children: [_jsxs(XStack, { gap: "$2", items: "center", flexWrap: "wrap", children: [_jsx(YStack, { width: 140, children: _jsx(Input, { value: amount, onChangeText: setAmount, "aria-label": "Amount in dollars", placeholder: "25", inputMode: "decimal" }) }), _jsxs(SizableText, { flex: 1, minW: 160, size: "$1", color: "$soft", children: ["Charged to ", label(card)] })] }), _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", onPress: () => setBuying(false), children: "Cancel" }), _jsx(Button, { size: "sm", disabled: working, onPress: buy, children: adding ? `Add ${money(adding)}` : 'Add funds' })] })] })) : (_jsxs(XStack, { gap: "$2", items: "center", flexWrap: "wrap", children: [_jsx(SizableText, { flex: 1, minW: 160, size: "$2", color: "$soft", children: cards.loading ? 'Reading cards…' : 'Add a card to buy more.' }), onCard ? (_jsx(Button, { size: "sm", variant: "outline", onPress: onCard, children: "Add card" })) : null] })) })) : null] }), live.length ? (_jsx(Card, { children: live.map((g, i) => (_jsx(Row, { first: i === 0, title: g.name || 'Credit grant', detail: [`${money(g.remaining, g.currency)} left of ${money(g.amount, g.currency)}`, g.expires ? `expires ${day(g.expires)}` : ''].filter(Boolean).join(' · ') }, g.id))) })) : null] }), _jsx(Group, { title: "Auto-reload", detail: "Tops the balance up from the card on file whenever it falls below a floor.", children: rule.error ? (_jsx(Soft, { children: rule.error.message })) : !rule.value ? (_jsx(Soft, { children: "Reading auto-reload\u2026" })) : (_jsxs(Card, { children: [_jsx(Row, { first: true, title: rule.value.enabled ? `Adds ${money(rule.value.amount)} below ${money(rule.value.threshold)}` : 'Off', detail: rule.value.last ? `Last reloaded ${day(rule.value.last)}` : admin ? undefined : 'An org admin sets auto-reload.', trailing: admin ? (_jsx(Switch, { "aria-label": "Auto-reload", checked: rule.value.enabled, disabled: working, onCheckedChange: (on) => saveRule(on) })) : undefined }), admin ? (_jsxs(XStack, { gap: "$2", items: "center", px: "$3", py: "$3", borderTopWidth: 1, borderColor: "$borderColor", flexWrap: "wrap", children: [_jsx(YStack, { width: 150, children: _jsx(Input, { value: below, onChangeText: setBelow, "aria-label": "Reload below, in dollars", placeholder: "Below $5", inputMode: "decimal" }) }), _jsx(YStack, { width: 150, children: _jsx(Input, { value: reload, onChangeText: setReload, "aria-label": "Amount to add, in dollars", placeholder: "Add $25", inputMode: "decimal" }) }), _jsx(XStack, { flex: 1, justify: "flex-end", children: _jsx(Button, { size: "sm", variant: "outline", disabled: working, onPress: () => saveRule(true), children: rule.value.enabled ? 'Save' : 'Turn on' }) })] })) : null] })) }), _jsx(Group, { title: "Spend this month", detail: "What credits paid for, by kind.", children: spent.error ? (_jsx(Soft, { children: spent.error.message })) : !s ? (_jsx(Soft, { children: "Reading spend\u2026" })) : s.categories.length === 0 ? (_jsx(Card, { children: _jsx(Soft, { children: "Nothing spent this month." }) })) : (_jsx(Card, { children: s.categories.map((c, i) => (_jsx(Meter, { first: i === 0, title: c.name, used: c.cents, of: top, says: money(c.cents), limit: false }, c.name))) })) }), _jsxs(Group, { title: "Monthly limit", detail: "Spend from credits past it is refused until the month starts over.", children: [ceilings.error ? (_jsx(Soft, { children: ceilings.error.message })) : (_jsxs(Card, { children: [cap ? (_jsx(Meter, { first: true, title: `${money(cap.threshold)} a month`, used: cap.spent ?? 0, of: cap.threshold, says: [cap.spent === null ? 'Spend unknown' : `${money(cap.spent)} spent`, resets(cap.resets)].filter(Boolean).join(' · ') })) : (_jsx(Row, { first: true, title: "No monthly limit", detail: "Spend is bounded by the balance alone." })), others.map((c) => (_jsx(Row, { title: c.title || 'Limit', detail: [c.project && `project ${c.project}`, c.service && `service ${c.service}`, c.enforce ? 'refuses spend past it' : 'warns only']
|
|
108
|
+
.filter(Boolean)
|
|
109
|
+
.join(' · '), trailing: _jsx(SizableText, { size: "$2", color: "$ink", children: c.threshold ? money(c.threshold) : '—' }) }, c.id))), admin ? (_jsxs(XStack, { gap: "$2", items: "center", px: "$3", py: "$3", borderTopWidth: 1, borderColor: "$borderColor", flexWrap: "wrap", children: [_jsx(YStack, { width: 160, children: _jsx(Input, { value: ceiling, onChangeText: setCeiling, "aria-label": "Monthly limit in dollars", placeholder: cap ? String(cap.threshold / 100) : '500', inputMode: "decimal" }) }), _jsxs(XStack, { flex: 1, gap: "$2", justify: "flex-end", children: [cap ? (_jsx(Button, { size: "sm", variant: "ghost", disabled: working, onPress: () => void act(() => lift(t, cap.id), 'The monthly limit is lifted', ceilings.reload), children: "Remove" })) : null, _jsx(Button, { size: "sm", variant: "outline", disabled: working || !ceiling.trim(), onPress: setLimit, children: cap ? 'Change limit' : 'Set limit' })] })] })) : null] })), !admin ? (_jsx(SizableText, { size: "$1", color: "$soft", children: "An org admin sets the monthly limit." })) : null] }), _jsx(Note, { children: note })] }));
|
|
110
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Target } from '../api/call.ts';
|
|
2
|
+
import { type Standing } from '../standing.ts';
|
|
3
|
+
/** The view, once read. */
|
|
4
|
+
export declare function Reading({ s, target, onPlans, onBilling, now, }: {
|
|
5
|
+
s: Standing;
|
|
6
|
+
target: Target;
|
|
7
|
+
onPlans?: () => void;
|
|
8
|
+
onBilling: () => void;
|
|
9
|
+
now?: number;
|
|
10
|
+
}): import("react").JSX.Element;
|
|
11
|
+
/** The plan's usage, read as `target`. */
|
|
12
|
+
export declare function Plan({ target, onPlans, onBilling, now }: {
|
|
13
|
+
target: Target;
|
|
14
|
+
onPlans?: () => void;
|
|
15
|
+
onBilling: () => void;
|
|
16
|
+
now?: number;
|
|
17
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* The plan's usage, as the one view every Usage page draws: Dev's Settings,
|
|
4
|
+
* the rooms' Settings and hanzo.ai's organization settings.
|
|
5
|
+
*
|
|
6
|
+
* On a PLAN: its name, its period, the request windows (session, day) and each
|
|
7
|
+
* class of model's share of the period with its shorter window — shares only,
|
|
8
|
+
* never money — and the choice of what happens when the allowance runs out:
|
|
9
|
+
* `Use credits when my plan's allowance runs out` (PUT /v1/ai/limits), off
|
|
10
|
+
* until the payer turns it on, and saying what it spends. Once a class is used
|
|
11
|
+
* up it leads with that, its reset, and the two ways on.
|
|
12
|
+
*
|
|
13
|
+
* On FREE: the share of today's free allowance left and the shared pool's
|
|
14
|
+
* state, and Upgrade. With CREDITS and no plan: the balance, because it is
|
|
15
|
+
* what the account spends.
|
|
16
|
+
*
|
|
17
|
+
* It reads as `target`, so it needs no host: a host says where the plans and
|
|
18
|
+
* Billing are (`onPlans`, `onBilling`).
|
|
19
|
+
*/
|
|
20
|
+
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
21
|
+
import { Button, Switch } from '@hanzo/ui';
|
|
22
|
+
import { useState } from 'react';
|
|
23
|
+
import { money } from '../api/billing.js';
|
|
24
|
+
import { allow, CLASSES } from '../api/limits.js';
|
|
25
|
+
import { INK, Title } from '../meter.js';
|
|
26
|
+
import { spent, ways, when } from '../plan.js';
|
|
27
|
+
import { left, useStanding } from '../standing.js';
|
|
28
|
+
import { Card, day, Group, Note, Row, Soft } from './ui.js';
|
|
29
|
+
const NAME = { premium: 'Premium models', ours: 'Hanzo models' };
|
|
30
|
+
const FILL = { ok: '$ink', near: '$yellow10', limited: '$red10' };
|
|
31
|
+
/** A thin bar of a share used. */
|
|
32
|
+
function Bar({ percent, state }) {
|
|
33
|
+
return (_jsx(YStack, { height: 6, rounded: 999, bg: "$hover", overflow: "hidden", role: "progressbar", "aria-valuenow": percent, "aria-valuemin": 0, "aria-valuemax": 100, children: _jsx(YStack, { height: 6, width: `${percent}%`, rounded: 999, bg: FILL[state] }) }));
|
|
34
|
+
}
|
|
35
|
+
/** One measured row: what, how much of it is used, when it starts over, its bar, and what hangs under it. */
|
|
36
|
+
function Gauge({ title, w, first, says, now, children }) {
|
|
37
|
+
const at = when(w.resets, now);
|
|
38
|
+
return (_jsxs(YStack, { gap: "$2", px: "$3", py: "$3", borderTopWidth: first ? 0 : 1, borderColor: "$borderColor", "data-slot": "plan-window", "data-state": w.state, children: [_jsxs(XStack, { items: "baseline", justify: "space-between", flexWrap: "wrap", columnGap: "$3", rowGap: "$1", children: [_jsx(SizableText, { size: "$2", color: "$ink", children: title }), _jsx(SizableText, { size: "$1", color: INK[w.state], style: { fontVariantNumeric: 'tabular-nums' }, children: [says ?? `${w.percent}% used`, at ? `Resets ${at}` : ''].filter(Boolean).join(' · ') })] }), _jsx(Bar, { percent: w.percent, state: w.state }), children] }));
|
|
39
|
+
}
|
|
40
|
+
/** The view, once read. */
|
|
41
|
+
export function Reading({ s, target, onPlans, onBilling, now, }) {
|
|
42
|
+
const [after, setAfter] = useState(null);
|
|
43
|
+
const [busy, setBusy] = useState(false);
|
|
44
|
+
const [note, setNote] = useState('');
|
|
45
|
+
const l = after ?? s.limits;
|
|
46
|
+
const upgrade = l?.actions.find((a) => a.kind === 'upgrade');
|
|
47
|
+
const choose = async (on) => {
|
|
48
|
+
setBusy(true);
|
|
49
|
+
setNote('');
|
|
50
|
+
try {
|
|
51
|
+
setAfter(await allow(target, on));
|
|
52
|
+
}
|
|
53
|
+
catch (e) {
|
|
54
|
+
setNote(e.message);
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
setBusy(false);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
const go = (a) => {
|
|
61
|
+
if (a.kind === 'credits')
|
|
62
|
+
return void choose(true);
|
|
63
|
+
if (a.kind === 'upgrade' && onPlans)
|
|
64
|
+
return onPlans();
|
|
65
|
+
if (a.kind === 'topup')
|
|
66
|
+
return onBilling();
|
|
67
|
+
if (a.url)
|
|
68
|
+
window.location.assign(a.url);
|
|
69
|
+
};
|
|
70
|
+
// On a plan: a class used up, or one model paused, leads with its ways on, the upgrade among them.
|
|
71
|
+
const used = s.kind === 'plan' && l ? CLASSES.filter((c) => l.classes[c]?.state === 'limited') : [];
|
|
72
|
+
const capped = s.kind === 'plan' ? (l?.paused[0] ?? null) : null;
|
|
73
|
+
const resets = used.map((c) => l.classes[c].resets).sort()[0] ?? '';
|
|
74
|
+
const notice = used.length
|
|
75
|
+
? spent(s.label, used, resets, now)
|
|
76
|
+
: capped
|
|
77
|
+
? `${capped.model.replace(/\*$/, '')} is paused${when(capped.resets, now) ? ` until ${when(capped.resets, now)}` : ''}${capped.fallback ? `; chat continues on ${capped.fallback}` : ''}.`
|
|
78
|
+
: '';
|
|
79
|
+
const head = (_jsxs(XStack, { items: "center", gap: "$3", flexWrap: "wrap", "data-slot": "plan-head", children: [_jsxs(YStack, { flex: 1, minW: 200, gap: "$1", children: [s.label ? _jsx(Title, { label: s.label, size: "$5" }) : null, _jsx(SizableText, { size: "$2", color: "$soft", children: s.kind === 'plan'
|
|
80
|
+
? l?.start && l.end
|
|
81
|
+
? `Current period ${day(l.start)} – ${day(l.end)}`
|
|
82
|
+
: 'Your plan'
|
|
83
|
+
: s.kind === 'free'
|
|
84
|
+
? 'Limited usage of free models, from a pool every free account shares.'
|
|
85
|
+
: 'No plan. Usage is paid from your credits.' })] }), (upgrade && !notice) || (s.kind !== 'plan' && onPlans) ? (_jsx(Button, { size: "sm", variant: s.kind === 'plan' ? 'outline' : 'primary', onPress: () => (upgrade ? go(upgrade) : onPlans?.()), children: upgrade?.label || 'Upgrade' })) : null] }));
|
|
86
|
+
if (s.kind === 'free') {
|
|
87
|
+
const share = left(s.allowance);
|
|
88
|
+
const a = s.allowance;
|
|
89
|
+
return (_jsxs(YStack, { gap: "$5", "data-slot": "plan-usage", "data-kind": "free", children: [head, a && share !== null ? (_jsxs(Card, { children: [_jsx(Gauge, { first: true, title: a.window === 'hour' ? 'This hour' : 'Today', w: { percent: 100 - share, state: share === 0 ? 'limited' : share <= 20 ? 'near' : 'ok', resets: a.resets }, says: `${share}% left`, now: now }), a.pool ? (_jsx(Row, { title: "Shared pool", detail: a.pool.state === 'exhausted' ? `Used up${when(a.pool.resets, now) ? ` until ${when(a.pool.resets, now)}` : ''}` : a.pool.state === 'busy' ? 'Busy. Try again in a minute.' : 'Every free request is being served.' })) : null] })) : (_jsx(Soft, { children: "The free allowance could not be read." })), _jsxs(XStack, { items: "center", gap: "$3", flexWrap: "wrap", children: [_jsx(SizableText, { size: "$2", color: "$soft", flex: 1, minW: 200, children: "Credits pay as you go for any model, with or without a plan." }), _jsx(Button, { size: "sm", variant: "outline", onPress: onBilling, children: "Add credits" })] })] }));
|
|
90
|
+
}
|
|
91
|
+
if (s.kind === 'credits') {
|
|
92
|
+
return (_jsxs(YStack, { gap: "$5", "data-slot": "plan-usage", "data-kind": "credits", children: [head, _jsx(Card, { children: _jsx(Row, { first: true, title: "Credits", detail: "What your usage is paid from, at each model\u2019s list price.", trailing: _jsx(SizableText, { size: "$2", color: "$ink", "data-slot": "plan-balance", children: s.balance === null ? 'Unavailable' : money(s.balance) }) }) }), _jsx(XStack, { children: _jsx(Button, { size: "sm", variant: "outline", onPress: onBilling, children: "Add funds" }) })] }));
|
|
93
|
+
}
|
|
94
|
+
const on = l?.credits ?? false;
|
|
95
|
+
return (_jsxs(YStack, { gap: "$5", "data-slot": "plan-usage", "data-kind": "plan", children: [head, notice ? (_jsxs(XStack, { items: "center", gap: "$2.5", flexWrap: "wrap", px: "$3", py: "$2.5", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", "data-slot": "plan-spent", children: [_jsx(YStack, { width: 8, height: 8, rounded: 4, bg: "$red10", shrink: 0, "aria-hidden": true }), _jsx(SizableText, { size: "$2", color: "$ink", flex: 1, minW: 200, children: notice }), _jsx(XStack, { gap: "$2", flexWrap: "wrap", children: ways(l?.actions ?? []).map((a, i) => (_jsx(Button, { size: "sm", variant: i === 0 ? 'primary' : 'outline', disabled: busy, "data-kind": a.kind, onPress: () => go(a), children: a.label }, a.kind))) })] })) : null, !l ? (_jsx(Soft, { children: "The plan\u2019s usage could not be read." })) : !l.session && !l.day && !Object.keys(l.classes).length ? (_jsx(Soft, { children: "This plan sets no usage limits." })) : (_jsxs(_Fragment, { children: [l.session || l.day ? (_jsx(Group, { title: "Requests", detail: "Every request the plan covers counts in both.", children: _jsxs(Card, { children: [l.session ? _jsx(Gauge, { first: true, title: "Session", w: l.session, says: l.session.resets ? undefined : `${l.session.percent}% used · Starts with your next request`, now: now }) : null, l.day ? _jsx(Gauge, { first: !l.session, title: "Today", w: l.day, now: now }) : null] }) })) : null, Object.keys(l.classes).length ? (_jsx(Group, { title: "By model class", detail: "Each class\u2019s share of the billing period, and of its shorter window where the plan sets one.", children: _jsx(Card, { children: CLASSES.filter((c) => l.classes[c]).map((c, i) => {
|
|
96
|
+
const k = l.classes[c];
|
|
97
|
+
return (_jsx(YStack, { "data-class": c, children: _jsx(Gauge, { first: i === 0, title: NAME[c], w: k, says: k.paying === 'credits' ? `${k.percent}% used · Paying from credits` : undefined, now: now, children: k.window ? (_jsxs(XStack, { gap: "$2", items: "center", "data-slot": "plan-short", children: [_jsx(SizableText, { size: "$1", color: "$soft", shrink: 0, children: "Shorter window" }), _jsx(YStack, { flex: 1, children: _jsx(Bar, { percent: k.window.percent, state: k.window.state }) }), _jsx(SizableText, { size: "$1", color: "$soft", shrink: 0, children: [`${k.window.percent}%`, when(k.window.resets, now) ? `Resets ${when(k.window.resets, now)}` : ''].filter(Boolean).join(' · ') })] })) : null }) }, c));
|
|
98
|
+
}) }) })) : null, _jsx(Group, { title: "When the allowance runs out", detail: "Credits are separate from your plan\u2019s usage allowance. They live in Billing.", children: _jsxs(Card, { children: [_jsx(Row, { first: true, title: "Use credits when my plan\u2019s allowance runs out", detail: on
|
|
99
|
+
? 'On. A model whose included usage is used keeps working, paid from credits — prepaid first, then granted credit where the model takes it — at its list price.'
|
|
100
|
+
: 'Off. Chat continues on a free model, and other requests wait for the plan to reset.', trailing: _jsx(Switch, { "aria-label": "Use credits when my plan\u2019s allowance runs out", checked: on, disabled: busy, onCheckedChange: (next) => void choose(next) }) }), _jsx(Row, { title: "Credits", detail: s.balance === 0 ? 'There are no credits to spend yet.' : 'The balance, top-ups and auto-reload.', trailing: _jsx(Button, { size: "sm", variant: "ghost", onPress: onBilling, children: s.balance === 0 ? 'Add credits' : 'Billing' }) })] }) })] })), _jsx(Note, { children: note })] }));
|
|
101
|
+
}
|
|
102
|
+
/** The plan's usage, read as `target`. */
|
|
103
|
+
export function Plan({ target, onPlans, onBilling, now }) {
|
|
104
|
+
const read = useStanding(target);
|
|
105
|
+
if (read.value)
|
|
106
|
+
return _jsx(Reading, { s: read.value, target: target, onPlans: onPlans, onBilling: onBilling, now: now });
|
|
107
|
+
return _jsx(Soft, { children: read.error ? read.error.message : 'Reading the plan…' });
|
|
108
|
+
}
|
package/lib/settings/usage.js
CHANGED
|
@@ -1,128 +1,20 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
|
-
* Usage:
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* balance
|
|
3
|
+
* Usage: the plan's windows and what each class of model has used of it, as
|
|
4
|
+
* shares; the free allowance on Free; the balance with no plan. What happens
|
|
5
|
+
* when a plan's allowance runs out is chosen here. Credits themselves — the
|
|
6
|
+
* balance, top-ups, auto-reload, spend and its limit — are Billing's.
|
|
7
7
|
*/
|
|
8
|
-
import {
|
|
9
|
-
import { Button, Input } from '@hanzo/ui';
|
|
10
|
-
import { useState } from 'react';
|
|
11
|
-
import { allowance, balance, caps, cents, chosen, credit, label, lift, limit, methods, money, month, monthly, spend, topup, } from '../api/billing.js';
|
|
12
|
-
import { useRead } from '../data.js';
|
|
8
|
+
import { YStack } from '@hanzo/gui';
|
|
13
9
|
import { useHost, useTarget } from '../host.js';
|
|
14
10
|
import { path } from '../route.js';
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
function Bar({ part, whole, limit }) {
|
|
19
|
-
const pct = Math.min(100, Math.max(0, (part / whole) * 100));
|
|
20
|
-
return (_jsx(YStack, { height: 6, rounded: 999, bg: "$hover", overflow: "hidden", role: "progressbar", "aria-valuenow": Math.round(pct), "aria-valuemin": 0, "aria-valuemax": 100, children: _jsx(YStack, { height: 6, width: `${pct}%`, rounded: 999, bg: limit && pct >= 100 ? '$red10' : '$ink' }) }));
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* One measured row: what, how much, when it starts over, and its bar. A limit's
|
|
24
|
-
* bar turns red when it is used up; a share of a total never does.
|
|
25
|
-
*/
|
|
26
|
-
function Meter({ title, used, of, says, first, limit = true }) {
|
|
27
|
-
return (_jsxs(YStack, { gap: "$2", px: "$3", py: "$3", borderTopWidth: first ? 0 : 1, borderColor: "$borderColor", children: [_jsxs(XStack, { items: "baseline", justify: "space-between", flexWrap: "wrap", columnGap: "$3", rowGap: "$1", children: [_jsx(SizableText, { size: "$2", color: "$ink", children: title }), _jsx(SizableText, { size: "$1", color: "$soft", children: says })] }), _jsx(Bar, { part: used, whole: of, limit: limit })] }));
|
|
28
|
-
}
|
|
29
|
-
/** `in 3 hr 12 min` or `Oct 1, 2026`, for when a window starts over. */
|
|
30
|
-
function resets(iso) {
|
|
31
|
-
const at = iso ? new Date(iso).getTime() : NaN;
|
|
32
|
-
if (Number.isNaN(at))
|
|
33
|
-
return '';
|
|
34
|
-
const mins = Math.round((at - Date.now()) / 60000);
|
|
35
|
-
if (mins <= 0)
|
|
36
|
-
return 'Resets now';
|
|
37
|
-
if (mins < 60)
|
|
38
|
-
return `Resets in ${mins} min`;
|
|
39
|
-
if (mins < 24 * 60)
|
|
40
|
-
return `Resets in ${Math.floor(mins / 60)} hr ${mins % 60} min`;
|
|
41
|
-
return `Resets ${day(iso)}`;
|
|
42
|
-
}
|
|
43
|
-
const POOL = { available: 'Available', busy: 'Busy', exhausted: 'Used up' };
|
|
44
|
-
const POOL_SAYS = {
|
|
45
|
-
available: () => 'Every free request is being served',
|
|
46
|
-
busy: () => 'Try again in a minute',
|
|
47
|
-
exhausted: (refill) => refill || 'Refills when the accounts reset',
|
|
48
|
-
};
|
|
49
|
-
/** `Refills at 16:00 UTC`, for a window or the pool: the UTC time it opens again, with the date when not today. */
|
|
50
|
-
function refills(iso) {
|
|
51
|
-
const at = iso ? new Date(iso) : null;
|
|
52
|
-
if (!at || Number.isNaN(at.getTime()))
|
|
53
|
-
return '';
|
|
54
|
-
const hhmm = `${String(at.getUTCHours()).padStart(2, '0')}:${String(at.getUTCMinutes()).padStart(2, '0')} UTC`;
|
|
55
|
-
return at.toISOString().slice(0, 10) === new Date().toISOString().slice(0, 10) ? `Refills at ${hhmm}` : `Refills ${day(iso)}, ${hhmm}`;
|
|
56
|
-
}
|
|
11
|
+
import { useStanding } from '../standing.js';
|
|
12
|
+
import { Reading } from './plan.js';
|
|
13
|
+
import { Heading, Soft } from './ui.js';
|
|
57
14
|
export function Usage() {
|
|
58
15
|
const host = useHost();
|
|
59
16
|
const t = useTarget();
|
|
60
17
|
const signed = Boolean(host.person);
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
const left = useRead(signed ? () => balance(t) : null, 0, [t, signed]);
|
|
64
|
-
const granted = useRead(signed ? () => credit(t) : null, 0, [t, signed]);
|
|
65
|
-
const spent = useRead(signed ? () => spend(t) : null, null, [t, signed]);
|
|
66
|
-
const ceilings = useRead(signed ? () => caps(t) : null, [], [t, signed]);
|
|
67
|
-
const cards = useRead(signed ? () => methods(t) : null, [], [t, signed]);
|
|
68
|
-
const [buying, setBuying] = useState(false);
|
|
69
|
-
const [amount, setAmount] = useState('25');
|
|
70
|
-
const [ceiling, setCeiling] = useState('');
|
|
71
|
-
const [working, setWorking] = useState(false);
|
|
72
|
-
const [note, setNote] = useState('');
|
|
73
|
-
if (!signed) {
|
|
74
|
-
return (_jsxs(YStack, { gap: "$5", children: [_jsx(Heading, { title: "Usage" }), _jsx(Soft, { children: "Sign in to see this organization\u2019s usage." })] }));
|
|
75
|
-
}
|
|
76
|
-
const act = async (what, done, after) => {
|
|
77
|
-
setWorking(true);
|
|
78
|
-
setNote('');
|
|
79
|
-
try {
|
|
80
|
-
await what();
|
|
81
|
-
after();
|
|
82
|
-
setNote(done);
|
|
83
|
-
}
|
|
84
|
-
catch (e) {
|
|
85
|
-
setNote(e.message);
|
|
86
|
-
}
|
|
87
|
-
finally {
|
|
88
|
-
setWorking(false);
|
|
89
|
-
}
|
|
90
|
-
};
|
|
91
|
-
const m = plan.value;
|
|
92
|
-
const windows = (m?.windows ?? []).filter((w) => w.limit > 0);
|
|
93
|
-
const card = chosen(cards.value);
|
|
94
|
-
const cap = monthly(ceilings.value);
|
|
95
|
-
const others = ceilings.value.filter((c) => c !== cap);
|
|
96
|
-
const s = spent.value;
|
|
97
|
-
const top = Math.max(0, ...(s?.categories ?? []).map((c) => c.cents));
|
|
98
|
-
const adding = cents(amount);
|
|
99
|
-
// Offered only with a card to charge.
|
|
100
|
-
const buy = () => {
|
|
101
|
-
if (!adding) {
|
|
102
|
-
setNote('Enter an amount in dollars');
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
void act(() => topup(t, adding, card.id), `${money(adding)} is added to the balance`, () => {
|
|
106
|
-
setBuying(false);
|
|
107
|
-
left.reload();
|
|
108
|
-
});
|
|
109
|
-
};
|
|
110
|
-
const setLimit = () => {
|
|
111
|
-
const c = cents(ceiling);
|
|
112
|
-
if (!c) {
|
|
113
|
-
setNote('Enter a monthly limit in dollars');
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
void act(() => limit(t, ceilings.value, c), `Spend stops at ${money(c)} a month`, () => {
|
|
117
|
-
setCeiling('');
|
|
118
|
-
ceilings.reload();
|
|
119
|
-
});
|
|
120
|
-
};
|
|
121
|
-
return (_jsxs(YStack, { gap: "$6", children: [_jsx(Heading, { title: "Usage", detail: `What ${host.org ?? 'this organization'} has used, has left, and may spend.` }), free.value?.pooled && free.value.limit > 0 ? (_jsx(Group, { title: "Free plan", detail: "Free \u2014 limited usage, from a pool shared by all free users.", action: _jsx(Button, { size: "sm", variant: "outline", onPress: () => host.go(path({ kind: 'screen', screen: 'plans' })), children: "Upgrade" }), children: _jsxs(Card, { children: [_jsx(Meter, { first: true, title: `${Math.max(0, free.value.limit - free.value.used).toLocaleString()} of ${free.value.limit.toLocaleString()} left ${free.value.window === 'hour' ? 'this hour' : free.value.window === 'day' ? 'today' : 'this period'}`, used: free.value.used, of: free.value.limit, says: [`${free.value.used.toLocaleString()} used`, refills(free.value.resets)].filter(Boolean).join(' · ') }), free.value.pool ? (_jsx(Row, { title: "Shared pool", detail: POOL_SAYS[free.value.pool.state]?.(refills(free.value.pool.resets)) ?? '', trailing: _jsx(SizableText, { size: "$2", color: free.value.pool.state === 'exhausted' ? '$red10' : '$ink', children: POOL[free.value.pool.state] ?? free.value.pool.state }) })) : null] }) })) : null, _jsx(Group, { title: "Plan usage limits", detail: m?.plan ? `Included with ${m.plan}.` : 'What the plan includes.', children: plan.error ? (_jsx(Soft, { children: plan.error.message })) : plan.loading && !m ? (_jsx(Soft, { children: "Reading the plan\u2019s limits\u2026" })) : !m || (windows.length === 0 && m.included === 0) ? (_jsx(Card, { children: _jsx(Soft, { children: "This plan sets no usage limits. Usage is paid from the balance." }) })) : (_jsxs(Card, { children: [m.included > 0 ? (_jsx(Meter, { first: true, title: "Included spend", used: m.used, of: m.included, says: `${money(m.used)} of ${money(m.included)}${m.overage ? ` · ${money(m.overage)} over` : ''}` })) : null, windows.map((w, i) => (_jsx(Meter, { first: i === 0 && m.included === 0, title: `${SPAN[w.span] ?? w.span} · ${w.used.toLocaleString()} of ${w.limit.toLocaleString()} requests`, used: w.used, of: w.limit, says: `${Math.round((Math.min(w.used, w.limit) / w.limit) * 100)}% used${resets(w.resets) ? ` · ${resets(w.resets)}` : ''}` }, w.span)))] })) }), _jsx(Group, { title: "Balance", detail: "Prepaid money pays for usage past the plan. Credits are spent first.", action: buying || !host.admin ? undefined : (_jsx(Button, { size: "sm", variant: "outline", onPress: () => setBuying(true), children: "Buy more" })), children: _jsxs(Card, { children: [_jsx(Row, { first: true, title: "Balance", detail: "What is left to spend", trailing: _jsx(Figure, { read: left }) }), _jsx(Row, { title: "Credits", detail: "Granted, and spent before the balance", trailing: _jsx(Figure, { read: granted }) }), _jsx(Row, { title: "Spent this month", detail: s && !s.known ? 'The ledger did not answer, so this is not a measurement' : undefined, trailing: _jsx(Figure, { read: { ...spent, value: s?.total ?? 0 } }) }), buying ? (_jsx(YStack, { gap: "$2", px: "$3", py: "$3", borderTopWidth: 1, borderColor: "$borderColor", children: card ? (_jsxs(_Fragment, { children: [_jsxs(XStack, { gap: "$2", items: "center", flexWrap: "wrap", children: [_jsx(YStack, { width: 140, children: _jsx(Input, { value: amount, onChangeText: setAmount, "aria-label": "Amount in dollars", placeholder: "25", inputMode: "decimal" }) }), _jsxs(SizableText, { flex: 1, minW: 160, size: "$1", color: "$soft", children: ["Charged to ", label(card)] })] }), _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", onPress: () => setBuying(false), children: "Cancel" }), _jsx(Button, { size: "sm", disabled: working, onPress: buy, children: adding ? `Add ${money(adding)}` : 'Add funds' })] })] })) : (_jsxs(XStack, { gap: "$2", items: "center", flexWrap: "wrap", children: [_jsx(SizableText, { flex: 1, minW: 160, size: "$2", color: "$soft", children: cards.loading ? 'Reading cards…' : 'Add a card in Billing to buy more.' }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => host.go(path({ kind: 'settings', section: 'billing' })), children: "Billing" })] })) })) : null] }) }), _jsx(Group, { title: "Spend this month", detail: "By what it paid for.", children: spent.error ? (_jsx(Soft, { children: spent.error.message })) : !s ? (_jsx(Soft, { children: "Reading spend\u2026" })) : s.categories.length === 0 ? (_jsx(Card, { children: _jsx(Soft, { children: "Nothing spent this month." }) })) : (_jsx(Card, { children: s.categories.map((c, i) => (_jsx(Meter, { first: i === 0, title: c.name, used: c.cents, of: top, says: money(c.cents), limit: false }, c.name))) })) }), _jsxs(Group, { title: "Monthly limit", detail: "Spend past it is refused until the month starts over.", children: [ceilings.error ? (_jsx(Soft, { children: ceilings.error.message })) : (_jsxs(Card, { children: [cap ? (_jsx(Meter, { first: true, title: `${money(cap.threshold)} a month`, used: cap.spent ?? 0, of: cap.threshold, says: [cap.spent === null ? 'Spend unknown' : `${money(cap.spent)} spent`, resets(cap.resets)].filter(Boolean).join(' · ') })) : (_jsx(Row, { first: true, title: "No monthly limit", detail: "Spend is bounded by the balance alone." })), others.map((c) => (_jsx(Row, { title: c.title || 'Limit', detail: [c.project && `project ${c.project}`, c.service && `service ${c.service}`, c.enforce ? 'refuses spend past it' : 'warns only']
|
|
122
|
-
.filter(Boolean)
|
|
123
|
-
.join(' · '), trailing: _jsx(SizableText, { size: "$2", color: "$ink", children: c.threshold ? money(c.threshold) : '—' }) }, c.id))), host.admin ? (_jsxs(XStack, { gap: "$2", items: "center", px: "$3", py: "$3", borderTopWidth: 1, borderColor: "$borderColor", flexWrap: "wrap", children: [_jsx(YStack, { width: 160, children: _jsx(Input, { value: ceiling, onChangeText: setCeiling, "aria-label": "Monthly limit in dollars", placeholder: cap ? String(cap.threshold / 100) : '500', inputMode: "decimal" }) }), _jsxs(XStack, { flex: 1, gap: "$2", justify: "flex-end", children: [cap ? (_jsx(Button, { size: "sm", variant: "ghost", disabled: working, onPress: () => void act(() => lift(t, cap.id), 'The monthly limit is lifted', ceilings.reload), children: "Remove" })) : null, _jsx(Button, { size: "sm", variant: "outline", disabled: working || !ceiling.trim(), onPress: setLimit, children: cap ? 'Change limit' : 'Set limit' })] })] })) : null] })), !host.admin ? (_jsx(SizableText, { size: "$1", color: "$soft", children: "An org admin sets the monthly limit." })) : null] }), _jsx(Note, { children: note })] }));
|
|
124
|
-
}
|
|
125
|
-
/** A money figure as read: the amount, or why there is none. */
|
|
126
|
-
function Figure({ read }) {
|
|
127
|
-
return (_jsx(SizableText, { size: "$2", color: read.error ? '$soft' : '$ink', numberOfLines: 1, maxW: 200, children: read.error ? 'Unavailable' : read.loading ? '…' : money(read.value) }));
|
|
18
|
+
const read = useStanding(t, signed);
|
|
19
|
+
return (_jsxs(YStack, { gap: "$6", children: [_jsx(Heading, { title: "Usage", detail: `What ${host.org ?? 'this organization'}’s plan includes, and how much of it is used.` }), !signed ? (_jsx(Soft, { children: "Sign in to see this organization\u2019s usage." })) : read.value ? (_jsx(Reading, { s: read.value, target: t, onPlans: () => host.go(path({ kind: 'screen', screen: 'plans' })), onBilling: () => host.go(path({ kind: 'settings', section: 'billing' })) })) : (_jsx(Soft, { children: read.error ? read.error.message : 'Reading the plan…' }))] }));
|
|
128
20
|
}
|
package/lib/shelf.js
CHANGED
|
@@ -10,7 +10,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
10
10
|
*/
|
|
11
11
|
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
12
12
|
import { Boxes, ExternalLink, Globe, Lock, MoreHorizontal } from '@hanzogui/lucide-icons-2';
|
|
13
|
-
import { DropdownMenu } from '@hanzo/ui';
|
|
13
|
+
import { Button, DropdownMenu } from '@hanzo/ui';
|
|
14
14
|
import { useState } from 'react';
|
|
15
15
|
import { change, fork, remove, templates } from './api/projects.js';
|
|
16
16
|
import { Confirm, Rename } from './ask.js';
|
|
@@ -97,7 +97,7 @@ export function Artifacts() {
|
|
|
97
97
|
}
|
|
98
98
|
};
|
|
99
99
|
const said = failed || note;
|
|
100
|
-
return (_jsxs(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$6", py: "$6", items: "center", "$max-md": { px: '$4' }, children: [_jsxs(YStack, { width: "100%", maxW: 1080, children: [_jsx(Head, { title: "Artifacts", says: "What this organization has built. Open one to keep building it." }), said ? (_jsx(SizableText, { size: "$2", color: "$soft", role: "status", pb: "$3", children: said })) : null, starters.value.length ? (_jsxs(YStack, { gap: "$3", pb: "$6", children: [_jsx(SizableText, { size: "$3", color: "$ink", children: "Make something new" }), _jsx(XStack, { flexWrap: "wrap", gap: "$3", children: starters.value.slice(0, 4).map((x) => (_jsxs(Card, { small: true, label: `Start from ${x.title}`, onPress: () => void take(x.slug), children: [_jsx(Shot, { slug: x.slug }), _jsx(YStack, { px: "$3", py: "$2.5", children: _jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 1, children: busy === x.slug ? 'Copying…' : x.title }) })] }, x.slug))) })] })) : null, list.error ? (_jsx(SizableText, { size: "$2", color: "$soft", children: list.error.message })) : !host.person ? (_jsx(SizableText, { size: "$2", color: "$soft", children: "Sign in to see what your organization has built." })) : list.loading && list.value.length === 0 ? (_jsx(SizableText, { size: "$2", color: "$soft", children: "Reading your projects\u2026" })) : list.value.length === 0 ? (_jsx(SizableText, { size: "$2", color: "$soft", children: "Nothing built yet. Describe something on the New page and it lands here." })) : (_jsx(YStack, { gap: "$5", children: months(list.value).map((g) => (_jsxs(YStack, { gap: "$2", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: g.label }), _jsx(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$3", overflow: "hidden", children: g.rows.map((p, i) => (_jsxs(XStack, { items: "center", pr: "$2", borderTopWidth: i ? 1 : 0, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, children: [_jsxs(XStack, { flex: 1, minW: 0, render: "button", "aria-label": `Open ${p.name}`, onPress: () => host.go(p.slug), items: "center", gap: "$3", px: "$3", py: "$2.5", cursor: "pointer", style: { textAlign: 'left' }, children: [p.visibility === 'private' ? _jsx(Lock, { size: 16, opacity: 0.7 }) : _jsx(Globe, { size: 16, opacity: 0.7 }), _jsxs(YStack, { flex: 1, minW: 0, gap: "$0.5", children: [_jsx(SizableText, { size: "$3", color: "$ink", numberOfLines: 1, children: p.name }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: [p.visibility === 'private' ? 'Private' : 'Public', edited(when(p)), p.live ? new URL(p.live).host : 'Not deployed yet']
|
|
100
|
+
return (_jsxs(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$6", py: "$6", items: "center", "$max-md": { px: '$4' }, children: [_jsxs(YStack, { width: "100%", maxW: 1080, children: [_jsx(Head, { title: "Artifacts", says: "What this organization has built. Open one to keep building it." }), said ? (_jsx(SizableText, { size: "$2", color: "$soft", role: "status", pb: "$3", children: said })) : null, starters.value.length ? (_jsxs(YStack, { gap: "$3", pb: "$6", children: [_jsx(SizableText, { size: "$3", color: "$ink", children: "Make something new" }), _jsx(XStack, { flexWrap: "wrap", gap: "$3", children: starters.value.slice(0, 4).map((x) => (_jsxs(Card, { small: true, label: `Start from ${x.title}`, onPress: () => void take(x.slug), children: [_jsx(Shot, { slug: x.slug }), _jsx(YStack, { px: "$3", py: "$2.5", children: _jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 1, children: busy === x.slug ? 'Copying…' : x.title }) })] }, x.slug))) })] })) : null, list.error ? (_jsx(SizableText, { size: "$2", color: "$soft", children: list.error.message })) : !host.person ? (_jsx(SizableText, { size: "$2", color: "$soft", children: "Sign in to see what your organization has built." })) : list.loading && list.value.length === 0 ? (_jsx(SizableText, { size: "$2", color: "$soft", children: "Reading your projects\u2026" })) : list.value.length === 0 ? (_jsxs(YStack, { gap: "$3", items: "flex-start", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: "Nothing built yet. Describe something on the New page and it lands here." }), _jsx(Button, { size: "sm", onPress: () => host.go(''), children: "New run" })] })) : (_jsx(YStack, { gap: "$5", children: months(list.value).map((g) => (_jsxs(YStack, { gap: "$2", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: g.label }), _jsx(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$3", overflow: "hidden", children: g.rows.map((p, i) => (_jsxs(XStack, { items: "center", pr: "$2", borderTopWidth: i ? 1 : 0, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, children: [_jsxs(XStack, { flex: 1, minW: 0, render: "button", "aria-label": `Open ${p.name}`, onPress: () => host.go(p.slug), items: "center", gap: "$3", px: "$3", py: "$2.5", cursor: "pointer", style: { textAlign: 'left' }, children: [p.visibility === 'private' ? _jsx(Lock, { size: 16, opacity: 0.7 }) : _jsx(Globe, { size: 16, opacity: 0.7 }), _jsxs(YStack, { flex: 1, minW: 0, gap: "$0.5", children: [_jsx(SizableText, { size: "$3", color: "$ink", numberOfLines: 1, children: p.name }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: [p.visibility === 'private' ? 'Private' : 'Public', edited(when(p)), p.live ? new URL(p.live).host : 'Not deployed yet']
|
|
101
101
|
.filter(Boolean)
|
|
102
102
|
.join(' · ') })] })] }), _jsx(DropdownMenu, { trigger: _jsx(XStack, { render: "button", "aria-label": `Actions for ${p.name}`, px: "$1.5", py: "$1", rounded: "$2", hoverStyle: { bg: '$raised' }, children: _jsx(MoreHorizontal, { size: 16 }) }), items: [
|
|
103
103
|
{ key: 'rename', label: 'Rename', onSelect: () => setRenaming(p) },
|
|
@@ -127,5 +127,5 @@ export function Templates() {
|
|
|
127
127
|
const t = useTarget();
|
|
128
128
|
const list = useRead(() => templates(t), [], [t.api]);
|
|
129
129
|
const { busy, failed, take } = useFork();
|
|
130
|
-
return (_jsx(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$6", py: "$6", items: "center", children: _jsxs(YStack, { width: "100%", maxW: 1080, children: [_jsx(Head, { title: "Templates", says: "Start from a working app. Picking one copies its repository into a project of yours." }), failed ? (_jsx(SizableText, { size: "$2", color: "$soft", role: "status", pb: "$3", children: failed })) : null, list.error ? (_jsx(SizableText, { size: "$2", color: "$soft", children: list.error.message })) : (_jsx(XStack, { flexWrap: "wrap", gap: "$3", children: list.value.map((x) => (_jsxs(Card, { label: `Start from ${x.title}`, onPress: () => void take(x.slug), children: [_jsx(Shot, { slug: x.slug }), _jsxs(YStack, { p: "$3", gap: "$1", children: [_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(SizableText, { size: "$3", color: "$ink", numberOfLines: 1, flex: 1, children: busy === x.slug ? 'Copying…' : x.title }), x.framework ? (_jsx(SizableText, { size: "$1", color: "$soft", children: x.framework })) : null] }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 2, children: x.description })] })] }, x.slug))) })), _jsx(YStack, { pt: "$5", children: _jsxs(Out, { href: "https://gallery.hanzo.ai", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: "The whole catalog" }), _jsx(ExternalLink, { size: 12, opacity: 0.6 })] }) })] }) }));
|
|
130
|
+
return (_jsx(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$6", py: "$6", items: "center", children: _jsxs(YStack, { width: "100%", maxW: 1080, children: [_jsx(Head, { title: "Templates", says: "Start from a working app. Picking one copies its repository into a project of yours." }), failed ? (_jsx(SizableText, { size: "$2", color: "$soft", role: "status", pb: "$3", children: failed })) : null, list.error ? (_jsx(SizableText, { size: "$2", color: "$soft", children: list.error.message })) : list.loading && !list.value.length ? (_jsx(SizableText, { size: "$2", color: "$soft", children: "Reading templates\u2026" })) : !list.value.length ? (_jsx(SizableText, { size: "$2", color: "$soft", children: "No templates here right now. The whole catalog has more." })) : (_jsx(XStack, { flexWrap: "wrap", gap: "$3", children: list.value.map((x) => (_jsxs(Card, { label: `Start from ${x.title}`, onPress: () => void take(x.slug), children: [_jsx(Shot, { slug: x.slug }), _jsxs(YStack, { p: "$3", gap: "$1", children: [_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(SizableText, { size: "$3", color: "$ink", numberOfLines: 1, flex: 1, children: busy === x.slug ? 'Copying…' : x.title }), x.framework ? (_jsx(SizableText, { size: "$1", color: "$soft", children: x.framework })) : null] }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 2, children: x.description })] })] }, x.slug))) })), _jsx(YStack, { pt: "$5", children: _jsxs(Out, { href: "https://gallery.hanzo.ai", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: "The whole catalog" }), _jsx(ExternalLink, { size: 12, opacity: 0.6 })] }) })] }) }));
|
|
131
131
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where an account stands: which plan it is on, which meter it reads, and the
|
|
3
|
+
* reads behind it — the tier for the plan's name, the limits for its windows,
|
|
4
|
+
* the free allowance, and the prepaid balance. One read for every surface that
|
|
5
|
+
* shows a plan (the account menus, Usage, Billing), so they cannot disagree.
|
|
6
|
+
*
|
|
7
|
+
* Each read stands alone: one that fails leaves its part empty and the rest
|
|
8
|
+
* drawn. Only when none answers is the whole read an error.
|
|
9
|
+
*/
|
|
10
|
+
import { type Allowance } from './api/billing.ts';
|
|
11
|
+
import type { Target } from './api/call.ts';
|
|
12
|
+
import { type Limits } from './api/limits.ts';
|
|
13
|
+
import { type Read } from './data.ts';
|
|
14
|
+
import { type Kind, type Label, type State } from './plan.ts';
|
|
15
|
+
export interface Standing {
|
|
16
|
+
kind: Kind;
|
|
17
|
+
/** What the account is on, as a title: the plan's name, Free, or Pay as you go. */
|
|
18
|
+
label: Label | null;
|
|
19
|
+
limits: Limits | null;
|
|
20
|
+
/** The free allowance, where the platform answered it. */
|
|
21
|
+
allowance: Allowance | null;
|
|
22
|
+
/** The prepaid balance in cents, or null where it could not be read. */
|
|
23
|
+
balance: number | null;
|
|
24
|
+
}
|
|
25
|
+
/** Every read at once. Rejects only when all of them were refused, with the first refusal. */
|
|
26
|
+
export declare function stand(t: Target): Promise<Standing>;
|
|
27
|
+
/** The standing for `t`, read when `enabled` (a menu that is open) and again when the org changes. */
|
|
28
|
+
export declare function useStanding(t: Target, enabled?: boolean): Read<Standing | null>;
|
|
29
|
+
/** One window of a plan as a menu lists it. */
|
|
30
|
+
export interface Row {
|
|
31
|
+
name: 'Session' | 'Today' | 'Month';
|
|
32
|
+
percent: number;
|
|
33
|
+
state: State;
|
|
34
|
+
/** `Resets 5:00 PM`, or '' where nothing is running yet. */
|
|
35
|
+
resets: string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* A plan's windows, shortest first: the session, the UTC day, and the month —
|
|
39
|
+
* the billing period of whichever class of model has used the most of it,
|
|
40
|
+
* because that is the one that stops a request first.
|
|
41
|
+
*/
|
|
42
|
+
export declare function rows(l: Limits | null, now?: number): Row[];
|
|
43
|
+
/** The share of the free allowance left in its window, 0 to 100, or null where none bounds the account. */
|
|
44
|
+
export declare function left(a: Allowance | null): number | null;
|