@hanzo/rooms 0.1.92 → 0.1.94

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.d.ts CHANGED
@@ -2,10 +2,9 @@ export declare function Chat(): import("react").JSX.Element;
2
2
  /**
3
3
  * The same conversation, embedded in a page that is not /chat.
4
4
  *
5
- * R8: there were TWO live chat surfaces with different consent semantics — this
6
- * one asks at SEND and fails closed, the other gated on ARRIVAL through a
7
- * separate copy of the consent code. Two answers to one question is how they
8
- * drift, so /models mounts this and the second surface is gone. It carries no
5
+ * R8: there were TWO live chat surfaces, so /models mounts this one and the
6
+ * second is gone. The data-share choice is made once at onboarding; this room
7
+ * does not ask again when a message is sent. It carries no
9
8
  * heading of its own: the host page already has one, and a second `<h1>` is the
10
9
  * defect that took the landings off /chat and /dev.
11
10
  *
@@ -32,4 +31,3 @@ export declare function refusalOf(error: Error): {
32
31
  visitor: boolean;
33
32
  provider: boolean;
34
33
  };
35
- /** The agreement free asks for, in @hanzo/ai's words so every surface says one thing. */
package/dist/Chat.js CHANGED
@@ -17,10 +17,10 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
17
17
  // the free pool, so what changes at sign-in is the menu, not the room.
18
18
  //
19
19
  // The FREE lane is about PRICE, not identity: a free turn spends no balance and
20
- // is data-shared in exchange, which is what the consent block asks about, at
21
- // SEND rather than on arrival — consent is owed when someone sends, not when
22
- // they walk in. Signing in is what buys the paid models and a history that
23
- // follows you; it is never the price of asking a question.
20
+ // is data-shared in exchange. That choice is made once, at onboarding, where
21
+ // the terms are linked. Nothing under the composer repeats it. Signing in buys
22
+ // the paid models and a history that follows you; it is never the price of
23
+ // asking a question.
24
24
  //
25
25
  // This file used to branch on `isAuthenticated` and hand a visitor a composer
26
26
  // whose only action was a trip to hanzo.id, captioned "Sign in to send". The
@@ -31,7 +31,6 @@ import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "r
31
31
  import { billed, detail, familyOf, planRequired, plainly, refused } from "./failure.js";
32
32
  import { Prose } from "./Prose.js";
33
33
  import { useAgent, useAi, useChat, } from "@hanzo/ai/react";
34
- import { freeCopy, grantConsent, hasConsent, isFree, } from "@hanzo/ai";
35
34
  import { ASK, Chat as ChatSurface, Failure, Parts, words } from "@hanzo/ui/chat";
36
35
  import { carriedIn, carrying, compose, partsOf, said as told, weigh } from "./lib/attach.js";
37
36
  import { download, dropped, fileOf, pct, readable, refOf, retrieve } from "./lib/files.js";
@@ -69,10 +68,8 @@ import { checkoutUrl } from './lib/pay.js';
69
68
  import { Upgrade, useFree } from './Upgrade.js';
70
69
  import { enter, LOGIN } from './lib/destination.js';
71
70
  import { first } from './lib/first.js';
72
- import { BAD, mix } from "./lib/mix.js";
73
- import { brand, site } from './where.js';
74
- /** The veil under the free lane's consent sheet. */
75
- const SCRIM = mix("var(--pure-black)", 72, "srgb");
71
+ import { BAD } from "./lib/mix.js";
72
+ import { brand } from './where.js';
76
73
  // NO REASONING PANEL HERE. Every assistant turn opened a "Thinking" disclosure
77
74
  // listing seven steps — "Planning CSS extraction for chat styles", "Designing
78
75
  // responsive top bar grid layout" — typed into this file as a literal array.
@@ -89,24 +86,6 @@ const SCRIM = mix("var(--pure-black)", 72, "srgb");
89
86
  * room has always read at, so an untouched setting changes nothing.
90
87
  */
91
88
  const MEASURE = "var(--container-prose, 48rem)";
