@hanzo/rooms 0.1.0
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/LICENSE-APACHE +205 -0
- package/LICENSE-MIT +21 -0
- package/README.md +23 -0
- package/dist/Account.d.ts +8 -0
- package/dist/Account.js +58 -0
- package/dist/Beluga.d.ts +12 -0
- package/dist/Beluga.js +152 -0
- package/dist/Billing.d.ts +1 -0
- package/dist/Billing.js +31 -0
- package/dist/Board.d.ts +9 -0
- package/dist/Board.js +447 -0
- package/dist/Boxes.d.ts +1 -0
- package/dist/Boxes.js +173 -0
- package/dist/CalScheduler.d.ts +15 -0
- package/dist/CalScheduler.js +196 -0
- package/dist/Chat.d.ts +35 -0
- package/dist/Chat.js +1049 -0
- package/dist/Column.d.ts +15 -0
- package/dist/Column.js +88 -0
- package/dist/Contacts.d.ts +1 -0
- package/dist/Contacts.js +301 -0
- package/dist/Directory.d.ts +24 -0
- package/dist/Directory.js +356 -0
- package/dist/Drive.d.ts +2 -0
- package/dist/Drive.js +466 -0
- package/dist/Find.d.ts +7 -0
- package/dist/Find.js +82 -0
- package/dist/Guide.d.ts +6 -0
- package/dist/Guide.js +272 -0
- package/dist/Home.d.ts +39 -0
- package/dist/Home.js +306 -0
- package/dist/Inbox.d.ts +9 -0
- package/dist/Inbox.js +193 -0
- package/dist/Market.d.ts +3 -0
- package/dist/Market.js +149 -0
- package/dist/Me.d.ts +27 -0
- package/dist/Me.js +251 -0
- package/dist/Meet.d.ts +9 -0
- package/dist/Meet.js +370 -0
- package/dist/ModeMark.d.ts +8 -0
- package/dist/ModeMark.js +18 -0
- package/dist/OrgSwitcher.d.ts +31 -0
- package/dist/OrgSwitcher.js +120 -0
- package/dist/Orgs.d.ts +10 -0
- package/dist/Orgs.js +582 -0
- package/dist/Profile.d.ts +4 -0
- package/dist/Profile.js +67 -0
- package/dist/Prose.d.ts +5 -0
- package/dist/Prose.js +111 -0
- package/dist/ProviderMark.d.ts +17 -0
- package/dist/ProviderMark.js +277 -0
- package/dist/Recents.d.ts +10 -0
- package/dist/Recents.js +91 -0
- package/dist/RightPane.d.ts +13 -0
- package/dist/RightPane.js +426 -0
- package/dist/Room.d.ts +8 -0
- package/dist/Room.js +50 -0
- package/dist/Scene.d.ts +12 -0
- package/dist/Scene.js +105 -0
- package/dist/Settings.d.ts +46 -0
- package/dist/Settings.js +321 -0
- package/dist/Setup.d.ts +1 -0
- package/dist/Setup.js +36 -0
- package/dist/Share.d.ts +3 -0
- package/dist/Share.js +130 -0
- package/dist/Shared.d.ts +1 -0
- package/dist/Shared.js +113 -0
- package/dist/Shell.d.ts +63 -0
- package/dist/Shell.js +1225 -0
- package/dist/Start.d.ts +1 -0
- package/dist/Start.js +96 -0
- package/dist/Support.d.ts +14 -0
- package/dist/Support.js +122 -0
- package/dist/Talk.d.ts +4 -0
- package/dist/Talk.js +107 -0
- package/dist/Upgrade.d.ts +11 -0
- package/dist/Upgrade.js +75 -0
- package/dist/Usage.d.ts +1 -0
- package/dist/Usage.js +21 -0
- package/dist/Vibe.d.ts +2 -0
- package/dist/Vibe.js +188 -0
- package/dist/Work.d.ts +12 -0
- package/dist/Work.js +256 -0
- package/dist/bots/Bots.d.ts +2 -0
- package/dist/bots/Bots.js +456 -0
- package/dist/bots/Lights.d.ts +8 -0
- package/dist/bots/Lights.js +13 -0
- package/dist/bots/api.d.ts +120 -0
- package/dist/bots/api.js +75 -0
- package/dist/bots/face.d.ts +5 -0
- package/dist/bots/face.js +13 -0
- package/dist/bots/ink.d.ts +7 -0
- package/dist/bots/ink.js +29 -0
- package/dist/bots/kit.d.ts +113 -0
- package/dist/bots/kit.js +43 -0
- package/dist/bots/runtime.d.ts +4 -0
- package/dist/bots/runtime.js +326 -0
- package/dist/card.d.ts +52 -0
- package/dist/card.js +52 -0
- package/dist/cast.d.ts +143 -0
- package/dist/cast.js +251 -0
- package/dist/connect.d.ts +29 -0
- package/dist/connect.js +64 -0
- package/dist/contacts-store.d.ts +111 -0
- package/dist/contacts-store.js +276 -0
- package/dist/conversations.d.ts +54 -0
- package/dist/conversations.js +98 -0
- package/dist/copy.d.ts +23 -0
- package/dist/copy.js +39 -0
- package/dist/crew.d.ts +36 -0
- package/dist/crew.js +304 -0
- package/dist/dev.d.ts +3 -0
- package/dist/dev.js +102 -0
- package/dist/enso.d.ts +70 -0
- package/dist/enso.js +112 -0
- package/dist/failure.d.ts +57 -0
- package/dist/failure.js +143 -0
- package/dist/ground.d.ts +32 -0
- package/dist/ground.js +32 -0
- package/dist/host.d.ts +76 -0
- package/dist/host.js +76 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +12 -0
- package/dist/kept.d.ts +5 -0
- package/dist/kept.js +27 -0
- package/dist/lib/account.d.ts +49 -0
- package/dist/lib/account.js +229 -0
- package/dist/lib/ai.d.ts +46 -0
- package/dist/lib/ai.js +140 -0
- package/dist/lib/api.d.ts +43 -0
- package/dist/lib/api.js +62 -0
- package/dist/lib/bare.d.ts +9 -0
- package/dist/lib/bare.js +7 -0
- package/dist/lib/coding.d.ts +105 -0
- package/dist/lib/coding.js +87 -0
- package/dist/lib/destination.d.ts +102 -0
- package/dist/lib/destination.js +260 -0
- package/dist/lib/durable.d.ts +66 -0
- package/dist/lib/durable.js +107 -0
- package/dist/lib/first.d.ts +11 -0
- package/dist/lib/first.js +23 -0
- package/dist/lib/home.d.ts +61 -0
- package/dist/lib/home.js +54 -0
- package/dist/lib/host.d.ts +104 -0
- package/dist/lib/host.js +132 -0
- package/dist/lib/hostname.d.ts +1 -0
- package/dist/lib/hostname.js +14 -0
- package/dist/lib/hydrated.d.ts +1 -0
- package/dist/lib/hydrated.js +20 -0
- package/dist/lib/integrations.d.ts +21 -0
- package/dist/lib/integrations.js +1 -0
- package/dist/lib/intent.d.ts +6 -0
- package/dist/lib/intent.js +71 -0
- package/dist/lib/invite.d.ts +155 -0
- package/dist/lib/invite.js +245 -0
- package/dist/lib/limits.d.ts +55 -0
- package/dist/lib/limits.js +119 -0
- package/dist/lib/list.d.ts +9 -0
- package/dist/lib/list.js +11 -0
- package/dist/lib/mark.d.ts +13 -0
- package/dist/lib/mark.js +85 -0
- package/dist/lib/mix.d.ts +22 -0
- package/dist/lib/mix.js +22 -0
- package/dist/lib/models.d.ts +27 -0
- package/dist/lib/models.js +106 -0
- package/dist/lib/org.d.ts +109 -0
- package/dist/lib/org.js +140 -0
- package/dist/lib/path.d.ts +9 -0
- package/dist/lib/path.js +29 -0
- package/dist/lib/plans.d.ts +193 -0
- package/dist/lib/plans.js +310 -0
- package/dist/lib/prose.d.ts +13 -0
- package/dist/lib/prose.js +62 -0
- package/dist/lib/reach.d.ts +17 -0
- package/dist/lib/reach.js +70 -0
- package/dist/lib/rooms.json +23 -0
- package/dist/lib/served.d.ts +20 -0
- package/dist/lib/served.js +92 -0
- package/dist/lib/session.d.ts +211 -0
- package/dist/lib/session.js +493 -0
- package/dist/lib/share.d.ts +131 -0
- package/dist/lib/share.js +216 -0
- package/dist/lib/signout.d.ts +14 -0
- package/dist/lib/signout.js +40 -0
- package/dist/lib/tags.d.ts +27 -0
- package/dist/lib/tags.js +46 -0
- package/dist/lib/tap.d.ts +17 -0
- package/dist/lib/tap.js +9 -0
- package/dist/lib/tier.d.ts +115 -0
- package/dist/lib/tier.js +207 -0
- package/dist/lib/tiers.d.ts +9 -0
- package/dist/lib/tiers.js +141 -0
- package/dist/lib/todo.d.ts +125 -0
- package/dist/lib/todo.js +379 -0
- package/dist/look.d.ts +1 -0
- package/dist/look.js +21 -0
- package/dist/meters.d.ts +8 -0
- package/dist/meters.js +31 -0
- package/dist/model.d.ts +4 -0
- package/dist/model.js +44 -0
- package/dist/network.d.ts +64 -0
- package/dist/network.js +216 -0
- package/dist/open.d.ts +81 -0
- package/dist/open.js +149 -0
- package/dist/pane.d.ts +92 -0
- package/dist/pane.js +231 -0
- package/dist/progress.d.ts +46 -0
- package/dist/progress.js +51 -0
- package/dist/recent.d.ts +67 -0
- package/dist/recent.js +161 -0
- package/dist/rooms.d.ts +131 -0
- package/dist/rooms.js +195 -0
- package/dist/roster.d.ts +28 -0
- package/dist/roster.js +70 -0
- package/dist/span.d.ts +14 -0
- package/dist/span.js +50 -0
- package/dist/speech.d.ts +44 -0
- package/dist/speech.js +229 -0
- package/dist/stars.d.ts +16 -0
- package/dist/stars.js +75 -0
- package/dist/tabs.d.ts +26 -0
- package/dist/tabs.js +25 -0
- package/dist/team.d.ts +11 -0
- package/dist/team.gen.d.ts +10 -0
- package/dist/team.gen.js +11 -0
- package/dist/team.js +30 -0
- package/dist/tools.d.ts +35 -0
- package/dist/tools.js +143 -0
- package/dist/tours.d.ts +24 -0
- package/dist/tours.js +311 -0
- package/dist/waiting.d.ts +15 -0
- package/dist/waiting.js +17 -0
- package/dist/where.d.ts +54 -0
- package/dist/where.js +14 -0
- package/package.json +108 -0
package/dist/crew.js
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
// The crew, under the composer: who you can talk to, and a place to make one more.
|
|
4
|
+
//
|
|
5
|
+
// The first disc is an outline with a plus — the shape of a face that is not
|
|
6
|
+
// drawn yet — and it opens the sheet that makes an agent: a name, what it does,
|
|
7
|
+
// which mind, and a likeness you upload or describe. The rest are the org's
|
|
8
|
+
// agents, the house crew first in the order the cast lists them, each drawn by
|
|
9
|
+
// the one `Face` every surface draws with. The house crew is the core team
|
|
10
|
+
// hanzoai/personas publishes (team.ts).
|
|
11
|
+
//
|
|
12
|
+
// Under a chat, pressing a face adds that character to the conversation that is
|
|
13
|
+
// open, and the reply comes back in each voice in the room; pressing one again
|
|
14
|
+
// lets them out. The page does not move: the organization, the thread and the
|
|
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';
|
|
19
|
+
import { useRooms } from './host.js';
|
|
20
|
+
import { Camera, Check, Plus, Upload } from 'lucide-react';
|
|
21
|
+
import { useAgents, useModels } from '@hanzo/ai/react';
|
|
22
|
+
import { Box, Button, Input, Text, Textarea, View, XStack, YStack } from '@hanzo/ui';
|
|
23
|
+
import { useAi } from './lib/ai.js';
|
|
24
|
+
import { mark } from './lib/mark.js';
|
|
25
|
+
import { reach } from './lib/reach.js';
|
|
26
|
+
import { CAST, Face } from './cast.js';
|
|
27
|
+
import { useOpen } from './open.js';
|
|
28
|
+
import { TEAM, called } from './team.js';
|
|
29
|
+
import { bare } from './lib/bare.js';
|
|
30
|
+
import { BAD, mix } from './lib/mix.js';
|
|
31
|
+
/** One disc's edge, and the name under it. */
|
|
32
|
+
const DISC = 48;
|
|
33
|
+
const FIELD = {
|
|
34
|
+
width: '100%',
|
|
35
|
+
bg: 'transparent',
|
|
36
|
+
borderWidth: 1,
|
|
37
|
+
borderColor: 'var(--border)',
|
|
38
|
+
rounded: 'var(--radius-md)',
|
|
39
|
+
px: 10,
|
|
40
|
+
py: 8,
|
|
41
|
+
outlineStyle: 'none',
|
|
42
|
+
color: 'inherit',
|
|
43
|
+
fontSize: '$2',
|
|
44
|
+
};
|
|
45
|
+
/** The camera's veil over a face already chosen. */
|
|
46
|
+
const VEIL = mix('var(--pure-black)', 55, 'srgb');
|
|
47
|
+
/** Fired on `window` when the sheet below makes an agent. */
|
|
48
|
+
const MADE = 'hanzo:agent:made';
|
|
49
|
+
/**
|
|
50
|
+
* The org's agents, read again whenever the sheet makes one. `useAgents` holds
|
|
51
|
+
* one list per caller, so the strip, the chat header and the sheet each hold
|
|
52
|
+
* their own; the event is how a new agent reaches every one of them.
|
|
53
|
+
*/
|
|
54
|
+
export function useCrew() {
|
|
55
|
+
const agents = useAgents();
|
|
56
|
+
const { reload } = agents;
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
window.addEventListener(MADE, reload);
|
|
59
|
+
return () => window.removeEventListener(MADE, reload);
|
|
60
|
+
}, [reload]);
|
|
61
|
+
return agents;
|
|
62
|
+
}
|
|
63
|
+
/** The house crew's names, which a reply may be split by even after one has left the room. */
|
|
64
|
+
export const HOUSE = TEAM.map((one) => one.name);
|
|
65
|
+
/** The core team, drawn for an org that does not keep a member under its own row. */
|
|
66
|
+
const DEFAULT_HOUSE_AGENTS = TEAM.map((one) => ({ id: `house-${one.id}`, name: one.name, avatar: `/agents/${one.id}.png` }));
|
|
67
|
+
/** The org's agents, the cast first in its own order, then the rest as listed. */
|
|
68
|
+
function ordered(agents) {
|
|
69
|
+
const list = Array.isArray(agents) ? agents : [];
|
|
70
|
+
const names = new Set(list.map((a) => a.name?.toLowerCase()));
|
|
71
|
+
const combined = [...list];
|
|
72
|
+
for (const house of DEFAULT_HOUSE_AGENTS) {
|
|
73
|
+
if (!names.has(house.name.toLowerCase())) {
|
|
74
|
+
combined.push(house);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const rank = new Map(Object.keys(CAST).map((name, i) => [name, i]));
|
|
78
|
+
return combined.sort((a, b) => {
|
|
79
|
+
const ra = rank.get(a.name?.toLowerCase() ?? '') ?? Infinity;
|
|
80
|
+
const rb = rank.get(b.name?.toLowerCase() ?? '') ?? Infinity;
|
|
81
|
+
return ra - rb;
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* `to` is the chat a press opens, for a strip on a page that is not one. Absent,
|
|
86
|
+
* the strip sits under a chat and a press changes who is in it.
|
|
87
|
+
*/
|
|
88
|
+
export function Crew({ to } = {}) {
|
|
89
|
+
const { agents } = useCrew();
|
|
90
|
+
const { agents: room, openAgent, withAgents } = useOpen();
|
|
91
|
+
const { router, route } = useRooms();
|
|
92
|
+
const here = route;
|
|
93
|
+
const [hiring, setHiring] = useState(false);
|
|
94
|
+
const talk = (name) => {
|
|
95
|
+
if (!to)
|
|
96
|
+
return openAgent(name);
|
|
97
|
+
withAgents([name]);
|
|
98
|
+
if (here !== to)
|
|
99
|
+
router.push(to);
|
|
100
|
+
};
|
|
101
|
+
return (_jsxs(_Fragment, { children: [_jsxs(XStack, { items: "flex-start", flexWrap: "nowrap", overflowX: "auto", gap: "$2.5", px: "$4", py: "$3", width: "100%", maxW: 820, mx: "auto", "data-slot": "crew", ...bare, role: "group", "aria-label": "Agents", children: [_jsx(Box, { render: "button", onClick: () => setHiring(true), "aria-label": "New agent", "aria-haspopup": "dialog", shrink: 0, children: _jsxs(YStack, { items: "center", gap: "$1.5", width: 68, children: [_jsx(YStack, { width: DISC, height: DISC, rounded: DISC / 2, borderWidth: 1.5, borderStyle: "dashed", borderColor: "$soft", items: "center", justify: "center", hoverStyle: { borderColor: '$ink', bg: '$raised' }, children: _jsx(Plus, { size: 18, "aria-hidden": true }) }), _jsx(Text, { fontSize: "$1", color: "$soft", numberOfLines: 1, children: "New" })] }) }), ordered(agents).map((one) => {
|
|
102
|
+
const name = called(one.name);
|
|
103
|
+
const on = room.some((there) => there.toLowerCase() === one.name.toLowerCase());
|
|
104
|
+
return (_jsx(Box, { render: "button", onClick: () => talk(name), "aria-pressed": on, "aria-label": `Chat with ${name}`, title: to ? `Chat with ${name}` : on ? `Remove ${name} from this chat` : `Add ${name} to this chat`, shrink: 0, children: _jsxs(YStack, { items: "center", gap: "$1.5", width: 68, children: [_jsxs(YStack, { position: "relative", rounded: DISC / 2 + 2, p: 2, borderWidth: 1.5, borderColor: on ? '$ink' : 'transparent', hoverStyle: { borderColor: on ? '$ink' : '$borderColor' }, children: [_jsx(Face, { src: one.avatar, emoji: one.emoji, name: one.name, size: DISC }), on ? (_jsx(YStack, { "aria-hidden": true, position: "absolute", b: -2, r: -2, width: 18, height: 18, rounded: 9, bg: "$ink", borderWidth: 2, borderColor: "$background", items: "center", justify: "center", children: _jsx(Check, { size: 10, strokeWidth: 3, color: "var(--background)" }) })) : null] }), _jsx(Text, { fontSize: "$1", color: on ? '$ink' : '$soft', numberOfLines: 1, maxW: 68, children: name })] }) }, one.id || one.name));
|
|
105
|
+
})] }), hiring ? _jsx(Hire, { onClose: () => setHiring(false), onMade: talk }) : null] }));
|
|
106
|
+
}
|
|
107
|
+
/** The agents API's name rule (cloud apps/agents `nameRE`), checked before the request. */
|
|
108
|
+
const NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
109
|
+
/** A dollar amount as the agents API counts money: whole micro-dollars. */
|
|
110
|
+
const micro = (dollars) => Math.round(Number(dollars) * 1_000_000);
|
|
111
|
+
/** The windows a spending limit resets on (cloud apps/agents `validPeriod`). */
|
|
112
|
+
const PERIODS = [
|
|
113
|
+
['day', 'a day'],
|
|
114
|
+
['week', 'a week'],
|
|
115
|
+
['month', 'a month'],
|
|
116
|
+
];
|
|
117
|
+
/**
|
|
118
|
+
* A refusal from POST /v1/agents, in plain words, and the field it concerns.
|
|
119
|
+
* The platform's sentence is kept where it already reads plainly; the budget's
|
|
120
|
+
* refusals name request fields (`cap_micro_usd`), so those are said in dollars.
|
|
121
|
+
*/
|
|
122
|
+
function refusal(e) {
|
|
123
|
+
const text = reach(e);
|
|
124
|
+
const status = e?.status;
|
|
125
|
+
if (/cap_micro_usd/.test(text))
|
|
126
|
+
return { field: 'budget', text: 'Set a spending limit above $0.' };
|
|
127
|
+
if (/max_task_micro_usd cannot exceed/.test(text))
|
|
128
|
+
return { field: 'budget', text: 'The most one task may spend cannot be more than the spending limit.' };
|
|
129
|
+
if (/max_task_micro_usd/.test(text))
|
|
130
|
+
return { field: 'budget', text: 'Set the most one task may spend, above $0.' };
|
|
131
|
+
if (/\bperiod\b/.test(text))
|
|
132
|
+
return { field: 'budget', text: 'Choose a day, a week or a month for the spending limit.' };
|
|
133
|
+
if (status === 409 || /\bname\b/i.test(text))
|
|
134
|
+
return { field: 'name', text };
|
|
135
|
+
if (/\bmodel\b/i.test(text))
|
|
136
|
+
return { field: 'model', text };
|
|
137
|
+
if (/avatar|image|emoji/i.test(text))
|
|
138
|
+
return { field: 'picture', text };
|
|
139
|
+
if (/instructions/i.test(text))
|
|
140
|
+
return { field: 'does', text: 'What it does is too long. Shorten it.' };
|
|
141
|
+
return { field: 'form', text };
|
|
142
|
+
}
|
|
143
|
+
/** A field's own refusal, under it, announced when it appears. */
|
|
144
|
+
function Said({ id, text }) {
|
|
145
|
+
if (!text)
|
|
146
|
+
return null;
|
|
147
|
+
return (_jsx(Text, { id: id, role: "alert", fontSize: "$2", color: BAD, children: text }));
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* The sheet that makes an agent.
|
|
151
|
+
*
|
|
152
|
+
* A name, a mind and a spending limit are what the agents API requires; what it
|
|
153
|
+
* does becomes its instructions; the likeness is whichever the maker gives — a
|
|
154
|
+
* photo uploaded onto the round picture (pressed, or dropped on), or a face
|
|
155
|
+
* described and drawn by the image model. Both go through `mark`, the one crop
|
|
156
|
+
* every saved face takes, so a drawn face and an uploaded one are the same kind
|
|
157
|
+
* of picture. The row keeps at most one of a picture and an emoji, so choosing
|
|
158
|
+
* one clears the other here too.
|
|
159
|
+
*
|
|
160
|
+
* Create stays off until the required fields are filled in the API's own
|
|
161
|
+
* terms, and a refusal is said in plain words under the field it concerns.
|
|
162
|
+
*
|
|
163
|
+
* EXPORTED, because Contacts adds agents too and there is one way to make one.
|
|
164
|
+
*/
|
|
165
|
+
export function Hire({ onClose, onMade }) {
|
|
166
|
+
const { create } = useAgents();
|
|
167
|
+
const { models } = useModels();
|
|
168
|
+
const { client } = useAi();
|
|
169
|
+
const [name, setName] = useState('');
|
|
170
|
+
const [does, setDoes] = useState('');
|
|
171
|
+
const [model, setModel] = useState('enso');
|
|
172
|
+
const [look, setLook] = useState('');
|
|
173
|
+
const [avatar, setAvatar] = useState('');
|
|
174
|
+
const [emoji, setEmoji] = useState('');
|
|
175
|
+
const [cap, setCap] = useState('5');
|
|
176
|
+
const [task, setTask] = useState('1');
|
|
177
|
+
const [period, setPeriod] = useState('month');
|
|
178
|
+
const [busy, setBusy] = useState(null);
|
|
179
|
+
const [wrong, setWrong] = useState(null);
|
|
180
|
+
const [over, setOver] = useState(false);
|
|
181
|
+
const [lit, setLit] = useState(false);
|
|
182
|
+
const picking = useRef(null);
|
|
183
|
+
useEffect(() => {
|
|
184
|
+
const key = (e) => {
|
|
185
|
+
if (e.key === 'Escape')
|
|
186
|
+
onClose();
|
|
187
|
+
};
|
|
188
|
+
window.addEventListener('keydown', key);
|
|
189
|
+
return () => window.removeEventListener('keydown', key);
|
|
190
|
+
}, [onClose]);
|
|
191
|
+
// WHAT CREATE WAITS FOR, in the API's own terms, so a request it would refuse
|
|
192
|
+
// is never sent. Each rule names the field it belongs to.
|
|
193
|
+
const called = name.trim();
|
|
194
|
+
const nameWrong = !called ? null : NAME.test(called) ? null : 'Use letters, digits, dots, dashes or underscores, starting with a letter or digit, up to 64.';
|
|
195
|
+
const capOk = Number.isFinite(Number(cap)) && micro(cap) > 0;
|
|
196
|
+
const taskOk = Number.isFinite(Number(task)) && micro(task) > 0;
|
|
197
|
+
const budgetWrong = !capOk
|
|
198
|
+
? 'Set a spending limit above $0.'
|
|
199
|
+
: !taskOk
|
|
200
|
+
? 'Set the most one task may spend, above $0.'
|
|
201
|
+
: micro(task) > micro(cap)
|
|
202
|
+
? 'The most one task may spend cannot be more than the spending limit.'
|
|
203
|
+
: null;
|
|
204
|
+
const ready = Boolean(called) && !nameWrong && !budgetWrong && Boolean(model);
|
|
205
|
+
const shown = (field) => (wrong?.field === field ? wrong.text : null);
|
|
206
|
+
const pick = async (file) => {
|
|
207
|
+
if (!file)
|
|
208
|
+
return;
|
|
209
|
+
setWrong(null);
|
|
210
|
+
try {
|
|
211
|
+
setAvatar(await mark(file));
|
|
212
|
+
setEmoji('');
|
|
213
|
+
}
|
|
214
|
+
catch (e) {
|
|
215
|
+
setWrong({ field: 'picture', text: reach(e) });
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
// The image model draws the face; `mark` makes it the same 256px square every
|
|
219
|
+
// saved picture is, so a drawn face weighs what an uploaded one weighs.
|
|
220
|
+
const draw = async () => {
|
|
221
|
+
if (!client || !look.trim())
|
|
222
|
+
return;
|
|
223
|
+
setBusy('draw');
|
|
224
|
+
setWrong(null);
|
|
225
|
+
try {
|
|
226
|
+
const out = await client.http.json({
|
|
227
|
+
method: 'POST',
|
|
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('');
|
|
242
|
+
}
|
|
243
|
+
catch (e) {
|
|
244
|
+
setWrong({ field: 'picture', text: reach(e) });
|
|
245
|
+
}
|
|
246
|
+
finally {
|
|
247
|
+
setBusy(null);
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
const make = async () => {
|
|
251
|
+
if (!ready || busy)
|
|
252
|
+
return;
|
|
253
|
+
setBusy('make');
|
|
254
|
+
setWrong(null);
|
|
255
|
+
// The budget fields are the agents API's (cloud apps/agents createAgentIn);
|
|
256
|
+
// @hanzo/ai 0.6.16's AgentCreateParams predates them.
|
|
257
|
+
const body = {
|
|
258
|
+
name: called,
|
|
259
|
+
model,
|
|
260
|
+
...(does.trim() ? { instructions: does.trim() } : {}),
|
|
261
|
+
...(avatar ? { avatar } : emoji.trim() ? { emoji: emoji.trim() } : {}),
|
|
262
|
+
cap_micro_usd: micro(cap),
|
|
263
|
+
max_task_micro_usd: micro(task),
|
|
264
|
+
period,
|
|
265
|
+
};
|
|
266
|
+
try {
|
|
267
|
+
await create(body);
|
|
268
|
+
window.dispatchEvent(new Event(MADE));
|
|
269
|
+
onMade(called);
|
|
270
|
+
onClose();
|
|
271
|
+
}
|
|
272
|
+
catch (e) {
|
|
273
|
+
setWrong(refusal(e));
|
|
274
|
+
}
|
|
275
|
+
finally {
|
|
276
|
+
setBusy(null);
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
const face = avatar ? (_jsx(View, { render: _jsx("img", { src: avatar, alt: "" }), width: "100%", height: "100%", objectFit: "cover" })) : emoji ? (_jsx(Text, { fontSize: 36, lineHeight: 40, children: emoji })) : null;
|
|
280
|
+
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
|
+
if (e.target === e.currentTarget)
|
|
282
|
+
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: "New agent" }), _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) => {
|
|
284
|
+
e.preventDefault();
|
|
285
|
+
setOver(true);
|
|
286
|
+
}, onDragLeave: () => setOver(false), onDrop: (e) => {
|
|
287
|
+
e.preventDefault();
|
|
288
|
+
setOver(false);
|
|
289
|
+
void pick(e.dataTransfer.files?.[0]);
|
|
290
|
+
}, "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) => {
|
|
291
|
+
void pick(e.target.files?.[0]);
|
|
292
|
+
e.target.value = '';
|
|
293
|
+
} }), _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. PNG, JPEG, GIF or WebP.' })] })] }), _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) => {
|
|
294
|
+
if (e.key === 'Enter')
|
|
295
|
+
void draw();
|
|
296
|
+
}, placeholder: "A calm engineer with round glasses" }) }), _jsx(Button, { size: "sm", variant: "outline", disabled: busy !== null || !look.trim(), onClick: () => void draw(), children: busy === 'draw' ? 'Drawing…' : '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) => {
|
|
297
|
+
setEmoji(e.target.value);
|
|
298
|
+
if (e.target.value)
|
|
299
|
+
setAvatar('');
|
|
300
|
+
}, "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) => {
|
|
301
|
+
if (e.key === 'Enter')
|
|
302
|
+
void make();
|
|
303
|
+
}, 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
|
+
}
|
package/dist/dev.d.ts
ADDED
package/dist/dev.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
// The web app's host: everything @hanzo/build's `<Builder>` needs from hanzo.ai,
|
|
3
|
+
// as values, so the builder imports neither IAM nor a router. hanzo.build's own
|
|
4
|
+
// host (hanzoai/build `src/app/root.tsx`) is the other one, and this answers
|
|
5
|
+
// each field the way that one does — who is signed in, the organizations they
|
|
6
|
+
// belong to and the one they work in, the theme, where the address is, and how
|
|
7
|
+
// to leave. The name drawn at the top left is Hanzo Dev: this host is the
|
|
8
|
+
// dev agent. Chat, on the rooms shell, is Hanzo AI.
|
|
9
|
+
//
|
|
10
|
+
// THE ADDRESS IS THE QUERY. The app is `/` (app/page.tsx), and what it shows
|
|
11
|
+
// rides `?at=` in the builder's own grammar (lib/host.ts `AT`), so a run, a
|
|
12
|
+
// screen, a Settings section, a Customize tab, the plans and a project all
|
|
13
|
+
// survive a reload, and Back steps through them. A move is a native history
|
|
14
|
+
// entry, which the Next router folds into `useSearchParams`, so reading the
|
|
15
|
+
// address and moving it are one query.
|
|
16
|
+
import { useMemo } from 'react';
|
|
17
|
+
import { useLook, useRooms } from './host.js';
|
|
18
|
+
import { useIam } from '@hanzo/iam/react';
|
|
19
|
+
import { useSignOut } from './lib/signout.js';
|
|
20
|
+
import { administers, path, route } from '@hanzo/build';
|
|
21
|
+
import { bearer, named, org, orgs, pick } from './lib/session.js';
|
|
22
|
+
import { enter } from './lib/destination.js';
|
|
23
|
+
import { api } from './lib/api.js';
|
|
24
|
+
import { app, AT } from './lib/host.js';
|
|
25
|
+
import { link } from './lib/tags.js';
|
|
26
|
+
/** Where the builder links out: every one of them an address of this site. */
|
|
27
|
+
const LINKS = {
|
|
28
|
+
github: app('-/sync'),
|
|
29
|
+
customize: app('-/customize'),
|
|
30
|
+
settings: app('-/settings'),
|
|
31
|
+
home: '/',
|
|
32
|
+
};
|
|
33
|
+
/** The person's theme, when they have chosen one the builder can name. */
|
|
34
|
+
const theme = (t) => (t === 'light' || t === 'dark' || t === 'system' ? t : undefined);
|
|
35
|
+
/** The app address a query value names, canonical; '' for anything that is not one. */
|
|
36
|
+
const canonical = (at) => path(route(at ?? ''));
|
|
37
|
+
/** The host for a signed-in reader. */
|
|
38
|
+
export function useDevHost() {
|
|
39
|
+
const { user, accessToken } = useIam();
|
|
40
|
+
const logout = useSignOut();
|
|
41
|
+
const { router, search } = useRooms();
|
|
42
|
+
const here = canonical(search?.get(AT) ?? null);
|
|
43
|
+
const look = useLook();
|
|
44
|
+
// The token's claims change only when the token does: a sign-in, a refresh.
|
|
45
|
+
const memberships = useMemo(() => orgs(), [accessToken]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
46
|
+
const scoped = useMemo(() => org(), [accessToken]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
47
|
+
const claimed = useMemo(() => named(), [accessToken]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
48
|
+
const u = user;
|
|
49
|
+
const name = u?.displayName || u?.name || claimed?.name || '';
|
|
50
|
+
const email = u?.email || claimed?.email || '';
|
|
51
|
+
const avatar = typeof u?.avatar === 'string' && u.avatar.startsWith('https://') ? u.avatar : '';
|
|
52
|
+
return useMemo(() => ({
|
|
53
|
+
name: 'Hanzo Dev',
|
|
54
|
+
api: api(),
|
|
55
|
+
token: bearer,
|
|
56
|
+
org: scoped,
|
|
57
|
+
memberships,
|
|
58
|
+
// Scope this browser to one of the token's organizations and open Dev's
|
|
59
|
+
// New in it: a run or a project read under the last one is not this one's.
|
|
60
|
+
chooseOrg: (next) => {
|
|
61
|
+
if (!orgs().includes(next))
|
|
62
|
+
return;
|
|
63
|
+
pick(next);
|
|
64
|
+
window.location.assign(app(''));
|
|
65
|
+
},
|
|
66
|
+
theme: theme(look.pref.theme),
|
|
67
|
+
chooseTheme: (t) => look.set({ theme: theme(t) }),
|
|
68
|
+
person: { name, email, avatar },
|
|
69
|
+
admin: administers(bearer(), scoped),
|
|
70
|
+
path: here,
|
|
71
|
+
go: (to, how) => {
|
|
72
|
+
const next = app(canonical(to));
|
|
73
|
+
if (`${window.location.pathname}${window.location.search}` === next)
|
|
74
|
+
return;
|
|
75
|
+
if (how?.replace)
|
|
76
|
+
window.history.replaceState(null, '', next);
|
|
77
|
+
else
|
|
78
|
+
window.history.pushState(null, '', next);
|
|
79
|
+
},
|
|
80
|
+
links: LINKS,
|
|
81
|
+
// An address of this site moves in the router; another origin is left in
|
|
82
|
+
// this tab. platform.hanzo.ai is another product, and a click that names
|
|
83
|
+
// it stays here — everything the builder had it for is in its Settings.
|
|
84
|
+
open: (href) => {
|
|
85
|
+
let url;
|
|
86
|
+
try {
|
|
87
|
+
url = new URL(href, window.location.origin);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (url.hostname === 'platform.hanzo.ai')
|
|
93
|
+
return;
|
|
94
|
+
if (url.origin === window.location.origin)
|
|
95
|
+
router.push(`${url.pathname}${url.search}${url.hash}`);
|
|
96
|
+
else
|
|
97
|
+
window.location.assign(link(url.href));
|
|
98
|
+
},
|
|
99
|
+
signIn: () => enter(),
|
|
100
|
+
signOut: () => void logout(),
|
|
101
|
+
}), [scoped, memberships, look.pref.theme, look.set, name, email, avatar, here, router, logout]);
|
|
102
|
+
}
|
package/dist/enso.d.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How hard to think — and, one press inside, what thinks.
|
|
3
|
+
*
|
|
4
|
+
* EFFORT IS ON THE BAR AND THE MODEL IS NOT. That is the product's position
|
|
5
|
+
* rather than a layout preference: Enso routes the ask, so naming a model is an
|
|
6
|
+
* override and belongs inside this panel, not on the row where it asked every
|
|
7
|
+
* reader to hold an opinion about a catalogue before they could type a
|
|
8
|
+
* sentence.
|
|
9
|
+
*
|
|
10
|
+
* Effort earns the bar because it belongs to the ASK. The same person wants a
|
|
11
|
+
* fast answer to one question and a careful one to the next; the model they
|
|
12
|
+
* want rarely changes between two messages, which is what makes it a
|
|
13
|
+
* preference and this a control.
|
|
14
|
+
*
|
|
15
|
+
* `reasoning_effort` is an OpenAI-compatible completion parameter and rides
|
|
16
|
+
* `useChat`'s `params`, so on /chat it reaches the API. A surface that cannot
|
|
17
|
+
* carry it passes no `onEffort` and the chip names the router instead — the
|
|
18
|
+
* coding plane takes a `model` and has no effort field, and a control that
|
|
19
|
+
* reaches nothing is worse than one that is absent.
|
|
20
|
+
*/
|
|
21
|
+
export declare const EFFORTS: readonly [{
|
|
22
|
+
readonly id: 'minimal';
|
|
23
|
+
readonly label: 'Instant';
|
|
24
|
+
readonly note: 'Answer now, no deliberation';
|
|
25
|
+
}, {
|
|
26
|
+
readonly id: 'low';
|
|
27
|
+
readonly label: 'Low';
|
|
28
|
+
readonly note: 'A quick pass';
|
|
29
|
+
}, {
|
|
30
|
+
readonly id: 'medium';
|
|
31
|
+
readonly label: 'Medium';
|
|
32
|
+
readonly note: 'Thinks before it writes';
|
|
33
|
+
}, {
|
|
34
|
+
readonly id: 'high';
|
|
35
|
+
readonly label: 'High';
|
|
36
|
+
readonly note: 'Slow and careful, for hard problems';
|
|
37
|
+
}];
|
|
38
|
+
export type EffortId = (typeof EFFORTS)[number]['id'];
|
|
39
|
+
/**
|
|
40
|
+
* The effort preference, shared by every surface that sends a completion.
|
|
41
|
+
*
|
|
42
|
+
* Same shape as `useModel` — one storage key, one change event, read on mount
|
|
43
|
+
* rather than in the initialiser so the server and the first client render
|
|
44
|
+
* agree and hydration does not warn.
|
|
45
|
+
*/
|
|
46
|
+
export declare function useEffort(): readonly [EffortId, (next: EffortId) => void];
|
|
47
|
+
/** The name a served model goes by: its row's label, its name, or its id made readable. */
|
|
48
|
+
export declare function nameOf(models: {
|
|
49
|
+
id: string;
|
|
50
|
+
}[], id: string): string;
|
|
51
|
+
export interface EnsoProps {
|
|
52
|
+
/** Current effort. Omit `onEffort` where the surface cannot send one. */
|
|
53
|
+
effort?: EffortId;
|
|
54
|
+
onEffort?: (next: EffortId) => void;
|
|
55
|
+
/** The override. `models` empty hides it rather than offering an empty menu. */
|
|
56
|
+
model?: string;
|
|
57
|
+
onModel?: (id: string) => void;
|
|
58
|
+
models?: {
|
|
59
|
+
id: string;
|
|
60
|
+
}[];
|
|
61
|
+
disabled?: boolean;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The chip that sits beside send, and the panel it opens.
|
|
65
|
+
*
|
|
66
|
+
* The chip names what answers the next message: Enso while Enso routes it
|
|
67
|
+
* (`ENSO`, or `FREE` on the free route), and the model itself once one is
|
|
68
|
+
* picked. Beside the name it states the effort where the surface sends one.
|
|
69
|
+
*/
|
|
70
|
+
export declare function Enso({ effort, onEffort, model, onModel, models, disabled }: EnsoProps): import("react").JSX.Element;
|
package/dist/enso.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
import { Check, ChevronDown } from 'lucide-react';
|
|
5
|
+
import { Box, Popover, PopoverContent, PopoverTrigger, Separator, Text, XStack, YStack, } from '@hanzo/ui';
|
|
6
|
+
import { ModelSelector, RESEARCH } from '@hanzo/ui/models';
|
|
7
|
+
import { ProviderMark } from './ProviderMark.js';
|
|
8
|
+
import { ENSO, FREE } from './lib/ai.js';
|
|
9
|
+
import { modelName } from './lib/models.js';
|
|
10
|
+
/**
|
|
11
|
+
* How hard to think — and, one press inside, what thinks.
|
|
12
|
+
*
|
|
13
|
+
* EFFORT IS ON THE BAR AND THE MODEL IS NOT. That is the product's position
|
|
14
|
+
* rather than a layout preference: Enso routes the ask, so naming a model is an
|
|
15
|
+
* override and belongs inside this panel, not on the row where it asked every
|
|
16
|
+
* reader to hold an opinion about a catalogue before they could type a
|
|
17
|
+
* sentence.
|
|
18
|
+
*
|
|
19
|
+
* Effort earns the bar because it belongs to the ASK. The same person wants a
|
|
20
|
+
* fast answer to one question and a careful one to the next; the model they
|
|
21
|
+
* want rarely changes between two messages, which is what makes it a
|
|
22
|
+
* preference and this a control.
|
|
23
|
+
*
|
|
24
|
+
* `reasoning_effort` is an OpenAI-compatible completion parameter and rides
|
|
25
|
+
* `useChat`'s `params`, so on /chat it reaches the API. A surface that cannot
|
|
26
|
+
* carry it passes no `onEffort` and the chip names the router instead — the
|
|
27
|
+
* coding plane takes a `model` and has no effort field, and a control that
|
|
28
|
+
* reaches nothing is worse than one that is absent.
|
|
29
|
+
*/
|
|
30
|
+
export const EFFORTS = [
|
|
31
|
+
{ id: 'minimal', label: 'Instant', note: 'Answer now, no deliberation' },
|
|
32
|
+
{ id: 'low', label: 'Low', note: 'A quick pass' },
|
|
33
|
+
{ id: 'medium', label: 'Medium', note: 'Thinks before it writes' },
|
|
34
|
+
{ id: 'high', label: 'High', note: 'Slow and careful, for hard problems' },
|
|
35
|
+
];
|
|
36
|
+
const DEFAULT = 'medium';
|
|
37
|
+
const KEY = 'hanzo.chat.effort';
|
|
38
|
+
const CHANGE = 'hanzo:effort';
|
|
39
|
+
const read = () => {
|
|
40
|
+
if (typeof window === 'undefined')
|
|
41
|
+
return DEFAULT;
|
|
42
|
+
try {
|
|
43
|
+
const held = window.localStorage.getItem(KEY);
|
|
44
|
+
return EFFORTS.some((e) => e.id === held) ? held : DEFAULT;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return DEFAULT;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* The effort preference, shared by every surface that sends a completion.
|
|
52
|
+
*
|
|
53
|
+
* Same shape as `useModel` — one storage key, one change event, read on mount
|
|
54
|
+
* rather than in the initialiser so the server and the first client render
|
|
55
|
+
* agree and hydration does not warn.
|
|
56
|
+
*/
|
|
57
|
+
export function useEffort() {
|
|
58
|
+
const [effort, setEffort] = useState(DEFAULT);
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
setEffort(read());
|
|
61
|
+
const sync = () => setEffort(read());
|
|
62
|
+
window.addEventListener('storage', sync);
|
|
63
|
+
window.addEventListener(CHANGE, sync);
|
|
64
|
+
return () => {
|
|
65
|
+
window.removeEventListener('storage', sync);
|
|
66
|
+
window.removeEventListener(CHANGE, sync);
|
|
67
|
+
};
|
|
68
|
+
}, []);
|
|
69
|
+
const choose = (next) => {
|
|
70
|
+
setEffort(next);
|
|
71
|
+
try {
|
|
72
|
+
window.localStorage.setItem(KEY, next);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// A private browser may refuse persistence; this tab still keeps the choice.
|
|
76
|
+
}
|
|
77
|
+
window.dispatchEvent(new Event(CHANGE));
|
|
78
|
+
};
|
|
79
|
+
return [effort, choose];
|
|
80
|
+
}
|
|
81
|
+
/** The name a served model goes by: its row's label, its name, or its id made readable. */
|
|
82
|
+
export function nameOf(models, id) {
|
|
83
|
+
const row = models.find((m) => m.id === id);
|
|
84
|
+
return modelName({ id, name: String(row?.label ?? row?.name ?? '') });
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The chip that sits beside send, and the panel it opens.
|
|
88
|
+
*
|
|
89
|
+
* The chip names what answers the next message: Enso while Enso routes it
|
|
90
|
+
* (`ENSO`, or `FREE` on the free route), and the model itself once one is
|
|
91
|
+
* picked. Beside the name it states the effort where the surface sends one.
|
|
92
|
+
*/
|
|
93
|
+
export function Enso({ effort, onEffort, model, onModel, models = [], disabled }) {
|
|
94
|
+
const [open, setOpen] = useState(false);
|
|
95
|
+
const chosen = EFFORTS.find((e) => e.id === effort);
|
|
96
|
+
const pace = onEffort && chosen ? chosen.label : null;
|
|
97
|
+
const picked = model && model !== ENSO && model !== FREE ? model : undefined;
|
|
98
|
+
const name = picked ? nameOf(models, picked) : 'Enso';
|
|
99
|
+
// The panel opens above the chip when there is no room below it, so the model
|
|
100
|
+
// picker at its foot stays on screen on a laptop-height window. It is SOLID:
|
|
101
|
+
// it stands over a transcript that streams, which would read through glass,
|
|
102
|
+
// and the model list it opens inherits the same ground.
|
|
103
|
+
return (_jsxs(Popover, { open: open, onOpenChange: setOpen, allowFlip: true, children: [_jsx(PopoverTrigger, { disabled: disabled, children: _jsxs(XStack, { "data-slot": "enso", items: "center", gap: "$1.5", px: "$2.5", height: 32, maxW: 220, rounded: "$10", borderWidth: 1, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, opacity: disabled ? 0.4 : 1, "aria-label": pace ? `${name}: ${pace}` : name, children: [_jsx(ProviderMark, { provider: "enso", model: picked, size: 14 }), _jsx(Text, { fontSize: "$1", color: "$ink", fontWeight: "600", numberOfLines: 1, children: name }), pace ? (_jsx(Text, { fontSize: "$1", color: "$soft", fontWeight: "500", children: pace })) : null, _jsx(ChevronDown, { size: 12, "aria-hidden": true })] }) }), _jsx(PopoverContent, { solid: true, align: "end", p: 6, width: 268, children: _jsxs(YStack, { gap: "$1", children: [onEffort
|
|
104
|
+
? EFFORTS.map((e) => (_jsx(Box, { render: "button", onClick: () => {
|
|
105
|
+
onEffort(e.id);
|
|
106
|
+
setOpen(false);
|
|
107
|
+
}, px: "$2", py: "$2", rounded: "$3", width: "100%", hoverStyle: { bg: '$hover' }, "aria-pressed": e.id === effort, children: _jsxs(XStack, { items: "center", gap: "$2", width: "100%", children: [_jsxs(YStack, { flex: 1, minW: 0, items: "flex-start", children: [_jsx(Text, { fontSize: "$2", color: "$ink", text: "left", children: e.label }), _jsx(Text, { fontSize: "$1", color: "$soft", text: "left", children: e.note })] }), e.id === effort ? _jsx(Check, { size: 14, "aria-hidden": true }) : null] }) }, e.id)))
|
|
108
|
+
: null, onModel && models.length ? (_jsxs(_Fragment, { children: [onEffort ? _jsx(Separator, { my: "$1" }) : null, _jsxs(YStack, { px: "$2", pt: "$1", pb: "$2", gap: "$2", children: [_jsx(Text, { fontSize: "$1", color: "$soft", children: "Enso picks the model. Override it for this account:" }), _jsx(ModelSelector, { models: [...models, ...RESEARCH], value: model, onChange: (id) => {
|
|
109
|
+
onModel(id);
|
|
110
|
+
setOpen(false);
|
|
111
|
+
}, size: "sm" })] })] })) : null] }) })] }));
|
|
112
|
+
}
|