@hanzo/rooms 0.1.22 → 0.1.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Chat.js +2 -2
- package/dist/Column.js +3 -4
- package/dist/Meet.js +3 -3
- package/dist/Profile.js +2 -2
- package/dist/speech.js +2 -2
- package/package.json +3 -3
- package/dist/Guide.d.ts +0 -6
- package/dist/Guide.js +0 -273
- package/dist/Scene.d.ts +0 -12
- package/dist/Scene.js +0 -105
- package/dist/tours.d.ts +0 -24
- package/dist/tours.js +0 -319
package/dist/Chat.js
CHANGED
|
@@ -50,7 +50,7 @@ import { WEB } from "./lib/web.js";
|
|
|
50
50
|
import { useHydrated } from "./lib/hydrated.js";
|
|
51
51
|
import { base, served } from "./lib/ai.js";
|
|
52
52
|
import { onServed } from "./lib/served.js";
|
|
53
|
-
import {
|
|
53
|
+
import { refused as worded, speech, useDictation, useTalk, useVoice, Voice } from "@hanzo/voice";
|
|
54
54
|
import { ArrowUp, AudioLines, Image as ImageMark, Mic, Paperclip, Square, Star, PanelRight, X } from "lucide-react";
|
|
55
55
|
import { ENSO, FREE } from "./lib/ai.js";
|
|
56
56
|
import { openThread, useOpen } from "./open.js";
|
|
@@ -1198,7 +1198,7 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
|
|
|
1198
1198
|
// WHAT GOES WITH THE MESSAGE, above the frame: one chip a file, each
|
|
1199
1199
|
// with its own remove, and the reason any file was refused.
|
|
1200
1200
|
head: files.length || turnedAway || preparing || talk.open || talk.refusal || pause ? (_jsxs(YStack, { width: "calc(100% - 32px)", maxW: `calc(${MEASURE} - 3rem)`, mx: "auto", gap: "$1.5", children: [pause ? (_jsx(LimitedBanner, { message: pause.message, actions: pause.actions, onUsage: () => showSettings("usage") })) : null, files.length ? (_jsx(XStack, { gap: "$1.5", flexWrap: "wrap", role: "list", "aria-label": "Attached files", "data-slot": "composer-attached", children: files.map((one) => (_jsx(HeldChip, { one: one, onRemove: () => drop(at, one.id) }, one.id))) })) : null, preparing ? (_jsxs(Text, { role: "status", "aria-live": "polite", "data-slot": "composer-preparing", fontSize: "$2", color: "$soft", children: [preparing, "\u2026"] })) : null, turnedAway ? (_jsx(Text, { role: "alert", fontSize: "$2", color: BAD, children: turnedAway })) : null, talk.open || talk.refusal ? (_jsx(Text, { role: "status", "aria-live": "polite", "data-slot": "composer-talk-reply", fontSize: "$2", color: talk.refusal ? BAD : "$soft", children: talk.refusal
|
|
1201
|
-
?
|
|
1201
|
+
? worded(talk.refusal)
|
|
1202
1202
|
: talk.reply
|
|
1203
1203
|
? `Hanzo: ${talk.reply}`
|
|
1204
1204
|
: talk.state === "listening"
|
package/dist/Column.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
// The app's one column, the same in both modes: the Chat / Dev switch, a filter,
|
|
4
4
|
// New, Dev's places, the Recents of the mode it is in, and at the foot the
|
|
5
|
-
//
|
|
5
|
+
// account (components/workspace/Me.tsx).
|
|
6
6
|
//
|
|
7
7
|
// It is a pane on the frame's ground (app/_web.tsx), sized by the edge in the
|
|
8
8
|
// gutter to its right: dragged, keyed or double-clicked back (@hanzo/build's
|
|
@@ -11,7 +11,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
11
11
|
// choice outlives the move.
|
|
12
12
|
import { Text, XStack, YStack } from '@hanzo/gui';
|
|
13
13
|
import { ChevronDown, ChevronUp, PanelLeft, Plus, Search } from 'lucide-react';
|
|
14
|
-
import { Grip, nav,
|
|
14
|
+
import { Grip, nav, route } from '@hanzo/build';
|
|
15
15
|
import { Sidebar, SidebarIconButton, SidebarItem } from '@hanzo/ui/chat';
|
|
16
16
|
import { HanzoMark } from '@hanzogui/shell';
|
|
17
17
|
import { useState } from 'react';
|
|
@@ -54,7 +54,6 @@ const ROW = { rounded: '$4', minH: 34 };
|
|
|
54
54
|
export function Column({ host, mode, thread, onChat, onDev, go, drawer = false, }) {
|
|
55
55
|
const [kept, setCollapsed] = useKept('hanzo.build.rail', false);
|
|
56
56
|
const collapsed = kept && !drawer;
|
|
57
|
-
const [slack, setSlack] = useKept('hanzo.build.slack', false);
|
|
58
57
|
const [unfolded, setUnfolded] = useState(false);
|
|
59
58
|
const [words, setWords] = useState('');
|
|
60
59
|
const span = useSpan(SPAN, WIDTH, FLOOR, CEIL);
|
|
@@ -83,7 +82,7 @@ export function Column({ host, mode, thread, onChat, onDev, go, drawer = false,
|
|
|
83
82
|
setWords('');
|
|
84
83
|
}, placeholder: "Filter", "aria-label": "Filter recents" }), flex: 1, minW: 0, bg: "transparent", borderWidth: 0, outlineStyle: "none", color: "inherit", fontSize: "$2" })] }), _jsxs(YStack, { role: "list", mt: "$2.5", gap: 2, shrink: 0, children: [_jsx(YStack, { role: "listitem", children: _jsx(SidebarItem, { "data-slot": "app-new", ...ROW, active: fresh, icon: _jsx(Plus, { size: 16, "aria-hidden": true }), onPress: create, ...keys(create), children: mode === 'chat' ? 'New chat' : 'New run' }) }), mode === 'dev'
|
|
85
84
|
? links.map((link) => (_jsx(YStack, { role: "listitem", children: _jsx(SidebarItem, { ...ROW, active: link.active, icon: link.icon, onPress: link.onPress, ...keys(() => link.onPress?.()), children: link.label }) }, link.id)))
|
|
86
|
-
: null, mode === 'dev' ? (_jsxs(YStack, { role: "listitem", children: [_jsx(SidebarItem, { ...ROW, "aria-expanded": unfolded, icon: unfolded ? _jsx(ChevronUp, { size: 16, "aria-hidden": true }) : _jsx(ChevronDown, { size: 16, "aria-hidden": true }), onPress: () => setUnfolded((v) => !v), ...keys(() => setUnfolded((v) => !v)), children: "More" }), unfolded ? (_jsx(YStack, { role: "list", gap: 2, children: more.map((link) => (_jsx(YStack, { role: "listitem", children: _jsx(SidebarItem, { ...ROW, active: link.active, icon: link.icon, onPress: link.onPress, ...keys(() => link.onPress?.()), children: link.label }) }, link.id))) })) : null] })) : null] }), _jsx(XStack, { height: 1, bg: "$borderColor", mx: "$1.5", mt: "$3", shrink: 0 }), _jsx(Recents, { host: host, mode: mode, thread: thread, words: words, onOpen: open }), _jsxs(YStack, { gap: "$2", pt: "$2", shrink: 0, children: [_jsx(Made, {}), _jsx(Setup, {})
|
|
85
|
+
: null, mode === 'dev' ? (_jsxs(YStack, { role: "listitem", children: [_jsx(SidebarItem, { ...ROW, "aria-expanded": unfolded, icon: unfolded ? _jsx(ChevronUp, { size: 16, "aria-hidden": true }) : _jsx(ChevronDown, { size: 16, "aria-hidden": true }), onPress: () => setUnfolded((v) => !v), ...keys(() => setUnfolded((v) => !v)), children: "More" }), unfolded ? (_jsx(YStack, { role: "list", gap: 2, children: more.map((link) => (_jsx(YStack, { role: "listitem", children: _jsx(SidebarItem, { ...ROW, active: link.active, icon: link.icon, onPress: link.onPress, ...keys(() => link.onPress?.()), children: link.label }) }, link.id))) })) : null] })) : null] }), _jsx(XStack, { height: 1, bg: "$borderColor", mx: "$1.5", mt: "$3", shrink: 0 }), _jsx(Recents, { host: host, mode: mode, thread: thread, words: words, onOpen: open }), _jsxs(YStack, { gap: "$2", pt: "$2", shrink: 0, children: [_jsx(Made, {}), _jsx(Setup, {})] }), _jsxs(XStack, { "data-slot": "app-foot", items: "center", gap: "$1", pt: "$2", shrink: 0, children: [_jsx(YStack, { flex: 1, minW: 0, children: _jsx(Me, { host: host, onOrg: org }) }), drawer ? null : (_jsx(SidebarIconButton, { label: "Collapse sidebar", onPress: () => setCollapsed(true), width: 32, height: 32, rounded: 999, children: _jsx(PanelLeft, { size: 16 }) }))] })] }), drawer ? null : (_jsx(Grip, { side: "left", span: span.span, floor: FLOOR, ceil: CEIL, reset: WIDTH, onSpan: span.move, onKeep: span.keep, onShut: () => setCollapsed(true), label: "Resize sidebar",
|
|
87
86
|
// In the gutter between the column and the pane, not over the column's own rows.
|
|
88
87
|
r: GUTTER_END, width: "var(--pane-gap)" }))] }));
|
|
89
88
|
}
|
package/dist/Meet.js
CHANGED
|
@@ -5,7 +5,7 @@ import { Video, VideoOff, Mic, MicOff, PhoneOff, Copy, Check, Plus, Calendar, Ex
|
|
|
5
5
|
import { Text, View, XStack, YStack } from '@hanzo/gui';
|
|
6
6
|
import { Button } from '@hanzo/ui';
|
|
7
7
|
import { useIamToken } from '@hanzo/iam/react';
|
|
8
|
-
import {
|
|
8
|
+
import { refused, speech, useTranscript, useVoice } from '@hanzo/voice';
|
|
9
9
|
import { Face, voiceOf } from './cast.js';
|
|
10
10
|
import { member } from './team.js';
|
|
11
11
|
import { cleanForSpeech, speakAgent, stopAgentSpeech, AudioWave, useAgentSpeech } from './speech.js';
|
|
@@ -231,7 +231,7 @@ export function Meet() {
|
|
|
231
231
|
onPartial: setCaption,
|
|
232
232
|
onSettled: (text) => setLiveNotes((prev) => [...prev, `${clock()}: You: ${text}`]),
|
|
233
233
|
});
|
|
234
|
-
const transcriberNote = transcriber.reason ?? (transcriber.refusal ?
|
|
234
|
+
const transcriberNote = transcriber.reason ?? (transcriber.refusal ? refused(transcriber.refusal) : null);
|
|
235
235
|
const handleSpokenInput = useCallback(async (transcript) => {
|
|
236
236
|
const time = clock();
|
|
237
237
|
setLiveNotes((prev) => [...prev, `${time}: You (voice): ${transcript}`]);
|
|
@@ -269,7 +269,7 @@ export function Meet() {
|
|
|
269
269
|
};
|
|
270
270
|
// What the hands-free control says: why it cannot run, or that Hanzo's speech
|
|
271
271
|
// refused and the browser stood in (or could not) — never silence.
|
|
272
|
-
const handsFreeNote = handsFree.reason ?? (handsFree.refusal ?
|
|
272
|
+
const handsFreeNote = handsFree.reason ?? (handsFree.refusal ? refused(handsFree.refusal) : null);
|
|
273
273
|
return (_jsxs(YStack, { width: "100%", height: "100%", bg: "$background", overflowX: "auto", overflowY: "auto", children: [_jsxs(XStack, { minH: 56, px: 20, py: 8, gap: 10, flexWrap: "wrap", bg: "$panel", borderBottomWidth: 1, borderBottomColor: "$borderColor", items: "center", justify: "space-between", children: [_jsxs(XStack, { items: "center", gap: 12, shrink: 1, minW: "auto", children: [_jsx(XStack, { width: 32, height: 32, rounded: 8, bg: "$raised", items: "center", justify: "center", shrink: 1, minW: "auto", children: _jsx(Video, { size: 18, color: "var(--foreground)" }) }), _jsxs(YStack, { shrink: 1, minW: "auto", children: [_jsxs(XStack, { items: "center", gap: 8, children: [_jsx(Text, { fontSize: "$4", fontWeight: "700", color: "$ink", children: "Hanzo Meet" }), _jsx(Text, { fontSize: FLOOR, fontWeight: "700", bg: "$hover", color: "$soft", borderWidth: 1, borderColor: "$borderColor", px: 6, py: 1, rounded: 4, children: "LIVE \u00B7 meet.hanzo.ai" })] }), _jsx(Text, { fontSize: "$1", color: "$soft", children: "Native WebRTC video conferencing with AI meeting bot transcription & voice" })] })] }), activeMeetingRoom ? (_jsxs(XStack, { items: "center", gap: 8, shrink: 1, minW: "auto", flexWrap: "wrap", children: [_jsx(XStack, { bg: "$hover", rounded: 8, p: 2, gap: 2, borderWidth: 1, borderColor: "$borderColor", shrink: 1, minW: "auto", children: [
|
|
274
274
|
['iframe', 'SFU Stage', Tv],
|
|
275
275
|
['grid', 'Agent Grid', LayoutGrid],
|
package/dist/Profile.js
CHANGED
|
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
// The reader's own profile, in the column beside the room.
|
|
4
4
|
//
|
|
5
5
|
// A WORKSPACE OPENS A PROFILE IN A PANEL, not a page. The page exists — it is
|
|
6
|
-
// /
|
|
6
|
+
// Settings (/settings), where the whole record is edited — but reaching it from a
|
|
7
7
|
// conversation costs the conversation: the room, the sidebar and the rail all
|
|
8
8
|
// go, and coming back is a navigation rather than a close. Slack, Teams and
|
|
9
9
|
// every app shaped like them put this in the right column for that reason, and
|
|
@@ -26,7 +26,7 @@ import { mark } from './lib/mark.js';
|
|
|
26
26
|
import { BAD } from './lib/mix.js';
|
|
27
27
|
/** Slack's is 380 and the thread panel that shares this slot is the same. */
|
|
28
28
|
const WIDTH = 380;
|
|
29
|
-
const ACCOUNT = '/
|
|
29
|
+
const ACCOUNT = '/settings';
|
|
30
30
|
export function Profile({ open, onClose }) {
|
|
31
31
|
const { Link } = useRooms();
|
|
32
32
|
const { user, updateUserProfile } = useAccount();
|
package/dist/speech.js
CHANGED
|
@@ -18,7 +18,7 @@ import { Volume2, Square } from 'lucide-react';
|
|
|
18
18
|
import { View, XStack } from '@hanzo/gui';
|
|
19
19
|
import { Button } from '@hanzo/ui';
|
|
20
20
|
import { useIamToken } from '@hanzo/iam/react';
|
|
21
|
-
import { mouth,
|
|
21
|
+
import { mouth, refused, speech } from '@hanzo/voice';
|
|
22
22
|
import { GOOD } from './lib/mix.js';
|
|
23
23
|
import { base } from './lib/ai.js';
|
|
24
24
|
import { voiceOf, voiceProfileOf } from './cast.js';
|
|
@@ -211,7 +211,7 @@ export function SpeakButton({ text, agentName, compact = false, }) {
|
|
|
211
211
|
const voice = voiceOf(agentName);
|
|
212
212
|
// A browser voice standing in for a refused platform sounds like success;
|
|
213
213
|
// the label is the one place that says otherwise.
|
|
214
|
-
const stood = refusal ? ` ${
|
|
214
|
+
const stood = refusal ? ` ${refused(refusal)}` : '';
|
|
215
215
|
// An @hanzo/ui Button: ghost at rest, the quiet pressed ground while it
|
|
216
216
|
// speaks. The word for speaking is the one status here, so it alone takes
|
|
217
217
|
// the success ink (GOOD holds 4.5:1 on either theme); the icons follow the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanzo/rooms",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.24",
|
|
4
4
|
"description": "The workspace rooms — Home, Chat, Inbox, Contacts, Meet, Cal, Drive, Board, Work, Bots — and the frame that holds them, once, for every Hanzo surface.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"@hanzo/plans": ">=1.8.9",
|
|
59
59
|
"@hanzo/ui": ">=8.27.57",
|
|
60
60
|
"@hanzo/usage": ">=0.1.18",
|
|
61
|
-
"@hanzo/voice": ">=0.1.
|
|
61
|
+
"@hanzo/voice": ">=0.1.16",
|
|
62
62
|
"@hanzogui/shell": "^8.3.20",
|
|
63
63
|
"lucide-react": ">=1.42.0",
|
|
64
64
|
"react": ">=19.2.8",
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
"@hanzo/plans": "1.8.9",
|
|
78
78
|
"@hanzo/ui": "8.27.57",
|
|
79
79
|
"@hanzo/usage": "0.1.18",
|
|
80
|
-
"@hanzo/voice": "0.1.
|
|
80
|
+
"@hanzo/voice": "0.1.16",
|
|
81
81
|
"@hanzogui/config": "8.3.5",
|
|
82
82
|
"@hanzogui/core": "8.3.5",
|
|
83
83
|
"@hanzogui/lucide-icons-2": "8.3.5",
|
package/dist/Guide.d.ts
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import { type Tour } from './tours.js';
|
|
2
|
-
export declare function Guide({ room }: {
|
|
3
|
-
room?: Tour;
|
|
4
|
-
}): import("react").ReactPortal;
|
|
5
|
-
/** Let a room's guide be asked for again — a menu row can call this and reload. */
|
|
6
|
-
export declare function replayGuide(room?: Tour): void;
|
package/dist/Guide.js
DELETED
|
@@ -1,273 +0,0 @@
|
|
|
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 { brand } from './where.js';
|
|
33
|
-
import { useIam } from '@hanzo/iam/react';
|
|
34
|
-
import { useAi } from './lib/ai.js';
|
|
35
|
-
import { Next, Scene } from './Scene.js';
|
|
36
|
-
import { TOURS } from './tours.js';
|
|
37
|
-
/** The ring on the spot: design's focus ring and its halo, one above the dim. gui hands a `calc()` to the page
|
|
38
|
-
* unchanged; its types only name custom properties. */
|
|
39
|
-
const RING = '0 0 0 1px var(--ring), 0 0 26px var(--ring-halo)';
|
|
40
|
-
const ABOVE = 'calc(var(--z-overlay) + 1)';
|
|
41
|
-
/** design's smallest rung (`--text-floor`, 10px), below gui's `$1`; gui's font size takes a rung or a number. */
|
|
42
|
-
const FLOOR = 10;
|
|
43
|
-
/** The page's own leading (design's `--leading-base`, 1.35rem), which a face's scope would otherwise replace with its
|
|
44
|
-
* 20px; gui's line height takes a rung or a number. */
|
|
45
|
-
const LEAD = 21.6;
|
|
46
|
-
/** Where the card's pointer sits, and which two edges it draws, for each side the card can take. */
|
|
47
|
-
const POINTER = {
|
|
48
|
-
bottom: { t: -6.5, l: '50%', ml: -5.5, borderLeftWidth: 1, borderTopWidth: 1 },
|
|
49
|
-
top: { b: -6.5, l: '50%', ml: -5.5, borderRightWidth: 1, borderBottomWidth: 1 },
|
|
50
|
-
right: { l: -6.5, t: '50%', mt: -5.5, borderLeftWidth: 1, borderBottomWidth: 1 },
|
|
51
|
-
left: { r: -6.5, t: '50%', mt: -5.5, borderRightWidth: 1, borderTopWidth: 1 },
|
|
52
|
-
};
|
|
53
|
-
/** Where a room's tour is marked seen, in this browser and in the person's preferences. */
|
|
54
|
-
const keyOf = (room) => (room === 'chat' ? 'hanzo:chat:guide' : `hanzo:${room}:guide`);
|
|
55
|
-
const prefOf = (room) => `${room}Guide`;
|
|
56
|
-
const PAD = 8;
|
|
57
|
-
const GAP = 14;
|
|
58
|
-
const CARD_W = 352;
|
|
59
|
-
export function Guide({ room = 'chat' }) {
|
|
60
|
-
const KEY = keyOf(room);
|
|
61
|
-
const PREF = prefOf(room);
|
|
62
|
-
const STOPS = TOURS[room];
|
|
63
|
-
const { client } = useAi();
|
|
64
|
-
// ONLY FOR SOMEONE WHO IS SIGNED IN. `useAi` hands back a client either way —
|
|
65
|
-
// the anonymous one the free composer runs on — so a client is not proof of a
|
|
66
|
-
// reader. Opening for a visitor put a dimmed backdrop over the page before
|
|
67
|
-
// they had an account, and its panels sat on top of the Sign in button: the
|
|
68
|
-
// guide blocked the one action the page was asking for. It also has nothing
|
|
69
|
-
// true to say to a stranger, since half of what it points at is theirs only
|
|
70
|
-
// once they have a workspace.
|
|
71
|
-
const { isAuthenticated, isLoading } = useIam();
|
|
72
|
-
const [live, setLive] = useState(false);
|
|
73
|
-
const [i, setI] = useState(0);
|
|
74
|
-
const [spot, setSpot] = useState(null);
|
|
75
|
-
const [side, setSide] = useState('bottom');
|
|
76
|
-
const [card, setCard] = useState(null);
|
|
77
|
-
const cardRef = useRef(null);
|
|
78
|
-
// The stops whose anchors exist right now. A destination the org does not
|
|
79
|
-
// carry has no element, so it is not a step — the tour describes THIS
|
|
80
|
-
// workspace rather than a catalogue.
|
|
81
|
-
const stops = useMemo(() => (live ? STOPS.filter((s) => s.find() !== null) : STOPS), [live, STOPS]);
|
|
82
|
-
const stop = stops[i];
|
|
83
|
-
const done = useCallback((how) => {
|
|
84
|
-
setLive(false);
|
|
85
|
-
const mark = JSON.stringify({ at: new Date().toISOString(), how });
|
|
86
|
-
try {
|
|
87
|
-
localStorage.setItem(KEY, mark);
|
|
88
|
-
}
|
|
89
|
-
catch {
|
|
90
|
-
/* a browser with storage off still gets the guide once per visit */
|
|
91
|
-
}
|
|
92
|
-
// Follows the person, not the browser. Failure is silent on purpose: the
|
|
93
|
-
// local mark already settled it, and a guide is not worth an error.
|
|
94
|
-
client?.http
|
|
95
|
-
.json({ method: 'PATCH', path: '/v1/pref', body: { prefs: { [PREF]: { at: new Date().toISOString(), how } } } })
|
|
96
|
-
.catch(() => { });
|
|
97
|
-
}, [client, KEY, PREF]);
|
|
98
|
-
// Should it open at all? Local first — it answers instantly and it answers
|
|
99
|
-
// for a reader who never signs in.
|
|
100
|
-
useEffect(() => {
|
|
101
|
-
if (isLoading || !isAuthenticated)
|
|
102
|
-
return;
|
|
103
|
-
let held = false;
|
|
104
|
-
try {
|
|
105
|
-
held = Boolean(localStorage.getItem(KEY));
|
|
106
|
-
}
|
|
107
|
-
catch {
|
|
108
|
-
/* treat unreadable storage as "not yet shown" */
|
|
109
|
-
}
|
|
110
|
-
if (held)
|
|
111
|
-
return;
|
|
112
|
-
let alive = true;
|
|
113
|
-
const open = () => {
|
|
114
|
-
if (!alive)
|
|
115
|
-
return;
|
|
116
|
-
// Wait for the room to actually render before pointing at it.
|
|
117
|
-
const t = window.setTimeout(() => alive && setLive(true), 900);
|
|
118
|
-
return () => window.clearTimeout(t);
|
|
119
|
-
};
|
|
120
|
-
if (!client)
|
|
121
|
-
return open();
|
|
122
|
-
client.http
|
|
123
|
-
.json({ path: '/v1/pref' })
|
|
124
|
-
.then((doc) => {
|
|
125
|
-
if (!alive)
|
|
126
|
-
return;
|
|
127
|
-
if (doc?.prefs && doc.prefs[PREF]) {
|
|
128
|
-
try {
|
|
129
|
-
localStorage.setItem(KEY, JSON.stringify(doc.prefs[PREF]));
|
|
130
|
-
}
|
|
131
|
-
catch {
|
|
132
|
-
/* nothing to do */
|
|
133
|
-
}
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
// THE TOUR IS FOR A NEW ACCOUNT. One that has already had a conversation
|
|
137
|
-
// here is not new: it may never have met this tour, and it does not
|
|
138
|
-
// need to. It is marked as such, so the question is not asked again.
|
|
139
|
-
client.threads
|
|
140
|
-
.list()
|
|
141
|
-
.then((had) => {
|
|
142
|
-
if (!alive)
|
|
143
|
-
return;
|
|
144
|
-
if (Array.isArray(had) && had.length > 0)
|
|
145
|
-
done('unneeded');
|
|
146
|
-
else
|
|
147
|
-
open();
|
|
148
|
-
})
|
|
149
|
-
.catch(() => open());
|
|
150
|
-
})
|
|
151
|
-
.catch(() => open());
|
|
152
|
-
return () => {
|
|
153
|
-
alive = false;
|
|
154
|
-
};
|
|
155
|
-
}, [client, isAuthenticated, isLoading, KEY, PREF, done]);
|
|
156
|
-
// Measure the anchor, and re-measure when the page moves under it.
|
|
157
|
-
//
|
|
158
|
-
// NOT on every frame. A rAF loop that calls setSpot each tick re-renders the
|
|
159
|
-
// card sixty times a second, which makes it a target nothing can click: the
|
|
160
|
-
// element under the pointer is replaced between the hit test and the press.
|
|
161
|
-
// Measure on the events that actually move things, and write state only when
|
|
162
|
-
// the rectangle really changed.
|
|
163
|
-
useLayoutEffect(() => {
|
|
164
|
-
if (!live || !stop)
|
|
165
|
-
return;
|
|
166
|
-
let last = '';
|
|
167
|
-
const measure = () => {
|
|
168
|
-
const el = stop.find();
|
|
169
|
-
if (!el) {
|
|
170
|
-
setSpot(null);
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
const r = el.getBoundingClientRect();
|
|
174
|
-
const next = { top: r.top - PAD, left: r.left - PAD, width: r.width + PAD * 2, height: r.height + PAD * 2 };
|
|
175
|
-
const key = `${Math.round(next.top)}:${Math.round(next.left)}:${Math.round(next.width)}:${Math.round(next.height)}`;
|
|
176
|
-
if (key === last)
|
|
177
|
-
return;
|
|
178
|
-
last = key;
|
|
179
|
-
setSpot(next);
|
|
180
|
-
};
|
|
181
|
-
// Bring it into view once per step, then measure after the scroll settles.
|
|
182
|
-
stop.find()?.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
|
183
|
-
measure();
|
|
184
|
-
const settle = window.setTimeout(measure, 260);
|
|
185
|
-
const ro = new ResizeObserver(measure);
|
|
186
|
-
ro.observe(document.body);
|
|
187
|
-
const el = stop.find();
|
|
188
|
-
if (el)
|
|
189
|
-
ro.observe(el);
|
|
190
|
-
window.addEventListener('scroll', measure, true);
|
|
191
|
-
window.addEventListener('resize', measure);
|
|
192
|
-
return () => {
|
|
193
|
-
window.clearTimeout(settle);
|
|
194
|
-
ro.disconnect();
|
|
195
|
-
window.removeEventListener('scroll', measure, true);
|
|
196
|
-
window.removeEventListener('resize', measure);
|
|
197
|
-
};
|
|
198
|
-
}, [live, stop, i]);
|
|
199
|
-
// Place the card against the measured spot, flipping rather than overflowing.
|
|
200
|
-
useLayoutEffect(() => {
|
|
201
|
-
if (!spot) {
|
|
202
|
-
setCard(null);
|
|
203
|
-
return;
|
|
204
|
-
}
|
|
205
|
-
const h = cardRef.current?.offsetHeight ?? 168;
|
|
206
|
-
const vw = window.innerWidth;
|
|
207
|
-
const vh = window.innerHeight;
|
|
208
|
-
let want = stop?.side ?? 'bottom';
|
|
209
|
-
if (want === 'bottom' && spot.top + spot.height + GAP + h > vh - 12)
|
|
210
|
-
want = 'top';
|
|
211
|
-
if (want === 'top' && spot.top - GAP - h < 12)
|
|
212
|
-
want = 'bottom';
|
|
213
|
-
if (want === 'right' && spot.left + spot.width + GAP + CARD_W > vw - 12)
|
|
214
|
-
want = 'left';
|
|
215
|
-
if (want === 'left' && spot.left - GAP - CARD_W < 12)
|
|
216
|
-
want = 'right';
|
|
217
|
-
let top;
|
|
218
|
-
let left;
|
|
219
|
-
if (want === 'bottom' || want === 'top') {
|
|
220
|
-
top = want === 'bottom' ? spot.top + spot.height + GAP : spot.top - GAP - h;
|
|
221
|
-
left = spot.left + spot.width / 2 - CARD_W / 2;
|
|
222
|
-
}
|
|
223
|
-
else {
|
|
224
|
-
left = want === 'right' ? spot.left + spot.width + GAP : spot.left - GAP - CARD_W;
|
|
225
|
-
top = spot.top + spot.height / 2 - h / 2;
|
|
226
|
-
}
|
|
227
|
-
setSide(want);
|
|
228
|
-
setCard({ top: Math.max(12, Math.min(top, vh - h - 12)), left: Math.max(12, Math.min(left, vw - CARD_W - 12)) });
|
|
229
|
-
}, [spot, stop]);
|
|
230
|
-
const next = useCallback(() => {
|
|
231
|
-
if (i + 1 >= stops.length)
|
|
232
|
-
done('finished');
|
|
233
|
-
else
|
|
234
|
-
setI(i + 1);
|
|
235
|
-
}, [i, stops.length, done]);
|
|
236
|
-
const back = useCallback(() => setI((n) => Math.max(0, n - 1)), []);
|
|
237
|
-
useEffect(() => {
|
|
238
|
-
if (!live)
|
|
239
|
-
return;
|
|
240
|
-
const onKey = (e) => {
|
|
241
|
-
if (e.key === 'Escape') {
|
|
242
|
-
e.preventDefault();
|
|
243
|
-
done('skipped');
|
|
244
|
-
}
|
|
245
|
-
else if (e.key === 'ArrowRight' || e.key === 'Enter') {
|
|
246
|
-
e.preventDefault();
|
|
247
|
-
next();
|
|
248
|
-
}
|
|
249
|
-
else if (e.key === 'ArrowLeft') {
|
|
250
|
-
e.preventDefault();
|
|
251
|
-
back();
|
|
252
|
-
}
|
|
253
|
-
};
|
|
254
|
-
document.addEventListener('keydown', onKey, true);
|
|
255
|
-
return () => document.removeEventListener('keydown', onKey, true);
|
|
256
|
-
}, [live, next, back, done]);
|
|
257
|
-
if (!live || !stop || !spot || !card || typeof document === 'undefined')
|
|
258
|
-
return null;
|
|
259
|
-
const last = i + 1 >= stops.length;
|
|
260
|
-
// The dim is four panels around the spot rather than one box with a hole:
|
|
261
|
-
// a hole wants a mask, and a mask over a blurred backdrop reads as a seam.
|
|
262
|
-
const panel = { position: 'fixed', bg: 'var(--surface-scrim)', z: 'var(--z-overlay)', onClick: () => done('skipped') };
|
|
263
|
-
return createPortal(_jsxs("div", { role: "dialog", "aria-modal": "true", "aria-label": `Getting started with ${brand()}`, 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);
|
|
264
|
-
}
|
|
265
|
-
/** Let a room's guide be asked for again — a menu row can call this and reload. */
|
|
266
|
-
export function replayGuide(room = 'chat') {
|
|
267
|
-
try {
|
|
268
|
-
localStorage.removeItem(keyOf(room));
|
|
269
|
-
}
|
|
270
|
-
catch {
|
|
271
|
-
/* nothing to clear */
|
|
272
|
-
}
|
|
273
|
-
}
|
package/dist/Scene.d.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
export declare function Scene({ step, onNext, onSkip }: {
|
|
2
|
-
step: string;
|
|
3
|
-
onNext: () => void;
|
|
4
|
-
onSkip: () => void;
|
|
5
|
-
}): import("react").JSX.Element;
|
|
6
|
-
/** A stop's one primary control, the scene's and the card's. It takes focus when the stop opens, which is what
|
|
7
|
-
* `autoFocus` did on the element it replaced; a stop that only relabels it (Next to Done) keeps the focus it has.
|
|
8
|
-
*/
|
|
9
|
-
export declare function Next({ onPress, label }: {
|
|
10
|
-
onPress: () => void;
|
|
11
|
-
label?: string;
|
|
12
|
-
}): import("react").JSX.Element;
|
package/dist/Scene.js
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
// The scene: one chat, the team brought in, and the call it turns into — played
|
|
4
|
-
// rather than read. It is drawn on a stage of its own, so nothing typed here
|
|
5
|
-
// reaches the composer, and nothing said here is sent or saved.
|
|
6
|
-
import { useEffect, useRef, useState } from 'react';
|
|
7
|
-
import { Text, View, XStack, YStack } from '@hanzo/gui';
|
|
8
|
-
import { Button } from '@hanzo/ui';
|
|
9
|
-
import { CAST as CREW } from './cast.js';
|
|
10
|
-
const CAST = {
|
|
11
|
-
you: { name: 'You', part: 'Owner', initial: 'Y', ground: 'var(--neutral-700)' },
|
|
12
|
-
des: { name: 'Des', part: 'Designer', face: '/agents/des.png', initial: 'D', ground: CREW.des.ground },
|
|
13
|
-
vi: { name: 'Vi', part: 'Visionary', face: '/agents/vi.png', initial: 'V', ground: CREW.vi.ground },
|
|
14
|
-
alex: { name: 'Alex', part: 'Teammate', initial: 'A', ground: 'var(--neutral-600)' },
|
|
15
|
-
};
|
|
16
|
-
/** The tour's own prompt: typed for show, never sent. */
|
|
17
|
-
const SAMPLE = 'Help me plan the next version of our app';
|
|
18
|
-
const ADDED = ['des', 'vi', 'alex'];
|
|
19
|
-
const CHAT = [
|
|
20
|
-
{ who: 'you', text: 'We need to create a more immersive experience.' },
|
|
21
|
-
{ who: 'vi', text: 'Should we create UI for wearable glasses?' },
|
|
22
|
-
{ who: 'des', text: 'I love this idea. We could explore the best settings in Roblox and other top games and create some of the best ideas!' },
|
|
23
|
-
{ who: 'alex', text: "What a great idea, let's jump on a call now so we can plan and create deliverables and due dates." },
|
|
24
|
-
];
|
|
25
|
-
const CALL = [
|
|
26
|
-
{ who: 'alex', text: "Let's plan it. Vi, you own the concept. Des, the first mockups. I'll set the deliverables and due dates." },
|
|
27
|
-
{ who: 'vi', text: 'First deliverable: a glasses-ready layout, due Friday.' },
|
|
28
|
-
{ who: 'des', text: "I'll pull references from the top Roblox worlds and draft three directions, due Wednesday." },
|
|
29
|
-
{ who: 'you', text: 'Perfect. Save the transcript and put the deliverables on the Board.' },
|
|
30
|
-
];
|
|
31
|
-
// The beats, in order: the prompt, three people added, four lines of chat, the
|
|
32
|
-
// move to a call, four lines said on it, and the record of it.
|
|
33
|
-
const PROMPT = 0;
|
|
34
|
-
const ADD = 1; // .. 3
|
|
35
|
-
const TALK = 4; // .. 7
|
|
36
|
-
const RING = 8;
|
|
37
|
-
const SAY = 9; // .. 12
|
|
38
|
-
const SAVED = 13;
|
|
39
|
-
const MS = 26;
|
|
40
|
-
/** Text revealed a character at a time; says when it is all there. */
|
|
41
|
-
function Typed({ text, go, onDone }) {
|
|
42
|
-
const [n, setN] = useState(go ? 0 : text.length);
|
|
43
|
-
useEffect(() => {
|
|
44
|
-
if (!go)
|
|
45
|
-
return;
|
|
46
|
-
if (n >= text.length) {
|
|
47
|
-
const t = window.setTimeout(onDone, 520);
|
|
48
|
-
return () => window.clearTimeout(t);
|
|
49
|
-
}
|
|
50
|
-
const t = window.setTimeout(() => setN((k) => k + 1), MS);
|
|
51
|
-
return () => window.clearTimeout(t);
|
|
52
|
-
}, [go, n, text, onDone]);
|
|
53
|
-
return _jsx(_Fragment, { children: text.slice(0, n) });
|
|
54
|
-
}
|
|
55
|
-
function Face({ who, size = 28 }) {
|
|
56
|
-
const c = CAST[who];
|
|
57
|
-
return (_jsx(View, { render: "span", "aria-hidden": true, width: size, height: size, rounded: 9999, bg: c.ground, display: "inline-flex", items: "center", justify: "center", overflow: "hidden", shrink: 0, children: c.face ? (_jsx("img", { src: c.face, alt: "", width: size, height: size })) : (_jsx(Text, { fontFamily: "$body", fontSize: size * 0.42, fontWeight: "600", color: "var(--pure-white)", children: c.initial })) }));
|
|
58
|
-
}
|
|
59
|
-
const ink = '$ink';
|
|
60
|
-
const dim = '$soft';
|
|
61
|
-
const line = '$borderColor';
|
|
62
|
-
// The stage is portalled to <body> by the Guide, outside every font scope gui
|
|
63
|
-
// writes, so a `$N` size or leading resolves only on a Text that names `$body`
|
|
64
|
-
// itself — which is why every Text below does.
|
|
65
|
-
/** A line of the scene's own type: the base rung on its own leading. */
|
|
66
|
-
const SAID = { fontSize: '$3', lineHeight: '$3' };
|
|
67
|
-
export function Scene({ step, onNext, onSkip }) {
|
|
68
|
-
const [beat, setBeat] = useState(PROMPT);
|
|
69
|
-
const [typed, setTyped] = useState(0);
|
|
70
|
-
const advance = () => setBeat((b) => Math.min(b + 1, SAVED));
|
|
71
|
-
const settle = () => setTyped((k) => k + 1);
|
|
72
|
-
// Beats that type advance themselves when the typing is done; the ones that
|
|
73
|
-
// only appear advance on a short pause.
|
|
74
|
-
useEffect(() => {
|
|
75
|
-
const typing = beat === PROMPT || (beat >= TALK && beat < RING) || (beat >= SAY && beat < SAVED);
|
|
76
|
-
if (typing)
|
|
77
|
-
return;
|
|
78
|
-
if (beat === SAVED)
|
|
79
|
-
return;
|
|
80
|
-
const t = window.setTimeout(advance, beat === RING ? 1400 : 900);
|
|
81
|
-
return () => window.clearTimeout(t);
|
|
82
|
-
}, [beat]);
|
|
83
|
-
useEffect(() => {
|
|
84
|
-
if (typed === 0)
|
|
85
|
-
return;
|
|
86
|
-
advance();
|
|
87
|
-
}, [typed]);
|
|
88
|
-
const added = ADDED.slice(0, Math.max(0, Math.min(ADDED.length, beat - ADD + 1)));
|
|
89
|
-
const said = CHAT.slice(0, Math.max(0, Math.min(CHAT.length, beat - TALK + 1)));
|
|
90
|
-
const onCall = beat >= RING;
|
|
91
|
-
const spoken = CALL.slice(0, Math.max(0, Math.min(CALL.length, beat - SAY + 1)));
|
|
92
|
-
const speaker = onCall && beat >= SAY && beat < SAVED ? CALL[beat - SAY].who : null;
|
|
93
|
-
return (_jsxs(YStack, { role: "group", "aria-label": "A chat that becomes a call", position: "fixed", l: "50%", t: "50%", x: "-50%", y: "-50%", width: "min(720px, calc(100vw - 32px))", maxH: "calc(100vh - 32px)", overflowY: "auto", z: "var(--z-modal)", bg: "var(--popover)", borderWidth: 1, borderColor: "var(--border-control)", rounded: 14, boxShadow: "var(--shadow-floating)", p: 18, children: [_jsxs(XStack, { items: "center", gap: 10, mb: 12, children: [_jsx(Text, { fontFamily: "$body", fontSize: 10, letterSpacing: 1.1, textTransform: "uppercase", color: "$faint", children: step }), _jsx(View, { flex: 1, height: 1, bg: line }), _jsx(Text, { fontFamily: "$body", fontSize: "$2", color: dim, children: onCall ? 'On a call' : 'In chat' })] }), _jsx(Text, { fontFamily: "$body", render: "h2", mb: 4, fontSize: "$6", fontWeight: "600", lineHeight: "$4", color: ink, children: "More than chat" }), _jsx(Text, { fontFamily: "$body", render: "p", mb: 14, ...SAID, color: "$quiet", children: "Start with a bot, bring in the team, and take it to a call. Nothing here is sent or saved." }), !onCall ? (_jsxs(YStack, { borderWidth: 1, borderColor: line, rounded: 12, overflow: "hidden", children: [_jsxs(XStack, { items: "center", gap: 10, px: 12, py: 10, borderBottomWidth: 1, borderColor: line, children: [_jsx(Face, { who: "des" }), _jsxs(YStack, { children: [_jsx(Text, { fontFamily: "$body", fontSize: "$3", fontWeight: "600", lineHeight: "$1", color: ink, children: "des" }), _jsx(Text, { fontFamily: "$body", fontSize: "$2", lineHeight: "$1", color: dim, children: "Designer \u00B7 bot" })] }), _jsx(View, { flex: 1 }), _jsxs(XStack, { gap: 6, flexWrap: "wrap", justify: "flex-end", children: [added.map((w) => (_jsxs(XStack, { items: "center", gap: 6, pl: 4, pr: 8, py: 3, rounded: 999, borderWidth: 1, borderColor: line, children: [_jsx(Face, { who: w, size: 18 }), _jsx(Text, { fontFamily: "$body", fontSize: "$2", color: ink, children: CAST[w].name }), _jsxs(Text, { fontFamily: "$body", fontSize: "$2", color: dim, children: ["\u00B7 ", CAST[w].part] })] }, w))), beat >= ADD && beat < TALK ? (_jsx(Text, { fontFamily: "$body", px: 8, py: 3, rounded: 999, borderWidth: 1, borderStyle: "dashed", borderColor: line, fontSize: "$2", color: dim, children: "+ Add to chat" })) : null] })] }), _jsxs(View, { display: "grid", p: 12, gap: 10, minH: 168, children: [said.map((m, k) => (_jsxs(XStack, { gap: 10, items: "flex-start", flexDirection: m.who === 'you' ? 'row-reverse' : 'row', children: [_jsx(Face, { who: m.who }), _jsxs(YStack, { maxW: "78%", px: 12, py: 8, rounded: 12, bg: m.who === 'you' ? 'var(--foreground)' : '$hover', children: [m.who !== 'you' ? (_jsx(Text, { fontFamily: "$body", fontSize: "$1", color: dim, mb: 2, children: CAST[m.who].name })) : null, _jsx(Text, { fontFamily: "$body", ...SAID, color: m.who === 'you' ? 'var(--background)' : ink, children: _jsx(Typed, { text: m.text, go: k === said.length - 1 && beat < RING, onDone: settle }) })] })] }, k))), said.length === 0 ? (_jsx(Text, { fontFamily: "$body", fontSize: "$2", color: dim, text: "center", pt: 48, children: beat < ADD ? 'A chat with des, your designer bot.' : 'Bringing the team in…' })) : null] }), _jsxs(Text, { fontFamily: "$body", m: 12, px: 12, py: 10, rounded: 10, borderWidth: 1, borderColor: line, ...SAID, color: beat === PROMPT ? ink : dim, minH: 40, children: [beat === PROMPT ? _jsx(Typed, { text: SAMPLE, go: true, onDone: settle }) : beat < TALK ? SAMPLE : 'Ask anything', _jsx(View, { render: "span", "aria-hidden": true, display: beat === PROMPT ? 'inline-flex' : 'none', width: 1, height: 14, bg: ink, ml: 1, y: 2 })] })] })) : (_jsxs(YStack, { borderWidth: 1, borderColor: line, rounded: 12, overflow: "hidden", children: [_jsx(View, { display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: 8, p: 12, borderBottomWidth: 1, borderColor: line, children: ['you', 'des', 'vi', 'alex'].map((w) => (_jsxs(YStack, { items: "center", gap: 6, px: 6, py: 12, rounded: 10, bg: "$hover", outlineWidth: 2, outlineStyle: "solid", outlineColor: speaker === w ? ink : 'transparent', transition: "200ms", children: [_jsx(Face, { who: w, size: 40 }), _jsx(Text, { fontFamily: "$body", fontSize: "$2", color: ink, children: CAST[w].name })] }, w))) }), _jsxs(YStack, { p: 12, minH: 168, children: [_jsx(Text, { fontFamily: "$body", fontSize: "$1", letterSpacing: 1.1, textTransform: "uppercase", color: dim, mb: 8, children: beat === RING ? 'Calling…' : 'Live transcript' }), _jsx(View, { display: "grid", gap: 8, children: spoken.map((m, k) => (_jsxs(XStack, { gap: 8, children: [_jsx(Text, { fontFamily: "$body", ...SAID, color: dim, shrink: 0, width: 44, children: CAST[m.who].name }), _jsx(Text, { fontFamily: "$body", ...SAID, color: ink, children: _jsx(Typed, { text: m.text, go: k === spoken.length - 1 && beat < SAVED, onDone: settle }) })] }, k))) }), beat === SAVED ? (_jsx(Text, { fontFamily: "$body", mt: 14, px: 10, py: 8, rounded: 8, borderWidth: 1, borderColor: line, fontSize: "$2", color: ink, children: "\u2713 Transcript saved \u00B7 Deliverables and due dates on the Board" })) : null] })] })), _jsxs(XStack, { items: "center", gap: 8, mt: 14, children: [_jsx(Button, { variant: "linkMuted", size: "sm", px: 2, onClick: onSkip, children: "Skip" }), _jsx(View, { flex: 1 }), beat === SAVED ? (_jsx(Button, { size: "sm", onClick: () => {
|
|
94
|
-
setTyped(0);
|
|
95
|
-
setBeat(PROMPT);
|
|
96
|
-
}, children: "Replay" })) : null, _jsx(Next, { onPress: onNext })] })] }));
|
|
97
|
-
}
|
|
98
|
-
/** A stop's one primary control, the scene's and the card's. It takes focus when the stop opens, which is what
|
|
99
|
-
* `autoFocus` did on the element it replaced; a stop that only relabels it (Next to Done) keeps the focus it has.
|
|
100
|
-
*/
|
|
101
|
-
export function Next({ onPress, label = 'Next' }) {
|
|
102
|
-
const ref = useRef(null);
|
|
103
|
-
useEffect(() => ref.current?.focus(), []);
|
|
104
|
-
return (_jsx(Button, { ref: ref, variant: "primary", size: "sm", onClick: onPress, children: label }));
|
|
105
|
-
}
|
package/dist/tours.d.ts
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
/** One stop: what to point at, and what to say about it. */
|
|
2
|
-
export interface Stop {
|
|
3
|
-
id: string;
|
|
4
|
-
title: string;
|
|
5
|
-
body: string;
|
|
6
|
-
/** Where the card sits relative to the anchor; it flips if it would leave the viewport. */
|
|
7
|
-
side?: 'top' | 'bottom' | 'left' | 'right';
|
|
8
|
-
/** Resolve the live element. Returning null skips the stop. */
|
|
9
|
-
find: () => Element | null;
|
|
10
|
-
/** Played rather than read: the scene, drawn on a stage instead of a card. */
|
|
11
|
-
scene?: boolean;
|
|
12
|
-
}
|
|
13
|
-
export type Tour = 'chat' | 'meet' | 'calendar' | 'contacts' | 'inbox' | 'drive' | 'board';
|
|
14
|
-
export declare const seen: (el: Element | null) => Element | null;
|
|
15
|
-
export declare const byLabel: (...labels: string[]) => Element | null;
|
|
16
|
-
export declare const byPlaceholder: (...values: string[]) => Element;
|
|
17
|
-
export declare const byTitle: (...values: string[]) => Element;
|
|
18
|
-
/** The smallest element whose own text is exactly this word. */
|
|
19
|
-
export declare const byText: (text: string, within?: Element | null) => Element | null;
|
|
20
|
-
/** The composer is the one visible textarea in the room. */
|
|
21
|
-
export declare const composer: () => Element | null;
|
|
22
|
-
/** The Enso control on the composer: effort, and the model override inside it. */
|
|
23
|
-
export declare const modelChip: () => Element | null;
|
|
24
|
-
export declare const TOURS: Record<Tour, Stop[]>;
|
package/dist/tours.js
DELETED
|
@@ -1,319 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
// The tours, one per room, and the finders they point with.
|
|
3
|
-
//
|
|
4
|
-
// A stop RESOLVES A LIVE ELEMENT by what it already carries — its label, its
|
|
5
|
-
// role, its text — never a marker sprinkled for the tour. A stop whose anchor is
|
|
6
|
-
// absent is skipped, so each tour describes the room as it is that day.
|
|
7
|
-
import { brand } from './where.js';
|
|
8
|
-
export const seen = (el) => {
|
|
9
|
-
if (!el)
|
|
10
|
-
return null;
|
|
11
|
-
const r = el.getBoundingClientRect();
|
|
12
|
-
if (r.width < 6 || r.height < 6)
|
|
13
|
-
return null;
|
|
14
|
-
const s = getComputedStyle(el);
|
|
15
|
-
if (s.visibility === 'hidden' || s.display === 'none' || Number(s.opacity) === 0)
|
|
16
|
-
return null;
|
|
17
|
-
return el;
|
|
18
|
-
};
|
|
19
|
-
export const byLabel = (...labels) => {
|
|
20
|
-
for (const l of labels) {
|
|
21
|
-
const el = seen(document.querySelector(`[aria-label="${l}"]`));
|
|
22
|
-
if (el)
|
|
23
|
-
return el;
|
|
24
|
-
}
|
|
25
|
-
return null;
|
|
26
|
-
};
|
|
27
|
-
const byAttr = (attr, ...values) => {
|
|
28
|
-
for (const v of values) {
|
|
29
|
-
const el = seen(document.querySelector(`[${attr}="${v}"]`));
|
|
30
|
-
if (el)
|
|
31
|
-
return el;
|
|
32
|
-
}
|
|
33
|
-
return null;
|
|
34
|
-
};
|
|
35
|
-
export const byPlaceholder = (...values) => byAttr('placeholder', ...values);
|
|
36
|
-
export const byTitle = (...values) => byAttr('title', ...values);
|
|
37
|
-
/** The smallest element whose own text is exactly this word. */
|
|
38
|
-
export const byText = (text, within) => {
|
|
39
|
-
const root = within ?? document.body;
|
|
40
|
-
let best = null;
|
|
41
|
-
for (const el of root.querySelectorAll('button,a,[role="button"]')) {
|
|
42
|
-
if ((el.textContent || '').trim() !== text)
|
|
43
|
-
continue;
|
|
44
|
-
const ok = seen(el);
|
|
45
|
-
if (!ok)
|
|
46
|
-
continue;
|
|
47
|
-
if (!best || el.getBoundingClientRect().width < best.getBoundingClientRect().width)
|
|
48
|
-
best = el;
|
|
49
|
-
}
|
|
50
|
-
return best;
|
|
51
|
-
};
|
|
52
|
-
/** A field whose placeholder starts with these words. */
|
|
53
|
-
const byPlaceholderStart = (start) => {
|
|
54
|
-
for (const el of document.querySelectorAll('input,textarea')) {
|
|
55
|
-
if ((el.getAttribute('placeholder') || '').startsWith(start) && seen(el))
|
|
56
|
-
return el;
|
|
57
|
-
}
|
|
58
|
-
return null;
|
|
59
|
-
};
|
|
60
|
-
/** The composer is the one visible textarea in the room. */
|
|
61
|
-
export const composer = () => {
|
|
62
|
-
const all = [...document.querySelectorAll('textarea')].map(seen).filter(Boolean);
|
|
63
|
-
return all.sort((a, b) => b.getBoundingClientRect().width - a.getBoundingClientRect().width)[0] ?? null;
|
|
64
|
-
};
|
|
65
|
-
/** The Enso control on the composer: effort, and the model override inside it. */
|
|
66
|
-
export const modelChip = () => seen(document.querySelector('[aria-label^="Enso"]'));
|
|
67
|
-
/** A place on the rail, by the word under its icon. */
|
|
68
|
-
const rail = (label) => {
|
|
69
|
-
const b = byText(label);
|
|
70
|
-
return b?.parentElement ?? b;
|
|
71
|
-
};
|
|
72
|
-
const CHAT = [
|
|
73
|
-
{
|
|
74
|
-
id: 'composer',
|
|
75
|
-
get title() {
|
|
76
|
-
return `What are you interested in? How can ${brand()} help you?`;
|
|
77
|
-
},
|
|
78
|
-
body: 'Type what you want in plain words. Paste an error, describe a feature, or drop in a file.',
|
|
79
|
-
side: 'top',
|
|
80
|
-
find: composer,
|
|
81
|
-
},
|
|
82
|
-
{
|
|
83
|
-
id: 'model',
|
|
84
|
-
title: 'Perfectly tuned with 500+ of the leading models',
|
|
85
|
-
body: 'Enso reads your question and picks the best model for it. Tap here to choose one yourself.',
|
|
86
|
-
side: 'top',
|
|
87
|
-
find: modelChip,
|
|
88
|
-
},
|
|
89
|
-
{
|
|
90
|
-
id: 'work',
|
|
91
|
-
title: 'Audit the work-flow',
|
|
92
|
-
body: "Show work opens the AI's steps and the tools it used. Turn it on when an answer surprises you.",
|
|
93
|
-
side: 'top',
|
|
94
|
-
find: () => byText('Show work') || byText('Hide work'),
|
|
95
|
-
},
|
|
96
|
-
{
|
|
97
|
-
id: 'scene',
|
|
98
|
-
title: 'More than chat',
|
|
99
|
-
body: 'Chat with a bot, bring in the team, and take it to a call. Watch one happen.',
|
|
100
|
-
side: 'right',
|
|
101
|
-
scene: true,
|
|
102
|
-
find: () => rail('Chat'),
|
|
103
|
-
},
|
|
104
|
-
{
|
|
105
|
-
id: 'roster',
|
|
106
|
-
title: 'People and agents, one list',
|
|
107
|
-
body: 'Your team and your AI agents sit together. Click any of them to start talking. An agent answers like a person.',
|
|
108
|
-
side: 'right',
|
|
109
|
-
find: () => {
|
|
110
|
-
const az = byLabel('A-Z', 'Sort A-Z');
|
|
111
|
-
if (az?.parentElement)
|
|
112
|
-
return az.parentElement;
|
|
113
|
-
const dir = byText('Directory');
|
|
114
|
-
return dir?.parentElement ?? dir;
|
|
115
|
-
},
|
|
116
|
-
},
|
|
117
|
-
{
|
|
118
|
-
id: 'find',
|
|
119
|
-
title: 'Find anything',
|
|
120
|
-
body: 'Press ⌘K to search chats, people, agents and every section at once.',
|
|
121
|
-
side: 'bottom',
|
|
122
|
-
find: () => {
|
|
123
|
-
for (const b of document.querySelectorAll('button,[role="button"]')) {
|
|
124
|
-
if (/^Search\b/.test((b.textContent || '').trim()) && seen(b))
|
|
125
|
-
return b;
|
|
126
|
-
}
|
|
127
|
-
return null;
|
|
128
|
-
},
|
|
129
|
-
},
|
|
130
|
-
{
|
|
131
|
-
id: 'aside',
|
|
132
|
-
title: 'Results stay beside you',
|
|
133
|
-
body: 'Files, sources and previews collect in the side panel, so the chat stays easy to read.',
|
|
134
|
-
side: 'bottom',
|
|
135
|
-
find: () => byLabel('Open side panel', 'Close side panel'),
|
|
136
|
-
},
|
|
137
|
-
{
|
|
138
|
-
id: 'orgs',
|
|
139
|
-
title: 'Switch workspaces',
|
|
140
|
-
body: 'Each icon on the far left is a workspace, with its own chats, agents, files and billing.',
|
|
141
|
-
side: 'right',
|
|
142
|
-
find: () => {
|
|
143
|
-
// The rail is a column of small square marks pinned to the far edge —
|
|
144
|
-
// that shape is what identifies it, not a label, because each mark is
|
|
145
|
-
// labelled with its own workspace ("LUX", "ZOO", an initial) and none of
|
|
146
|
-
// them names the rail. Take their common parent.
|
|
147
|
-
const marks = [...document.querySelectorAll('button,[role="button"]')].filter((el) => {
|
|
148
|
-
const r = el.getBoundingClientRect();
|
|
149
|
-
return r.left < 64 && r.width >= 26 && r.width <= 60 && Math.abs(r.width - r.height) < 10 && seen(el);
|
|
150
|
-
});
|
|
151
|
-
if (marks.length < 2)
|
|
152
|
-
return byLabel('Switch organization');
|
|
153
|
-
let node = marks[0].parentElement;
|
|
154
|
-
while (node && !marks.every((m) => node.contains(m)))
|
|
155
|
-
node = node.parentElement;
|
|
156
|
-
return seen(node) ?? marks[0];
|
|
157
|
-
},
|
|
158
|
-
},
|
|
159
|
-
{
|
|
160
|
-
id: 'account',
|
|
161
|
-
title: 'Your plan',
|
|
162
|
-
body: 'See your credit, manage payment and change settings. Free uses the open models. Paid unlocks the rest.',
|
|
163
|
-
side: 'top',
|
|
164
|
-
find: () => byLabel('Account, plan and workspace'),
|
|
165
|
-
},
|
|
166
|
-
];
|
|
167
|
-
const MEET = [
|
|
168
|
-
{
|
|
169
|
-
id: 'start',
|
|
170
|
-
title: 'Start a meeting',
|
|
171
|
-
body: 'One click opens a room. Share the link and anyone can join from a browser.',
|
|
172
|
-
side: 'bottom',
|
|
173
|
-
find: () => byText('Start Instant Meeting'),
|
|
174
|
-
},
|
|
175
|
-
{
|
|
176
|
-
id: 'join',
|
|
177
|
-
title: 'Join with a code',
|
|
178
|
-
body: 'Paste a meeting code or link here to join a call someone else started.',
|
|
179
|
-
side: 'bottom',
|
|
180
|
-
find: () => byPlaceholder('Enter meeting code or link'),
|
|
181
|
-
},
|
|
182
|
-
{
|
|
183
|
-
id: 'transcript',
|
|
184
|
-
title: 'Every call is written down',
|
|
185
|
-
body: 'A Hanzo bot sits in and transcribes. What everyone said is saved with the meeting.',
|
|
186
|
-
side: 'left',
|
|
187
|
-
find: () => {
|
|
188
|
-
for (const el of document.querySelectorAll('div,p,section')) {
|
|
189
|
-
if (el.childElementCount === 0 && /transcription/i.test(el.textContent || '') && seen(el))
|
|
190
|
-
return el.parentElement ?? el;
|
|
191
|
-
}
|
|
192
|
-
return null;
|
|
193
|
-
},
|
|
194
|
-
},
|
|
195
|
-
];
|
|
196
|
-
const CALENDAR = [
|
|
197
|
-
{
|
|
198
|
-
id: 'schedule',
|
|
199
|
-
title: 'Add an event or a routine',
|
|
200
|
-
body: 'A meeting, a reminder, or a job an agent runs on a schedule. Name it and pick the time.',
|
|
201
|
-
side: 'bottom',
|
|
202
|
-
find: () => byText('Schedule Event / Routine'),
|
|
203
|
-
},
|
|
204
|
-
{
|
|
205
|
-
id: 'crons',
|
|
206
|
-
title: 'Bots on a timer',
|
|
207
|
-
body: 'Bot Crons lists what your bots run and when. Change a time here and the bot follows it.',
|
|
208
|
-
side: 'bottom',
|
|
209
|
-
find: () => byText('Bot Crons'),
|
|
210
|
-
},
|
|
211
|
-
{
|
|
212
|
-
id: 'booker',
|
|
213
|
-
title: 'Let people book you',
|
|
214
|
-
body: 'A public page where anyone can pick a free time on your calendar.',
|
|
215
|
-
side: 'bottom',
|
|
216
|
-
find: () => byText('Public Booker'),
|
|
217
|
-
},
|
|
218
|
-
];
|
|
219
|
-
const CONTACTS = [
|
|
220
|
-
{
|
|
221
|
-
id: 'search',
|
|
222
|
-
title: 'Everyone in one place',
|
|
223
|
-
body: 'People and agents, side by side. Search by name to find anyone fast.',
|
|
224
|
-
side: 'bottom',
|
|
225
|
-
find: () => byLabel('Search contacts') || byPlaceholder('Search'),
|
|
226
|
-
},
|
|
227
|
-
{
|
|
228
|
-
id: 'invite',
|
|
229
|
-
title: 'Invite your team',
|
|
230
|
-
body: 'Send an invite by email, or share a link anyone can use to join your organization.',
|
|
231
|
-
side: 'bottom',
|
|
232
|
-
find: () => byLabel('Invite') || byText('Invite'),
|
|
233
|
-
},
|
|
234
|
-
{
|
|
235
|
-
id: 'agent',
|
|
236
|
-
title: 'Make an agent',
|
|
237
|
-
body: 'Give it a name and a job. It joins the roster next to your people and answers like one.',
|
|
238
|
-
side: 'bottom',
|
|
239
|
-
find: () => byText('New agent'),
|
|
240
|
-
},
|
|
241
|
-
];
|
|
242
|
-
const INBOX = [
|
|
243
|
-
{
|
|
244
|
-
id: 'connect',
|
|
245
|
-
title: 'Every channel, one inbox',
|
|
246
|
-
body: 'Connect Slack, email, Discord and the rest. Their messages land here together.',
|
|
247
|
-
side: 'bottom',
|
|
248
|
-
find: () => byText('Connect an app'),
|
|
249
|
-
},
|
|
250
|
-
{
|
|
251
|
-
id: 'channels',
|
|
252
|
-
title: 'Your channels',
|
|
253
|
-
body: 'Each connected app is a channel on the left. Nothing gets lost between them.',
|
|
254
|
-
side: 'right',
|
|
255
|
-
find: () => byLabel('Resize inbox channel list')?.parentElement ?? null,
|
|
256
|
-
},
|
|
257
|
-
{
|
|
258
|
-
id: 'reply',
|
|
259
|
-
title: 'Reply where it came from',
|
|
260
|
-
body: 'Answer here and it goes back on the same channel the message arrived on.',
|
|
261
|
-
side: 'top',
|
|
262
|
-
find: () => byPlaceholderStart('Reply on'),
|
|
263
|
-
},
|
|
264
|
-
];
|
|
265
|
-
const DRIVE = [
|
|
266
|
-
{
|
|
267
|
-
id: 'search',
|
|
268
|
-
title: 'Search everything',
|
|
269
|
-
body: 'Every file your team and your agents made, found by name or by what is inside it.',
|
|
270
|
-
side: 'bottom',
|
|
271
|
-
find: () => byLabel('Search Drive'),
|
|
272
|
-
},
|
|
273
|
-
{
|
|
274
|
-
id: 'upload',
|
|
275
|
-
title: 'Bring files in',
|
|
276
|
-
body: 'Upload anything. Agents can read it, and it shows up in chat when you ask.',
|
|
277
|
-
side: 'bottom',
|
|
278
|
-
find: () => byTitle('Upload'),
|
|
279
|
-
},
|
|
280
|
-
{
|
|
281
|
-
id: 'folder',
|
|
282
|
-
title: 'Keep it tidy',
|
|
283
|
-
body: 'Make folders for projects, clients or agents. Drag files between them.',
|
|
284
|
-
side: 'bottom',
|
|
285
|
-
find: () => byTitle('Add Folder'),
|
|
286
|
-
},
|
|
287
|
-
];
|
|
288
|
-
const BOARD = [
|
|
289
|
-
{
|
|
290
|
-
id: 'add',
|
|
291
|
-
title: 'Add a card',
|
|
292
|
-
body: 'One card per task. Give it a date, a priority, and a person or an agent to do it.',
|
|
293
|
-
side: 'bottom',
|
|
294
|
-
find: () => byText('Add a card') || byPlaceholder('Add an item'),
|
|
295
|
-
},
|
|
296
|
-
{
|
|
297
|
-
id: 'planner',
|
|
298
|
-
title: 'Plan by day',
|
|
299
|
-
body: 'Planner lays the cards out by date, so today is one column and next week is a glance away.',
|
|
300
|
-
side: 'bottom',
|
|
301
|
-
find: () => byText('Planner'),
|
|
302
|
-
},
|
|
303
|
-
{
|
|
304
|
-
id: 'filter',
|
|
305
|
-
title: 'See only what matters now',
|
|
306
|
-
body: 'Filter by who, by when, or by what is finished.',
|
|
307
|
-
side: 'bottom',
|
|
308
|
-
find: () => byText('Filter'),
|
|
309
|
-
},
|
|
310
|
-
];
|
|
311
|
-
export const TOURS = {
|
|
312
|
-
chat: CHAT,
|
|
313
|
-
meet: MEET,
|
|
314
|
-
calendar: CALENDAR,
|
|
315
|
-
contacts: CONTACTS,
|
|
316
|
-
inbox: INBOX,
|
|
317
|
-
drive: DRIVE,
|
|
318
|
-
board: BOARD,
|
|
319
|
-
};
|