@hanzo/rooms 0.1.0
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/LICENSE-APACHE +205 -0
- package/LICENSE-MIT +21 -0
- package/README.md +23 -0
- package/dist/Account.d.ts +8 -0
- package/dist/Account.js +58 -0
- package/dist/Beluga.d.ts +12 -0
- package/dist/Beluga.js +152 -0
- package/dist/Billing.d.ts +1 -0
- package/dist/Billing.js +31 -0
- package/dist/Board.d.ts +9 -0
- package/dist/Board.js +447 -0
- package/dist/Boxes.d.ts +1 -0
- package/dist/Boxes.js +173 -0
- package/dist/CalScheduler.d.ts +15 -0
- package/dist/CalScheduler.js +196 -0
- package/dist/Chat.d.ts +35 -0
- package/dist/Chat.js +1049 -0
- package/dist/Column.d.ts +15 -0
- package/dist/Column.js +88 -0
- package/dist/Contacts.d.ts +1 -0
- package/dist/Contacts.js +301 -0
- package/dist/Directory.d.ts +24 -0
- package/dist/Directory.js +356 -0
- package/dist/Drive.d.ts +2 -0
- package/dist/Drive.js +466 -0
- package/dist/Find.d.ts +7 -0
- package/dist/Find.js +82 -0
- package/dist/Guide.d.ts +6 -0
- package/dist/Guide.js +272 -0
- package/dist/Home.d.ts +39 -0
- package/dist/Home.js +306 -0
- package/dist/Inbox.d.ts +9 -0
- package/dist/Inbox.js +193 -0
- package/dist/Market.d.ts +3 -0
- package/dist/Market.js +149 -0
- package/dist/Me.d.ts +27 -0
- package/dist/Me.js +251 -0
- package/dist/Meet.d.ts +9 -0
- package/dist/Meet.js +370 -0
- package/dist/ModeMark.d.ts +8 -0
- package/dist/ModeMark.js +18 -0
- package/dist/OrgSwitcher.d.ts +31 -0
- package/dist/OrgSwitcher.js +120 -0
- package/dist/Orgs.d.ts +10 -0
- package/dist/Orgs.js +582 -0
- package/dist/Profile.d.ts +4 -0
- package/dist/Profile.js +67 -0
- package/dist/Prose.d.ts +5 -0
- package/dist/Prose.js +111 -0
- package/dist/ProviderMark.d.ts +17 -0
- package/dist/ProviderMark.js +277 -0
- package/dist/Recents.d.ts +10 -0
- package/dist/Recents.js +91 -0
- package/dist/RightPane.d.ts +13 -0
- package/dist/RightPane.js +426 -0
- package/dist/Room.d.ts +8 -0
- package/dist/Room.js +50 -0
- package/dist/Scene.d.ts +12 -0
- package/dist/Scene.js +105 -0
- package/dist/Settings.d.ts +46 -0
- package/dist/Settings.js +321 -0
- package/dist/Setup.d.ts +1 -0
- package/dist/Setup.js +36 -0
- package/dist/Share.d.ts +3 -0
- package/dist/Share.js +130 -0
- package/dist/Shared.d.ts +1 -0
- package/dist/Shared.js +113 -0
- package/dist/Shell.d.ts +63 -0
- package/dist/Shell.js +1225 -0
- package/dist/Start.d.ts +1 -0
- package/dist/Start.js +96 -0
- package/dist/Support.d.ts +14 -0
- package/dist/Support.js +122 -0
- package/dist/Talk.d.ts +4 -0
- package/dist/Talk.js +107 -0
- package/dist/Upgrade.d.ts +11 -0
- package/dist/Upgrade.js +75 -0
- package/dist/Usage.d.ts +1 -0
- package/dist/Usage.js +21 -0
- package/dist/Vibe.d.ts +2 -0
- package/dist/Vibe.js +188 -0
- package/dist/Work.d.ts +12 -0
- package/dist/Work.js +256 -0
- package/dist/bots/Bots.d.ts +2 -0
- package/dist/bots/Bots.js +456 -0
- package/dist/bots/Lights.d.ts +8 -0
- package/dist/bots/Lights.js +13 -0
- package/dist/bots/api.d.ts +120 -0
- package/dist/bots/api.js +75 -0
- package/dist/bots/face.d.ts +5 -0
- package/dist/bots/face.js +13 -0
- package/dist/bots/ink.d.ts +7 -0
- package/dist/bots/ink.js +29 -0
- package/dist/bots/kit.d.ts +113 -0
- package/dist/bots/kit.js +43 -0
- package/dist/bots/runtime.d.ts +4 -0
- package/dist/bots/runtime.js +326 -0
- package/dist/card.d.ts +52 -0
- package/dist/card.js +52 -0
- package/dist/cast.d.ts +143 -0
- package/dist/cast.js +251 -0
- package/dist/connect.d.ts +29 -0
- package/dist/connect.js +64 -0
- package/dist/contacts-store.d.ts +111 -0
- package/dist/contacts-store.js +276 -0
- package/dist/conversations.d.ts +54 -0
- package/dist/conversations.js +98 -0
- package/dist/copy.d.ts +23 -0
- package/dist/copy.js +39 -0
- package/dist/crew.d.ts +36 -0
- package/dist/crew.js +304 -0
- package/dist/dev.d.ts +3 -0
- package/dist/dev.js +102 -0
- package/dist/enso.d.ts +70 -0
- package/dist/enso.js +112 -0
- package/dist/failure.d.ts +57 -0
- package/dist/failure.js +143 -0
- package/dist/ground.d.ts +32 -0
- package/dist/ground.js +32 -0
- package/dist/host.d.ts +76 -0
- package/dist/host.js +76 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +12 -0
- package/dist/kept.d.ts +5 -0
- package/dist/kept.js +27 -0
- package/dist/lib/account.d.ts +49 -0
- package/dist/lib/account.js +229 -0
- package/dist/lib/ai.d.ts +46 -0
- package/dist/lib/ai.js +140 -0
- package/dist/lib/api.d.ts +43 -0
- package/dist/lib/api.js +62 -0
- package/dist/lib/bare.d.ts +9 -0
- package/dist/lib/bare.js +7 -0
- package/dist/lib/coding.d.ts +105 -0
- package/dist/lib/coding.js +87 -0
- package/dist/lib/destination.d.ts +102 -0
- package/dist/lib/destination.js +260 -0
- package/dist/lib/durable.d.ts +66 -0
- package/dist/lib/durable.js +107 -0
- package/dist/lib/first.d.ts +11 -0
- package/dist/lib/first.js +23 -0
- package/dist/lib/home.d.ts +61 -0
- package/dist/lib/home.js +54 -0
- package/dist/lib/host.d.ts +104 -0
- package/dist/lib/host.js +132 -0
- package/dist/lib/hostname.d.ts +1 -0
- package/dist/lib/hostname.js +14 -0
- package/dist/lib/hydrated.d.ts +1 -0
- package/dist/lib/hydrated.js +20 -0
- package/dist/lib/integrations.d.ts +21 -0
- package/dist/lib/integrations.js +1 -0
- package/dist/lib/intent.d.ts +6 -0
- package/dist/lib/intent.js +71 -0
- package/dist/lib/invite.d.ts +155 -0
- package/dist/lib/invite.js +245 -0
- package/dist/lib/limits.d.ts +55 -0
- package/dist/lib/limits.js +119 -0
- package/dist/lib/list.d.ts +9 -0
- package/dist/lib/list.js +11 -0
- package/dist/lib/mark.d.ts +13 -0
- package/dist/lib/mark.js +85 -0
- package/dist/lib/mix.d.ts +22 -0
- package/dist/lib/mix.js +22 -0
- package/dist/lib/models.d.ts +27 -0
- package/dist/lib/models.js +106 -0
- package/dist/lib/org.d.ts +109 -0
- package/dist/lib/org.js +140 -0
- package/dist/lib/path.d.ts +9 -0
- package/dist/lib/path.js +29 -0
- package/dist/lib/plans.d.ts +193 -0
- package/dist/lib/plans.js +310 -0
- package/dist/lib/prose.d.ts +13 -0
- package/dist/lib/prose.js +62 -0
- package/dist/lib/reach.d.ts +17 -0
- package/dist/lib/reach.js +70 -0
- package/dist/lib/rooms.json +23 -0
- package/dist/lib/served.d.ts +20 -0
- package/dist/lib/served.js +92 -0
- package/dist/lib/session.d.ts +211 -0
- package/dist/lib/session.js +493 -0
- package/dist/lib/share.d.ts +131 -0
- package/dist/lib/share.js +216 -0
- package/dist/lib/signout.d.ts +14 -0
- package/dist/lib/signout.js +40 -0
- package/dist/lib/tags.d.ts +27 -0
- package/dist/lib/tags.js +46 -0
- package/dist/lib/tap.d.ts +17 -0
- package/dist/lib/tap.js +9 -0
- package/dist/lib/tier.d.ts +115 -0
- package/dist/lib/tier.js +207 -0
- package/dist/lib/tiers.d.ts +9 -0
- package/dist/lib/tiers.js +141 -0
- package/dist/lib/todo.d.ts +125 -0
- package/dist/lib/todo.js +379 -0
- package/dist/look.d.ts +1 -0
- package/dist/look.js +21 -0
- package/dist/meters.d.ts +8 -0
- package/dist/meters.js +31 -0
- package/dist/model.d.ts +4 -0
- package/dist/model.js +44 -0
- package/dist/network.d.ts +64 -0
- package/dist/network.js +216 -0
- package/dist/open.d.ts +81 -0
- package/dist/open.js +149 -0
- package/dist/pane.d.ts +92 -0
- package/dist/pane.js +231 -0
- package/dist/progress.d.ts +46 -0
- package/dist/progress.js +51 -0
- package/dist/recent.d.ts +67 -0
- package/dist/recent.js +161 -0
- package/dist/rooms.d.ts +131 -0
- package/dist/rooms.js +195 -0
- package/dist/roster.d.ts +28 -0
- package/dist/roster.js +70 -0
- package/dist/span.d.ts +14 -0
- package/dist/span.js +50 -0
- package/dist/speech.d.ts +44 -0
- package/dist/speech.js +229 -0
- package/dist/stars.d.ts +16 -0
- package/dist/stars.js +75 -0
- package/dist/tabs.d.ts +26 -0
- package/dist/tabs.js +25 -0
- package/dist/team.d.ts +11 -0
- package/dist/team.gen.d.ts +10 -0
- package/dist/team.gen.js +11 -0
- package/dist/team.js +30 -0
- package/dist/tools.d.ts +35 -0
- package/dist/tools.js +143 -0
- package/dist/tours.d.ts +24 -0
- package/dist/tours.js +311 -0
- package/dist/waiting.d.ts +15 -0
- package/dist/waiting.js +17 -0
- package/dist/where.d.ts +54 -0
- package/dist/where.js +14 -0
- package/package.json +108 -0
package/dist/Guide.js
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
// The first-run guide: what this place can do, said once, standing next to the
|
|
4
|
+
// thing that does it.
|
|
5
|
+
//
|
|
6
|
+
// WHY COACHMARKS AND NOT A TOUR PAGE. A page of screenshots is read somewhere
|
|
7
|
+
// else and remembered nowhere; a card pinned to the actual control is read
|
|
8
|
+
// while looking at it, and the next time the reader sees that control they have
|
|
9
|
+
// already been introduced. So every step here RESOLVES A LIVE ELEMENT and
|
|
10
|
+
// positions against its measured rectangle. Nothing is drawn at a coordinate
|
|
11
|
+
// somebody typed.
|
|
12
|
+
//
|
|
13
|
+
// A STEP WHOSE ANCHOR IS ABSENT IS SKIPPED, not faked. Destinations come and go
|
|
14
|
+
// with the org's tier and with what the reader has hidden, so the tour is a
|
|
15
|
+
// list of candidates and the ones present at that moment are the tour. That is
|
|
16
|
+
// also why anchors are found by what the elements ALREADY carry — their aria
|
|
17
|
+
// labels, their roles, their text — rather than by `data-tour` attributes
|
|
18
|
+
// sprinkled through six other components: a marker added for a tour is a marker
|
|
19
|
+
// somebody deletes while tidying, and the tour breaks silently three months
|
|
20
|
+
// later. An accessible name is load-bearing for other reasons and survives.
|
|
21
|
+
//
|
|
22
|
+
// IT RUNS ONCE, and "once" belongs to the person rather than to the browser.
|
|
23
|
+
// Completion is written to the caller's own preference document (`/v1/pref`,
|
|
24
|
+
// whose `prefs` is an opaque object each surface keys as it likes), so it
|
|
25
|
+
// follows them to a second machine. localStorage is written FIRST and read
|
|
26
|
+
// first: it settles instantly, it answers for a reader who never signs in, and
|
|
27
|
+
// it keeps the guide from re-opening in the seconds a PATCH is in flight.
|
|
28
|
+
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
29
|
+
import { createPortal } from 'react-dom';
|
|
30
|
+
import { Text, View, XStack, YStack } from '@hanzo/gui';
|
|
31
|
+
import { Button } from '@hanzo/ui';
|
|
32
|
+
import { useIam } from '@hanzo/iam/react';
|
|
33
|
+
import { useAi } from './lib/ai.js';
|
|
34
|
+
import { Next, Scene } from './Scene.js';
|
|
35
|
+
import { TOURS } from './tours.js';
|
|
36
|
+
/** The ring on the spot: design's focus ring and its halo, one above the dim. gui hands a `calc()` to the page
|
|
37
|
+
* unchanged; its types only name custom properties. */
|
|
38
|
+
const RING = '0 0 0 1px var(--ring), 0 0 26px var(--ring-halo)';
|
|
39
|
+
const ABOVE = 'calc(var(--z-overlay) + 1)';
|
|
40
|
+
/** design's smallest rung (`--text-floor`, 10px), below gui's `$1`; gui's font size takes a rung or a number. */
|
|
41
|
+
const FLOOR = 10;
|
|
42
|
+
/** The page's own leading (design's `--leading-base`, 1.35rem), which a face's scope would otherwise replace with its
|
|
43
|
+
* 20px; gui's line height takes a rung or a number. */
|
|
44
|
+
const LEAD = 21.6;
|
|
45
|
+
/** Where the card's pointer sits, and which two edges it draws, for each side the card can take. */
|
|
46
|
+
const POINTER = {
|
|
47
|
+
bottom: { t: -6.5, l: '50%', ml: -5.5, borderLeftWidth: 1, borderTopWidth: 1 },
|
|
48
|
+
top: { b: -6.5, l: '50%', ml: -5.5, borderRightWidth: 1, borderBottomWidth: 1 },
|
|
49
|
+
right: { l: -6.5, t: '50%', mt: -5.5, borderLeftWidth: 1, borderBottomWidth: 1 },
|
|
50
|
+
left: { r: -6.5, t: '50%', mt: -5.5, borderRightWidth: 1, borderTopWidth: 1 },
|
|
51
|
+
};
|
|
52
|
+
/** Where a room's tour is marked seen, in this browser and in the person's preferences. */
|
|
53
|
+
const keyOf = (room) => (room === 'chat' ? 'hanzo:chat:guide' : `hanzo:${room}:guide`);
|
|
54
|
+
const prefOf = (room) => `${room}Guide`;
|
|
55
|
+
const PAD = 8;
|
|
56
|
+
const GAP = 14;
|
|
57
|
+
const CARD_W = 352;
|
|
58
|
+
export function Guide({ room = 'chat' }) {
|
|
59
|
+
const KEY = keyOf(room);
|
|
60
|
+
const PREF = prefOf(room);
|
|
61
|
+
const STOPS = TOURS[room];
|
|
62
|
+
const { client } = useAi();
|
|
63
|
+
// ONLY FOR SOMEONE WHO IS SIGNED IN. `useAi` hands back a client either way —
|
|
64
|
+
// the anonymous one the free composer runs on — so a client is not proof of a
|
|
65
|
+
// reader. Opening for a visitor put a dimmed backdrop over the page before
|
|
66
|
+
// they had an account, and its panels sat on top of the Sign in button: the
|
|
67
|
+
// guide blocked the one action the page was asking for. It also has nothing
|
|
68
|
+
// true to say to a stranger, since half of what it points at is theirs only
|
|
69
|
+
// once they have a workspace.
|
|
70
|
+
const { isAuthenticated, isLoading } = useIam();
|
|
71
|
+
const [live, setLive] = useState(false);
|
|
72
|
+
const [i, setI] = useState(0);
|
|
73
|
+
const [spot, setSpot] = useState(null);
|
|
74
|
+
const [side, setSide] = useState('bottom');
|
|
75
|
+
const [card, setCard] = useState(null);
|
|
76
|
+
const cardRef = useRef(null);
|
|
77
|
+
// The stops whose anchors exist right now. A destination the org does not
|
|
78
|
+
// carry has no element, so it is not a step — the tour describes THIS
|
|
79
|
+
// workspace rather than a catalogue.
|
|
80
|
+
const stops = useMemo(() => (live ? STOPS.filter((s) => s.find() !== null) : STOPS), [live, STOPS]);
|
|
81
|
+
const stop = stops[i];
|
|
82
|
+
const done = useCallback((how) => {
|
|
83
|
+
setLive(false);
|
|
84
|
+
const mark = JSON.stringify({ at: new Date().toISOString(), how });
|
|
85
|
+
try {
|
|
86
|
+
localStorage.setItem(KEY, mark);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
/* a browser with storage off still gets the guide once per visit */
|
|
90
|
+
}
|
|
91
|
+
// Follows the person, not the browser. Failure is silent on purpose: the
|
|
92
|
+
// local mark already settled it, and a guide is not worth an error.
|
|
93
|
+
client?.http
|
|
94
|
+
.json({ method: 'PATCH', path: '/v1/pref', body: { prefs: { [PREF]: { at: new Date().toISOString(), how } } } })
|
|
95
|
+
.catch(() => { });
|
|
96
|
+
}, [client, KEY, PREF]);
|
|
97
|
+
// Should it open at all? Local first — it answers instantly and it answers
|
|
98
|
+
// for a reader who never signs in.
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
if (isLoading || !isAuthenticated)
|
|
101
|
+
return;
|
|
102
|
+
let held = false;
|
|
103
|
+
try {
|
|
104
|
+
held = Boolean(localStorage.getItem(KEY));
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
/* treat unreadable storage as "not yet shown" */
|
|
108
|
+
}
|
|
109
|
+
if (held)
|
|
110
|
+
return;
|
|
111
|
+
let alive = true;
|
|
112
|
+
const open = () => {
|
|
113
|
+
if (!alive)
|
|
114
|
+
return;
|
|
115
|
+
// Wait for the room to actually render before pointing at it.
|
|
116
|
+
const t = window.setTimeout(() => alive && setLive(true), 900);
|
|
117
|
+
return () => window.clearTimeout(t);
|
|
118
|
+
};
|
|
119
|
+
if (!client)
|
|
120
|
+
return open();
|
|
121
|
+
client.http
|
|
122
|
+
.json({ path: '/v1/pref' })
|
|
123
|
+
.then((doc) => {
|
|
124
|
+
if (!alive)
|
|
125
|
+
return;
|
|
126
|
+
if (doc?.prefs && doc.prefs[PREF]) {
|
|
127
|
+
try {
|
|
128
|
+
localStorage.setItem(KEY, JSON.stringify(doc.prefs[PREF]));
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
/* nothing to do */
|
|
132
|
+
}
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
// THE TOUR IS FOR A NEW ACCOUNT. One that has already had a conversation
|
|
136
|
+
// here is not new: it may never have met this tour, and it does not
|
|
137
|
+
// need to. It is marked as such, so the question is not asked again.
|
|
138
|
+
client.threads
|
|
139
|
+
.list()
|
|
140
|
+
.then((had) => {
|
|
141
|
+
if (!alive)
|
|
142
|
+
return;
|
|
143
|
+
if (Array.isArray(had) && had.length > 0)
|
|
144
|
+
done('unneeded');
|
|
145
|
+
else
|
|
146
|
+
open();
|
|
147
|
+
})
|
|
148
|
+
.catch(() => open());
|
|
149
|
+
})
|
|
150
|
+
.catch(() => open());
|
|
151
|
+
return () => {
|
|
152
|
+
alive = false;
|
|
153
|
+
};
|
|
154
|
+
}, [client, isAuthenticated, isLoading, KEY, PREF, done]);
|
|
155
|
+
// Measure the anchor, and re-measure when the page moves under it.
|
|
156
|
+
//
|
|
157
|
+
// NOT on every frame. A rAF loop that calls setSpot each tick re-renders the
|
|
158
|
+
// card sixty times a second, which makes it a target nothing can click: the
|
|
159
|
+
// element under the pointer is replaced between the hit test and the press.
|
|
160
|
+
// Measure on the events that actually move things, and write state only when
|
|
161
|
+
// the rectangle really changed.
|
|
162
|
+
useLayoutEffect(() => {
|
|
163
|
+
if (!live || !stop)
|
|
164
|
+
return;
|
|
165
|
+
let last = '';
|
|
166
|
+
const measure = () => {
|
|
167
|
+
const el = stop.find();
|
|
168
|
+
if (!el) {
|
|
169
|
+
setSpot(null);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const r = el.getBoundingClientRect();
|
|
173
|
+
const next = { top: r.top - PAD, left: r.left - PAD, width: r.width + PAD * 2, height: r.height + PAD * 2 };
|
|
174
|
+
const key = `${Math.round(next.top)}:${Math.round(next.left)}:${Math.round(next.width)}:${Math.round(next.height)}`;
|
|
175
|
+
if (key === last)
|
|
176
|
+
return;
|
|
177
|
+
last = key;
|
|
178
|
+
setSpot(next);
|
|
179
|
+
};
|
|
180
|
+
// Bring it into view once per step, then measure after the scroll settles.
|
|
181
|
+
stop.find()?.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
|
182
|
+
measure();
|
|
183
|
+
const settle = window.setTimeout(measure, 260);
|
|
184
|
+
const ro = new ResizeObserver(measure);
|
|
185
|
+
ro.observe(document.body);
|
|
186
|
+
const el = stop.find();
|
|
187
|
+
if (el)
|
|
188
|
+
ro.observe(el);
|
|
189
|
+
window.addEventListener('scroll', measure, true);
|
|
190
|
+
window.addEventListener('resize', measure);
|
|
191
|
+
return () => {
|
|
192
|
+
window.clearTimeout(settle);
|
|
193
|
+
ro.disconnect();
|
|
194
|
+
window.removeEventListener('scroll', measure, true);
|
|
195
|
+
window.removeEventListener('resize', measure);
|
|
196
|
+
};
|
|
197
|
+
}, [live, stop, i]);
|
|
198
|
+
// Place the card against the measured spot, flipping rather than overflowing.
|
|
199
|
+
useLayoutEffect(() => {
|
|
200
|
+
if (!spot) {
|
|
201
|
+
setCard(null);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const h = cardRef.current?.offsetHeight ?? 168;
|
|
205
|
+
const vw = window.innerWidth;
|
|
206
|
+
const vh = window.innerHeight;
|
|
207
|
+
let want = stop?.side ?? 'bottom';
|
|
208
|
+
if (want === 'bottom' && spot.top + spot.height + GAP + h > vh - 12)
|
|
209
|
+
want = 'top';
|
|
210
|
+
if (want === 'top' && spot.top - GAP - h < 12)
|
|
211
|
+
want = 'bottom';
|
|
212
|
+
if (want === 'right' && spot.left + spot.width + GAP + CARD_W > vw - 12)
|
|
213
|
+
want = 'left';
|
|
214
|
+
if (want === 'left' && spot.left - GAP - CARD_W < 12)
|
|
215
|
+
want = 'right';
|
|
216
|
+
let top;
|
|
217
|
+
let left;
|
|
218
|
+
if (want === 'bottom' || want === 'top') {
|
|
219
|
+
top = want === 'bottom' ? spot.top + spot.height + GAP : spot.top - GAP - h;
|
|
220
|
+
left = spot.left + spot.width / 2 - CARD_W / 2;
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
left = want === 'right' ? spot.left + spot.width + GAP : spot.left - GAP - CARD_W;
|
|
224
|
+
top = spot.top + spot.height / 2 - h / 2;
|
|
225
|
+
}
|
|
226
|
+
setSide(want);
|
|
227
|
+
setCard({ top: Math.max(12, Math.min(top, vh - h - 12)), left: Math.max(12, Math.min(left, vw - CARD_W - 12)) });
|
|
228
|
+
}, [spot, stop]);
|
|
229
|
+
const next = useCallback(() => {
|
|
230
|
+
if (i + 1 >= stops.length)
|
|
231
|
+
done('finished');
|
|
232
|
+
else
|
|
233
|
+
setI(i + 1);
|
|
234
|
+
}, [i, stops.length, done]);
|
|
235
|
+
const back = useCallback(() => setI((n) => Math.max(0, n - 1)), []);
|
|
236
|
+
useEffect(() => {
|
|
237
|
+
if (!live)
|
|
238
|
+
return;
|
|
239
|
+
const onKey = (e) => {
|
|
240
|
+
if (e.key === 'Escape') {
|
|
241
|
+
e.preventDefault();
|
|
242
|
+
done('skipped');
|
|
243
|
+
}
|
|
244
|
+
else if (e.key === 'ArrowRight' || e.key === 'Enter') {
|
|
245
|
+
e.preventDefault();
|
|
246
|
+
next();
|
|
247
|
+
}
|
|
248
|
+
else if (e.key === 'ArrowLeft') {
|
|
249
|
+
e.preventDefault();
|
|
250
|
+
back();
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
document.addEventListener('keydown', onKey, true);
|
|
254
|
+
return () => document.removeEventListener('keydown', onKey, true);
|
|
255
|
+
}, [live, next, back, done]);
|
|
256
|
+
if (!live || !stop || !spot || !card || typeof document === 'undefined')
|
|
257
|
+
return null;
|
|
258
|
+
const last = i + 1 >= stops.length;
|
|
259
|
+
// The dim is four panels around the spot rather than one box with a hole:
|
|
260
|
+
// a hole wants a mask, and a mask over a blurred backdrop reads as a seam.
|
|
261
|
+
const panel = { position: 'fixed', bg: 'var(--surface-scrim)', z: 'var(--z-overlay)', onClick: () => done('skipped') };
|
|
262
|
+
return createPortal(_jsxs("div", { role: "dialog", "aria-modal": "true", "aria-label": "Getting started with Hanzo", children: [_jsx(View, { ...panel, t: 0, l: 0, r: 0, height: Math.max(0, spot.top) }), _jsx(View, { ...panel, t: spot.top + spot.height, l: 0, r: 0, b: 0 }), _jsx(View, { ...panel, t: spot.top, l: 0, width: Math.max(0, spot.left), height: spot.height }), _jsx(View, { ...panel, t: spot.top, l: spot.left + spot.width, r: 0, height: spot.height }), _jsx(View, { position: "fixed", t: spot.top, l: spot.left, width: spot.width, height: spot.height, rounded: 10, borderWidth: 1, borderColor: "var(--ring)", boxShadow: RING, pointerEvents: "none", z: ABOVE }), stop.scene ? (_jsx(Scene, { step: `${i + 1} / ${stops.length}`, onNext: next, onSkip: () => done('skipped') })) : (_jsxs(YStack, { ref: cardRef, position: "fixed", t: card.top, l: card.left, width: CARD_W, z: "var(--z-modal)", bg: "var(--popover)", borderWidth: 1, borderColor: "var(--border-control)", rounded: 12, boxShadow: "var(--shadow-floating)", pt: 18, px: 18, pb: 15, children: [_jsx(View, { "aria-hidden": true, position: "absolute", width: 11, height: 11, bg: "var(--popover)", rotate: "45deg", borderColor: "var(--border-control)", ...POINTER[side] }), _jsxs(XStack, { items: "center", gap: 8, mb: 9, children: [_jsxs(Text, { fontFamily: "$body", lineHeight: LEAD, fontSize: FLOOR, letterSpacing: 1.1, textTransform: "uppercase", color: "$faint", fontVariant: ['tabular-nums'], children: [i + 1, " / ", stops.length] }), _jsx(View, { flex: 1, height: 1, bg: "$borderColor" }), _jsx(XStack, { gap: 3, children: stops.map((s, n) => (_jsx(View, { width: n === i ? 14 : 5, height: 5, rounded: 3, bg: n <= i ? '$ink' : '$rim', transition: "200ms" }, s.id))) })] }), _jsx(Text, { render: "h2", fontFamily: "$body", mb: 7, fontSize: "$6", fontWeight: "600", lineHeight: 22.1, letterSpacing: -0.17, color: "$ink", children: stop.title }), _jsx(Text, { render: "p", fontFamily: "$body", mb: 16, fontSize: "$3", lineHeight: 22.4, color: "$quiet", children: stop.body }), _jsxs(XStack, { items: "center", gap: 8, children: [_jsx(Button, { variant: "linkMuted", size: "sm", px: 2, onClick: () => done('skipped'), children: "Skip" }), _jsx(View, { flex: 1 }), i > 0 ? (_jsx(Button, { size: "sm", onClick: back, children: "Back" })) : null, _jsx(Next, { onPress: next, label: last ? 'Done' : 'Next' })] })] }))] }), document.body);
|
|
263
|
+
}
|
|
264
|
+
/** Let a room's guide be asked for again — a menu row can call this and reload. */
|
|
265
|
+
export function replayGuide(room = 'chat') {
|
|
266
|
+
try {
|
|
267
|
+
localStorage.removeItem(keyOf(room));
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
/* nothing to clear */
|
|
271
|
+
}
|
|
272
|
+
}
|
package/dist/Home.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** What a question can start here. Four, in the order they escalate. */
|
|
2
|
+
declare const MODES: readonly [{
|
|
3
|
+
readonly id: 'chat';
|
|
4
|
+
readonly label: 'Chat';
|
|
5
|
+
readonly asks: 'Ask anything';
|
|
6
|
+
}, {
|
|
7
|
+
readonly id: 'dev';
|
|
8
|
+
readonly label: 'Dev';
|
|
9
|
+
readonly asks: 'Describe what to build';
|
|
10
|
+
}, {
|
|
11
|
+
readonly id: 'news';
|
|
12
|
+
readonly label: 'News';
|
|
13
|
+
readonly asks: 'What happened?';
|
|
14
|
+
}, {
|
|
15
|
+
readonly id: 'research';
|
|
16
|
+
readonly label: 'Research';
|
|
17
|
+
readonly asks: 'What should we dig into?';
|
|
18
|
+
}];
|
|
19
|
+
export type Mode = (typeof MODES)[number]['id'];
|
|
20
|
+
/**
|
|
21
|
+
* Ask, however the host answers.
|
|
22
|
+
*
|
|
23
|
+
* The default sends the question to /chat, which is the one room on this site
|
|
24
|
+
* that reads a question off the address (`?q=`, the name every Hanzo surface
|
|
25
|
+
* takes). A host that already HAS the thread — the chat pane, where this home
|
|
26
|
+
* belongs — passes its own `send` instead and nothing navigates.
|
|
27
|
+
*/
|
|
28
|
+
export interface HomeProps {
|
|
29
|
+
ask?: (question: string, mode: Mode, sources: string[]) => void;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* A ROOM IS READ HERE. Home is the org's room: the rail's channels and direct
|
|
33
|
+
* messages open in this pane (`Talk`), keyed `team:<space>/<id>` in the
|
|
34
|
+
* selection slot and in the address, and the Rooms card below lists them and
|
|
35
|
+
* opens a new one. What other networks carry in stays in the Inbox, under its
|
|
36
|
+
* own key.
|
|
37
|
+
*/
|
|
38
|
+
export declare function Home({ ask }: HomeProps): import("react").JSX.Element;
|
|
39
|
+
export {};
|
package/dist/Home.js
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
// Home: one question at the top, and what to do next underneath.
|
|
4
|
+
//
|
|
5
|
+
// THE COMPOSER IS THE PAGE. Everything else is a card, and every card is a real
|
|
6
|
+
// affordance reading real cloud data — no placeholder feed, no invented metric,
|
|
7
|
+
// and no list that renders unknown as zero.
|
|
8
|
+
//
|
|
9
|
+
// The field is `@hanzo/ui/chat`'s `Composer`, the same component the browser
|
|
10
|
+
// extension's home mounts and the same one `Chat.tsx` and `Work.tsx` already
|
|
11
|
+
// use. Its `children` is the documented toolbar slot — "where a surface puts
|
|
12
|
+
// its ModelSelector, attachment control or mode chips" — so the source chips go
|
|
13
|
+
// THERE and the package needs no change to carry them. The four options above
|
|
14
|
+
// it are this surface's, for the same reason: a home decides what it starts.
|
|
15
|
+
import { dev } from './lib/host.js';
|
|
16
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
17
|
+
import { AtSign, Blocks, Bot, Code2, FolderGit2, Hash, MessageSquare, Plus, UserPlus } from 'lucide-react';
|
|
18
|
+
import { useAgents, usePeople, useSessions, useThreads } from '@hanzo/ai/react';
|
|
19
|
+
import { Composer } from '@hanzo/ui/chat';
|
|
20
|
+
import { Crew } from './crew.js';
|
|
21
|
+
import { Box, Text, XStack, YStack } from '@hanzo/ui';
|
|
22
|
+
import { tap } from './lib/tap.js';
|
|
23
|
+
import { Grid } from '@hanzo/ui/grid';
|
|
24
|
+
import { useIam, useOrganizations } from '@hanzo/iam/react';
|
|
25
|
+
import { hasSession } from './lib/session.js';
|
|
26
|
+
import { useAi } from './lib/ai.js';
|
|
27
|
+
import { useHydrated } from './lib/hydrated.js';
|
|
28
|
+
import { useOpen } from './open.js';
|
|
29
|
+
import { plainly, say } from './failure.js';
|
|
30
|
+
import { Directory } from './Directory.js';
|
|
31
|
+
import { Bar } from './progress.js';
|
|
32
|
+
import { Card, Go, INK, Quiet, Row, SOFT, CHOSEN } from './card.js';
|
|
33
|
+
import { Talk } from './Talk.js';
|
|
34
|
+
import { useMe } from './roster.js';
|
|
35
|
+
import { roomKey, teamKey, useTeamRooms } from './rooms.js';
|
|
36
|
+
import { invite, apps, projects } from './lib/home.js';
|
|
37
|
+
import { BAD } from './lib/mix.js';
|
|
38
|
+
/** What a question can start here. Four, in the order they escalate. */
|
|
39
|
+
const MODES = [
|
|
40
|
+
{ id: 'chat', label: 'Chat', asks: 'Ask anything' },
|
|
41
|
+
{ id: 'dev', label: 'Dev', asks: 'Describe what to build' },
|
|
42
|
+
{ id: 'news', label: 'News', asks: 'What happened?' },
|
|
43
|
+
{ id: 'research', label: 'Research', asks: 'What should we dig into?' },
|
|
44
|
+
];
|
|
45
|
+
/**
|
|
46
|
+
* Where a grounded answer may look, as the `@source` hints the answer engine
|
|
47
|
+
* already reads (`SearchOptions.sources` in `@hanzo/ai`). Chat and Dev do not
|
|
48
|
+
* search, so the chips are drawn only for the two modes that do — and they live
|
|
49
|
+
* in the composer's footer row, whose height the send control already sets, so
|
|
50
|
+
* switching modes moves nothing.
|
|
51
|
+
*/
|
|
52
|
+
const SOURCES = ['web', 'news', 'academic', 'github', 'reddit', 'x'];
|
|
53
|
+
const GROUNDS = (mode) => mode === 'news' || mode === 'research';
|
|
54
|
+
/**
|
|
55
|
+
* A ROOM IS READ HERE. Home is the org's room: the rail's channels and direct
|
|
56
|
+
* messages open in this pane (`Talk`), keyed `team:<space>/<id>` in the
|
|
57
|
+
* selection slot and in the address, and the Rooms card below lists them and
|
|
58
|
+
* opens a new one. What other networks carry in stays in the Inbox, under its
|
|
59
|
+
* own key.
|
|
60
|
+
*/
|
|
61
|
+
export function Home({ ask }) {
|
|
62
|
+
const [mode, setMode] = useState('chat');
|
|
63
|
+
const [draft, setDraft] = useState('');
|
|
64
|
+
const [picked, setPicked] = useState([]);
|
|
65
|
+
// Connecting an app happens in the Directory, so the home mounts the
|
|
66
|
+
// Directory rather than linking somewhere that is not it. It is a Dialog —
|
|
67
|
+
// closed, it draws nothing — so a second instance beside the shell's costs a
|
|
68
|
+
// component and no pixels, and there is still ONE directory in the codebase.
|
|
69
|
+
const [directory, setDirectory] = useState(false);
|
|
70
|
+
const { room, openRoom } = useOpen();
|
|
71
|
+
const held = useTeamRooms();
|
|
72
|
+
const here = MODES.find((m) => m.id === mode) ?? MODES[0];
|
|
73
|
+
const grounded = GROUNDS(mode);
|
|
74
|
+
const at = roomKey(room);
|
|
75
|
+
if (at)
|
|
76
|
+
return _jsx(Open, { at: at, rooms: held, back: () => openRoom(null) });
|
|
77
|
+
const send = () => {
|
|
78
|
+
const question = draft.trim();
|
|
79
|
+
if (!question)
|
|
80
|
+
return;
|
|
81
|
+
setDraft('');
|
|
82
|
+
const hints = grounded ? picked : [];
|
|
83
|
+
if (ask) {
|
|
84
|
+
ask(question, mode, hints);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
// The hints ride IN the question, which is what they are: the engine reads
|
|
88
|
+
// `@news` in the query. Appending them to a chat turn would be noise, so
|
|
89
|
+
// only a grounded mode carries them.
|
|
90
|
+
const q = hints.length ? `${question} ${hints.map((s) => `@${s}`).join(' ')}` : question;
|
|
91
|
+
window.location.href = `/chat?q=${encodeURIComponent(q)}`;
|
|
92
|
+
};
|
|
93
|
+
return (_jsxs(YStack, { "data-home": true, flex: 1, minH: 0, width: "100%", overflow: "scroll", children: [_jsxs(YStack, { width: "100%", maxW: 720, mx: "auto", px: "$4", pt: "$8", gap: "$4", $sm: { pt: '$10' }, children: [_jsx(Text, { render: "h1", fontSize: "$8", fontWeight: "500", color: INK, text: "center", $sm: { fontSize: '$9' }, children: "What are we building?" }), _jsx(XStack, { justify: "center", gap: "$1", flexWrap: "wrap", children: MODES.map((one) => (_jsx(Box, { "data-tab": one.id, render: "button", onClick: () => setMode(one.id), "aria-current": mode === one.id, px: "$3", py: "$2", rounded: "$10", bg: mode === one.id ? CHOSEN : 'transparent', hoverStyle: { bg: '$hover' }, children: _jsx(Text, { fontSize: "$3", fontWeight: "500", color: mode === one.id ? INK : SOFT, children: one.label }) }, one.id))) }), _jsx(Composer, { value: draft, onChange: setDraft, onSend: send, placeholder: here.asks, label: here.asks, rows: 2, maxHeight: 240, children: grounded ? (_jsx(XStack, { gap: "$1", flexWrap: "wrap", shrink: 1, minW: 0, children: SOURCES.map((s) => {
|
|
94
|
+
const on = picked.includes(s);
|
|
95
|
+
return (_jsx(Box, { "data-chip": s, render: "button", "aria-pressed": on, onClick: () => setPicked((held) => (on ? held.filter((h) => h !== s) : [...held, s])), px: "$2", py: "$1", rounded: "$10", bg: on ? CHOSEN : 'transparent', hoverStyle: { bg: '$hover' }, children: _jsxs(Text, { fontSize: "$2", color: on ? INK : SOFT, children: ["@", s] }) }, s));
|
|
96
|
+
}) })) : null }), _jsx(Crew, { to: "/chat" })] }), _jsx(YStack, { width: "100%", maxW: 880, mx: "auto", px: "$4", py: "$8", children: _jsxs(Grid, { columns: { min: 260, max: 3 }, gap: 12, children: [_jsx(Rooms, { ...held }), _jsx(Projects, {}), _jsx(Chats, {}), _jsx(Next, { onConnect: () => setDirectory(true) }), _jsx(Apps, { onConnect: () => setDirectory(true) }), _jsx(Team, {}), _jsx(Build, {})] }) }), _jsx(Directory, { open: directory, onClose: () => setDirectory(false) })] }));
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* The open room, in place of the question and the cards.
|
|
100
|
+
*
|
|
101
|
+
* The key names a space and an id; the org's list answers which room that is.
|
|
102
|
+
* The ladder is the one every card climbs: the session first, because that is
|
|
103
|
+
* the state that never resolves, then the refusal, then the read, then a key no
|
|
104
|
+
* room answers to — a shared link to a room this reader cannot see.
|
|
105
|
+
*/
|
|
106
|
+
function Open({ at, rooms, back, }) {
|
|
107
|
+
const { isAuthenticated, isLoading } = useIam();
|
|
108
|
+
const found = rooms.rooms?.find((one) => one.space === at.space && one.id === at.id);
|
|
109
|
+
if (found)
|
|
110
|
+
return _jsx(Talk, { room: found }, teamKey(found));
|
|
111
|
+
const reading = isLoading || (isAuthenticated && rooms.rooms === null && !rooms.wrong);
|
|
112
|
+
const line = reading
|
|
113
|
+
? 'Reading the room.'
|
|
114
|
+
: !isAuthenticated
|
|
115
|
+
? 'Sign in to read this room.'
|
|
116
|
+
: rooms.wrong
|
|
117
|
+
? say(rooms.wrong, 'this room')
|
|
118
|
+
: 'That room is not here.';
|
|
119
|
+
return (_jsxs(YStack, { flex: 1, items: "center", justify: "center", p: "$6", gap: "$3", children: [_jsx(Text, { fontSize: "$3", color: SOFT, children: line }), reading ? null : (_jsx(Box, { render: "button", onClick: back, ...tap, children: _jsx(Text, { fontSize: "$2", fontWeight: "500", color: INK, children: "Back to home" }) }))] }));
|
|
120
|
+
}
|
|
121
|
+
/* ─────────────────────────────────────────────────────────────────────────────
|
|
122
|
+
The cards. Each one owns its own read, so a card that cannot answer says so
|
|
123
|
+
on its own rather than emptying the grid.
|
|
124
|
+
────────────────────────────────────────────────────────────────────────── */
|
|
125
|
+
/**
|
|
126
|
+
* The org's rooms, and the way to open one.
|
|
127
|
+
*
|
|
128
|
+
* The list is the read the rail makes (`useTeamRooms`), so the two cannot
|
|
129
|
+
* disagree about what the org has. A new room is posted with the reader as its
|
|
130
|
+
* first member — the platform adds nobody on its own — in the space of the
|
|
131
|
+
* org's first room, which is the space an org with one has; an org with none
|
|
132
|
+
* leaves it unsaid. The row the platform answers with is opened at once.
|
|
133
|
+
*/
|
|
134
|
+
function Rooms({ rooms, wrong, open }) {
|
|
135
|
+
const { openRoom } = useOpen();
|
|
136
|
+
const me = useMe();
|
|
137
|
+
const [naming, setNaming] = useState(false);
|
|
138
|
+
const [name, setName] = useState('');
|
|
139
|
+
const [opening, setOpening] = useState(false);
|
|
140
|
+
const [refused, setRefused] = useState(null);
|
|
141
|
+
const make = async () => {
|
|
142
|
+
const wanted = name.trim();
|
|
143
|
+
if (!wanted || opening || !me)
|
|
144
|
+
return;
|
|
145
|
+
setOpening(true);
|
|
146
|
+
setRefused(null);
|
|
147
|
+
try {
|
|
148
|
+
const made = await open({ name: wanted, space: rooms?.[0]?.space, members: [me] });
|
|
149
|
+
setName('');
|
|
150
|
+
setNaming(false);
|
|
151
|
+
openRoom(teamKey(made));
|
|
152
|
+
}
|
|
153
|
+
catch (e) {
|
|
154
|
+
setRefused(plainly(e, 'this room'));
|
|
155
|
+
}
|
|
156
|
+
finally {
|
|
157
|
+
setOpening(false);
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
return (_jsxs(Card, { kind: "rooms", icon: _jsx(Hash, { size: 14, "aria-hidden": true }), title: "Rooms", children: [rooms === null ? (_jsx(Waiting, { wrong: wrong, subject: "your rooms" })) : rooms.length === 0 ? (_jsx(Quiet, { children: "No rooms yet. Open one and the conversation has somewhere to live." })) : (_jsx(YStack, { gap: "$1", children: rooms.slice(0, 6).map((one) => (_jsx(Row, { icon: one.direct ? _jsx(AtSign, { size: 12, "aria-hidden": true }) : _jsx(Hash, { size: 12, "aria-hidden": true }), onPress: () => openRoom(teamKey(one)), children: one.name || 'Direct message' }, teamKey(one)))) })), !me ? null : naming ? (_jsxs(YStack, { gap: "$1", children: [_jsxs(XStack, { gap: "$1", items: "center", children: [_jsx(Text, { render: _jsx("input", { value: name, onChange: (e) => setName(e.target.value), onKeyDown: (e) => {
|
|
161
|
+
if (e.key === 'Enter')
|
|
162
|
+
void make();
|
|
163
|
+
if (e.key === 'Escape')
|
|
164
|
+
setNaming(false);
|
|
165
|
+
}, placeholder: "Name", "aria-label": "Room name", autoFocus: true }), flex: 1, minW: 0, bg: "transparent", borderWidth: 1, borderColor: "var(--border)", rounded: 6, px: 8, py: 6, color: "inherit", fontSize: "$2" }), _jsx(Box, { render: "button", onClick: () => void make(), "aria-disabled": opening, opacity: opening ? 0.5 : 1, px: "$2", py: "$1.5", rounded: "$2", borderWidth: 1, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, children: _jsx(Text, { fontSize: "$2", fontWeight: "500", color: INK, children: "Open it" }) })] }), refused ? (_jsx(Text, { fontSize: "$2", color: BAD, children: refused })) : null] })) : (_jsx(Row, { icon: _jsx(Plus, { size: 12, "aria-hidden": true }), onPress: () => setNaming(true), children: "New room" }))] }));
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* A read the SDK has no hook for.
|
|
169
|
+
*
|
|
170
|
+
* `null` while the answer is not in — including when the request failed, which
|
|
171
|
+
* is the case a `[]` would misreport as "the org has none".
|
|
172
|
+
*
|
|
173
|
+
* The reader is passed by REFERENCE (`lib/home.ts`'s own functions), not built
|
|
174
|
+
* in the caller, so the effect's dependency is honest and it runs once per
|
|
175
|
+
* client rather than once per render.
|
|
176
|
+
*/
|
|
177
|
+
function useRead(read) {
|
|
178
|
+
const { client } = useAi();
|
|
179
|
+
const [held, setHeld] = useState(null);
|
|
180
|
+
// WHY THERE IS NOTHING, kept rather than swallowed. `null` alone says only
|
|
181
|
+
// that no answer is held, which is equally true of a request still in flight
|
|
182
|
+
// and one that came back no — and a card cannot choose its sentence without
|
|
183
|
+
// knowing which.
|
|
184
|
+
const [wrong, setWrong] = useState(null);
|
|
185
|
+
useEffect(() => {
|
|
186
|
+
if (!client)
|
|
187
|
+
return;
|
|
188
|
+
const stop = new AbortController();
|
|
189
|
+
void read(client, stop.signal)
|
|
190
|
+
.then((got) => {
|
|
191
|
+
if (!stop.signal.aborted)
|
|
192
|
+
setHeld(got);
|
|
193
|
+
})
|
|
194
|
+
.catch((e) => {
|
|
195
|
+
if (!stop.signal.aborted)
|
|
196
|
+
setWrong(e);
|
|
197
|
+
});
|
|
198
|
+
return () => stop.abort();
|
|
199
|
+
}, [client, read]);
|
|
200
|
+
return { held, wrong };
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* WHAT A CARD SAYS BEFORE IT HAS ANYTHING.
|
|
204
|
+
*
|
|
205
|
+
* Three states wore one word. "Reading." is true only while an answer is on its
|
|
206
|
+
* way — a reader with no session will never be sent one, and a store that
|
|
207
|
+
* refused has already answered. Measured on production: Projects, Apps and Team
|
|
208
|
+
* all still said "Reading." twelve seconds after load, to a visitor nobody was
|
|
209
|
+
* ever going to answer.
|
|
210
|
+
*
|
|
211
|
+
* The session is asked FIRST, because that is the state that never resolves.
|
|
212
|
+
*/
|
|
213
|
+
function Waiting({ wrong, subject }) {
|
|
214
|
+
const { isAuthenticated } = useIam();
|
|
215
|
+
const signedIn = isAuthenticated || hasSession();
|
|
216
|
+
if (!signedIn)
|
|
217
|
+
return _jsxs(Quiet, { children: ["Sign in to see ", subject, "."] });
|
|
218
|
+
if (wrong)
|
|
219
|
+
return _jsx(Quiet, { children: say(wrong, subject) });
|
|
220
|
+
return _jsx(Quiet, { children: "Reading." });
|
|
221
|
+
}
|
|
222
|
+
const useProjects = () => useRead(projects);
|
|
223
|
+
const useApps = () => useRead(apps);
|
|
224
|
+
function Projects() {
|
|
225
|
+
const { held, wrong } = useProjects();
|
|
226
|
+
return (_jsxs(Card, { kind: "projects", icon: _jsx(FolderGit2, { size: 14, "aria-hidden": true }), title: "Projects", children: [held === null ? (_jsx(Waiting, { wrong: wrong, subject: "your projects" })) : held.length === 0 ? (_jsx(Quiet, { children: "Nothing deployed yet." })) : (_jsx(YStack, { gap: "$1", children: held.slice(0, 3).map((p) => (_jsx(Row, { href: p.liveUrl, children: p.name || p.slug }, p.slug))) })), _jsx(Go, { href: dev(), children: "Ship one" })] }));
|
|
227
|
+
}
|
|
228
|
+
function Chats() {
|
|
229
|
+
const { threads, loading } = useThreads();
|
|
230
|
+
const { open } = useOpen();
|
|
231
|
+
return (_jsx(Card, { kind: "chats", icon: _jsx(MessageSquare, { size: 14, "aria-hidden": true }), title: "Chats", children: loading && threads.length === 0 ? (_jsx(Waiting, { subject: "your chats" })) : threads.length === 0 ? (_jsx(Quiet, { children: "No chats yet." })) : (_jsx(YStack, { gap: "$1", children: threads.slice(0, 3).map((t) => (_jsx(Row, { onPress: () => open(t.id), children: t.title || 'Untitled' }, t.id))) })) }));
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* What is going, then what to do next.
|
|
235
|
+
*
|
|
236
|
+
* Both halves are DERIVED — every line is a fact this page measured, and it
|
|
237
|
+
* names the fact rather than a recommendation nobody can check. A running run
|
|
238
|
+
* carries its own bar; see `progress.tsx` for why that bar is usually invisible
|
|
239
|
+
* today.
|
|
240
|
+
*/
|
|
241
|
+
function Next({ onConnect }) {
|
|
242
|
+
const { sessions } = useSessions({ limit: 100 });
|
|
243
|
+
const { agents } = useAgents();
|
|
244
|
+
const { held } = useProjects();
|
|
245
|
+
const { held: connected } = useApps();
|
|
246
|
+
const safeSessions = Array.isArray(sessions) ? sessions : [];
|
|
247
|
+
const safeAgents = Array.isArray(agents) ? agents : [];
|
|
248
|
+
const running = useMemo(() => safeSessions.filter((s) => s && s.status === 'running'), [safeSessions]);
|
|
249
|
+
const rows = useMemo(() => {
|
|
250
|
+
const out = [];
|
|
251
|
+
const failed = safeSessions.filter((s) => s && s.status === 'error').length;
|
|
252
|
+
if (failed)
|
|
253
|
+
out.push({ key: 'failed', say: `${failed} failed`, href: '/work' });
|
|
254
|
+
if (safeAgents.length === 0)
|
|
255
|
+
out.push({ key: 'agent', say: 'Hire your first agent', href: '/work' });
|
|
256
|
+
if (held?.length === 0)
|
|
257
|
+
out.push({ key: 'ship', say: 'Ship your first project', href: dev() });
|
|
258
|
+
if (connected && Array.isArray(connected) && !connected.some((a) => a?.connected))
|
|
259
|
+
out.push({ key: 'connect', say: 'Connect an app', press: true });
|
|
260
|
+
return out;
|
|
261
|
+
}, [safeSessions, safeAgents, held, connected]);
|
|
262
|
+
return (_jsxs(Card, { kind: "next", icon: _jsx(Bot, { size: 14, "aria-hidden": true }), title: "Next", children: [running.length ? (_jsx(YStack, { gap: "$2", children: running.slice(0, 2).map((s) => (_jsxs(YStack, { gap: "$1", children: [_jsx(Row, { href: "/work", children: s.title || s.agent || 'Run' }), _jsx(Bar, { run: s, label: false })] }, s.id))) })) : null, rows.length === 0 && running.length === 0 ? (_jsx(Quiet, { children: "Nothing needs you." })) : (_jsx(YStack, { gap: "$1", children: rows.slice(0, 3).map((r) => (_jsx(Row, { href: r.href, onPress: r.press ? onConnect : undefined, children: r.say }, r.key))) }))] }));
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* The apps the org has connected.
|
|
266
|
+
*
|
|
267
|
+
* A connected provider has no page on this site — `/integrations/<slug>` is the
|
|
268
|
+
* SDK guide catalogue (openai-sdk, langchain, cursor …) and shares no ids with
|
|
269
|
+
* the cloud's connectors — so a name here is a name, and the ACTION opens the
|
|
270
|
+
* Directory, which is where connecting actually happens.
|
|
271
|
+
*/
|
|
272
|
+
function Apps({ onConnect }) {
|
|
273
|
+
const { held, wrong } = useApps();
|
|
274
|
+
const on = held?.filter((a) => a.connected) ?? [];
|
|
275
|
+
return (_jsxs(Card, { kind: "apps", icon: _jsx(Blocks, { size: 14, "aria-hidden": true }), title: "Apps", children: [held === null ? (_jsx(Waiting, { wrong: wrong, subject: "connected apps" })) : on.length === 0 ? (_jsx(Quiet, { children: "Nothing connected." })) : (_jsx(YStack, { gap: "$1", children: on.slice(0, 3).map((a) => (_jsx(Row, { children: a.name }, a.id))) })), _jsx(Go, { onPress: onConnect, children: "Connect" })] }));
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* THE ONE CARD A PRERENDER CANNOT DRAW.
|
|
279
|
+
*
|
|
280
|
+
* Every other read here starts empty on the server AND on the first client
|
|
281
|
+
* render, so the two agree and hydration is quiet. `useOrganizations` does not:
|
|
282
|
+
* it resolves the org from the SDK synchronously, so the client's first render
|
|
283
|
+
* already knows the reader and the server's never did — a TEXT mismatch, which
|
|
284
|
+
* React answers by throwing the whole tree away and re-rendering it.
|
|
285
|
+
*
|
|
286
|
+
* `useHydrated` is false on the server and false on that first client render,
|
|
287
|
+
* so both draw the same line and the real answer arrives on the render
|
|
288
|
+
* hydration was going to perform anyway.
|
|
289
|
+
*/
|
|
290
|
+
function Team() {
|
|
291
|
+
const hydrated = useHydrated();
|
|
292
|
+
const { currentOrgId } = useOrganizations();
|
|
293
|
+
if (!hydrated || !currentOrgId) {
|
|
294
|
+
return (_jsxs(Card, { kind: "team", icon: _jsx(UserPlus, { size: 14, "aria-hidden": true }), title: "Team", children: [_jsx(Waiting, { subject: "your team" }), _jsx(Go, { href: invite(), children: "Invite" })] }));
|
|
295
|
+
}
|
|
296
|
+
return _jsx(Mates, { owner: currentOrgId });
|
|
297
|
+
}
|
|
298
|
+
/** The org's people. Split out because `usePeople` needs an owner and a hook
|
|
299
|
+
* cannot be called conditionally. */
|
|
300
|
+
function Mates({ owner }) {
|
|
301
|
+
const { people, loading } = usePeople({ owner, service: false });
|
|
302
|
+
return (_jsxs(Card, { kind: "team", icon: _jsx(UserPlus, { size: 14, "aria-hidden": true }), title: "Team", children: [loading && people.length === 0 ? (_jsx(Waiting, { subject: "your team" })) : people.length < 2 ? (_jsx(Quiet, { children: "Just you." })) : (_jsxs(Quiet, { children: [people.length, " people"] })), _jsx(Go, { href: invite(), children: "Invite" })] }));
|
|
303
|
+
}
|
|
304
|
+
function Build() {
|
|
305
|
+
return (_jsxs(Card, { kind: "build", icon: _jsx(Code2, { size: 14, "aria-hidden": true }), title: "Build", children: [_jsx(Quiet, { children: "Hanzo Dev writes it, runs it, ships it." }), _jsx(Go, { href: dev(), children: "Open Dev" })] }));
|
|
306
|
+
}
|
package/dist/Inbox.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pane, and the session its reads need.
|
|
3
|
+
*
|
|
4
|
+
* SPLIT so the hooks below never run for a reader with none: every route this
|
|
5
|
+
* pane reads is org-scoped and answers 403 without one, and a hook cannot be
|
|
6
|
+
* called conditionally — so the condition is a component boundary rather than
|
|
7
|
+
* an `if` the rules forbid.
|
|
8
|
+
*/
|
|
9
|
+
export declare function Inbox(): import("react").JSX.Element;
|