@hanzo/rooms 0.1.15 → 0.1.17

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 CHANGED
@@ -34,7 +34,7 @@ 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, readable, refOf, retrieve } from "./lib/files.js";
37
+ import { dropped, pct, readable, refOf, retrieve } from "./lib/files.js";
38
38
  import { channel, drop, hold, landed, 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";
@@ -320,6 +320,11 @@ function unread(list) {
320
320
  const said = parts.filter(Boolean).join("; ");
321
321
  return said || undefined;
322
322
  }
323
+ /** The files a turn carries that are indexed less than whole, and why. */
324
+ function partly(list) {
325
+ const parts = list.filter((f) => f.note).map((f) => `${f.name}: ${f.note.replace(/\n/g, " ")}`);
326
+ return parts.length ? parts.join("; ") : undefined;
327
+ }
323
328
  /** How far a file has got, in a word or two. */
324
329
  function progress(state, sent, f) {
325
330
  if (state === "uploading")
@@ -327,27 +332,26 @@ function progress(state, sent, f) {
327
332
  if (state === "queued")
328
333
  return "queued";
329
334
  if (state === "indexing") {
330
- switch (f?.stage) {
331
- case "extract":
332
- return "reading";
333
- case "toc":
334
- return "contents";
335
- case "passages":
336
- return "passages";
337
- case "embed":
338
- return f.passages ? `indexing ${Math.floor(((f.embedded ?? 0) / f.passages) * 100)}%` : "indexing";
339
- case "graph":
340
- return "linking";
341
- default:
342
- return "indexing";
343
- }
335
+ const word = f?.stage ? STAGE[f.stage] : "indexing";
336
+ const at = f ? pct(f) : undefined;
337
+ return at === undefined ? word : `${word} ${at}%`;
344
338
  }
339
+ if (state === "ready" && f?.clipped)
340
+ return "ready, in part";
345
341
  return state;
346
342
  }
343
+ /** What each stage of a file's ingest is doing, in a word. */
344
+ const STAGE = {
345
+ extract: "reading",
346
+ toc: "contents",
347
+ passages: "passages",
348
+ graph: "linking",
349
+ embed: "vectors",
350
+ };
347
351
  /** One chip: the file's glyph, its name, and how far it has got. */
348
352
  function Chip({ name, type, state, sent, file, onRemove, }) {
349
353
  const bad = state === "failed";
350
- const why = state === "failed" || state === "stored" ? file?.error : undefined;
354
+ const why = state === "failed" || state === "stored" ? file?.error : file?.note;
351
355
  return (_jsxs(XStack, { role: "listitem", items: "center", gap: "$1.5", pl: "$2.5", pr: onRemove ? "$1" : "$2.5", py: "$1", rounded: "$10", borderWidth: 1, borderColor: "$borderColor", bg: "$hover", maxW: 320, "data-slot": "file-chip", "data-state": state, "data-stage": file?.stage ?? "", ...(why ? { title: why } : null), children: [type.startsWith("image/") ? (_jsx(ImageMark, { size: 12, "aria-hidden": true, color: "var(--muted-foreground)" })) : (_jsx(Paperclip, { size: 12, "aria-hidden": true, color: "var(--muted-foreground)" })), _jsx(Text, { fontSize: "$2", color: "$ink", numberOfLines: 1, children: name }), _jsx(Text, { fontSize: "$1", color: bad ? BAD : "$soft", numberOfLines: 1, shrink: 0, children: progress(state, sent, file) }), onRemove ? (_jsx(Box, { render: "button", "aria-label": `Remove ${name}`, onClick: onRemove, p: "$1", rounded: "$10", hoverStyle: { bg: "$raised" }, children: _jsx(X, { size: 12, "aria-hidden": true }) })) : null] }));
352
356
  }
353
357
  /** A file held for the next message: uploading, then indexing, then ready. */
@@ -917,7 +921,7 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
917
921
  }
918
922
  if (!grounds?.passages.length)
919
923
  why = why ?? unread(now);
920
- guarded(compose(text, { attached, reused }, grounds, why));
924
+ guarded(compose(text, { attached, reused }, grounds, why, partly(now)));
921
925
  }
