@hanzo/rooms 0.1.73 → 0.1.74
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/assets/personas/ada.md +187 -0
- package/assets/personas/adi_shamir.md +155 -0
- package/assets/personas/al_khwarizmi.md +168 -0
- package/assets/personas/al_kindi.md +172 -0
- package/assets/personas/alan_kay.md +154 -0
- package/assets/personas/analyst.md +138 -0
- package/assets/personas/andrew_yao.md +155 -0
- package/assets/personas/auguste_kerckhoffs.md +168 -0
- package/assets/personas/barbara_liskov.md +154 -0
- package/assets/personas/butler_lampson.md +156 -0
- package/assets/personas/charles_babbage.md +175 -0
- package/assets/personas/creative.md +141 -0
- package/assets/personas/curry.md +169 -0
- package/assets/personas/cynthia_dwork.md +158 -0
- package/assets/personas/david_chaum.md +157 -0
- package/assets/personas/david_parnas.md +153 -0
- package/assets/personas/dennis_ritchie.md +153 -0
- package/assets/personas/dijkstra.md +187 -0
- package/assets/personas/donald_knuth.md +156 -0
- package/assets/personas/douglas_engelbart.md +175 -0
- package/assets/personas/edgar_codd.md +151 -0
- package/assets/personas/engineer.md +140 -0
- package/assets/personas/feynman.md +178 -0
- package/assets/personas/finney.md +187 -0
- package/assets/personas/fred_brooks.md +152 -0
- package/assets/personas/geoffrey_hinton.md +194 -0
- package/assets/personas/herbert_simon.md +190 -0
- package/assets/personas/hickey.md +169 -0
- package/assets/personas/hopper.md +191 -0
- package/assets/personas/hubert_dreyfus.md +184 -0
- package/assets/personas/humberto_maturana.md +156 -0
- package/assets/personas/index.json +1 -0
- package/assets/personas/jcr_licklider.md +178 -0
- package/assets/personas/jobs.md +177 -0
- package/assets/personas/joe_armstrong.md +155 -0
- package/assets/personas/john_backus.md +179 -0
- package/assets/personas/john_mccarthy.md +177 -0
- package/assets/personas/jon_postel.md +149 -0
- package/assets/personas/joseph_weizenbaum.md +185 -0
- package/assets/personas/judea_pearl.md +192 -0
- package/assets/personas/karen_sparck_jones.md +154 -0
- package/assets/personas/konrad_zuse.md +175 -0
- package/assets/personas/kunihiko_fukushima.md +152 -0
- package/assets/personas/leslie_lamport.md +156 -0
- package/assets/personas/markov.md +171 -0
- package/assets/personas/mentor.md +141 -0
- package/assets/personas/nancy_lynch.md +154 -0
- package/assets/personas/niklaus_wirth.md +154 -0
- package/assets/personas/norbert_wiener.md +179 -0
- package/assets/personas/north-star.md +138 -0
- package/assets/personas/peter_naur.md +151 -0
- package/assets/personas/pike.md +165 -0
- package/assets/personas/radia_perlman.md +156 -0
- package/assets/personas/raj_reddy.md +154 -0
- package/assets/personas/ralph_merkle.md +152 -0
- package/assets/personas/richard_sutton.md +156 -0
- package/assets/personas/robin_milner.md +149 -0
- package/assets/personas/shafi_goldwasser.md +156 -0
- package/assets/personas/shannon.md +184 -0
- package/assets/personas/stafford_beer.md +157 -0
- package/assets/personas/taher_elgamal.md +180 -0
- package/assets/personas/tesla.md +181 -0
- package/assets/personas/tony_hoare.md +153 -0
- package/assets/personas/torvalds.md +188 -0
- package/assets/personas/tpope.md +163 -0
- package/assets/personas/turing.md +188 -0
- package/assets/personas/whitfield_diffie.md +159 -0
- package/assets/personas/zeekay.md +151 -0
- package/dist/App.js +26 -22
- package/dist/Chat.js +51 -38
- package/dist/Column.d.ts +3 -3
- package/dist/Column.js +5 -27
- package/dist/Me.d.ts +8 -3
- package/dist/Me.js +79 -108
- package/dist/Palette.d.ts +18 -0
- package/dist/Palette.js +131 -0
- package/dist/Rail.js +3 -15
- package/dist/Recents.d.ts +1 -2
- package/dist/Recents.js +9 -9
- package/dist/Shell.d.ts +7 -0
- package/dist/Shell.js +65 -46
- package/dist/Top.d.ts +17 -0
- package/dist/Top.js +58 -0
- package/dist/agents.d.ts +64 -0
- package/dist/agents.js +62 -0
- package/dist/crew.d.ts +26 -5
- package/dist/crew.js +208 -83
- package/dist/help.d.ts +43 -0
- package/dist/help.js +58 -0
- package/dist/lib/web.d.ts +6 -0
- package/dist/lib/web.js +39 -7
- package/dist/recent.d.ts +2 -2
- package/dist/recent.js +3 -5
- package/dist/seek.d.ts +37 -0
- package/dist/seek.js +55 -0
- package/dist/team.js +3 -11
- package/dist/thread.js +7 -2
- package/dist/view.d.ts +44 -0
- package/dist/view.js +98 -0
- package/package.json +5 -5
- package/dist/Account.d.ts +0 -12
- package/dist/Account.js +0 -65
- package/dist/Find.d.ts +0 -7
- package/dist/Find.js +0 -82
- package/dist/Profile.d.ts +0 -4
- package/dist/Profile.js +0 -67
- package/dist/Standing.d.ts +0 -4
- package/dist/Standing.js +0 -16
package/dist/crew.js
CHANGED
|
@@ -1,31 +1,32 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
//
|
|
3
|
+
// Who a conversation can hold, and the sheet that makes one more.
|
|
4
4
|
//
|
|
5
|
-
// The
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
5
|
+
// The composer's `+` › Agents (`useAgentShelf`) lists the org's agents, the
|
|
6
|
+
// default cast first: the core team hanzoai/personas publishes (team.ts), drawn
|
|
7
|
+
// for an org that keeps no row of its own for a member. Picking one adds it to
|
|
8
|
+
// the conversation that is open, and the reply comes back in each voice in the
|
|
9
|
+
// room; picking it again lets it out. The page does not move. Create agent and
|
|
10
|
+
// Browse personas open the sheet (`Hire`, mounted once by `AgentMaker`); Manage
|
|
11
|
+
// agents is Dev's Customize › Agents.
|
|
11
12
|
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
-
// address stay as they are. On a page that is not a chat (`to`), pressing a
|
|
16
|
-
// face opens a chat with that character there. The sidebar's roster row opens a
|
|
17
|
-
// chat with one character alone.
|
|
18
|
-
import { useEffect, useRef, useState } from 'react';
|
|
13
|
+
// The crew strip (`Crew`) is the same list as faces, on Home: pressing one opens
|
|
14
|
+
// a chat with that character there (`to`).
|
|
15
|
+
import { useEffect, useRef, useState, useSyncExternalStore } from 'react';
|
|
19
16
|
import { useRooms } from './host.js';
|
|
20
|
-
import { Camera, Check, Plus, Upload } from 'lucide-react';
|
|
17
|
+
import { ArrowLeft, Camera, Check, Plus, Sparkles, Upload, Users } from 'lucide-react';
|
|
18
|
+
import { generateImages } from '@hanzo/ai';
|
|
21
19
|
import { useAgents, useModels } from '@hanzo/ai/react';
|
|
22
20
|
import { Box, Button, Input, Text, Textarea, View, XStack, YStack } from '@hanzo/ui';
|
|
23
21
|
import { useAi } from './lib/ai.js';
|
|
24
22
|
import { mark } from './lib/mark.js';
|
|
25
23
|
import { reach } from './lib/reach.js';
|
|
26
|
-
import {
|
|
24
|
+
import { Face } from './cast.js';
|
|
27
25
|
import { useOpen } from './open.js';
|
|
28
|
-
import { TEAM, called } from './team.js';
|
|
26
|
+
import { TEAM, called, member } from './team.js';
|
|
27
|
+
import { FIGURES, MEMOJI, PAINTER, draft, line, ordered, title } from './agents.js';
|
|
28
|
+
import { app, go } from './lib/host.js';
|
|
29
|
+
import { site } from './where.js';
|
|
29
30
|
import { bare } from './lib/bare.js';
|
|
30
31
|
import { BAD, mix } from './lib/mix.js';
|
|
31
32
|
/** One disc's edge, and the name under it. */
|
|
@@ -62,24 +63,64 @@ export function useCrew() {
|
|
|
62
63
|
}
|
|
63
64
|
/** The house crew's names, which a reply may be split by even after one has left the room. */
|
|
64
65
|
export const HOUSE = TEAM.map((one) => one.name);
|
|
65
|
-
/**
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
66
|
+
/** Two names for one agent differ only in case ("Dev" and "dev"). */
|
|
67
|
+
const same = (a, b) => a.toLowerCase() === b.toLowerCase();
|
|
68
|
+
// THE SHEET IS OPENED FROM A MENU AND DRAWN BESIDE THE COMPOSER, two places
|
|
69
|
+
// that share no parent, so whether it is open lives here, outside React, the
|
|
70
|
+
// way open.ts keeps the conversation.
|
|
71
|
+
let making = null;
|
|
72
|
+
const heard = new Set();
|
|
73
|
+
const listen = (f) => {
|
|
74
|
+
heard.add(f);
|
|
75
|
+
return () => void heard.delete(f);
|
|
76
|
+
};
|
|
77
|
+
/** Opens the New agent sheet, blank or on its persona list; null shuts it. */
|
|
78
|
+
export function make(how) {
|
|
79
|
+
making = how;
|
|
80
|
+
for (const f of heard)
|
|
81
|
+
f();
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The New agent sheet, wherever the host mounts this once (beside the
|
|
85
|
+
* composer). It is drawn while `make` holds it open, and an agent it makes joins
|
|
86
|
+
* the conversation that is open.
|
|
87
|
+
*/
|
|
88
|
+
export function AgentMaker() {
|
|
89
|
+
const how = useSyncExternalStore(listen, () => making, () => null);
|
|
90
|
+
const { agents: room, openAgent } = useOpen();
|
|
91
|
+
if (!how)
|
|
92
|
+
return null;
|
|
93
|
+
return (_jsx(Hire, { start: how === 'persona', onClose: () => make(null), onMade: (name) => {
|
|
94
|
+
if (!room.some((there) => same(there, name)))
|
|
95
|
+
openAgent(name);
|
|
96
|
+
} }));
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* The composer's `+` › Agents: the org's agents, the default cast first, each
|
|
100
|
+
* by its face; on is in the open conversation, and a pick toggles it there.
|
|
101
|
+
*/
|
|
102
|
+
export function useAgentShelf() {
|
|
103
|
+
const { agents, loading, error } = useCrew();
|
|
104
|
+
const { agents: room, openAgent } = useOpen();
|
|
105
|
+
const pathed = useRooms().path !== null;
|
|
106
|
+
return {
|
|
107
|
+
rows: ordered(agents).map((one) => ({
|
|
108
|
+
id: one.name,
|
|
109
|
+
label: called(one.name),
|
|
110
|
+
icon: _jsx(Face, { src: one.avatar, emoji: one.emoji, name: one.name, size: 16 }),
|
|
111
|
+
note: member(one.name)?.role ?? one.description,
|
|
112
|
+
on: room.some((there) => same(there, one.name)),
|
|
113
|
+
})),
|
|
114
|
+
pick: (id) => openAgent(called(id)),
|
|
115
|
+
loading,
|
|
116
|
+
error: error ? reach(error) : null,
|
|
117
|
+
search: 'Search agents',
|
|
118
|
+
create: { label: 'Create agent', onPress: () => make('new') },
|
|
119
|
+
// Dev's Customize › Agents: a move inside the app where the host reads the
|
|
120
|
+
// path (hanzo.ai), else Dev on hanzo.ai, as Chat's Create file or site goes.
|
|
121
|
+
manage: { label: 'Manage agents', onPress: () => (pathed ? go(app('-/customize/agents')) : window.location.assign(site(app('-/customize/agents')))) },
|
|
122
|
+
browse: { label: 'Browse personas', onPress: () => make('persona') },
|
|
123
|
+
};
|
|
83
124
|
}
|
|
84
125
|
/**
|
|
85
126
|
* `to` is the chat a press opens, for a strip on a page that is not one. Absent,
|
|
@@ -146,23 +187,72 @@ function Said({ id, text }) {
|
|
|
146
187
|
return null;
|
|
147
188
|
return (_jsx(Text, { id: id, role: "alert", fontSize: "$2", color: BAD, children: text }));
|
|
148
189
|
}
|
|
190
|
+
/** A file the host serves, as text; a refusal says which and why. */
|
|
191
|
+
async function text(path) {
|
|
192
|
+
const r = await fetch(path);
|
|
193
|
+
if (!r.ok)
|
|
194
|
+
throw new Error(`${path} did not load (${r.status}).`);
|
|
195
|
+
return r.text();
|
|
196
|
+
}
|
|
197
|
+
/** The roster, read once a page when the list first opens; a failed read is asked again next time. */
|
|
198
|
+
let roster = null;
|
|
199
|
+
const figures = () => (roster ??= text(`${FIGURES}/index.json`)
|
|
200
|
+
.then((t) => JSON.parse(t))
|
|
201
|
+
.catch((e) => {
|
|
202
|
+
roster = null;
|
|
203
|
+
throw e;
|
|
204
|
+
}));
|
|
205
|
+
/** How many matches the list draws; typing narrows the rest. */
|
|
206
|
+
const SHOWN = 50;
|
|
207
|
+
/**
|
|
208
|
+
* Start from a persona: @hanzo/personas' figures by name and one line, the ones
|
|
209
|
+
* with a written PERSONA.md first, searched by either.
|
|
210
|
+
*/
|
|
211
|
+
function Figures({ onPick }) {
|
|
212
|
+
const [all, setAll] = useState(null);
|
|
213
|
+
const [failed, setFailed] = useState(null);
|
|
214
|
+
const [q, setQ] = useState('');
|
|
215
|
+
useEffect(() => {
|
|
216
|
+
let live = true;
|
|
217
|
+
figures().then((list) => live && setAll([...list].sort((a, b) => Number(!a.called) - Number(!b.called))), (e) => live && setFailed(reach(e)));
|
|
218
|
+
return () => {
|
|
219
|
+
live = false;
|
|
220
|
+
};
|
|
221
|
+
}, []);
|
|
222
|
+
const needle = q.trim().toLowerCase();
|
|
223
|
+
const found = (all ?? []).filter((f) => !needle || `${title(f)} ${line(f)}`.toLowerCase().includes(needle));
|
|
224
|
+
return (_jsxs(YStack, { gap: "$2", children: [_jsx(Input, { value: q, onChange: (e) => setQ(e.target.value), placeholder: all ? `Search ${all.length.toLocaleString()} figures` : 'Search figures', "aria-label": "Search personas", autoFocus: true }), _jsx(YStack, { maxH: 360, overflowY: "auto", overflowX: "hidden", role: "group", "aria-label": "Personas", children: found.slice(0, SHOWN).map((f) => (_jsxs(YStack, { render: "button", onClick: () => onPick(f), items: "flex-start", gap: 2, px: "$2", py: "$2", rounded: "$3", cursor: "pointer", hoverStyle: { bg: '$raised' }, focusVisibleStyle: { bg: '$raised' }, children: [_jsx(Text, { fontSize: "$3", fontWeight: "600", color: "$ink", numberOfLines: 1, children: title(f) }), _jsx(Text, { fontSize: "$2", color: "$soft", numberOfLines: 1, children: line(f) })] }, f.slug))) }), _jsx(Text, { fontSize: "$2", color: failed ? BAD : '$soft', role: failed ? 'alert' : undefined, children: failed ??
|
|
225
|
+
(!all
|
|
226
|
+
? 'Reading the personas…'
|
|
227
|
+
: !found.length
|
|
228
|
+
? 'No figure matches.'
|
|
229
|
+
: found.length > SHOWN
|
|
230
|
+
? `${SHOWN} of ${found.length.toLocaleString()} shown. Type to narrow them.`
|
|
231
|
+
: null) })] }));
|
|
232
|
+
}
|
|
149
233
|
/**
|
|
150
234
|
* The sheet that makes an agent.
|
|
151
235
|
*
|
|
152
236
|
* A name, a mind and a spending limit are what the agents API requires; what it
|
|
153
237
|
* does becomes its instructions; the likeness is whichever the maker gives — a
|
|
154
238
|
* photo uploaded onto the round picture (pressed, or dropped on), or a face
|
|
155
|
-
* described and drawn by
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
239
|
+
* described and drawn by Hanzo's image model (`PAINTER`), one call per press of
|
|
240
|
+
* Draw or Regenerate. An agent created with neither a picture nor an emoji has
|
|
241
|
+
* one drawn then, once, from the description or else its name. Every face goes
|
|
242
|
+
* through `mark`, the one crop every saved face takes, and is kept as the row's
|
|
243
|
+
* `avatar`. The row keeps at most one of a picture and an emoji, so choosing one
|
|
244
|
+
* clears the other here too.
|
|
245
|
+
*
|
|
246
|
+
* Start from a persona fills the name, the instructions and the face's
|
|
247
|
+
* description from a figure of @hanzo/personas' roster (agents.ts `draft`);
|
|
248
|
+
* `start` opens the sheet on that list.
|
|
159
249
|
*
|
|
160
250
|
* Create stays off until the required fields are filled in the API's own
|
|
161
251
|
* terms, and a refusal is said in plain words under the field it concerns.
|
|
162
252
|
*
|
|
163
253
|
* EXPORTED, because Contacts adds agents too and there is one way to make one.
|
|
164
254
|
*/
|
|
165
|
-
export function Hire({ onClose, onMade }) {
|
|
255
|
+
export function Hire({ onClose, onMade, start = false }) {
|
|
166
256
|
const { create } = useAgents();
|
|
167
257
|
const { models } = useModels();
|
|
168
258
|
const { client } = useAi();
|
|
@@ -179,15 +269,22 @@ export function Hire({ onClose, onMade }) {
|
|
|
179
269
|
const [wrong, setWrong] = useState(null);
|
|
180
270
|
const [over, setOver] = useState(false);
|
|
181
271
|
const [lit, setLit] = useState(false);
|
|
272
|
+
const [choosing, setChoosing] = useState(start);
|
|
273
|
+
/** A drawing failed on Create, so the next press makes the agent without one. */
|
|
274
|
+
const [faceless, setFaceless] = useState(false);
|
|
182
275
|
const picking = useRef(null);
|
|
183
276
|
useEffect(() => {
|
|
184
277
|
const key = (e) => {
|
|
185
|
-
if (e.key
|
|
278
|
+
if (e.key !== 'Escape')
|
|
279
|
+
return;
|
|
280
|
+
if (choosing)
|
|
281
|
+
setChoosing(false);
|
|
282
|
+
else
|
|
186
283
|
onClose();
|
|
187
284
|
};
|
|
188
285
|
window.addEventListener('keydown', key);
|
|
189
286
|
return () => window.removeEventListener('keydown', key);
|
|
190
|
-
}, [onClose]);
|
|
287
|
+
}, [onClose, choosing]);
|
|
191
288
|
// WHAT CREATE WAITS FOR, in the API's own terms, so a request it would refuse
|
|
192
289
|
// is never sent. Each rule names the field it belongs to.
|
|
193
290
|
const called = name.trim();
|
|
@@ -215,30 +312,28 @@ export function Hire({ onClose, onMade }) {
|
|
|
215
312
|
setWrong({ field: 'picture', text: reach(e) });
|
|
216
313
|
}
|
|
217
314
|
};
|
|
218
|
-
|
|
219
|
-
|
|
315
|
+
/** What a drawing is of: the description, else the name. */
|
|
316
|
+
const subject = look.trim() || called;
|
|
317
|
+
// One call to the image model; `mark` makes the answer the same 256px square
|
|
318
|
+
// every saved picture is, so a drawn face weighs what an uploaded one weighs.
|
|
319
|
+
const paint = async () => {
|
|
320
|
+
const [image] = await generateImages(client, `${subject}, ${MEMOJI}`, { model: PAINTER, n: 1, size: '512x512', response_format: 'b64_json' });
|
|
321
|
+
if (!image?.b64_json)
|
|
322
|
+
throw new Error('The image model answered with no picture.');
|
|
323
|
+
const bytes = Uint8Array.from(atob(image.b64_json), (c) => c.charCodeAt(0));
|
|
324
|
+
const face = await mark(new File([bytes], 'face.png', { type: 'image/png' }));
|
|
325
|
+
setAvatar(face);
|
|
326
|
+
setEmoji('');
|
|
327
|
+
return face;
|
|
328
|
+
};
|
|
220
329
|
const draw = async () => {
|
|
221
|
-
if (!client || !
|
|
330
|
+
if (!client || !subject || busy)
|
|
222
331
|
return;
|
|
223
332
|
setBusy('draw');
|
|
224
333
|
setWrong(null);
|
|
225
334
|
try {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
path: '/v1/images/generations',
|
|
229
|
-
body: {
|
|
230
|
-
prompt: `${look.trim()}, memoji style 3D emoji portrait, head and shoulders, centered, plain background`,
|
|
231
|
-
n: 1,
|
|
232
|
-
size: '512x512',
|
|
233
|
-
response_format: 'b64_json',
|
|
234
|
-
},
|
|
235
|
-
});
|
|
236
|
-
const b64 = out?.data?.[0]?.b64_json;
|
|
237
|
-
if (!b64)
|
|
238
|
-
throw new Error('The image model answered with no picture.');
|
|
239
|
-
const bytes = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
|
|
240
|
-
setAvatar(await mark(new File([bytes], 'face.png', { type: 'image/png' })));
|
|
241
|
-
setEmoji('');
|
|
335
|
+
await paint();
|
|
336
|
+
setFaceless(false);
|
|
242
337
|
}
|
|
243
338
|
catch (e) {
|
|
244
339
|
setWrong({ field: 'picture', text: reach(e) });
|
|
@@ -247,18 +342,48 @@ export function Hire({ onClose, onMade }) {
|
|
|
247
342
|
setBusy(null);
|
|
248
343
|
}
|
|
249
344
|
};
|
|
345
|
+
// A figure fills the sheet: its PERSONA.md is read where one is written.
|
|
346
|
+
const become = async (f) => {
|
|
347
|
+
setChoosing(false);
|
|
348
|
+
setWrong(null);
|
|
349
|
+
try {
|
|
350
|
+
const md = f.called ? await text(`${FIGURES}/${f.slug}.md`) : null;
|
|
351
|
+
const d = draft(f, md);
|
|
352
|
+
setName(d.name);
|
|
353
|
+
setDoes(d.does);
|
|
354
|
+
setLook(d.look);
|
|
355
|
+
setFaceless(false);
|
|
356
|
+
}
|
|
357
|
+
catch (e) {
|
|
358
|
+
setWrong({ field: 'form', text: reach(e) });
|
|
359
|
+
}
|
|
360
|
+
};
|
|
250
361
|
const make = async () => {
|
|
251
362
|
if (!ready || busy)
|
|
252
363
|
return;
|
|
253
364
|
setBusy('make');
|
|
254
365
|
setWrong(null);
|
|
366
|
+
// NO PICTURE CHOSEN, SO ONE IS DRAWN, once. A drawing that fails says why,
|
|
367
|
+
// and the next press makes the agent without one.
|
|
368
|
+
let face = avatar;
|
|
369
|
+
if (!face && !emoji.trim() && !faceless && client) {
|
|
370
|
+
try {
|
|
371
|
+
face = await paint();
|
|
372
|
+
}
|
|
373
|
+
catch (e) {
|
|
374
|
+
setWrong({ field: 'picture', text: `${reach(e).replace(/[.!?]?$/, '.')} Press Create again to make ${called} without a picture.` });
|
|
375
|
+
setFaceless(true);
|
|
376
|
+
setBusy(null);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
255
380
|
// The budget fields are the agents API's (cloud apps/agents createAgentIn);
|
|
256
381
|
// @hanzo/ai 0.6.16's AgentCreateParams predates them.
|
|
257
382
|
const body = {
|
|
258
383
|
name: called,
|
|
259
384
|
model,
|
|
260
385
|
...(does.trim() ? { instructions: does.trim() } : {}),
|
|
261
|
-
...(
|
|
386
|
+
...(face ? { avatar: face } : emoji.trim() ? { emoji: emoji.trim() } : {}),
|
|
262
387
|
cap_micro_usd: micro(cap),
|
|
263
388
|
max_task_micro_usd: micro(task),
|
|
264
389
|
period,
|
|
@@ -280,25 +405,25 @@ export function Hire({ onClose, onMade }) {
|
|
|
280
405
|
return (_jsx(Box, { position: "fixed", inset: 0, z: "var(--z-modal)", display: "flex", items: "center", justify: "center", p: "$4", bg: "$sunken", overflowY: "auto", onClick: (e) => {
|
|
281
406
|
if (e.target === e.currentTarget)
|
|
282
407
|
onClose();
|
|
283
|
-
}, children: _jsxs(YStack, { role: "dialog", "aria-modal": true, "aria-labelledby": "hire-title", width: "100%", maxW: 460, gap: "$3", p: "$5", rounded: "$6", borderWidth: 1, borderColor: "$borderColor", bg: "$background", children: [_jsx(Text, { id: "hire-title", render: "h2", fontSize: "$5", fontWeight: "600", color: "$ink", children:
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
408
|
+
}, children: _jsxs(YStack, { role: "dialog", "aria-modal": true, "aria-labelledby": "hire-title", width: "100%", maxW: 460, gap: "$3", p: "$5", rounded: "$6", borderWidth: 1, borderColor: "$borderColor", bg: "$background", children: [_jsxs(XStack, { items: "center", justify: "space-between", gap: "$2", flexWrap: "wrap", children: [_jsx(Text, { id: "hire-title", render: "h2", fontSize: "$5", fontWeight: "600", color: "$ink", children: choosing ? 'Start from a persona' : 'New agent' }), choosing ? (_jsxs(Button, { size: "sm", variant: "ghost", onClick: () => setChoosing(false), children: [_jsx(ArrowLeft, { size: 14, "aria-hidden": true }), " Back"] })) : (_jsxs(Button, { size: "sm", variant: "outline", onClick: () => setChoosing(true), children: [_jsx(Users, { size: 14, "aria-hidden": true }), " Start from a persona"] }))] }), choosing ? (_jsx(Figures, { onPick: (f) => void become(f) })) : (_jsxs(_Fragment, { children: [_jsxs(XStack, { items: "center", gap: "$4", flexWrap: "wrap", children: [_jsxs(Box, { render: "button", onClick: () => picking.current?.click(), onMouseEnter: () => setLit(true), onMouseLeave: () => setLit(false), onFocus: () => setLit(true), onBlur: () => setLit(false), onDragOver: (e) => {
|
|
409
|
+
e.preventDefault();
|
|
410
|
+
setOver(true);
|
|
411
|
+
}, onDragLeave: () => setOver(false), onDrop: (e) => {
|
|
412
|
+
e.preventDefault();
|
|
413
|
+
setOver(false);
|
|
414
|
+
void pick(e.dataTransfer.files?.[0]);
|
|
415
|
+
}, "aria-label": avatar ? 'Change photo' : 'Upload photo', "aria-describedby": "hire-picture-hint", position: "relative", width: 88, height: 88, rounded: 9999, overflow: "hidden", shrink: 0, items: "center", justify: "center", bg: "$raised", borderWidth: 2, borderStyle: avatar || emoji ? 'solid' : 'dashed', borderColor: over || lit ? '$ink' : '$soft', children: [face, !face || lit || over ? (_jsx(YStack, { "aria-hidden": true, position: "absolute", inset: 0, items: "center", justify: "center", gap: "$1", bg: face ? VEIL : undefined, children: _jsx(Camera, { size: 22, color: face ? 'var(--pure-white)' : 'var(--soft, var(--muted-foreground))' }) })) : null] }), _jsx("input", { ref: picking, type: "file", hidden: true, accept: "image/png,image/jpeg,image/gif,image/webp", onChange: (e) => {
|
|
416
|
+
void pick(e.target.files?.[0]);
|
|
417
|
+
e.target.value = '';
|
|
418
|
+
} }), _jsxs(YStack, { flex: 1, minW: 180, gap: "$1.5", children: [_jsxs(XStack, { gap: "$2", items: "center", flexWrap: "wrap", children: [_jsxs(Button, { size: "sm", variant: "outline", onClick: () => picking.current?.click(), children: [_jsx(Upload, { size: 14, "aria-hidden": true }), " ", avatar ? 'Change photo' : 'Upload photo'] }), avatar ? (_jsx(Button, { size: "sm", variant: "ghost", onClick: () => setAvatar(''), children: "Remove" })) : null] }), _jsx(Text, { id: "hire-picture-hint", fontSize: "$2", color: "$soft", children: over ? 'Drop the photo to use it.' : 'Press the circle or drop a photo on it, or Create draws one.' })] })] }), _jsxs(YStack, { gap: "$1.5", children: [_jsx(Text, { render: "label", htmlFor: "hire-look", fontSize: "$2", color: "var(--soft, var(--muted-foreground))", children: "Or describe a face and press Draw" }), _jsxs(XStack, { gap: "$1.5", items: "center", children: [_jsx(Box, { flex: 1, children: _jsx(Input, { id: "hire-look", value: look, onChange: (e) => setLook(e.target.value), onKeyDown: (e) => {
|
|
419
|
+
if (e.key === 'Enter')
|
|
420
|
+
void draw();
|
|
421
|
+
}, placeholder: "A calm engineer with round glasses" }) }), _jsxs(Button, { size: "sm", variant: "outline", disabled: busy !== null || !subject || !client, onClick: () => void draw(), children: [_jsx(Sparkles, { size: 14, "aria-hidden": true }), " ", busy === 'draw' ? 'Drawing…' : avatar ? 'Regenerate' : 'Draw'] })] }), _jsxs(XStack, { gap: "$2", items: "center", children: [_jsx(Text, { render: "label", htmlFor: "hire-emoji", fontSize: "$2", color: "var(--soft, var(--muted-foreground))", children: "Or one emoji" }), _jsx(Box, { width: 64, children: _jsx(Input, { id: "hire-emoji", value: emoji, onChange: (e) => {
|
|
422
|
+
setEmoji(e.target.value);
|
|
423
|
+
if (e.target.value)
|
|
424
|
+
setAvatar('');
|
|
425
|
+
}, "aria-describedby": "hire-picture-wrong" }) })] }), _jsx(Said, { id: "hire-picture-wrong", text: shown('picture') })] }), _jsxs(YStack, { gap: "$1.5", children: [_jsx(Text, { render: "label", htmlFor: "hire-name", fontSize: "$2", fontWeight: "600", children: "Name" }), _jsx(Input, { id: "hire-name", value: name, onChange: (e) => setName(e.target.value), onKeyDown: (e) => {
|
|
426
|
+
if (e.key === 'Enter')
|
|
427
|
+
void make();
|
|
428
|
+
}, placeholder: "Michael", autoFocus: true, "aria-required": "true", "aria-invalid": Boolean(nameWrong || shown('name')), "aria-describedby": "hire-name-wrong" }), _jsx(Said, { id: "hire-name-wrong", text: nameWrong ?? shown('name') })] }), _jsxs(YStack, { gap: "$1.5", children: [_jsx(Text, { render: "label", htmlFor: "hire-model", fontSize: "$2", fontWeight: "600", children: "Model" }), _jsxs(Text, { render: _jsx("select", { id: "hire-model", value: model, onChange: (e) => setModel(e.target.value), "aria-describedby": "hire-model-wrong" }), ...FIELD, children: [_jsx("option", { value: "enso", children: "enso" }), models.map((one) => (_jsx("option", { value: one.id, children: one.id }, one.id)))] }), _jsx(Said, { id: "hire-model-wrong", text: shown('model') })] }), _jsxs(YStack, { gap: "$1.5", children: [_jsxs(Text, { render: "label", htmlFor: "hire-does", fontSize: "$2", fontWeight: "600", children: ["What it does", ' ', _jsx(Text, { fontWeight: "400", color: "var(--soft, var(--muted-foreground))", children: "\u2014 optional" })] }), _jsx(Textarea, { id: "hire-does", value: does, onChangeText: setDoes, placeholder: "The work it takes and the voice it answers in", rows: 3, "aria-describedby": "hire-does-wrong" }), _jsx(Said, { id: "hire-does-wrong", text: shown('does') })] }), _jsxs(View, { render: _jsx("fieldset", { "aria-describedby": "hire-budget-wrong" }), display: "block", borderWidth: 0, p: 0, m: 0, children: [_jsx(Text, { render: "legend", display: "block", fontSize: "$2", fontWeight: "600", mb: 6, children: "Spending limit" }), _jsxs(YStack, { gap: "$2", children: [_jsxs(XStack, { gap: "$2", items: "center", flexWrap: "wrap", children: [_jsx(Text, { fontSize: "$2", color: "$soft", children: "Up to $" }), _jsx(Box, { width: 84, children: _jsx(Input, { "aria-label": "Spending limit in dollars", inputMode: "decimal", value: cap, onChange: (e) => setCap(e.target.value), "aria-invalid": !capOk }) }), _jsx(Text, { render: _jsx("select", { "aria-label": "Spending limit period", value: period, onChange: (e) => setPeriod(e.target.value) }), ...FIELD, width: "auto", children: PERIODS.map(([value, words]) => (_jsx("option", { value: value, children: words }, value))) })] }), _jsxs(XStack, { gap: "$2", items: "center", flexWrap: "wrap", children: [_jsx(Text, { fontSize: "$2", color: "$soft", children: "At most $" }), _jsx(Box, { width: 84, children: _jsx(Input, { "aria-label": "Most one task may spend, in dollars", inputMode: "decimal", value: task, onChange: (e) => setTask(e.target.value), "aria-invalid": !taskOk }) }), _jsx(Text, { fontSize: "$2", color: "$soft", children: "on one task" })] })] }), _jsx(Box, { mt: "$1.5", children: _jsx(Said, { id: "hire-budget-wrong", text: budgetWrong ?? shown('budget') }) })] }), _jsx(Said, { id: "hire-form-wrong", text: shown('form') }), _jsxs(XStack, { justify: "flex-end", gap: "$2", children: [_jsx(Button, { size: "sm", variant: "ghost", onClick: onClose, children: "Cancel" }), _jsx(Button, { size: "sm", disabled: busy !== null || !ready, onClick: () => void make(), children: busy === 'make' ? 'Creating…' : 'Create' })] })] }))] }) }));
|
|
304
429
|
}
|
package/dist/help.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** One Help row that leaves for a page. */
|
|
2
|
+
export interface Page {
|
|
3
|
+
key: string;
|
|
4
|
+
label: string;
|
|
5
|
+
href: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The Help menu's pages, read at call time: `site()` is the app's origin on
|
|
9
|
+
* hanzo.ai and hanzo.ai itself on hanzo.team. Help center is /support, the
|
|
10
|
+
* page that leads to the help center, the docs, status and a person.
|
|
11
|
+
*/
|
|
12
|
+
export declare const pages: () => Page[];
|
|
13
|
+
/** The agreements under the Help menu's rule. */
|
|
14
|
+
export declare const legal: () => Page[];
|
|
15
|
+
/** One of the app's keys: what it does, and how a Mac and every other platform spell it. */
|
|
16
|
+
export interface Key {
|
|
17
|
+
does: string;
|
|
18
|
+
mac: string;
|
|
19
|
+
pc: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Every key the app answers, in the order a reader looks for them. Each is
|
|
23
|
+
* handled where it acts: App.tsx (⌘K, ⌘, and, in a window, Back and Forward),
|
|
24
|
+
* Me.tsx (⌘/), @hanzo/build's panel (⌘.), the composer (Enter, ⌘U), chord.ts
|
|
25
|
+
* (the conversation's chords, spelled by `said` so the two cannot drift).
|
|
26
|
+
*/
|
|
27
|
+
export declare const KEYS: Key[];
|
|
28
|
+
/** The part of a keydown `asked` reads. */
|
|
29
|
+
export interface Press {
|
|
30
|
+
key?: string;
|
|
31
|
+
ctrlKey: boolean;
|
|
32
|
+
metaKey: boolean;
|
|
33
|
+
altKey: boolean;
|
|
34
|
+
repeat?: boolean;
|
|
35
|
+
}
|
|
36
|
+
/** Whether a keydown asks for the keyboard shortcuts: ⌘/ or Ctrl+/, once a press. */
|
|
37
|
+
export declare const asked: (e: Press) => boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Opens an address in a tab of its own, through an anchor, so a host that
|
|
40
|
+
* sends links out of its window (the desktop's system browser) hears it as the
|
|
41
|
+
* link it is.
|
|
42
|
+
*/
|
|
43
|
+
export declare function out(href: string): void;
|
package/dist/help.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// The Help menu's pages and the app's keys, as data: the account menu (Me.tsx)
|
|
2
|
+
// draws both, its shortcuts dialog renders KEYS, and a spec reads them as they
|
|
3
|
+
// are. No React, so a spec reads it as it is.
|
|
4
|
+
import { said } from './chord.js';
|
|
5
|
+
import { site } from './where.js';
|
|
6
|
+
/** Where the release notes are published: the site's own nav names the same. */
|
|
7
|
+
const RELEASES = 'https://github.com/hanzoai/cli/releases';
|
|
8
|
+
/**
|
|
9
|
+
* The Help menu's pages, read at call time: `site()` is the app's origin on
|
|
10
|
+
* hanzo.ai and hanzo.ai itself on hanzo.team. Help center is /support, the
|
|
11
|
+
* page that leads to the help center, the docs, status and a person.
|
|
12
|
+
*/
|
|
13
|
+
export const pages = () => [
|
|
14
|
+
{ key: 'help', label: 'Help center', href: site('/support') },
|
|
15
|
+
{ key: 'center', label: 'Privacy center', href: site('/legal') },
|
|
16
|
+
{ key: 'releases', label: 'Release notes', href: RELEASES },
|
|
17
|
+
{ key: 'download', label: 'Download apps', href: site('/download') },
|
|
18
|
+
];
|
|
19
|
+
/** The agreements under the Help menu's rule. */
|
|
20
|
+
export const legal = () => [
|
|
21
|
+
{ key: 'terms', label: 'Terms of Service', href: site('/legal/terms') },
|
|
22
|
+
{ key: 'privacy', label: 'Privacy Policy', href: site('/legal/privacy') },
|
|
23
|
+
];
|
|
24
|
+
/**
|
|
25
|
+
* Every key the app answers, in the order a reader looks for them. Each is
|
|
26
|
+
* handled where it acts: App.tsx (⌘K, ⌘, and, in a window, Back and Forward),
|
|
27
|
+
* Me.tsx (⌘/), @hanzo/build's panel (⌘.), the composer (Enter, ⌘U), chord.ts
|
|
28
|
+
* (the conversation's chords, spelled by `said` so the two cannot drift).
|
|
29
|
+
*/
|
|
30
|
+
export const KEYS = [
|
|
31
|
+
{ does: 'Search chats', mac: '⌘K', pc: 'Ctrl+K' },
|
|
32
|
+
{ does: 'Settings', mac: '⌘,', pc: 'Ctrl+,' },
|
|
33
|
+
{ does: 'Keyboard shortcuts', mac: '⌘/', pc: 'Ctrl+/' },
|
|
34
|
+
{ does: 'Side panel', mac: '⌘.', pc: 'Ctrl+.' },
|
|
35
|
+
{ does: 'Attach files', mac: '⌘U', pc: 'Ctrl+U' },
|
|
36
|
+
{ does: 'Send', mac: 'Enter', pc: 'Enter' },
|
|
37
|
+
{ does: 'New line', mac: '⇧Enter', pc: 'Shift+Enter' },
|
|
38
|
+
{ does: 'Rename chat', mac: said('rename', true), pc: said('rename', false) },
|
|
39
|
+
{ does: 'Pin or unpin chat', mac: said('pin', true), pc: said('pin', false) },
|
|
40
|
+
{ does: 'Back', mac: '⌘[', pc: 'Alt+←' },
|
|
41
|
+
{ does: 'Forward', mac: '⌘]', pc: 'Alt+→' },
|
|
42
|
+
];
|
|
43
|
+
/** Whether a keydown asks for the keyboard shortcuts: ⌘/ or Ctrl+/, once a press. */
|
|
44
|
+
export const asked = (e) => (e.metaKey || e.ctrlKey) && !e.altKey && !e.repeat && e.key === '/';
|
|
45
|
+
/**
|
|
46
|
+
* Opens an address in a tab of its own, through an anchor, so a host that
|
|
47
|
+
* sends links out of its window (the desktop's system browser) hears it as the
|
|
48
|
+
* link it is.
|
|
49
|
+
*/
|
|
50
|
+
export function out(href) {
|
|
51
|
+
const a = document.createElement('a');
|
|
52
|
+
a.href = href;
|
|
53
|
+
a.target = '_blank';
|
|
54
|
+
a.rel = 'noopener noreferrer';
|
|
55
|
+
document.body.appendChild(a);
|
|
56
|
+
a.click();
|
|
57
|
+
a.remove();
|
|
58
|
+
}
|
package/dist/lib/web.d.ts
CHANGED
|
@@ -37,6 +37,12 @@
|
|
|
37
37
|
import { type ChatCompletionTool } from '@hanzo/ai';
|
|
38
38
|
/** Pages a turn that names them reads. */
|
|
39
39
|
export declare const PAGES = 3;
|
|
40
|
+
/**
|
|
41
|
+
* Research: beside `WEB`, a turn that carries this reads deeper — ten results
|
|
42
|
+
* listed, five pages read — whatever its shape, and the model is asked for a
|
|
43
|
+
* report that cites them.
|
|
44
|
+
*/
|
|
45
|
+
export declare const RESEARCH: ChatCompletionTool;
|
|
40
46
|
/** The two tools that mark a turn as one that may use the web. */
|
|
41
47
|
export declare const WEB: ChatCompletionTool[];
|
|
42
48
|
/**
|
package/dist/lib/web.js
CHANGED
|
@@ -41,6 +41,9 @@ export const PAGES = 3;
|
|
|
41
41
|
/** Results a search lists, and how many of them it reads. */
|
|
42
42
|
const LISTED = 6;
|
|
43
43
|
const READ = 2;
|
|
44
|
+
/** What Research lists and reads instead. */
|
|
45
|
+
const DEEP_LISTED = 10;
|
|
46
|
+
const DEEP_READ = 5;
|
|
44
47
|
/** Characters of a page a search quotes, and a named page quotes. */
|
|
45
48
|
const PAGE_CHARS = 3500;
|
|
46
49
|
const CRAWL_CHARS = 6000;
|
|
@@ -53,6 +56,21 @@ const GATE = 8192;
|
|
|
53
56
|
const SEARCH_MS = 20_000;
|
|
54
57
|
const SKIM_MS = 10_000;
|
|
55
58
|
const READ_MS = 25_000;
|
|
59
|
+
/**
|
|
60
|
+
* Research: beside `WEB`, a turn that carries this reads deeper — ten results
|
|
61
|
+
* listed, five pages read — whatever its shape, and the model is asked for a
|
|
62
|
+
* report that cites them.
|
|
63
|
+
*/
|
|
64
|
+
export const RESEARCH = {
|
|
65
|
+
type: 'function',
|
|
66
|
+
function: {
|
|
67
|
+
name: 'research',
|
|
68
|
+
description: 'Research the question in depth on the live web and answer with a cited report.',
|
|
69
|
+
parameters: { type: 'object', properties: {}, required: [] },
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
/** The word a Research turn's model is given. */
|
|
73
|
+
const REPORT = 'Research mode: answer with a detailed report — a short direct answer first, then the evidence under headings — citing each claim with the numbered sources above as [n](url). Say plainly where the sources disagree or fall short.';
|
|
56
74
|
/** The two tools that mark a turn as one that may use the web. */
|
|
57
75
|
export const WEB = [
|
|
58
76
|
{
|
|
@@ -527,7 +545,7 @@ function pointed(text) {
|
|
|
527
545
|
/** What the model is told of a page it was not given. */
|
|
528
546
|
const withheld = (url) => `The person's message names ${label(url)}. It was not opened: opening an address like it can sign in, confirm, unsubscribe or spend a one-time link for them.`;
|
|
529
547
|
/** Runs one search: the results, best first, and the text of the best pages. */
|
|
530
|
-
async function search(call, q, lang, now) {
|
|
548
|
+
async function search(call, q, lang, now, deep = false) {
|
|
531
549
|
let found;
|
|
532
550
|
try {
|
|
533
551
|
found = await call('/v1/websearch', lang ? { q, language: lang } : { q }, SEARCH_MS);
|
|
@@ -536,11 +554,11 @@ async function search(call, q, lang, now) {
|
|
|
536
554
|
return unavailable(e);
|
|
537
555
|
}
|
|
538
556
|
const words = keywords(q);
|
|
539
|
-
const ranked = rank(found?.results ?? [], words).slice(0, LISTED);
|
|
557
|
+
const ranked = rank(found?.results ?? [], words).slice(0, deep ? DEEP_LISTED : LISTED);
|
|
540
558
|
const head = `Web search for "${q}", ${stamp(now)}.`;
|
|
541
559
|
if (!ranked.length)
|
|
542
560
|
return `${head}\nNothing was found.`;
|
|
543
|
-
const pages = await Promise.all(ranked.slice(0, READ).map((r) => call('/v1/crawl', { url: r.url }, SKIM_MS)
|
|
561
|
+
const pages = await Promise.all(ranked.slice(0, deep ? DEEP_READ : READ).map((r) => call('/v1/crawl', { url: r.url }, SKIM_MS)
|
|
544
562
|
.then((d) => (d?.success ? excerpt(d.data?.markdown ?? '', words, PAGE_CHARS) : ''))
|
|
545
563
|
.catch(() => '')));
|
|
546
564
|
const items = ranked.map((r, i) => {
|
|
@@ -563,6 +581,19 @@ async function read(call, url, words) {
|
|
|
563
581
|
return `Could not read ${url}: ${e instanceof Error ? e.message : String(e)}.`;
|
|
564
582
|
}
|
|
565
583
|
}
|
|
584
|
+
/** A Research turn is looked up whatever its shape: its own words, searched. Files and pictures still stay home. */
|
|
585
|
+
function forced(messages, spoken) {
|
|
586
|
+
const last = messages.at(-1);
|
|
587
|
+
if (last?.role !== 'user')
|
|
588
|
+
return null;
|
|
589
|
+
const { text: said, carried } = told(textOf(last.content));
|
|
590
|
+
if (carried || pictured(last.content))
|
|
591
|
+
return null;
|
|
592
|
+
const plain = spell(said.trim().slice(0, GATE)).trim();
|
|
593
|
+
if (!plain)
|
|
594
|
+
return null;
|
|
595
|
+
return { pages: [], held: [], q: plain.slice(0, QUERY), lang: language(plain, spoken), about: keywords(plain) };
|
|
596
|
+
}
|
|
566
597
|
/**
|
|
567
598
|
* The lookup the last turn needs, or null. A short follow-up ("and in
|
|
568
599
|
* Moscow?") is read with the question before it, the way the person meant it.
|
|
@@ -667,7 +698,8 @@ export function researched(base, clock = () => new Date(), spoken = reader) {
|
|
|
667
698
|
if (!asked.stream || !Array.isArray(asked.messages) || !WEB.every((t) => names.has(t.function.name)))
|
|
668
699
|
return base(input, init);
|
|
669
700
|
const { tools: _tools, tool_choice: _choice, ...bare } = asked;
|
|
670
|
-
const
|
|
701
|
+
const deep = names.has(RESEARCH.function.name);
|
|
702
|
+
const look = lookup(asked.messages, spoken()) ?? (deep ? forced(asked.messages, spoken()) : null);
|
|
671
703
|
if (!look)
|
|
672
704
|
return base(input, { ...init, body: JSON.stringify(bare) });
|
|
673
705
|
// The reader's stop, and the stream's own cancel, end every request below.
|
|
@@ -695,10 +727,10 @@ export function researched(base, clock = () => new Date(), spoken = reader) {
|
|
|
695
727
|
};
|
|
696
728
|
const found = (await Promise.all([
|
|
697
729
|
...look.pages.map((u) => read(call, u, look.about)),
|
|
698
|
-
...(look.q ? [search(call, look.q, look.lang, now)] : []),
|
|
730
|
+
...(look.q ? [search(call, look.q, look.lang, now, deep)] : []),
|
|
699
731
|
...look.held.map(async (u) => withheld(u)),
|
|
700
732
|
])).join('\n\n');
|
|
701
|
-
const messages = primed(asked.messages, found, now);
|
|
733
|
+
const messages = primed(asked.messages, found, now, deep ? REPORT : '');
|
|
702
734
|
const first = await send(messages);
|
|
703
735
|
if (!streams(first))
|
|
704
736
|
return first;
|
|
@@ -709,7 +741,7 @@ export function researched(base, clock = () => new Date(), spoken = reader) {
|
|
|
709
741
|
// results: asked once more, it answers. One that failed is not asked again.
|
|
710
742
|
const { said, failed } = await relay(first, out);
|
|
711
743
|
if (!said.trim() && !failed) {
|
|
712
|
-
const again = await send(primed(asked.messages, found, now, SILENT));
|
|
744
|
+
const again = await send(primed(asked.messages, found, now, deep ? `${REPORT}\n\n${SILENT}` : SILENT));
|
|
713
745
|
if (streams(again))
|
|
714
746
|
await relay(again, out);
|
|
715
747
|
else
|