@hanzo/rooms 0.1.78 → 0.1.80

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.
Files changed (2) hide show
  1. package/dist/Chat.js +84 -73
  2. package/package.json +3 -3
package/dist/Chat.js CHANGED
@@ -63,7 +63,7 @@ import { Share } from "./Share.js";
63
63
  import { ConversationMenu, ThreadMore, useThreadList } from "./thread.js";
64
64
  import { Schedule } from "./Schedule.js";
65
65
  import { keepFile, useAttach, useKeep } from "./keep.js";
66
- import { artifacts, lead, nameOf, PAGE, Panel, PanelToggle, prompt, renders, Reply, Served, Summary, useDeck, useKey, Suggestions, useCarry, useLanguage, useMind, useSuggestions, usePanel, frames, name as tabName, } from "@hanzo/build";
66
+ import { artifacts, lead, nameOf, PAGE, Panel, PanelToggle, prompt, renders, Reply, Served, Summary, useDeck, useKey, Suggestions, useCarry, useLanguage, useMention, useMind, useSuggestions, usePanel, frames, name as tabName, } from "@hanzo/build";
67
67
  import { checkoutUrl } from './lib/pay.js';
68
68
  import { Upgrade, useFree } from './Upgrade.js';
69
69
  import { enter, LOGIN } from './lib/destination.js';
@@ -757,8 +757,14 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
757
757
  if (kept && kept !== open) {
758
758
  written.current.add(kept);
759
759
  reopen(kept);
760
+ // THE SERVER TITLES A THREAD FROM ITS FIRST TURN, block and all: a turn
761
+ // that carried files or a plugin's words is renamed to the words alone.
762
+ const first = chat.messages.find((m) => m.role === "user");
763
+ const read = first ? told(words(first.content)) : null;
764
+ if (read?.carried)
765
+ void ai.threads.update(kept, { title: read.text.trim().slice(0, 80) || "Files" }).catch(() => { });
760
766
  }
761
- }, [kept, open, reopen]);
767
+ }, [kept, open, reopen]); // eslint-disable-line react-hooks/exhaustive-deps
762
768
  // The thread's turns, replaced wholesale or through an updater. `set` is the
763
769
  // hook's own state setter, so it takes either.
764
770
  const apply = useRef(set);
@@ -1181,7 +1187,8 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
1181
1187
  // The files held for this message, as bytes, for a plugin that reads them (PDF).
1182
1188
  const bytes = await Promise.all(files.filter((h) => h.href).map(async (h) => new File([await (await fetch(h.href)).blob()], h.name, { type: h.type })));
1183
1189
  const got = await carry.prep({ text, files: bytes, thread: open ?? "" }, new AbortController().signal);
1184
- if (got.reply && !got.system)
1190
+ // An answer of a plugin's own (a deck) is the answer, whatever else was prepared.
1191
+ if (got.reply)
1185
1192
  return answered(text, got.reply);
1186
1193
  deliver(text, got.system ?? "");
1187
1194
  }
@@ -1341,6 +1348,79 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
1341
1348
  window.location.assign(dev(slug));
1342
1349
  },
1343
1350
  };
