@hanzo/rooms 0.1.25 → 0.1.27

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/Chat.js CHANGED
@@ -44,7 +44,7 @@ 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, house, useLimits } from "./lib/limits.js";
47
+ import { chats, house, paidPlan, setCreditsAfterAllowance, useLimits } from "./lib/limits.js";
48
48
  import { parseModels } from "@hanzo/ui/models/catalog";
49
49
  import { LimitedBanner } from "@hanzo/ui/product/LimitedBanner";
50
50
  import { WEB } from "./lib/web.js";
@@ -540,7 +540,7 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
540
540
  // only once the reader is turned away: a reply answered from a free model, or
541
541
  // a refusal.
542
542
  const { limits, notice, reload: reread } = useLimits(!anonymous, org(), (id) => nameOf(catalog, id));
543
- const held = Boolean(limits?.plan);
543
+ const held = paidPlan(limits);
544
544
  const pause = notice && (notice.fallback || notice.refused) ? notice : null;
545
545
  // EVERY MODEL IS CHOSEN, never asked for: one the plan does not cover still
546
546
  // picks, and the gateway answers it with the policy's refusal or from Enso,
@@ -829,6 +829,37 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
829
829
  }
830
830
  void latest.current(text);
831
831
  }, []);
832
+ // THE WAYS PAST A PAUSE, as the server sends them. A page opens; Continue
833
+ // with credits turns the org's choice on and asks the last question again on
834
+ // the model the reader picked; a switch moves the picker. A refusal of the
835
+ // write (only an org admin may make it) is said under the banner.
836
+ const [actWrong, setActWrong] = useState("");
837
+ const act = useCallback((a) => {
838
+ setActWrong("");
839
+ if (a.kind === "switch") {
840
+ if (a.model)
841
+ choose(a.model);
842
+ return;
843
+ }
844
+ if (a.kind !== "credits") {
845
+ if (a.url)
846
+ window.location.assign(a.url);
847
+ return;
848
+ }
849
+ setCreditsAfterAllowance(true)
850
+ .then(() => {
851
+ reread();
852
+ // A refused question is asked again in its place; one a free model
853
+ // answered is asked once more after that answer.
854
+ const q = [...turns.current].reverse().find((m) => m.role === "user");
855
+ if (q)
856
+ speak(words(q.content), failures[q.id] ? q.id : undefined);
857
+ })
858
+ .catch((e) => setActWrong(e instanceof Error ? e.message : "Credits could not be turned on."));
859
+ },
860
+ // `choose` is rebuilt each render; the handler follows it.
861
+ // eslint-disable-next-line react-hooks/exhaustive-deps
862
+ [reread, speak, failures, picked, preferred, listed]);
832
863
  // A TURN THAT NEVER SPEAKS IS STOPPED. A stream that stays open with nothing
833
864
  // in it would leave a Stop button and no answer for as long as the upstream
834
865
  // holds the connection; after `STALL` it is ended and its question is said to
@@ -1208,7 +1239,7 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
1208
1239
  },
1209
1240
  // WHAT GOES WITH THE MESSAGE, above the frame: one chip a file, each
1210
1241
  // with its own remove, and the reason any file was refused.
1211
- 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
1242
+ 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, onAction: act, onUsage: () => showSettings("usage") })) : null, actWrong ? (_jsx(Text, { role: "alert", "data-slot": "limited-banner-wrong", fontSize: "$2", color: BAD, children: actWrong })) : 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
1212
1243
  ? worded(talk.refusal)
1213
1244
  : talk.reply
1214
1245
  ? `Hanzo: ${talk.reply}`
package/dist/Settings.js CHANGED
@@ -35,11 +35,11 @@ import { parseModels } from '@hanzo/ui/models/catalog';
35
35
  import { UsagePanel } from '@hanzo/usage/panel';
36
36
  import { Catalog } from './Directory.js';
37
37
  import { useModel } from './model.js';
38
- import { payPage } from './lib/pay.js';
38
+ import { checkoutUrl, payPage } from './lib/pay.js';
39
39
  import { planName } from './lib/plans.js';
40
40
  import { api } from './lib/api.js';
41
41
  import { renewal, useSubscription, useTier } from './lib/tier.js';
