@hanzo/rooms 0.1.21 → 0.1.23
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 +3 -1
- package/dist/Account.js +4 -4
- package/dist/Chat.js +20 -20
- package/dist/Column.js +3 -4
- package/dist/Profile.js +2 -2
- package/dist/Settings.js +8 -7
- package/dist/Shell.js +1 -1
- package/dist/failure.d.ts +5 -16
- package/dist/failure.js +8 -13
- package/dist/lib/limits.d.ts +13 -44
- package/dist/lib/limits.js +9 -96
- package/dist/lib/served.d.ts +2 -1
- package/dist/lib/served.js +9 -1
- package/dist/lib/tier.js +5 -1
- package/package.json +5 -5
- package/dist/Guide.d.ts +0 -6
- package/dist/Guide.js +0 -273
- package/dist/Scene.d.ts +0 -12
- package/dist/Scene.js +0 -105
- package/dist/Usage.d.ts +0 -1
- package/dist/Usage.js +0 -21
- package/dist/meters.d.ts +0 -8
- package/dist/meters.js +0 -31
- package/dist/tours.d.ts +0 -24
- package/dist/tours.js +0 -319
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 {
|
|
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,
|
|
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
|
|
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
|
-
//
|
|
531
|
-
//
|
|
532
|
-
|
|
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
|
|
975
|
-
//
|
|
976
|
-
//
|
|
977
|
-
const
|
|
978
|
-
const
|
|
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
|
-
:
|
|
988
|
-
?
|
|
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 ||
|
|
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/Column.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
// The app's one column, the same in both modes: the Chat / Dev switch, a filter,
|
|
4
4
|
// New, Dev's places, the Recents of the mode it is in, and at the foot the
|
|
5
|
-
//
|
|
5
|
+
// account (components/workspace/Me.tsx).
|
|
6
6
|
//
|
|
7
7
|
// It is a pane on the frame's ground (app/_web.tsx), sized by the edge in the
|
|
8
8
|
// gutter to its right: dragged, keyed or double-clicked back (@hanzo/build's
|
|
@@ -11,7 +11,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
11
11
|
// choice outlives the move.
|
|
12
12
|
import { Text, XStack, YStack } from '@hanzo/gui';
|
|
13
13
|
import { ChevronDown, ChevronUp, PanelLeft, Plus, Search } from 'lucide-react';
|
|
14
|
-
import { Grip, nav,
|
|
14
|
+
import { Grip, nav, route } from '@hanzo/build';
|
|
15
15
|
import { Sidebar, SidebarIconButton, SidebarItem } from '@hanzo/ui/chat';
|
|
16
16
|
import { HanzoMark } from '@hanzogui/shell';
|
|
17
17
|
import { useState } from 'react';
|
|
@@ -54,7 +54,6 @@ const ROW = { rounded: '$4', minH: 34 };
|
|
|
54
54
|
export function Column({ host, mode, thread, onChat, onDev, go, drawer = false, }) {
|
|
55
55
|
const [kept, setCollapsed] = useKept('hanzo.build.rail', false);
|
|
56
56
|
const collapsed = kept && !drawer;
|
|
57
|
-
const [slack, setSlack] = useKept('hanzo.build.slack', false);
|
|
58
57
|
const [unfolded, setUnfolded] = useState(false);
|
|
59
58
|
const [words, setWords] = useState('');
|
|
60
59
|
const span = useSpan(SPAN, WIDTH, FLOOR, CEIL);
|
|
@@ -83,7 +82,7 @@ export function Column({ host, mode, thread, onChat, onDev, go, drawer = false,
|
|
|
83
82
|
setWords('');
|
|
84
83
|
}, placeholder: "Filter", "aria-label": "Filter recents" }), flex: 1, minW: 0, bg: "transparent", borderWidth: 0, outlineStyle: "none", color: "inherit", fontSize: "$2" })] }), _jsxs(YStack, { role: "list", mt: "$2.5", gap: 2, shrink: 0, children: [_jsx(YStack, { role: "listitem", children: _jsx(SidebarItem, { "data-slot": "app-new", ...ROW, active: fresh, icon: _jsx(Plus, { size: 16, "aria-hidden": true }), onPress: create, ...keys(create), children: mode === 'chat' ? 'New chat' : 'New run' }) }), mode === 'dev'
|
|
85
84
|
? links.map((link) => (_jsx(YStack, { role: "listitem", children: _jsx(SidebarItem, { ...ROW, active: link.active, icon: link.icon, onPress: link.onPress, ...keys(() => link.onPress?.()), children: link.label }) }, link.id)))
|
|
86
|
-
: null, mode === 'dev' ? (_jsxs(YStack, { role: "listitem", children: [_jsx(SidebarItem, { ...ROW, "aria-expanded": unfolded, icon: unfolded ? _jsx(ChevronUp, { size: 16, "aria-hidden": true }) : _jsx(ChevronDown, { size: 16, "aria-hidden": true }), onPress: () => setUnfolded((v) => !v), ...keys(() => setUnfolded((v) => !v)), children: "More" }), unfolded ? (_jsx(YStack, { role: "list", gap: 2, children: more.map((link) => (_jsx(YStack, { role: "listitem", children: _jsx(SidebarItem, { ...ROW, active: link.active, icon: link.icon, onPress: link.onPress, ...keys(() => link.onPress?.()), children: link.label }) }, link.id))) })) : null] })) : null] }), _jsx(XStack, { height: 1, bg: "$borderColor", mx: "$1.5", mt: "$3", shrink: 0 }), _jsx(Recents, { host: host, mode: mode, thread: thread, words: words, onOpen: open }), _jsxs(YStack, { gap: "$2", pt: "$2", shrink: 0, children: [_jsx(Made, {}), _jsx(Setup, {})
|
|
85
|
+
: null, mode === 'dev' ? (_jsxs(YStack, { role: "listitem", children: [_jsx(SidebarItem, { ...ROW, "aria-expanded": unfolded, icon: unfolded ? _jsx(ChevronUp, { size: 16, "aria-hidden": true }) : _jsx(ChevronDown, { size: 16, "aria-hidden": true }), onPress: () => setUnfolded((v) => !v), ...keys(() => setUnfolded((v) => !v)), children: "More" }), unfolded ? (_jsx(YStack, { role: "list", gap: 2, children: more.map((link) => (_jsx(YStack, { role: "listitem", children: _jsx(SidebarItem, { ...ROW, active: link.active, icon: link.icon, onPress: link.onPress, ...keys(() => link.onPress?.()), children: link.label }) }, link.id))) })) : null] })) : null] }), _jsx(XStack, { height: 1, bg: "$borderColor", mx: "$1.5", mt: "$3", shrink: 0 }), _jsx(Recents, { host: host, mode: mode, thread: thread, words: words, onOpen: open }), _jsxs(YStack, { gap: "$2", pt: "$2", shrink: 0, children: [_jsx(Made, {}), _jsx(Setup, {})] }), _jsxs(XStack, { "data-slot": "app-foot", items: "center", gap: "$1", pt: "$2", shrink: 0, children: [_jsx(YStack, { flex: 1, minW: 0, children: _jsx(Me, { host: host, onOrg: org }) }), drawer ? null : (_jsx(SidebarIconButton, { label: "Collapse sidebar", onPress: () => setCollapsed(true), width: 32, height: 32, rounded: 999, children: _jsx(PanelLeft, { size: 16 }) }))] })] }), drawer ? null : (_jsx(Grip, { side: "left", span: span.span, floor: FLOOR, ceil: CEIL, reset: WIDTH, onSpan: span.move, onKeep: span.keep, onShut: () => setCollapsed(true), label: "Resize sidebar",
|
|
87
86
|
// In the gutter between the column and the pane, not over the column's own rows.
|
|
88
87
|
r: GUTTER_END, width: "var(--pane-gap)" }))] }));
|
|
89
88
|
}
|
package/dist/Profile.js
CHANGED
|
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
// The reader's own profile, in the column beside the room.
|
|
4
4
|
//
|
|
5
5
|
// A WORKSPACE OPENS A PROFILE IN A PANEL, not a page. The page exists — it is
|
|
6
|
-
// /
|
|
6
|
+
// Settings (/settings), where the whole record is edited — but reaching it from a
|
|
7
7
|
// conversation costs the conversation: the room, the sidebar and the rail all
|
|
8
8
|
// go, and coming back is a navigation rather than a close. Slack, Teams and
|
|
9
9
|
// every app shaped like them put this in the right column for that reason, and
|
|
@@ -26,7 +26,7 @@ import { mark } from './lib/mark.js';
|
|
|
26
26
|
import { BAD } from './lib/mix.js';
|
|
27
27
|
/** Slack's is 380 and the thread panel that shares this slot is the same. */
|
|
28
28
|
const WIDTH = 380;
|
|
29
|
-
const ACCOUNT = '/
|
|
29
|
+
const ACCOUNT = '/settings';
|
|
30
30
|
export function Profile({ open, onClose }) {
|
|
31
31
|
const { Link } = useRooms();
|
|
32
32
|
const { user, updateUserProfile } = useAccount();
|
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 {
|
|
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
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
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
|
|
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
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
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
|
|
121
|
-
const
|
|
122
|
-
if (
|
|
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
|
|
121
|
+
return refusalOf(error?.body, code);
|
|
127
122
|
}
|
|
128
123
|
function ours(href) {
|
|
129
124
|
if (typeof href !== 'string')
|
package/dist/lib/limits.d.ts
CHANGED
|
@@ -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
|
|
21
|
-
*
|
|
22
|
-
*
|
|
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
|
-
|
|
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
|
|
11
|
+
export declare function readLimits(signal: AbortSignal): Promise<unknown>;
|
|
27
12
|
/**
|
|
28
|
-
* The limits
|
|
29
|
-
*
|
|
30
|
-
*
|
|
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
|
|
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;
|
package/dist/lib/limits.js
CHANGED
|
@@ -1,105 +1,33 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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 {
|
|
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
|
-
|
|
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
|
|
67
|
-
*
|
|
68
|
-
*
|
|
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
|
|
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
|
-
}
|
package/dist/lib/served.d.ts
CHANGED
|
@@ -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 {
|
package/dist/lib/served.js
CHANGED
|
@@ -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 = '';
|