1351
+ // THE COMPOSER'S OPTIONS, held apart so its keys (the `/` and `@` lists' arrows,
1352
+ // Ctrl+U) hook in through `useMention` before the frame is drawn.
1353
+ const composing = {
1354
+ value: draft,
1355
+ onChange: setDraft,
1356
+ onSend: submit,
1357
+ onStop: chat.stop,
1358
+ busy: chat.streaming,
1359
+ // Files alone are a message: what is held goes with an empty draft too.
1360
+ ready: Boolean(draft.trim() || files.length),
1361
+ // `label` keeps the accessible name constant; the placeholder is the composer's own.
1362
+ label: ASK,
1363
+ note: caution(),
1364
+ // A phone's pill is the tall one: the field across the top, the controls under it.
1365
+ narrow: !media.sm,
1366
+ // In the app's frame the composer floats on the pane as a pane of its own.
1367
+ framed,
1368
+ // A file or an image pasted into the field is held like a picked one;
1369
+ // words paste as words.
1370
+ field: {
1371
+ onPaste: (e) => {
1372
+ if (!e.clipboardData?.files?.length)
1373
+ return;
1374
+ e.preventDefault();
1375
+ void hold(at, Array.from(e.clipboardData.files), via);
1376
+ },
1377
+ },
1378
+ // WHAT GOES WITH THE MESSAGE, above the frame and at its width: the
1379
+ // pause and its ways past, one chip a file, and talk mode's words.
1380
+ head: files.length || turnedAway || prepWrong || preparing || talk.open || talk.refusal || pause ? (_jsxs(_Fragment, { children: [banner ? (_jsx(LimitedBanner, { message: banner.message, actions: banner.actions, onAction: act, onUsage: () => showSettings("usage") })) : null, actWrong ? (_jsx(Text, { role: "alert", "data-slot": "limited-banner-wrong", fontSize: "$2", color: BAD, children: actWrong })) : 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, onOpen: openFile, onRemove: () => drop(at, one.id), onSave: anonymous ? undefined : (url, id) => keepOne(url, id, one.name, one.type) }, one.id))) })) : null, preparing ? (_jsxs(Text, { role: "status", "aria-live": "polite", "data-slot": "composer-preparing", fontSize: "$2", color: "$soft", children: [preparing, "\u2026"] })) : null, turnedAway || prepWrong ? (_jsx(Text, { role: "alert", fontSize: "$2", color: BAD, children: turnedAway || prepWrong })) : 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
1381
+ ? worded(talk.refusal)
1382
+ : talk.reply
1383
+ ? `Hanzo: ${talk.reply}`
1384
+ : talk.state === "listening"
1385
+ ? "Listening — speak, and Hanzo answers aloud."
1386
+ : "Connecting…" })) : null] })) : undefined,
1387
+ // THE `+`: files (the Sources tab's "Attach files", one press closer),
1388
+ // who is in the conversation, the skills, and the two switches.
1389
+ plus: anonymous
1390
+ ? { upload: () => take(at, via) }
1391
+ : {
1392
+ upload: () => take(at, via),
1393
+ agents: agentShelf,
1394
+ skills: carry.skills,
1395
+ plugins: carry.plugins,
1396
+ research: { on: carry.gear.gear.research, set: (on) => carry.gear.set({ research: on }) },
1397
+ memory: { on: carry.gear.gear.memory, set: (on) => carry.gear.set({ memory: on }) },
1398
+ },
1399
+ // Who is in the room, then what the conversation carries, as chips after `+`.
1400
+ on: [
1401
+ ...room.map((name) => ({
1402
+ id: `agent:${name}`,
1403
+ label: name,
1404
+ icon: _jsx(Face, { src: faces[name]?.avatar, emoji: faces[name]?.emoji, name: name, size: 16 }),
1405
+ onRemove: () => seat(room.filter((one) => one !== name)),
1406
+ })),
1407
+ ...(anonymous ? [] : carry.on),
1408
+ ],
1409
+ slash: anonymous ? undefined : carry.skills,
1410
+ at: anonymous ? undefined : carry.plugins,
1411
+ mode: anonymous ? null : { value: carry.gear.gear.mode, set: (m) => carry.gear.set({ mode: m }) },
1412
+ // The model, the effort and the mode under the frame (@hanzo/build).
1413
+ mind,
1414
+ limits,
1415
+ onUpgrade: (id) => setAsk({ model: nameOf(id, listed) }),
1416
+ voice,
1417
+ language: spoken,
1418
+ onLanguage: speakIn,
1419
+ talk,
1420
+ // While nothing has been said: what is worth doing next.
1421
+ below: said.length === 0 && !anonymous ? _jsx(Suggestions, { rows: hints.rows, onPick: setDraft, onDismiss: hints.dismiss }) : undefined,
1422
+ };
1423
+ const mention = useMention(composing);
1344
1424
  return (_jsxs(YStack, { flex: 1, minH: 0, width: "100%", position: "relative", "data-slot": "chat-room",
1345
1425
  // A LINK IN AN ANSWER OPENS IN A TAB beside the conversation when the tab
1346
1426
  // can show it — a site or preview we host — and in a browser tab of its
@@ -1450,76 +1530,7 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
1450
1530
  empty: heading === null ? undefined : (_jsxs(YStack, { "data-slot": "opening", items: "center", width: "100%", py: "$6", ...(media.sm ? null : { flex: 1, justify: "center" }), children: [room.length > 1 ? (_jsx(XStack, { items: "center", justify: "center", mb: "$4", children: room.map((name, i) => (_jsx(Box, { ml: i ? -16 : 0, rounded: 40, borderWidth: 2, borderColor: "$background", z: room.length - i, children: _jsx(Face, { src: faces[name]?.avatar, emoji: faces[name]?.emoji, name: name, size: 72 }) }, name))) })) : portrait ? (_jsx(Box, { width: 160, height: 160, rounded: 24, overflow: "hidden", borderWidth: 2, borderColor: "var(--white-15)", boxShadow: "0 10px 24px color-mix(in srgb, var(--pure-black) 70%, transparent)", mb: "$4", bg: "var(--neutral-900)", children: _jsx(View, { render: _jsx("img", { src: portrait, alt: withWhom }), width: "100%", height: "100%", objectFit: "cover" }) })) : null, room.length === 1 && !portrait ? (_jsx(Box, { mb: "$3", children: _jsx(Face, { emoji: mate?.emoji ?? persona?.emoji, name: withWhom, size: 72 }) })) : null, _jsx(Text, { render: "h1", fontSize: "$7", lineHeight: "$7", fontWeight: "700", color: "$ink", text: "center", mb: "$1", children: join(room) || heading || "What can I help with?" }), room.length === 1 && (mate?.description ?? persona?.description) ? (_jsx(Text, { fontSize: "$3", color: "$soft", text: "center", maxW: 500, mb: "$4", lineHeight: "$4", children: mate?.description ?? persona?.description })) : null] })),
1451
1531
  // The transcript reads at the same measure. `Thread` defaults to 768
1452
1532
  // itself; handing it the column here is what lets Width reach it.
1453
- column: { maxW: MEASURE }, composer: prompt({
1454
- value: draft,
1455
- onChange: setDraft,
1456
- onSend: submit,
1457
- onStop: chat.stop,
1458
- busy: chat.streaming,
1459
- // Files alone are a message: what is held goes with an empty draft too.
1460
- ready: Boolean(draft.trim() || files.length),
1461
- // `label` keeps the accessible name constant; the placeholder is the composer's own.
1462
- label: ASK,
1463
- note: caution(),
1464
- // A phone's pill is the tall one: the field across the top, the controls under it.
1465
- narrow: !media.sm,
1466
- // In the app's frame the composer floats on the pane as a pane of its own.
1467
- framed,
1468
- // A file or an image pasted into the field is held like a picked one;
1469
- // words paste as words.
1470
- field: {
1471
- onPaste: (e) => {
1472
- if (!e.clipboardData?.files?.length)
1473
- return;
1474
- e.preventDefault();
1475
- void hold(at, Array.from(e.clipboardData.files), via);
1476
- },
1477
- },
1478
- // WHAT GOES WITH THE MESSAGE, above the frame and at its width: the
1479
- // pause and its ways past, one chip a file, and talk mode's words.
1480
- head: files.length || turnedAway || prepWrong || preparing || talk.open || talk.refusal || pause ? (_jsxs(_Fragment, { children: [banner ? (_jsx(LimitedBanner, { message: banner.message, actions: banner.actions, onAction: act, onUsage: () => showSettings("usage") })) : null, actWrong ? (_jsx(Text, { role: "alert", "data-slot": "limited-banner-wrong", fontSize: "$2", color: BAD, children: actWrong })) : 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, onOpen: openFile, onRemove: () => drop(at, one.id), onSave: anonymous ? undefined : (url, id) => keepOne(url, id, one.name, one.type) }, one.id))) })) : null, preparing ? (_jsxs(Text, { role: "status", "aria-live": "polite", "data-slot": "composer-preparing", fontSize: "$2", color: "$soft", children: [preparing, "\u2026"] })) : null, turnedAway || prepWrong ? (_jsx(Text, { role: "alert", fontSize: "$2", color: BAD, children: turnedAway || prepWrong })) : 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
1481
- ? worded(talk.refusal)
1482
- : talk.reply
1483
- ? `Hanzo: ${talk.reply}`
1484
- : talk.state === "listening"
1485
- ? "Listening — speak, and Hanzo answers aloud."
1486
- : "Connecting…" })) : null] })) : undefined,
1487
- // THE `+`: files (the Sources tab's "Attach files", one press closer),
1488
- // who is in the conversation, the skills, and the two switches.
1489
- plus: anonymous
1490
- ? { upload: () => take(at, via) }
1491
- : {
1492
- upload: () => take(at, via),
1493
- agents: agentShelf,
1494
- skills: carry.skills,
1495
- plugins: carry.plugins,
1496
- research: { on: carry.gear.gear.research, set: (on) => carry.gear.set({ research: on }) },
1497
- memory: { on: carry.gear.gear.memory, set: (on) => carry.gear.set({ memory: on }) },
1498
- },
1499
- // Who is in the room, then what the conversation carries, as chips after `+`.
1500
- on: [
1501
- ...room.map((name) => ({
1502
- id: `agent:${name}`,
1503
- label: name,
1504
- icon: _jsx(Face, { src: faces[name]?.avatar, emoji: faces[name]?.emoji, name: name, size: 16 }),
1505
- onRemove: () => seat(room.filter((one) => one !== name)),
1506
- })),
1507
- ...(anonymous ? [] : carry.on),
1508
- ],
1509
- slash: anonymous ? undefined : carry.skills,
1510
- at: anonymous ? undefined : carry.plugins,
1511
- mode: anonymous ? null : { value: carry.gear.gear.mode, set: (m) => carry.gear.set({ mode: m }) },
1512
- // The model, the effort and the mode under the frame (@hanzo/build).
1513
- mind,
1514
- limits,
1515
- onUpgrade: (id) => setAsk({ model: nameOf(id, listed) }),
1516
- voice,
1517
- language: spoken,
1518
- onLanguage: speakIn,
1519
- talk,
1520
- // While nothing has been said: what is worth doing next.
1521
- below: said.length === 0 && !anonymous ? _jsx(Suggestions, { rows: hints.rows, onPick: setDraft, onDismiss: hints.dismiss }) : undefined,
1522
- }) }), _jsx(AgentMaker, {}), carry.dialog, isFree(model) ? (_jsx(XStack, { items: "center", justify: "center", py: "$1", px: "$3", borderTopWidth: 1, borderColor: "$borderColor", bg: "$background", children: _jsxs(Text, { fontSize: "$1", color: "$soft", children: ["Free chat is data-shared to train models. ", _jsx(Anchor, { href: site("/privacy"), fontSize: "$1", color: "$ink", children: "Privacy" })] }) })) : null, consent, _jsx(Upgrade, { ask: ask, onClose: () => setAsk(null) })] }));
1533
+ column: { maxW: MEASURE }, composer: prompt({ ...composing, field: mention.field }, mention.state) }), _jsx(AgentMaker, {}), carry.dialog, isFree(model) ? (_jsx(XStack, { items: "center", justify: "center", py: "$1", px: "$3", borderTopWidth: 1, borderColor: "$borderColor", bg: "$background", children: _jsxs(Text, { fontSize: "$1", color: "$soft", children: ["Free chat is data-shared to train models. ", _jsx(Anchor, { href: site("/privacy"), fontSize: "$1", color: "$ink", children: "Privacy" })] }) })) : null, consent, _jsx(Upgrade, { ask: ask, onClose: () => setAsk(null) })] }));
1523
1534
  }
1524
1535
  /** The agreement free asks for, in @hanzo/ai's words so every surface says one thing. */
1525
1536
  function Consent({ question, onAgree, onCancel, }) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzo/rooms",
3
- "version": "0.1.78",
3
+ "version": "0.1.80",
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",
@@ -49,7 +49,7 @@
49
49
  "peerDependencies": {
50
50
  "@hanzo/ai": ">=0.6.23",
51
51
  "@hanzo/appearance": ">=0.2.6",
52
- "@hanzo/build": ">=0.2.65",
52
+ "@hanzo/build": ">=0.2.67",
53
53
  "@hanzo/composer": ">=0.2.0",
54
54
  "@hanzo/event": ">=0.3.56",
55
55
  "@hanzo/gui": "^8.3.5",
@@ -67,7 +67,7 @@
67
67
  "devDependencies": {
68
68
  "@hanzo/ai": "0.6.23",
69
69
  "@hanzo/appearance": "0.2.8",
70
- "@hanzo/build": "0.2.65",
70
+ "@hanzo/build": "0.2.67",
71
71
  "@hanzo/composer": "0.2.0",
72
72
  "@hanzo/design": "0.5.34",
73
73
  "@hanzo/event": "0.3.56",