@hanzo/rooms 0.1.33 → 0.1.34
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 +125 -15
- package/dist/RightPane.d.ts +35 -0
- package/dist/RightPane.js +14 -8
- package/dist/pane.d.ts +2 -0
- package/dist/pane.js +12 -1
- package/package.json +1 -1
package/dist/Chat.js
CHANGED
|
@@ -34,8 +34,8 @@ import { useAgent, useAi, useChat, useModels, } from "@hanzo/ai/react";
|
|
|
34
34
|
import { freeCopy, grantConsent, hasConsent, isFree, } from "@hanzo/ai";
|
|
35
35
|
import { ASK, Chat as ChatSurface, Code, Failure, Parts, words } from "@hanzo/ui/chat";
|
|
36
36
|
import { carriedIn, compose, partsOf, said as told, weigh } from "./lib/attach.js";
|
|
37
|
-
import { dropped, pct, readable, refOf, retrieve } from "./lib/files.js";
|
|
38
|
-
import { channel, drop, hold, landed, spend, take, usePane, useWorkFile } from "./pane.js";
|
|
37
|
+
import { download, dropped, fileOf, pct, readable, refOf, retrieve } from "./lib/files.js";
|
|
38
|
+
import { channel, drop, hold, landed, localOf, spend, take, usePane, useWorkFile } from "./pane.js";
|
|
39
39
|
import { Face, brief as roomBrief, join, roleOf, roomTurn, roster as rosterLine, rosterOf, speakers, speaksOf, voiceOf } from "./cast.js";
|
|
40
40
|
import { SpeakButton, cleanForSpeech } from "./speech.js";
|
|
41
41
|
import { Crew, HOUSE, useCrew } from "./crew.js";
|
|
@@ -52,12 +52,13 @@ import { useHydrated } from "./lib/hydrated.js";
|
|
|
52
52
|
import { base, served } from "./lib/ai.js";
|
|
53
53
|
import { onServed } from "./lib/served.js";
|
|
54
54
|
import { refused as worded, speech, useDictation, useTalk, useVoice, Voice } from "@hanzo/voice";
|
|
55
|
-
import { ArrowUp, AudioLines, Image as ImageMark, Mic, Paperclip, Square, Star, PanelRight, X } from "lucide-react";
|
|
55
|
+
import { ArrowUp, AudioLines, FileText, Image as ImageMark, Mic, Paperclip, Square, Star, PanelRight, X } from "lucide-react";
|
|
56
56
|
import { ENSO, FREE } from "./lib/ai.js";
|
|
57
57
|
import { openThread, useOpen } from "./open.js";
|
|
58
58
|
import { Beside, Framed } from "./Shell.js";
|
|
59
59
|
import { pane } from "./ground.js";
|
|
60
|
-
import { RightPane, usePinned } from "./RightPane.js";
|
|
60
|
+
import { RightPane, addressOf, look, usePinned } from "./RightPane.js";
|
|
61
|
+
import { useMedia } from "@hanzo/gui";
|
|
61
62
|
import { Take } from "./copy.js";
|
|
62
63
|
import { Share } from "./Share.js";
|
|
63
64
|
import { useStarred } from "./stars.js";
|
|
@@ -351,27 +352,113 @@ const STAGE = {
|
|
|
351
352
|
graph: "linking",
|
|
352
353
|
embed: "vectors",
|
|
353
354
|
};
|
|
354
|
-
/**
|
|
355
|
-
function
|
|
355
|
+
/** How a file is drawn: a picture, a PDF's tile, or a chip. Read off its type, else its name, since a store may answer with neither. */
|
|
356
|
+
function kindOf(name, type) {
|
|
357
|
+
if (type.startsWith("image/") || /\.(png|jpe?g|gif|webp|avif|svg)$/i.test(name))
|
|
358
|
+
return "image";
|
|
359
|
+
if (type === "application/pdf" || /\.pdf$/i.test(name))
|
|
360
|
+
return "pdf";
|
|
361
|
+
return "file";
|
|
362
|
+
}
|
|
363
|
+
// SIGNED ADDRESSES FOR PICTURES, by workspace file: a list of turns mounts and
|
|
364
|
+
// remounts its chips, and asking the store each time would be a request per
|
|
365
|
+
// picture per redraw. Kept for less than a signature lives (the store's are
|
|
366
|
+
// five minutes), so a chip mounted later reads a fresh one; a picture already
|
|
367
|
+
// drawn keeps its pixels whatever becomes of its address.
|
|
368
|
+
const signed = new Map();
|
|
369
|
+
const SIGNED_FOR = 4 * 60_000;
|
|
370
|
+
function signedOf(api, id, file) {
|
|
371
|
+
const kept = signed.get(id);
|
|
372
|
+
if (kept && Date.now() - kept.at < SIGNED_FOR)
|
|
373
|
+
return kept.got;
|
|
374
|
+
const got = (file?.bucket && file.key ? Promise.resolve(file) : fileOf(api, id)).then((f) => download(api, f.bucket, f.key));
|
|
375
|
+
signed.set(id, { at: Date.now(), got });
|
|
376
|
+
got.catch(() => signed.delete(id));
|
|
377
|
+
return got;
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Where a picture's thumbnail reads from: the bytes on this machine where this
|
|
381
|
+
* document holds them (the held file's object URL, which outlives its send),
|
|
382
|
+
* else a signed address for the workspace's copy — a file taken from Drive, or
|
|
383
|
+
* a turn read back after a reload. '' until there is one.
|
|
384
|
+
*/
|
|
385
|
+
function usePicture(on, href, id, file) {
|
|
386
|
+
const api = useAi();
|
|
387
|
+
const near = on ? href || (id ? localOf(id) : "") : "";
|
|
388
|
+
const [far, setFar] = useState("");
|
|
389
|
+
const bucket = file?.bucket;
|
|
390
|
+
useEffect(() => {
|
|
391
|
+
if (!on || near || !id)
|
|
392
|
+
return;
|
|
393
|
+
let live = true;
|
|
394
|
+
signedOf(api, id, file).then((url) => live && setFar(url), () => { });
|
|
395
|
+
return () => {
|
|
396
|
+
live = false;
|
|
397
|
+
};
|
|
398
|
+
// The file's record moves with every stage of its ingest; its address does not.
|
|
399
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
400
|
+
}, [on, near, id, bucket, api]);
|
|
401
|
+
return near || far;
|
|
402
|
+
}
|
|
403
|
+
/** Edge of a picture's or a PDF's thumbnail, in px. */
|
|
404
|
+
const THUMB = 48;
|
|
405
|
+
/**
|
|
406
|
+
* One file put into a message.
|
|
407
|
+
*
|
|
408
|
+
* A PICTURE SHOWS ITSELF and a PDF shows a tile, each with its name and size
|
|
409
|
+
* beside it; any other kind is a chip of its name. Nothing is filled behind
|
|
410
|
+
* them: a hairline on whatever they sit on, so a file reads alike over the
|
|
411
|
+
* composer and inside a turn's bubble, in a light room and a dark one. A second
|
|
412
|
+
* fill on the bubble's was the darker grey that read as a stain. The word for
|
|
413
|
+
* how far a file has got shows only while it is getting there — a picture of
|
|
414
|
+
* the file already says it is attached — and `data-state` keeps the index's own
|
|
415
|
+
* word for whoever reads it.
|
|
416
|
+
*
|
|
417
|
+
* Pressing one opens it beside the conversation (`onOpen`).
|
|
418
|
+
*/
|
|
419
|
+
function Chip({ name, type, size, state, sent, file, picture, onOpen, onRemove, }) {
|
|
420
|
+
const kind = kindOf(name, type);
|
|
356
421
|
const bad = state === "failed";
|
|
357
422
|
const why = state === "failed" || state === "stored" ? file?.error : file?.note;
|
|
358
|
-
|
|
423
|
+
// A picture the browser cannot draw (HEIC, a lapsed signature) falls back to its glyph.
|
|
424
|
+
const [broken, setBroken] = useState("");
|
|
425
|
+
const word = state === "ready" ? (file?.clipped ? "in part" : "") : progress(state, sent, file);
|
|
426
|
+
const meta = [kind === "pdf" ? "PDF" : "", size > 0 ? weigh(size) : "", word].filter(Boolean).join(" \u00b7 ");
|
|
427
|
+
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 marks = {
|
|
429
|
+
role: "listitem",
|
|
430
|
+
"data-slot": "file-chip",
|
|
431
|
+
"data-state": state,
|
|
432
|
+
"data-kind": kind,
|
|
433
|
+
"data-stage": file?.stage ?? "",
|
|
434
|
+
...(why ? { title: why } : null),
|
|
435
|
+
};
|
|
436
|
+
if (kind === "file")
|
|
437
|
+
return (_jsxs(XStack, { ...marks, self: "center", items: "center", gap: "$1", pr: onRemove ? "$1" : "$2.5", rounded: "$10", borderWidth: 1, borderColor: "$borderColor", maxW: 320, children: [_jsxs(XStack, { render: "button", "aria-label": `Open ${name}`, onClick: onOpen, items: "center", gap: "$1.5", pl: "$2.5", py: "$1", minW: 0, cursor: "pointer", children: [_jsx(Paperclip, { size: 12, "aria-hidden": true, color: "var(--muted-foreground)" }), _jsx(Text, { fontSize: "$2", color: "$ink", numberOfLines: 1, children: name }), meta ? (_jsx(Text, { fontSize: "$1", color: bad ? BAD : "$soft", numberOfLines: 1, shrink: 0, children: meta })) : null] }), remove] }));
|
|
438
|
+
const drawn = kind === "image" && picture && broken !== picture;
|
|
439
|
+
const Glyph = kind === "pdf" ? FileText : ImageMark;
|
|
440
|
+
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] }));
|
|
359
441
|
}
|
|
360
442
|
/** A file held for the next message: uploading, then indexing, then ready. */
|
|
361
|
-
function HeldChip({ one, onRemove }) {
|
|
443
|
+
function HeldChip({ one, onOpen, onRemove }) {
|
|
362
444
|
const live = useWorkFile(one.file?.id);
|
|
363
445
|
const file = live ?? one.file;
|
|
364
446
|
const state = one.state === "uploading" ? one.state : (file?.status ?? one.state);
|
|
365
|
-
|
|
447
|
+
const picture = usePicture(kindOf(one.name, one.type) === "image", one.href, file?.id, file);
|
|
448
|
+
// The mark the column's Sources row opens for the same file, so both open it alike.
|
|
449
|
+
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 };
|
|
450
|
+
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 }));
|
|
366
451
|
}
|
|
367
452
|
/** A file a sent turn carried, as the index says of it now. */
|
|
368
|
-
function TurnChip({ f }) {
|
|
453
|
+
function TurnChip({ f, onOpen }) {
|
|
369
454
|
const live = useWorkFile(f.id);
|
|
370
|
-
|
|
455
|
+
const picture = usePicture(kindOf(f.name, f.type) === "image", "", f.id, live);
|
|
456
|
+
const mark = { id: f.id, name: f.name, kind: kindOf(f.name, f.type) === "image" ? "image" : "file", file: f.id, mime: f.type };
|
|
457
|
+
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) });
|
|
371
458
|
}
|
|
372
459
|
/** A question that carried files: its words, the files put into it, and those read again. */
|
|
373
|
-
function Carried({ text, attached, reused }) {
|
|
374
|
-
return (_jsxs(YStack, { gap: "$
|
|
460
|
+
function Carried({ text, attached, reused, onOpen }) {
|
|
461
|
+
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] }));
|
|
375
462
|
}
|
|
376
463
|
/**
|
|
377
464
|
* The conversation. One component for a visitor and for an account.
|
|
@@ -420,6 +507,29 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
|
|
|
420
507
|
// switch for it. Kept per conversation by `pane.ts`, so the header's control
|
|
421
508
|
// reflects THIS room rather than the last one that was open.
|
|
422
509
|
const [pinned, flipPin] = usePinned();
|
|
510
|
+
// The frame draws the column only from `$lg` (Shell.tsx `Paned`).
|
|
511
|
+
const wide = useMedia().lg;
|
|
512
|
+
/**
|
|
513
|
+
* OPENS A FILE A MESSAGE CARRIES, by the road the column's Sources row takes
|
|
514
|
+
* (`look`): a tab in the browser beside the conversation, the column opened
|
|
515
|
+
* first when it is shut. A phone has no column — the frame spends the width
|
|
516
|
+
* on the room — so there the file opens in a tab of the browser's own, minted
|
|
517
|
+
* before the signed address is asked for, since a window opened after an
|
|
518
|
+
* await is a popup the browser blocks.
|
|
519
|
+
*/
|
|
520
|
+
const openFile = useCallback((m) => {
|
|
521
|
+
if (wide) {
|
|
522
|
+
if (aside === null)
|
|
523
|
+
showAside("tasks");
|
|
524
|
+
void look(at, m, ai);
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
const tab = window.open("", "_blank");
|
|
528
|
+
if (!tab)
|
|
529
|
+
return;
|
|
530
|
+
tab.opener = null;
|
|
531
|
+
void addressOf(m, ai).then((href) => (href ? tab.location.replace(href) : tab.close()));
|
|
532
|
+
}, [wide, aside, showAside, at, ai]);
|
|
423
533
|
/**
|
|
424
534
|
* WHO YOU ARE TALKING TO, when it is somebody rather than the plain model.
|
|
425
535
|
*
|
|
@@ -1132,7 +1242,7 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
|
|
|
1132
1242
|
if (turn.role === "user" && said) {
|
|
1133
1243
|
const read = told(said);
|
|
1134
1244
|
if (read.carried)
|
|
1135
|
-
return _jsx(Carried, { text: read.text, attached: read.carried.attached, reused: read.carried.reused });
|
|
1245
|
+
return _jsx(Carried, { text: read.text, attached: read.carried.attached, reused: read.carried.reused, onOpen: openFile });
|
|
1136
1246
|
}
|
|
1137
1247
|
// WHILE THE ANSWER HAS NO WORDS YET: who is answering (the agents the
|
|
1138
1248
|
// question addresses, or the whole room when it names nobody), and the
|
|
@@ -1238,7 +1348,7 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
|
|
|
1238
1348
|
},
|
|
1239
1349
|
// WHAT GOES WITH THE MESSAGE, above the frame: one chip a file, each
|
|
1240
1350
|
// with its own remove, and the reason any file was refused.
|
|
1241
|
-
head: files.length || turnedAway || preparing || talk.open || talk.refusal || pause ? (_jsxs(YStack, { width: "calc(100% - 32px)", maxW: `calc(${MEASURE} - 1.5rem)`, mx: "auto", gap: "$1.5", children: [pause ? (_jsx(LimitedBanner, { message: pause.message, actions: pause.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, 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
|
|
1351
|
+
head: files.length || turnedAway || preparing || talk.open || talk.refusal || pause ? (_jsxs(YStack, { width: "calc(100% - 32px)", maxW: `calc(${MEASURE} - 1.5rem)`, mx: "auto", gap: "$1.5", children: [pause ? (_jsx(LimitedBanner, { message: pause.message, actions: pause.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
|
|
1242
1352
|
? worded(talk.refusal)
|
|
1243
1353
|
: talk.reply
|
|
1244
1354
|
? `Hanzo: ${talk.reply}`
|
package/dist/RightPane.d.ts
CHANGED
|
@@ -1,4 +1,23 @@
|
|
|
1
1
|
import { type ChatMessage } from '@hanzo/ai/react';
|
|
2
|
+
import { type Api } from './lib/files.js';
|
|
3
|
+
/**
|
|
4
|
+
* One thing the conversation carries.
|
|
5
|
+
*
|
|
6
|
+
* `href` is where it can be opened and `body` is the text of something that was
|
|
7
|
+
* never a file — a fenced block exists only in the transcript until somebody
|
|
8
|
+
* asks to look at it, at which point `body` becomes a blob and gets an address.
|
|
9
|
+
*/
|
|
10
|
+
export interface Mark {
|
|
11
|
+
id: string;
|
|
12
|
+
name: string;
|
|
13
|
+
kind: 'image' | 'code' | 'file';
|
|
14
|
+
href?: string;
|
|
15
|
+
body?: string;
|
|
16
|
+
/** The type a blob should carry, so a browser renders it rather than downloads it. */
|
|
17
|
+
mime?: string;
|
|
18
|
+
/** A workspace file's id: it opens through a signed address minted when it is looked at. */
|
|
19
|
+
file?: string;
|
|
20
|
+
}
|
|
2
21
|
/**
|
|
3
22
|
* The column. `said` is the conversation it stands beside; without one it draws
|
|
4
23
|
* the same shape with nothing in it, which is what a room nobody has spoken in
|
|
@@ -8,6 +27,22 @@ export declare function RightPane({ said, onClose, }: {
|
|
|
8
27
|
said?: ChatMessage[];
|
|
9
28
|
onClose?: () => void;
|
|
10
29
|
}): import("react").JSX.Element;
|
|
30
|
+
/**
|
|
31
|
+
* Opens a mark in this conversation's browser.
|
|
32
|
+
*
|
|
33
|
+
* A BLOCK BECOMES A FILE THE MOMENT SOMEBODY LOOKS AT IT. It has no address
|
|
34
|
+
* until then — it is text inside a turn — so one is minted here from its own
|
|
35
|
+
* bytes, carrying the type its fence declared. That is what makes an answer's
|
|
36
|
+
* `index.html` something you can actually see rather than something you can
|
|
37
|
+
* only read the source of.
|
|
38
|
+
*/
|
|
39
|
+
export declare function look(at: string, m: Mark, api: Api): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Where a mark can be opened: its own address, a signed one minted for its
|
|
42
|
+
* workspace file, or one minted from its text. Null when there is none — a file
|
|
43
|
+
* forgotten or a store unreachable opens nothing, and the row stays.
|
|
44
|
+
*/
|
|
45
|
+
export declare function addressOf(m: Mark, api: Api): Promise<string | null>;
|
|
11
46
|
/** Shows or hides the summary above this conversation's browser. Exported for
|
|
12
47
|
* the conversation header, which is where the control belongs. */
|
|
13
48
|
export declare function usePinned(): [boolean, () => void];
|
package/dist/RightPane.js
CHANGED
|
@@ -210,21 +210,27 @@ function Marks({ at, kind, title, icon, marks, children, }) {
|
|
|
210
210
|
* `index.html` something you can actually see rather than something you can
|
|
211
211
|
* only read the source of.
|
|
212
212
|
*/
|
|
213
|
-
async function look(at, m, api) {
|
|
213
|
+
export async function look(at, m, api) {
|
|
214
|
+
const href = await addressOf(m, api);
|
|
215
|
+
if (href)
|
|
216
|
+
openTab(at, href, m.name);
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Where a mark can be opened: its own address, a signed one minted for its
|
|
220
|
+
* workspace file, or one minted from its text. Null when there is none — a file
|
|
221
|
+
* forgotten or a store unreachable opens nothing, and the row stays.
|
|
222
|
+
*/
|
|
223
|
+
export async function addressOf(m, api) {
|
|
214
224
|
if (!m.href && m.file) {
|
|
215
225
|
try {
|
|
216
226
|
const f = await fileOf(api, m.file);
|
|
217
|
-
|
|
227
|
+
return await download(api, f.bucket, f.key);
|
|
218
228
|
}
|
|
219
229
|
catch {
|
|
220
|
-
|
|
230
|
+
return null;
|
|
221
231
|
}
|
|
222
|
-
return;
|
|
223
232
|
}
|
|
224
|
-
|
|
225
|
-
if (!href)
|
|
226
|
-
return;
|
|
227
|
-
openTab(at, href, m.name);
|
|
233
|
+
return m.href ?? (m.body === undefined ? null : URL.createObjectURL(new Blob([m.body], { type: m.mime })));
|
|
228
234
|
}
|
|
229
235
|
/**
|
|
230
236
|
* A menu row's words.
|
package/dist/pane.d.ts
CHANGED
|
@@ -103,6 +103,8 @@ export declare function shutTab(key: string, id: string): void;
|
|
|
103
103
|
export declare function pin(key: string, on: boolean): void;
|
|
104
104
|
/** The workspace file `id` as the index last said, or undefined until it has. */
|
|
105
105
|
export declare function useWorkFile(id: string | undefined): WorkFile | undefined;
|
|
106
|
+
/** The object URL over workspace file `id`'s bytes on this machine, or '' where this document never held them. */
|
|
107
|
+
export declare const localOf: (id: string) => string;
|
|
106
108
|
/**
|
|
107
109
|
* Takes files into this conversation: each goes straight to the org's
|
|
108
110
|
* workspace — in parts when it is large, several at once — is registered with
|
package/dist/pane.js
CHANGED
|
@@ -229,6 +229,13 @@ export function useWorkFile(id) {
|
|
|
229
229
|
};
|
|
230
230
|
}, () => (id ? files.get(id) : undefined), () => undefined);
|
|
231
231
|
}
|
|
232
|
+
// THE BYTES ON THIS MACHINE, by the workspace file they became. `spend` leaves a
|
|
233
|
+
// held file's object URL standing, so a turn that carried a picture draws it
|
|
234
|
+
// from here with no request, for as long as this document lives. A reload ends
|
|
235
|
+
// it, and the turn reads the workspace's copy instead.
|
|
236
|
+
const local = new Map();
|
|
237
|
+
/** The object URL over workspace file `id`'s bytes on this machine, or '' where this document never held them. */
|
|
238
|
+
export const localOf = (id) => local.get(id) ?? '';
|
|
232
239
|
// Each held file's upload, by held id: what stops it, and what it settles to.
|
|
233
240
|
const carrying = new Map();
|
|
234
241
|
/** How many files go up at once; each large one also sends several parts at once. */
|
|
@@ -294,6 +301,7 @@ export async function hold(key, list, via) {
|
|
|
294
301
|
const objectKey = chatKey(file);
|
|
295
302
|
await upload(via.api, bucket, objectKey, file, (sent) => patch(key, h.id, { sent }), stop.signal);
|
|
296
303
|
const got = await register(via.api, bucket, objectKey);
|
|
304
|
+
local.set(got.id, h.href);
|
|
297
305
|
patch(key, h.id, fromFile(got));
|
|
298
306
|
watch(via.api, got, key, h.id);
|
|
299
307
|
land(got);
|
|
@@ -363,8 +371,11 @@ export function drop(key, id) {
|
|
|
363
371
|
return was;
|
|
364
372
|
carrying.get(id)?.stop.abort();
|
|
365
373
|
carrying.delete(id);
|
|
366
|
-
if (going.href)
|
|
374
|
+
if (going.href) {
|
|
375
|
+
if (going.file)
|
|
376
|
+
local.delete(going.file.id);
|
|
367
377
|
URL.revokeObjectURL(going.href);
|
|
378
|
+
}
|
|
368
379
|
return { ...was, held: was.held.filter((h) => h.id !== id) };
|
|
369
380
|
});
|
|
370
381
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanzo/rooms",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.34",
|
|
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",
|