92
- /**
93
- * The store, or null when the browser will not give us one.
94
- *
95
- * Reading `window.localStorage` THROWS `SecurityError` where site data is
96
- * blocked — third-party contexts, Safari's Block All Cookies, a hardened
97
- * profile — and this is called from inside a send handler, so an unguarded
98
- * access takes the send down rather than the storage.
99
- */
100
- const store = () => {
101
- if (typeof window === "undefined")
102
- return null;
103
- try {
104
- return window.localStorage;
105
- }
106
- catch {
107
- return null;
108
- }
109
- };
110
89
  /**
111
90
  * The text this screen arrived with, TYPED INTO THE COMPOSER — never sent.
112
91
  *
@@ -182,8 +161,8 @@ export function Chat() {
182
161
  // mounted under the provider the shell already put above it.
183
162
  //
184
163
  // ONE room, for everyone. `anonymous` changes which models are on offer and
185
- // nothing else — the composer, the consent and the transcript are the same
186
- // components either way, because a visitor is having the same conversation.
164
+ // nothing else — the composer and the transcript are the same components
165
+ // either way, because a visitor is having the same conversation.
187
166
  //
188
167
  // The conversations are NOT drawn here. `Threads` is the list and the shell
189
168
  // owns the column it sits in — one sidebar for six rooms, already built, and a
@@ -194,10 +173,9 @@ export function Chat() {
194
173
  /**
195
174
  * The same conversation, embedded in a page that is not /chat.
196
175
  *
197
- * R8: there were TWO live chat surfaces with different consent semantics — this
198
- * one asks at SEND and fails closed, the other gated on ARRIVAL through a
199
- * separate copy of the consent code. Two answers to one question is how they
200
- * drift, so /models mounts this and the second surface is gone. It carries no
176
+ * R8: there were TWO live chat surfaces, so /models mounts this one and the
177
+ * second is gone. The data-share choice is made once at onboarding; this room
178
+ * does not ask again when a message is sent. It carries no
201
179
  * heading of its own: the host page already has one, and a second `<h1>` is the
202
180
  * defect that took the landings off /chat and /dev.
203
181
  *
@@ -476,9 +454,9 @@ function Plan({ onPress }) {
476
454
  *
477
455
  * `anonymous` opens on the free pool and offers only what the free pool can
478
456
  * answer, because listing a paid model to someone with no balance is an offer
479
- * that fails at send. Everything else — the transcript, the composer, the
480
- * consent — is identical, which is the point: signing in later changes the
481
- * models on the menu, not the room.
457
+ * that fails at send. Everything else — the transcript, the composer — is
458
+ * identical, which is the point: signing in later changes the models on the
459
+ * menu, not the room.
482
460
  */
