@hanzo/rooms 0.1.73 → 0.1.75
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 +63 -46
- 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/failure.d.ts +1 -1
- package/dist/failure.js +7 -6
- 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/failure.d.ts
CHANGED
|
@@ -25,7 +25,7 @@ export declare function plainly(error: unknown, subject: string): string;
|
|
|
25
25
|
*
|
|
26
26
|
* The gateway refuses a model the plan does not include with 402
|
|
27
27
|
* `plan_required`, a Free caller whose share is spent with 402
|
|
28
|
-
* `allowance_spent`, and a free request the shared pool cannot serve with 429
|
|
28
|
+
* (now 429) `allowance_spent`, and a free request the shared pool cannot serve with 429
|
|
29
29
|
* `pool_busy` or `pool_exhausted`, each with an `upgrade_url`. The code decides,
|
|
30
30
|
* never the sentence. The address is kept only when it is an https page on
|
|
31
31
|
* hanzo.ai or one of its hosts; any other leaves `href` empty and the caller
|
package/dist/failure.js
CHANGED
|
@@ -86,17 +86,18 @@ export function plainly(error, subject) {
|
|
|
86
86
|
* the pool spent for the window, and the pool every free user shares used up.
|
|
87
87
|
*/
|
|
88
88
|
const LIFTED = {
|
|
89
|
-
plan_required: 402,
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
89
|
+
plan_required: [402],
|
|
90
|
+
// 429 since the gateway's free notice (object/free_notice.go); 402 before it.
|
|
91
|
+
allowance_spent: [402, 429],
|
|
92
|
+
pool_busy: [429],
|
|
93
|
+
pool_exhausted: [429],
|
|
93
94
|
};
|
|
94
95
|
/**
|
|
95
96
|
* Whether a refusal asks for a plan, and the checkout it names.
|
|
96
97
|
*
|
|
97
98
|
* The gateway refuses a model the plan does not include with 402
|
|
98
99
|
* `plan_required`, a Free caller whose share is spent with 402
|
|
99
|
-
* `allowance_spent`, and a free request the shared pool cannot serve with 429
|
|
100
|
+
* (now 429) `allowance_spent`, and a free request the shared pool cannot serve with 429
|
|
100
101
|
* `pool_busy` or `pool_exhausted`, each with an `upgrade_url`. The code decides,
|
|
101
102
|
* never the sentence. The address is kept only when it is an https page on
|
|
102
103
|
* hanzo.ai or one of its hosts; any other leaves `href` empty and the caller
|
|
@@ -105,7 +106,7 @@ const LIFTED = {
|
|
|
105
106
|
export function planRequired(error) {
|
|
106
107
|
const said = error?.body?.error;
|
|
107
108
|
const code = typeof said?.code === 'string' ? said.code : '';
|
|
108
|
-
if (!LIFTED[code]
|
|
109
|
+
if (!LIFTED[code]?.includes(status(error) ?? 0))
|
|
109
110
|
return null;
|
|
110
111
|
return { href: ours(said?.upgrade_url) };
|
|
111
112
|
}
|
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
|
/**
|