@hanzo/rooms 0.1.20 → 0.1.22

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/dist/Account.d.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  /** The block at the foot of the sidebar: press it for the menu. */
2
- export declare function Account({ onToggleCollapse, onProfile, onAppearance, }?: {
2
+ export declare function Account({ onToggleCollapse, onProfile, onAppearance, onUsage, }?: {
3
3
  onToggleCollapse?: () => void;
4
4
  /** Opens the profile beside the room. Absent, the row goes to hanzo.id. */
5
5
  onProfile?: () => void;
6
6
  /** Opens the appearance settings. Absent, the row is not drawn. */
7
7
  onAppearance?: () => void;
8
+ /** Opens the plan's usage settings. Absent, the row is not drawn. */
9
+ onUsage?: () => void;
8
10
  }): import("react").JSX.Element;
package/dist/Account.js CHANGED
@@ -3,13 +3,13 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  /**
4
4
  * WHO YOU ARE, at the foot of the sidebar: the person, and the person's menu.
5
5
  *
6
- * Profile, appearance, security and sign out — the account, and nothing that
6
+ * Profile, appearance, usage, security and sign out — the account, and nothing that
7
7
  * belongs to the workspace. Which workspace you stand in is the switcher at the
8
8
  * TOP of the sidebar, and what it may spend is its Billing, so this card shows
9
9
  * neither: a balance here read as the person's money when it is the workspace's.
10
10
  */
11
11
  import { useEffect, useRef, useState } from 'react';
12
- import { ChevronsUpDown, LogOut, PanelLeftClose, Palette, Shield, UserRound } from 'lucide-react';
12
+ import { Activity, ChevronsUpDown, LogOut, PanelLeftClose, Palette, Shield, UserRound } from 'lucide-react';
13
13
  import { XStack, YStack, Text, Box } from '@hanzo/ui';
14
14
  import { sheet } from '@hanzo/ui/glass';
15
15
  import { useIam } from '@hanzo/iam/react';
@@ -22,7 +22,7 @@ function Row({ icon, onPress, children, }) {
22
22
  return (_jsx(Box, { render: "button", onClick: onPress, width: "100%", px: "$3", py: "$2", rounded: "$2", hoverStyle: { bg: '$hover' }, children: _jsxs(XStack, { items: "center", gap: "$2", children: [icon, _jsx(Text, { fontSize: "$2", color: "$ink", children: children })] }) }));
23
23
  }
24
24
  /** The block at the foot of the sidebar: press it for the menu. */
25
- export function Account({ onToggleCollapse, onProfile, onAppearance, } = {}) {
25
+ export function Account({ onToggleCollapse, onProfile, onAppearance, onUsage, } = {}) {
26
26
  const { user } = useIam();
27
27
  const logout = useSignOut();
28
28
  const { user: account } = useAccount();
@@ -54,5 +54,5 @@ export function Account({ onToggleCollapse, onProfile, onAppearance, } = {}) {
54
54
  // @ts-expect-error — ref is forwarded to the DOM node by the primitive.
55
55
  , {
56
56
  // @ts-expect-error — ref is forwarded to the DOM node by the primitive.
57
- ref: holder, position: "relative", borderTopWidth: 1, borderColor: "$borderColor", children: [open ? (_jsxs(YStack, { role: "menu", "aria-label": "Account", position: "absolute", b: "calc(100% + 6px)", l: "$2", r: "$2", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", ...sheet(2), py: "$2", gap: "$1", z: "var(--z-modal)", children: [_jsx(Row, { icon: _jsx(UserRound, { size: 15, "aria-hidden": true }), onPress: act(() => (onProfile ? onProfile() : window.location.assign(link(`${iam()}/account`)))), children: "Profile" }), onAppearance ? (_jsx(Row, { icon: _jsx(Palette, { size: 15, "aria-hidden": true }), onPress: act(onAppearance), children: "Appearance" })) : null, _jsx(Row, { icon: _jsx(Shield, { size: 15, "aria-hidden": true }), onPress: act(() => window.location.assign(link(`${iam()}/account/security`))), children: "Security" }), _jsx(Box, { height: 1, bg: "$borderColor", my: "$1" }), _jsx(Row, { icon: _jsx(LogOut, { size: 15, "aria-hidden": true }), onPress: act(() => void logout()), children: "Sign out" })] })) : null, _jsxs(XStack, { items: "center", width: "100%", children: [_jsx(Box, { render: "button", onClick: () => setOpen((v) => !v), "aria-expanded": open, "aria-haspopup": "menu", "aria-label": "Account", flex: 1, minW: 0, px: "$3", py: "$2", hoverStyle: { bg: '$hover' }, children: _jsxs(XStack, { items: "center", gap: "$2", minW: 0, children: [_jsx(Face, { src: face, name: called, size: 22 }), _jsxs(YStack, { flex: 1, minW: 0, items: "flex-start", children: [_jsx(Text, { fontSize: "$2", fontWeight: "600", color: "$color", numberOfLines: 1, children: called }), email && email !== called ? (_jsx(Text, { fontSize: "$1", color: "$soft", numberOfLines: 1, children: email })) : null] }), _jsx(ChevronsUpDown, { size: 14, "aria-hidden": true })] }) }), onToggleCollapse ? (_jsx(Box, { render: "button", onClick: onToggleCollapse, "aria-label": "Collapse sidebar", p: "$2", mr: "$1.5", rounded: "$2", hoverStyle: { bg: '$hover' }, cursor: "pointer", children: _jsx(PanelLeftClose, { size: 16, "aria-hidden": true }) })) : null] })] }));
57
+ ref: holder, position: "relative", borderTopWidth: 1, borderColor: "$borderColor", children: [open ? (_jsxs(YStack, { role: "menu", "aria-label": "Account", position: "absolute", b: "calc(100% + 6px)", l: "$2", r: "$2", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", ...sheet(2), py: "$2", gap: "$1", z: "var(--z-modal)", children: [_jsx(Row, { icon: _jsx(UserRound, { size: 15, "aria-hidden": true }), onPress: act(() => (onProfile ? onProfile() : window.location.assign(link(`${iam()}/account`)))), children: "Profile" }), onAppearance ? (_jsx(Row, { icon: _jsx(Palette, { size: 15, "aria-hidden": true }), onPress: act(onAppearance), children: "Appearance" })) : null, onUsage ? (_jsx(Row, { icon: _jsx(Activity, { size: 15, "aria-hidden": true }), onPress: act(onUsage), children: "Usage" })) : null, _jsx(Row, { icon: _jsx(Shield, { size: 15, "aria-hidden": true }), onPress: act(() => window.location.assign(link(`${iam()}/account/security`))), children: "Security" }), _jsx(Box, { height: 1, bg: "$borderColor", my: "$1" }), _jsx(Row, { icon: _jsx(LogOut, { size: 15, "aria-hidden": true }), onPress: act(() => void logout()), children: "Sign out" })] })) : null, _jsxs(XStack, { items: "center", width: "100%", children: [_jsx(Box, { render: "button", onClick: () => setOpen((v) => !v), "aria-expanded": open, "aria-haspopup": "menu", "aria-label": "Account", flex: 1, minW: 0, px: "$3", py: "$2", hoverStyle: { bg: '$hover' }, children: _jsxs(XStack, { items: "center", gap: "$2", minW: 0, children: [_jsx(Face, { src: face, name: called, size: 22 }), _jsxs(YStack, { flex: 1, minW: 0, items: "flex-start", children: [_jsx(Text, { fontSize: "$2", fontWeight: "600", color: "$color", numberOfLines: 1, children: called }), email && email !== called ? (_jsx(Text, { fontSize: "$1", color: "$soft", numberOfLines: 1, children: email })) : null] }), _jsx(ChevronsUpDown, { size: 14, "aria-hidden": true })] }) }), onToggleCollapse ? (_jsx(Box, { render: "button", onClick: onToggleCollapse, "aria-label": "Collapse sidebar", p: "$2", mr: "$1.5", rounded: "$2", hoverStyle: { bg: '$hover' }, cursor: "pointer", children: _jsx(PanelLeftClose, { size: 16, "aria-hidden": true }) })) : null] })] }));
58
58
  }
package/dist/Chat.js CHANGED
@@ -28,7 +28,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
28
28
  // belongs in the thread where the reader can see it, not in a caption that
29
29
  // pre-emptively refuses on the gateway's behalf.
30
30
  import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
31
- import { capped, detail, familyOf, planRequired, plainly, refused } from "./failure.js";
31
+ import { billed, detail, familyOf, planRequired, plainly, refused } from "./failure.js";
32
32
  import { Prose } from "./Prose.js";
33
33
  import { useAgent, useAi, useChat, useModels, } from "@hanzo/ai/react";
34
34
  import { freeCopy, grantConsent, hasConsent, isFree, } from "@hanzo/ai";
@@ -44,9 +44,9 @@ import { Anchor, Box, Button, Text, Tooltip, TooltipContent, TooltipTrigger, Vie
44
44
  import { Control } from "@hanzo/composer";
45
45
  import { useIam, useIamToken } from "@hanzo/iam/react";
46
46
  import { hasSession, org } from "./lib/session.js";
47
- import { chats, clock, sku, useLimits } from "./lib/limits.js";
47
+ import { chats, sku, useLimits } from "./lib/limits.js";
48
+ import { LimitedBanner } from "@hanzo/ui/product/LimitedBanner";
48
49
  import { WEB } from "./lib/web.js";
49
- import { Meters } from "./meters.js";
50
50
  import { useHydrated } from "./lib/hydrated.js";
51
51
  import { base, served } from "./lib/ai.js";
52
52
  import { onServed } from "./lib/served.js";
@@ -63,7 +63,7 @@ import { useStarred } from "./stars.js";
63
63
  import { useModel } from "./model.js";
64
64
  import { Enso, nameOf, useEffort } from "./enso.js";
65
65
  import { checkoutUrl } from './lib/pay.js';
66
- import { onFree, planName } from './lib/plans.js';
66
+ import { onFree } from './lib/plans.js';
67
67
  import { Upgrade, useFree } from './Upgrade.js';
68
68
  import { enter, LOGIN } from './lib/destination.js';
69
69
  import { first } from './lib/first.js';
@@ -527,10 +527,14 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
527
527
  const offered = useMemo(() => models.filter((m) => sku(m.id)), [models]);
528
528
  const wanted = sku(preferred) ? preferred : ENSO;
529
529
  const model = persona?.model || served(models, anonymous ? FREE : wanted);
530
- // WHAT THE PLAN HAS LEFT, as shares of the session, the day and the month
531
- // (lib/hanzo/limits.ts). Read again whenever a send settles, below.
532
- const { limits, reload: reread } = useLimits(!anonymous, org());
530
+ // THE PLAN'S USAGE (lib/limits.ts), kept current by every chat answer's
531
+ // headers and read again whenever a send settles, below. The room draws no
532
+ // bars and no near note — those live on Settings → Usage. It says something
533
+ // only once the reader is turned away: a reply answered from a free model, or
534
+ // a refusal.
535
+ const { limits, notice, reload: reread } = useLimits(!anonymous, org(), (id) => nameOf(catalog, id));
533
536
  const held = Boolean(limits?.plan);
537
+ const pause = notice && (notice.fallback || notice.refused) ? notice : null;
534
538
  // A MODEL THE FREE PLAN DOES NOT INCLUDE IS ASKED FOR, NOT CHOSEN. On Free,
535
539
  // picking a model the plan's row does not run (`onFree`) leaves the choice
536
540
  // where it was and opens the upgrade, titled with the model; so does a turn
@@ -971,12 +975,11 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
971
975
  const whom = addressed(text, room);
972
976
  const wrong = failed?.error ?? null;
973
977
  const how = wrong ? refusalOf(wrong) : null;
974
- // A SPENT WINDOW is the plan holder's own limit, said with its reset and,
975
- // below the top plan, the next plan's checkout. Asking again before the
976
- // reset meets the same answer, so it offers no Try again.
977
- const cap = wrong ? capped(wrong) : null;
978
- const next = cap?.href ? planName(limits?.upgrade) : null;
979
- const resign = !cap && !!wrong && (refused(wrong) || /audience not allowed|invalid access token/i.test(wrong.message));
978
+ // A BILLING REFUSAL is said in the gateway's words; its way past —
979
+ // Upgrade, Add prepaid credit — is the banner over the composer. The free
980
+ // plan's cap answers the same until it resets, so it offers no Try again.
981
+ const bill = wrong ? billed(wrong) : null;
982
+ const resign = !bill && !!wrong && (refused(wrong) || /audience not allowed|invalid access token/i.test(wrong.message));
980
983
  const lift = free ? planRequired(wrong) : null;
981
984
  const said = !failed
982
985
  ? "No answer came back for this question."
@@ -984,8 +987,8 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
984
987
  ? `${nameOf(models, model)} sent nothing for ${STALL / 60_000} minutes, so the turn was stopped.`
985
988
  : failed.empty
986
989
  ? `${nameOf(models, model)} answered with nothing.`
987
- : cap
988
- ? `You've used ${cap.limit === "session" ? "this session's" : "today's"} limit.${cap.resets ? ` Resets at ${clock(cap.resets)}.` : ""}`
990
+ : bill
991
+ ? notice?.message ?? (bill.message || plainly(wrong, "this conversation"))
989
992
  : resign
990
993
  ? anonymous
991
994
  ? "Sign in to ask this."
@@ -993,7 +996,7 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
993
996
  : how?.provider
994
997
  ? `${familyOf(model) || nameOf(models, model)} is unavailable right now.`
995
998
  : plainly(wrong, "this conversation");
996
- return (_jsx(Failure, { onRetry: resign || cap ? undefined : () => speak(text, question.id), children: _jsxs(YStack, { gap: "$2", children: [_jsxs(Text, { fontSize: "$3", color: "$ink", children: [whom.length ? `No answer from ${join(whom)}. ` : "", said] }), how?.provider && wrong ? _jsx(Why, { text: detail(wrong) }) : null, cap?.href || resign || lift || how?.wallet || how?.visitor ? (_jsx(XStack, { gap: "$2", flexWrap: "wrap", children: cap?.href ? (_jsx(Anchor, { href: cap.href, children: _jsx(Button, { size: "sm", variant: "outline", children: next ? `Upgrade to ${next}` : "Upgrade" }) })) : resign ? (_jsx(Button, { size: "sm", onClick: () => {
999
+ return (_jsx(Failure, { onRetry: resign || bill?.code === "free_plan_cap" ? undefined : () => speak(text, question.id), children: _jsxs(YStack, { gap: "$2", children: [_jsxs(Text, { fontSize: "$3", color: "$ink", children: [whom.length ? `No answer from ${join(whom)}. ` : "", said] }), how?.provider && wrong ? _jsx(Why, { text: detail(wrong) }) : null, !bill && (resign || lift || how?.wallet || how?.visitor) ? (_jsx(XStack, { gap: "$2", flexWrap: "wrap", children: resign ? (_jsx(Button, { size: "sm", onClick: () => {
997
1000
  if (typeof window !== "undefined") {
998
1001
  localStorage.removeItem("hanzo_iam_access_token");
999
1002
  localStorage.removeItem("hanzo_iam_refresh_token");
@@ -1194,7 +1197,7 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
1194
1197
  },
1195
1198
  // WHAT GOES WITH THE MESSAGE, above the frame: one chip a file, each
1196
1199
  // with its own remove, and the reason any file was refused.
1197
- head: files.length || turnedAway || preparing || talk.open || talk.refusal ? (_jsxs(YStack, { width: "calc(100% - 32px)", maxW: `calc(${MEASURE} - 3rem)`, mx: "auto", gap: "$1.5", children: [files.length ? (_jsx(XStack, { gap: "$1.5", flexWrap: "wrap", role: "list", "aria-label": "Attached files", "data-slot": "composer-attached", children: files.map((one) => (_jsx(HeldChip, { one: one, onRemove: () => drop(at, one.id) }, one.id))) })) : null, preparing ? (_jsxs(Text, { role: "status", "aria-live": "polite", "data-slot": "composer-preparing", fontSize: "$2", color: "$soft", children: [preparing, "\u2026"] })) : null, turnedAway ? (_jsx(Text, { role: "alert", fontSize: "$2", color: BAD, children: turnedAway })) : null, talk.open || talk.refusal ? (_jsx(Text, { role: "status", "aria-live": "polite", "data-slot": "composer-talk-reply", fontSize: "$2", color: talk.refusal ? BAD : "$soft", children: talk.refusal
1200
+ head: files.length || turnedAway || preparing || talk.open || talk.refusal || pause ? (_jsxs(YStack, { width: "calc(100% - 32px)", maxW: `calc(${MEASURE} - 3rem)`, mx: "auto", gap: "$1.5", children: [pause ? (_jsx(LimitedBanner, { message: pause.message, actions: pause.actions, onUsage: () => showSettings("usage") })) : null, files.length ? (_jsx(XStack, { gap: "$1.5", flexWrap: "wrap", role: "list", "aria-label": "Attached files", "data-slot": "composer-attached", children: files.map((one) => (_jsx(HeldChip, { one: one, onRemove: () => drop(at, one.id) }, one.id))) })) : null, preparing ? (_jsxs(Text, { role: "status", "aria-live": "polite", "data-slot": "composer-preparing", fontSize: "$2", color: "$soft", children: [preparing, "\u2026"] })) : null, turnedAway ? (_jsx(Text, { role: "alert", fontSize: "$2", color: BAD, children: turnedAway })) : null, talk.open || talk.refusal ? (_jsx(Text, { role: "status", "aria-live": "polite", "data-slot": "composer-talk-reply", fontSize: "$2", color: talk.refusal ? BAD : "$soft", children: talk.refusal
1198
1201
  ? REFUSED.lost
1199
1202
  : talk.reply
1200
1203
  ? `Hanzo: ${talk.reply}`
@@ -1206,9 +1209,6 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
1206
1209
  listening: "In conversation — click to hang up",
1207
1210
  speaking: "Hanzo is speaking — talk to interrupt",
1208
1211
  }, children: () => _jsx(AudioLines, { size: 15 }) }) }), _jsx(Control, { type: "button", size: ROUND, fill: true, "data-slot": "composer-send", "aria-label": chat.streaming ? "Stop" : "Send", opacity: chat.streaming || draft.trim() || files.length ? 1 : 0.4, onClick: () => (chat.streaming ? chat.stop() : submit()), children: chat.streaming ? (_jsx(Square, { size: 13, fill: "currentColor" })) : (_jsx(ArrowUp, { size: 15, strokeWidth: 2.5 })) })] })),
1209
- // WHAT THE PLAN HAS LEFT, under the frame and at its measure, for a
1210
- // plan holder only: Free keeps the free lane's own notice.
1211
- foot: held && limits && (limits.session || limits.day || limits.month) ? (_jsx(YStack, { width: "calc(100% - 32px)", maxW: `calc(${MEASURE} - 3rem)`, mx: "auto", children: _jsx(Meters, { limits: limits }) })) : undefined,
1212
1212
  } }), _jsx(Crew, {}), isFree(model) ? (_jsx(XStack, { items: "center", justify: "center", py: "$1", px: "$3", borderTopWidth: 1, borderColor: "$borderColor", bg: "$background", children: _jsxs(Text, { fontSize: "$1", color: "$soft", children: ["Free chat is data-shared to train models. ", _jsx(Anchor, { href: site("/privacy"), fontSize: "$1", color: "$ink", children: "Privacy" })] }) })) : null, consent, _jsx(Upgrade, { ask: ask, onClose: () => setAsk(null) })] }));
1213
1213
  }
1214
1214
  /** The agreement free asks for, in @hanzo/ai's words so every surface says one thing. */
package/dist/Settings.js CHANGED
@@ -39,7 +39,7 @@ import { planName } from './lib/plans.js';
39
39
  import { api } from './lib/api.js';
40
40
  import { renewal, useSubscription, useTier } from './lib/tier.js';
41
41
  import { sku, spendShown, useLimits } from './lib/limits.js';
42
- import { Meters } from './meters.js';
42
+ import { PlanUsage } from '@hanzo/ui/product/PlanUsage';
43
43
  import { Look } from './look.js';
44
44
  import { ProviderMark } from './ProviderMark.js';
45
45
  import { org, pick } from './lib/session.js';
@@ -232,7 +232,8 @@ function Billing() {
232
232
  /**
233
233
  * The one usage panel every Hanzo product renders, reading as this account — for a
234
234
  * reader with no plan. A plan holder's usage is the plan's shares (`meters` draws
235
- * them where usage is the subject), never money (spendShown).
235
+ * them, with the plan's terms and its actions, where usage is the subject), never
236
+ * money (spendShown).
236
237
  */
237
238
  function Usage({ sections, title, meters = false }) {
238
239
  const { accessToken, isAuthenticated } = useIam();
@@ -240,15 +241,15 @@ function Usage({ sections, title, meters = false }) {
240
241
  if (!isAuthenticated || !accessToken) {
241
242
  return _jsx(Text, { fontSize: "$3", color: "$soft", children: "Sign in to see your usage." });
242
243
  }
243
- if (read.limits?.plan)
244
- return meters ? _jsx(Meters, { limits: read.limits, large: true }) : null;
245
- if (!spendShown(read))
246
- return null;
244
+ const plan = meters && read.limits ? _jsx(PlanUsage, { limits: read.limits, plan: planName(read.limits.plan) || undefined, notice: read.notice }) : null;
245
+ if (read.limits?.plan || !spendShown(read))
246
+ return plan;
247
247
  // The address is read HERE rather than at import: the panel sends the bearer
248
248
  // with every read, and a credentialed read to an absolute address from a page
249
249
  // the gateway does not admit dies in preflight — which drew this panel as an
250
250
  // account that had spent nothing.
251
- return _jsx(UsagePanel, { baseUrl: api(), token: accessToken, sections: sections, title: title, upgrade: payPage() });
251
+ const spend = _jsx(UsagePanel, { baseUrl: api(), token: accessToken, sections: sections, title: title, upgrade: payPage() });
252
+ return plan ? (_jsxs(YStack, { gap: "$6", children: [plan, spend] })) : (spend);
252
253
  }
253
254
  /** Keys are minted and revoked through the console, which holds the one issuer. */
254
255
  function Keys() {
package/dist/Shell.js CHANGED
@@ -1221,7 +1221,7 @@ function Frame({ mode, children, navigate, back, forward, }) {
1221
1221
  placeholder: "Filter", "aria-label": "Filter sidebar" }), flex: 1, minW: 0, bg: "transparent", borderWidth: 0, outlineStyle: "none", color: "inherit", fontSize: "$2" })] }) }), settingsPane ? (_jsx(SidebarScroll, { children: _jsx(SettingsList, { active: settingsPane, filter: filter, onPick: pick, onBack: () => showSettings(null) }) })) : (_jsx(_Fragment, { children: _jsx(SidebarScroll, { children: _jsxs(_Fragment, { children: [settled ? (_jsx(Index, { mode: mode, owner: owner, filter: filter, onPick: onPick, navigate: navigate })) : null, products.length ? (_jsx(SidebarFolder, { name: "Cloud & Platform", open: filter ? true : undefined, children: products.map((prod) => (_jsx(SidebarItem, { icon: _jsx(prod.icon, { size: 13, "aria-hidden": true }), onPress: () => window.open(prod.href, "_blank", "noopener"), children: prod.label }, prod.id))) })) : null, more.length ? (_jsx(SidebarFolder, { name: "More", open: filter ? true : undefined, children: more.map((tool) => (_jsx(SidebarItem, { icon: _jsx(tool.icon, { size: 13, "aria-hidden": true }), onPress: tool.go, children: tool.label }, tool.id))) })) : null] }) }) })), settled && onApex ? _jsx(Leave, { href: signedIn ? ENTRY : WORKSPACE, children: "Workspace" }) : null, settled ? (signedIn ? (_jsxs(_Fragment, { children: [superAdmin() ? (_jsx(SidebarItem, { icon: _jsx(Building2, { size: 15, "aria-hidden": true }), onPress: () => setSupporting(true), children: "All organizations" })) : null, _jsx(SidebarItem, { icon: _jsx(UserPlus, { size: 15, "aria-hidden": true }), onPress: () => window.open(invite(), "_blank", "noopener"), children: "Invite Teammates" }), _jsx(SidebarItem, { icon: _jsx(Share2, { size: 15, "aria-hidden": true }), onPress: () => {
1222
1222
  navigate?.(site("/referral"));
1223
1223
  dismiss();
1224
- }, children: "Share Hanzo" }), _jsx(SidebarItem, { icon: _jsx(Sparkles, { size: 15, "aria-hidden": true }), onPress: () => window.open(BILLING, "_blank", "noopener"), children: "Upgrade" }), _jsx(Account, { onToggleCollapse: toggle, onProfile: () => setProfiling(true), onAppearance: () => showSettings("general") })] })) : null) : null] }));
1224
+ }, children: "Share Hanzo" }), _jsx(SidebarItem, { icon: _jsx(Sparkles, { size: 15, "aria-hidden": true }), onPress: () => window.open(BILLING, "_blank", "noopener"), children: "Upgrade" }), _jsx(Account, { onToggleCollapse: toggle, onProfile: () => setProfiling(true), onAppearance: () => showSettings("general"), onUsage: () => showSettings("usage") })] })) : null) : null] }));
1225
1225
  return (_jsxs(YStack, { "data-slot": "workspace", "data-hydrated": settled ? "true" : undefined, "data-sidebar": asked ? "open" : "shut", display: "grid", gridTemplateColumns: `${asked ? 0 : 64}px auto minmax(0, 1fr) auto`, gridTemplateRows: "44px minmax(0, 1fr)", gridTemplateAreas: '"rail bar bar bar" "rail list pane side"', "$max-md": {
1226
1226
  gridTemplateColumns: "minmax(0, 1fr)",
1227
1227
  gridTemplateRows: `44px minmax(0, 1fr) ${TABS_HEIGHT}`,
package/dist/failure.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { type Refusal } from '@hanzo/ui/product/limits';
1
2
  /** Whether the store answered and the answer was no — exported so a surface can
2
3
  * offer the way past a refusal rather than only describing it. */
3
4
  export declare const refused: (error: unknown) => boolean;
@@ -33,24 +34,12 @@ export declare function plainly(error: unknown, subject: string): string;
33
34
  export declare function planRequired(error: unknown): {
34
35
  href?: string;
35
36
  } | null;
36
- /** A plan holder's spent window. */
37
- export interface Cap {
38
- limit: 'session' | 'day';
39
- /** When it opens again, RFC 3339. */
40
- resets: string | null;
41
- /** The next plan's checkout, on hanzo.ai only. Absent at the top plan. */
42
- href?: string;
43
- }
44
37
  /**
45
- * Whether a refusal is a plan's usage limit, and which window.
46
- *
47
- * A paid plan is a subscription with limits: a spent session or day comes back
48
- * 429 `usage_cap_exceeded` naming the window, its reset and, below the top
49
- * plan, an `upgrade_url`. The month refuses nothing — Enso and Zen keep
50
- * answering — so no other window is a cap. The code decides, never the
51
- * sentence, and the address is kept by the rule `planRequired` keeps.
38
+ * A billing refusal in the gateway's envelope — the plan's included usage
39
+ * spent, a paid plan required, the free plan's cap, or no balance — or null for
40
+ * any other failure. The code decides, never the sentence.
52
41
  */
53
- export declare function capped(error: unknown): Cap | null;
42
+ export declare function billed(error: unknown): Refusal | null;
54
43
  /** The family a model id belongs to, as a reader knows it, or '' for one outside them. */
55
44
  export declare const familyOf: (id: string) => string;
56
45
  /** What the server said about a failure, for the detail folded under the plain line. */
package/dist/failure.js CHANGED
@@ -9,6 +9,7 @@
9
9
  //
10
10
  // ONE FUNCTION, because every room owes the same three answers and three copies
11
11
  // would drift into three vocabularies for one event.
12
+ import { refusalOf } from '@hanzo/ui/product/limits';
12
13
  import { told } from './lib/reach.js';
13
14
  /**
14
15
  * THE STATUS, WHERE THE FAILURE CARRIES ONE.
@@ -109,21 +110,15 @@ export function planRequired(error) {
109
110
  return { href: ours(said?.upgrade_url) };
110
111
  }
111
112
  /**
112
- * Whether a refusal is a plan's usage limit, and which window.
113
- *
114
- * A paid plan is a subscription with limits: a spent session or day comes back
115
- * 429 `usage_cap_exceeded` naming the window, its reset and, below the top
116
- * plan, an `upgrade_url`. The month refuses nothing — Enso and Zen keep
117
- * answering — so no other window is a cap. The code decides, never the
118
- * sentence, and the address is kept by the rule `planRequired` keeps.
113
+ * A billing refusal in the gateway's envelope — the plan's included usage
114
+ * spent, a paid plan required, the free plan's cap, or no balance — or null for
115
+ * any other failure. The code decides, never the sentence.
119
116
  */
120
- export function capped(error) {
121
- const said = error?.body?.error;
122
- if (said?.code !== 'usage_cap_exceeded' || status(error) !== 429)
123
- return null;
124
- if (said.limit !== 'session' && said.limit !== 'day')
117
+ export function billed(error) {
118
+ const code = status(error);
119
+ if (code !== 402 && code !== 429)
125
120
  return null;
126
- return { limit: said.limit, resets: typeof said.resets_at === 'string' ? said.resets_at : null, href: ours(said.upgrade_url) };
121
+ return refusalOf(error?.body, code);
127
122
  }
128
123
  function ours(href) {
129
124
  if (typeof href !== 'string')
@@ -1,50 +1,25 @@
1
- /** One window: the percent of it used, and when it starts over. */
2
- export interface Span {
3
- /** Used, 0–100. */
4
- percent: number;
5
- /** RFC 3339, or null when no window is running — a session starts at the next request. */
6
- resets_at: string | null;
7
- }
8
- export interface Limits {
9
- /** The plan slug ("max-20x"), or "" for a caller with no plan. */
10
- plan: string;
11
- period_start?: string;
12
- period_end?: string;
13
- session?: Span;
14
- day?: Span;
15
- month?: Span;
16
- /** The next plan's slug, "" at the top of the ladder. */
17
- upgrade?: string;
18
- }
19
1
  /**
20
- * The answer, checked field by field: it is a network response. A body without
21
- * a string `plan` is not an answer at all; a window without a numeric percent
22
- * is left out rather than drawn as full or empty.
2
+ * The plan's usage, as @hanzo/ui holds it: `GET /v1/ai/limits` read as whoever
3
+ * holds the bearer in this browser's organization, kept current by every AI
4
+ * call `observed` (lib/served.ts) hands to `observe`. Shares only.
5
+ *
6
+ * Same base URL and headers as every billing read (lib/hanzo/tier.ts): the
7
+ * bearer is the subject and `X-Org-Id` the organization this browser works in.
23
8
  */
24
- export declare function limitsOf(body: unknown): Limits | null;
9
+ import { type UseLimits } from '@hanzo/ui/product/useLimits';
25
10
  /** Reads the limits as whoever holds the bearer, in this browser's organization. */
26
- export declare function readLimits(signal?: AbortSignal): Promise<Limits>;
11
+ export declare function readLimits(signal: AbortSignal): Promise<unknown>;
27
12
  /**
28
- * The limits, held, and the way to read them again. `limits` is `null` until
29
- * answered, and after a refusal: a reader whose limits are unknown is drawn no
30
- * bars. `answered` says the first read has come back either way, so a surface
31
- * that shows something only to a reader with no plan waits for it.
13
+ * The limits for a signed-in reader, in organization `org`; `name` turns a
14
+ * fallback model id into the name the reader knows. Signed out reads nothing,
15
+ * and still hears a refusal from a served call.
32
16
  */
33
- export declare function useLimits(enabled?: boolean, org?: string | null): {
34
- limits: Limits | null;
35
- answered: boolean;
36
- reload: () => void;
37
- };
17
+ export declare const useLimits: (enabled?: boolean, org?: string | null, name?: (id: string) => string) => UseLimits;
38
18
  /**
39
19
  * Whether a reader's dollar spend may be shown: their limits came back and name no
40
20
  * plan, or could not be read. A plan holder sees usage as shares, never money.
41
21
  */
42
- export declare const spendShown: ({ limits, answered }: {
43
- limits: Limits | null;
44
- answered: boolean;
45
- }) => boolean;
46
- /** What is left of a window, 0–100: the bar goes down as it is used. */
47
- export declare const left: (s: Span) => number;
22
+ export declare const spendShown: ({ limits, answered }: Pick<UseLimits, 'limits' | 'answered'>) => boolean;
48
23
  /** Whether a model is a Hanzo SKU — an Enso or Zen id — the only models a consumer surface offers. */
49
24
  export declare const sku: (id: string) => boolean;
50
25
  /**
@@ -56,9 +31,3 @@ export declare const chats: (model: {
56
31
  id: string;
57
32
  outputs?: unknown;
58
33
  }) => boolean;
59
- /** A reset as a clock reads it in this browser — "17:00" — or '' for none. */
60
- export declare function clock(iso: string | null | undefined): string;
61
- /** A period's end as a sentence states it — "Oct 30, 2026" — or '' for none. Periods end on UTC days. */
62
- export declare function date(iso: string | null | undefined): string;
63
- /** A period's end without its year — "Oct 30" — for the month bar. */
64
- export declare function day(iso: string | null | undefined): string;
@@ -1,105 +1,33 @@
1
1
  'use client';
2
2
  /**
3
- * WHAT A PLAN HOLDER HAS LEFT, AS A SHARE.
4
- *
5
- * A paid plan is a subscription with usage limits, never a debit from a
6
- * balance. `GET /v1/ai/limits` answers the acting org's plan and three
7
- * windows — the five-hour session, the day, and the billing month — each as
8
- * the percent USED with the moment it resets, and the next plan up. Shares
9
- * only: nothing here is a count of requests or an amount of money.
10
- *
11
- * The month is the plan's included usage for the period. Spending it refuses
12
- * nothing — Enso and Zen keep answering from free models — so it is a line
13
- * under the bars, never a refusal. The session and the day refuse when spent.
14
- *
15
- * `plan` "" is a caller with no plan (Free), and then nothing else is sent:
16
- * that reader keeps the Free display. A read that fails is `null`, which is not
17
- * knowing, and nothing is drawn from it.
3
+ * The plan's usage, as @hanzo/ui holds it: `GET /v1/ai/limits` read as whoever
4
+ * holds the bearer in this browser's organization, kept current by every AI
5
+ * call `observed` (lib/served.ts) hands to `observe`. Shares only.
18
6
  *
19
7
  * Same base URL and headers as every billing read (lib/hanzo/tier.ts): the
20
8
  * bearer is the subject and `X-Org-Id` the organization this browser works in.
21
9
  */
22
- import { useCallback, useEffect, useState } from 'react';
10
+ import { useLimits as useHeld } from '@hanzo/ui/product/useLimits';
23
11
  import { api } from './api.js';
24
12
  import { scope } from './session.js';
25
- const text = (v) => (typeof v === 'string' ? v : null);
26
- function span(v) {
27
- const o = v;
28
- const p = o?.percent;
29
- if (typeof p !== 'number' || !Number.isFinite(p))
30
- return undefined;
31
- return { percent: Math.min(100, Math.max(0, Math.round(p))), resets_at: text(o?.resets_at) };
32
- }
33
- /**
34
- * The answer, checked field by field: it is a network response. A body without
35
- * a string `plan` is not an answer at all; a window without a numeric percent
36
- * is left out rather than drawn as full or empty.
37
- */
38
- export function limitsOf(body) {
39
- const o = body;
40
- const plan = text(o?.plan);
41
- if (plan === null)
42
- return null;
43
- if (plan === '')
44
- return { plan };
45
- return {
46
- plan,
47
- period_start: text(o?.period_start) ?? undefined,
48
- period_end: text(o?.period_end) ?? undefined,
49
- session: span(o?.session),
50
- day: span(o?.day),
51
- month: span(o?.month),
52
- upgrade: text(o?.upgrade) ?? '',
53
- };
54
- }
55
13
  /** Reads the limits as whoever holds the bearer, in this browser's organization. */
56
14
  export async function readLimits(signal) {
57
15
  const res = await fetch(`${api()}/v1/ai/limits`, { headers: scope(), signal });
58
16
  if (!res.ok)
59
17
  throw Object.assign(new Error(`Limits answered ${res.status}`), { status: res.status });
60
- const got = limitsOf(await res.json());
61
- if (!got)
62
- throw Object.assign(new Error('Limits answered without a plan'), { status: 502 });
63
- return got;
18
+ return res.json();
64
19
  }
65
20
  /**
66
- * The limits, held, and the way to read them again. `limits` is `null` until
67
- * answered, and after a refusal: a reader whose limits are unknown is drawn no
68
- * bars. `answered` says the first read has come back either way, so a surface
69
- * that shows something only to a reader with no plan waits for it.
21
+ * The limits for a signed-in reader, in organization `org`; `name` turns a
22
+ * fallback model id into the name the reader knows. Signed out reads nothing,
23
+ * and still hears a refusal from a served call.
70
24
  */
71
- export function useLimits(enabled = true, org = null) {
72
- const [limits, setLimits] = useState(null);
73
- const [answered, setAnswered] = useState(false);
74
- const [nonce, setNonce] = useState(0);
75
- useEffect(() => {
76
- if (!enabled)
77
- return;
78
- const stop = new AbortController();
79
- readLimits(stop.signal)
80
- .then((got) => {
81
- if (!stop.signal.aborted)
82
- setLimits(got);
83
- })
84
- .catch(() => {
85
- if (!stop.signal.aborted)
86
- setLimits(null);
87
- })
88
- .finally(() => {
89
- if (!stop.signal.aborted)
90
- setAnswered(true);
91
- });
92
- return () => stop.abort();
93
- }, [enabled, nonce, org]);
94
- return { limits, answered, reload: useCallback(() => setNonce((n) => n + 1), []) };
95
- }
25
+ export const useLimits = (enabled = true, org = null, name) => useHeld(enabled ? readLimits : null, org, name);
96
26
  /**
97
27
  * Whether a reader's dollar spend may be shown: their limits came back and name no
98
28
  * plan, or could not be read. A plan holder sees usage as shares, never money.
99
29
  */
100
30
  export const spendShown = ({ limits, answered }) => answered && !limits?.plan;
101
- /** What is left of a window, 0–100: the bar goes down as it is used. */
102
- export const left = (s) => 100 - s.percent;
103
31
  /** Whether a model is a Hanzo SKU — an Enso or Zen id — the only models a consumer surface offers. */
104
32
  export const sku = (id) => /^(hanzo\/)?(enso|zen)/i.test(id);
105
33
  /**
@@ -108,18 +36,3 @@ export const sku = (id) => /^(hanzo\/)?(enso|zen)/i.test(id);
108
36
  * Zen ids the catalog lists, and neither answers a question.
109
37
  */
110
38
  export const chats = (model) => !Array.isArray(model.outputs) || model.outputs.includes('text');
111
- /** A reset as a clock reads it in this browser — "17:00" — or '' for none. */
112
- export function clock(iso) {
113
- const t = iso ? Date.parse(iso) : NaN;
114
- return Number.isNaN(t) ? '' : new Date(t).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hourCycle: 'h23' });
115
- }
116
- /** A period's end as a sentence states it — "Oct 30, 2026" — or '' for none. Periods end on UTC days. */
117
- export function date(iso) {
118
- const t = iso ? Date.parse(iso) : NaN;
119
- return Number.isNaN(t) ? '' : new Date(t).toLocaleDateString('en-US', { timeZone: 'UTC', year: 'numeric', month: 'short', day: 'numeric' });
120
- }
121
- /** A period's end without its year — "Oct 30" — for the month bar. */
122
- export function day(iso) {
123
- const t = iso ? Date.parse(iso) : NaN;
124
- return Number.isNaN(t) ? '' : new Date(t).toLocaleDateString('en-US', { timeZone: 'UTC', month: 'short', day: 'numeric' });
125
- }
@@ -7,7 +7,8 @@
7
7
  * neither, so the site's client is built on `observed(fetch)`, which reads
8
8
  * them off each completion as it passes and tells the listeners — the header
9
9
  * when the gateway exposes it, else the first frame that names a model. Every
10
- * byte is passed through untouched.
10
+ * byte is passed through untouched. Each chat answer's usage headers, and a
11
+ * billing refusal's envelope, go to @hanzo/ui's `observe`.
11
12
  */
12
13
  /** A completion's asked and answering models. */
13
14
  export interface Served {
@@ -7,8 +7,10 @@
7
7
  * neither, so the site's client is built on `observed(fetch)`, which reads
8
8
  * them off each completion as it passes and tells the listeners — the header
9
9
  * when the gateway exposes it, else the first frame that names a model. Every
10
- * byte is passed through untouched.
10
+ * byte is passed through untouched. Each chat answer's usage headers, and a
11
+ * billing refusal's envelope, go to @hanzo/ui's `observe`.
11
12
  */
13
+ import { observe } from '@hanzo/ui/product/useLimits';
12
14
  const listeners = new Set();
13
15
  /** Hears every served completion from now on; answers the unsubscribe. */
14
16
  export function onServed(listener) {
@@ -23,11 +25,17 @@ const tell = (heard) => {
23
25
  };
24
26
  /** The completion route, whatever host the client was built for. */
25
27
  const COMPLETION = /\/v1\/chat\/completions(?:[?#]|$)/;
28
+ /** Every chat route whose answer carries the plan's usage headers or a billing refusal. */
29
+ const CHAT = /\/v1\/chat\/(?:completions|public)(?:[?#]|$)/;
26
30
  /** A fetch that tells `onServed` listeners which model answered each completion. */
27
31
  export function observed(base) {
28
32
  return async (input, init) => {
29
33
  const res = await base(input, init);
30
34
  const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;
35
+ // The plan's usage rides every chat answer and every refusal: the limits
36
+ // hear both before anything else reads the body.
37
+ if (CHAT.test(url))
38
+ observe(res);
31
39
  if (!COMPLETION.test(url) || !res.ok)
32
40
  return res;
33
41
  let asked = '';
package/dist/lib/tier.js CHANGED
@@ -26,7 +26,11 @@ import { api } from './api.js';
26
26
  import { bearer, org, orgs, scope } from './session.js';
27
27
  import { list } from './list.js';
28
28
  import { money } from './pay.js';
29
- import { date } from './limits.js';
29
+ /** A period's end as a sentence states it — "Oct 30, 2026" — or '' for none. Periods end on UTC days. */
30
+ function date(iso) {
31
+ const t = iso ? Date.parse(iso) : NaN;
32
+ return Number.isNaN(t) ? '' : new Date(t).toLocaleDateString('en-US', { timeZone: 'UTC', year: 'numeric', month: 'short', day: 'numeric' });
33
+ }
30
34
  /** An error that names the status, for `say()` to turn into a sentence. */
31
35
  const refusal = (status, message) => Object.assign(new Error(message), { status });
32
36
  /** GETs `path` as whoever holds the bearer, in this browser's organization. */
package/dist/lib/web.d.ts CHANGED
@@ -26,7 +26,8 @@
26
26
  *
27
27
  * Bounded: at most ROUNDS rounds of lookups the model asks for, CALLS lookups in
28
28
  * a round, and every lookup on its own clock. The request after the last round
29
- * offers no tools, so the turn ends in words.
29
+ * offers no tools, so the turn ends in words; a reply of no words is asked for
30
+ * once more.
30
31
  *
31
32
  * A page is read only when the conversation or one of this turn's searches
32
33
  * named it. Page text is written by strangers and can tell a model to fetch an
package/dist/lib/web.js CHANGED
@@ -26,7 +26,8 @@
26
26
  *
27
27
  * Bounded: at most ROUNDS rounds of lookups the model asks for, CALLS lookups in
28
28
  * a round, and every lookup on its own clock. The request after the last round
29
- * offers no tools, so the turn ends in words.
29
+ * offers no tools, so the turn ends in words; a reply of no words is asked for
30
+ * once more.
30
31
  *
31
32
  * A page is read only when the conversation or one of this turn's searches
32
33
  * named it. Page text is written by strangers and can tell a model to fetch an
@@ -200,6 +201,8 @@ function rule(now) {
200
201
  }
201
202
  /** What the model is told when its rounds are spent. */
202
203
  const SPENT = 'You have used every web lookup this turn allows. Answer the person now, in words, from what you have found, citing each fact inline as a Markdown link [page title](url) to the page it came from.';
204
+ /** What a model that answered with nothing is told, once. */
205
+ const SILENT = 'Your last reply was empty. Answer the person now, in words, from what the lookups found, citing each fact inline as a Markdown link [page title](url).';
203
206
  /** A search that could not run, said so the model says it plainly. */
204
207
  const unavailable = (why) => `Live web search is unavailable right now (${why instanceof Error ? why.message : String(why)}). ` +
205
208
  'Tell the person in one plain sentence that you could not search the web just now, then answer from what you know and say it may be out of date.';
@@ -418,10 +421,22 @@ export function researched(base, clock = () => new Date()) {
418
421
  async start(out) {
419
422
  try {
420
423
  let res = first;
424
+ let nudged = false;
421
425
  for (let round = 0;; round++) {
422
426
  const hop = await relay(res, out);
423
- if (!hop.calls.length)
424
- break;
427
+ if (!hop.calls.length) {
428
+ // AN ANSWER OF NO WORDS after a reasoning model has read a page of
429
+ // results: asked once more, without tools, it answers.
430
+ if (hop.content.trim() || nudged)
431
+ break;
432
+ nudged = true;
433
+ res = await ask([...messages, { role: 'system', content: SILENT }], true);
434
+ if (!streams(res)) {
435
+ out.enqueue(event({ error: { message: await refusal(res) } }));
436
+ break;
437
+ }
438
+ continue;
439
+ }
425
440
  const results = await Promise.all(hop.calls.map((tc, i) => i < CALLS ? run(call, tc, words, now, seen) : Promise.resolve(`skipped: at most ${CALLS} lookups run in one step.`)));
426
441
  messages = [
427
442
  ...messages,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzo/rooms",
3
- "version": "0.1.20",
3
+ "version": "0.1.22",
4
4
  "description": "The workspace rooms — Home, Chat, Inbox, Contacts, Meet, Cal, Drive, Board, Work, Bots — and the frame that holds them, once, for every Hanzo surface.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -56,8 +56,8 @@
56
56
  "@hanzo/iam": ">=0.21.28",
57
57
  "@hanzo/logo": ">=1.0.26",
58
58
  "@hanzo/plans": ">=1.8.9",
59
- "@hanzo/ui": ">=8.27.51",
60
- "@hanzo/usage": ">=0.1.17",
59
+ "@hanzo/ui": ">=8.27.57",
60
+ "@hanzo/usage": ">=0.1.18",
61
61
  "@hanzo/voice": ">=0.1.12",
62
62
  "@hanzogui/shell": "^8.3.20",
63
63
  "lucide-react": ">=1.42.0",
@@ -75,8 +75,8 @@
75
75
  "@hanzo/iam": "0.21.28",
76
76
  "@hanzo/logo": "1.0.26",
77
77
  "@hanzo/plans": "1.8.9",
78
- "@hanzo/ui": "8.27.51",
79
- "@hanzo/usage": "0.1.17",
78
+ "@hanzo/ui": "8.27.57",
79
+ "@hanzo/usage": "0.1.18",
80
80
  "@hanzo/voice": "0.1.12",
81
81
  "@hanzogui/config": "8.3.5",
82
82
  "@hanzogui/core": "8.3.5",
package/dist/Usage.d.ts DELETED
@@ -1 +0,0 @@
1
- export declare function Usage(): import("react").JSX.Element;
package/dist/Usage.js DELETED
@@ -1,21 +0,0 @@
1
- 'use client';
2
- import { jsxs as _jsxs } from "react/jsx-runtime";
3
- // Free's usage, said where it is spent. Free is limited, and what it spends
4
- // comes from one pool every free user shares, so the line says both, and what
5
- // is left of today's when billing states a day window for the plan (the tier's
6
- // `windows`, lib/hanzo/tier.ts). It renders nothing for any other plan, or
7
- // while the plan is unread.
8
- import { useIam } from '@hanzo/iam/react';
9
- import { Text } from '@hanzo/ui';
10
- import { org } from './lib/session.js';
11
- import { useTier } from './lib/tier.js';
12
- /** The free rung's name in the billing catalog. */
13
- const FREE = 'free';
14
- export function Usage() {
15
- const { isAuthenticated } = useIam();
16
- const { tier } = useTier(isAuthenticated, org());
17
- if (tier?.tier?.name !== FREE)
18
- return null;
19
- const today = tier.windows?.find((w) => /day/i.test(w.span));
20
- return (_jsxs(Text, { role: "status", fontSize: "$1", color: "$soft", text: "center", children: ["Free \u00B7 limited, shared pool", today ? ` · ${today.remaining} of ${today.limit} left today` : ''] }));
21
- }
package/dist/meters.d.ts DELETED
@@ -1,8 +0,0 @@
1
- import { type Limits } from './lib/limits.js';
2
- /** What a spent month says. */
3
- export declare const SPENT = "Monthly included usage is used; Enso and Zen keep answering.";
4
- /** The plan holder's windows, or nothing where the limits name none. */
5
- export declare function Meters({ limits, large }: {
6
- limits: Limits;
7
- large?: boolean;
8
- }): import("react").JSX.Element;
package/dist/meters.js DELETED
@@ -1,31 +0,0 @@
1
- 'use client';
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- // A plan's windows as bars of what is LEFT: the five-hour session, the day and
4
- // the billing month, each with when it starts over. Shares only — a plan is a
5
- // subscription with limits, never a balance, so no count and no amount is
6
- // drawn. The month spent refuses nothing (Enso and Zen keep answering), which
7
- // is why it is a quiet line under the bars rather than a warning.
8
- //
9
- // One component for the composer's foot and for the usage page: `large` is the
10
- // page's size. Each bar is drawn above its words so the three bars stay on one
11
- // line however the words under them wrap on a phone.
12
- import { Progress, Text, XStack, YStack } from '@hanzo/ui';
13
- import { clock, day, left } from './lib/limits.js';
14
- /** What a spent month says. */
15
- export const SPENT = 'Monthly included usage is used; Enso and Zen keep answering.';
16
- const WINDOWS = [
17
- ['session', 'Session', (s) => (s.resets_at ? `resets ${clock(s.resets_at)}` : '')],
18
- ['day', 'Today', (s) => (s.resets_at ? `resets ${clock(s.resets_at)}` : '')],
19
- ['month', 'This month', (s) => (s.resets_at ? `renews ${day(s.resets_at)}` : '')],
20
- ];
21
- /** The plan holder's windows, or nothing where the limits name none. */
22
- export function Meters({ limits, large = false }) {
23
- const shown = WINDOWS.flatMap(([key, name, when]) => {
24
- const span = limits[key];
25
- return span ? [{ name, span, when: when(span) }] : [];
26
- });
27
- if (!shown.length)
28
- return null;
29
- const size = large ? '$3' : '$1';
30
- return (_jsxs(YStack, { gap: large ? '$4' : '$1.5', width: "100%", minW: 0, children: [_jsx(XStack, { gap: large ? '$6' : '$3', rowGap: "$4", width: "100%", flexWrap: large ? 'wrap' : 'nowrap', children: shown.map(({ name, span, when }) => (_jsxs(YStack, { flex: 1, minW: large ? 200 : 0, gap: large ? '$2' : '$1', children: [_jsx(Progress, { value: left(span), height: large ? 8 : 4, minW: 0, "aria-label": name }), _jsxs(XStack, { flexWrap: "wrap", columnGap: "$1.5", items: "baseline", minW: 0, children: [_jsx(Text, { fontSize: size, color: large ? '$ink' : '$soft', numberOfLines: 1, children: name }), _jsxs(XStack, { children: [_jsxs(Text, { fontSize: size, color: "$ink", fontWeight: "500", fontVariant: ['tabular-nums'], children: [left(span), "%"] }), _jsx(Text, { fontSize: size, color: "$ink", display: large ? 'flex' : 'none', $sm: { display: 'flex' }, children: "\u00A0left" })] }), when ? (_jsx(Text, { fontSize: large ? '$2' : '$1', color: "$soft", numberOfLines: 1, children: when })) : null] })] }, name))) }), limits.month && limits.month.percent >= 100 ? (_jsx(Text, { fontSize: large ? '$2' : '$1', color: "$soft", children: SPENT })) : null] }));
31
- }