@hanzo/rooms 0.1.10 → 0.1.12
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 +174 -32
- package/dist/Contents.d.ts +23 -0
- package/dist/Contents.js +84 -0
- package/dist/Drive.js +131 -39
- package/dist/Meet.js +26 -7
- package/dist/RightPane.d.ts +1 -1
- package/dist/RightPane.js +26 -5
- package/dist/lib/attach.d.ts +40 -33
- package/dist/lib/attach.js +93 -58
- package/dist/lib/files.d.ts +162 -0
- package/dist/lib/files.js +289 -0
- package/dist/pane.d.ts +39 -17
- package/dist/pane.js +125 -29
- package/package.json +3 -3
package/dist/pane.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type Api, type WorkFile } from './lib/files.js';
|
|
2
2
|
/** One open tab in the pane's browser. */
|
|
3
3
|
export interface Leaf {
|
|
4
4
|
id: string;
|
|
@@ -8,20 +8,34 @@ export interface Leaf {
|
|
|
8
8
|
title: string;
|
|
9
9
|
}
|
|
10
10
|
/**
|
|
11
|
-
* A file
|
|
12
|
-
* waiting to go with the next message.
|
|
11
|
+
* A file put into this conversation, on its way to the workspace or already
|
|
12
|
+
* there, waiting to go with the next message.
|
|
13
13
|
*
|
|
14
14
|
* `href` is an object URL and is the reason this is never written down — see
|
|
15
|
-
* the note at the top.
|
|
16
|
-
*
|
|
15
|
+
* the note at the top. A name and a size are the file system's, or the
|
|
16
|
+
* workspace file's, never a guess.
|
|
17
17
|
*/
|
|
18
|
-
export interface Held
|
|
18
|
+
export interface Held {
|
|
19
19
|
id: string;
|
|
20
20
|
name: string;
|
|
21
21
|
size: number;
|
|
22
22
|
/** The browser's type for it, or '' where it could not tell from the name. */
|
|
23
23
|
type: string;
|
|
24
|
+
/** An object URL over the bytes on this machine; '' for a file taken from Drive. */
|
|
24
25
|
href: string;
|
|
26
|
+
/** Sending its bytes, then the index's own word for how far it has got. */
|
|
27
|
+
state: 'uploading' | WorkFile['status'];
|
|
28
|
+
/** The fraction of the bytes the store holds, while uploading. */
|
|
29
|
+
sent: number;
|
|
30
|
+
/** The workspace file, once the upload landed and was registered. */
|
|
31
|
+
file?: WorkFile;
|
|
32
|
+
/** Why it failed, in the reader's words. */
|
|
33
|
+
error?: string;
|
|
34
|
+
}
|
|
35
|
+
/** What carries a file: the client that reaches the platform, and the org whose workspace it lands in. */
|
|
36
|
+
export interface Via {
|
|
37
|
+
api: Api;
|
|
38
|
+
org: string | null;
|
|
25
39
|
}
|
|
26
40
|
/** Everything the column beside one conversation is holding. */
|
|
27
41
|
export interface Pane {
|
|
@@ -87,14 +101,22 @@ export declare function pickTab(key: string, id: string): void;
|
|
|
87
101
|
export declare function shutTab(key: string, id: string): void;
|
|
88
102
|
/** Shows or hides the summary column beside this conversation's browser. */
|
|
89
103
|
export declare function pin(key: string, on: boolean): void;
|
|
104
|
+
/** The workspace file `id` as the index last said, or undefined until it has. */
|
|
105
|
+
export declare function useWorkFile(id: string | undefined): WorkFile | undefined;
|
|
90
106
|
/**
|
|
91
|
-
* Takes files
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
|
|
107
|
+
* Takes files into this conversation: each goes straight to the org's
|
|
108
|
+
* workspace — in parts when it is large, several at once — is registered with
|
|
109
|
+
* the index, and is followed through its stages. Nothing is refused for its
|
|
110
|
+
* size or its kind: a file the index cannot read is still kept, and says why.
|
|
111
|
+
*/
|
|
112
|
+
export declare function hold(key: string, list: readonly File[], via: Via): Promise<void>;
|
|
113
|
+
/**
|
|
114
|
+
* Puts files already in the workspace into this conversation — Drive's "Ask in
|
|
115
|
+
* chat". Nothing is uploaded; each is followed if its ingest has not settled.
|
|
96
116
|
*/
|
|
97
|
-
export declare function
|
|
117
|
+
export declare function attach(key: string, list: readonly WorkFile[], via: Via): void;
|
|
118
|
+
/** The workspace file a held file became, once its upload settles; null when it did not. */
|
|
119
|
+
export declare function landed(h: Held): Promise<WorkFile | null>;
|
|
98
120
|
/**
|
|
99
121
|
* Asks for files and holds what is given.
|
|
100
122
|
*
|
|
@@ -103,16 +125,16 @@ export declare function hold(key: string, files: readonly File[]): Promise<void>
|
|
|
103
125
|
* `<input type=file>` parked in the tree is one more thing for a screen reader
|
|
104
126
|
* to walk past.
|
|
105
127
|
*/
|
|
106
|
-
export declare function take(key: string): void;
|
|
128
|
+
export declare function take(key: string, via: Via): void;
|
|
107
129
|
/**
|
|
108
130
|
* Hands over everything held to go with a message, and lets go of it.
|
|
109
131
|
*
|
|
110
|
-
* The turn carries each file from here
|
|
111
|
-
*
|
|
112
|
-
*
|
|
132
|
+
* The turn carries each file by reference from here. The object URLs are left
|
|
133
|
+
* standing, so a tab already open on one keeps showing it, and an upload still
|
|
134
|
+
* running keeps running: the turn names the file it lands as.
|
|
113
135
|
*/
|
|
114
136
|
export declare function spend(key: string): Held[];
|
|
115
|
-
/** Lets go of a file, and
|
|
137
|
+
/** Lets go of a file: stops its upload if it is still going, and drops the handle the browser minted. */
|
|
116
138
|
export declare function drop(key: string, id: string): void;
|
|
117
139
|
/** This conversation's pane, and it alone — a change to another redraws nothing
|
|
118
140
|
* here, because the snapshot returned is that key's own object. */
|
package/dist/pane.js
CHANGED
|
@@ -22,12 +22,13 @@
|
|
|
22
22
|
// worse than no row, so they are held for the session and no longer.
|
|
23
23
|
//
|
|
24
24
|
// ONE WAY IN FOR A FILE. The composer's paperclip, a drop on the room, a paste
|
|
25
|
-
// into the field and the column's `+` all call `hold`, so a file
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
25
|
+
// into the field and the column's `+` all call `hold`, so a file goes up by one
|
|
26
|
+
// road (`lib/files`: straight to the org's workspace, in parts when it is
|
|
27
|
+
// large), shows as a chip over the field and a row under Sources at once, and
|
|
28
|
+
// goes with the next message by reference. Sending spends what is held; from
|
|
29
|
+
// then on the turn itself is the record Sources reads.
|
|
29
30
|
import { useSyncExternalStore } from 'react';
|
|
30
|
-
import {
|
|
31
|
+
import { chatKey, follow, pool, register, settled, upload, workspace } from './lib/files.js';
|
|
31
32
|
/** A pane nobody has touched. Shared, and never mutated — see `EMPTY` in
|
|
32
33
|
* `open.ts` for why a fresh object per read is an infinite render. */
|
|
33
34
|
const BARE = { tabs: [], at: null, pinned: true, held: [], refused: null };
|
|
@@ -209,27 +210,119 @@ export function shutTab(key, id) {
|
|
|
209
210
|
export function pin(key, on) {
|
|
210
211
|
edit(key, (was) => (was.pinned === on ? was : { ...was, pinned: on }));
|
|
211
212
|
}
|
|
213
|
+
// THE WORKSPACE FILES THIS DOCUMENT HAS SEEN, by id, as the index last said.
|
|
214
|
+
// A chip over the field and a chip in a sent turn read one record, so a file
|
|
215
|
+
// that finishes indexing after the message went shows ready in both.
|
|
216
|
+
const files = new Map();
|
|
217
|
+
const fileListeners = new Set();
|
|
218
|
+
function know(f) {
|
|
219
|
+
files.set(f.id, f);
|
|
220
|
+
for (const l of fileListeners)
|
|
221
|
+
l();
|
|
222
|
+
}
|
|
223
|
+
/** The workspace file `id` as the index last said, or undefined until it has. */
|
|
224
|
+
export function useWorkFile(id) {
|
|
225
|
+
return useSyncExternalStore((l) => {
|
|
226
|
+
fileListeners.add(l);
|
|
227
|
+
return () => {
|
|
228
|
+
fileListeners.delete(l);
|
|
229
|
+
};
|
|
230
|
+
}, () => (id ? files.get(id) : undefined), () => undefined);
|
|
231
|
+
}
|
|
232
|
+
// Each held file's upload, by held id: what stops it, and what it settles to.
|
|
233
|
+
const carrying = new Map();
|
|
234
|
+
/** How many files go up at once; each large one also sends several parts at once. */
|
|
235
|
+
const FILES_AT_ONCE = 3;
|
|
236
|
+
function patch(key, id, fields) {
|
|
237
|
+
edit(key, (was) => {
|
|
238
|
+
const at = was.held.findIndex((h) => h.id === id);
|
|
239
|
+
if (at < 0)
|
|
240
|
+
return was;
|
|
241
|
+
const held = was.held.slice();
|
|
242
|
+
held[at] = { ...held[at], ...fields };
|
|
243
|
+
return { ...was, held };
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
const fromFile = (f) => ({ state: f.status, sent: 1, file: f, error: f.status === 'failed' || f.status === 'stored' ? f.error : undefined });
|
|
247
|
+
/** Watches a workspace file until its ingest settles, keeping every chip that names it current. */
|
|
248
|
+
function watch(api, f, key, id) {
|
|
249
|
+
know(f);
|
|
250
|
+
if (settled(f))
|
|
251
|
+
return;
|
|
252
|
+
void follow(api, f.id, (seen) => {
|
|
253
|
+
know(seen);
|
|
254
|
+
if (key && id)
|
|
255
|
+
patch(key, id, fromFile(seen));
|
|
256
|
+
}).catch(() => { });
|
|
257
|
+
}
|
|
212
258
|
/**
|
|
213
|
-
* Takes files
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
* cannot go.
|
|
259
|
+
* Takes files into this conversation: each goes straight to the org's
|
|
260
|
+
* workspace — in parts when it is large, several at once — is registered with
|
|
261
|
+
* the index, and is followed through its stages. Nothing is refused for its
|
|
262
|
+
* size or its kind: a file the index cannot read is still kept, and says why.
|
|
218
263
|
*/
|
|
219
|
-
export async function hold(key,
|
|
220
|
-
if (!
|
|
264
|
+
export async function hold(key, list, via) {
|
|
265
|
+
if (!list.length)
|
|
266
|
+
return;
|
|
267
|
+
if (!via.org) {
|
|
268
|
+
edit(key, (was) => ({ ...was, refused: 'Sign in to attach files: they are kept in your workspace Drive.' }));
|
|
221
269
|
return;
|
|
222
|
-
|
|
223
|
-
const
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
270
|
+
}
|
|
271
|
+
const org = via.org;
|
|
272
|
+
const taken = list.map((file) => ({
|
|
273
|
+
id: mint('h'),
|
|
274
|
+
name: file.name,
|
|
275
|
+
size: file.size,
|
|
276
|
+
type: file.type,
|
|
277
|
+
href: URL.createObjectURL(file),
|
|
278
|
+
state: 'uploading',
|
|
279
|
+
sent: 0,
|
|
280
|
+
}));
|
|
281
|
+
edit(key, (was) => ({ ...was, held: [...was.held, ...taken], refused: null }));
|
|
282
|
+
const jobs = taken.map((h, i) => {
|
|
283
|
+
const stop = new AbortController();
|
|
284
|
+
let land;
|
|
285
|
+
const done = new Promise((r) => (land = r));
|
|
286
|
+
carrying.set(h.id, { stop, done });
|
|
287
|
+
return { h, file: list[i], stop, land };
|
|
288
|
+
});
|
|
289
|
+
await pool(jobs, FILES_AT_ONCE, async ({ h, file, stop, land }) => {
|
|
290
|
+
try {
|
|
291
|
+
if (stop.signal.aborted)
|
|
292
|
+
return land(null);
|
|
293
|
+
const bucket = await workspace(via.api, org);
|
|
294
|
+
const objectKey = chatKey(file);
|
|
295
|
+
await upload(via.api, bucket, objectKey, file, (sent) => patch(key, h.id, { sent }), stop.signal);
|
|
296
|
+
const got = await register(via.api, bucket, objectKey);
|
|
297
|
+
patch(key, h.id, fromFile(got));
|
|
298
|
+
watch(via.api, got, key, h.id);
|
|
299
|
+
land(got);
|
|
300
|
+
}
|
|
301
|
+
catch (e) {
|
|
302
|
+
if (!stop.signal.aborted)
|
|
303
|
+
patch(key, h.id, { state: 'failed', error: e?.message || `${file.name} did not upload.` });
|
|
304
|
+
land(null);
|
|
305
|
+
}
|
|
231
306
|
});
|
|
232
|
-
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Puts files already in the workspace into this conversation — Drive's "Ask in
|
|
310
|
+
* chat". Nothing is uploaded; each is followed if its ingest has not settled.
|
|
311
|
+
*/
|
|
312
|
+
export function attach(key, list, via) {
|
|
313
|
+
const taken = list.map((f) => ({ id: mint('h'), name: f.name, size: f.size, type: f.type, href: '', state: f.status, sent: 1, file: f }));
|
|
314
|
+
edit(key, (was) => ({ ...was, held: [...was.held.filter((h) => !list.some((f) => f.id === h.file?.id)), ...taken], refused: null }));
|
|
315
|
+
taken.forEach((h) => {
|
|
316
|
+
carrying.set(h.id, { stop: new AbortController(), done: Promise.resolve(h.file ?? null) });
|
|
317
|
+
if (h.file)
|
|
318
|
+
watch(via.api, h.file, key, h.id);
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
/** The workspace file a held file became, once its upload settles; null when it did not. */
|
|
322
|
+
export function landed(h) {
|
|
323
|
+
if (h.file)
|
|
324
|
+
return Promise.resolve(h.file);
|
|
325
|
+
return carrying.get(h.id)?.done ?? Promise.resolve(null);
|
|
233
326
|
}
|
|
234
327
|
/**
|
|
235
328
|
* Asks for files and holds what is given.
|
|
@@ -239,21 +332,21 @@ export async function hold(key, files) {
|
|
|
239
332
|
* `<input type=file>` parked in the tree is one more thing for a screen reader
|
|
240
333
|
* to walk past.
|
|
241
334
|
*/
|
|
242
|
-
export function take(key) {
|
|
335
|
+
export function take(key, via) {
|
|
243
336
|
if (typeof document === 'undefined')
|
|
244
337
|
return;
|
|
245
338
|
const ask = document.createElement('input');
|
|
246
339
|
ask.type = 'file';
|
|
247
340
|
ask.multiple = true;
|
|
248
|
-
ask.onchange = () => void hold(key, Array.from(ask.files ?? []));
|
|
341
|
+
ask.onchange = () => void hold(key, Array.from(ask.files ?? []), via);
|
|
249
342
|
ask.click();
|
|
250
343
|
}
|
|
251
344
|
/**
|
|
252
345
|
* Hands over everything held to go with a message, and lets go of it.
|
|
253
346
|
*
|
|
254
|
-
* The turn carries each file from here
|
|
255
|
-
*
|
|
256
|
-
*
|
|
347
|
+
* The turn carries each file by reference from here. The object URLs are left
|
|
348
|
+
* standing, so a tab already open on one keeps showing it, and an upload still
|
|
349
|
+
* running keeps running: the turn names the file it lands as.
|
|
257
350
|
*/
|
|
258
351
|
export function spend(key) {
|
|
259
352
|
const was = read(key);
|
|
@@ -262,13 +355,16 @@ export function spend(key) {
|
|
|
262
355
|
write(key, { ...was, held: [], refused: null });
|
|
263
356
|
return was.held;
|
|
264
357
|
}
|
|
265
|
-
/** Lets go of a file, and
|
|
358
|
+
/** Lets go of a file: stops its upload if it is still going, and drops the handle the browser minted. */
|
|
266
359
|
export function drop(key, id) {
|
|
267
360
|
edit(key, (was) => {
|
|
268
361
|
const going = was.held.find((h) => h.id === id);
|
|
269
362
|
if (!going)
|
|
270
363
|
return was;
|
|
271
|
-
|
|
364
|
+
carrying.get(id)?.stop.abort();
|
|
365
|
+
carrying.delete(id);
|
|
366
|
+
if (going.href)
|
|
367
|
+
URL.revokeObjectURL(going.href);
|
|
272
368
|
return { ...was, held: was.held.filter((h) => h.id !== id) };
|
|
273
369
|
});
|
|
274
370
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanzo/rooms",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.12",
|
|
4
4
|
"description": "The workspace rooms — Home, Chat, Inbox, Contacts, Meet, Cal, Drive, Board, Work, Bots — and the frame that holds them, once, for every Hanzo surface.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"@hanzo/plans": ">=1.8.9",
|
|
59
59
|
"@hanzo/ui": ">=8.27.51",
|
|
60
60
|
"@hanzo/usage": ">=0.1.17",
|
|
61
|
-
"@hanzo/voice": ">=0.1.
|
|
61
|
+
"@hanzo/voice": ">=0.1.11",
|
|
62
62
|
"@hanzogui/shell": "^8.3.20",
|
|
63
63
|
"lucide-react": ">=1.42.0",
|
|
64
64
|
"react": ">=19.2.8",
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
"@hanzo/plans": "1.8.9",
|
|
78
78
|
"@hanzo/ui": "8.27.51",
|
|
79
79
|
"@hanzo/usage": "0.1.17",
|
|
80
|
-
"@hanzo/voice": "0.1.
|
|
80
|
+
"@hanzo/voice": "0.1.11",
|
|
81
81
|
"@hanzogui/config": "8.3.5",
|
|
82
82
|
"@hanzogui/core": "8.3.5",
|
|
83
83
|
"@hanzogui/lucide-icons-2": "8.3.5",
|