42
- import { spendShown, useLimits } from './lib/limits.js';
42
+ import { paidPlan, setCreditsAfterAllowance, spendShown, useLimits } from './lib/limits.js';
43
43
  import { PlanUsage } from '@hanzo/ui/product/PlanUsage';
44
44
  import { Look } from './look.js';
45
45
  import { ProviderMark } from './ProviderMark.js';
@@ -238,8 +238,8 @@ function Usage({ sections, title, meters = false }) {
238
238
  if (!isAuthenticated || !accessToken) {
239
239
  return _jsx(Text, { fontSize: "$3", color: "$soft", children: "Sign in to see your usage." });
240
240
  }
241
- const plan = meters && read.limits ? _jsx(PlanUsage, { limits: read.limits, plan: planName(read.limits.plan) || undefined, notice: read.notice }) : null;
242
- if (read.limits?.plan || !spendShown(read))
241
+ const plan = meters && read.limits ? (_jsx(PlanUsage, { limits: read.limits, plan: planName(read.limits.plan) || undefined, notice: read.notice, onCredits: (on) => setCreditsAfterAllowance(on).then(read.reload), creditsHref: payPage(), addCreditsHref: checkoutUrl() })) : null;
242
+ if (paidPlan(read.limits) || !spendShown(read))
243
243
  return plan;
244
244
  // The address is read HERE rather than at import: the panel sends the bearer
245
245
  // with every read, and a credentialed read to an absolute address from a page
package/dist/Shell.js CHANGED
@@ -761,7 +761,7 @@ function Bots({ show, owner, filter = "", onPick, navigate, }) {
761
761
  return (_jsxs(YStack, { children: [_jsxs(XStack, { items: "center", gap: "$1.5", px: "$3", py: "$1", rounded: "$2", hoverStyle: { bg: "$hover" }, children: [_jsx(Box, { render: "button", onClick: () => navigate?.(show === "agents" ? "/bots" : "/contacts"), flex: 1, minW: 0, children: _jsxs(XStack, { items: "center", gap: "$1.5", children: [_jsx(Text, { fontSize: "$2", color: "$quiet", numberOfLines: 1, children: show === "agents" ? "Agents" : "People" }), folk.length + shown.length > 0 ? (_jsx(Text, { fontSize: "$1", color: "$soft", children: folk.length + shown.length })) : null] }) }), _jsx(Box, { render: "button", onClick: (e) => {
762
762
  e.stopPropagation();
763
763
  setSortMode((m) => (m === "alpha" ? "recent" : "alpha"));
764
- }, title: sortMode === "alpha" ? "Sorting A-Z (Click for Recent)" : "Sorting Recent (Click for A-Z)", px: "$1.5", py: "$0.5", rounded: "$1", bg: "$raised", hoverStyle: { opacity: 0.8 }, children: _jsx(Text, { fontSize: 10, fontWeight: "600", color: "$soft", children: sortMode === "alpha" ? "A-Z" : "Recent" }) }), show === "agents" ? (_jsx(Box, { render: "button", onClick: () => {
764
+ }, title: sortMode === "alpha" ? "Sorting A-Z (Click for Recent)" : "Sorting Recent (Click for A-Z)", px: "$1.5", py: "$0.5", rounded: "$1", bg: "$raised", hoverStyle: { opacity: 0.8 }, display: "flex", items: "center", justify: "center", $touchable: { minH: 40, minW: 40 }, children: _jsx(Text, { fontSize: 12, fontWeight: "600", color: "$soft", children: sortMode === "alpha" ? "A-Z" : "Recent" }) }), show === "agents" ? (_jsx(Box, { render: "button", onClick: () => {
765
765
  if (busy)
766
766
  return;
767
767
  setOpen(true);
@@ -940,8 +940,8 @@ function Rail({ mode, places, team, open, navigate, onSettings, onMore, }) {
940
940
  overflow: "hidden",
941
941
  }, children: [places.map((place, i) => {
942
942
  const here = place.id === mode;
943
- return (_jsxs(Box, { render: "button", onClick: () => navigate?.(place.route), "aria-label": place.label, "aria-current": here ? "page" : undefined, title: place.title, display: "flex", "$max-md": { display: i < 4 ? "flex" : "none", width: "auto", minW: 0 }, flexDirection: "column", width: 56, minH: 44, py: "$1", rounded: "$2", items: "center", justify: "center", gap: 2, bg: here ? "$edge" : undefined, hoverStyle: { bg: here ? "$edge" : "$hover" }, children: [_jsx(place.icon, { size: 18, "aria-hidden": true }), _jsx(Text, { fontSize: 10, lineHeight: 12, color: here ? "$ink" : "$quiet", numberOfLines: 1, children: place.label })] }, place.id));
944
- }), team ? (_jsx(XStack, { "$max-md": { display: places.length < 4 ? "flex" : "none", width: "auto", minW: 0 }, children: _jsx(Text, { render: _jsx("a", { href: team, "aria-label": "Workspace", title: "Hanzo Team" }), display: "flex", width: 56, color: "inherit", children: _jsxs(Box, { display: "flex", flex: 1, flexDirection: "column", minH: 44, py: "$1", rounded: "$2", items: "center", justify: "center", gap: 2, hoverStyle: { bg: "$hover" }, children: [_jsx(LayoutGrid, { size: 18, "aria-hidden": true }), _jsx(Text, { fontSize: 10, lineHeight: 12, color: "$quiet", numberOfLines: 1, children: "Workspace" })] }) }) })) : null, _jsxs(Box, { "data-slot": "more", render: "button", onClick: onMore, "aria-label": "More places", title: "More", display: "none", "$max-md": { display: "flex", width: "auto", minW: 0 }, flexDirection: "column", width: 56, minH: 44, py: "$1", rounded: "$2", items: "center", justify: "center", gap: 2, hoverStyle: { bg: "$hover" }, children: [_jsx(Search, { size: 18, "aria-hidden": true }), _jsx(Text, { fontSize: 10, lineHeight: 12, color: "$quiet", children: "More" })] })] }), _jsx(YStack, { items: "center", pb: "$2", children: _jsxs(Box, { render: "button", onClick: onSettings, "aria-label": "Settings", title: "Settings", display: "flex", flexDirection: "column", width: 56, minH: 44, py: "$1", rounded: "$2", items: "center", justify: "center", gap: 2, hoverStyle: { bg: "$hover" }, children: [_jsx(Settings2, { size: 18, "aria-hidden": true }), _jsx(Text, { fontSize: 10, lineHeight: 12, color: "$quiet", children: "Settings" })] }) })] }));
943
+ return (_jsxs(Box, { render: "button", onClick: () => navigate?.(place.route), "aria-label": place.label, "aria-current": here ? "page" : undefined, title: place.title, display: "flex", "$max-md": { display: i < 4 ? "flex" : "none", width: "auto", minW: 0 }, flexDirection: "column", width: 56, minH: 44, py: "$1", rounded: "$2", items: "center", justify: "center", gap: 2, bg: here ? "$edge" : undefined, hoverStyle: { bg: here ? "$edge" : "$hover" }, children: [_jsx(place.icon, { size: 18, "aria-hidden": true }), _jsx(Text, { fontSize: 12, lineHeight: 14, color: here ? "$ink" : "$quiet", numberOfLines: 1, children: place.label })] }, place.id));
944
+ }), team ? (_jsx(XStack, { "$max-md": { display: places.length < 4 ? "flex" : "none", width: "auto", minW: 0 }, children: _jsx(Text, { render: _jsx("a", { href: team, "aria-label": "Workspace", title: "Hanzo Team" }), display: "flex", width: 56, color: "inherit", children: _jsxs(Box, { display: "flex", flex: 1, flexDirection: "column", minH: 44, py: "$1", rounded: "$2", items: "center", justify: "center", gap: 2, hoverStyle: { bg: "$hover" }, children: [_jsx(LayoutGrid, { size: 18, "aria-hidden": true }), _jsx(Text, { fontSize: 12, lineHeight: 14, color: "$quiet", numberOfLines: 1, children: "Workspace" })] }) }) })) : null, _jsxs(Box, { "data-slot": "more", render: "button", onClick: onMore, "aria-label": "More places", title: "More", display: "none", "$max-md": { display: "flex", width: "auto", minW: 0 }, flexDirection: "column", width: 56, minH: 44, py: "$1", rounded: "$2", items: "center", justify: "center", gap: 2, hoverStyle: { bg: "$hover" }, children: [_jsx(Search, { size: 18, "aria-hidden": true }), _jsx(Text, { fontSize: 12, lineHeight: 14, color: "$quiet", children: "More" })] })] }), _jsx(YStack, { items: "center", pb: "$2", children: _jsxs(Box, { render: "button", onClick: onSettings, "aria-label": "Settings", title: "Settings", display: "flex", flexDirection: "column", width: 56, minH: 44, py: "$1", rounded: "$2", items: "center", justify: "center", gap: 2, hoverStyle: { bg: "$hover" }, children: [_jsx(Settings2, { size: 18, "aria-hidden": true }), _jsx(Text, { fontSize: 12, lineHeight: 14, color: "$quiet", children: "Settings" })] }) })] }));
945
945
  }
