@hanzo/rooms 0.1.91 → 0.1.93
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/App.js +2 -2
- package/dist/Betas.d.ts +21 -0
- package/dist/Betas.js +127 -0
- package/dist/Chat.d.ts +3 -5
- package/dist/Chat.js +65 -82
- package/dist/Me.js +4 -3
- package/dist/Palette.d.ts +20 -6
- package/dist/Palette.js +203 -99
- package/dist/Rail.d.ts +1 -1
- package/dist/Rail.js +20 -4
- package/dist/Shell.js +1 -1
- package/dist/lib/betas.d.ts +38 -0
- package/dist/lib/betas.js +66 -0
- package/dist/lib/session.d.ts +3 -1
- package/dist/lib/session.js +3 -2
- package/dist/lib/title.d.ts +2 -0
- package/dist/lib/title.js +5 -0
- package/dist/seek.d.ts +10 -17
- package/dist/seek.js +37 -20
- package/package.json +3 -3
package/dist/App.js
CHANGED
|
@@ -119,7 +119,7 @@ function Window({ bar }) {
|
|
|
119
119
|
const shut = useCallback(() => setDrawer(false), []);
|
|
120
120
|
// The sidebar shut, kept per browser under the key the builder's rail kept it under.
|
|
121
121
|
const [closed, setClosed] = useKept('hanzo.build.rail', false);
|
|
122
|
-
// The palette is open (
|
|
122
|
+
// The palette is open (the sidebar's search; ⌘K toggles it).
|
|
123
123
|
const [finding, setFinding] = useState(false);
|
|
124
124
|
const found = useCallback(() => setFinding(false), []);
|
|
125
125
|
const dev = useDevHost();
|
|
@@ -217,7 +217,7 @@ function Window({ bar }) {
|
|
|
217
217
|
const key = (e) => {
|
|
218
218
|
if (summons(e)) {
|
|
219
219
|
e.preventDefault();
|
|
220
|
-
setFinding(
|
|
220
|
+
setFinding((open) => !open);
|
|
221
221
|
return;
|
|
222
222
|
}
|
|
223
223
|
if (typing(e))
|
package/dist/Betas.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** The apps in beta as this reader stands: which open, which they asked for, and the ask. */
|
|
2
|
+
export declare function useBetas(): {
|
|
3
|
+
/** The one predicate: whether this reader may open the app `id`. */
|
|
4
|
+
open: (id: string) => boolean;
|
|
5
|
+
/** Whether they asked for it. */
|
|
6
|
+
asked: (id: string) => boolean;
|
|
7
|
+
/** Ask for these apps. */
|
|
8
|
+
request: (ids: string[]) => Promise<void>;
|
|
9
|
+
/** The cloud has answered for this reader. */
|
|
10
|
+
ready: boolean;
|
|
11
|
+
};
|
|
12
|
+
/** Every app in beta, by group, each with its action. */
|
|
13
|
+
export declare function BetasList({ go }: {
|
|
14
|
+
go?: (href: string) => void;
|
|
15
|
+
}): import("react").JSX.Element;
|
|
16
|
+
/** The Betas list in a dialog, from the rail's More. */
|
|
17
|
+
export declare function BetasDialog({ open, onOpenChange, go }: {
|
|
18
|
+
open: boolean;
|
|
19
|
+
onOpenChange: (open: boolean) => void;
|
|
20
|
+
go?: (href: string) => void;
|
|
21
|
+
}): import("react").JSX.Element;
|
package/dist/Betas.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
// The apps in beta: which this reader may open, and the list they ask from.
|
|
4
|
+
//
|
|
5
|
+
// WHO MAY OPEN ONE is the cloud's answer, never this page's. Each app in beta is a
|
|
6
|
+
// principal switch, `beta.<id>`, decided per caller from the validated org and
|
|
7
|
+
// person (cloud apps/flags), and GET /v1/flag/platform answers every one for the
|
|
8
|
+
// signed-in caller. A SuperAdmin grants one to a person from their CRM record.
|
|
9
|
+
// `opens` (lib/betas) reads that answer, the one predicate every caller asks.
|
|
10
|
+
//
|
|
11
|
+
// WHAT THEY ASKED FOR is their CRM record's, apps/marketing's beta record:
|
|
12
|
+
// GET /v1/marketing/beta reads it and POST /v1/marketing/beta/request adds to it,
|
|
13
|
+
// stamped with the time and stated on the event plane under the person and the
|
|
14
|
+
// org, so the request reaches the team where it works the person.
|
|
15
|
+
//
|
|
16
|
+
// Signed out, still loading, or on any error nothing is open: a flags outage
|
|
17
|
+
// cannot leak an unfinished app.
|
|
18
|
+
import { useCallback, useEffect, useState, useSyncExternalStore } from 'react';
|
|
19
|
+
import { useIam } from '@hanzo/iam/react';
|
|
20
|
+
import { Button, Dialog, DialogContent, DialogTitle, Text, XStack, YStack } from '@hanzo/ui';
|
|
21
|
+
import { api } from './lib/api.js';
|
|
22
|
+
import { BETAS, GROUPS, opens } from './lib/betas.js';
|
|
23
|
+
import { bearer, scope, superAdmin } from './lib/session.js';
|
|
24
|
+
const NONE = { flags: {}, asked: [] };
|
|
25
|
+
/** One read per token, shared by every caller on the page. */
|
|
26
|
+
let held = null;
|
|
27
|
+
let reading = null;
|
|
28
|
+
const listeners = new Set();
|
|
29
|
+
const tell = () => listeners.forEach((f) => f());
|
|
30
|
+
/** A switch read this long ago is read again on the next mount. */
|
|
31
|
+
const FRESH = 30_000;
|
|
32
|
+
async function get(path) {
|
|
33
|
+
const r = await fetch(`${api()}${path}`, { headers: scope() });
|
|
34
|
+
return r.ok ? (await r.json()) : null;
|
|
35
|
+
}
|
|
36
|
+
function read(token) {
|
|
37
|
+
if (reading)
|
|
38
|
+
return reading;
|
|
39
|
+
reading = Promise.all([
|
|
40
|
+
get('/v1/flag/platform').catch(() => null),
|
|
41
|
+
get('/v1/marketing/beta').catch(() => null),
|
|
42
|
+
])
|
|
43
|
+
.then(([f, b]) => {
|
|
44
|
+
held = { token, said: { flags: f?.flags ?? {}, asked: b?.betas ?? [] }, at: Date.now() };
|
|
45
|
+
})
|
|
46
|
+
.finally(() => {
|
|
47
|
+
reading = null;
|
|
48
|
+
tell();
|
|
49
|
+
});
|
|
50
|
+
return reading;
|
|
51
|
+
}
|
|
52
|
+
const subscribe = (f) => {
|
|
53
|
+
listeners.add(f);
|
|
54
|
+
return () => listeners.delete(f);
|
|
55
|
+
};
|
|
56
|
+
/** The apps in beta as this reader stands: which open, which they asked for, and the ask. */
|
|
57
|
+
export function useBetas() {
|
|
58
|
+
const { accessToken } = useIam();
|
|
59
|
+
const [sudo, setSudo] = useState(false);
|
|
60
|
+
const [local, setLocal] = useState(false);
|
|
61
|
+
// Facts only the browser holds arrive after mount, so the first render
|
|
62
|
+
// matches the HTML it hydrates.
|
|
63
|
+
useEffect(() => setLocal(here()), []);
|
|
64
|
+
useEffect(() => setSudo(superAdmin()), [accessToken]);
|
|
65
|
+
const token = accessToken ? bearer() ?? '' : '';
|
|
66
|
+
const now = useSyncExternalStore(subscribe, () => held, () => null);
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (token && (!held || held.token !== token || Date.now() - held.at > FRESH))
|
|
69
|
+
void read(token);
|
|
70
|
+
}, [token]);
|
|
71
|
+
const said = token && now?.token === token ? now.said : NONE;
|
|
72
|
+
const open = useCallback((id) => opens(id, said.flags, sudo || local), [said, sudo, local]);
|
|
73
|
+
const asked = useCallback((id) => said.asked.includes(id), [said]);
|
|
74
|
+
const request = useCallback(async (ids) => {
|
|
75
|
+
const r = await fetch(`${api()}/v1/marketing/beta/request`, {
|
|
76
|
+
method: 'POST',
|
|
77
|
+
headers: { 'content-type': 'application/json', ...scope() },
|
|
78
|
+
body: JSON.stringify({ betas: ids }),
|
|
79
|
+
});
|
|
80
|
+
if (!r.ok)
|
|
81
|
+
throw new Error(r.status === 401 ? 'Sign in to ask for access.' : `The request was not taken (${r.status}).`);
|
|
82
|
+
const record = (await r.json());
|
|
83
|
+
if (held && held.token === token) {
|
|
84
|
+
held = { ...held, said: { ...held.said, asked: record.betas ?? [...held.said.asked, ...ids] } };
|
|
85
|
+
tell();
|
|
86
|
+
}
|
|
87
|
+
}, [token]);
|
|
88
|
+
return { open, asked, request, ready: Boolean(token) && now?.token === token };
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Whether this page is served from a developer's own machine, where every app
|
|
92
|
+
* opens: a room nobody can open is a room nobody can work on. The ORIGIN, which
|
|
93
|
+
* no deployment can acquire by configuration.
|
|
94
|
+
*/
|
|
95
|
+
function here() {
|
|
96
|
+
if (typeof window === 'undefined')
|
|
97
|
+
return false;
|
|
98
|
+
const host = window.location.hostname;
|
|
99
|
+
return host === 'localhost' || host === '127.0.0.1' || host === '[::1]' || host.endsWith('.local') || /^10\./.test(host) || /^192\.168\./.test(host);
|
|
100
|
+
}
|
|
101
|
+
/** One app's action: Open where it may, Requested once asked, else Request access. */
|
|
102
|
+
function Ask({ beta, go }) {
|
|
103
|
+
const { open, asked, request } = useBetas();
|
|
104
|
+
const [busy, setBusy] = useState(false);
|
|
105
|
+
const [wrong, setWrong] = useState(null);
|
|
106
|
+
if (open(beta.id)) {
|
|
107
|
+
return beta.href && go ? (_jsx(Button, { size: "sm", rounded: 999, onPress: () => go(beta.href), "data-slot": "beta-open", children: "Open" })) : (_jsx(Text, { fontSize: "$2", color: "$color11", "data-slot": "beta-on", children: "You\u2019re in" }));
|
|
108
|
+
}
|
|
109
|
+
if (asked(beta.id)) {
|
|
110
|
+
return (_jsx(Text, { fontSize: "$2", color: "$color11", "data-slot": "beta-requested", children: "Requested" }));
|
|
111
|
+
}
|
|
112
|
+
return (_jsxs(YStack, { items: "flex-end", gap: "$1", children: [_jsx(Button, { size: "sm", rounded: 999, variant: "outline", disabled: busy, "data-slot": "beta-request", onPress: () => {
|
|
113
|
+
setBusy(true);
|
|
114
|
+
setWrong(null);
|
|
115
|
+
request([beta.id])
|
|
116
|
+
.catch((e) => setWrong(e instanceof Error ? e.message : String(e)))
|
|
117
|
+
.finally(() => setBusy(false));
|
|
118
|
+
}, children: busy ? 'Requesting…' : 'Request access' }), wrong ? (_jsx(Text, { fontSize: "$1", color: "$red10", children: wrong })) : null] }));
|
|
119
|
+
}
|
|
120
|
+
/** Every app in beta, by group, each with its action. */
|
|
121
|
+
export function BetasList({ go }) {
|
|
122
|
+
return (_jsx(YStack, { gap: "$4", "data-slot": "betas", children: GROUPS.map((group) => (_jsxs(YStack, { gap: "$2", children: [_jsx(Text, { fontSize: "$1", color: "$color10", textTransform: "uppercase", letterSpacing: 0.6, children: group }), BETAS.filter((b) => b.group === group).map((b) => (_jsxs(XStack, { items: "center", gap: "$3", "data-beta": b.id, children: [_jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(Text, { fontSize: "$3", fontWeight: "500", children: b.name }), _jsx(Text, { fontSize: "$2", color: "$color11", children: b.line })] }), _jsx(Ask, { beta: b, go: go })] }, b.id)))] }, group))) }));
|
|
123
|
+
}
|
|
124
|
+
/** The Betas list in a dialog, from the rail's More. */
|
|
125
|
+
export function BetasDialog({ open, onOpenChange, go }) {
|
|
126
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { maxW: 520, rounded: "$6", gap: "$3", children: [_jsx(DialogTitle, { children: "Betas" }), _jsx(Text, { fontSize: "$2", color: "$color11", children: "Apps we have not opened to everyone yet. Ask for the ones you want and we will let you in." }), _jsx(YStack, { maxH: "60vh", overflowY: "auto", overflowX: "hidden", pr: "$2", children: _jsx(BetasList, { go: go }) })] }) }));
|
|
127
|
+
}
|
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
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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,9 +17,9 @@ 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
|
|
21
|
-
//
|
|
22
|
-
//
|
|
20
|
+
// is data-shared in exchange. That choice is made once, at onboarding, where
|
|
21
|
+
// the terms are linked. The line under the composer says so for as long as a
|
|
22
|
+
// free model answers. Signing in buys the paid models and a history that
|
|
23
23
|
// follows you; it is never the price of asking a question.
|
|
24
24
|
//
|
|
25
25
|
// This file used to branch on `isAuthenticated` and hand a visitor a composer
|
|
@@ -31,7 +31,7 @@ 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 {
|
|
34
|
+
import { isFree } from "@hanzo/ai";
|
|
35
35
|
import { ASK, Chat as ChatSurface, Failure, Parts, words } from "@hanzo/ui/chat";
|
|
36
36
|
import { carriedIn, carrying, compose, partsOf, said as told, weigh } from "./lib/attach.js";
|
|
37
37
|
import { download, dropped, fileOf, pct, readable, refOf, retrieve } from "./lib/files.js";
|
|
@@ -42,7 +42,7 @@ import { AgentMaker, HOUSE, useAgentShelf, useCrew } from "./crew.js";
|
|
|
42
42
|
import { called } from "./team.js";
|
|
43
43
|
import { Anchor, Box, Button, Text, View, XStack, YStack } from "@hanzo/ui";
|
|
44
44
|
import { useMedia } from "@hanzo/gui";
|
|
45
|
-
import { tidy } from "./lib/title.js";
|
|
45
|
+
import { given, tidy } from "./lib/title.js";
|
|
46
46
|
import { useIam, useIamToken } from "@hanzo/iam/react";
|
|
47
47
|
import { bearer, hasSession, named, org } from "./lib/session.js";
|
|
48
48
|
import { api } from "./lib/api.js";
|
|
@@ -64,15 +64,13 @@ import { Share } from "./Share.js";
|
|
|
64
64
|
import { ConversationMenu, ThreadMore, useThreadList } from "./thread.js";
|
|
65
65
|
import { Schedule } from "./Schedule.js";
|
|
66
66
|
import { keepFile, useAttach, useKeep } from "./keep.js";
|
|
67
|
-
import { artifacts, lead, nameOf, PAGE, Panel, PanelToggle, prompt, renders, Reply, Served, Summary, useDeck, useKey, Suggestions, useCarry, useLanguage, useMention, useMind, useSuggestions, CHAT_MODES, usePanel, frames, name as tabName, } from "@hanzo/build";
|
|
67
|
+
import { artifacts, lead, nameOf, PAGE, Panel, PanelToggle, prompt, renders, Reply, Served, Summary, useDeck, useKey, Suggestions, useCarry, useLanguage, useMention, useMind, useSuggestions, book, hand, taken, want, path as devPath, CHAT_MODES, usePanel, frames, name as tabName, } from "@hanzo/build";
|
|
68
68
|
import { checkoutUrl } from './lib/pay.js';
|
|
69
69
|
import { Upgrade, useFree } from './Upgrade.js';
|
|
70
70
|
import { enter, LOGIN } from './lib/destination.js';
|
|
71
71
|
import { first } from './lib/first.js';
|
|
72
|
-
import { BAD
|
|
72
|
+
import { BAD } from "./lib/mix.js";
|
|
73
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");
|
|
76
74
|
// NO REASONING PANEL HERE. Every assistant turn opened a "Thinking" disclosure
|
|
77
75
|
// listing seven steps — "Planning CSS extraction for chat styles", "Designing
|
|
78
76
|
// responsive top bar grid layout" — typed into this file as a literal array.
|
|
@@ -89,24 +87,6 @@ const SCRIM = mix("var(--pure-black)", 72, "srgb");
|
|
|
89
87
|
* room has always read at, so an untouched setting changes nothing.
|
|
90
88
|
*/
|
|
91
89
|
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
90
|
/**
|
|
111
91
|
* The text this screen arrived with, TYPED INTO THE COMPOSER — never sent.
|
|
112
92
|
*
|
|
@@ -182,8 +162,8 @@ export function Chat() {
|
|
|
182
162
|
// mounted under the provider the shell already put above it.
|
|
183
163
|
//
|
|
184
164
|
// ONE room, for everyone. `anonymous` changes which models are on offer and
|
|
185
|
-
// nothing else — the composer
|
|
186
|
-
//
|
|
165
|
+
// nothing else — the composer and the transcript are the same components
|
|
166
|
+
// either way, because a visitor is having the same conversation.
|
|
187
167
|
//
|
|
188
168
|
// The conversations are NOT drawn here. `Threads` is the list and the shell
|
|
189
169
|
// owns the column it sits in — one sidebar for six rooms, already built, and a
|
|
@@ -194,10 +174,9 @@ export function Chat() {
|
|
|
194
174
|
/**
|
|
195
175
|
* The same conversation, embedded in a page that is not /chat.
|
|
196
176
|
*
|
|
197
|
-
* R8: there were TWO live chat surfaces
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
* drift, so /models mounts this and the second surface is gone. It carries no
|
|
177
|
+
* R8: there were TWO live chat surfaces, so /models mounts this one and the
|
|
178
|
+
* second is gone. The data-share choice is made once at onboarding; this room
|
|
179
|
+
* does not ask again when a message is sent. It carries no
|
|
201
180
|
* heading of its own: the host page already has one, and a second `<h1>` is the
|
|
202
181
|
* defect that took the landings off /chat and /dev.
|
|
203
182
|
*
|
|
@@ -476,9 +455,9 @@ function Plan({ onPress }) {
|
|
|
476
455
|
*
|
|
477
456
|
* `anonymous` opens on the free pool and offers only what the free pool can
|
|
478
457
|
* 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
|
|
480
|
-
*
|
|
481
|
-
*
|
|
458
|
+
* that fails at send. Everything else — the transcript, the composer — is
|
|
459
|
+
* identical, which is the point: signing in later changes the models on the
|
|
460
|
+
* menu, not the room.
|
|
482
461
|
*/
|
|
483
462
|
function Thread({ placeholder = message(), initial, anonymous = false, heading, }) {
|
|
484
463
|
/**
|
|
@@ -487,16 +466,14 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
|
|
|
487
466
|
* `useState(anonymous ? FREE : ENSO)` read the flag ONCE, on the first render
|
|
488
467
|
* — which is the render before hydration settles, where `anonymous` is always
|
|
489
468
|
* false. So every visitor opened on ENSO while the composer beside them said
|
|
490
|
-
* "Enso Free", `isFree(model)` was false,
|
|
491
|
-
*
|
|
492
|
-
*
|
|
493
|
-
* nothing, because a state initialiser cannot see a value that arrives later.
|
|
469
|
+
* "Enso Free", and `isFree(model)` was false, so the data-sharing notice
|
|
470
|
+
* never appeared. A visitor was sending on the paid pool, because a state
|
|
471
|
+
* initialiser cannot see a value that arrives later.
|
|
494
472
|
*
|
|
495
473
|
* A CHOICE, or the identity. `null` is "nobody has chosen", which is a
|
|
496
474
|
* different fact from either model and the only one that can defer to a flag
|
|
497
475
|
* that settles after mount. Picking from the menu still wins, permanently.
|
|
498
476
|
*/
|
|
499
|
-
const [asking, setAsking] = useState(null);
|
|
500
477
|
// THE MODEL AND THE EFFORT are the one choice Chat and Dev share
|
|
501
478
|
// (@hanzo/build `useMind`): the catalog from `GET /v1/models` and what the
|
|
502
479
|
// reader kept, changed from the composer's chip or from Settings.
|
|
@@ -527,8 +504,9 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
|
|
|
527
504
|
go: (href) => (/^https?:/.test(href) ? void window.open(href, "_blank", "noopener,noreferrer") : go(href)),
|
|
528
505
|
}, CHAT_MODES);
|
|
529
506
|
const agentShelf = useAgentShelf();
|
|
530
|
-
// The opening greets the person by
|
|
531
|
-
|
|
507
|
+
// The opening greets the person by the first word of their display name, capitalised ("What’s up, Z?").
|
|
508
|
+
// A username is a handle, not a name: without a display name the greeting has none.
|
|
509
|
+
const firstName = useMemo(() => (anonymous ? "" : given(named()?.display ?? "")), [anonymous]);
|
|
532
510
|
// Up to three things worth doing next, from the person's own activity (@hanzo/build).
|
|
533
511
|
const hints = useSuggestions(target);
|
|
534
512
|
// THE HEADER'S ⋯ IS THE ROW'S MENU (thread.tsx): rename, pin, share, its
|
|
@@ -1137,17 +1115,9 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
|
|
|
1137
1115
|
* no longer allowed to draw. `:focus-within` would say this in CSS; the
|
|
1138
1116
|
* stylesheet that would hold it cannot reach past gui's own rule, so the
|
|
1139
1117
|
* state is read where the event is. */
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
// us no store answered "no consent needed" and sent a data-shared turn
|
|
1144
|
-
// having asked nobody. No store means no record, and no record means ask.
|
|
1145
|
-
if (isFree(model) && (!s || !hasConsent(s))) {
|
|
1146
|
-
setAsking(text);
|
|
1147
|
-
return;
|
|
1148
|
-
}
|
|
1149
|
-
speak(text);
|
|
1150
|
-
}, [model, speak]);
|
|
1118
|
+
// A free send goes. The data-share choice was made at onboarding, once,
|
|
1119
|
+
// and the line under the composer still says what a free model does.
|
|
1120
|
+
const guarded = speak;
|
|
1151
1121
|
/**
|
|
1152
1122
|
* The draft and the files, sent as one turn.
|
|
1153
1123
|
*
|
|
@@ -1179,8 +1149,8 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
|
|
|
1179
1149
|
.then((id) => (!open && id ? reopen(id) : undefined))
|
|
1180
1150
|
.catch(() => { });
|
|
1181
1151
|
};
|
|
1182
|
-
const submit = () => {
|
|
1183
|
-
const text =
|
|
1152
|
+
const submit = (ask = draft) => {
|
|
1153
|
+
const text = ask.trim();
|
|
1184
1154
|
if (preparing || (!text && !files.length))
|
|
1185
1155
|
return;
|
|
1186
1156
|
if (!carry.preparing)
|
|
@@ -1316,25 +1286,6 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
|
|
|
1316
1286
|
enter(LOGIN);
|
|
1317
1287
|
}, 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] }) }));
|
|
1318
1288
|
};
|
|
1319
|
-
// The agreement is asked OVER the room, not instead of it. Returning the
|
|
1320
|
-
// consent on its own replaced the whole pane: the question the reader had
|
|
1321
|
-
// just typed left the screen, and the first thing a stranger saw of the
|
|
1322
|
-
// product was a page about training data. The room stays; the sheet sits on
|
|
1323
|
-
// top of it, and the question is still there behind it.
|
|
1324
|
-
const consent = asking === null ? null : (_jsx(Consent, { question: told(asking).text, onAgree: () => {
|
|
1325
|
-
const s = store();
|
|
1326
|
-
// Nothing to persist into is not a reason to refuse the turn the
|
|
1327
|
-
// reader just agreed to; it means the question is asked again next
|
|
1328
|
-
// time, which is the honest cost of a browser that stores nothing.
|
|
1329
|
-
if (s)
|
|
1330
|
-
grantConsent(s);
|
|
1331
|
-
const held = asking;
|
|
1332
|
-
setAsking(null);
|
|
1333
|
-
if (held)
|
|
1334
|
-
speak(held);
|
|
1335
|
-
}, onCancel: () => {
|
|
1336
|
-
setAsking(null);
|
|
1337
|
-
} }));
|
|
1338
1289
|
// Dev, from the Summary's Create file or site: a move inside the app where the
|
|
1339
1290
|
// host's app reads the path (hanzo.ai), else Dev on hanzo.ai, as the shell's
|
|
1340
1291
|
// Dev place goes from a host without it (hanzo.team).
|
|
@@ -1356,10 +1307,46 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
|
|
|
1356
1307
|
};
|
|
1357
1308
|
// THE COMPOSER'S OPTIONS, held apart so its keys (the `/` and `@` lists' arrows,
|
|
1358
1309
|
// Ctrl+U) hook in through `useMention` before the frame is drawn.
|
|
1310
|
+
// What pressing a suggestion does, at once: open what it names, send its prompt
|
|
1311
|
+
// here (the room is a new chat while one shows), hand its run to Dev, or make
|
|
1312
|
+
// its scheduled task and show it in Scheduled tasks.
|
|
1313
|
+
const hint = (a) => {
|
|
1314
|
+
switch (a.kind) {
|
|
1315
|
+
case "open":
|
|
1316
|
+
if (a.item)
|
|
1317
|
+
want(a.item);
|
|
1318
|
+
if (/^https:\/\//.test(a.to))
|
|
1319
|
+
window.open(a.to, "_blank", "noopener,noreferrer");
|
|
1320
|
+
else
|
|
1321
|
+
go(a.to.startsWith("/") ? a.to : app(a.to));
|
|
1322
|
+
return;
|
|
1323
|
+
case "send":
|
|
1324
|
+
submit(a.prompt);
|
|
1325
|
+
return;
|
|
1326
|
+
case "task":
|
|
1327
|
+
hand("task", a.prompt);
|
|
1328
|
+
go(app(""));
|
|
1329
|
+
return;
|
|
1330
|
+
case "schedule":
|
|
1331
|
+
void book(target, a, "chat").then((made) => go(app(devPath({ kind: "automation", id: made.id }))), (e) => setPrepWrong(e instanceof Error && e.message ? e.message : "The scheduled task could not be made."));
|
|
1332
|
+
return;
|
|
1333
|
+
}
|
|
1334
|
+
};
|
|
1335
|
+
// A chat a suggestion in Dev left for this room is sent once the room is a new
|
|
1336
|
+
// chat: the address's own conversation let go of, nothing said yet.
|
|
1337
|
+
const [handed] = useState(() => ({ prompt: anonymous ? "" : taken("send") }));
|
|
1338
|
+
useEffect(() => {
|
|
1339
|
+
if (!handed.prompt || open || said.length)
|
|
1340
|
+
return;
|
|
1341
|
+
const p = handed.prompt;
|
|
1342
|
+
handed.prompt = "";
|
|
1343
|
+
submit(p);
|
|
1344
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1345
|
+
}, [open, said.length]);
|
|
1359
1346
|
const composing = {
|
|
1360
1347
|
value: draft,
|
|
1361
1348
|
onChange: setDraft,
|
|
1362
|
-
onSend: submit,
|
|
1349
|
+
onSend: () => submit(),
|
|
1363
1350
|
onStop: chat.stop,
|
|
1364
1351
|
busy: chat.streaming,
|
|
1365
1352
|
// Files alone are a message: what is held goes with an empty draft too.
|
|
@@ -1424,7 +1411,7 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
|
|
|
1424
1411
|
onLanguage: speakIn,
|
|
1425
1412
|
talk,
|
|
1426
1413
|
// While nothing has been said: what is worth doing next.
|
|
1427
|
-
below: said.length === 0 && !anonymous ? _jsx(Suggestions, { rows: hints.rows,
|
|
1414
|
+
below: said.length === 0 && !anonymous ? _jsx(Suggestions, { rows: hints.rows, onAct: hint, onDismiss: hints.dismiss }) : undefined,
|
|
1428
1415
|
};
|
|
1429
1416
|
const mention = useMention(composing);
|
|
1430
1417
|
return (_jsxs(YStack, { flex: 1, minH: 0, width: "100%", position: "relative", "data-slot": "chat-room",
|
|
@@ -1536,9 +1523,5 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
|
|
|
1536
1523
|
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] })),
|
|
1537
1524
|
// The transcript reads at the same measure. `Thread` defaults to 768
|
|
1538
1525
|
// itself; handing it the column here is what lets Width reach it.
|
|
1539
|
-
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" })] })) }),
|
|
1540
|
-
}
|
|
1541
|
-
/** The agreement free asks for, in @hanzo/ai's words so every surface says one thing. */
|
|
1542
|
-
function Consent({ question, onAgree, onCancel, }) {
|
|
1543
|
-
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" })] })] }) }));
|
|
1526
|
+
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" })] })) }), _jsx(Upgrade, { ask: ask, onClose: () => setAsk(null) })] }));
|
|
1544
1527
|
}
|
package/dist/Me.js
CHANGED
|
@@ -5,7 +5,8 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
5
5
|
// other app (Shell.tsx).
|
|
6
6
|
//
|
|
7
7
|
// Top to bottom: who, with their plan, a row whose › opens the account itself;
|
|
8
|
-
// then Upgrade plan, Personalization, Profile and Settings (⌘, / Ctrl+,);
|
|
8
|
+
// then Upgrade plan, Personalization, Profile and Settings (⌘, / Ctrl+,); Beta,
|
|
9
|
+
// the person's place on the beta waitlist (the site's /beta/status); then
|
|
9
10
|
// Help ›, the site's own pages and the keyboard shortcuts (⌘/ / Ctrl+/ from
|
|
10
11
|
// anywhere); and Log out. Every row opens a real destination: Settings'
|
|
11
12
|
// sections through `showSettings` (open.ts), which the app answers with the
|
|
@@ -29,7 +30,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
29
30
|
// mints a token that carries it.
|
|
30
31
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
31
32
|
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
32
|
-
import { ArrowUpRight, Building2, Check, ChevronsUpDown, CircleHelp, CircleUserRound, Download, FileText, Info, Keyboard, LifeBuoy, LogOut, Palette, Plus, ScrollText, Settings, ShieldCheck, Sparkles, Users, X, } from 'lucide-react';
|
|
33
|
+
import { ArrowUpRight, Building2, Check, ChevronsUpDown, CircleHelp, CircleUserRound, Download, FileText, FlaskConical, Info, Keyboard, LifeBuoy, LogOut, Palette, Plus, ScrollText, Settings, ShieldCheck, Sparkles, Users, X, } from 'lucide-react';
|
|
33
34
|
import { label, Meter, said as spoken, useStanding } from '@hanzo/build';
|
|
34
35
|
import { useOrganizations } from '@hanzo/iam/react';
|
|
35
36
|
import { Button, Dialog, DialogContent, DialogTitle, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, Input, Kbd, } from '@hanzo/ui';
|
|
@@ -196,7 +197,7 @@ export function Me({ host, narrow = false, above = false, onOrg, }) {
|
|
|
196
197
|
// Clear of the rail's mark (or the row) it opens from, and flush with its foot.
|
|
197
198
|
offset: 8, children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(XStack, { ref: (el) => {
|
|
198
199
|
row.current = el instanceof HTMLElement ? el : null;
|
|
199
|
-
}, render: "button", "data-slot": "me", "aria-label": `Account: ${[who, current].filter(Boolean).join(' · ')}`, ...{ title: who }, width: narrow ? 36 : '100%', minH: narrow ? 36 : 48, px: narrow ? 0 : '$2', gap: "$2.5", rounded: narrow ? 999 : '$5', items: "center", justify: narrow ? 'center' : 'flex-start', hoverStyle: { bg: '$hover' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, cursor: "pointer", children: [_jsx(Face, { host: host, size: narrow ? 26 : 32 }), narrow ? null : (_jsxs(_Fragment, { children: [_jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$3", fontWeight: "600", color: "$ink", numberOfLines: 1, text: "left", children: who }), working ? (_jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, text: "left", children: working })) : null] }), _jsx(ChevronsUpDown, { size: 14, "aria-hidden": true, opacity: 0.6 })] }))] }) }), _jsxs(DropdownMenuContent, { minW: 260, maxW: "calc(100vw - 16px)", "aria-labelledby": undefined, "aria-label": "Account", onCloseAutoFocus: back, children: [_jsxs(DropdownMenuSub, { open: account, onOpenChange: setAccount, children: [_jsxs(DropdownMenuSubTrigger, { minH: 48, gap: "$2.5", children: [_jsx(Face, { host: host, size: 28 }), _jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$2", fontWeight: "600", color: "$ink", numberOfLines: 1, children: who }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: host.person.email || working })] })] }), account ? (_jsxs(DropdownMenuSubContent, { minW: 260, maxW: "calc(100vw - 16px)", pointerEvents: "auto", children: [_jsx(Meter, { read: read, onPlans: () => leave('here'), onBilling: () => (setOpen(false), showSettings('billing')) }), _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuLabel, { children: "Organizations" }), all.map((o) => (_jsx(DropdownMenuItem, { onSelect: () => o.name !== current && onOrg(o.name), children: _jsx(Line, { icon: _jsx(Mark, { org: o }), sub: [o.personal ? 'Personal' : said(o.role), planFor(o)].filter(Boolean).join(' · '), trail: o.name === current ? _jsx(Check, { size: 16, "aria-label": "Current" }) : null, children: o.display }) }, o.name))), _jsx(DropdownMenuItem, { onSelect: () => setCreating(true), children: _jsx(Line, { icon: _jsx(Plus, { size: 16, "aria-hidden": true }), children: "Create organization" }) }), _jsx(DropdownMenuItem, { onSelect: () => window.location.assign(site(ORG_SETTINGS)), children: _jsx(Line, { icon: _jsx(Settings, { size: 16, "aria-hidden": true }), children: "Organization settings" }) }), where().team ? null : here && !here.personal ? (_jsx(DropdownMenuItem, { onSelect: () => out(`${ENTRY}&org=${encodeURIComponent(here.name)}`), children: _jsx(Line, { icon: _jsx(Users, { size: 16, "aria-hidden": true }), trail: _jsx(ArrowUpRight, { size: 14, "aria-hidden": true, opacity: 0.6 }), children: "Open in Hanzo Team" }) })) : (_jsx(DropdownMenuItem, { onSelect: () => setCreating(true), children: _jsx(Line, { icon: _jsx(Users, { size: 16, "aria-hidden": true }), children: "Create a team" }) })), superAdmin() ? (_jsx(DropdownMenuItem, { onSelect: choose, children: _jsx(Line, { icon: _jsx(Building2, { size: 16, "aria-hidden": true }), children: "All organizations" }) })) : null] })) : null] }), team ? (_jsx(DropdownMenuItem, { "data-slot": "me-team", onSelect: () => team.name !== current && onOrg(team.name), children: _jsx(Line, { icon: _jsx(Mark, { org: team }), trail: team.name === current ? _jsx(Check, { size: 16, "aria-label": "Current" }) : null, children: `${team.display} · ${teamPlan || 'No plan'}` }) })) : null, personal ? (_jsx(DropdownMenuItem, { "data-slot": "me-personal", onSelect: () => personal.name !== current && onOrg(personal.name), actionsVisibility: "always", actions: ownPlan ? undefined : (_jsx(Button, { size: "xs", variant: "outline", onPress: getPro, children: "Get Pro" })), children: _jsx(Line, { icon: _jsx(CircleUserRound, { size: 16, "aria-hidden": true }), trail: personal.name === current ? _jsx(Check, { size: 16, "aria-label": "Current" }) : null, children: `Personal · ${ownPlan || 'No plan'}` }) })) : null, _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuItem, { onSelect: () => leave('here'), children: _jsx(Line, { icon: _jsx(Sparkles, { size: 16, "aria-hidden": true }), children: upgrade(read.value?.label?.name) }) }), _jsx(DropdownMenuItem, { onSelect: () => showSettings('general'), children: _jsx(Line, { icon: _jsx(Palette, { size: 16, "aria-hidden": true }), children: "Personalization" }) }), _jsx(DropdownMenuItem, { onSelect: () => showSettings('account'), children: _jsx(Line, { icon: _jsx(CircleUserRound, { size: 16, "aria-hidden": true }), children: "Profile" }) }), _jsx(DropdownMenuItem, { "aria-keyshortcuts": "Control+, Meta+,", onSelect: () => showSettings('general'), children: _jsx(Line, { icon: _jsx(Settings, { size: 16, "aria-hidden": true }), trail: _jsx(DropdownMenuShortcut, { "aria-hidden": true, children: apple ? '⌘,' : 'Ctrl+,' }), children: "Settings" }) }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuSub, { open: help, onOpenChange: setHelp, children: [_jsx(DropdownMenuSubTrigger, { children: _jsx(Line, { icon: _jsx(LifeBuoy, { size: 16, "aria-hidden": true }), children: "Help" }) }), help ? (_jsxs(DropdownMenuSubContent, { minW: 240, maxW: "calc(100vw - 16px)", pointerEvents: "auto", children: [pages().map((p) => (_jsx(DropdownMenuItem, { onSelect: () => out(p.href), children: _jsx(Line, { icon: ICONS[p.key], trail: p.key === 'help' ? _jsx(ArrowUpRight, { size: 14, "aria-hidden": true, opacity: 0.6 }) : null, children: p.label }) }, p.key))), _jsx(DropdownMenuItem, { "aria-keyshortcuts": "Control+/ Meta+/", onSelect: () => setKeys(true), children: _jsx(Line, { icon: _jsx(Keyboard, { size: 16, "aria-hidden": true }), trail: _jsx(DropdownMenuShortcut, { "aria-hidden": true, children: apple ? '⌘/' : 'Ctrl+/' }), children: "Keyboard shortcuts" }) }), _jsx(DropdownMenuSeparator, {}), legal().map((p) => (_jsx(DropdownMenuItem, { onSelect: () => out(p.href), children: _jsx(Line, { icon: ICONS[p.key], children: p.label }) }, p.key)))] })) : null] }), host.signOut ? (_jsx(DropdownMenuItem, { onSelect: () => host.signOut?.(), children: _jsx(Line, { icon: _jsx(LogOut, { size: 16, "aria-hidden": true }), children: "Log out" }) })) : null] })] }), _jsx(Create, { open: creating, onOpenChange: setCreating }), _jsx(Shortcuts, { open: keys, onOpenChange: setKeys, apple: apple })] }));
|
|
200
|
+
}, render: "button", "data-slot": "me", "aria-label": `Account: ${[who, current].filter(Boolean).join(' · ')}`, ...{ title: who }, width: narrow ? 36 : '100%', minH: narrow ? 36 : 48, px: narrow ? 0 : '$2', gap: "$2.5", rounded: narrow ? 999 : '$5', items: "center", justify: narrow ? 'center' : 'flex-start', hoverStyle: { bg: '$hover' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, cursor: "pointer", children: [_jsx(Face, { host: host, size: narrow ? 26 : 32 }), narrow ? null : (_jsxs(_Fragment, { children: [_jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$3", fontWeight: "600", color: "$ink", numberOfLines: 1, text: "left", children: who }), working ? (_jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, text: "left", children: working })) : null] }), _jsx(ChevronsUpDown, { size: 14, "aria-hidden": true, opacity: 0.6 })] }))] }) }), _jsxs(DropdownMenuContent, { minW: 260, maxW: "calc(100vw - 16px)", "aria-labelledby": undefined, "aria-label": "Account", onCloseAutoFocus: back, children: [_jsxs(DropdownMenuSub, { open: account, onOpenChange: setAccount, children: [_jsxs(DropdownMenuSubTrigger, { minH: 48, gap: "$2.5", children: [_jsx(Face, { host: host, size: 28 }), _jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$2", fontWeight: "600", color: "$ink", numberOfLines: 1, children: who }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: host.person.email || working })] })] }), account ? (_jsxs(DropdownMenuSubContent, { minW: 260, maxW: "calc(100vw - 16px)", pointerEvents: "auto", children: [_jsx(Meter, { read: read, onPlans: () => leave('here'), onBilling: () => (setOpen(false), showSettings('billing')) }), _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuLabel, { children: "Organizations" }), all.map((o) => (_jsx(DropdownMenuItem, { onSelect: () => o.name !== current && onOrg(o.name), children: _jsx(Line, { icon: _jsx(Mark, { org: o }), sub: [o.personal ? 'Personal' : said(o.role), planFor(o)].filter(Boolean).join(' · '), trail: o.name === current ? _jsx(Check, { size: 16, "aria-label": "Current" }) : null, children: o.display }) }, o.name))), _jsx(DropdownMenuItem, { onSelect: () => setCreating(true), children: _jsx(Line, { icon: _jsx(Plus, { size: 16, "aria-hidden": true }), children: "Create organization" }) }), _jsx(DropdownMenuItem, { onSelect: () => window.location.assign(site(ORG_SETTINGS)), children: _jsx(Line, { icon: _jsx(Settings, { size: 16, "aria-hidden": true }), children: "Organization settings" }) }), where().team ? null : here && !here.personal ? (_jsx(DropdownMenuItem, { onSelect: () => out(`${ENTRY}&org=${encodeURIComponent(here.name)}`), children: _jsx(Line, { icon: _jsx(Users, { size: 16, "aria-hidden": true }), trail: _jsx(ArrowUpRight, { size: 14, "aria-hidden": true, opacity: 0.6 }), children: "Open in Hanzo Team" }) })) : (_jsx(DropdownMenuItem, { onSelect: () => setCreating(true), children: _jsx(Line, { icon: _jsx(Users, { size: 16, "aria-hidden": true }), children: "Create a team" }) })), superAdmin() ? (_jsx(DropdownMenuItem, { onSelect: choose, children: _jsx(Line, { icon: _jsx(Building2, { size: 16, "aria-hidden": true }), children: "All organizations" }) })) : null] })) : null] }), team ? (_jsx(DropdownMenuItem, { "data-slot": "me-team", onSelect: () => team.name !== current && onOrg(team.name), children: _jsx(Line, { icon: _jsx(Mark, { org: team }), trail: team.name === current ? _jsx(Check, { size: 16, "aria-label": "Current" }) : null, children: `${team.display} · ${teamPlan || 'No plan'}` }) })) : null, personal ? (_jsx(DropdownMenuItem, { "data-slot": "me-personal", onSelect: () => personal.name !== current && onOrg(personal.name), actionsVisibility: "always", actions: ownPlan ? undefined : (_jsx(Button, { size: "xs", variant: "outline", onPress: getPro, children: "Get Pro" })), children: _jsx(Line, { icon: _jsx(CircleUserRound, { size: 16, "aria-hidden": true }), trail: personal.name === current ? _jsx(Check, { size: 16, "aria-label": "Current" }) : null, children: `Personal · ${ownPlan || 'No plan'}` }) })) : null, _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuItem, { onSelect: () => leave('here'), children: _jsx(Line, { icon: _jsx(Sparkles, { size: 16, "aria-hidden": true }), children: upgrade(read.value?.label?.name) }) }), _jsx(DropdownMenuItem, { onSelect: () => showSettings('general'), children: _jsx(Line, { icon: _jsx(Palette, { size: 16, "aria-hidden": true }), children: "Personalization" }) }), _jsx(DropdownMenuItem, { onSelect: () => showSettings('account'), children: _jsx(Line, { icon: _jsx(CircleUserRound, { size: 16, "aria-hidden": true }), children: "Profile" }) }), _jsx(DropdownMenuItem, { "aria-keyshortcuts": "Control+, Meta+,", onSelect: () => showSettings('general'), children: _jsx(Line, { icon: _jsx(Settings, { size: 16, "aria-hidden": true }), trail: _jsx(DropdownMenuShortcut, { "aria-hidden": true, children: apple ? '⌘,' : 'Ctrl+,' }), children: "Settings" }) }), _jsx(DropdownMenuItem, { onSelect: () => window.location.assign(site('/beta/status')), children: _jsx(Line, { icon: _jsx(FlaskConical, { size: 16, "aria-hidden": true }), children: "Beta" }) }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuSub, { open: help, onOpenChange: setHelp, children: [_jsx(DropdownMenuSubTrigger, { children: _jsx(Line, { icon: _jsx(LifeBuoy, { size: 16, "aria-hidden": true }), children: "Help" }) }), help ? (_jsxs(DropdownMenuSubContent, { minW: 240, maxW: "calc(100vw - 16px)", pointerEvents: "auto", children: [pages().map((p) => (_jsx(DropdownMenuItem, { onSelect: () => out(p.href), children: _jsx(Line, { icon: ICONS[p.key], trail: p.key === 'help' ? _jsx(ArrowUpRight, { size: 14, "aria-hidden": true, opacity: 0.6 }) : null, children: p.label }) }, p.key))), _jsx(DropdownMenuItem, { "aria-keyshortcuts": "Control+/ Meta+/", onSelect: () => setKeys(true), children: _jsx(Line, { icon: _jsx(Keyboard, { size: 16, "aria-hidden": true }), trail: _jsx(DropdownMenuShortcut, { "aria-hidden": true, children: apple ? '⌘/' : 'Ctrl+/' }), children: "Keyboard shortcuts" }) }), _jsx(DropdownMenuSeparator, {}), legal().map((p) => (_jsx(DropdownMenuItem, { onSelect: () => out(p.href), children: _jsx(Line, { icon: ICONS[p.key], children: p.label }) }, p.key)))] })) : null] }), host.signOut ? (_jsx(DropdownMenuItem, { onSelect: () => host.signOut?.(), children: _jsx(Line, { icon: _jsx(LogOut, { size: 16, "aria-hidden": true }), children: "Log out" }) })) : null] })] }), _jsx(Create, { open: creating, onOpenChange: setCreating }), _jsx(Shortcuts, { open: keys, onOpenChange: setKeys, apple: apple })] }));
|
|
200
201
|
}
|
|
201
202
|
/** The app's keys (help.ts `KEYS`), one row each, spelled for this platform. */
|
|
202
203
|
function Shortcuts({ open, onOpenChange, apple }) {
|
package/dist/Palette.d.ts
CHANGED
|
@@ -3,22 +3,36 @@ import { type Host } from '@hanzo/build';
|
|
|
3
3
|
import { type ShortcutKeys } from '@hanzo/ui';
|
|
4
4
|
import { type Place } from './lib/host.js';
|
|
5
5
|
import { type Hit } from './seek.js';
|
|
6
|
-
/** A row: what it is called, its glyph, and what pressing it does. */
|
|
6
|
+
/** A row: what it is called, its glyph, where it lives, and what pressing it does. */
|
|
7
7
|
export interface Action extends Hit {
|
|
8
8
|
icon?: ReactNode;
|
|
9
9
|
shortcut?: ShortcutKeys;
|
|
10
|
+
/** Where it lives, dimmed after its name: Settings, Customize, Help. */
|
|
11
|
+
where?: string;
|
|
10
12
|
run: () => void;
|
|
11
13
|
/** Pressing it keeps the palette open. */
|
|
12
14
|
stay?: boolean;
|
|
13
15
|
}
|
|
14
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Every place the reader can be taken: `top` is what an empty field lists, the
|
|
18
|
+
* products and the places of the app; `deep` is what only a query reaches.
|
|
19
|
+
* Without a host (hanzo.team's frame) the builder's registries are not there,
|
|
20
|
+
* and the products and the site's pages are.
|
|
21
|
+
*/
|
|
22
|
+
export declare function places(host: Host | undefined, go: (address: string) => void): {
|
|
23
|
+
top: Action[];
|
|
24
|
+
deep: Action[];
|
|
25
|
+
};
|
|
26
|
+
/** The palette's actions; a host that cannot do one (no theme to change, no org to pick) is not offered it. */
|
|
27
|
+
export declare function actions(host: Host | undefined, go: (address: string) => void, signOut: () => void, keys: () => void): Action[];
|
|
28
|
+
export declare function Palette({ onClose, go, actions: frame, mode, host, }: {
|
|
15
29
|
onClose: () => void;
|
|
16
|
-
/** Moves the app to one of its addresses (lib/host `talk`, `app`). */
|
|
30
|
+
/** Moves the app to one of its addresses (lib/host `talk`, `app`, `studio`). */
|
|
17
31
|
go: (address: string) => void;
|
|
18
|
-
/** The frame's own
|
|
32
|
+
/** The frame's own places; one with a palette row's id stands in for it, the rest follow under Go to. */
|
|
19
33
|
actions?: Action[];
|
|
20
|
-
/** The product the reader is in:
|
|
34
|
+
/** The product the reader is in: its own things lead a tie. */
|
|
21
35
|
mode?: Place['mode'];
|
|
22
|
-
/**
|
|
36
|
+
/** The builder's host: its registries, its runs, its theme and organization. */
|
|
23
37
|
host?: Host;
|
|
24
38
|
}): import("react").JSX.Element;
|