483
461
  function Thread({ placeholder = message(), initial, anonymous = false, heading, }) {
484
462
  /**
@@ -487,16 +465,13 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
487
465
  * `useState(anonymous ? FREE : ENSO)` read the flag ONCE, on the first render
488
466
  * — which is the render before hydration settles, where `anonymous` is always
489
467
  * false. So every visitor opened on ENSO while the composer beside them said
490
- * "Enso Free", `isFree(model)` was false, and the two things that hang off it
491
- * both went quiet: the data-sharing notice never appeared, and the consent
492
- * block never asked. A visitor was sending on the paid pool having agreed to
493
- * nothing, because a state initialiser cannot see a value that arrives later.
468
+ * "Enso Free". A visitor was sending on the paid pool, because a state
469
+ * initialiser cannot see a value that arrives later.
494
470
  *
495
471
  * A CHOICE, or the identity. `null` is "nobody has chosen", which is a
496
472
  * different fact from either model and the only one that can defer to a flag
497
473
  * that settles after mount. Picking from the menu still wins, permanently.
498
474
  */
499
- const [asking, setAsking] = useState(null);
500
475
  // THE MODEL AND THE EFFORT are the one choice Chat and Dev share
501
476
  // (@hanzo/build `useMind`): the catalog from `GET /v1/models` and what the
502
477
  // reader kept, changed from the composer's chip or from Settings.
@@ -707,8 +682,7 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
707
682
  // EVERY MODEL IS CHOSEN, never asked for: one the plan does not cover still
708
683
  // picks, and the gateway answers it with the policy's refusal or from Enso,
709
684
  // which the room then shows. A turn the gateway refuses `plan_required`
710
- // opens the upgrade, titled with the model. `isFree` still drives the
711
- // free-lane notice.
685
+ // opens the upgrade, titled with the model.
712
686
  const free = useFree(!anonymous) && !held;
713
687
  const [ask, setAsk] = useState(null);
714
688
  // KEEPING THE CONVERSATION is the reader's account, not this pane's choice:
@@ -1138,17 +1112,8 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
1138
1112
  * no longer allowed to draw. `:focus-within` would say this in CSS; the
1139
1113
  * stylesheet that would hold it cannot reach past gui's own rule, so the
1140
1114
  * state is read where the event is. */
1141
- const guarded = useCallback((text) => {
1142
- const s = store();
1143
- // FAILS CLOSED. This read `s && !hasConsent(s)`, so a browser that gives
1144
- // us no store answered "no consent needed" and sent a data-shared turn
1145
- // having asked nobody. No store means no record, and no record means ask.
1146
- if (isFree(model) && (!s || !hasConsent(s))) {
1147
- setAsking(text);
1148
- return;
1149
- }
1150
- speak(text);
1151
- }, [model, speak]);
1115
+ // A free send goes. The data-share choice was made at onboarding, once.
1116
+ const guarded = speak;
1152
1117
  /**
1153
1118
  * The draft and the files, sent as one turn.
1154
1119
  *
@@ -1317,25 +1282,6 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
1317
1282
  enter(LOGIN);
1318
1283
  }, children: "Sign in" })) : lift ? (_jsx(Button, { size: "sm", variant: "outline", onClick: () => setAsk({ ...lift, model: nameOf(model, listed) }), children: "Upgrade" })) : how?.visitor ? (_jsx(Button, { size: "sm", variant: "outline", onClick: () => enter(), children: "Sign in to keep going" })) : (_jsx(Anchor, { href: checkoutUrl(), children: _jsx(Button, { size: "sm", variant: "outline", children: "Add credit" }) })) })) : null] }) }));
1319
1284
  };
1320
- // The agreement is asked OVER the room, not instead of it. Returning the
1321
- // consent on its own replaced the whole pane: the question the reader had
1322
- // just typed left the screen, and the first thing a stranger saw of the
1323
- // product was a page about training data. The room stays; the sheet sits on
1324
- // top of it, and the question is still there behind it.
1325
- const consent = asking === null ? null : (_jsx(Consent, { question: told(asking).text, onAgree: () => {
1326
- const s = store();
1327
- // Nothing to persist into is not a reason to refuse the turn the
1328
- // reader just agreed to; it means the question is asked again next
1329
- // time, which is the honest cost of a browser that stores nothing.
1330
- if (s)
1331
- grantConsent(s);
1332
- const held = asking;
1333
- setAsking(null);
1334
- if (held)
1335
- speak(held);
1336
- }, onCancel: () => {
1337
- setAsking(null);
1338
- } }));
1339
1285
  // Dev, from the Summary's Create file or site: a move inside the app where the
1340
1286
  // host's app reads the path (hanzo.ai), else Dev on hanzo.ai, as the shell's
1341
1287
  // Dev place goes from a host without it (hanzo.team).
@@ -1573,9 +1519,5 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
1573
1519
  empty: heading === null ? undefined : (_jsxs(YStack, { "data-slot": "opening", items: "center", width: "100%", pt: "$6", pb: "$2", ...(media.sm ? null : { flex: 1, justify: "flex-end" }), children: [room.length > 1 ? (_jsx(XStack, { items: "center", justify: "center", mb: "$4", children: room.map((name, i) => (_jsx(Box, { ml: i ? -16 : 0, rounded: 40, borderWidth: 2, borderColor: "$background", z: room.length - i, children: _jsx(Face, { src: faces[name]?.avatar, emoji: faces[name]?.emoji, name: name, size: 72 }) }, name))) })) : portrait ? (_jsx(Box, { width: 160, height: 160, rounded: 24, overflow: "hidden", borderWidth: 2, borderColor: "var(--white-15)", boxShadow: "0 10px 24px color-mix(in srgb, var(--pure-black) 70%, transparent)", mb: "$4", bg: "var(--neutral-900)", children: _jsx(View, { render: _jsx("img", { src: portrait, alt: withWhom }), width: "100%", height: "100%", objectFit: "cover" }) })) : null, room.length === 1 && !portrait ? (_jsx(Box, { mb: "$3", children: _jsx(Face, { emoji: mate?.emoji ?? persona?.emoji, name: withWhom, size: 72 }) })) : null, _jsx(Text, { render: "h1", fontSize: "$7", lineHeight: "$7", fontWeight: "700", color: "$ink", text: "center", mb: "$1", children: join(room) || heading || (firstName ? `What’s up, ${firstName}?` : "What’s up?") }), room.length === 1 && (mate?.description ?? persona?.description) ? (_jsx(Text, { fontSize: "$3", color: "$soft", text: "center", maxW: 500, mb: "$4", lineHeight: "$4", children: mate?.description ?? persona?.description })) : null] })),
1574
1520
  // The transcript reads at the same measure. `Thread` defaults to 768
1575
1521
  // itself; handing it the column here is what lets Width reach it.
1576
- column: { maxW: MEASURE }, composer: prompt({ ...composing, field: mention.field }, mention.state) }), _jsx(AgentMaker, {}), carry.dialog, _jsx(XStack, { items: "center", justify: "center", py: "$1", px: "$3", gap: "$2", children: isFree(model) ? (_jsxs(Text, { fontSize: "$1", color: "$soft", children: ["Free chat is data-shared to train models.", " ", _jsx(Anchor, { href: site("/legal/privacy"), fontSize: "$1", color: "$ink", children: "Privacy" })] })) : (_jsxs(_Fragment, { children: [_jsx(Anchor, { href: site("/legal/terms"), fontSize: "$1", color: "$soft", children: "Terms" }), _jsx(Text, { fontSize: "$1", color: "$soft", children: "\u00B7" }), _jsx(Anchor, { href: site("/legal/privacy"), fontSize: "$1", color: "$soft", children: "Privacy" })] })) }), consent, _jsx(Upgrade, { ask: ask, onClose: () => setAsk(null) })] }));
1577
- }
1578
- /** The agreement free asks for, in @hanzo/ai's words so every surface says one thing. */
1579
- function Consent({ question, onAgree, onCancel, }) {
1580
- return (_jsx(YStack, { position: "absolute", t: 0, l: 0, r: 0, b: 0, items: "center", justify: "center", p: "$4", z: 40, bg: SCRIM, children: _jsxs(YStack, { width: "100%", maxW: 512, px: "$5", py: "$5", gap: "$4", rounded: "$4", borderWidth: 1, borderColor: "$borderColor", bg: "$background", children: [_jsx(Text, { render: "h2", fontSize: "$6", fontWeight: "500", color: "$ink", children: freeCopy.consentTitle }), question ? (_jsxs(Text, { fontSize: "$3", color: "$soft", numberOfLines: 2, children: ["Your question is waiting: \u201C", question, "\u201D"] })) : null, _jsx(Text, { fontSize: "$4", color: "$soft", children: freeCopy.consentBody }), _jsx(YStack, { gap: "$2", children: freeCopy.consentPoints.map((point) => (_jsx(Text, { fontSize: "$3", color: "$soft", children: point }, point))) }), _jsxs(XStack, { gap: "$3", flexWrap: "wrap", children: [_jsx(Button, { onClick: onAgree, children: freeCopy.consentAgreeCta }), _jsx(Button, { variant: "outline", onClick: onCancel, children: freeCopy.consentCancelCta })] }), _jsxs(Text, { fontSize: "$2", color: "$soft", children: [freeCopy.termsText, " ", _jsx(Anchor, { href: site("/legal/terms"), children: "Terms" }), " · ", _jsx(Anchor, { href: site("/legal/privacy"), children: "Privacy" })] })] }) }));
1522
+ column: { maxW: MEASURE }, composer: prompt({ ...composing, field: mention.field }, mention.state) }), _jsx(AgentMaker, {}), carry.dialog, _jsx(Upgrade, { ask: ask, onClose: () => setAsk(null) })] }));
1581
1523
  }