946
946
  /**
947
947
  * Chat's column: start a chat, the projects, then every conversation. The app
@@ -1210,7 +1210,7 @@ function Frame({ mode, children, navigate, back, forward, }) {
1210
1210
  return;
1211
1211
  e.preventDefault();
1212
1212
  navigate("/");
1213
- }, "aria-label": `${brand()} home`, title: "Home" }), "data-slot": "home", display: "flex", items: "center", justify: "center", width: 28, height: 28, color: "inherit", children: _jsx(HanzoMark, { size: 18 }) }), settled && isAuthenticated ? (_jsx(XStack, { shrink: 1, minW: 0, children: _jsx(OrgProjectSwitcher, { ...orgState, alwaysShow: true }) })) : null] }), _jsxs(YStack, { "data-slot": "places-list", px: "$2", pb: "$2", "$max-md": { display: "none" }, children: [places.filter((place) => matches(place.label, filter)).map((place) => (_jsx(SidebarItem, { icon: _jsx(place.icon, { size: 16, "aria-hidden": true }), active: place.id === mode, onPress: () => {
1213
+ }, "aria-label": `${brand()} home`, title: "Home" }), "data-slot": "home", display: "flex", items: "center", justify: "center", width: 28, height: 28, $touchable: { width: 40, height: 40 }, color: "inherit", children: _jsx(HanzoMark, { size: 18 }) }), settled && isAuthenticated ? (_jsx(XStack, { shrink: 1, minW: 0, children: _jsx(OrgProjectSwitcher, { ...orgState, alwaysShow: true }) })) : null] }), _jsxs(YStack, { "data-slot": "places-list", px: "$2", pb: "$2", "$max-md": { display: "none" }, children: [places.filter((place) => matches(place.label, filter)).map((place) => (_jsx(SidebarItem, { icon: _jsx(place.icon, { size: 16, "aria-hidden": true }), active: place.id === mode, onPress: () => {
1214
1214
  navigate?.(place.route);
1215
1215
  onPick?.();
1216
1216
  }, children: place.label }, place.id))), matches("Settings", filter) ? (_jsx(SidebarItem, { icon: _jsx(Settings2, { size: 16, "aria-hidden": true }), onPress: () => showSettings(FIRST), children: "Settings" })) : null] }), _jsx(XStack, { px: "$3", pb: "$2", children: _jsxs(XStack, { flex: 1, height: 30, items: "center", gap: "$2", px: "$2", rounded: "$2", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(Search, { size: 13, "aria-hidden": true }), _jsx(Text, { render: _jsx("input", { value: filter, onChange: (e) => setFilter(e.target.value), onKeyDown: (e) => {
@@ -7,8 +7,15 @@
7
7
  * bearer is the subject and `X-Org-Id` the organization this browser works in.
8
8
  */
