@hanzo/rooms 0.1.38 → 0.1.40
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/README.md +2 -2
- package/dist/Account.js +18 -9
- package/dist/Chat.js +125 -202
- package/dist/Column.js +16 -15
- package/dist/Directory.d.ts +4 -9
- package/dist/Directory.js +60 -155
- package/dist/Me.js +20 -16
- package/dist/Prose.js +5 -4
- package/dist/RightPane.d.ts +20 -28
- package/dist/RightPane.js +107 -327
- package/dist/Settings.js +42 -28
- package/dist/Shell.js +19 -22
- package/dist/dev.js +42 -26
- package/dist/host.d.ts +5 -9
- package/dist/host.js +5 -6
- package/dist/lib/destination.d.ts +6 -6
- package/dist/lib/destination.js +6 -6
- package/dist/lib/host.d.ts +50 -31
- package/dist/lib/host.js +98 -48
- package/dist/lib/limits.d.ts +13 -9
- package/dist/lib/limits.js +20 -0
- package/dist/lib/plans.d.ts +2 -13
- package/dist/lib/plans.js +2 -19
- package/dist/lib/session.d.ts +9 -12
- package/dist/lib/session.js +7 -19
- package/dist/lib/web.d.ts +46 -38
- package/dist/lib/web.js +480 -213
- package/dist/pane.d.ts +4 -57
- package/dist/pane.js +13 -159
- package/package.json +3 -5
- package/dist/Billing.d.ts +0 -1
- package/dist/Billing.js +0 -32
- package/dist/enso.d.ts +0 -72
- package/dist/enso.js +0 -112
- package/dist/model.d.ts +0 -4
- package/dist/model.js +0 -44
package/dist/RightPane.d.ts
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { type ChatMessage } from '@hanzo/ai/react';
|
|
2
|
+
import { type Deck, type Kind, type Tabs } from '@hanzo/build';
|
|
2
3
|
import { type Api } from './lib/files.js';
|
|
4
|
+
import { type Held } from './pane.js';
|
|
5
|
+
/** A conversation's panel before anyone has arranged it. */
|
|
6
|
+
export declare const SEED: Tabs;
|
|
3
7
|
/**
|
|
4
8
|
* One thing the conversation carries.
|
|
5
9
|
*
|
|
6
|
-
* `href` is where it can be opened and `body`
|
|
10
|
+
* `href` is where it can be opened and `body` the text of something that was
|
|
7
11
|
* never a file — a fenced block exists only in the transcript until somebody
|
|
8
|
-
*
|
|
12
|
+
* looks at it, and then it is rendered from these bytes.
|
|
9
13
|
*/
|
|
10
14
|
export interface Mark {
|
|
11
15
|
id: string;
|
|
@@ -13,36 +17,24 @@ export interface Mark {
|
|
|
13
17
|
kind: 'image' | 'code' | 'file';
|
|
14
18
|
href?: string;
|
|
15
19
|
body?: string;
|
|
16
|
-
/** The type a
|
|
20
|
+
/** The type a browser should take it as, so it renders rather than downloads. */
|
|
17
21
|
mime?: string;
|
|
18
22
|
/** A workspace file's id: it opens through a signed address minted when it is looked at. */
|
|
19
23
|
file?: string;
|
|
20
24
|
}
|
|
21
25
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* honestly looks like.
|
|
25
|
-
*/
|
|
26
|
-
export declare function RightPane({ said, onClose, }: {
|
|
27
|
-
said?: ChatMessage[];
|
|
28
|
-
onClose?: () => void;
|
|
29
|
-
}): import("react").JSX.Element;
|
|
30
|
-
/**
|
|
31
|
-
* Opens a mark in this conversation's browser.
|
|
26
|
+
* Opens a mark as a page tab: a row under Artifacts or Sources, or a file a
|
|
27
|
+
* message carries (Chat's chips).
|
|
32
28
|
*
|
|
33
|
-
* A
|
|
34
|
-
*
|
|
35
|
-
* bytes
|
|
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.
|
|
29
|
+
* A block is rendered from its own bytes, which the tab keeps, so it is there
|
|
30
|
+
* after a reload. A workspace file opens through a signed address minted now,
|
|
31
|
+
* the bytes this document still holds first (`localOf`).
|
|
44
32
|
*/
|
|
45
|
-
export declare function
|
|
46
|
-
/**
|
|
47
|
-
|
|
48
|
-
|
|
33
|
+
export declare function look(deck: Deck, m: Mark, api: Api): Promise<void>;
|
|
34
|
+
/** Chat's kinds of tab, over the conversation `said` and the files `held` for the next message. */
|
|
35
|
+
export declare function useKinds({ at, said, held, deck }: {
|
|
36
|
+
at: string;
|
|
37
|
+
said: ChatMessage[];
|
|
38
|
+
held: Held[];
|
|
39
|
+
deck: Deck;
|
|
40
|
+
}): Kind[];
|
package/dist/RightPane.js
CHANGED
|
@@ -1,85 +1,40 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// channels, hand-written into the markup beside a roster of eleven agents. It
|
|
9
|
-
// drew a NAVIGATION surface in the one place the frame reserves for a room's
|
|
10
|
-
// own contents, duplicating the rail on the opposite edge, and it did it in
|
|
11
|
-
// sky blue against a design that has no hue in it. Nothing here reads a list
|
|
12
|
-
// somebody typed.
|
|
3
|
+
// What Chat puts in the one side panel (@hanzo/build `Panel`, the panel Dev's
|
|
4
|
+
// runs use): Artifacts, what the conversation made, and Sources, what went into
|
|
5
|
+
// it and what it looked up. Pressing either opens it as a page tab — an
|
|
6
|
+
// answer's `index.html` rendered from its own bytes, a picture, a workspace
|
|
7
|
+
// file — beside the two.
|
|
13
8
|
//
|
|
14
9
|
// OUTPUTS AND SOURCES ARE DERIVED FROM THE TURNS, never from a second list kept
|
|
15
|
-
// in step with them
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
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.
|
|
23
|
-
//
|
|
24
|
-
// THE COLUMN IS STACKED, NOT SPLIT. The reference this follows puts a ~300px
|
|
25
|
-
// summary beside a wide browser, which needs about 900px of column; the frame
|
|
26
|
-
// gives this one 200–480 (`FLOOR`/`CEIL` in `Shell.tsx`), so side by side would
|
|
27
|
-
// leave the browser about 80px wide. Stacked, both are usable at every width
|
|
28
|
-
// the frame allows, and the pin earns its keep — hiding the summary gives the
|
|
29
|
-
// browser the whole height rather than a slightly wider strip.
|
|
10
|
+
// in step with them: the conversation is the record, so a fenced block in an
|
|
11
|
+
// answer IS an output (@hanzo/build `artifacts`), and an image in a question, a
|
|
12
|
+
// file it carried, or a block in it that names a file IS a source. The one
|
|
13
|
+
// thing listed that is not yet a turn is what is held for the next message
|
|
14
|
+
// (`hold` in `pane.ts`), which a send spends into one.
|
|
30
15
|
//
|
|
31
|
-
// The
|
|
32
|
-
//
|
|
16
|
+
// The panel's tabs, their order and the open one are kept per conversation by
|
|
17
|
+
// @hanzo/build (`useDeck`, scope `chat:<channel>`); the frame owns the column
|
|
18
|
+
// it stands in (`Beside` in `Shell.tsx`).
|
|
33
19
|
import { useMemo, useState } from 'react';
|
|
34
|
-
import {
|
|
35
|
-
import { Box, DropdownMenu, DropdownMenuContent, DropdownMenuCheckboxItem, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, Text,
|
|
20
|
+
import { FileCode, Image as ImageMark, Paperclip, Plus, Search } from 'lucide-react';
|
|
21
|
+
import { Box, DropdownMenu, DropdownMenuContent, DropdownMenuCheckboxItem, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, Text, YStack, } from '@hanzo/ui';
|
|
36
22
|
import { useAi } from '@hanzo/ai/react';
|
|
23
|
+
import { artifacts, name as titleOf, PAGE } from '@hanzo/build';
|
|
37
24
|
import { Card, Quiet, Row } from './card.js';
|
|
38
25
|
import { said as told } from './lib/attach.js';
|
|
39
26
|
import { download, fileOf } from './lib/files.js';
|
|
40
27
|
import { org } from './lib/session.js';
|
|
41
|
-
import {
|
|
42
|
-
import { address, channel, name as titleOf, openTab, pickTab, pin, shutTab, take, usePane, } from './pane.js';
|
|
28
|
+
import { localOf, take } from './pane.js';
|
|
43
29
|
import { useProviders, useSkills } from './tools.js';
|
|
44
30
|
import { activate, startDevice } from './connect.js';
|
|
45
|
-
/**
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
html: 'text/html',
|
|
53
|
-
htm: 'text/html',
|
|
54
|
-
svg: 'image/svg+xml',
|
|
55
|
-
css: 'text/css',
|
|
56
|
-
csv: 'text/csv',
|
|
57
|
-
json: 'application/json',
|
|
58
|
-
md: 'text/markdown',
|
|
59
|
-
markdown: 'text/markdown',
|
|
60
|
-
txt: 'text/plain',
|
|
61
|
-
};
|
|
62
|
-
/** The extension, lowercased, or '' where a name has none. */
|
|
63
|
-
const suffix = (s) => {
|
|
64
|
-
const dot = s.lastIndexOf('.');
|
|
65
|
-
return dot > 0 ? s.slice(dot + 1).toLowerCase() : '';
|
|
66
|
-
};
|
|
67
|
-
/**
|
|
68
|
-
* What a fence is called.
|
|
69
|
-
*
|
|
70
|
-
* AN INFO STRING IS A FILENAME OR A LANGUAGE, and telling them apart is the
|
|
71
|
-
* whole of it: a model writing to a file names it, and one showing a snippet
|
|
72
|
-
* names the language. A dot and no spaces is a name; anything else is a
|
|
73
|
-
* language, said as what it is rather than dressed up as a file that does not
|
|
74
|
-
* exist.
|
|
75
|
-
*/
|
|
76
|
-
const fenceName = (info, n) => {
|
|
77
|
-
const word = info.trim().split(/\s+/)[0] ?? '';
|
|
78
|
-
if (!word)
|
|
79
|
-
return `Block ${n}`;
|
|
80
|
-
if (word.includes('.') && suffix(word))
|
|
81
|
-
return word;
|
|
82
|
-
return `${word} block`;
|
|
31
|
+
/** A conversation's panel before anyone has arranged it. */
|
|
32
|
+
export const SEED = {
|
|
33
|
+
tabs: [
|
|
34
|
+
{ id: 'artifacts', kind: 'artifacts', title: 'Artifacts' },
|
|
35
|
+
{ id: 'sources', kind: 'sources', title: 'Sources' },
|
|
36
|
+
],
|
|
37
|
+
at: 'artifacts',
|
|
83
38
|
};
|
|
84
39
|
/** The text of a turn, whichever shape the wire used. */
|
|
85
40
|
const words = (m) => typeof m.content === 'string'
|
|
@@ -91,197 +46,133 @@ const words = (m) => typeof m.content === 'string'
|
|
|
91
46
|
const pictures = (m) => Array.isArray(m.content)
|
|
92
47
|
? m.content.flatMap((p) => (p.type === 'image_url' && p.image_url?.url ? [p.image_url.url] : []))
|
|
93
48
|
: [];
|
|
94
|
-
/**
|
|
95
|
-
* What the assistant made in this conversation.
|
|
96
|
-
*
|
|
97
|
-
* Fenced blocks and pictures, in the order they were said. The id carries the
|
|
98
|
-
* turn's index so it survives a re-render without a counter of its own, and so
|
|
99
|
-
* two identical blocks in one conversation stay two rows.
|
|
100
|
-
*/
|
|
49
|
+
/** What the assistant made: fenced blocks and pictures, in the order they were said. */
|
|
101
50
|
function outputs(said) {
|
|
102
51
|
const out = [];
|
|
103
52
|
said.forEach((m, turn) => {
|
|
104
53
|
if (m.role !== 'assistant')
|
|
105
54
|
return;
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
n += 1;
|
|
109
|
-
const named = fenceName(info, n);
|
|
110
|
-
out.push({
|
|
111
|
-
id: `o${turn}.${n}`,
|
|
112
|
-
name: named,
|
|
113
|
-
kind: 'code',
|
|
114
|
-
body,
|
|
115
|
-
mime: MIME[suffix(named) || info.trim().toLowerCase()] ?? 'text/plain',
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
pictures(m).forEach((href, i) => {
|
|
119
|
-
out.push({ id: `oi${turn}.${i}`, name: 'Generated image', kind: 'image', href });
|
|
120
|
-
});
|
|
55
|
+
artifacts(words(m)).forEach((a, n) => out.push({ id: `o${turn}.${n}`, name: a.name, kind: 'code', body: a.body, mime: a.mime }));
|
|
56
|
+
pictures(m).forEach((href, i) => out.push({ id: `oi${turn}.${i}`, name: 'Generated image', kind: 'image', href }));
|
|
121
57
|
});
|
|
122
58
|
return out;
|
|
123
59
|
}
|
|
124
60
|
/**
|
|
125
|
-
* What went into this conversation: pictures asked about, files
|
|
126
|
-
* under their names (`compose` in `lib/attach`), and files
|
|
127
|
-
* message.
|
|
61
|
+
* What went into this conversation: pictures asked about, files a turn carried,
|
|
62
|
+
* files sent fenced under their names (`compose` in `lib/attach`), and files
|
|
63
|
+
* held for the next message.
|
|
128
64
|
*/
|
|
129
65
|
function sources(said, held) {
|
|
130
66
|
const out = [];
|
|
131
67
|
said.forEach((m, turn) => {
|
|
132
68
|
if (m.role !== 'user')
|
|
133
69
|
return;
|
|
134
|
-
pictures(m).forEach((href, i) => {
|
|
135
|
-
out.push({ id: `s${turn}.${i}`, name: 'Attached image', kind: 'image', href });
|
|
136
|
-
});
|
|
137
|
-
// Files a turn carried by reference: their names in the turn, their bytes in the workspace.
|
|
70
|
+
pictures(m).forEach((href, i) => out.push({ id: `s${turn}.${i}`, name: 'Attached image', kind: 'image', href }));
|
|
138
71
|
const read = told(words(m));
|
|
139
72
|
for (const f of read.carried?.attached ?? []) {
|
|
140
73
|
out.push({ id: `sw${turn}.${f.id}`, name: f.name, kind: f.type.startsWith('image/') ? 'image' : 'file', file: f.id, mime: f.type });
|
|
141
74
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
continue;
|
|
148
|
-
out.push({ id: `sf${turn}.${n}`, name: named, kind: 'file', body, mime: MIME[suffix(named)] ?? 'text/plain' });
|
|
149
|
-
}
|
|
75
|
+
// A block sent under a file's name; a block that names only a language is not a source.
|
|
76
|
+
artifacts(read.text).forEach((a, n) => {
|
|
77
|
+
if (a.name.includes('.'))
|
|
78
|
+
out.push({ id: `sf${turn}.${n}`, name: a.name, kind: 'file', body: a.body, mime: a.mime });
|
|
79
|
+
});
|
|
150
80
|
});
|
|
151
81
|
for (const h of held) {
|
|
152
82
|
out.push({ id: h.id, name: h.name, kind: h.type.startsWith('image/') ? 'image' : 'file', href: h.href || undefined, file: h.file?.id, mime: h.type });
|
|
153
83
|
}
|
|
154
84
|
return out;
|
|
155
85
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
*/
|
|
166
|
-
export function RightPane({ said = [], onClose, }) {
|
|
167
|
-
const open = useOpen();
|
|
168
|
-
// ONE KEY, DERIVED, so the pane follows the conversation without anything
|
|
169
|
-
// having to tell it that the conversation changed.
|
|
170
|
-
const at = channel(open);
|
|
171
|
-
const pane = usePane(at);
|
|
172
|
-
return (_jsxs(YStack, { flex: 1, minH: 320, bg: "$background", borderLeftWidth: 1, borderColor: "$borderColor", children: [pane.pinned ? _jsx(Summary, { at: at, said: said, held: pane.held }) : null, _jsx(Browser, { at: at, pane: pane, onClose: onClose })] }));
|
|
173
|
-
}
|
|
174
|
-
/** The two cards: out of the conversation, and into it. */
|
|
175
|
-
function Summary({ at, said, held }) {
|
|
176
|
-
// Recomputed only when the turns or the held files change — a channel switch
|
|
177
|
-
// remounts with different turns and costs one pass over them, never a read.
|
|
178
|
-
const made = useMemo(() => outputs(said), [said]);
|
|
179
|
-
const took = useMemo(() => sources(said, held), [said, held]);
|
|
180
|
-
return (_jsxs(YStack, { shrink: 0,
|
|
181
|
-
// A CAP AND NOT A HEIGHT. The summary is as tall as it needs to be until
|
|
182
|
-
// it would take the browser's half, and scrolls after that — so two rows
|
|
183
|
-
// do not reserve a screen and thirty do not push the browser off the
|
|
184
|
-
// bottom of the column.
|
|
185
|
-
maxH: "45%", overflowY: "auto", overflowX: "hidden", p: "$3", gap: "$3", borderBottomWidth: 1, borderColor: "$borderColor", children: [_jsx(Marks, { at: at, kind: "outputs", title: "Outputs", icon: _jsx(FileCode, { size: 16, "aria-hidden": true }), marks: made, children: "This conversation has produced nothing yet." }), _jsx(Marks, { at: at, kind: "sources", title: "Sources", icon: _jsx(Paperclip, { size: 16, "aria-hidden": true }), marks: took, children: "Nothing has been put into this conversation yet." })] }));
|
|
86
|
+
/** What the conversation looked up: each call, what it asked, and what came back, keyed by the call. */
|
|
87
|
+
function lookups(said) {
|
|
88
|
+
const back = new Map();
|
|
89
|
+
for (const m of said)
|
|
90
|
+
if (m.role === 'tool' && m.tool_call_id)
|
|
91
|
+
back.set(m.tool_call_id, typeof m.content === 'string' ? m.content : '');
|
|
92
|
+
return said
|
|
93
|
+
.flatMap((m) => m.tool_calls ?? [])
|
|
94
|
+
.map((call) => ({ id: call.id, name: call.function.name, asked: call.function.arguments, got: back.get(call.id) ?? null }));
|
|
186
95
|
}
|
|
96
|
+
const GLYPH = { image: ImageMark, code: FileCode, file: Paperclip };
|
|
187
97
|
/**
|
|
188
|
-
*
|
|
189
|
-
*
|
|
98
|
+
* Opens a mark as a page tab: a row under Artifacts or Sources, or a file a
|
|
99
|
+
* message carries (Chat's chips).
|
|
190
100
|
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
101
|
+
* A block is rendered from its own bytes, which the tab keeps, so it is there
|
|
102
|
+
* after a reload. A workspace file opens through a signed address minted now,
|
|
103
|
+
* the bytes this document still holds first (`localOf`).
|
|
193
104
|
*/
|
|
194
|
-
function
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
105
|
+
export async function look(deck, m, api) {
|
|
106
|
+
if (m.body !== undefined)
|
|
107
|
+
return deck.open({ kind: PAGE, title: m.name, body: m.body, type: m.mime });
|
|
108
|
+
if (m.href)
|
|
109
|
+
return deck.open({ kind: PAGE, title: m.name, url: m.href, type: m.mime });
|
|
110
|
+
if (!m.file)
|
|
111
|
+
return;
|
|
112
|
+
const near = localOf(m.file);
|
|
113
|
+
if (near)
|
|
114
|
+
return deck.open({ kind: PAGE, title: m.name, url: near, type: m.mime });
|
|
115
|
+
try {
|
|
116
|
+
const f = await fileOf(api, m.file);
|
|
117
|
+
deck.open({ kind: PAGE, title: m.name, url: await download(api, f.bucket, f.key), type: m.mime });
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
// A file forgotten or a store unreachable opens nothing; the row stays.
|
|
121
|
+
}
|
|
203
122
|
}
|
|
204
|
-
/**
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
* only read the source of.
|
|
212
|
-
*/
|
|
213
|
-
export async function look(at, m, api) {
|
|
214
|
-
const href = await addressOf(m, api);
|
|
215
|
-
if (href)
|
|
216
|
-
openTab(at, href, m.name);
|
|
123
|
+
/** A list of marks, every one of them; pressing one opens it. */
|
|
124
|
+
function Marks({ kind, title, icon, marks, deck, action, children }) {
|
|
125
|
+
const ai = useAi();
|
|
126
|
+
return (_jsxs(Card, { kind: kind, icon: icon, title: title, action: action, children: [marks.length === 0 ? _jsx(Quiet, { children: children }) : null, marks.map((m) => {
|
|
127
|
+
const Glyph = GLYPH[m.kind];
|
|
128
|
+
return (_jsx(Row, { icon: _jsx(Glyph, { size: 16, "aria-hidden": true }), onPress: () => void look(deck, m, ai), children: m.name }, m.id));
|
|
129
|
+
})] }));
|
|
217
130
|
}
|
|
218
|
-
/**
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
131
|
+
/** Chat's kinds of tab, over the conversation `said` and the files `held` for the next message. */
|
|
132
|
+
export function useKinds({ at, said, held, deck }) {
|
|
133
|
+
const made = useMemo(() => outputs(said), [said]);
|
|
134
|
+
const took = useMemo(() => sources(said, held), [said, held]);
|
|
135
|
+
const done = useMemo(() => lookups(said), [said]);
|
|
136
|
+
return [
|
|
137
|
+
{
|
|
138
|
+
id: 'artifacts',
|
|
139
|
+
label: 'Artifacts',
|
|
140
|
+
render: () => (_jsx(Marks, { kind: "outputs", title: "Outputs", icon: _jsx(FileCode, { size: 16, "aria-hidden": true }), marks: made, deck: deck, children: "This conversation has produced nothing yet." })),
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
id: 'sources',
|
|
144
|
+
label: 'Sources',
|
|
145
|
+
render: () => (_jsxs(YStack, { gap: "$3", children: [_jsx(Marks, { kind: "sources", title: "Sources", icon: _jsx(Paperclip, { size: 16, "aria-hidden": true }), marks: took, deck: deck, action: _jsx(Add, { at: at }), children: "Nothing has been put into this conversation yet." }), _jsxs(Card, { kind: "lookups", icon: _jsx(Search, { size: 16, "aria-hidden": true }), title: "Looked up", children: [done.length === 0 ? _jsx(Quiet, { children: "Nothing was looked up for this conversation yet." }) : null, done.map((call) => (_jsxs(YStack, { gap: "$1", p: "$2", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(Text, { fontSize: "$2", color: "$ink", children: call.name }), _jsx(Text, { fontSize: "$1", color: "$soft", numberOfLines: 2, children: call.asked }), _jsx(Text, { fontSize: "$1", color: "$soft", numberOfLines: 6, children: call.got ?? 'Running…' })] }, call.id)))] })] })),
|
|
146
|
+
},
|
|
147
|
+
];
|
|
234
148
|
}
|
|
235
149
|
/**
|
|
236
150
|
* A menu row's words.
|
|
237
151
|
*
|
|
238
152
|
* gui RENDERS NO BARE STRING. Every view is a react-native-web `View`, which
|
|
239
|
-
* refuses a raw text child — `DropdownMenuItem`
|
|
240
|
-
* `DropdownMenuSubTrigger` does not
|
|
241
|
-
* "Unexpected text node" and took the whole menu down with it. One wrapper for
|
|
242
|
-
* every row means the difference between those two components stops mattering.
|
|
153
|
+
* refuses a raw text child — `DropdownMenuItem` wraps one for you and
|
|
154
|
+
* `DropdownMenuSubTrigger` does not — so every row is wrapped once here.
|
|
243
155
|
*/
|
|
244
156
|
const Word = ({ children }) => (_jsx(Text, { fontSize: "$2", color: "$ink", numberOfLines: 1, children: children }));
|
|
245
|
-
/** A menu row that is a statement rather than a control
|
|
246
|
-
* what was refused, which code to type. Wrapped for the same reason. */
|
|
157
|
+
/** A menu row that is a statement rather than a control, wrapped for the same reason. */
|
|
247
158
|
const Note = ({ children }) => (_jsx(DropdownMenuLabel, { children: _jsx(Text, { fontSize: "$2", color: "$soft", children: children }) }));
|
|
248
159
|
/**
|
|
249
|
-
* The
|
|
160
|
+
* The `+` on Sources: everything a reader can bring into this conversation.
|
|
250
161
|
*
|
|
251
162
|
* EVERY ROW DOES SOMETHING. Files come off the reader's own disk. Skills are
|
|
252
|
-
* `GET /v1/tools/skills` and toggling one is `PUT /v1/tools/activation`,
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
* There is no "Plugins" row. `/v1/tools/plugins` reads like the next one and is
|
|
259
|
-
* an operator's inventory of what this deployment booted with — see the note in
|
|
260
|
-
* `tools.ts`. A row offering a reader subsystems they cannot switch is a row
|
|
261
|
-
* that does nothing.
|
|
262
|
-
*
|
|
263
|
-
* ONE PANEL, NOT FLYOUTS, and that is a fact about WHERE this menu is rather
|
|
264
|
-
* than a preference. The reference has "Use plugins ▸" opening a submenu, which
|
|
265
|
-
* works when the menu has screen to its side; this one hangs off a `+` at the
|
|
266
|
-
* right edge of a column that is itself at the right edge of the window.
|
|
267
|
-
* Measured at 1440: the sub-panel opened at x=1414 and is 190 wide, so 184px of
|
|
268
|
-
* it — every row in it — was off the screen, and `Menu.SubContent` publishes no
|
|
269
|
-
* placement to flip it with. Sections in one panel cannot fall off anything,
|
|
270
|
-
* and they put a skill one press away instead of two.
|
|
163
|
+
* `GET /v1/tools/skills` and toggling one is `PUT /v1/tools/activation`, the
|
|
164
|
+
* one write path that turns skills, plugins and connectors into callable tools.
|
|
165
|
+
* Services are `GET /v1/integrations/connectors/providers`, connected by the
|
|
166
|
+
* device flow their cards declare. One panel, not flyouts: it hangs off the
|
|
167
|
+
* right edge of the window, where a submenu has no room to open.
|
|
271
168
|
*/
|
|
272
169
|
function Add({ at }) {
|
|
273
170
|
const ai = useAi();
|
|
274
171
|
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, { api: ai, org: org() }), children: _jsx(Word, { children: "Attach files" }) }), _jsx(DropdownMenuSeparator, {}), _jsx(Skills, {}), _jsx(DropdownMenuSeparator, {}), _jsx(Services, {})] })] }));
|
|
275
172
|
}
|
|
276
173
|
/**
|
|
277
|
-
* What this org's agents can reach for, as
|
|
278
|
-
*
|
|
279
|
-
* A CHECKBOX ROW AND NOT A TICK IN A LABEL. `DropdownMenuCheckboxItem` is
|
|
280
|
-
* `role="menuitemcheckbox"` with `aria-checked`, so a screen reader says
|
|
281
|
-
* "checked" where a glyph in the text says nothing at all — and it draws the
|
|
282
|
-
* mark in gui's own indicator well, which a hand-rolled column beside the label
|
|
283
|
-
* cannot line up with. Activation is a two-state fact; this is the component
|
|
284
|
-
* for one.
|
|
174
|
+
* What this org's agents can reach for, as checkbox rows: `menuitemcheckbox`
|
|
175
|
+
* with `aria-checked`, so the state is read as well as seen.
|
|
285
176
|
*/
|
|
286
177
|
function Skills() {
|
|
287
178
|
const { rows, trouble } = useSkills();
|
|
@@ -298,14 +189,10 @@ function Skills() {
|
|
|
298
189
|
return (_jsxs(_Fragment, { children: [_jsx(Note, { children: "Skills" }), rows.map((s) => {
|
|
299
190
|
const lit = isOn(s.name, s.activated);
|
|
300
191
|
return (_jsx(DropdownMenuCheckboxItem, { checked: lit,
|
|
301
|
-
//
|
|
302
|
-
// menu that shuts on the first makes it two.
|
|
192
|
+
// Held open: switching two skills on is one errand.
|
|
303
193
|
onSelect: (e) => e?.preventDefault(), onCheckedChange: () => {
|
|
304
194
|
setBusy(s.name);
|
|
305
|
-
//
|
|
306
|
-
// we hold rather than flipping our own flag: a refusal, a clamp,
|
|
307
|
-
// or another tab's change is visible in the reply and invisible
|
|
308
|
-
// in an optimistic toggle.
|
|
195
|
+
// The answer is the whole set, so a refusal or another tab's change shows.
|
|
309
196
|
void activate(s.name, !lit)
|
|
310
197
|
.then((enabled) => setOn(new Set(enabled)))
|
|
311
198
|
.catch(() => { })
|
|
@@ -313,15 +200,7 @@ function Skills() {
|
|
|
313
200
|
}, children: _jsx(Word, { children: busy === s.name ? `${s.name}…` : s.name }) }, s.name));
|
|
314
201
|
})] }));
|
|
315
202
|
}
|
|
316
|
-
/**
|
|
317
|
-
* What this account could connect.
|
|
318
|
-
*
|
|
319
|
-
* NARROWED TO THE ONE INTAKE THIS PANE CAN FINISH. A provider card declares its
|
|
320
|
-
* methods — `device`, `oauth`, `token` — and the device flow is the only one
|
|
321
|
-
* that completes without a redirect back into a page this column does not own.
|
|
322
|
-
* Offering the others here would be offering a press that goes nowhere; they
|
|
323
|
-
* are connected from the Directory, which is built for it.
|
|
324
|
-
*/
|
|
203
|
+
/** What this account could connect, narrowed to the device flow, the one intake this menu can finish. */
|
|
325
204
|
function Services() {
|
|
326
205
|
const { rows, trouble } = useProviders();
|
|
327
206
|
const [step, setStep] = useState(null);
|
|
@@ -336,109 +215,10 @@ function Services() {
|
|
|
336
215
|
setStep(`Starting ${p.name}…`);
|
|
337
216
|
void startDevice(p.id)
|
|
338
217
|
.then((begun) => {
|
|
339
|
-
// The code is the reader's to type at the provider's page
|
|
340
|
-
// it is said here and the page is opened for them.
|
|
218
|
+
// The code is the reader's to type at the provider's page.
|
|
341
219
|
setStep(`Enter ${begun.userCode} at ${titleOf(begun.verifyUrl)}`);
|
|
342
220
|
window.open(begun.verifyUrl, '_blank', 'noopener,noreferrer');
|
|
343
221
|
})
|
|
344
222
|
.catch((e) => setStep(String(e?.message ?? 'Could not start')));
|
|
345
223
|
}, children: _jsx(Word, { children: p.name }) }, p.id)))] }));
|
|
346
224
|
}
|
|
347
|
-
/** The tab strip, the address, and whatever the chosen tab is showing. */
|
|
348
|
-
function Browser({ at, pane, onClose, }) {
|
|
349
|
-
const leaf = pane.tabs.find((t) => t.id === pane.at) ?? null;
|
|
350
|
-
// Bumping this remounts the frame, which is the only way to reload a document
|
|
351
|
-
// on an origin we cannot script into.
|
|
352
|
-
const [again, reload] = useState(0);
|
|
353
|
-
return (_jsxs(YStack, { flex: 1, minH: 0, children: [_jsxs(XStack
|
|
354
|
-
// 44 IS THE FLOOR A THUMB NEEDS, and the strip scrolls rather than
|
|
355
|
-
// squeezing its tabs under it — the rule `tabs.tsx` exists to hold.
|
|
356
|
-
, {
|
|
357
|
-
// 44 IS THE FLOOR A THUMB NEEDS, and the strip scrolls rather than
|
|
358
|
-
// squeezing its tabs under it — the rule `tabs.tsx` exists to hold.
|
|
359
|
-
minH: 44, shrink: 0, items: "center", gap: "$1", px: "$2", overflowX: "auto", overflowY: "hidden", borderBottomWidth: 1, borderColor: "$borderColor", children: [pane.tabs.map((t) => (_jsx(Sheet, { at: at, leaf: t, on: t.id === pane.at }, t.id))), _jsx(Box, { render: "button", onClick: () => openTab(at, 'https://hanzo.ai'), "aria-label": "New tab", p: "$1.5", rounded: "$2", shrink: 0, hoverStyle: { bg: '$hover' }, children: _jsx(Plus, { size: 16, "aria-hidden": true }) }), _jsx(Box, { flex: 1, minW: "$1" }), leaf ? (_jsxs(_Fragment, { children: [_jsx(Box, { render: "button", onClick: () => reload((n) => n + 1), "aria-label": "Reload this tab", p: "$1.5", rounded: "$2", shrink: 0, hoverStyle: { bg: '$hover' }, children: _jsx(RotateCw, { size: 16, "aria-hidden": true }) }), _jsx(Box, { render: "button", onClick: () => window.open(leaf.url, '_blank', 'noopener,noreferrer'), "aria-label": "Open this tab in a new window", p: "$1.5", rounded: "$2", shrink: 0, hoverStyle: { bg: '$hover' }, children: _jsx(ExternalLink, { size: 16, "aria-hidden": true }) })] })) : null, onClose ? (_jsx(Box, { render: "button", onClick: onClose, "aria-label": "Hide side panel", p: "$1.5", rounded: "$2", shrink: 0, hoverStyle: { bg: '$hover' }, children: _jsx(PanelRightClose, { size: 16, "aria-hidden": true }) })) : null] }), leaf ? _jsx(Where, { at: at, leaf: leaf }) : null, _jsx(YStack, { flex: 1, minH: 0, children: leaf ? (_jsx(Preview, { leaf: leaf, held: pane.held }, `${leaf.id}.${again}`)) : (_jsxs(YStack, { flex: 1, items: "center", justify: "center", p: "$5", gap: "$2", children: [_jsx(Quiet, { children: "Nothing open here." }), _jsx(Quiet, { children: "Press an output or a source to look at it, or open a tab." })] })) })] }));
|
|
360
|
-
}
|
|
361
|
-
/** One tab: the thing you press, and the cross that shuts it. */
|
|
362
|
-
function Sheet({ at, leaf, on }) {
|
|
363
|
-
return (_jsxs(XStack, { items: "center", gap: "$1", pl: "$2", pr: "$1", minH: 32, maxW: 140, shrink: 0, rounded: "$2",
|
|
364
|
-
// A CHOSEN TAB IS THE ONLY ONE WITH AN OUTLINE. Everything else in the
|
|
365
|
-
// strip is a plain control that answers to hover, which is the rule the
|
|
366
|
-
// rest of this pane follows.
|
|
367
|
-
borderWidth: 1, borderColor: on ? '$borderColor' : 'transparent', bg: on ? '$raised' : 'transparent', hoverStyle: { bg: on ? '$raised' : '$hover' }, children: [_jsx(Box, { render: "button", onClick: () => pickTab(at, leaf.id), "aria-current": on, flex: 1, minW: 0, children: _jsx(Text, { fontSize: "$2", color: on ? '$ink' : '$soft', numberOfLines: 1, children: leaf.title }) }), _jsx(Box, { render: "button", onClick: () => shutTab(at, leaf.id), "aria-label": `Close ${leaf.title}`, p: "$1", rounded: "$1", hoverStyle: { bg: '$edge' }, children: _jsx(X, { size: 16, "aria-hidden": true }) })] }));
|
|
368
|
-
}
|
|
369
|
-
/**
|
|
370
|
-
* Where the open tab is, and how to go somewhere else.
|
|
371
|
-
*
|
|
372
|
-
* A PATH FOR A FILE AND A FIELD FOR A PAGE, because they are different
|
|
373
|
-
* questions: a blob has no address a reader can retype, so offering one is
|
|
374
|
-
* offering a control that cannot work. The field is the one place in this pane
|
|
375
|
-
* with an outline, which is what the house rule reserves an outline for.
|
|
376
|
-
*/
|
|
377
|
-
function Where({ at, leaf }) {
|
|
378
|
-
const local = leaf.url.startsWith('blob:');
|
|
379
|
-
const [typed, setTyped] = useState(leaf.url);
|
|
380
|
-
if (local) {
|
|
381
|
-
return (_jsx(XStack, { px: "$3", py: "$2", borderBottomWidth: 1, borderColor: "$borderColor", children: _jsx(Text, { fontSize: "$2", color: "$soft", numberOfLines: 1, children: leaf.title }) }));
|
|
382
|
-
}
|
|
383
|
-
return (_jsx(XStack, { items: "center", px: "$2", py: "$2", borderBottomWidth: 1, borderColor: "$borderColor", children: _jsx(XStack, { flex: 1, minW: 0, items: "center", px: "$2", minH: 28, rounded: "$2", borderWidth: 1, borderColor: "$borderColor", bg: "$raised", children: _jsx(Text, { render: _jsx("input", {
|
|
384
|
-
// KEYED BY THE TAB at the call site above, so switching tabs starts
|
|
385
|
-
// this field at the new tab's address rather than carrying the old
|
|
386
|
-
// one's half-typed text across.
|
|
387
|
-
defaultValue: leaf.url, onChange: (e) => setTyped(e.target.value), onKeyDown: (e) => {
|
|
388
|
-
if (e.key === 'Enter')
|
|
389
|
-
openTab(at, address(typed), titleOf(address(typed)));
|
|
390
|
-
}, "aria-label": "Address", spellCheck: false }), width: "100%", borderWidth: 0, outlineStyle: "none", bg: "transparent", color: "var(--foreground)", fontSize: "$2" }) }) }));
|
|
391
|
-
}
|
|
392
|
-
/**
|
|
393
|
-
* What a tab is showing, or why it is not.
|
|
394
|
-
*
|
|
395
|
-
* A BROWSER CANNOT SHOW EVERY FILE AND SAYING SO IS THE FEATURE. An archive, a
|
|
396
|
-
* font, a binary — a frame pointed at one either downloads it or draws nothing,
|
|
397
|
-
* and drawing nothing is indistinguishable from a broken pane. The type comes
|
|
398
|
-
* off the held file where there is one, because a blob address carries no
|
|
399
|
-
* extension to guess from.
|
|
400
|
-
*/
|
|
401
|
-
function Preview({ leaf, held }) {
|
|
402
|
-
const mine = held.find((h) => h.href === leaf.url);
|
|
403
|
-
const type = mine?.type ?? '';
|
|
404
|
-
const ext = suffix(mine?.name ?? leaf.title);
|
|
405
|
-
if (type.startsWith('image/') || ['png', 'jpg', 'jpeg', 'gif', 'webp', 'avif', 'svg'].includes(ext)) {
|
|
406
|
-
return (_jsx(YStack, { flex: 1, minH: 0, items: "center", justify: "center", p: "$3", overflowY: "auto", overflowX: "hidden", children: _jsx(View, { render: _jsx("img", { src: leaf.url, alt: leaf.title }), maxW: "100%", maxH: "100%", objectFit: "contain" }) }));
|
|
407
|
-
}
|
|
408
|
-
// THE LIST IS OF WHAT A FRAME CAN RENDER, not of what a file might be. A type
|
|
409
|
-
// absent from it is unsupported, which is the honest default: guessing that
|
|
410
|
-
// an unknown type will render is how a reader gets a blank rectangle.
|
|
411
|
-
const shows = !mine ||
|
|
412
|
-
type.startsWith('text/') ||
|
|
413
|
-
type === 'application/pdf' ||
|
|
414
|
-
type === 'application/json' ||
|
|
415
|
-
['html', 'htm', 'txt', 'md', 'json', 'csv', 'pdf', 'css', 'js', 'ts'].includes(ext);
|
|
416
|
-
if (!shows) {
|
|
417
|
-
return (_jsxs(YStack, { flex: 1, items: "center", justify: "center", p: "$5", gap: "$2", children: [_jsxs(Quiet, { children: [(ext || type || 'This').toUpperCase(), " previews aren\u2019t supported yet."] }), _jsx(Quiet, { children: "Open this file in another app to view it." })] }));
|
|
418
|
-
}
|
|
419
|
-
return (_jsx(View, { render: _jsx("iframe", { src: leaf.url, title: leaf.title,
|
|
420
|
-
// TWO SANDBOXES, BECAUSE THERE ARE TWO KINDS OF DOCUMENT HERE AND ONE
|
|
421
|
-
// RULE WOULD BE WRONG FOR ONE OF THEM.
|
|
422
|
-
//
|
|
423
|
-
// A `blob:` address INHERITS THE ORIGIN OF THE PAGE THAT MINTED IT — this
|
|
424
|
-
// one. So a model's fenced block granted `allow-same-origin` would run as
|
|
425
|
-
// us, with our `localStorage`, which is where the IAM access token lives
|
|
426
|
-
// (`hanzo_iam_access_token`). An answer that wrote a script tag would be
|
|
427
|
-
// reading the reader's session. It gets `allow-scripts` so a generated
|
|
428
|
-
// page still lays out and runs, and never the pair.
|
|
429
|
-
//
|
|
430
|
-
// A remote page is the opposite case: the iframe takes the REMOTE
|
|
431
|
-
// origin, so `allow-same-origin` grants it nothing of ours and withholding
|
|
432
|
-
// it breaks the site — no cookies, no storage, no fonts on many hosts.
|
|
433
|
-
sandbox: leaf.url.startsWith('blob:')
|
|
434
|
-
? 'allow-scripts allow-popups allow-forms'
|
|
435
|
-
: 'allow-same-origin allow-scripts allow-popups allow-forms' }), width: "100%", height: "100%", borderWidth: 0, bg: "var(--background)" }));
|
|
436
|
-
}
|
|
437
|
-
/** Shows or hides the summary above this conversation's browser. Exported for
|
|
438
|
-
* the conversation header, which is where the control belongs. */
|
|
439
|
-
export function usePinned() {
|
|
440
|
-
const open = useOpen();
|
|
441
|
-
const at = channel(open);
|
|
442
|
-
const pane = usePane(at);
|
|
443
|
-
return [pane.pinned, () => pin(at, !pane.pinned)];
|
|
444
|
-
}
|