package/dist/lib/ai.d.ts CHANGED
@@ -40,7 +40,7 @@ export declare const base: () => {
40
40
  export interface Ai {
41
41
  client: AiClient | null;
42
42
  model: string;
43
- /** Answering free, so the turn is data-shared and owes the notice. */
43
+ /** Answering on the free pool. The data-share choice was made at onboarding. */
44
44
  free: boolean;
45
45
  /** Mounted client-side. The static export always prerenders the signed-out
46
46
  * page, so nothing interactive may render until this is true. */
package/dist/lib/ai.js CHANGED
@@ -5,8 +5,8 @@
5
5
  // their account like every other turn they run.
6
6
  //
7
7
  // Signed out — the FREE POOL, anonymously. There is no account to bill and none
8
- // is needed: a free turn spends no balance, and what it costs instead is stated
9
- // in the consent block the composer asks before the first send. A widget key, if
8
+ // is needed: a free turn spends no balance. The data-share choice is made
9
+ // once, at onboarding. A widget key, if
10
10
  // one is configured, rides along so the turn attributes to the org that minted
11
11
  // it; without one the turn is simply anonymous. Either way the client EXISTS,
12
12
  // because a chat you cannot send is not a chat.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzo/rooms",
3
- "version": "0.1.92",
3
+ "version": "0.1.94",
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",