@hanzo/rooms 0.1.69 → 0.1.71
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 +81 -17
- package/dist/Schedule.d.ts +14 -0
- package/dist/Schedule.js +107 -0
- package/dist/keep.d.ts +40 -0
- package/dist/keep.js +148 -0
- package/dist/lib/auto.d.ts +35 -0
- package/dist/lib/auto.js +62 -0
- package/dist/lib/library.d.ts +72 -0
- package/dist/lib/library.js +83 -0
- package/dist/lib/session.js +15 -5
- package/dist/thread.d.ts +5 -2
- package/dist/thread.js +6 -3
- package/dist/where.d.ts +6 -0
- package/package.json +1 -1
package/dist/Chat.js
CHANGED
|
@@ -54,13 +54,15 @@ import { useHydrated } from "./lib/hydrated.js";
|
|
|
54
54
|
import { base, served } from "./lib/ai.js";
|
|
55
55
|
import { onServed } from "./lib/served.js";
|
|
56
56
|
import { refused as worded, speech, useDictation, useTalk, useVoice } from "@hanzo/voice";
|
|
57
|
-
import { FileText, Image as ImageMark, Paperclip, X } from "lucide-react";
|
|
57
|
+
import { Check, Clock, FileText, Image as ImageMark, Library as LibraryMark, Paperclip, X } from "lucide-react";
|
|
58
58
|
import { FREE } from "./lib/ai.js";
|
|
59
59
|
import { openThread, useOpen } from "./open.js";
|
|
60
60
|
import { Beside, Framed } from "./Shell.js";
|
|
61
61
|
import { SEED, look, useSections, useSites } from "./RightPane.js";
|
|
62
62
|
import { Share } from "./Share.js";
|
|
63
63
|
import { ConversationMenu, ThreadMore, useThreadList } from "./thread.js";
|
|
64
|
+
import { Schedule } from "./Schedule.js";
|
|
65
|
+
import { keepFile, useAttach, useKeep } from "./keep.js";
|
|
64
66
|
import { artifacts, lead, nameOf, PAGE, Panel, PanelToggle, prompt, renders, Reply, Served, Summary, useDeck, useKey, useMind, usePanel, frames, name as tabName, } from "@hanzo/build";
|
|
65
67
|
import { checkoutUrl } from './lib/pay.js';
|
|
66
68
|
import { Upgrade, useFree } from './Upgrade.js';
|
|
@@ -415,7 +417,7 @@ const THUMB = 48;
|
|
|
415
417
|
*
|
|
416
418
|
* Pressing one opens it beside the conversation (`onOpen`).
|
|
417
419
|
*/
|
|
418
|
-
function Chip({ name, type, size, state, sent, file, picture, onOpen, onRemove, }) {
|
|
420
|
+
function Chip({ name, type, size, state, sent, file, picture, onOpen, onRemove, onSave, }) {
|
|
419
421
|
const kind = kindOf(name, type);
|
|
420
422
|
const bad = state === "failed";
|
|
421
423
|
const why = state === "failed" || state === "stored" ? file?.error : file?.note;
|
|
@@ -423,7 +425,7 @@ function Chip({ name, type, size, state, sent, file, picture, onOpen, onRemove,
|
|
|
423
425
|
const [broken, setBroken] = useState("");
|
|
424
426
|
const word = state === "ready" ? (file?.clipped ? "in part" : "") : progress(state, sent, file);
|
|
425
427
|
const meta = [kind === "pdf" ? "PDF" : "", size > 0 ? weigh(size) : "", word].filter(Boolean).join(" \u00b7 ");
|
|
426
|
-
const remove = onRemove ? (_jsx(Box, { render: "button", "aria-label": `Remove ${name}`, onClick: onRemove, p: "$1", rounded: "$10", shrink: 0, hoverStyle: { bg: "$hover" }, children: _jsx(X, { size: 12, "aria-hidden": true }) })) : null;
|
|
428
|
+
const remove = (_jsxs(_Fragment, { children: [onSave ? _jsx(Keep, { name: name, onSave: onSave }) : null, onRemove ? (_jsx(Box, { render: "button", "aria-label": `Remove ${name}`, onClick: onRemove, p: "$1", rounded: "$10", shrink: 0, hoverStyle: { bg: "$hover" }, children: _jsx(X, { size: 12, "aria-hidden": true }) })) : null] }));
|
|
427
429
|
const marks = {
|
|
428
430
|
role: "listitem",
|
|
429
431
|
"data-slot": "file-chip",
|
|
@@ -438,26 +440,57 @@ function Chip({ name, type, size, state, sent, file, picture, onOpen, onRemove,
|
|
|
438
440
|
const Glyph = kind === "pdf" ? FileText : ImageMark;
|
|
439
441
|
return (_jsxs(XStack, { ...marks, items: "center", gap: "$1", p: "$1", pr: onRemove ? "$1" : "$2.5", rounded: "$4", borderWidth: 1, borderColor: "$borderColor", maxW: 280, children: [_jsxs(XStack, { render: "button", "aria-label": `Open ${name}`, onClick: onOpen, items: "center", gap: "$2", minW: 0, cursor: "pointer", children: [_jsx(YStack, { "data-slot": "file-thumb", width: THUMB, height: THUMB, shrink: 0, rounded: "$3", overflow: "hidden", items: "center", justify: "center", gap: "$0.5", borderWidth: drawn ? 0 : 1, borderColor: "$borderColor", children: drawn ? (_jsx(View, { render: _jsx("img", { src: picture, alt: "", loading: "lazy", decoding: "async", onError: () => setBroken(picture) }), width: "100%", height: "100%", objectFit: "cover", display: "block" })) : (_jsxs(_Fragment, { children: [_jsx(Glyph, { size: 18, "aria-hidden": true, color: "var(--muted-foreground)" }), kind === "pdf" ? (_jsx(Text, { fontSize: 9, fontWeight: "600", letterSpacing: 0.5, color: "$soft", children: "PDF" })) : null] })) }), _jsxs(YStack, { minW: 0, gap: "$0.5", children: [_jsx(Text, { fontSize: "$2", color: "$ink", numberOfLines: 1, text: "left", children: name }), meta ? (_jsx(Text, { fontSize: "$1", color: bad ? BAD : "$soft", numberOfLines: 1, text: "left", children: meta })) : null] })] }), remove] }));
|
|
440
442
|
}
|
|
443
|
+
/**
|
|
444
|
+
* "Save to Library" on a file the reader put into the conversation: pressed, it
|
|
445
|
+
* keeps the file in their Library and says so; a refusal is its title.
|
|
446
|
+
*/
|
|
447
|
+
function Keep({ name, onSave }) {
|
|
448
|
+
const [state, setState] = useState("");
|
|
449
|
+
const [wrong, setWrong] = useState("");
|
|
450
|
+
const label = state === "saved" ? `${name} is in your Library` : wrong || `Save ${name} to Library`;
|
|
451
|
+
return (_jsx(Box, { render: "button", "aria-label": label, ...{ title: label }, "data-slot": "keep", "data-state": state || (wrong ? "failed" : ""), "aria-disabled": state !== "", onClick: () => {
|
|
452
|
+
if (state)
|
|
453
|
+
return;
|
|
454
|
+
setState("saving");
|
|
455
|
+
setWrong("");
|
|
456
|
+
onSave().then(() => setState("saved"), (e) => {
|
|
457
|
+
setState("");
|
|
458
|
+
setWrong(e?.message || `${name} was not saved.`);
|
|
459
|
+
});
|
|
460
|
+
}, p: "$1", rounded: "$10", shrink: 0, opacity: state === "saving" ? 0.5 : 1, hoverStyle: { bg: "$hover" }, children: state === "saved" ? _jsx(Check, { size: 12, "aria-hidden": true }) : _jsx(LibraryMark, { size: 12, "aria-hidden": true, color: wrong ? BAD : undefined }) }));
|
|
461
|
+
}
|
|
441
462
|
/** A file held for the next message: uploading, then indexing, then ready. */
|
|
442
|
-
function HeldChip({ one, onOpen, onRemove }) {
|
|
463
|
+
function HeldChip({ one, onOpen, onRemove, onSave }) {
|
|
443
464
|
const live = useWorkFile(one.file?.id);
|
|
444
465
|
const file = live ?? one.file;
|
|
445
466
|
const state = one.state === "uploading" ? one.state : (file?.status ?? one.state);
|
|
446
467
|
const picture = usePicture(kindOf(one.name, one.type) === "image", one.href, file?.id, file);
|
|
447
468
|
// The mark the column's Sources row opens for the same file, so both open it alike.
|
|
448
469
|
const mark = { id: one.id, name: one.name, kind: kindOf(one.name, one.type) === "image" ? "image" : "file", href: one.href || undefined, file: file?.id, mime: one.type };
|
|
449
|
-
return (_jsx(Chip, { name: one.name, type: one.type, size: one.size, state: state, sent: one.sent, file: file ?? (one.error ? { error: one.error } : undefined), picture: picture, onOpen: () => onOpen(mark), onRemove: onRemove
|
|
470
|
+
return (_jsx(Chip, { name: one.name, type: one.type, size: one.size, state: state, sent: one.sent, file: file ?? (one.error ? { error: one.error } : undefined), picture: picture, onOpen: () => onOpen(mark), onRemove: onRemove,
|
|
471
|
+
// Its bytes are on this machine until the upload lands, and are kept by the workspace file it became.
|
|
472
|
+
onSave: onSave && one.href ? () => onSave(one.href, file?.id ?? one.id) : undefined }));
|
|
450
473
|
}
|
|
451
474
|
/** A file a sent turn carried, as the index says of it now. */
|
|
452
|
-
function TurnChip({ f, onOpen }) {
|
|
475
|
+
function TurnChip({ f, onOpen, onSave }) {
|
|
476
|
+
const api = useAi();
|
|
453
477
|
const live = useWorkFile(f.id);
|
|
454
478
|
const picture = usePicture(kindOf(f.name, f.type) === "image", "", f.id, live);
|
|
455
479
|
const mark = { id: f.id, name: f.name, kind: kindOf(f.name, f.type) === "image" ? "image" : "file", file: f.id, mime: f.type };
|
|
456
|
-
|
|
480
|
+
// The bytes on this machine where this document still holds them, else the workspace's copy.
|
|
481
|
+
const save = onSave ? async () => onSave(localOf(f.id) || (await signedOf(api, f.id, live)), f.id) : undefined;
|
|
482
|
+
return _jsx(Chip, { name: f.name, type: f.type, size: f.size, state: live?.status ?? "ready", sent: 1, file: live, picture: picture, onOpen: () => onOpen(mark), onSave: save });
|
|
457
483
|
}
|
|
458
484
|
/** A question that carried files: its words, the files put into it, and those read again. */
|
|
459
|
-
function Carried({ text, attached, reused, onOpen }) {
|
|
460
|
-
return (_jsxs(YStack, { gap: "$2", "data-slot": "turn-files", children: [text ? _jsx(Prose, { text: text }) : null, attached.length ? (_jsx(XStack, { gap: "$1.5", flexWrap: "wrap", role: "list", "aria-label": "Files in this message", children: attached.map((f) => (_jsx(TurnChip, { f: f, onOpen: onOpen }, f.id))) })) : null, reused.length ? (_jsxs(Text, { fontSize: "$1", color: "$soft", children: ["Read again: ", reused.map((f) => `${f.name} (${weigh(f.size)})`).join(", ")] })) : null] }));
|
|
485
|
+
function Carried({ text, attached, reused, onOpen, onSave, }) {
|
|
486
|
+
return (_jsxs(YStack, { gap: "$2", "data-slot": "turn-files", children: [text ? _jsx(Prose, { text: text }) : null, attached.length ? (_jsx(XStack, { gap: "$1.5", flexWrap: "wrap", role: "list", "aria-label": "Files in this message", children: attached.map((f) => (_jsx(TurnChip, { f: f, onOpen: onOpen, onSave: onSave ? (url, id) => onSave(url, id, f) : undefined }, f.id))) })) : null, reused.length ? (_jsxs(Text, { fontSize: "$1", color: "$soft", children: ["Read again: ", reused.map((f) => `${f.name} (${weigh(f.size)})`).join(", ")] })) : null] }));
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* "Schedule this" under a question: the quiet control that opens the dialog
|
|
490
|
+
* which makes the question run itself (Schedule.tsx).
|
|
491
|
+
*/
|
|
492
|
+
function Plan({ onPress }) {
|
|
493
|
+
return (_jsx(XStack, { justify: "flex-end", "data-slot": "plan", children: _jsxs(XStack, { render: "button", "aria-label": "Schedule this", onPress: onPress, items: "center", gap: "$1", px: "$1", height: 22, rounded: "$2", cursor: "pointer", opacity: 0.55, hoverStyle: { opacity: 1, bg: "$hover" }, focusStyle: { opacity: 1 }, children: [_jsx(Clock, { size: 12, "aria-hidden": true }), _jsx(Text, { fontSize: "$1", color: "$ink", children: "Schedule this" })] }) }));
|
|
461
494
|
}
|
|
462
495
|
/**
|
|
463
496
|
* The conversation. One component for a visitor and for an account.
|
|
@@ -827,6 +860,12 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
|
|
|
827
860
|
// read from it for the question.
|
|
828
861
|
const { held: files, refused: turnedAway } = usePane(at);
|
|
829
862
|
const via = useMemo(() => ({ api: ai, org: anonymous ? null : org() }), [ai, anonymous]);
|
|
863
|
+
// "Use in Chat" from the Library: `?attach=<item>` holds it for the next message.
|
|
864
|
+
useAttach(at, via);
|
|
865
|
+
// "Schedule this": the prompt the dialog opens on, or null while it is shut.
|
|
866
|
+
const [plan, setPlan] = useState(null);
|
|
867
|
+
// A file the reader keeps from this conversation goes to their Library under it.
|
|
868
|
+
const keepOne = useCallback((url, id, name, type) => keepFile(ai, { id, name, type, url, thread: kept ?? open }), [ai, kept, open]);
|
|
830
869
|
// What the send is waiting on — uploads landing, the files being read — said
|
|
831
870
|
// over the field, and the reason a second press does nothing meanwhile.
|
|
832
871
|
const [preparing, setPreparing] = useState(null);
|
|
@@ -885,6 +924,16 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
|
|
|
885
924
|
streaming.current = chat.streaming;
|
|
886
925
|
const turns = useRef(chat.messages);
|
|
887
926
|
turns.current = chat.messages;
|
|
927
|
+
// The conversation's last question as its person wrote it: what the header's
|
|
928
|
+
// Schedule… opens on.
|
|
929
|
+
const lastAsked = () => {
|
|
930
|
+
const q = [...turns.current].reverse().find((m) => m.role === "user");
|
|
931
|
+
if (!q)
|
|
932
|
+
return "";
|
|
933
|
+
if (typeof q.content === "string")
|
|
934
|
+
return told(q.content).text;
|
|
935
|
+
return Array.isArray(q.content) ? partsOf(q.content).flatMap((p) => (p.type === "text" ? [p.text] : [])).join("\n") : "";
|
|
936
|
+
};
|
|
888
937
|
/**
|
|
889
938
|
* WHAT EACH UNANSWERED QUESTION WAS TOLD, by the question's own turn id: the
|
|
890
939
|
* refusal it met, or that its answer came back empty, or that it streamed
|
|
@@ -1020,6 +1069,10 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
|
|
|
1020
1069
|
const a = lead(artifacts(text));
|
|
1021
1070
|
return a ? () => see(a) : undefined;
|
|
1022
1071
|
};
|
|
1072
|
+
// WHAT AN ANSWER MAKES IS KEPT IN THE LIBRARY as it finishes (keep.ts): the
|
|
1073
|
+
// answer that just stopped, once the conversation it belongs to has an id.
|
|
1074
|
+
const [finished, setFinished] = useState(null);
|
|
1075
|
+
useKeep(ai, !anonymous, kept ?? open, finished, artifacts);
|
|
1023
1076
|
// WHAT AN ANSWER MAKES OPENS BESIDE IT. With the panel open, a page or a
|
|
1024
1077
|
// picture an answer just finished writing is opened as a tab; with it shut,
|
|
1025
1078
|
// the answer's own Open does it.
|
|
@@ -1027,9 +1080,14 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
|
|
|
1027
1080
|
useEffect(() => {
|
|
1028
1081
|
const was = writing.current;
|
|
1029
1082
|
writing.current = chat.streaming;
|
|
1030
|
-
if (!was || chat.streaming
|
|
1083
|
+
if (!was || chat.streaming)
|
|
1031
1084
|
return;
|
|
1032
|
-
const
|
|
1085
|
+
const done = [...turns.current].reverse().find((m) => m.role === "assistant");
|
|
1086
|
+
if (done && typeof done.content === "string" && done.content)
|
|
1087
|
+
setFinished({ id: String(done.id), text: done.content });
|
|
1088
|
+
if (!panel)
|
|
1089
|
+
return;
|
|
1090
|
+
const answer = done;
|
|
1033
1091
|
const made = typeof answer?.content === "string" ? artifacts(answer.content).filter(renders) : [];
|
|
1034
1092
|
if (made.length)
|
|
1035
1093
|
see(made[made.length - 1]);
|
|
@@ -1275,7 +1333,10 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
|
|
|
1275
1333
|
e.preventDefault();
|
|
1276
1334
|
// A dropped folder arrives as entries; each file under it goes up with its path.
|
|
1277
1335
|
void dropped(e.dataTransfer).then((list) => hold(at, list, via));
|
|
1278
|
-
}, children: [
|
|
1336
|
+
}, children: [_jsx(Schedule, { open: plan !== null, onOpenChange: (open) => {
|
|
1337
|
+
if (!open)
|
|
1338
|
+
setPlan(null);
|
|
1339
|
+
}, prompt: plan ?? "", model: model, models: models.map((m) => ({ id: m.id, name: nameOf(m.id, listed) })) }), dragging ? (_jsx(YStack, { "aria-hidden": true, position: "absolute", t: 0, r: 0, b: 0, l: 0, z: 10, items: "center", justify: "center", bg: "var(--surface-scrim)", borderWidth: 2, borderStyle: "dashed", borderColor: "$borderColor", rounded: "var(--pane-round)", pointerEvents: "none", children: _jsxs(XStack, { items: "center", gap: "$2", px: "$4", py: "$2.5", rounded: "$10", bg: "$background", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(Paperclip, { size: 15, "aria-hidden": true }), _jsx(Text, { fontSize: "$4", color: "$ink", children: "Drop files to attach" })] }) })) : null, _jsxs(XStack, { "data-slot": "conversation-head", height: 52, items: "center", justify: "space-between", gap: "$3", pl: "$4", pr: "$2", shrink: 0, children: [_jsxs(XStack, { items: "center", gap: "$2", minW: 0, flex: 1, children: [mine ? (_jsx(Text, { fontSize: 15, lineHeight: 20, color: "$ink", numberOfLines: 1, shrink: 1, minW: 0, children: mine.title || "Untitled" })) : null, room.length ? (_jsx(XStack, { role: "list", "aria-label": "Agents in this chat", items: "center", gap: "$1.5", minW: 0, overflowX: "auto", "$platform-web": { scrollbarWidth: "none" }, children: room.map((name) => (_jsxs(XStack, { role: "listitem", items: "center", gap: "$1.5", pl: "$1", pr: "$0.5", py: "$0.5", rounded: 999, borderWidth: 1, borderColor: "$borderColor", shrink: 0, children: [_jsx(Face, { src: faces[name]?.avatar ?? (name === withWhom ? portrait : undefined), emoji: faces[name]?.emoji ?? (name === withWhom ? persona?.emoji : undefined), name: name, size: 22 }), _jsx(Text, { fontSize: "$2", fontWeight: "600", color: "$color", numberOfLines: 1, display: "none", $sm: { display: "flex" }, children: name }), _jsx(Box, { render: "button", onClick: () => openAgent(name), "aria-label": `Remove ${name} from this chat`, title: `Remove ${name}`, width: 24, height: 24, rounded: 999, items: "center", justify: "center", hoverStyle: { bg: "$hover" }, children: _jsx(X, { size: 13, "aria-hidden": true, color: "var(--soft, var(--muted-foreground))" }) })] }, name))) })) : null] }), _jsxs(XStack, { "data-slot": "conversation-controls", items: "center", gap: 2, shrink: 0, children: [open ? _jsx(Share, { thread: open }) : null, mine ? (_jsx(ConversationMenu, { threads: alone, active: open, onSchedule: anonymous ? undefined : () => setPlan(lastAsked()), children: _jsx(XStack, { children: _jsx(ThreadMore, { t: mine }) }) })) : null, _jsx(Summary, { sections: sections, onOpen: () => setPanel(true) }), _jsx(PanelToggle, { open: panel, onToggle: () => setPanel(!panel) })] })] }), wide && aside !== null ? (_jsx(Beside, { children: _jsx(Panel, { side: deck, kinds: NONE, onHide: () => setPanel(false), label: "Tabs", ...browsing }) })) : null, !wide && panel ? _jsx(Panel, { side: deck, kinds: NONE, sheet: true, onHide: () => setPanel(false), label: "Tabs", ...browsing }) : null, isFree(model) ? (_jsxs(Text, { fontSize: "$2", color: "$soft", text: "center", maxW: MEASURE, mx: "auto", px: "$4", py: "$2", children: [freeCopy.notice, " ", _jsx(Anchor, { href: site("/legal/privacy"), children: freeCopy.noticeCta })] })) : null, _jsx(ChatSurface, { ...chat, messages: shown, send: guarded,
|
|
1279
1340
|
// AN EMPTY ROOM CENTRES ITS COMPOSER. While nothing has been said the
|
|
1280
1341
|
// opening (the heading and the starters) and the composer sit as one
|
|
1281
1342
|
// group in the middle of the height, with or without the plan bars in
|
|
@@ -1294,15 +1355,18 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
|
|
|
1294
1355
|
return question ? failure(question) : null;
|
|
1295
1356
|
}
|
|
1296
1357
|
// A QUESTION THAT CARRIED PICTURES draws them beside its words.
|
|
1297
|
-
if (turn.role === "user" && Array.isArray(turn.content))
|
|
1298
|
-
|
|
1358
|
+
if (turn.role === "user" && Array.isArray(turn.content)) {
|
|
1359
|
+
const parts = partsOf(turn.content);
|
|
1360
|
+
const asked = parts.flatMap((p) => (p.type === "text" ? [p.text] : [])).join("\n");
|
|
1361
|
+
return (_jsxs(YStack, { gap: "$1", children: [_jsx(Parts, { parts: parts, prose: (text) => _jsx(Prose, { text: text }) }), !anonymous && asked ? _jsx(Plan, { onPress: () => setPlan(asked) }) : null] }));
|
|
1362
|
+
}
|
|
1299
1363
|
const said = typeof turn.content === "string" ? turn.content : "";
|
|
1300
1364
|
// A QUESTION THAT CARRIED FILES draws its words and the files' chips;
|
|
1301
1365
|
// the passages it handed the model are the model's to read, not ours.
|
|
1302
1366
|
if (turn.role === "user" && said) {
|
|
1303
1367
|
const read = told(said);
|
|
1304
1368
|
if (read.carried)
|
|
1305
|
-
return _jsx(Carried, { text: read.text, attached: read.carried.attached, reused: read.carried.reused, onOpen: openFile });
|
|
1369
|
+
return (_jsxs(YStack, { gap: "$1", children: [_jsx(Carried, { text: read.text, attached: read.carried.attached, reused: read.carried.reused, onOpen: openFile, onSave: anonymous ? undefined : (url, id, f) => keepOne(url, id, f.name, f.type) }), !anonymous && read.text ? _jsx(Plan, { onPress: () => setPlan(read.text) }) : null] }));
|
|
1306
1370
|
}
|
|
1307
1371
|
// WHILE THE ANSWER HAS NO WORDS YET: who is answering (the agents the
|
|
1308
1372
|
// question addresses, or the whole room when it names nobody), and the
|
|
@@ -1329,7 +1393,7 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
|
|
|
1329
1393
|
: [];
|
|
1330
1394
|
return (_jsxs(YStack, { gap: "$3", children: [parts.map((part, i) => (_jsxs(YStack, { gap: "$1", children: [part.who ? (_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(Face, { src: faces[part.who]?.avatar, emoji: faces[part.who]?.emoji, name: part.who, size: 20 }), _jsx(Text, { fontSize: "$2", fontWeight: "600", color: "$ink", children: part.who })] })) : null, _jsx(Prose, { text: part.text }), part.text ? _jsx(Reply, { text: part.text, listen: listen(part.who ?? withWhom ?? undefined), onOpen: opener(part.text) }) : null] }, i))), silent.length ? (_jsxs(Text, { fontSize: "$2", color: "$soft", children: ["No answer from ", join(silent), " in this reply."] })) : null, byline(turn.id)] }));
|
|
1331
1395
|
}
|
|
1332
|
-
return (_jsxs(YStack, { gap: "$1", children: [_jsx(Prose, { text: said }), turn.role === "assistant" && said ? _jsx(Reply, { text: said, listen: listen(withWhom ?? undefined), onOpen: opener(said) }) : null, turn.role === "assistant" && said ? byline(turn.id) : null] }));
|
|
1396
|
+
return (_jsxs(YStack, { gap: "$1", children: [_jsx(Prose, { text: said }), turn.role === "assistant" && said ? _jsx(Reply, { text: said, listen: listen(withWhom ?? undefined), onOpen: opener(said) }) : null, turn.role === "assistant" && said ? byline(turn.id) : null, turn.role === "user" && said && !anonymous ? _jsx(Plan, { onPress: () => setPlan(said) }) : null] }));
|
|
1333
1397
|
},
|
|
1334
1398
|
// The page's ONE heading, and it belongs to the empty transcript rather
|
|
1335
1399
|
// than to a signed-out branch — the branch is gone, and the heading went
|
|
@@ -1370,7 +1434,7 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
|
|
|
1370
1434
|
},
|
|
1371
1435
|
// WHAT GOES WITH THE MESSAGE, above the frame and at its width: the
|
|
1372
1436
|
// pause and its ways past, one chip a file, and talk mode's words.
|
|
1373
|
-
head: files.length || turnedAway || 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) }, 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
|
|
1437
|
+
head: files.length || turnedAway || 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 ? (_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
|
|
1374
1438
|
? worded(talk.refusal)
|
|
1375
1439
|
: talk.reply
|
|
1376
1440
|
? `Hanzo: ${talk.reply}`
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** A model the dialog offers: its id and the name a reader knows it by. */
|
|
2
|
+
export interface Choice {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function Schedule({ open, onOpenChange, prompt, model, models, }: {
|
|
7
|
+
open: boolean;
|
|
8
|
+
onOpenChange: (open: boolean) => void;
|
|
9
|
+
/** The prompt it opens with. */
|
|
10
|
+
prompt: string;
|
|
11
|
+
/** The model it opens on: the conversation's. */
|
|
12
|
+
model: string;
|
|
13
|
+
models: readonly Choice[];
|
|
14
|
+
}): import("react").JSX.Element;
|
package/dist/Schedule.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
// "Schedule this": a prompt from Chat that runs itself.
|
|
4
|
+
//
|
|
5
|
+
// The dialog holds the prompt, a "When" said in the reader's words — read live
|
|
6
|
+
// by the platform's scheduler (`POST /v1/auto/cron`) in their own time zone and
|
|
7
|
+
// said back with the next run — and the model it thinks with. Create keeps it
|
|
8
|
+
// as an automation made in Chat (lib/auto); its runs land in the Library, and
|
|
9
|
+
// the list of them is Scheduled tasks.
|
|
10
|
+
import { useEffect, useRef, useState } from 'react';
|
|
11
|
+
import { useAi } from '@hanzo/ai/react';
|
|
12
|
+
import { Anchor, Button, Dialog, DialogContent, DialogTitle, Input, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, SizableText, Text, Textarea, XStack, YStack } from '@hanzo/ui';
|
|
13
|
+
import { cron, schedule, zone } from './lib/auto.js';
|
|
14
|
+
import { app } from './lib/host.js';
|
|
15
|
+
import { site } from './where.js';
|
|
16
|
+
import { BAD } from './lib/mix.js';
|
|
17
|
+
/** How long the "When" field rests before it is read. */
|
|
18
|
+
const REST = 400;
|
|
19
|
+
/** A moment as the reader's clock says it. */
|
|
20
|
+
function local(at) {
|
|
21
|
+
const d = new Date(at);
|
|
22
|
+
if (Number.isNaN(d.getTime()))
|
|
23
|
+
return '';
|
|
24
|
+
return d.toLocaleString(undefined, { weekday: 'short', month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' });
|
|
25
|
+
}
|
|
26
|
+
export function Schedule({ open, onOpenChange, prompt, model, models, }) {
|
|
27
|
+
const ai = useAi();
|
|
28
|
+
const [words, setWords] = useState('');
|
|
29
|
+
const [said, setSaid] = useState('');
|
|
30
|
+
const [chose, setChose] = useState(model);
|
|
31
|
+
const [read, setRead] = useState(null);
|
|
32
|
+
const [reading, setReading] = useState(false);
|
|
33
|
+
const [wrong, setWrong] = useState('');
|
|
34
|
+
const [busy, setBusy] = useState(false);
|
|
35
|
+
const [made, setMade] = useState(false);
|
|
36
|
+
// Each opening starts from what it was opened on; the typing that follows is the reader's.
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (!open)
|
|
39
|
+
return;
|
|
40
|
+
setWords(prompt);
|
|
41
|
+
setSaid('');
|
|
42
|
+
setChose(model);
|
|
43
|
+
setRead(null);
|
|
44
|
+
setWrong('');
|
|
45
|
+
setMade(false);
|
|
46
|
+
}, [open, prompt, model]);
|
|
47
|
+
// The "When", read once the reader rests, and the last read only.
|
|
48
|
+
const asked = useRef(0);
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
const text = said.trim();
|
|
51
|
+
if (!open || !text) {
|
|
52
|
+
setRead(null);
|
|
53
|
+
setReading(false);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const n = ++asked.current;
|
|
57
|
+
const stop = new AbortController();
|
|
58
|
+
setReading(true);
|
|
59
|
+
const t = setTimeout(() => {
|
|
60
|
+
cron(ai, text, zone(), stop.signal).then((got) => {
|
|
61
|
+
if (n !== asked.current)
|
|
62
|
+
return;
|
|
63
|
+
setRead(got);
|
|
64
|
+
setWrong('');
|
|
65
|
+
setReading(false);
|
|
66
|
+
}, (e) => {
|
|
67
|
+
if (n !== asked.current || stop.signal.aborted)
|
|
68
|
+
return;
|
|
69
|
+
setRead(null);
|
|
70
|
+
setWrong(e?.message || 'That is not a time the scheduler can read.');
|
|
71
|
+
setReading(false);
|
|
72
|
+
});
|
|
73
|
+
}, REST);
|
|
74
|
+
return () => {
|
|
75
|
+
clearTimeout(t);
|
|
76
|
+
stop.abort();
|
|
77
|
+
};
|
|
78
|
+
}, [said, open, ai]);
|
|
79
|
+
const ready = Boolean(words.trim() && read && !reading && !busy);
|
|
80
|
+
const create = async () => {
|
|
81
|
+
if (!ready || !read)
|
|
82
|
+
return;
|
|
83
|
+
setBusy(true);
|
|
84
|
+
setWrong('');
|
|
85
|
+
try {
|
|
86
|
+
await schedule(ai, words, read.schedule, chose || undefined);
|
|
87
|
+
setMade(true);
|
|
88
|
+
}
|
|
89
|
+
catch (e) {
|
|
90
|
+
setWrong(e?.message || 'The task was not scheduled.');
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
setBusy(false);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
const options = models.some((m) => m.id === chose) || !chose ? models : [{ id: chose, name: chose }, ...models];
|
|
97
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { maxW: 520, rounded: "$6", gap: "$3", "data-slot": "schedule", children: [_jsx(DialogTitle, { children: "Schedule this" }), made ? (_jsxs(YStack, { gap: "$3", children: [_jsxs(SizableText, { size: "$3", color: "$ink", role: "status", children: ["Scheduled.", ' ', _jsx(Anchor, { href: site(app('-/automations')), color: "$ink", textDecorationLine: "underline", children: "See it in Scheduled tasks." })] }), _jsx(XStack, { justify: "flex-end", children: _jsx(Button, { size: "sm", rounded: 999, onPress: () => onOpenChange(false), children: "Done" }) })] })) : (_jsxs(YStack, { gap: "$3", children: [_jsxs(YStack, { gap: "$1", children: [_jsx(Text, { fontSize: "$1", color: "$soft", children: "Prompt" }), _jsx(Textarea, { value: words, onChangeText: setWords, rows: 4, "aria-label": "Prompt" })] }), _jsxs(YStack, { gap: "$1", children: [_jsx(Text, { fontSize: "$1", color: "$soft", children: "When" }), _jsx(Input, { autoFocus: true, value: said, onChangeText: setSaid, placeholder: "Every Monday at 9am", "aria-label": "When it runs", "aria-describedby": "schedule-when", onKeyDown: (e) => {
|
|
98
|
+
if (e.key === 'Enter' && !e.nativeEvent?.isComposing)
|
|
99
|
+
void create();
|
|
100
|
+
} }), _jsx(Text, { id: "schedule-when", fontSize: "$1", color: wrong ? BAD : '$soft', role: "status", minH: 18, children: wrong
|
|
101
|
+
? wrong
|
|
102
|
+
: reading
|
|
103
|
+
? 'Reading.'
|
|
104
|
+
: read
|
|
105
|
+
? [read.says, read.next ? `next ${local(read.next)}` : 'when you run it'].filter(Boolean).join(' · ')
|
|
106
|
+
: 'Say it the way you would: every weekday at 8, every 5 minutes, tomorrow at noon.' })] }), options.length ? (_jsxs(YStack, { gap: "$1", children: [_jsx(Text, { fontSize: "$1", color: "$soft", children: "Model" }), _jsxs(Select, { value: chose, onValueChange: setChose, children: [_jsx(SelectTrigger, { "aria-label": "Model", width: "100%", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: options.map((m) => (_jsx(SelectItem, { value: m.id, children: m.name }, m.id))) })] })] })) : null, _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "outline", rounded: 999, onPress: () => onOpenChange(false), children: "Cancel" }), _jsx(Button, { size: "sm", rounded: 999, disabled: !ready, onPress: () => void create(), children: busy ? 'Scheduling…' : 'Create' })] })] }))] }) }));
|
|
107
|
+
}
|
package/dist/keep.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Artifact } from '@hanzo/build';
|
|
2
|
+
import { type Api, type Item } from './lib/library.js';
|
|
3
|
+
import { type Via } from './pane.js';
|
|
4
|
+
/** A picture's still: its SVG drawn onto a canvas, as base64 PNG, with its size. Null where the browser cannot draw it. */
|
|
5
|
+
export declare function still(svg: string): Promise<{
|
|
6
|
+
png: string;
|
|
7
|
+
width: number;
|
|
8
|
+
height: number;
|
|
9
|
+
} | null>;
|
|
10
|
+
/** Keeps what one finished answer made. Each save stands alone: one that fails leaves the rest kept. */
|
|
11
|
+
export declare function keepAnswer(api: Api, thread: string, message: string, blocks: readonly Artifact[]): Promise<Item[]>;
|
|
12
|
+
/**
|
|
13
|
+
* Keeps each answer's makings once it finishes. `finished` is the answer that
|
|
14
|
+
* just stopped streaming (its id and its words), `thread` the conversation's
|
|
15
|
+
* id, which can land a moment after the answer does: the answer waits for it.
|
|
16
|
+
*/
|
|
17
|
+
export declare function useKeep(api: Api, signedIn: boolean, thread: string | null, finished: {
|
|
18
|
+
id: string;
|
|
19
|
+
text: string;
|
|
20
|
+
} | null, blocks: (text: string) => Artifact[]): void;
|
|
21
|
+
/** A Library item's bytes as a File, read through its signed address. */
|
|
22
|
+
export declare function fileOf(item: Item): Promise<File>;
|
|
23
|
+
/**
|
|
24
|
+
* `/chat?attach=<item>`: the Library item, held for the next message in
|
|
25
|
+
* conversation `at`. The parameter is struck from the address once read, so a
|
|
26
|
+
* reload does not attach it twice. Answers why it could not be, or ''.
|
|
27
|
+
*/
|
|
28
|
+
export declare function useAttach(at: string, via: Via): string;
|
|
29
|
+
/**
|
|
30
|
+
* Keeps a file the reader put into the conversation, when they ask: its bytes
|
|
31
|
+
* read from `url` (the object URL this document holds, or the workspace copy's
|
|
32
|
+
* signed address), keyed by the workspace file it is, so asking twice keeps one.
|
|
33
|
+
*/
|
|
34
|
+
export declare function keepFile(api: Api, f: {
|
|
35
|
+
id: string;
|
|
36
|
+
name: string;
|
|
37
|
+
type: string;
|
|
38
|
+
url: string;
|
|
39
|
+
thread: string | null;
|
|
40
|
+
}): Promise<Item>;
|
package/dist/keep.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
// What Chat keeps in the Library, and what it takes back out.
|
|
3
|
+
//
|
|
4
|
+
// KEPT AS IT IS MADE. When an answer finishes, every page, picture and file it
|
|
5
|
+
// wrote out (lib/library `made`) is saved to the reader's Library under the
|
|
6
|
+
// conversation, keyed `<thread>/<message>/<index>`, so it is kept once whatever
|
|
7
|
+
// redraws it. A picture is kept with a still drawn here, so the Library shows it
|
|
8
|
+
// whatever type its bytes are served as. Nothing is kept for a reader with no
|
|
9
|
+
// account, or before the conversation has an id: the item names where it came
|
|
10
|
+
// from.
|
|
11
|
+
//
|
|
12
|
+
// TAKEN BACK IN by `/chat?attach=<item>` (the Library's "Use in Chat"): the
|
|
13
|
+
// item's bytes are held for the next message by the one road a picked file
|
|
14
|
+
// takes (pane.ts `hold`).
|
|
15
|
+
import { useEffect, useRef, useState } from 'react';
|
|
16
|
+
import { base64, get, keyOf, made, save } from './lib/library.js';
|
|
17
|
+
import { talk } from './lib/host.js';
|
|
18
|
+
import { hold } from './pane.js';
|
|
19
|
+
/** The longest side of a picture's still, px. */
|
|
20
|
+
const STILL = 512;
|
|
21
|
+
/** A picture's still: its SVG drawn onto a canvas, as base64 PNG, with its size. Null where the browser cannot draw it. */
|
|
22
|
+
export async function still(svg) {
|
|
23
|
+
if (typeof document === 'undefined')
|
|
24
|
+
return null;
|
|
25
|
+
const url = URL.createObjectURL(new Blob([svg], { type: 'image/svg+xml' }));
|
|
26
|
+
try {
|
|
27
|
+
const img = new Image();
|
|
28
|
+
await new Promise((done, fail) => {
|
|
29
|
+
img.onload = () => done();
|
|
30
|
+
img.onerror = () => fail(new Error('undrawable'));
|
|
31
|
+
img.src = url;
|
|
32
|
+
});
|
|
33
|
+
const w = img.naturalWidth || STILL;
|
|
34
|
+
const h = img.naturalHeight || STILL;
|
|
35
|
+
const scale = Math.min(1, STILL / Math.max(w, h));
|
|
36
|
+
const canvas = document.createElement('canvas');
|
|
37
|
+
canvas.width = Math.max(1, Math.round(w * scale));
|
|
38
|
+
canvas.height = Math.max(1, Math.round(h * scale));
|
|
39
|
+
canvas.getContext('2d')?.drawImage(img, 0, 0, canvas.width, canvas.height);
|
|
40
|
+
const blob = await new Promise((done) => canvas.toBlob(done, 'image/png'));
|
|
41
|
+
return blob ? { png: await base64(blob), width: w, height: h } : null;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
URL.revokeObjectURL(url);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/** Keeps what one finished answer made. Each save stands alone: one that fails leaves the rest kept. */
|
|
51
|
+
export async function keepAnswer(api, thread, message, blocks) {
|
|
52
|
+
const out = [];
|
|
53
|
+
for (const [i, a] of made(blocks).entries()) {
|
|
54
|
+
try {
|
|
55
|
+
const pic = a.mime === 'image/svg+xml' ? await still(a.body) : null;
|
|
56
|
+
out.push(await save(api, {
|
|
57
|
+
name: a.name,
|
|
58
|
+
mime: a.mime,
|
|
59
|
+
data: new Blob([a.body], { type: a.mime }),
|
|
60
|
+
ref: thread,
|
|
61
|
+
origin: talk(thread),
|
|
62
|
+
key: keyOf(thread, message, i),
|
|
63
|
+
...(pic ? { preview: pic.png, width: pic.width, height: pic.height } : {}),
|
|
64
|
+
}));
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// Kept on the next answer that makes it; the conversation holds it meanwhile.
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Keeps each answer's makings once it finishes. `finished` is the answer that
|
|
74
|
+
* just stopped streaming (its id and its words), `thread` the conversation's
|
|
75
|
+
* id, which can land a moment after the answer does: the answer waits for it.
|
|
76
|
+
*/
|
|
77
|
+
export function useKeep(api, signedIn, thread, finished, blocks) {
|
|
78
|
+
const waiting = useRef([]);
|
|
79
|
+
const kept = useRef(new Set());
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
if (!signedIn || !finished || kept.current.has(finished.id))
|
|
82
|
+
return;
|
|
83
|
+
waiting.current.push(finished);
|
|
84
|
+
kept.current.add(finished.id);
|
|
85
|
+
}, [signedIn, finished]);
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
if (!thread || !signedIn || !waiting.current.length)
|
|
88
|
+
return;
|
|
89
|
+
const now = waiting.current.splice(0);
|
|
90
|
+
for (const one of now) {
|
|
91
|
+
const list = blocks(one.text);
|
|
92
|
+
if (list.length)
|
|
93
|
+
void keepAnswer(api, thread, one.id, list);
|
|
94
|
+
}
|
|
95
|
+
}, [thread, signedIn, finished, api, blocks]);
|
|
96
|
+
}
|
|
97
|
+
/** A Library item's bytes as a File, read through its signed address. */
|
|
98
|
+
export async function fileOf(item) {
|
|
99
|
+
const res = await fetch(item.url);
|
|
100
|
+
if (!res.ok)
|
|
101
|
+
throw new Error(`${item.name} could not be read from your Library.`);
|
|
102
|
+
const blob = await res.blob();
|
|
103
|
+
return new File([blob], item.name, { type: item.mime || blob.type });
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* `/chat?attach=<item>`: the Library item, held for the next message in
|
|
107
|
+
* conversation `at`. The parameter is struck from the address once read, so a
|
|
108
|
+
* reload does not attach it twice. Answers why it could not be, or ''.
|
|
109
|
+
*/
|
|
110
|
+
export function useAttach(at, via) {
|
|
111
|
+
const [why, setWhy] = useState('');
|
|
112
|
+
const done = useRef(false);
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
if (done.current || typeof window === 'undefined' || !via.org)
|
|
115
|
+
return;
|
|
116
|
+
const url = new URL(window.location.href);
|
|
117
|
+
const id = url.searchParams.get('attach')?.trim();
|
|
118
|
+
if (!id)
|
|
119
|
+
return;
|
|
120
|
+
done.current = true;
|
|
121
|
+
url.searchParams.delete('attach');
|
|
122
|
+
window.history.replaceState(null, '', `${url.pathname}${url.search}${url.hash}`);
|
|
123
|
+
get(via.api, id)
|
|
124
|
+
.then(fileOf)
|
|
125
|
+
.then((f) => hold(at, [f], via))
|
|
126
|
+
.catch((e) => setWhy(e?.message || 'That item could not be attached.'));
|
|
127
|
+
}, [at, via]);
|
|
128
|
+
return why;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Keeps a file the reader put into the conversation, when they ask: its bytes
|
|
132
|
+
* read from `url` (the object URL this document holds, or the workspace copy's
|
|
133
|
+
* signed address), keyed by the workspace file it is, so asking twice keeps one.
|
|
134
|
+
*/
|
|
135
|
+
export async function keepFile(api, f) {
|
|
136
|
+
const res = await fetch(f.url);
|
|
137
|
+
if (!res.ok)
|
|
138
|
+
throw new Error(`${f.name} could not be read.`);
|
|
139
|
+
const blob = await res.blob();
|
|
140
|
+
return save(api, {
|
|
141
|
+
name: f.name,
|
|
142
|
+
mime: f.type || blob.type || 'application/octet-stream',
|
|
143
|
+
data: blob,
|
|
144
|
+
ref: f.thread ?? '',
|
|
145
|
+
origin: f.thread ? talk(f.thread) : talk(),
|
|
146
|
+
key: `file/${f.id}`,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SCHEDULED — a prompt from Chat that runs itself (`/v1/auto`).
|
|
3
|
+
*
|
|
4
|
+
* The one scheduler is the platform's automations: a schedule said the way a
|
|
5
|
+
* person says it ("every Monday at 9am") is turned into one by `POST
|
|
6
|
+
* /v1/auto/cron` in the reader's own time zone, and `POST /v1/auto/automations`
|
|
7
|
+
* keeps it, made in Chat (`source: "chat"`). Each run's answer lands in the
|
|
8
|
+
* reader's Library; the list of them is Scheduled tasks in Dev.
|
|
9
|
+
*/
|
|
10
|
+
import type { Api } from './library.js';
|
|
11
|
+
/** When it runs, in the platform's words. */
|
|
12
|
+
export interface Schedule {
|
|
13
|
+
kind: string;
|
|
14
|
+
at?: string;
|
|
15
|
+
day?: string;
|
|
16
|
+
cron?: string;
|
|
17
|
+
tz?: string;
|
|
18
|
+
}
|
|
19
|
+
/** A schedule read from words: itself, the words it is said back in, and its next run. */
|
|
20
|
+
export interface When {
|
|
21
|
+
schedule: Schedule;
|
|
22
|
+
says: string;
|
|
23
|
+
/** RFC 3339, or null when it runs only when asked. */
|
|
24
|
+
next: string | null;
|
|
25
|
+
}
|
|
26
|
+
/** The reader's own time zone, or UTC where the browser will not say. */
|
|
27
|
+
export declare function zone(): string;
|
|
28
|
+
/** Reads a schedule off the wire, or null for one with no kind. */
|
|
29
|
+
export declare function when(raw: unknown): When | null;
|
|
30
|
+
/** Turns words into a schedule, in `tz`. */
|
|
31
|
+
export declare function cron(api: Api, text: string, tz: string, signal?: AbortSignal): Promise<When>;
|
|
32
|
+
/** A schedule's name: the prompt's first line, held to what the platform keeps. */
|
|
33
|
+
export declare function title(prompt: string): string;
|
|
34
|
+
/** Keeps a prompt from Chat as an automation on `schedule`, and answers its id. */
|
|
35
|
+
export declare function schedule(api: Api, prompt: string, at: Schedule, model?: string): Promise<string>;
|
package/dist/lib/auto.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SCHEDULED — a prompt from Chat that runs itself (`/v1/auto`).
|
|
3
|
+
*
|
|
4
|
+
* The one scheduler is the platform's automations: a schedule said the way a
|
|
5
|
+
* person says it ("every Monday at 9am") is turned into one by `POST
|
|
6
|
+
* /v1/auto/cron` in the reader's own time zone, and `POST /v1/auto/automations`
|
|
7
|
+
* keeps it, made in Chat (`source: "chat"`). Each run's answer lands in the
|
|
8
|
+
* reader's Library; the list of them is Scheduled tasks in Dev.
|
|
9
|
+
*/
|
|
10
|
+
/** The reader's own time zone, or UTC where the browser will not say. */
|
|
11
|
+
export function zone() {
|
|
12
|
+
try {
|
|
13
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return 'UTC';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const obj = (v) => (v && typeof v === 'object' ? v : {});
|
|
20
|
+
const str = (v) => (typeof v === 'string' ? v : '');
|
|
21
|
+
/** Reads a schedule off the wire, or null for one with no kind. */
|
|
22
|
+
export function when(raw) {
|
|
23
|
+
const o = obj(raw);
|
|
24
|
+
const s = obj(o.schedule);
|
|
25
|
+
if (!str(s.kind))
|
|
26
|
+
return null;
|
|
27
|
+
const schedule = { kind: str(s.kind) };
|
|
28
|
+
for (const k of ['at', 'day', 'cron', 'tz'])
|
|
29
|
+
if (str(s[k]))
|
|
30
|
+
schedule[k] = str(s[k]);
|
|
31
|
+
return { schedule, says: str(o.says), next: str(o.next) || null };
|
|
32
|
+
}
|
|
33
|
+
/** Turns words into a schedule, in `tz`. */
|
|
34
|
+
export async function cron(api, text, tz, signal) {
|
|
35
|
+
const got = when(await api.http.json({ method: 'POST', path: '/v1/auto/cron', body: { text, tz }, signal }));
|
|
36
|
+
if (!got)
|
|
37
|
+
throw new Error('That is not a time the scheduler can read.');
|
|
38
|
+
return got;
|
|
39
|
+
}
|
|
40
|
+
/** A schedule's name: the prompt's first line, held to what the platform keeps. */
|
|
41
|
+
export function title(prompt) {
|
|
42
|
+
const first = prompt.trim().split('\n')[0]?.trim() ?? '';
|
|
43
|
+
return first.length > 80 ? `${first.slice(0, 79).trimEnd()}…` : first || 'Scheduled prompt';
|
|
44
|
+
}
|
|
45
|
+
/** Keeps a prompt from Chat as an automation on `schedule`, and answers its id. */
|
|
46
|
+
export async function schedule(api, prompt, at, model) {
|
|
47
|
+
const body = {
|
|
48
|
+
name: title(prompt),
|
|
49
|
+
instructions: prompt.trim(),
|
|
50
|
+
schedule: at,
|
|
51
|
+
source: 'chat',
|
|
52
|
+
permissions: 'ask',
|
|
53
|
+
notify: true,
|
|
54
|
+
};
|
|
55
|
+
if (model)
|
|
56
|
+
body.model = model;
|
|
57
|
+
const got = obj(await api.http.json({ method: 'POST', path: '/v1/auto/automations', body }));
|
|
58
|
+
const id = str(got.id);
|
|
59
|
+
if (!id)
|
|
60
|
+
throw new Error('The scheduler answered with no task.');
|
|
61
|
+
return id;
|
|
62
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE LIBRARY — everything a person made, in one place (`/v1/library`).
|
|
3
|
+
*
|
|
4
|
+
* Chat keeps what its answers make there: a page or a picture an answer wrote
|
|
5
|
+
* out, a file it wrote out whole, and a file the reader chose to keep. Each is
|
|
6
|
+
* one POST of its bytes; the item is the reader's alone until they share it.
|
|
7
|
+
*
|
|
8
|
+
* A save names itself with a key — `<thread>/<message>/<index>` for what an
|
|
9
|
+
* answer made — and the Library answers the item already kept for a key it has
|
|
10
|
+
* seen, so a turn drawn again, a retry, or a second tab files one item, not two.
|
|
11
|
+
*/
|
|
12
|
+
import type { HttpClient } from '@hanzo/ai';
|
|
13
|
+
import type { Artifact } from '@hanzo/build';
|
|
14
|
+
/** The HTTP half of an `@hanzo/ai` client: all this module needs. */
|
|
15
|
+
export interface Api {
|
|
16
|
+
http: Pick<HttpClient, 'json'>;
|
|
17
|
+
}
|
|
18
|
+
/** One thing in the Library, as it answers. */
|
|
19
|
+
export interface Item {
|
|
20
|
+
id: string;
|
|
21
|
+
kind: string;
|
|
22
|
+
source: string;
|
|
23
|
+
name: string;
|
|
24
|
+
mime: string;
|
|
25
|
+
bytes: number;
|
|
26
|
+
/** A signed address that opens it in place, for an hour. */
|
|
27
|
+
url: string;
|
|
28
|
+
/** A signed address that downloads it. */
|
|
29
|
+
download: string;
|
|
30
|
+
/** A signed address of its still, or '' for none. */
|
|
31
|
+
preview: string;
|
|
32
|
+
/** The path back to what made it. */
|
|
33
|
+
origin: string;
|
|
34
|
+
mine: boolean;
|
|
35
|
+
shared: boolean;
|
|
36
|
+
}
|
|
37
|
+
/** What a save sends. */
|
|
38
|
+
export interface Keep {
|
|
39
|
+
name: string;
|
|
40
|
+
mime: string;
|
|
41
|
+
/** The bytes: base64, or a Blob read here. */
|
|
42
|
+
data: string | Blob;
|
|
43
|
+
/** What made it: the conversation. */
|
|
44
|
+
ref: string;
|
|
45
|
+
/** The path back to it. */
|
|
46
|
+
origin: string;
|
|
47
|
+
/** Names the save, so the same one twice is one item. */
|
|
48
|
+
key: string;
|
|
49
|
+
/** A still that shows it, base64 PNG. */
|
|
50
|
+
preview?: string;
|
|
51
|
+
width?: number;
|
|
52
|
+
height?: number;
|
|
53
|
+
}
|
|
54
|
+
/** The most one save sends; the Library answers 413 past it. */
|
|
55
|
+
export declare const MAX: number;
|
|
56
|
+
/** The key of the `index`th thing message `message` of `thread` made. */
|
|
57
|
+
export declare const keyOf: (thread: string, message: string, index: number) => string;
|
|
58
|
+
/** A Blob's bytes, base64. */
|
|
59
|
+
export declare function base64(blob: Blob): Promise<string>;
|
|
60
|
+
/** Reads an item off the wire, or null for something that is not one. */
|
|
61
|
+
export declare function read(raw: unknown): Item | null;
|
|
62
|
+
/** Keeps one thing from Chat in the reader's Library. */
|
|
63
|
+
export declare function save(api: Api, k: Keep): Promise<Item>;
|
|
64
|
+
/** One item the reader may see — theirs, or shared in their organization. */
|
|
65
|
+
export declare function get(api: Api, id: string): Promise<Item>;
|
|
66
|
+
/**
|
|
67
|
+
* What an answer made, worth keeping, of the fenced blocks it wrote
|
|
68
|
+
* (@hanzo/build `artifacts`): a page or a picture, and a file written out under
|
|
69
|
+
* a file's name. A snippet in a language (`ts block`) is part of the answer,
|
|
70
|
+
* not a thing it made, so it is not kept.
|
|
71
|
+
*/
|
|
72
|
+
export declare function made(blocks: readonly Artifact[]): Artifact[];
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE LIBRARY — everything a person made, in one place (`/v1/library`).
|
|
3
|
+
*
|
|
4
|
+
* Chat keeps what its answers make there: a page or a picture an answer wrote
|
|
5
|
+
* out, a file it wrote out whole, and a file the reader chose to keep. Each is
|
|
6
|
+
* one POST of its bytes; the item is the reader's alone until they share it.
|
|
7
|
+
*
|
|
8
|
+
* A save names itself with a key — `<thread>/<message>/<index>` for what an
|
|
9
|
+
* answer made — and the Library answers the item already kept for a key it has
|
|
10
|
+
* seen, so a turn drawn again, a retry, or a second tab files one item, not two.
|
|
11
|
+
*/
|
|
12
|
+
/** The most one save sends; the Library answers 413 past it. */
|
|
13
|
+
export const MAX = 10 * 1024 * 1024;
|
|
14
|
+
/** The key of the `index`th thing message `message` of `thread` made. */
|
|
15
|
+
export const keyOf = (thread, message, index) => `${thread}/${message}/${index}`;
|
|
16
|
+
/** A Blob's bytes, base64. */
|
|
17
|
+
export async function base64(blob) {
|
|
18
|
+
const bytes = new Uint8Array(await blob.arrayBuffer());
|
|
19
|
+
let s = '';
|
|
20
|
+
for (let i = 0; i < bytes.length; i += 0x8000)
|
|
21
|
+
s += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
|
|
22
|
+
return btoa(s);
|
|
23
|
+
}
|
|
24
|
+
/** Reads an item off the wire, or null for something that is not one. */
|
|
25
|
+
export function read(raw) {
|
|
26
|
+
const o = (raw && typeof raw === 'object' ? raw : {});
|
|
27
|
+
const str = (k) => (typeof o[k] === 'string' ? o[k] : '');
|
|
28
|
+
if (!str('id'))
|
|
29
|
+
return null;
|
|
30
|
+
return {
|
|
31
|
+
id: str('id'),
|
|
32
|
+
kind: str('kind'),
|
|
33
|
+
source: str('source'),
|
|
34
|
+
name: str('name') || str('id'),
|
|
35
|
+
mime: str('mime'),
|
|
36
|
+
bytes: typeof o.bytes === 'number' ? o.bytes : 0,
|
|
37
|
+
url: str('url'),
|
|
38
|
+
download: str('download'),
|
|
39
|
+
preview: str('preview'),
|
|
40
|
+
origin: str('origin'),
|
|
41
|
+
mine: o.mine === true,
|
|
42
|
+
shared: o.shared === true,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/** Keeps one thing from Chat in the reader's Library. */
|
|
46
|
+
export async function save(api, k) {
|
|
47
|
+
const data = typeof k.data === 'string' ? k.data : await base64(k.data);
|
|
48
|
+
if ((data.length * 3) / 4 > MAX)
|
|
49
|
+
throw new Error(`${k.name} is larger than 10 MB, the most the Library takes from Chat.`);
|
|
50
|
+
const body = { name: k.name, mime: k.mime, data, source: 'chat', ref: k.ref, origin: k.origin, key: k.key };
|
|
51
|
+
if (k.preview)
|
|
52
|
+
body.preview = k.preview;
|
|
53
|
+
if (k.width)
|
|
54
|
+
body.width = Math.round(k.width);
|
|
55
|
+
if (k.height)
|
|
56
|
+
body.height = Math.round(k.height);
|
|
57
|
+
const got = read(await api.http.json({ method: 'POST', path: '/v1/library', body }));
|
|
58
|
+
if (!got)
|
|
59
|
+
throw new Error('The Library answered with no item.');
|
|
60
|
+
return got;
|
|
61
|
+
}
|
|
62
|
+
/** One item the reader may see — theirs, or shared in their organization. */
|
|
63
|
+
export async function get(api, id) {
|
|
64
|
+
const got = read(await api.http.json({ path: `/v1/library/${encodeURIComponent(id)}` }));
|
|
65
|
+
if (!got)
|
|
66
|
+
throw new Error('That item is not in your Library.');
|
|
67
|
+
return got;
|
|
68
|
+
}
|
|
69
|
+
const EXT = { 'text/html': 'html', 'image/svg+xml': 'svg' };
|
|
70
|
+
const FILE = /\.[a-z0-9]{1,8}$/i;
|
|
71
|
+
/** Whether a browser draws it as a page or a picture (@hanzo/build `renders`). */
|
|
72
|
+
const drawn = (a) => a.mime === 'text/html' || a.mime === 'image/svg+xml';
|
|
73
|
+
/**
|
|
74
|
+
* What an answer made, worth keeping, of the fenced blocks it wrote
|
|
75
|
+
* (@hanzo/build `artifacts`): a page or a picture, and a file written out under
|
|
76
|
+
* a file's name. A snippet in a language (`ts block`) is part of the answer,
|
|
77
|
+
* not a thing it made, so it is not kept.
|
|
78
|
+
*/
|
|
79
|
+
export function made(blocks) {
|
|
80
|
+
return blocks
|
|
81
|
+
.filter((a) => drawn(a) || FILE.test(a.name))
|
|
82
|
+
.map((a, i) => (FILE.test(a.name) ? a : { ...a, name: `${a.mime === 'image/svg+xml' ? 'Picture' : 'Page'} ${i + 1}.${EXT[a.mime] ?? 'txt'}` }));
|
|
83
|
+
}
|
package/dist/lib/session.js
CHANGED
|
@@ -15,12 +15,22 @@
|
|
|
15
15
|
* Two derivations of one name never agree; this is the one derivation.
|
|
16
16
|
*/
|
|
17
17
|
import { api } from './api.js';
|
|
18
|
+
import { where } from '../where.js';
|
|
18
19
|
/** The SDK's own keys, `hanzo_iam_`-prefixed. The far side of a boundary. */
|
|
19
20
|
const PREFIX = 'hanzo_iam_';
|
|
20
21
|
const ACCESS = `${PREFIX}access_token`;
|
|
21
22
|
const REFRESH = `${PREFIX}refresh_token`;
|
|
22
23
|
const EXPIRES = `${PREFIX}expires_at`;
|
|
23
24
|
const KEYS = [ACCESS, REFRESH, `${PREFIX}id_token`, EXPIRES];
|
|
25
|
+
/**
|
|
26
|
+
* The store the SDK keeps the session in: the host's (`configure({ session })`,
|
|
27
|
+
* Hanzo Desktop's keychain, where no token reaches the webview's localStorage),
|
|
28
|
+
* else this page's localStorage. Only the SDK's own keys are read from it; the
|
|
29
|
+
* rooms' own choices (the organization, the person) stay in localStorage.
|
|
30
|
+
*/
|
|
31
|
+
function tokens() {
|
|
32
|
+
return where().session ?? window.localStorage;
|
|
33
|
+
}
|
|
24
34
|
/**
|
|
25
35
|
* The access token, or null.
|
|
26
36
|
*
|
|
@@ -32,7 +42,7 @@ export function bearer() {
|
|
|
32
42
|
if (typeof window === 'undefined')
|
|
33
43
|
return null;
|
|
34
44
|
try {
|
|
35
|
-
return
|
|
45
|
+
return tokens().getItem(ACCESS);
|
|
36
46
|
}
|
|
37
47
|
catch {
|
|
38
48
|
return null;
|
|
@@ -48,7 +58,7 @@ export function live() {
|
|
|
48
58
|
if (typeof window === 'undefined')
|
|
49
59
|
return false;
|
|
50
60
|
try {
|
|
51
|
-
const expires =
|
|
61
|
+
const expires = tokens().getItem(EXPIRES);
|
|
52
62
|
return Boolean(bearer()) && (!expires || Date.now() < Number(expires));
|
|
53
63
|
}
|
|
54
64
|
catch {
|
|
@@ -386,7 +396,7 @@ export function scope() {
|
|
|
386
396
|
*/
|
|
387
397
|
export function renew() {
|
|
388
398
|
try {
|
|
389
|
-
|
|
399
|
+
tokens().setItem(EXPIRES, '0');
|
|
390
400
|
}
|
|
391
401
|
catch {
|
|
392
402
|
/* no store to renew */
|
|
@@ -459,8 +469,8 @@ export async function support(org) {
|
|
|
459
469
|
const exp = claimsOf(next ?? null)?.exp;
|
|
460
470
|
if (!res.ok || !next || typeof exp !== 'number')
|
|
461
471
|
throw new Error(body?.msg || `IAM answered ${res.status}.`);
|
|
462
|
-
|
|
463
|
-
|
|
472
|
+
tokens().setItem(ACCESS, next);
|
|
473
|
+
tokens().setItem(EXPIRES, String(exp * 1000));
|
|
464
474
|
pick(org ?? homeOf(next));
|
|
465
475
|
window.location.reload();
|
|
466
476
|
}
|
package/dist/thread.d.ts
CHANGED
|
@@ -29,12 +29,15 @@ export declare const href: (id: string) => string;
|
|
|
29
29
|
* child holds the rows (`ThreadRow`). `threads` are the list's conversations by
|
|
30
30
|
* id, `active` the one the pane holds (what the keys act on with no menu open),
|
|
31
31
|
* and `onGone` runs after that one is deleted — the room moves to a new chat —
|
|
32
|
-
* defaulting to emptying the room.
|
|
32
|
+
* defaulting to emptying the room. `onSchedule`, where the list's host can say
|
|
33
|
+
* what the conversation asks, adds Schedule…: the conversation's question as a
|
|
34
|
+
* task that runs itself.
|
|
33
35
|
*/
|
|
34
|
-
export declare function ConversationMenu({ threads, active, onGone, children, }: {
|
|
36
|
+
export declare function ConversationMenu({ threads, active, onGone, onSchedule, children, }: {
|
|
35
37
|
threads: Map<string, Thread>;
|
|
36
38
|
active?: string | null;
|
|
37
39
|
onGone?: () => void;
|
|
40
|
+
onSchedule?: (t: Thread) => void;
|
|
38
41
|
children: ReactElement;
|
|
39
42
|
}): import("react").JSX.Element;
|
|
40
43
|
/**
|
package/dist/thread.js
CHANGED
|
@@ -22,7 +22,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
22
22
|
// device they sign in on.
|
|
23
23
|
import { createContext, useCallback, useContext, useEffect, useId, useMemo, useRef, useState, useSyncExternalStore, } from 'react';
|
|
24
24
|
import { useAi } from '@hanzo/ai/react';
|
|
25
|
-
import { Archive, ArchiveRestore, ExternalLink, Link2, MoreHorizontal, Pencil, Pin, PinOff, Share2, Trash2 } from 'lucide-react';
|
|
25
|
+
import { Archive, ArchiveRestore, Clock, ExternalLink, Link2, MoreHorizontal, Pencil, Pin, PinOff, Share2, Trash2 } from 'lucide-react';
|
|
26
26
|
import { Button, ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuTrigger, Dialog, DialogContent, DialogTitle, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuTrigger, Input, SizableText, Text, XStack, YStack, toast, } from '@hanzo/ui';
|
|
27
27
|
import { empty } from './open.js';
|
|
28
28
|
import { say } from './lib/org.js';
|
|
@@ -182,9 +182,11 @@ const own = (row) => row?.querySelector('[data-slot="sidebar-item"], [role="butt
|
|
|
182
182
|
* child holds the rows (`ThreadRow`). `threads` are the list's conversations by
|
|
183
183
|
* id, `active` the one the pane holds (what the keys act on with no menu open),
|
|
184
184
|
* and `onGone` runs after that one is deleted — the room moves to a new chat —
|
|
185
|
-
* defaulting to emptying the room.
|
|
185
|
+
* defaulting to emptying the room. `onSchedule`, where the list's host can say
|
|
186
|
+
* what the conversation asks, adds Schedule…: the conversation's question as a
|
|
187
|
+
* task that runs itself.
|
|
186
188
|
*/
|
|
187
|
-
export function ConversationMenu({ threads, active = null, onGone = empty, children, }) {
|
|
189
|
+
export function ConversationMenu({ threads, active = null, onGone = empty, onSchedule, children, }) {
|
|
188
190
|
const act = useThreadActions();
|
|
189
191
|
// The conversation the pane holds NOW: a delete answered after the reader moved on leaves the room they moved to.
|
|
190
192
|
const holds = useRef(active);
|
|
@@ -280,6 +282,7 @@ export function ConversationMenu({ threads, active = null, onGone = empty, child
|
|
|
280
282
|
run: () => void act.pin(subject),
|
|
281
283
|
},
|
|
282
284
|
{ key: 'share', label: 'Share…', icon: _jsx(Share2, { size: 16, "aria-hidden": true }), run: () => ask('share', subject) },
|
|
285
|
+
...(onSchedule ? [{ key: 'schedule', label: 'Schedule…', icon: _jsx(Clock, { size: 16, "aria-hidden": true }), run: () => onSchedule(subject) }] : []),
|
|
283
286
|
{ key: 'copy', label: 'Copy link', icon: _jsx(Link2, { size: 16, "aria-hidden": true }), run: () => void copy(subject.id) },
|
|
284
287
|
{ key: 'tab', label: 'Open in new tab', icon: _jsx(ExternalLink, { size: 16, "aria-hidden": true }), run: () => away(subject.id) },
|
|
285
288
|
{
|
package/dist/where.d.ts
CHANGED
|
@@ -33,6 +33,12 @@ export interface Addresses {
|
|
|
33
33
|
brand?: string;
|
|
34
34
|
/** The publishable key anonymous calls carry. */
|
|
35
35
|
key?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Where the IAM session is kept when it is not this page's `localStorage`:
|
|
38
|
+
* the `storage` the host hands its IamProvider (Hanzo Desktop's keychain).
|
|
39
|
+
* The rooms read the access token, its expiry and its claims from it.
|
|
40
|
+
*/
|
|
41
|
+
session?: Storage;
|
|
36
42
|
/** The plan catalogue as the host's build last read it: the first paint. */
|
|
37
43
|
plans?: BillingPlan[];
|
|
38
44
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanzo/rooms",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.71",
|
|
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",
|