@hanzo/rooms 0.1.3 → 0.1.5
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 +57 -53
- package/dist/Meet.js +91 -148
- package/dist/RightPane.js +21 -30
- package/dist/cast.d.ts +17 -14
- package/dist/cast.js +39 -49
- package/dist/lib/attach.d.ts +19 -14
- package/dist/lib/attach.js +12 -9
- package/dist/lib/limits.d.ts +9 -0
- package/dist/lib/limits.js +6 -0
- package/dist/pane.d.ts +31 -4
- package/dist/pane.js +59 -11
- package/dist/speech.d.ts +23 -1
- package/dist/speech.js +70 -79
- package/package.json +6 -6
package/dist/RightPane.js
CHANGED
|
@@ -14,10 +14,12 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
14
14
|
// OUTPUTS AND SOURCES ARE DERIVED FROM THE TURNS, never from a second list kept
|
|
15
15
|
// in step with them. `LLM.md` already states this rule for `Work` and it is the
|
|
16
16
|
// same rule: the conversation is the record, so a fenced block in an answer IS
|
|
17
|
-
// an output and an image in a question
|
|
18
|
-
// nothing to invalidate, and no count that
|
|
19
|
-
// and switching channels costs no request,
|
|
20
|
-
// hand.
|
|
17
|
+
// an output, and an image in a question or a block in it that names a file IS a
|
|
18
|
+
// source. There is nothing to sync, nothing to invalidate, and no count that
|
|
19
|
+
// can disagree with the transcript — and switching channels costs no request,
|
|
20
|
+
// because the turns are already in hand. The one thing listed that is not yet
|
|
21
|
+
// a turn is what is held for the next message (`hold` in `pane.ts`), which a
|
|
22
|
+
// send spends into one. A conversation that has produced nothing says so.
|
|
21
23
|
//
|
|
22
24
|
// THE COLUMN IS STACKED, NOT SPLIT. The reference this follows puts a ~300px
|
|
23
25
|
// summary beside a wide browser, which needs about 900px of column; the frame
|
|
@@ -33,7 +35,7 @@ import { ExternalLink, FileCode, Image as ImageMark, Paperclip, PanelRightClose,
|
|
|
33
35
|
import { Box, DropdownMenu, DropdownMenuContent, DropdownMenuCheckboxItem, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, Text, View, XStack, YStack, } from '@hanzo/ui';
|
|
34
36
|
import { Card, Quiet, Row } from './card.js';
|
|
35
37
|
import { useOpen } from './open.js';
|
|
36
|
-
import { address, channel,
|
|
38
|
+
import { address, channel, name as titleOf, openTab, pickTab, pin, shutTab, take, usePane, } from './pane.js';
|
|
37
39
|
import { useProviders, useSkills } from './tools.js';
|
|
38
40
|
import { activate, startDevice } from './connect.js';
|
|
39
41
|
/** How many rows a card shows before it offers the rest. */
|
|
@@ -115,7 +117,11 @@ function outputs(said) {
|
|
|
115
117
|
});
|
|
116
118
|
return out;
|
|
117
119
|
}
|
|
118
|
-
/**
|
|
120
|
+
/**
|
|
121
|
+
* What went into this conversation: pictures asked about, files sent fenced
|
|
122
|
+
* under their names (`compose` in `lib/attach`), and files held for the next
|
|
123
|
+
* message.
|
|
124
|
+
*/
|
|
119
125
|
function sources(said, held) {
|
|
120
126
|
const out = [];
|
|
121
127
|
said.forEach((m, turn) => {
|
|
@@ -124,15 +130,17 @@ function sources(said, held) {
|
|
|
124
130
|
pictures(m).forEach((href, i) => {
|
|
125
131
|
out.push({ id: `s${turn}.${i}`, name: 'Attached image', kind: 'image', href });
|
|
126
132
|
});
|
|
133
|
+
let n = 0;
|
|
134
|
+
for (const [, info, body] of words(m).matchAll(FENCE)) {
|
|
135
|
+
n += 1;
|
|
136
|
+
const named = info.trim();
|
|
137
|
+
if (!suffix(named))
|
|
138
|
+
continue;
|
|
139
|
+
out.push({ id: `sf${turn}.${n}`, name: named, kind: 'file', body, mime: MIME[suffix(named)] ?? 'text/plain' });
|
|
140
|
+
}
|
|
127
141
|
});
|
|
128
142
|
for (const h of held) {
|
|
129
|
-
out.push({
|
|
130
|
-
id: h.id,
|
|
131
|
-
name: h.name,
|
|
132
|
-
kind: h.type.startsWith('image/') ? 'image' : 'file',
|
|
133
|
-
href: h.href,
|
|
134
|
-
mime: h.type,
|
|
135
|
-
});
|
|
143
|
+
out.push({ id: h.id, name: h.name, kind: h.kind === 'image' ? 'image' : 'file', href: h.href, mime: h.type });
|
|
136
144
|
}
|
|
137
145
|
return out;
|
|
138
146
|
}
|
|
@@ -238,23 +246,6 @@ const Note = ({ children }) => (_jsx(DropdownMenuLabel, { children: _jsx(Text, {
|
|
|
238
246
|
function Add({ at }) {
|
|
239
247
|
return (_jsxs(DropdownMenu, { placement: "bottom-end", maxHeight: 420, children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Box, { render: "button", "aria-label": "Add to this conversation", p: "$1", rounded: "$2", hoverStyle: { bg: '$hover' }, children: _jsx(Plus, { size: 16, "aria-hidden": true }) }) }), _jsxs(DropdownMenuContent, { align: "end", children: [_jsx(DropdownMenuItem, { onClick: () => take(at), children: _jsx(Word, { children: "Attach files" }) }), _jsx(DropdownMenuSeparator, {}), _jsx(Skills, {}), _jsx(DropdownMenuSeparator, {}), _jsx(Services, {})] })] }));
|
|
240
248
|
}
|
|
241
|
-
/**
|
|
242
|
-
* Asks for files and takes what is given.
|
|
243
|
-
*
|
|
244
|
-
* The picker is the browser's, minted and dropped per press rather than kept as
|
|
245
|
-
* a hidden input in the markup: a menu row is not a form control, and an
|
|
246
|
-
* `<input type=file>` parked in the tree is one more thing for a screen reader
|
|
247
|
-
* to walk past.
|
|
248
|
-
*/
|
|
249
|
-
function take(at) {
|
|
250
|
-
if (typeof document === 'undefined')
|
|
251
|
-
return;
|
|
252
|
-
const ask = document.createElement('input');
|
|
253
|
-
ask.type = 'file';
|
|
254
|
-
ask.multiple = true;
|
|
255
|
-
ask.onchange = () => hold(at, Array.from(ask.files ?? []));
|
|
256
|
-
ask.click();
|
|
257
|
-
}
|
|
258
249
|
/**
|
|
259
250
|
* What this org's agents can reach for, as a section of this menu.
|
|
260
251
|
*
|
package/dist/cast.d.ts
CHANGED
|
@@ -24,18 +24,17 @@ interface Role {
|
|
|
24
24
|
*/
|
|
25
25
|
ground?: string;
|
|
26
26
|
/**
|
|
27
|
-
* The voice the platform reads this character's replies in
|
|
28
|
-
* /v1/audio/speech
|
|
27
|
+
* The voice the platform reads this character's replies in: a speech-service
|
|
28
|
+
* voice id, sent to /v1/audio/speech as it is. The first letter is the accent
|
|
29
|
+
* (a American, b British), the second the register (f female, m male).
|
|
29
30
|
*
|
|
30
|
-
* ONE VOICE PER CHARACTER, and never two who would be in the same room.
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* the five the demo puts in one call are all distinct. `echo` was reading for
|
|
34
|
-
* three at once, which in a call is one person doing three parts.
|
|
31
|
+
* ONE VOICE PER CHARACTER, and never two who would be in the same room. A
|
|
32
|
+
* house part shares the voice of the character whose face it wears, so Hanzo
|
|
33
|
+
* Coder sounds like Dev; no two characters share one.
|
|
35
34
|
*
|
|
36
35
|
* CAST, NOT COPIED. These are the speech service's own voices, chosen for fit
|
|
37
|
-
* — a
|
|
38
|
-
*
|
|
36
|
+
* — a playful reader for the physicist, something low and measured for the
|
|
37
|
+
* quant. None of them is anybody's actual voice and none is trying to be:
|
|
39
38
|
* synthesising a real person saying words they never said is a different act
|
|
40
39
|
* from casting a part, and this is the second one.
|
|
41
40
|
*/
|
|
@@ -88,18 +87,22 @@ export declare function speakers(text: string, names: string[]): {
|
|
|
88
87
|
who: string | null;
|
|
89
88
|
text: string;
|
|
90
89
|
}[];
|
|
90
|
+
/**
|
|
91
|
+
* Every American and British voice the speech service reads in, which is the
|
|
92
|
+
* pool an agent nobody cast is given a voice from. `am_santa` is left out: it
|
|
93
|
+
* is a character voice, not a reader.
|
|
94
|
+
*/
|
|
91
95
|
export declare const VOICES: string[];
|
|
96
|
+
/** A voice for a name nobody cast: the same name always gets the same one. */
|
|
92
97
|
export declare function fallbackVoice(name: string | undefined): string;
|
|
93
98
|
/** The voice to read a character's replies in. Always returns a valid voice. */
|
|
94
99
|
export declare const voiceOf: (name: string | undefined) => string;
|
|
100
|
+
/** Who a voice sounds like, read off its id — what a browser voice standing in is matched on. */
|
|
95
101
|
export interface VoiceProfile {
|
|
96
102
|
voice: string;
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
description: string;
|
|
100
|
-
gender: 'male' | 'female' | 'neutral';
|
|
103
|
+
gender: 'male' | 'female';
|
|
104
|
+
accent: 'american' | 'british';
|
|
101
105
|
}
|
|
102
|
-
export declare const VOICE_PROFILES: Record<string, VoiceProfile>;
|
|
103
106
|
export declare function voiceProfileOf(name: string | undefined): VoiceProfile;
|
|
104
107
|
export declare function fallbackMemoji(name: string | undefined): string;
|
|
105
108
|
/**
|
package/dist/cast.js
CHANGED
|
@@ -24,30 +24,30 @@ export const CAST = {
|
|
|
24
24
|
//
|
|
25
25
|
// Dev wears its own memoji (black ground baked in, so it fills the disc and
|
|
26
26
|
// needs no rim colour); hanzo coder is that same face under the house name.
|
|
27
|
-
dev: { portrait: '/agents/dev.png', voice: '
|
|
28
|
-
des: { portrait: '/agents/des.png', ground: '#74569F', voice: '
|
|
29
|
-
vi: { portrait: '/agents/vi.png', ground: '#41649F', voice: '
|
|
30
|
-
antje: { portrait: '/agents/antje.jpg', voice: '
|
|
31
|
-
zach: { portrait: '/agents/zach.jpg', voice: '
|
|
32
|
-
feynman: { portrait: '/agents/feynman.png', ground: '#9C7C4F', voice: '
|
|
33
|
-
einstein: { portrait: '/agents/einstein.png', ground: '#7D6450', voice: '
|
|
34
|
-
nora: { portrait: '/agents/nora.png', voice: '
|
|
35
|
-
creative: { portrait: '/agents/creative.png', voice: '
|
|
36
|
-
maya: { portrait: '/agents/maya.png', voice: '
|
|
37
|
-
leo: { portrait: '/agents/leo.png', voice: '
|
|
27
|
+
dev: { portrait: '/agents/dev.png', voice: 'am_michael' },
|
|
28
|
+
des: { portrait: '/agents/des.png', ground: '#74569F', voice: 'af_bella' },
|
|
29
|
+
vi: { portrait: '/agents/vi.png', ground: '#41649F', voice: 'am_fenrir' },
|
|
30
|
+
antje: { portrait: '/agents/antje.jpg', voice: 'af_nova' },
|
|
31
|
+
zach: { portrait: '/agents/zach.jpg', voice: 'am_onyx' },
|
|
32
|
+
feynman: { portrait: '/agents/feynman.png', ground: '#9C7C4F', voice: 'am_puck' },
|
|
33
|
+
einstein: { portrait: '/agents/einstein.png', ground: '#7D6450', voice: 'bm_george' },
|
|
34
|
+
nora: { portrait: '/agents/nora.png', voice: 'af_heart' },
|
|
35
|
+
creative: { portrait: '/agents/creative.png', voice: 'bm_fable' },
|
|
36
|
+
maya: { portrait: '/agents/maya.png', voice: 'bf_emma' },
|
|
37
|
+
leo: { portrait: '/agents/leo.png', voice: 'am_liam' },
|
|
38
38
|
// THE HOUSE AGENTS, UNDER THE NAMES A READER SEES. Every entry above is keyed
|
|
39
39
|
// by the character; these are keyed by the part.
|
|
40
|
-
'hanzo coder': { portrait: '/agents/dev.png', voice: '
|
|
41
|
-
'hanzo researcher': { portrait: '/agents/feynman.png', voice: '
|
|
42
|
-
'hanzo designer': { portrait: '/agents/des.png', voice: '
|
|
43
|
-
'hanzo devops': { voice: '
|
|
44
|
-
'hanzo quant': { voice: '
|
|
45
|
-
'hanzo copywriter': { portrait: '/agents/creative.png', voice: '
|
|
46
|
-
'hanzo legal & compliance': { portrait: '/agents/einstein.png', voice: '
|
|
47
|
-
'hanzo executive assistant': { portrait: '/agents/maya.png', voice: '
|
|
48
|
-
'hanzo architect': { voice: '
|
|
49
|
-
'hanzo vision': { voice: '
|
|
50
|
-
'hanzo support': { portrait: '/agents/nora.png', voice: '
|
|
40
|
+
'hanzo coder': { portrait: '/agents/dev.png', voice: 'am_michael' },
|
|
41
|
+
'hanzo researcher': { portrait: '/agents/feynman.png', voice: 'am_puck' },
|
|
42
|
+
'hanzo designer': { portrait: '/agents/des.png', voice: 'af_bella' },
|
|
43
|
+
'hanzo devops': { voice: 'am_fenrir' },
|
|
44
|
+
'hanzo quant': { voice: 'am_onyx' },
|
|
45
|
+
'hanzo copywriter': { portrait: '/agents/creative.png', voice: 'bm_fable' },
|
|
46
|
+
'hanzo legal & compliance': { portrait: '/agents/einstein.png', voice: 'bm_george' },
|
|
47
|
+
'hanzo executive assistant': { portrait: '/agents/maya.png', voice: 'bf_emma' },
|
|
48
|
+
'hanzo architect': { voice: 'bm_george' },
|
|
49
|
+
'hanzo vision': { voice: 'bm_fable' },
|
|
50
|
+
'hanzo support': { portrait: '/agents/nora.png', voice: 'af_heart' },
|
|
51
51
|
};
|
|
52
52
|
/** The role for an agent name, or undefined for one nobody cast. */
|
|
53
53
|
export const roleOf = (name) => CAST[name?.toLowerCase() ?? ''];
|
|
@@ -132,22 +132,21 @@ export function speakers(text, names) {
|
|
|
132
132
|
}
|
|
133
133
|
return out.map((one) => ({ ...one, text: one.text.trim() })).filter((one) => one.text || one.who);
|
|
134
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* Every American and British voice the speech service reads in, which is the
|
|
137
|
+
* pool an agent nobody cast is given a voice from. `am_santa` is left out: it
|
|
138
|
+
* is a character voice, not a reader.
|
|
139
|
+
*/
|
|
135
140
|
export const VOICES = [
|
|
136
|
-
'
|
|
137
|
-
'
|
|
138
|
-
'
|
|
139
|
-
'
|
|
140
|
-
'onyx',
|
|
141
|
-
'fable',
|
|
142
|
-
'shimmer',
|
|
143
|
-
'alloy',
|
|
144
|
-
'verse',
|
|
145
|
-
'ballad',
|
|
146
|
-
'coral',
|
|
141
|
+
'af_alloy', 'af_aoede', 'af_bella', 'af_heart', 'af_jessica', 'af_kore', 'af_nicole', 'af_nova', 'af_river', 'af_sarah', 'af_sky',
|
|
142
|
+
'am_adam', 'am_echo', 'am_eric', 'am_fenrir', 'am_liam', 'am_michael', 'am_onyx', 'am_puck',
|
|
143
|
+
'bf_alice', 'bf_emma', 'bf_isabella', 'bf_lily',
|
|
144
|
+
'bm_daniel', 'bm_fable', 'bm_george', 'bm_lewis',
|
|
147
145
|
];
|
|
146
|
+
/** A voice for a name nobody cast: the same name always gets the same one. */
|
|
148
147
|
export function fallbackVoice(name) {
|
|
149
148
|
if (!name)
|
|
150
|
-
return
|
|
149
|
+
return 'af_heart';
|
|
151
150
|
let hash = 0;
|
|
152
151
|
for (let i = 0; i < name.length; i++) {
|
|
153
152
|
hash = (hash << 5) - hash + name.charCodeAt(i);
|
|
@@ -157,22 +156,13 @@ export function fallbackVoice(name) {
|
|
|
157
156
|
}
|
|
158
157
|
/** The voice to read a character's replies in. Always returns a valid voice. */
|
|
159
158
|
export const voiceOf = (name) => roleOf(name)?.voice || fallbackVoice(name);
|
|
160
|
-
export const VOICE_PROFILES = {
|
|
161
|
-
ash: { voice: 'ash', pitch: 1.0, rate: 1.0, description: 'Clear, direct, technical', gender: 'male' },
|
|
162
|
-
echo: { voice: 'echo', pitch: 1.1, rate: 1.05, description: 'Adaptive, creative, resonant', gender: 'female' },
|
|
163
|
-
sage: { voice: 'sage', pitch: 0.95, rate: 1.05, description: 'Authoritative, calm, systems-minded', gender: 'male' },
|
|
164
|
-
nova: { voice: 'nova', pitch: 1.1, rate: 1.0, description: 'Energetic, articulate, engaging', gender: 'female' },
|
|
165
|
-
onyx: { voice: 'onyx', pitch: 0.85, rate: 1.0, description: 'Deep, measured, quantitative', gender: 'male' },
|
|
166
|
-
fable: { voice: 'fable', pitch: 1.05, rate: 1.12, description: 'Warm, enthusiastic, pedagogical', gender: 'male' },
|
|
167
|
-
shimmer: { voice: 'shimmer', pitch: 1.1, rate: 0.98, description: 'Precise, professional, clear', gender: 'female' },
|
|
168
|
-
alloy: { voice: 'alloy', pitch: 0.95, rate: 1.05, description: 'Visionary, forward-leaning, focused', gender: 'male' },
|
|
169
|
-
verse: { voice: 'verse', pitch: 0.9, rate: 0.95, description: 'Intentional, iconic, design-first', gender: 'male' },
|
|
170
|
-
ballad: { voice: 'ballad', pitch: 0.92, rate: 1.0, description: 'Thoughtful, deep, expansive', gender: 'male' },
|
|
171
|
-
coral: { voice: 'coral', pitch: 1.15, rate: 1.05, description: 'Friendly, supportive, organized', gender: 'female' },
|
|
172
|
-
};
|
|
173
159
|
export function voiceProfileOf(name) {
|
|
174
|
-
const
|
|
175
|
-
return
|
|
160
|
+
const voice = voiceOf(name);
|
|
161
|
+
return {
|
|
162
|
+
voice,
|
|
163
|
+
gender: voice[1] === 'm' ? 'male' : 'female',
|
|
164
|
+
accent: voice[0] === 'b' ? 'british' : 'american',
|
|
165
|
+
};
|
|
176
166
|
}
|
|
177
167
|
/** Initials, for anyone with no likeness. Two at most: a disc this size holds
|
|
178
168
|
* two letters and turns three into a smudge. */
|
package/dist/lib/attach.d.ts
CHANGED
|
@@ -5,34 +5,39 @@
|
|
|
5
5
|
* model reads, and nothing else rides that wire, so a file goes one of two
|
|
6
6
|
* ways: an image as a `data:` URI beside the words, a text file as its own
|
|
7
7
|
* words after the message, fenced under its name. Any other kind is refused by
|
|
8
|
-
* name, before anything is
|
|
8
|
+
* name, before anything is held.
|
|
9
|
+
*
|
|
10
|
+
* This module judges and reads a file; `pane.ts` holds what it admits, per
|
|
11
|
+
* conversation, for the composer's chips and the column's Sources alike.
|
|
9
12
|
*/
|
|
10
13
|
import type { MessagePart } from '@hanzo/ui/chat';
|
|
11
|
-
/**
|
|
12
|
-
export interface
|
|
13
|
-
id: string;
|
|
14
|
-
name: string;
|
|
14
|
+
/** What a file contributes to a message once admitted. */
|
|
15
|
+
export interface Admitted {
|
|
15
16
|
kind: 'image' | 'text';
|
|
16
17
|
/** A `data:` URI for an image, the words for a text file. */
|
|
17
18
|
data: string;
|
|
18
|
-
size: number;
|
|
19
19
|
}
|
|
20
|
+
/** A file as it goes with a message. */
|
|
21
|
+
export type Going = Admitted & {
|
|
22
|
+
name: string;
|
|
23
|
+
};
|
|
20
24
|
export declare const IMAGE_MAX: number;
|
|
21
25
|
export declare const TEXT_MAX: number;
|
|
22
26
|
/** Which way a file goes with a message, or null when it cannot go. */
|
|
23
27
|
export declare function kindOf(file: {
|
|
24
28
|
name: string;
|
|
25
29
|
type: string;
|
|
26
|
-
}):
|
|
27
|
-
/** A file read
|
|
28
|
-
export declare function
|
|
30
|
+
}): Admitted['kind'] | null;
|
|
31
|
+
/** A file read for a message, or the sentence that says why it cannot go. */
|
|
32
|
+
export declare function admit(file: File): Promise<Admitted | string>;
|
|
29
33
|
/**
|
|
30
|
-
* The message as sent: the words, then each text file fenced
|
|
31
|
-
*
|
|
32
|
-
*
|
|
34
|
+
* The message as sent: the words, then each text file fenced with its name as
|
|
35
|
+
* the info string — the shape a model writes a file in, and the one the
|
|
36
|
+
* column's Sources reads back out of the turn. With no words and no text file,
|
|
37
|
+
* the images' names, so the turn says what it carries.
|
|
33
38
|
*/
|
|
34
|
-
export declare function compose(text: string, files: readonly
|
|
39
|
+
export declare function compose(text: string, files: readonly Going[]): string;
|
|
35
40
|
/** The images of a message, as the URIs @hanzo/ai's `send` takes. */
|
|
36
|
-
export declare const images: (files: readonly
|
|
41
|
+
export declare const images: (files: readonly Going[]) => string[];
|
|
37
42
|
/** A wire turn's parts — text and `image_url` — as the parts `@hanzo/ui/chat` draws. */
|
|
38
43
|
export declare function partsOf(content: readonly unknown[]): MessagePart[];
|
package/dist/lib/attach.js
CHANGED
|
@@ -5,7 +5,10 @@
|
|
|
5
5
|
* model reads, and nothing else rides that wire, so a file goes one of two
|
|
6
6
|
* ways: an image as a `data:` URI beside the words, a text file as its own
|
|
7
7
|
* words after the message, fenced under its name. Any other kind is refused by
|
|
8
|
-
* name, before anything is
|
|
8
|
+
* name, before anything is held.
|
|
9
|
+
*
|
|
10
|
+
* This module judges and reads a file; `pane.ts` holds what it admits, per
|
|
11
|
+
* conversation, for the composer's chips and the column's Sources alike.
|
|
9
12
|
*/
|
|
10
13
|
export const IMAGE_MAX = 8 * 1024 * 1024;
|
|
11
14
|
export const TEXT_MAX = 256 * 1024;
|
|
@@ -30,8 +33,8 @@ function read(file, as) {
|
|
|
30
33
|
reader.readAsText(file);
|
|
31
34
|
});
|
|
32
35
|
}
|
|
33
|
-
/** A file read
|
|
34
|
-
export async function
|
|
36
|
+
/** A file read for a message, or the sentence that says why it cannot go. */
|
|
37
|
+
export async function admit(file) {
|
|
35
38
|
const kind = kindOf(file);
|
|
36
39
|
if (!kind)
|
|
37
40
|
return `${file.name}: only images and text files go with a message.`;
|
|
@@ -39,8 +42,7 @@ export async function hold(file) {
|
|
|
39
42
|
if (file.size > max)
|
|
40
43
|
return `${file.name} is larger than ${kind === 'image' ? '8 MB' : '256 KB'}.`;
|
|
41
44
|
try {
|
|
42
|
-
|
|
43
|
-
return { id: crypto.randomUUID(), name: file.name, kind, data, size: file.size };
|
|
45
|
+
return { kind, data: await read(file, kind === 'image' ? 'url' : 'text') };
|
|
44
46
|
}
|
|
45
47
|
catch {
|
|
46
48
|
return `${file.name} could not be read.`;
|
|
@@ -52,16 +54,17 @@ function fence(words) {
|
|
|
52
54
|
return '`'.repeat(Math.max(3, longest + 1));
|
|
53
55
|
}
|
|
54
56
|
/**
|
|
55
|
-
* The message as sent: the words, then each text file fenced
|
|
56
|
-
*
|
|
57
|
-
*
|
|
57
|
+
* The message as sent: the words, then each text file fenced with its name as
|
|
58
|
+
* the info string — the shape a model writes a file in, and the one the
|
|
59
|
+
* column's Sources reads back out of the turn. With no words and no text file,
|
|
60
|
+
* the images' names, so the turn says what it carries.
|
|
58
61
|
*/
|
|
59
62
|
export function compose(text, files) {
|
|
60
63
|
const blocks = files
|
|
61
64
|
.filter((f) => f.kind === 'text')
|
|
62
65
|
.map((f) => {
|
|
63
66
|
const f3 = fence(f.data);
|
|
64
|
-
return `${f.name}\n${
|
|
67
|
+
return `${f3}${f.name}\n${f.data.replace(/\n$/, '')}\n${f3}`;
|
|
65
68
|
});
|
|
66
69
|
const out = [text.trim(), ...blocks].filter(Boolean).join('\n\n');
|
|
67
70
|
return out || files.map((f) => f.name).join(', ');
|
package/dist/lib/limits.d.ts
CHANGED
|
@@ -47,6 +47,15 @@ export declare const spendShown: ({ limits, answered }: {
|
|
|
47
47
|
export declare const left: (s: Span) => number;
|
|
48
48
|
/** Whether a model is a Hanzo SKU — an Enso or Zen id — the only models a consumer surface offers. */
|
|
49
49
|
export declare const sku: (id: string) => boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Whether a model can answer a chat turn: it names no outputs, or names text
|
|
52
|
+
* among them. `zen-scribe` (a transcript) and `zen-voice-mini` (audio) are
|
|
53
|
+
* Zen ids the catalog lists, and neither answers a question.
|
|
54
|
+
*/
|
|
55
|
+
export declare const chats: (model: {
|
|
56
|
+
id: string;
|
|
57
|
+
outputs?: unknown;
|
|
58
|
+
}) => boolean;
|
|
50
59
|
/** A reset as a clock reads it in this browser — "17:00" — or '' for none. */
|
|
51
60
|
export declare function clock(iso: string | null | undefined): string;
|
|
52
61
|
/** A period's end as a sentence states it — "Oct 30, 2026" — or '' for none. Periods end on UTC days. */
|
package/dist/lib/limits.js
CHANGED
|
@@ -102,6 +102,12 @@ export const spendShown = ({ limits, answered }) => answered && !limits?.plan;
|
|
|
102
102
|
export const left = (s) => 100 - s.percent;
|
|
103
103
|
/** Whether a model is a Hanzo SKU — an Enso or Zen id — the only models a consumer surface offers. */
|
|
104
104
|
export const sku = (id) => /^(hanzo\/)?(enso|zen)/i.test(id);
|
|
105
|
+
/**
|
|
106
|
+
* Whether a model can answer a chat turn: it names no outputs, or names text
|
|
107
|
+
* among them. `zen-scribe` (a transcript) and `zen-voice-mini` (audio) are
|
|
108
|
+
* Zen ids the catalog lists, and neither answers a question.
|
|
109
|
+
*/
|
|
110
|
+
export const chats = (model) => !Array.isArray(model.outputs) || model.outputs.includes('text');
|
|
105
111
|
/** A reset as a clock reads it in this browser — "17:00" — or '' for none. */
|
|
106
112
|
export function clock(iso) {
|
|
107
113
|
const t = iso ? Date.parse(iso) : NaN;
|
package/dist/pane.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type Admitted } from './lib/attach.js';
|
|
1
2
|
/** One open tab in the pane's browser. */
|
|
2
3
|
export interface Leaf {
|
|
3
4
|
id: string;
|
|
@@ -7,13 +8,14 @@ export interface Leaf {
|
|
|
7
8
|
title: string;
|
|
8
9
|
}
|
|
9
10
|
/**
|
|
10
|
-
* A file the reader put into this conversation from their own disk
|
|
11
|
+
* A file the reader put into this conversation from their own disk, read and
|
|
12
|
+
* waiting to go with the next message.
|
|
11
13
|
*
|
|
12
14
|
* `href` is an object URL and is the reason this is never written down — see
|
|
13
15
|
* the note at the top. Everything else is read off the `File` itself, so a name
|
|
14
16
|
* and a size here are the file system's, never a guess.
|
|
15
17
|
*/
|
|
16
|
-
export interface Held {
|
|
18
|
+
export interface Held extends Admitted {
|
|
17
19
|
id: string;
|
|
18
20
|
name: string;
|
|
19
21
|
size: number;
|
|
@@ -29,6 +31,8 @@ export interface Pane {
|
|
|
29
31
|
/** Whether the summary column stands beside the browser. */
|
|
30
32
|
pinned: boolean;
|
|
31
33
|
held: Held[];
|
|
34
|
+
/** Why the last files offered were refused, in the reader's words, or null. */
|
|
35
|
+
refused: string | null;
|
|
32
36
|
}
|
|
33
37
|
/**
|
|
34
38
|
* The conversation a pane belongs to.
|
|
@@ -83,8 +87,31 @@ export declare function pickTab(key: string, id: string): void;
|
|
|
83
87
|
export declare function shutTab(key: string, id: string): void;
|
|
84
88
|
/** Shows or hides the summary column beside this conversation's browser. */
|
|
85
89
|
export declare function pin(key: string, on: boolean): void;
|
|
86
|
-
/**
|
|
87
|
-
|
|
90
|
+
/**
|
|
91
|
+
* Takes files from the reader's disk into this conversation.
|
|
92
|
+
*
|
|
93
|
+
* Each is judged and read by `admit`; what is refused is said by name in
|
|
94
|
+
* `refused` and nothing of it is held, so a chip never stands for a file that
|
|
95
|
+
* cannot go.
|
|
96
|
+
*/
|
|
97
|
+
export declare function hold(key: string, files: readonly File[]): Promise<void>;
|
|
98
|
+
/**
|
|
99
|
+
* Asks for files and holds what is given.
|
|
100
|
+
*
|
|
101
|
+
* The picker is the browser's, minted and dropped per press rather than kept as
|
|
102
|
+
* a hidden input in the markup: a menu row is not a form control, and an
|
|
103
|
+
* `<input type=file>` parked in the tree is one more thing for a screen reader
|
|
104
|
+
* to walk past.
|
|
105
|
+
*/
|
|
106
|
+
export declare function take(key: string): void;
|
|
107
|
+
/**
|
|
108
|
+
* Hands over everything held to go with a message, and lets go of it.
|
|
109
|
+
*
|
|
110
|
+
* The turn carries each file from here — an image as its `data:` URI, a text
|
|
111
|
+
* file as its words. The object URLs are left standing, so a tab already open
|
|
112
|
+
* on one keeps showing it.
|
|
113
|
+
*/
|
|
114
|
+
export declare function spend(key: string): Held[];
|
|
88
115
|
/** Lets go of a file, and of the handle the browser minted for it. */
|
|
89
116
|
export declare function drop(key: string, id: string): void;
|
|
90
117
|
/** This conversation's pane, and it alone — a change to another redraws nothing
|
package/dist/pane.js
CHANGED
|
@@ -20,10 +20,17 @@
|
|
|
20
20
|
// with the document that minted it, so a persisted row would come back as a
|
|
21
21
|
// name pointing at nothing. A row that looks like a file and opens nothing is
|
|
22
22
|
// worse than no row, so they are held for the session and no longer.
|
|
23
|
+
//
|
|
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 is judged by
|
|
26
|
+
// one rule (`lib/attach`), shows as a chip over the field and a row under
|
|
27
|
+
// Sources at once, and goes with the next message. Sending spends what is
|
|
28
|
+
// held; from then on the turn itself is the record Sources reads.
|
|
23
29
|
import { useSyncExternalStore } from 'react';
|
|
30
|
+
import { admit } from './lib/attach.js';
|
|
24
31
|
/** A pane nobody has touched. Shared, and never mutated — see `EMPTY` in
|
|
25
32
|
* `open.ts` for why a fresh object per read is an infinite render. */
|
|
26
|
-
const BARE = { tabs: [], at: null, pinned: true, held: [] };
|
|
33
|
+
const BARE = { tabs: [], at: null, pinned: true, held: [], refused: null };
|
|
27
34
|
const where = (key) => `hanzo.pane.${key}`;
|
|
28
35
|
/**
|
|
29
36
|
* The conversation a pane belongs to.
|
|
@@ -90,6 +97,7 @@ function read(key) {
|
|
|
90
97
|
at: tabs.some((t) => t.id === kept.at) ? kept.at : (tabs[0]?.id ?? null),
|
|
91
98
|
pinned: kept.pinned !== false,
|
|
92
99
|
held: [],
|
|
100
|
+
refused: null,
|
|
93
101
|
};
|
|
94
102
|
}
|
|
95
103
|
}
|
|
@@ -201,18 +209,58 @@ export function shutTab(key, id) {
|
|
|
201
209
|
export function pin(key, on) {
|
|
202
210
|
edit(key, (was) => (was.pinned === on ? was : { ...was, pinned: on }));
|
|
203
211
|
}
|
|
204
|
-
/**
|
|
205
|
-
|
|
212
|
+
/**
|
|
213
|
+
* Takes files from the reader's disk into this conversation.
|
|
214
|
+
*
|
|
215
|
+
* Each is judged and read by `admit`; what is refused is said by name in
|
|
216
|
+
* `refused` and nothing of it is held, so a chip never stands for a file that
|
|
217
|
+
* cannot go.
|
|
218
|
+
*/
|
|
219
|
+
export async function hold(key, files) {
|
|
206
220
|
if (!files.length)
|
|
207
221
|
return;
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
222
|
+
const judged = await Promise.all(files.map(admit));
|
|
223
|
+
const taken = [];
|
|
224
|
+
const refused = [];
|
|
225
|
+
judged.forEach((one, i) => {
|
|
226
|
+
const file = files[i];
|
|
227
|
+
if (typeof one === 'string')
|
|
228
|
+
refused.push(one);
|
|
229
|
+
else
|
|
230
|
+
taken.push({ id: mint('h'), name: file.name, size: file.size, type: file.type, href: URL.createObjectURL(file), ...one });
|
|
231
|
+
});
|
|
232
|
+
edit(key, (was) => ({ ...was, held: [...was.held, ...taken], refused: refused.length ? refused.join(' ') : null }));
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Asks for files and holds what is given.
|
|
236
|
+
*
|
|
237
|
+
* The picker is the browser's, minted and dropped per press rather than kept as
|
|
238
|
+
* a hidden input in the markup: a menu row is not a form control, and an
|
|
239
|
+
* `<input type=file>` parked in the tree is one more thing for a screen reader
|
|
240
|
+
* to walk past.
|
|
241
|
+
*/
|
|
242
|
+
export function take(key) {
|
|
243
|
+
if (typeof document === 'undefined')
|
|
244
|
+
return;
|
|
245
|
+
const ask = document.createElement('input');
|
|
246
|
+
ask.type = 'file';
|
|
247
|
+
ask.multiple = true;
|
|
248
|
+
ask.onchange = () => void hold(key, Array.from(ask.files ?? []));
|
|
249
|
+
ask.click();
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Hands over everything held to go with a message, and lets go of it.
|
|
253
|
+
*
|
|
254
|
+
* The turn carries each file from here — an image as its `data:` URI, a text
|
|
255
|
+
* file as its words. The object URLs are left standing, so a tab already open
|
|
256
|
+
* on one keeps showing it.
|
|
257
|
+
*/
|
|
258
|
+
export function spend(key) {
|
|
259
|
+
const was = read(key);
|
|
260
|
+
if (!was.held.length && !was.refused)
|
|
261
|
+
return [];
|
|
262
|
+
write(key, { ...was, held: [], refused: null });
|
|
263
|
+
return was.held;
|
|
216
264
|
}
|
|
217
265
|
/** Lets go of a file, and of the handle the browser minted for it. */
|
|
218
266
|
export function drop(key, id) {
|
package/dist/speech.d.ts
CHANGED
|
@@ -1,16 +1,37 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import { type Refusal } from '@hanzo/voice';
|
|
3
|
+
import { type VoiceProfile } from './cast.js';
|
|
2
4
|
/** Strips code blocks, links, headers, and markdown markup for natural audio speech. */
|
|
3
5
|
export declare function cleanForSpeech(raw: string): string;
|
|
6
|
+
/**
|
|
7
|
+
* The browser voice that reads when the platform refuses: English, the cast
|
|
8
|
+
* voice's accent where this browser has it, its register where a name says so.
|
|
9
|
+
* Read per sentence, because the browser loads its voice list asynchronously.
|
|
10
|
+
*/
|
|
11
|
+
export declare function standIn({ gender, accent }: VoiceProfile, voices: {
|
|
12
|
+
name: string;
|
|
13
|
+
lang: string;
|
|
14
|
+
}[]): string | undefined;
|
|
4
15
|
export declare function stopAgentSpeech(): void;
|
|
5
16
|
export interface SpeakOptions {
|
|
6
17
|
agent?: string;
|
|
18
|
+
/** The reader's IAM bearer (`useIamToken`). Without one the platform refuses
|
|
19
|
+
* and the browser reads, which is reported through `onRefusal`. */
|
|
20
|
+
token?: string | null;
|
|
21
|
+
/** Speaking began. Fires at once: the request for the audio is part of it. */
|
|
7
22
|
onStart?: () => void;
|
|
23
|
+
/** Speaking stopped, for any reason: played out, stopped, or replaced. */
|
|
8
24
|
onEnd?: () => void;
|
|
9
25
|
onError?: (err: unknown) => void;
|
|
26
|
+
/** The platform refused, and whether the browser stood in. */
|
|
27
|
+
onRefusal?: (refusal: Refusal) => void;
|
|
10
28
|
}
|
|
11
29
|
/**
|
|
12
|
-
* Speak text in the assigned voice of an agent.
|
|
30
|
+
* Speak text in the assigned voice of an agent, through the platform.
|
|
13
31
|
* Returns a cancel function.
|
|
32
|
+
*
|
|
33
|
+
* Call it from the click that asks for it: the player is made inside that
|
|
34
|
+
* gesture, which is what lets Safari play the reply when it arrives.
|
|
14
35
|
*/
|
|
15
36
|
export declare function speakAgent(text: string, options?: SpeakOptions): () => void;
|
|
16
37
|
/**
|
|
@@ -19,6 +40,7 @@ export declare function speakAgent(text: string, options?: SpeakOptions): () =>
|
|
|
19
40
|
export declare function useAgentSpeech(defaultAgent?: string): {
|
|
20
41
|
speaking: boolean;
|
|
21
42
|
currentText: string;
|
|
43
|
+
refusal: Refusal;
|
|
22
44
|
speak: (text: string, agentName?: string) => void;
|
|
23
45
|
stop: () => void;
|
|
24
46
|
toggle: (text: string, agentName?: string) => void;
|