9
9
  import { type UseLimits } from '@hanzo/ui/product/useLimits';
10
+ import { paidPlan } from '@hanzo/ui/product/limits';
10
11
  /** Reads the limits as whoever holds the bearer, in this browser's organization. */
11
12
  export declare function readLimits(signal: AbortSignal): Promise<unknown>;
13
+ /**
14
+ * The org's choice to keep paying from credits once included usage runs out —
15
+ * `PUT /v1/ai/limits`, org admins only. A refusal throws with the server's own
16
+ * words, so the reader is told why.
17
+ */
18
+ export declare function setCreditsAfterAllowance(on: boolean): Promise<void>;
12
19
  /**
13
20
  * The limits for a signed-in reader, in organization `org`; `name` turns a
14
21
  * fallback model id into the name the reader knows. Signed out reads nothing,
@@ -17,9 +24,10 @@ export declare function readLimits(signal: AbortSignal): Promise<unknown>;
17
24
  export declare const useLimits: (enabled?: boolean, org?: string | null, name?: (id: string) => string) => UseLimits;
18
25
  /**
19
26
  * Whether a reader's dollar spend may be shown: their limits came back and name no
20
- * plan, or could not be read. A plan holder sees usage as shares, never money.
27
+ * paid plan, or could not be read. A plan holder sees usage as shares, never money.
21
28
  */
