@hanzo/ui 8.27.3 → 8.27.4
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/backdrop/Backdrop.cjs +420 -0
- package/dist/backdrop/Backdrop.d.ts +53 -0
- package/dist/backdrop/Backdrop.d.ts.map +1 -0
- package/dist/backdrop/Backdrop.js +418 -0
- package/dist/backdrop/Backdrop.js.map +1 -0
- package/dist/backdrop/index.cjs +50 -0
- package/dist/backdrop/index.d.ts +29 -0
- package/dist/backdrop/index.d.ts.map +1 -0
- package/dist/backdrop/index.js +29 -0
- package/dist/backdrop/index.js.map +1 -0
- package/dist/backdrop/scene.cjs +439 -0
- package/dist/backdrop/scene.d.ts +235 -0
- package/dist/backdrop/scene.d.ts.map +1 -0
- package/dist/backdrop/scene.js +423 -0
- package/dist/backdrop/scene.js.map +1 -0
- package/dist/chat/Chat.cjs +23 -1
- package/dist/chat/Chat.d.ts +6 -3
- package/dist/chat/Chat.d.ts.map +1 -1
- package/dist/chat/Chat.js +23 -1
- package/dist/chat/Chat.js.map +1 -1
- package/dist/chat/Inspector.cjs +51 -0
- package/dist/chat/Inspector.d.ts +49 -0
- package/dist/chat/Inspector.d.ts.map +1 -0
- package/dist/chat/Inspector.js +49 -0
- package/dist/chat/Inspector.js.map +1 -0
- package/dist/chat/Message.cjs +3 -1
- package/dist/chat/Message.d.ts +3 -1
- package/dist/chat/Message.d.ts.map +1 -1
- package/dist/chat/Message.js +3 -1
- package/dist/chat/Message.js.map +1 -1
- package/dist/chat/Parts.cjs +159 -0
- package/dist/chat/Parts.d.ts +186 -0
- package/dist/chat/Parts.d.ts.map +1 -0
- package/dist/chat/Parts.js +153 -0
- package/dist/chat/Parts.js.map +1 -0
- package/dist/chat/Persona.cjs +34 -0
- package/dist/chat/Persona.d.ts +37 -0
- package/dist/chat/Persona.d.ts.map +1 -0
- package/dist/chat/Persona.js +30 -0
- package/dist/chat/Persona.js.map +1 -0
- package/dist/chat/Preview.cjs +42 -0
- package/dist/chat/Preview.d.ts +36 -0
- package/dist/chat/Preview.d.ts.map +1 -0
- package/dist/chat/Preview.js +40 -0
- package/dist/chat/Preview.js.map +1 -0
- package/dist/chat/directive.cjs +272 -0
- package/dist/chat/directive.d.ts +167 -0
- package/dist/chat/directive.d.ts.map +1 -0
- package/dist/chat/directive.js +266 -0
- package/dist/chat/directive.js.map +1 -0
- package/dist/chat/index.cjs +59 -4
- package/dist/chat/index.d.ts +31 -3
- package/dist/chat/index.d.ts.map +1 -1
- package/dist/chat/index.js +31 -3
- package/dist/chat/index.js.map +1 -1
- package/dist/gallery.html +4 -4
- package/dist/styles.css +3 -3
- package/dist/theme.css +3 -3
- package/package.json +7 -1
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What paints behind a conversation, and the pure rules that read it.
|
|
3
|
+
*
|
|
4
|
+
* No React and no imports: every rule about what can play, what a link means and
|
|
5
|
+
* what a typed command does is decidable from data alone, so it is decidable in
|
|
6
|
+
* a test without rendering anything — and a surface can read a stored value
|
|
7
|
+
* before it has mounted anything.
|
|
8
|
+
*
|
|
9
|
+
* ONE switch. `mode: 'off'` is the only way to say "no backdrop"; there is
|
|
10
|
+
* deliberately no enable flag beside it, because a pair of them makes two states
|
|
11
|
+
* (off + video, on + off) that paint the same nothing, and that ambiguity is
|
|
12
|
+
* where a bug grows.
|
|
13
|
+
*
|
|
14
|
+
* WHAT CANNOT PLAY, AND WHY: only YouTube and Twitch are embeddable. Every DRM
|
|
15
|
+
* service — Netflix and its neighbours — publishes no embed, wraps playback in
|
|
16
|
+
* Widevine and forbids framing in its terms. There is no key and no clever
|
|
17
|
+
* iframe that changes that, so such a link is classed `other` and NEVER becomes
|
|
18
|
+
* a frame. A menu can offer it as a link that opens in a tab; anything else
|
|
19
|
+
* would be pretending.
|
|
20
|
+
*
|
|
21
|
+
* No provider password is ever asked for. A members-only or age-gated stream
|
|
22
|
+
* plays if — and only if — the viewer is already signed in to that provider in
|
|
23
|
+
* this browser, because the embed rides their own cookies. Collecting those
|
|
24
|
+
* credentials is not a feature this is missing; it is one it refuses.
|
|
25
|
+
*/
|
|
26
|
+
/** A blank canvas — the value a surface starts from and folds changes onto.
|
|
27
|
+
* `off`, because a component library must not begin streaming video on its
|
|
28
|
+
* own; a surface that wants footage says so. */
|
|
29
|
+
export const BLANK = {
|
|
30
|
+
mode: 'off',
|
|
31
|
+
photo: '',
|
|
32
|
+
video: '',
|
|
33
|
+
playlist: [],
|
|
34
|
+
clips: [],
|
|
35
|
+
loop: true,
|
|
36
|
+
sound: false,
|
|
37
|
+
};
|
|
38
|
+
/** How long a live stream holds the canvas before the list moves on. A Twitch
|
|
39
|
+
* channel never ends, so unlike a video it cannot announce its turn is up. */
|
|
40
|
+
export const DWELL = 5 * 60 * 1000;
|
|
41
|
+
/**
|
|
42
|
+
* The longest URL that will be kept. Browsers stopped being the limit long ago;
|
|
43
|
+
* the limit that matters is whatever storage a surface writes this to, one quota
|
|
44
|
+
* shared by every preference it holds. A URL is a string a visitor can paste, so
|
|
45
|
+
* without a ceiling one pasted megabyte fills that quota and the NEXT write of
|
|
46
|
+
* any unrelated setting is the one that throws. 2048 is what every server and
|
|
47
|
+
* proxy has accepted since forever, and far past any real link.
|
|
48
|
+
*/
|
|
49
|
+
const LONGEST = 2048;
|
|
50
|
+
/** The longest list. Same quota, same reasoning: appending is a loop a visitor
|
|
51
|
+
* can run, so it needs an end. Well past any list anyone watches. */
|
|
52
|
+
const MOST = 64;
|
|
53
|
+
/**
|
|
54
|
+
* A short list to choose from without knowing a link — offered, never a default
|
|
55
|
+
* (`BLANK` is the default, and it is off). Short by intent: the set someone can
|
|
56
|
+
* read in one glance, with the whole rest of the web one paste away.
|
|
57
|
+
*/
|
|
58
|
+
export const PRESETS = [
|
|
59
|
+
{ id: 'cetaceans', label: 'Cetaceans', url: 'https://www.youtube.com/watch?v=UY5YH6B4A9o' },
|
|
60
|
+
{ id: 'reef', label: 'Reef', url: 'https://www.youtube.com/watch?v=6lZ3CookYNg' },
|
|
61
|
+
{ id: 'deep', label: 'Deep water', url: 'https://www.youtube.com/watch?v=7Exxd2ievAw' },
|
|
62
|
+
];
|
|
63
|
+
/**
|
|
64
|
+
* The media hosts a file may be loaded from besides the page's own origin.
|
|
65
|
+
*
|
|
66
|
+
* The Hanzo store is the default because it is where our surfaces keep footage.
|
|
67
|
+
* A brand serving from somewhere else passes its own list — every function here
|
|
68
|
+
* that reads a URL takes one — and whatever it passes must also be what its
|
|
69
|
+
* `img-src` / `media-src` policy allows, or the element is refused by the
|
|
70
|
+
* browser and paints nothing with no error anywhere.
|
|
71
|
+
*/
|
|
72
|
+
export const HOSTS = ['s3.hanzo.ai', 's3-api.hanzo.ai'];
|
|
73
|
+
/**
|
|
74
|
+
* The URL if it is one worth loading, else ''. Anything that is not http(s) —
|
|
75
|
+
* `javascript:`, `data:`, `blob:` — is refused here, at the boundary, so no
|
|
76
|
+
* caller downstream has to remember to check.
|
|
77
|
+
*/
|
|
78
|
+
export function web(value) {
|
|
79
|
+
if (typeof value !== 'string' || value.trim() === '' || value.length > LONGEST)
|
|
80
|
+
return '';
|
|
81
|
+
try {
|
|
82
|
+
const url = new URL(value.trim());
|
|
83
|
+
return url.protocol === 'http:' || url.protocol === 'https:' ? url.href : '';
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return '';
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const named = (url) => {
|
|
90
|
+
try {
|
|
91
|
+
return new URL(url).hostname.replace(/^www\./, '').toLowerCase();
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return '';
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
/** Any absolute URL resolves against this, so a relative path can be told from
|
|
98
|
+
* one that only looks relative. Never fetched; it is a ruler, not a place. */
|
|
99
|
+
const HERE = 'https://backdrop.invalid';
|
|
100
|
+
/**
|
|
101
|
+
* A media file that may be painted or played, else ''.
|
|
102
|
+
*
|
|
103
|
+
* Narrower than `web`, and it is the one rule here that is not about what plays.
|
|
104
|
+
* An `<img>` or a `<video src>` fires a GET the moment it renders, with no click
|
|
105
|
+
* and no script, and a STORED one refires on every load afterwards — reporting
|
|
106
|
+
* the viewer's address to whoever owns the host, forever. So a file is ours: a
|
|
107
|
+
* path on this origin, kept RELATIVE because a surface is served under several
|
|
108
|
+
* brands' domains and a stored absolute origin would be wrong on all but one, or
|
|
109
|
+
* a file on a host the caller allows.
|
|
110
|
+
*
|
|
111
|
+
* A URL the browser's own policy would refuse is dropped HERE instead, where it
|
|
112
|
+
* can be answered honestly: a blocked element never fires `load`, so allowing it
|
|
113
|
+
* would mean a blank canvas and nothing anywhere saying why.
|
|
114
|
+
*
|
|
115
|
+
* Backslashes are why this parses rather than matches: `/\evil.example/x.png`
|
|
116
|
+
* looks like a path and resolves to a different host.
|
|
117
|
+
*
|
|
118
|
+
* A photo and a clip are the same question asked about two elements, so they are
|
|
119
|
+
* one function. Two copies would be two places to get the backslash wrong.
|
|
120
|
+
*/
|
|
121
|
+
export function media(value, hosts = HOSTS) {
|
|
122
|
+
if (typeof value !== 'string' || value.trim() === '' || value.length > LONGEST)
|
|
123
|
+
return '';
|
|
124
|
+
try {
|
|
125
|
+
const url = new URL(value.trim(), HERE);
|
|
126
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:')
|
|
127
|
+
return '';
|
|
128
|
+
if (url.origin === HERE)
|
|
129
|
+
return url.pathname + url.search;
|
|
130
|
+
return hosts.includes(named(url.href)) ? url.href : '';
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return '';
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* A list of files from an array, or from a comma- or space-separated string —
|
|
138
|
+
* whichever a configuration value supplies. Every entry passes through `media`,
|
|
139
|
+
* so an unusable one is dropped rather than trusted, and the whole is capped for
|
|
140
|
+
* the same quota reason a playlist is.
|
|
141
|
+
*/
|
|
142
|
+
export function files(value, hosts = HOSTS) {
|
|
143
|
+
const raw = Array.isArray(value)
|
|
144
|
+
? value.map((v) => (typeof v === 'string' ? v : ''))
|
|
145
|
+
: typeof value === 'string'
|
|
146
|
+
? value.split(/[\s,]+/)
|
|
147
|
+
: [];
|
|
148
|
+
const out = [];
|
|
149
|
+
for (const entry of raw) {
|
|
150
|
+
const ok = media(entry, hosts);
|
|
151
|
+
if (ok && !out.includes(ok))
|
|
152
|
+
out.push(ok);
|
|
153
|
+
if (out.length >= MOST)
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
return out;
|
|
157
|
+
}
|
|
158
|
+
/** Who serves this link, decided by host alone. */
|
|
159
|
+
export function provider(url) {
|
|
160
|
+
const name = named(url);
|
|
161
|
+
if (name === 'youtube.com' || name === 'm.youtube.com' || name === 'music.youtube.com') {
|
|
162
|
+
return 'youtube';
|
|
163
|
+
}
|
|
164
|
+
if (name === 'youtu.be')
|
|
165
|
+
return 'youtube';
|
|
166
|
+
if (name === 'twitch.tv' || name === 'm.twitch.tv' || name === 'player.twitch.tv') {
|
|
167
|
+
return 'twitch';
|
|
168
|
+
}
|
|
169
|
+
return 'other';
|
|
170
|
+
}
|
|
171
|
+
/** A YouTube video id is exactly 11 of these. */
|
|
172
|
+
const ID = /^[\w-]{11}$/;
|
|
173
|
+
/**
|
|
174
|
+
* The video id inside a YouTube URL, or '' if there is not one. Accepts the
|
|
175
|
+
* forms people actually paste — watch links, share links, shorts, embeds — and a
|
|
176
|
+
* bare id, which is what the address bar leaves you with often enough.
|
|
177
|
+
*/
|
|
178
|
+
export function videoId(value) {
|
|
179
|
+
const raw = value.trim();
|
|
180
|
+
if (ID.test(raw))
|
|
181
|
+
return raw;
|
|
182
|
+
const url = web(raw);
|
|
183
|
+
if (!url || provider(url) !== 'youtube')
|
|
184
|
+
return '';
|
|
185
|
+
const { pathname, searchParams } = new URL(url);
|
|
186
|
+
const query = searchParams.get('v') ?? '';
|
|
187
|
+
if (ID.test(query))
|
|
188
|
+
return query;
|
|
189
|
+
// youtu.be/ID, /embed/ID, /shorts/ID, /live/ID — the id is the last segment.
|
|
190
|
+
const last = pathname.split('/').filter(Boolean).pop() ?? '';
|
|
191
|
+
return ID.test(last) ? last : '';
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* What to ask the Twitch player for: a live `channel`, or a recorded `video`.
|
|
195
|
+
* Empty `name` means the URL names neither.
|
|
196
|
+
*
|
|
197
|
+
* A CLIP names neither. `twitch.tv/<channel>/clip/<slug>` is a few seconds
|
|
198
|
+
* somebody cut out, and the player this builds cannot ask for one — that embed
|
|
199
|
+
* is a different host with a different parameter. Falling back to the channel
|
|
200
|
+
* would put a LIVE STREAM on the canvas in place of the clip that was asked for:
|
|
201
|
+
* not the thing, and not obviously not the thing. So it is unplayable, and a
|
|
202
|
+
* menu says so with a link out. Refusing what we cannot serve beats serving
|
|
203
|
+
* something else.
|
|
204
|
+
*/
|
|
205
|
+
export function channel(value) {
|
|
206
|
+
const none = { key: 'channel', name: '' };
|
|
207
|
+
const url = web(value);
|
|
208
|
+
if (!url || provider(url) !== 'twitch')
|
|
209
|
+
return none;
|
|
210
|
+
const parsed = new URL(url);
|
|
211
|
+
const parts = parsed.pathname.split('/').filter(Boolean);
|
|
212
|
+
const direct = parsed.searchParams.get('channel') ?? parsed.searchParams.get('video') ?? '';
|
|
213
|
+
if (direct)
|
|
214
|
+
return { key: parsed.searchParams.get('video') ? 'video' : 'channel', name: direct };
|
|
215
|
+
if (parts[0] === 'videos' && parts[1])
|
|
216
|
+
return { key: 'video', name: parts[1] };
|
|
217
|
+
if (parts.includes('clip'))
|
|
218
|
+
return none;
|
|
219
|
+
return { key: 'channel', name: parts[0] ?? '' };
|
|
220
|
+
}
|
|
221
|
+
/** A playlist entry from a pasted URL, or null if it is not a URL at all. */
|
|
222
|
+
export function link(value) {
|
|
223
|
+
const url = web(value);
|
|
224
|
+
return url ? { url, provider: provider(url) } : null;
|
|
225
|
+
}
|
|
226
|
+
/** Whether this can actually go on the canvas. `other` never can. */
|
|
227
|
+
export function playable(entry) {
|
|
228
|
+
if (entry.provider === 'youtube')
|
|
229
|
+
return videoId(entry.url) !== '';
|
|
230
|
+
if (entry.provider === 'twitch')
|
|
231
|
+
return channel(entry.url).name !== '';
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
/** Every YouTube player param that keeps the chrome out of frame. */
|
|
235
|
+
const PLAYER = [
|
|
236
|
+
'autoplay=1',
|
|
237
|
+
'mute=1',
|
|
238
|
+
'controls=0',
|
|
239
|
+
'rel=0',
|
|
240
|
+
'playsinline=1',
|
|
241
|
+
'disablekb=1',
|
|
242
|
+
'fs=0',
|
|
243
|
+
'iv_load_policy=3',
|
|
244
|
+
'vq=hd1080',
|
|
245
|
+
// The listening handshake this enables is what lets the canvas wait for a real
|
|
246
|
+
// "playing" report before revealing anything.
|
|
247
|
+
'enablejsapi=1',
|
|
248
|
+
].join('&');
|
|
249
|
+
/**
|
|
250
|
+
* The embed URL for one or more YouTube videos.
|
|
251
|
+
*
|
|
252
|
+
* www.youtube.com, not youtube-nocookie: the nocookie host answers embeds with
|
|
253
|
+
* "video player configuration error" (153) in current Chrome.
|
|
254
|
+
*
|
|
255
|
+
* `loop` needs `playlist` even for a single video — that is YouTube's rule, not
|
|
256
|
+
* ours: a lone video with `loop=1` and no list plays once and stops.
|
|
257
|
+
*/
|
|
258
|
+
export function youtube(ids, loop, origin, start) {
|
|
259
|
+
const [first, ...rest] = ids;
|
|
260
|
+
const params = [PLAYER, `origin=${encodeURIComponent(origin)}`];
|
|
261
|
+
if (loop) {
|
|
262
|
+
params.push('loop=1', `playlist=${(rest.length ? rest : [first]).join(',')}`);
|
|
263
|
+
}
|
|
264
|
+
else if (rest.length) {
|
|
265
|
+
params.push(`playlist=${rest.join(',')}`);
|
|
266
|
+
}
|
|
267
|
+
// Where the video opens, in seconds. Native `start=` on the embed, so it is
|
|
268
|
+
// honoured before the first frame rather than seeked to after.
|
|
269
|
+
if (start && start > 0)
|
|
270
|
+
params.push(`start=${Math.floor(start)}`);
|
|
271
|
+
return `https://www.youtube.com/embed/${first}?${params.join('&')}`;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Optional knobs a brand carries in the SAME video URL, so one configuration
|
|
275
|
+
* value tunes all of them with no rebuild:
|
|
276
|
+
* `start` / `t` seconds to open on
|
|
277
|
+
* `rate` playback speed (YouTube quantises to its allowed steps —
|
|
278
|
+
* 0.25/0.5/0.75/1/…, so 0.42 plays at the nearest, 0.5)
|
|
279
|
+
* `zoom` cover-crop scale, to push a corner watermark off-frame
|
|
280
|
+
* Absent keys return undefined and the caller keeps its own default.
|
|
281
|
+
*/
|
|
282
|
+
export function knobs(value) {
|
|
283
|
+
const url = web(value);
|
|
284
|
+
if (!url)
|
|
285
|
+
return {};
|
|
286
|
+
const q = new URL(url).searchParams;
|
|
287
|
+
const num = (k) => {
|
|
288
|
+
const v = q.get(k);
|
|
289
|
+
if (v == null)
|
|
290
|
+
return undefined;
|
|
291
|
+
const n = Number(v);
|
|
292
|
+
return Number.isFinite(n) ? n : undefined;
|
|
293
|
+
};
|
|
294
|
+
return { start: num('start') ?? num('t'), rate: num('rate'), zoom: num('zoom') };
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* The embed URL for a Twitch channel or VOD. `parent` is mandatory — Twitch
|
|
298
|
+
* refuses to frame for a host it was not told about — and must be the bare
|
|
299
|
+
* hostname, no scheme, no port.
|
|
300
|
+
*/
|
|
301
|
+
export function twitch(value, parent) {
|
|
302
|
+
const { key, name } = channel(value);
|
|
303
|
+
return (`https://player.twitch.tv/?${key}=${encodeURIComponent(name)}` +
|
|
304
|
+
`&parent=${encodeURIComponent(parent)}&muted=true&autoplay=true&controls=false`);
|
|
305
|
+
}
|
|
306
|
+
const MODES = ['off', 'photo', 'video', 'playlist', 'clips'];
|
|
307
|
+
/**
|
|
308
|
+
* Fold an untrusted description of a backdrop onto the current one.
|
|
309
|
+
*
|
|
310
|
+
* This is the ONE gate, and it is deliberately total: anything it cannot make
|
|
311
|
+
* sense of is dropped and the current value kept, so no caller can push a scene
|
|
312
|
+
* into a shape the canvas has to defend against later. Everything a surface
|
|
313
|
+
* takes in comes through here — a settings panel, a card a model can propose,
|
|
314
|
+
* and the read back out of storage on the next load, which matters because bytes
|
|
315
|
+
* in storage are not ours and a rule enforced only on the way in lasts exactly
|
|
316
|
+
* until the page is refreshed.
|
|
317
|
+
*/
|
|
318
|
+
export function merge(current, change, hosts = HOSTS) {
|
|
319
|
+
if (typeof change !== 'object' || change === null)
|
|
320
|
+
return current;
|
|
321
|
+
const next = { ...current };
|
|
322
|
+
const given = change;
|
|
323
|
+
if (MODES.includes(given.mode))
|
|
324
|
+
next.mode = given.mode;
|
|
325
|
+
if (typeof given.loop === 'boolean')
|
|
326
|
+
next.loop = given.loop;
|
|
327
|
+
if (typeof given.sound === 'boolean')
|
|
328
|
+
next.sound = given.sound;
|
|
329
|
+
const photo = media(given.photo, hosts);
|
|
330
|
+
if (photo)
|
|
331
|
+
next.photo = photo;
|
|
332
|
+
if (given.video !== undefined && videoId(String(given.video))) {
|
|
333
|
+
next.video = String(given.video).trim();
|
|
334
|
+
}
|
|
335
|
+
if (Array.isArray(given.playlist)) {
|
|
336
|
+
// The provider is recomputed from the URL every time rather than believed: a
|
|
337
|
+
// caller claiming `provider: 'youtube'` for a netflix.com link must not be
|
|
338
|
+
// able to talk the canvas into framing it.
|
|
339
|
+
next.playlist = given.playlist
|
|
340
|
+
.map((entry) => link(typeof entry === 'string' ? entry : String(entry?.url ?? '')))
|
|
341
|
+
.filter((entry) => entry !== null)
|
|
342
|
+
.slice(0, MOST);
|
|
343
|
+
}
|
|
344
|
+
// Re-validated on the way in AND on the way out, the same as the playlist, so
|
|
345
|
+
// a file the media policy would refuse cannot reach a `<video src>` by having
|
|
346
|
+
// been written to storage under an older rule.
|
|
347
|
+
if (given.clips !== undefined)
|
|
348
|
+
next.clips = files(given.clips, hosts);
|
|
349
|
+
return next;
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* What a brand's single configuration string names.
|
|
353
|
+
*
|
|
354
|
+
* A list of direct video files — comma- or space-separated, or any value naming
|
|
355
|
+
* a media file the surface serves — opens on `clips`; anything else stays the
|
|
356
|
+
* YouTube video it has always been, so an existing single-URL value does not
|
|
357
|
+
* change meaning.
|
|
358
|
+
*
|
|
359
|
+
* Pure, and here rather than in a store, because "what does this value mean" is
|
|
360
|
+
* a rule about data, decidable in a test with no browser.
|
|
361
|
+
*/
|
|
362
|
+
export function scene(value, hosts = HOSTS) {
|
|
363
|
+
const list = files(value, hosts);
|
|
364
|
+
if (list.length > 0)
|
|
365
|
+
return { mode: 'clips', clips: list, video: '' };
|
|
366
|
+
return { mode: 'video', clips: [], video: value };
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* `/background` or `/bg`, and then a space or the end of the line — not merely a
|
|
370
|
+
* word boundary, which `/background-image …` also satisfies. A command is one of
|
|
371
|
+
* exactly two words; anything else is a message and is sent as one.
|
|
372
|
+
*/
|
|
373
|
+
const VERB = /^\/(?:background|bg)(?:\s|$)/i;
|
|
374
|
+
/**
|
|
375
|
+
* Read a `/background` (or `/bg`) line typed into a composer.
|
|
376
|
+
*
|
|
377
|
+
* Answers the scene it asks for, or null when the line is not one of these
|
|
378
|
+
* commands or names something unusable — null meaning "not handled", so the
|
|
379
|
+
* caller sends it on as an ordinary message and the viewer can see for
|
|
380
|
+
* themselves that nothing was applied.
|
|
381
|
+
*
|
|
382
|
+
* /bg off stop painting anything
|
|
383
|
+
* /bg photo <url> a still image
|
|
384
|
+
* /bg video <url> one YouTube video
|
|
385
|
+
* /bg add <url> append to the playlist and switch to it
|
|
386
|
+
* /bg loop on|off repeat at the end
|
|
387
|
+
*
|
|
388
|
+
* There is no `/bg <url>` that guesses: guessing is a second way to say what
|
|
389
|
+
* `photo` and `video` already say exactly, and the guess is wrong precisely when
|
|
390
|
+
* the URL is unusual — which is when being wrong costs the most.
|
|
391
|
+
*/
|
|
392
|
+
export function command(input, current, hosts = HOSTS) {
|
|
393
|
+
const line = input.trim();
|
|
394
|
+
if (!VERB.test(line))
|
|
395
|
+
return null;
|
|
396
|
+
const [, verb = '', ...rest] = line.split(/\s+/);
|
|
397
|
+
const argument = rest.join(' ').trim();
|
|
398
|
+
switch (verb.toLowerCase()) {
|
|
399
|
+
case 'off':
|
|
400
|
+
return { ...current, mode: 'off' };
|
|
401
|
+
case 'photo': {
|
|
402
|
+
const url = media(argument, hosts);
|
|
403
|
+
return url ? { ...current, mode: 'photo', photo: url } : null;
|
|
404
|
+
}
|
|
405
|
+
case 'video':
|
|
406
|
+
return videoId(argument) ? { ...current, mode: 'video', video: argument.trim() } : null;
|
|
407
|
+
case 'add': {
|
|
408
|
+
// Through `merge` so the list has ONE keeper of its length, rather than a
|
|
409
|
+
// second append here that forgets what the first one enforces.
|
|
410
|
+
const entry = link(argument);
|
|
411
|
+
return entry && playable(entry)
|
|
412
|
+
? merge(current, { mode: 'playlist', playlist: [...current.playlist, entry] }, hosts)
|
|
413
|
+
: null;
|
|
414
|
+
}
|
|
415
|
+
case 'loop': {
|
|
416
|
+
const on = argument.toLowerCase();
|
|
417
|
+
return on === 'on' || on === 'off' ? { ...current, loop: on === 'on' } : null;
|
|
418
|
+
}
|
|
419
|
+
default:
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
//# sourceMappingURL=scene.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scene.js","sourceRoot":"","sources":["../../src/backdrop/scene.ts"],"names":[],"mappings":"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"}
|
package/dist/chat/Chat.cjs
CHANGED
|
@@ -28,6 +28,10 @@ const jsx_runtime_1 = require("react/jsx-runtime");
|
|
|
28
28
|
* component reads it, and lifting it would put a keystroke through the surface's
|
|
29
29
|
* state on every letter.
|
|
30
30
|
*
|
|
31
|
+
* That same call now draws a turn whose content is a LIST, through `Parts` — so
|
|
32
|
+
* an image, a tool call, a citation and an artifact reach the screen with
|
|
33
|
+
* nothing added at the call site. They used to be dropped there.
|
|
34
|
+
*
|
|
31
35
|
* No transport, no store, no routing — `@hanzo/ui` stays fetch-free, so nothing
|
|
32
36
|
* here imports `@hanzo/ai`. `messages` is read structurally, which is what lets
|
|
33
37
|
* that hold while the two packages still compose exactly.
|
|
@@ -38,8 +42,26 @@ const ink_1 = require("../backends/gui/ink.cjs");
|
|
|
38
42
|
const slot_1 = require("../backends/gui/slot.cjs");
|
|
39
43
|
const Composer_1 = require("./Composer.cjs");
|
|
40
44
|
const Message_1 = require("./Message.cjs");
|
|
45
|
+
const Parts_1 = require("./Parts.cjs");
|
|
41
46
|
const Thread_1 = require("./Thread.cjs");
|
|
42
47
|
const words_1 = require("./words.cjs");
|
|
48
|
+
/**
|
|
49
|
+
* A turn's content, drawn by what it is.
|
|
50
|
+
*
|
|
51
|
+
* A string is prose and always was. A LIST goes to `Parts`, which is the change:
|
|
52
|
+
* it used to go to `words`, which keeps the text and drops the image, the tool
|
|
53
|
+
* call and the citation — so a surface passing the wire's own turns saw only the
|
|
54
|
+
* prose in them, and had to write its own switch to see the rest.
|
|
55
|
+
*
|
|
56
|
+
* Nothing regresses on the way through. `Parts` joins adjacent prose before it
|
|
57
|
+
* draws (the wire splits a sentence mid-word), and a part whose type it does not
|
|
58
|
+
* know contributes its text, so an all-text turn renders the identical string
|
|
59
|
+
* `words` produced.
|
|
60
|
+
*
|
|
61
|
+
* The cast is the one place the open wire meets the closed union, and it is safe
|
|
62
|
+
* in exactly that direction: `Parts` reads `type` before it reads anything else.
|
|
63
|
+
*/
|
|
64
|
+
const draw = (said) => typeof said === 'string' || said == null ? ((0, ink_1.ink)((0, words_1.words)(said))) : ((0, jsx_runtime_1.jsx)(Parts_1.Parts, { parts: said }));
|
|
43
65
|
function Chat({ messages, streaming = false, send: onSend, stop: onStop, body, empty, composer, ...thread }) {
|
|
44
66
|
const [draft, setDraft] = (0, react_1.useState)('');
|
|
45
67
|
// Clearing here rather than in an effect on `messages` is what keeps a send
|
|
@@ -63,5 +85,5 @@ function Chat({ messages, streaming = false, send: onSend, stop: onStop, body, e
|
|
|
63
85
|
// from the composer lands on a `display: contents` wrapper or on the page,
|
|
64
86
|
// and then reports a component that is fine as broken, or one that is broken
|
|
65
87
|
// as fine.
|
|
66
|
-
return ((0, jsx_runtime_1.jsxs)(gui_1.YStack, { flex: 1, width: "100%", ...(0, slot_1.slot)('chat'), children: [messages.length === 0 && empty ? (empty) : ((0, jsx_runtime_1.jsx)(Thread_1.Thread, { flex: 1, ...thread, children: messages.map((turn, i) => ((0, jsx_runtime_1.jsx)(Message_1.Message, { role: turn.role, busy: streaming && i === last && turn.role === 'assistant', children: body ? body(turn) : (
|
|
88
|
+
return ((0, jsx_runtime_1.jsxs)(gui_1.YStack, { flex: 1, width: "100%", ...(0, slot_1.slot)('chat'), children: [messages.length === 0 && empty ? (empty) : ((0, jsx_runtime_1.jsx)(Thread_1.Thread, { flex: 1, ...thread, children: messages.map((turn, i) => ((0, jsx_runtime_1.jsx)(Message_1.Message, { role: turn.role, busy: streaming && i === last && turn.role === 'assistant', children: body ? body(turn) : draw(turn.content) }, turn.id))) })), (0, jsx_runtime_1.jsx)(Composer_1.Composer, { value: draft, onChange: setDraft, onSend: send, busy: streaming, ...(onStop ? { onStop } : {}), ...composer })] }));
|
|
67
89
|
}
|
package/dist/chat/Chat.d.ts
CHANGED
|
@@ -21,7 +21,7 @@ export interface Turn {
|
|
|
21
21
|
* over: a completion's content is `string | ContentPart[]` (text beside an
|
|
22
22
|
* image), so a turn holding an attachment could not be handed to this at all.
|
|
23
23
|
* Narrowing the wire in a component fed BY the wire just relocates the
|
|
24
|
-
* mismatch to every caller. `words`
|
|
24
|
+
* mismatch to every caller. `words` flattens it to prose; `Parts` draws it.
|
|
25
25
|
*/
|
|
26
26
|
content?: Said;
|
|
27
27
|
}
|
|
@@ -45,12 +45,15 @@ export interface ChatProps extends Omit<ThreadProps, 'children' | 'ref'> {
|
|
|
45
45
|
/** End the turn in flight. Absent, the composer offers no stop. */
|
|
46
46
|
stop?: () => void;
|
|
47
47
|
/**
|
|
48
|
-
* Render a turn's body. Defaults to
|
|
48
|
+
* Render a turn's body. Defaults to `Parts` for a list and the words for a
|
|
49
|
+
* string.
|
|
49
50
|
*
|
|
50
51
|
* A surface with a markdown pipeline passes it here — the plugin set is
|
|
51
52
|
* per-surface (nine plugins, one plugin, or a regex under a bundle ceiling
|
|
52
53
|
* that cannot afford a parser), so this package ships none and renders the
|
|
53
|
-
* words plainly until someone says otherwise.
|
|
54
|
+
* words plainly until someone says otherwise. The usual form hands the
|
|
55
|
+
* pipeline to `Parts` rather than replacing it:
|
|
56
|
+
* `body={(t) => <Parts parts={t.content as MessagePart[]} prose={md} />}`.
|
|
54
57
|
*/
|
|
55
58
|
body?: (turn: Turn) => ReactNode;
|
|
56
59
|
/** Shown instead of the thread while nothing has been said. */
|
package/dist/chat/Chat.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Chat.d.ts","sourceRoot":"","sources":["../../src/chat/Chat.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"Chat.d.ts","sourceRoot":"","sources":["../../src/chat/Chat.tsx"],"names":[],"mappings":"AAoCA,OAAO,EAAyB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAI7D,OAAO,EAAY,KAAK,aAAa,EAAE,MAAM,YAAY,CAAA;AACzD,OAAO,EAAW,KAAK,IAAI,EAAE,MAAM,WAAW,CAAA;AAE9C,OAAO,EAAU,KAAK,WAAW,EAAE,MAAM,UAAU,CAAA;AACnD,OAAO,EAAS,KAAK,IAAI,EAAE,MAAM,SAAS,CAAA;AAE1C;;;;;;;GAOG;AACH,MAAM,WAAW,IAAI;IACnB,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,IAAI,CAAA;IACV;;;;;;;;OAQG;IACH,OAAO,CAAC,EAAE,IAAI,CAAA;CACf;AAED,MAAM,WAAW,SAAU,SAAQ,IAAI,CAAC,WAAW,EAAE,UAAU,GAAG,KAAK,CAAC;IACtE,sCAAsC;IACtC,QAAQ,EAAE,IAAI,EAAE,CAAA;IAChB,iFAAiF;IACjF,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB;;;;;;;;;;OAUG;IACH,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IAC5B,mEAAmE;IACnE,IAAI,CAAC,EAAE,MAAM,IAAI,CAAA;IACjB;;;;;;;;;;OAUG;IACH,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,SAAS,CAAA;IAChC,+DAA+D;IAC/D,KAAK,CAAC,EAAE,SAAS,CAAA;IACjB,iFAAiF;IACjF,QAAQ,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,CAAA;CAClC;AAyBD,wBAAgB,IAAI,CAAC,EACnB,QAAQ,EACR,SAAiB,EACjB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,IAAI,EACJ,KAAK,EACL,QAAQ,EACR,GAAG,MAAM,EACV,EAAE,SAAS,+BAoDX"}
|
package/dist/chat/Chat.js
CHANGED
|
@@ -25,6 +25,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
25
25
|
* component reads it, and lifting it would put a keystroke through the surface's
|
|
26
26
|
* state on every letter.
|
|
27
27
|
*
|
|
28
|
+
* That same call now draws a turn whose content is a LIST, through `Parts` — so
|
|
29
|
+
* an image, a tool call, a citation and an artifact reach the screen with
|
|
30
|
+
* nothing added at the call site. They used to be dropped there.
|
|
31
|
+
*
|
|
28
32
|
* No transport, no store, no routing — `@hanzo/ui` stays fetch-free, so nothing
|
|
29
33
|
* here imports `@hanzo/ai`. `messages` is read structurally, which is what lets
|
|
30
34
|
* that hold while the two packages still compose exactly.
|
|
@@ -35,8 +39,26 @@ import { ink } from '../backends/gui/ink.js';
|
|
|
35
39
|
import { slot } from '../backends/gui/slot.js';
|
|
36
40
|
import { Composer } from './Composer.js';
|
|
37
41
|
import { Message } from './Message.js';
|
|
42
|
+
import { Parts } from './Parts.js';
|
|
38
43
|
import { Thread } from './Thread.js';
|
|
39
44
|
import { words } from './words.js';
|
|
45
|
+
/**
|
|
46
|
+
* A turn's content, drawn by what it is.
|
|
47
|
+
*
|
|
48
|
+
* A string is prose and always was. A LIST goes to `Parts`, which is the change:
|
|
49
|
+
* it used to go to `words`, which keeps the text and drops the image, the tool
|
|
50
|
+
* call and the citation — so a surface passing the wire's own turns saw only the
|
|
51
|
+
* prose in them, and had to write its own switch to see the rest.
|
|
52
|
+
*
|
|
53
|
+
* Nothing regresses on the way through. `Parts` joins adjacent prose before it
|
|
54
|
+
* draws (the wire splits a sentence mid-word), and a part whose type it does not
|
|
55
|
+
* know contributes its text, so an all-text turn renders the identical string
|
|
56
|
+
* `words` produced.
|
|
57
|
+
*
|
|
58
|
+
* The cast is the one place the open wire meets the closed union, and it is safe
|
|
59
|
+
* in exactly that direction: `Parts` reads `type` before it reads anything else.
|
|
60
|
+
*/
|
|
61
|
+
const draw = (said) => typeof said === 'string' || said == null ? (ink(words(said))) : (_jsx(Parts, { parts: said }));
|
|
40
62
|
export function Chat({ messages, streaming = false, send: onSend, stop: onStop, body, empty, composer, ...thread }) {
|
|
41
63
|
const [draft, setDraft] = useState('');
|
|
42
64
|
// Clearing here rather than in an effect on `messages` is what keeps a send
|
|
@@ -60,6 +82,6 @@ export function Chat({ messages, streaming = false, send: onSend, stop: onStop,
|
|
|
60
82
|
// from the composer lands on a `display: contents` wrapper or on the page,
|
|
61
83
|
// and then reports a component that is fine as broken, or one that is broken
|
|
62
84
|
// as fine.
|
|
63
|
-
return (_jsxs(YStack, { flex: 1, width: "100%", ...slot('chat'), children: [messages.length === 0 && empty ? (empty) : (_jsx(Thread, { flex: 1, ...thread, children: messages.map((turn, i) => (_jsx(Message, { role: turn.role, busy: streaming && i === last && turn.role === 'assistant', children: body ? body(turn) :
|
|
85
|
+
return (_jsxs(YStack, { flex: 1, width: "100%", ...slot('chat'), children: [messages.length === 0 && empty ? (empty) : (_jsx(Thread, { flex: 1, ...thread, children: messages.map((turn, i) => (_jsx(Message, { role: turn.role, busy: streaming && i === last && turn.role === 'assistant', children: body ? body(turn) : draw(turn.content) }, turn.id))) })), _jsx(Composer, { value: draft, onChange: setDraft, onSend: send, busy: streaming, ...(onStop ? { onStop } : {}), ...composer })] }));
|
|
64
86
|
}
|
|
65
87
|
//# sourceMappingURL=Chat.js.map
|
package/dist/chat/Chat.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Chat.js","sourceRoot":"","sources":["../../src/chat/Chat.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;AAEZ
|
|
1
|
+
{"version":3,"file":"Chat.js","sourceRoot":"","sources":["../../src/chat/Chat.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
'use client';"use strict";
|
|
2
|
+
'use client';
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.Inspector = Inspector;
|
|
5
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
6
|
+
/**
|
|
7
|
+
* Inspector — what the agent did, beside what it said.
|
|
8
|
+
*
|
|
9
|
+
* The thread carries the answer. This carries the evidence: a summary at the
|
|
10
|
+
* top, then the run's own regions underneath — what it was given, what it
|
|
11
|
+
* produced, what ran on the way. It is the pane a person reads to decide
|
|
12
|
+
* whether to believe the answer, which is why the summary is first and the
|
|
13
|
+
* detail is under it rather than the other way round.
|
|
14
|
+
*
|
|
15
|
+
* It is the CONTENTS of the right rail, not the rail. `Aside` is the column —
|
|
16
|
+
* the border, the width, the fact that it is a column at all — and this goes
|
|
17
|
+
* inside it. Two components because a surface hides the rail at narrow widths
|
|
18
|
+
* by not rendering it, and that decision has nothing to do with what is in it.
|
|
19
|
+
*
|
|
20
|
+
* ONE inspector, whatever the run was. A section carries `facts` (name to
|
|
21
|
+
* value) or `parts` (drawn by `Parts`), and `MessagePart` already covers every
|
|
22
|
+
* shape a run produces: tool calls and their results for a chat turn, files and
|
|
23
|
+
* console output for a coding run, `citation` for a research run's sources, an
|
|
24
|
+
* `artifact` for anything it made. A second panel per kind of run would be four
|
|
25
|
+
* copies of this file disagreeing about spacing.
|
|
26
|
+
*
|
|
27
|
+
* A section with nothing in it does not render, so a caller passes the same
|
|
28
|
+
* list every time and does not write the emptiness checks.
|
|
29
|
+
*/
|
|
30
|
+
const gui_1 = require("@hanzo/gui");
|
|
31
|
+
const slot_1 = require("../backends/gui/slot.cjs");
|
|
32
|
+
const Code_1 = require("./Code.cjs");
|
|
33
|
+
const Parts_1 = require("./Parts.cjs");
|
|
34
|
+
const filled = (section) => (section.facts != null && Object.keys(section.facts).length > 0) ||
|
|
35
|
+
(section.parts != null && section.parts.length > 0);
|
|
36
|
+
function Inspector({ title = 'Run', summary, sections = [], placeholder, prose, onRetry, onOpen, onSource, ...props }) {
|
|
37
|
+
const shown = sections.filter(filled);
|
|
38
|
+
const bare = summary == null && shown.length === 0;
|
|
39
|
+
return ((0, jsx_runtime_1.jsxs)(gui_1.YStack, { ...(0, slot_1.slot)('inspector'), flex: 1, minH: 0, width: "100%", ...props, children: [(0, jsx_runtime_1.jsx)(gui_1.XStack, { items: "center", px: "$1", pb: "$2", shrink: 0, children: (0, jsx_runtime_1.jsx)(gui_1.SizableText, { size: "$1", color: "$soft", fontWeight: "600", children: title }) }), (0, jsx_runtime_1.jsx)(gui_1.ScrollView, { flex: 1, showsVerticalScrollIndicator: false, children: (0, jsx_runtime_1.jsxs)(gui_1.YStack, { gap: "$4", pb: "$4", children: [summary ? ((0, jsx_runtime_1.jsx)(gui_1.YStack, { ...(0, slot_1.slot)('inspector-summary'), p: "$3", gap: "$2", rounded: "$4", borderWidth: 1, borderColor: "$borderColor", bg: "$panel", children: (0, jsx_runtime_1.jsx)(gui_1.SizableText, { size: "$2", color: "$ink", children: summary }) })) : null, shown.map((section) => ((0, jsx_runtime_1.jsxs)(gui_1.YStack, { ...(0, slot_1.slot)('inspector-section'), gap: "$2", children: [(0, jsx_runtime_1.jsx)(gui_1.SizableText, { size: "$1", color: "$quiet", fontWeight: "600", children: section.name }), section.facts ? (0, jsx_runtime_1.jsx)(Table, { facts: section.facts }) : null, section.parts?.length ? ((0, jsx_runtime_1.jsx)(Parts_1.Parts, { parts: section.parts, prose: prose, onRetry: onRetry, onOpen: onOpen, onSource: onSource })) : null] }, section.name))), bare && placeholder != null ? ((0, jsx_runtime_1.jsx)(gui_1.SizableText, { size: "$2", color: "$soft", children: placeholder })) : null] }) })] }));
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* A record, as rows.
|
|
43
|
+
*
|
|
44
|
+
* A string is its own presentation; anything else goes through `Code`, because
|
|
45
|
+
* a nested object flattened into a line is unreadable and `show` already
|
|
46
|
+
* serialises one safely. That is the only rule here, and it is why this is a
|
|
47
|
+
* dozen lines rather than a formatting library.
|
|
48
|
+
*/
|
|
49
|
+
function Table({ facts }) {
|
|
50
|
+
return ((0, jsx_runtime_1.jsx)(gui_1.YStack, { ...(0, slot_1.slot)('inspector-facts'), rounded: "$4", borderWidth: 1, borderColor: "$borderColor", overflow: "hidden", children: Object.entries(facts).map(([name, value], i) => ((0, jsx_runtime_1.jsxs)(gui_1.YStack, { gap: "$1.5", px: "$3", py: "$2", borderTopWidth: i === 0 ? 0 : 1, borderColor: "$borderColor", children: [(0, jsx_runtime_1.jsx)(gui_1.SizableText, { size: "$1", color: "$quiet", children: name }), typeof value === 'string' ? ((0, jsx_runtime_1.jsx)(gui_1.SizableText, { size: "$2", color: "$ink", children: value })) : ((0, jsx_runtime_1.jsx)(Code_1.Code, { language: "json", children: (0, Parts_1.show)(value) }))] }, name))) }));
|
|
51
|
+
}
|