922
926
  finally {
923
927
  setPreparing(null);
package/dist/Contents.js CHANGED
@@ -12,32 +12,41 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
12
12
  import { useEffect, useState } from 'react';
13
13
  import { ArrowLeft, MessageSquare, X } from 'lucide-react';
14
14
  import { Box, Button, Text, XStack, YStack } from '@hanzo/ui';
15
- import { section as openSection, toc } from './lib/files.js';
15
+ import { pct, section as openSection, toc } from './lib/files.js';
16
16
  import { BAD } from './lib/mix.js';
17
+ /** What each stage of a file's ingest is doing, as a person reads it. */
18
+ const STAGE = {
19
+ extract: 'reading',
20
+ toc: 'contents',
21
+ passages: 'passages',
22
+ graph: 'linking',
23
+ embed: 'vectors',
24
+ };
17
25
  /** How far a file's ingest has got, as Drive's badge and this pane's head say it. */
18
26
  export function standing(f) {
27
+ const at = pct(f);
19
28
  switch (f.status) {
20
- case 'ready':
29
+ case 'ready': {
21
30
  // Ready means read; while its vectors fill in, search by meaning grows.
22
- if (f.stage === 'embed' && f.passages)
23
- return `Indexed · ${f.sections ?? 0} sections · vectors ${Math.floor(((f.embedded ?? 0) / f.passages) * 100)}%`;
24
- return `Indexed · ${f.sections ?? 0} sections`;
31
+ const head = `${f.clipped ? 'Indexed in part' : 'Indexed'} · ${f.sections ?? 0} sections`;
32
+ return f.stage === 'embed' && at !== undefined ? `${head} · vectors ${at}%` : head;
33
+ }
25
34
  case 'stored':
26
35
  return 'Kept, not indexed';
27
36
  case 'failed':
28
37
  return 'Indexing failed';
29
38
  case 'queued':
30
39
  return 'Queued';
31
- default:
32
- if (f.stage === 'embed' && f.passages)
33
- return `Indexing ${Math.floor(((f.embedded ?? 0) / f.passages) * 100)}%`;
34
- return `Indexing · ${f.stage ?? 'starting'}`;
40
+ default: {
41
+ const word = f.stage ? STAGE[f.stage] : 'starting';
42
+ return at === undefined ? `Indexing · ${word}` : `Indexing · ${word} ${at}%`;
43
+ }
35
44
  }
36
45
  }
37
46
  /** The badge a Drive row wears for its index state. */
38
47
  export function Badge({ f }) {
39
48
  const bad = f.status === 'failed';
40
- return (_jsx(Text, { fontSize: "$1", color: bad ? BAD : f.status === 'ready' ? '$soft' : '$faint', numberOfLines: 1, "data-slot": "drive-index", "data-state": f.status, "data-stage": f.stage ?? '', ...(f.error ? { title: f.error } : null), children: standing(f) }));
49
+ return (_jsx(Text, { fontSize: "$1", color: bad ? BAD : f.status === 'ready' ? '$soft' : '$faint', numberOfLines: 1, "data-slot": "drive-index", "data-state": f.status, "data-stage": f.stage ?? '', ...(f.error || f.note ? { title: f.error || f.note } : null), children: standing(f) }));
41
50
  }
42
51
  /** Passages of the workspace that match a search, each naming where it is. */
43
52
  export function Hits({ hits, onOpen }) {
@@ -77,7 +86,7 @@ export function Contents({ api, file, at, onClose, onAsk, onFollow, }) {
77
86
  };
78
87
  }, [api, file.id, reading]);
79
88
  const shown = tree?.file ?? file;