22
29
  export declare const spendShown: ({ limits, answered }: Pick<UseLimits, 'limits' | 'answered'>) => boolean;
30
+ export { paidPlan };
23
31
  /** Whether a catalog family is one of Hanzo's chat families, Enso or Zen, whose turns carry the live web. */
24
32
  export declare const house: (family: string | undefined) => boolean;
25
33
  /**
@@ -8,6 +8,7 @@
8
8
  * bearer is the subject and `X-Org-Id` the organization this browser works in.
9
9
  */
10
10
  import { useLimits as useHeld } from '@hanzo/ui/product/useLimits';
11
+ import { paidPlan } from '@hanzo/ui/product/limits';
11
12
  import { api } from './api.js';
12
13
  import { scope } from './session.js';
13
14
  /** Reads the limits as whoever holds the bearer, in this browser's organization. */
@@ -17,6 +18,23 @@ export async function readLimits(signal) {
17
18
  throw Object.assign(new Error(`Limits answered ${res.status}`), { status: res.status });
18
19
  return res.json();
19
20
  }
21
+ /**
22
+ * The org's choice to keep paying from credits once included usage runs out —
23
+ * `PUT /v1/ai/limits`, org admins only. A refusal throws with the server's own
24
+ * words, so the reader is told why.
25
+ */
26
+ export async function setCreditsAfterAllowance(on) {
27
+ const res = await fetch(`${api()}/v1/ai/limits`, {
28
+ method: 'PUT',
29
+ headers: { ...scope(), 'Content-Type': 'application/json' },
30
+ body: JSON.stringify({ creditsAfterAllowance: on }),
31
+ });
32
+ if (res.ok)
33
+ return;
34
+ const body = (await res.json().catch(() => null));
35
+ const said = typeof body?.error === 'string' ? body.error : typeof body?.error?.message === 'string' ? body.error.message : typeof body?.message === 'string' ? body.message : '';
36
+ throw Object.assign(new Error(said || (res.status === 403 ? 'Only an organization admin can turn on credits.' : `Credits answered ${res.status}`)), { status: res.status });
37
+ }
20
38
  /**
21
39
  * The limits for a signed-in reader, in organization `org`; `name` turns a
22
40
  * fallback model id into the name the reader knows. Signed out reads nothing,
@@ -25,9 +43,10 @@ export async function readLimits(signal) {
25
43
  export const useLimits = (enabled = true, org = null, name) => useHeld(enabled ? readLimits : null, org, name);
26
44
  /**
27
45
  * Whether a reader's dollar spend may be shown: their limits came back and name no
28
- * plan, or could not be read. A plan holder sees usage as shares, never money.
46
+ * paid plan, or could not be read. A plan holder sees usage as shares, never money.
29
47
  */
30
- export const spendShown = ({ limits, answered }) => answered && !limits?.plan;
48
+ export const spendShown = ({ limits, answered }) => answered && !paidPlan(limits);
49
+ export { paidPlan };
31
50
  /** Whether a catalog family is one of Hanzo's chat families, Enso or Zen, whose turns carry the live web. */
32
51
  export const house = (family) => family === 'enso' || family === 'zen';
33
52
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzo/rooms",
3
- "version": "0.1.25",
3
+ "version": "0.1.27",
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",