80
- return (_jsxs(YStack, { position: "absolute", t: 0, r: 0, b: 0, width: 440, maxW: "100%", bg: "$background", borderLeftWidth: 1, borderColor: "$borderColor", boxShadow: "var(--shadow-lg)", z: "var(--z-drawer)", "data-slot": "drive-contents", children: [_jsxs(XStack, { items: "center", justify: "space-between", gap: "$2", px: "$4", py: "$3", borderBottomWidth: 1, borderColor: "$borderColor", children: [_jsxs(YStack, { minW: 0, flex: 1, children: [_jsx(Text, { fontSize: "$3", fontWeight: "600", color: "$ink", numberOfLines: 1, children: shown.name }), _jsx(Text, { fontSize: "$1", color: "$soft", numberOfLines: 1, children: standing(shown) })] }), _jsxs(Button, { size: "sm", onClick: () => onAsk(shown), "aria-label": "Ask in chat", children: [_jsx(MessageSquare, { size: 14 }), "Ask in chat"] }), _jsx(Button, { variant: "ghost", size: "icon-sm", onClick: onClose, "aria-label": "Close contents", title: "Close", children: _jsx(X, { size: 15 }) })] }), _jsxs(YStack, { flex: 1, minH: 0, overflowY: "auto", px: "$4", py: "$3", gap: "$2", children: [failed ? (_jsx(Text, { fontSize: "$2", color: BAD, children: failed })) : null, open ? (_jsxs(YStack, { gap: "$3", "data-slot": "drive-section", children: [_jsxs(Button, { variant: "ghost", size: "sm", justify: "flex-start", px: 0, onClick: () => setReading(null), children: [_jsx(ArrowLeft, { size: 13 }), "Contents"] }), _jsx(Text, { fontSize: "$1", color: "$soft", children: open.section.path }), _jsx(Text, { fontSize: "$4", fontWeight: "600", color: "$ink", children: open.section.title }), open.summary ? (_jsx(Text, { fontSize: "$2", color: "$soft", children: open.summary })) : null, _jsx(Text, { fontSize: "$2", color: "$ink", whiteSpace: "pre-wrap", children: open.text }), open.children.length ? (_jsxs(YStack, { gap: "$1", children: [_jsx(Text, { fontSize: 11, fontWeight: "600", color: "$faint", letterSpacing: 0.6, children: "Subsections" }), open.children.map((c) => (_jsx(Entry, { e: c, onOpen: () => setReading(c.id) }, c.id)))] })) : null, open.links.length ? (_jsxs(YStack, { gap: "$1", "data-slot": "drive-links", children: [_jsx(Text, { fontSize: 11, fontWeight: "600", color: "$faint", letterSpacing: 0.6, children: "Linked" }), open.links.map((l, i) => (_jsxs(Box, { render: "button", items: "flex-start", py: "$1", onClick: () => (l.file.id === file.id ? setReading(l.section.id) : onFollow(l.file.id, l.section.id)), children: [_jsx(Text, { fontSize: "$2", color: "$ink", numberOfLines: 1, children: l.file.id === file.id ? l.section.path || l.section.title : `${l.file.name} › ${l.section.path || l.section.title}` }), _jsxs(Text, { fontSize: "$1", color: "$soft", numberOfLines: 1, children: [l.kind, l.label ? ` · ${l.label}` : ''] })] }, `${l.file.id}#${l.section.id}.${i}`)))] })) : null, open.entities.length ? (_jsxs(Text, { fontSize: "$1", color: "$soft", children: ["Names: ", open.entities.map((e) => e.name).join(', ')] })) : null] })) : !tree ? (failed ? null : (_jsx(Text, { fontSize: "$2", color: "$soft", children: "Reading the contents\u2026" }))) : tree.sections.length <= 1 ? (_jsx(Text, { fontSize: "$2", color: "$soft", children: shown.status === 'ready' ? 'This file has no sections.' : 'The contents appear once the file is indexed.' })) : (tree.sections
89
+ return (_jsxs(YStack, { position: "absolute", t: 0, r: 0, b: 0, width: 440, maxW: "100%", bg: "$background", borderLeftWidth: 1, borderColor: "$borderColor", boxShadow: "var(--shadow-lg)", z: "var(--z-drawer)", "data-slot": "drive-contents", children: [_jsxs(XStack, { items: "center", justify: "space-between", gap: "$2", px: "$4", py: "$3", borderBottomWidth: 1, borderColor: "$borderColor", children: [_jsxs(YStack, { minW: 0, flex: 1, children: [_jsx(Text, { fontSize: "$3", fontWeight: "600", color: "$ink", numberOfLines: 1, children: shown.name }), _jsx(Text, { fontSize: "$1", color: "$soft", numberOfLines: 1, children: standing(shown) }), shown.note ? (_jsx(Text, { fontSize: "$1", color: "$faint", "data-slot": "drive-note", children: shown.note.replace(/\n/g, ' ') })) : null] }), _jsxs(Button, { size: "sm", onClick: () => onAsk(shown), "aria-label": "Ask in chat", children: [_jsx(MessageSquare, { size: 14 }), "Ask in chat"] }), _jsx(Button, { variant: "ghost", size: "icon-sm", onClick: onClose, "aria-label": "Close contents", title: "Close", children: _jsx(X, { size: 15 }) })] }), _jsxs(YStack, { flex: 1, minH: 0, overflowY: "auto", px: "$4", py: "$3", gap: "$2", children: [failed ? (_jsx(Text, { fontSize: "$2", color: BAD, children: failed })) : null, open ? (_jsxs(YStack, { gap: "$3", "data-slot": "drive-section", children: [_jsxs(Button, { variant: "ghost", size: "sm", justify: "flex-start", px: 0, onClick: () => setReading(null), children: [_jsx(ArrowLeft, { size: 13 }), "Contents"] }), _jsx(Text, { fontSize: "$1", color: "$soft", children: open.section.path }), _jsx(Text, { fontSize: "$4", fontWeight: "600", color: "$ink", children: open.section.title }), open.summary ? (_jsx(Text, { fontSize: "$2", color: "$soft", children: open.summary })) : null, _jsx(Text, { fontSize: "$2", color: "$ink", whiteSpace: "pre-wrap", children: open.text }), open.children.length ? (_jsxs(YStack, { gap: "$1", children: [_jsx(Text, { fontSize: 11, fontWeight: "600", color: "$faint", letterSpacing: 0.6, children: "Subsections" }), open.children.map((c) => (_jsx(Entry, { e: c, onOpen: () => setReading(c.id) }, c.id)))] })) : null, open.links.length ? (_jsxs(YStack, { gap: "$1", "data-slot": "drive-links", children: [_jsx(Text, { fontSize: 11, fontWeight: "600", color: "$faint", letterSpacing: 0.6, children: "Linked" }), open.links.map((l, i) => (_jsxs(Box, { render: "button", items: "flex-start", py: "$1", onClick: () => (l.file.id === file.id ? setReading(l.section.id) : onFollow(l.file.id, l.section.id)), children: [_jsx(Text, { fontSize: "$2", color: "$ink", numberOfLines: 1, children: l.file.id === file.id ? l.section.path || l.section.title : `${l.file.name} › ${l.section.path || l.section.title}` }), _jsxs(Text, { fontSize: "$1", color: "$soft", numberOfLines: 1, children: [l.kind, l.label ? ` · ${l.label}` : ''] })] }, `${l.file.id}#${l.section.id}.${i}`)))] })) : null, open.entities.length ? (_jsxs(Text, { fontSize: "$1", color: "$soft", children: ["Names: ", open.entities.map((e) => e.name).join(', ')] })) : null] })) : !tree ? (failed ? null : (_jsx(Text, { fontSize: "$2", color: "$soft", children: "Reading the contents\u2026" }))) : tree.sections.length <= 1 ? (_jsx(Text, { fontSize: "$2", color: "$soft", children: shown.status === 'ready' ? 'This file has no sections.' : 'The contents appear once the file is indexed.' })) : (tree.sections
81
90
  .filter((e) => e.level > 0)
82
91
  .map((e) => _jsx(Entry, { e: e, onOpen: () => setReading(e.id) }, e.id)))] })] }));
83
92
  }
@@ -28,7 +28,7 @@ export declare function weigh(bytes: number): string;
28
28
  * stored without indexing, or the read failed — so the model says so rather
29
29
  * than answering as though it had read the files.
30
30
  */
31
- export declare function compose(text: string, carried: Carried, grounds: Grounds | null, why?: string): string;
31
+ export declare function compose(text: string, carried: Carried, grounds: Grounds | null, why?: string, partial?: string): string;
32
32
  /** A turn read back: the words a person wrote, and the files it carried. */
33
33
  export interface Said {
34
34
  text: string;
@@ -38,7 +38,7 @@ const inert = (s) => s.split(CLOSE).join('</workspace files>');
38
38
  * stored without indexing, or the read failed — so the model says so rather
39
39
  * than answering as though it had read the files.
40
40
  */
41
- export function compose(text, carried, grounds, why) {
41
+ export function compose(text, carried, grounds, why, partial) {
42
42
  const words = text.trim();
43
43
  const all = [...carried.attached, ...carried.reused];
44
44
  if (!all.length)
@@ -49,6 +49,8 @@ export function compose(text, carried, grounds, why) {
49
49
  JSON.stringify({ attached: carried.attached, reused: carried.reused }),
50
50
  `The person's workspace holds these files: ${all.map((f) => `${f.name} (${f.type || 'file'}, ${weigh(f.size)})`).join('; ')}.`,
51
51
  ];
52
+ if (partial)
53
+ lines.push(`Not all of every file was indexed (${inert(partial)}): if the answer may lie past that, say so.`);
52
54
  if (passages.length) {
53
55
  lines.push(words
54
56
  ? 'Answer from the passages below, read from those files for this question. Cite each claim with its number, like [1], and end with a Sources list giving each passage you cited as "[n] file › section › ¶n". If the passages do not answer the question, say what is missing.'
@@ -34,8 +34,15 @@ export interface WorkFile {
34
34
  sections?: number;
35
35
  passages?: number;
36
36
  embedded?: number;
37
+ /** How far the running stage has come, of `total`, in that stage's units. */
38
+ done?: number;
39
+ total?: number;
40
+ /** Where the file is indexed less than whole, and why, in words. */
41
+ note?: string;
37
42
  parent?: string;
38
43
  }
44
+ /** The running stage's progress as a whole percent, when the index reports one. */
45
+ export declare const pct: (f: Pick<WorkFile, 'done' | 'total'>) => number | undefined;
39
46
  /** What a chat message carries of a file: enough to name it and ask about it. */
40
47
  export interface FileRef {
41
48
  id: string;
@@ -115,7 +122,8 @@ export interface Grounds {
115
122
  summary?: string;
116
123
  }[];
117
124
  passages: Passage[];
118
- picked: 'model' | 'search';
125
+ /** whole: short enough to read every section; model: a model read the ToCs; search: the best passages' sections. */
126
+ picked: 'whole' | 'model' | 'search';
119
127
  degraded?: boolean;
120
128
  }
121
129
  /** ToC-first retrieval: the sections a model picks from the files' tables of contents, their passages, and the graph's. */
package/dist/lib/files.js CHANGED
@@ -13,6 +13,8 @@
13
13
  * A chat turn carries the file's REFERENCE — id, name, type, size — and the
14
14
  * passages `retrieve` reads for the question, each citing file › section › ¶n.
15
15
  */
16
+ /** The running stage's progress as a whole percent, when the index reports one. */
17
+ export const pct = (f) => f.total && f.total > 0 ? Math.min(100, Math.floor(((f.done ?? 0) / f.total) * 100)) : undefined;
16
18
  export const refOf = (f) => ({ id: f.id, name: f.name, type: f.type, size: f.size });
17
19
  /** A file's ingest has ended, one way or the other. */
18
20
  export const settled = (f) => f.status === 'ready' || f.status === 'stored' || f.status === 'failed';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzo/rooms",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
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",