@hanzo/build 0.2.68 → 0.2.70
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/lib/api/studio.d.ts +27 -0
- package/lib/api/studio.js +14 -0
- package/lib/carry.d.ts +18 -5
- package/lib/carry.js +78 -10
- package/lib/design.d.ts +134 -0
- package/lib/design.js +123 -0
- package/lib/gear.d.ts +10 -4
- package/lib/gear.js +12 -5
- package/lib/gutter.d.ts +17 -0
- package/lib/gutter.js +25 -0
- package/lib/index.d.ts +2 -1
- package/lib/index.js +2 -1
- package/lib/landing.js +13 -5
- package/lib/plugin.d.ts +4 -0
- package/lib/plugins/design.d.ts +8 -0
- package/lib/plugins/design.js +70 -0
- package/lib/plugins/index.d.ts +1 -1
- package/lib/plugins/index.js +6 -2
- package/lib/plugins/slides.js +1 -1
- package/lib/project.js +3 -2
- package/lib/prompt.d.ts +1 -0
- package/lib/prompt.js +1 -1
- package/lib/run.js +4 -9
- package/lib/suggest.d.ts +4 -0
- package/lib/suggest.js +15 -2
- package/lib/suggestions.d.ts +1 -1
- package/lib/suggestions.js +3 -2
- package/package.json +1 -1
package/lib/api/studio.d.ts
CHANGED
|
@@ -30,7 +30,34 @@ export interface Start {
|
|
|
30
30
|
input: Record<string, unknown>;
|
|
31
31
|
title?: string;
|
|
32
32
|
parent?: string;
|
|
33
|
+
/** A brand (design system) the run reads as brand.* — its palette, type and voice. */
|
|
34
|
+
brand?: string;
|
|
33
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* A design system: one of Studio's brands, the one store for them. Its tokens
|
|
38
|
+
* are W3C design tokens (color, font, type, space, radius) and two groups of our
|
|
39
|
+
* own, `components` and `voice`, each a line a model follows.
|
|
40
|
+
*
|
|
41
|
+
* GET /v1/studio/brands {data: [brand]} the ones you may read
|
|
42
|
+
* GET /v1/studio/brands/{id} one
|
|
43
|
+
* POST /v1/studio/brands {name, tokens, shared?} → brand
|
|
44
|
+
*/
|
|
45
|
+
export interface Brand {
|
|
46
|
+
id: string;
|
|
47
|
+
name: string;
|
|
48
|
+
tokens: Record<string, unknown>;
|
|
49
|
+
}
|
|
50
|
+
export declare function brandOf(raw: unknown): Brand;
|
|
51
|
+
export declare function brands(t: Target, init?: {
|
|
52
|
+
signal?: AbortSignal;
|
|
53
|
+
}): Promise<Brand[]>;
|
|
54
|
+
export declare function brand(t: Target, id: string, init?: {
|
|
55
|
+
signal?: AbortSignal;
|
|
56
|
+
}): Promise<Brand>;
|
|
57
|
+
export declare function keepBrand(t: Target, b: {
|
|
58
|
+
name: string;
|
|
59
|
+
tokens: Record<string, unknown>;
|
|
60
|
+
}, signal?: AbortSignal): Promise<Brand>;
|
|
34
61
|
export declare function run(raw: unknown): Run;
|
|
35
62
|
/** A run that has ended, one way or the other. */
|
|
36
63
|
export declare const ended: (r: Run) => boolean;
|
package/lib/api/studio.js
CHANGED
|
@@ -14,6 +14,20 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { call, Refusal, seg } from './call.js';
|
|
16
16
|
import { item } from './library.js';
|
|
17
|
+
export function brandOf(raw) {
|
|
18
|
+
const o = obj(raw);
|
|
19
|
+
return { id: str(o.id), name: str(o.name), tokens: obj(o.tokens) };
|
|
20
|
+
}
|
|
21
|
+
export async function brands(t, init) {
|
|
22
|
+
const raw = obj(await call(t, 'GET', '/v1/studio/brands', undefined, init));
|
|
23
|
+
return (Array.isArray(raw.data) ? raw.data : []).map(brandOf).filter((b) => b.id);
|
|
24
|
+
}
|
|
25
|
+
export async function brand(t, id, init) {
|
|
26
|
+
return brandOf(await call(t, 'GET', `/v1/studio/brands/${seg(id)}`, undefined, init));
|
|
27
|
+
}
|
|
28
|
+
export async function keepBrand(t, b, signal) {
|
|
29
|
+
return brandOf(await call(t, 'POST', '/v1/studio/brands', { name: b.name, tokens: b.tokens }, { signal }));
|
|
30
|
+
}
|
|
17
31
|
const str = (v) => (typeof v === 'string' ? v : '');
|
|
18
32
|
const obj = (v) => (v && typeof v === 'object' ? v : {});
|
|
19
33
|
export function run(raw) {
|
package/lib/carry.d.ts
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { type ReactNode } from 'react';
|
|
2
2
|
import { type Target } from './api/call.ts';
|
|
3
|
-
import { type GearState } from './gear.ts';
|
|
3
|
+
import { type GearState, type Mode } from './gear.ts';
|
|
4
4
|
import type { Composer, Prep, Turn } from './plugin.ts';
|
|
5
5
|
import type { Shelf } from './plus.tsx';
|
|
6
|
-
import type { Chip } from './prompt.tsx';
|
|
6
|
+
import type { Chip, PromptOptions } from './prompt.tsx';
|
|
7
7
|
/** What the surface lends the plugins: its draft, its files, its way about. */
|
|
8
8
|
export type Lent = Omit<Composer, 'target' | 'show'>;
|
|
9
9
|
export interface Carry {
|
|
10
10
|
gear: GearState;
|
|
11
11
|
skills: Shelf;
|
|
12
|
+
/** The design systems (Studio brands): attach one, or make one by chat. */
|
|
13
|
+
design: Shelf;
|
|
12
14
|
/** The plugins that run for this person (`@` lists the same). */
|
|
13
15
|
plugins: Shelf;
|
|
14
16
|
/** A dialog a plugin opened, for the surface to draw; null when none is. */
|
|
@@ -16,14 +18,25 @@ export interface Carry {
|
|
|
16
18
|
/** Whether a plugin that prepares a message is on. */
|
|
17
19
|
preparing: boolean;
|
|
18
20
|
/** What the plugins that are on add to the message about to go. */
|
|
19
|
-
prep: (turn: Turn, signal: AbortSignal) => Promise<Prep>;
|
|
21
|
+
prep: (turn: Omit<Turn, 'design'>, signal: AbortSignal) => Promise<Prep>;
|
|
20
22
|
/** The chips after `+`: the skills on, Research, and Plan. */
|
|
21
23
|
on: Chip[];
|
|
24
|
+
/** The mode pill: this surface's modes, the conversation's choice among them. */
|
|
25
|
+
mode: NonNullable<PromptOptions['mode']>;
|
|
26
|
+
/** Whether `what` may run: in Manual the person is asked (once, always in this conversation, or not); else yes. */
|
|
27
|
+
allow: (what: Asked) => Promise<boolean>;
|
|
22
28
|
/** The words the gear adds to every message, '' when none. */
|
|
23
29
|
text: string;
|
|
24
30
|
}
|
|
31
|
+
/** What Manual asks about: a plugin, or a Dev run. `id` is what "always" remembers. */
|
|
32
|
+
export interface Asked {
|
|
33
|
+
id: string;
|
|
34
|
+
name: string;
|
|
35
|
+
note: string;
|
|
36
|
+
}
|
|
25
37
|
/**
|
|
26
38
|
* The conversation `key` names (`''` for one not begun), for target `t`.
|
|
27
|
-
* `customize(tab)` opens Dev's Customize on that tab
|
|
39
|
+
* `customize(tab)` opens Dev's Customize on that tab; `modes` are the ones this
|
|
40
|
+
* surface offers (a choice it does not offer reads as Auto).
|
|
28
41
|
*/
|
|
29
|
-
export declare function useCarry(t: Target, key: string, customize: (tab: 'skills') => void, lent: Lent): Carry;
|
|
42
|
+
export declare function useCarry(t: Target, key: string, customize: (tab: 'skills') => void, lent: Lent, modes: readonly Mode[]): Carry;
|
package/lib/carry.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
3
|
* What a message carries beside its words, assembled once for Chat and Dev:
|
|
4
4
|
* the conversation's gear (gear.ts) drawn as the `+` menu's shelves and the
|
|
@@ -10,16 +10,21 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
10
10
|
* field, so the text rides the message
|
|
11
11
|
* memory what Hanzo remembers about the person (GET /v1/ai/memory/list):
|
|
12
12
|
* the platform injects none itself
|
|
13
|
-
* mode Plan: lay the plan out and wait; Auto says nothing
|
|
13
|
+
* mode Plan: lay the plan out and wait; Auto says nothing; Manual
|
|
14
|
+
* asks the person before each plugin runs for a message (`Allow`)
|
|
14
15
|
*
|
|
15
16
|
* A shelf is offered only where its service answers; a surface adds its own
|
|
16
17
|
* (Chat its agents, Dev its devices) beside these.
|
|
17
18
|
*/
|
|
18
|
-
import {
|
|
19
|
+
import { SizableText, XStack } from '@hanzo/gui';
|
|
20
|
+
import { BookOpen, ListChecks, Palette, ScrollText } from '@hanzogui/lucide-icons-2';
|
|
21
|
+
import { Button, Dialog, DialogContent, DialogDescription, DialogTitle } from '@hanzo/ui';
|
|
19
22
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
20
23
|
import { call, query, unwrap } from './api/call.js';
|
|
21
24
|
import { active, authored, document, nameOf as skillOf } from './api/skills.js';
|
|
25
|
+
import { brand, brands } from './api/studio.js';
|
|
22
26
|
import { useRead } from './data.js';
|
|
27
|
+
import { said as designSaid } from './design.js';
|
|
23
28
|
import { useGear } from './gear.js';
|
|
24
29
|
import { active as lighted, prep, shelf as pluginShelf, usePlugins } from './plugins/index.js';
|
|
25
30
|
/** The most of each skill a message carries. */
|
|
@@ -34,8 +39,13 @@ async function remembered(t) {
|
|
|
34
39
|
.filter(Boolean);
|
|
35
40
|
}
|
|
36
41
|
/** The words `skills`, `memory` and `mode` add to a message. */
|
|
37
|
-
async function said(t, skills, memory, mode) {
|
|
42
|
+
async function said(t, skills, memory, mode, design) {
|
|
38
43
|
const parts = [];
|
|
44
|
+
if (design) {
|
|
45
|
+
const b = await brand(t, design).catch(() => null);
|
|
46
|
+
if (b?.name)
|
|
47
|
+
parts.push(designSaid(b));
|
|
48
|
+
}
|
|
39
49
|
if (skills.length) {
|
|
40
50
|
const own = await authored(t).catch(() => []);
|
|
41
51
|
for (const n of skills) {
|
|
@@ -53,11 +63,16 @@ async function said(t, skills, memory, mode) {
|
|
|
53
63
|
parts.push('Plan mode: lay out the plan — the steps, what each changes and why — and stop. Change nothing and produce no final work until the person says go.');
|
|
54
64
|
return parts.join('\n\n');
|
|
55
65
|
}
|
|
66
|
+
/** Manual's pause: what is about to run, what it does, and the three answers. Closing it is a no. */
|
|
67
|
+
function Allow({ what, onAnswer }) {
|
|
68
|
+
return (_jsx(Dialog, { open: true, onOpenChange: (o) => !o && onAnswer('deny'), children: _jsxs(DialogContent, { maxW: 440, width: "100%", "data-slot": "allow", children: [_jsx(DialogTitle, { children: `Allow ${what.name}?` }), _jsx(DialogDescription, { children: what.note ? `${what.note}. Manual asks before each one runs.` : 'Manual asks before each one runs.' }), _jsxs(XStack, { justify: "flex-end", gap: "$2", pt: "$3", flexWrap: "wrap", children: [_jsx(Button, { size: "sm", variant: "ghost", onPress: () => onAnswer('deny'), children: "Deny" }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => onAnswer('always'), children: _jsx(SizableText, { fontSize: 14, children: "Always in this chat" }) }), _jsx(Button, { size: "sm", onPress: () => onAnswer('once'), children: "Allow once" })] })] }) }));
|
|
69
|
+
}
|
|
56
70
|
/**
|
|
57
71
|
* The conversation `key` names (`''` for one not begun), for target `t`.
|
|
58
|
-
* `customize(tab)` opens Dev's Customize on that tab
|
|
72
|
+
* `customize(tab)` opens Dev's Customize on that tab; `modes` are the ones this
|
|
73
|
+
* surface offers (a choice it does not offer reads as Auto).
|
|
59
74
|
*/
|
|
60
|
-
export function useCarry(t, key, customize, lent) {
|
|
75
|
+
export function useCarry(t, key, customize, lent, modes) {
|
|
61
76
|
const gear = useGear(t, key);
|
|
62
77
|
const [dialog, show] = useState(null);
|
|
63
78
|
// The composer a plugin acts on, read when it acts: the draft moves under it.
|
|
@@ -77,7 +92,8 @@ export function useCarry(t, key, customize, lent) {
|
|
|
77
92
|
const signed = Boolean(t.token());
|
|
78
93
|
const list = useRead(signed ? () => active(t) : null, [], [t, signed]);
|
|
79
94
|
const names = useMemo(() => list.value.map((s) => skillOf(s.name)).filter(Boolean), [list.value]);
|
|
80
|
-
const { skills, research, memory,
|
|
95
|
+
const { skills, research, memory, design } = gear.gear;
|
|
96
|
+
const mode = modes.includes(gear.gear.mode) ? gear.gear.mode : 'auto';
|
|
81
97
|
const shelf = {
|
|
82
98
|
rows: names.map((n) => ({ id: n, label: n, icon: _jsx(ScrollText, { size: 16 }), on: skills.includes(n) })),
|
|
83
99
|
pick: (id) => gear.toggle('skills', id),
|
|
@@ -89,11 +105,33 @@ export function useCarry(t, key, customize, lent) {
|
|
|
89
105
|
browse: { label: 'Browse skills', onPress: () => customize('skills') },
|
|
90
106
|
};
|
|
91
107
|
const plugins = pluginShelf(found, gear, composer);
|
|
108
|
+
// The design systems: Studio's brands, the one store for them.
|
|
109
|
+
const kits = useRead(signed ? () => brands(t) : null, [], [t, signed, design]);
|
|
110
|
+
const named = kits.value.find((b) => b.id === design);
|
|
111
|
+
const designs = {
|
|
112
|
+
rows: kits.value.map((b) => {
|
|
113
|
+
const c = (b.tokens.color ?? {});
|
|
114
|
+
return { id: b.id, label: b.name, note: [c.primary?.$value, c.accent?.$value].filter(Boolean).join(' '), icon: _jsx(Palette, { size: 16 }), on: b.id === design };
|
|
115
|
+
}),
|
|
116
|
+
pick: (id) => gear.set({ design: design === id ? '' : id }),
|
|
117
|
+
loading: kits.loading,
|
|
118
|
+
error: kits.error ? 'The design systems could not be read.' : null,
|
|
119
|
+
empty: 'No design systems yet',
|
|
120
|
+
create: {
|
|
121
|
+
label: 'Create design system',
|
|
122
|
+
onPress: () => {
|
|
123
|
+
if (!gear.gear.plugins.includes('design'))
|
|
124
|
+
gear.toggle('plugins', 'design');
|
|
125
|
+
lentNow.current.setDraft('Make me a design system for ');
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
};
|
|
92
129
|
const lit = lighted(found.plugins, gear.gear);
|
|
93
130
|
const on = [
|
|
94
131
|
...skills.map((n) => ({ id: `skill:${n}`, label: n, icon: _jsx(ScrollText, { size: 14 }), onRemove: () => gear.toggle('skills', n) })),
|
|
95
132
|
...(research ? [{ id: 'research', label: 'Research', icon: _jsx(BookOpen, { size: 14 }), onRemove: () => gear.set({ research: false }) }] : []),
|
|
96
133
|
...(mode === 'plan' ? [{ id: 'plan', label: 'Plan', icon: _jsx(ListChecks, { size: 14 }), onRemove: () => gear.set({ mode: 'auto' }) }] : []),
|
|
134
|
+
...(design ? [{ id: 'design', label: named?.name ?? 'Design system', icon: _jsx(Palette, { size: 14 }), onRemove: () => gear.set({ design: '' }) }] : []),
|
|
97
135
|
...lit
|
|
98
136
|
.filter((p) => p.id !== 'research')
|
|
99
137
|
.map((p) => ({ id: `plugin:${p.id}`, label: p.name, icon: p.icon, onRemove: () => gear.toggle('plugins', p.id) })),
|
|
@@ -105,21 +143,51 @@ export function useCarry(t, key, customize, lent) {
|
|
|
105
143
|
if (!signedIn)
|
|
106
144
|
return setText(mode === 'plan' ? 'Plan mode: lay out the plan and stop until the person says go.' : '');
|
|
107
145
|
let live = true;
|
|
108
|
-
void said(t, skills, memory, mode).then((x) => live && setText(x));
|
|
146
|
+
void said(t, skills, memory, mode, design).then((x) => live && setText(x));
|
|
109
147
|
return () => {
|
|
110
148
|
live = false;
|
|
111
149
|
};
|
|
112
150
|
// `skills` is read through `asked`, its contents.
|
|
113
151
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
114
|
-
}, [t, signedIn, asked, memory, mode]);
|
|
152
|
+
}, [t, signedIn, asked, memory, mode, design]);
|
|
153
|
+
// Manual: what is about to run is asked of the person, unless allowed for the rest of the conversation.
|
|
154
|
+
const kept = useRef([]);
|
|
155
|
+
kept.current = gear.gear.allowed;
|
|
156
|
+
const allow = async (what) => {
|
|
157
|
+
if (mode !== 'manual' || kept.current.includes(what.id))
|
|
158
|
+
return true;
|
|
159
|
+
const v = await new Promise((done) => show(_jsx(Allow, { what: what, onAnswer: (x) => {
|
|
160
|
+
show(null);
|
|
161
|
+
done(x);
|
|
162
|
+
} })));
|
|
163
|
+
if (v === 'always') {
|
|
164
|
+
kept.current = [...kept.current, what.id];
|
|
165
|
+
gear.set({ allowed: kept.current });
|
|
166
|
+
}
|
|
167
|
+
return v !== 'deny';
|
|
168
|
+
};
|
|
115
169
|
return {
|
|
116
170
|
gear,
|
|
171
|
+
mode: { value: mode, set: (m) => gear.set({ mode: m }), items: modes },
|
|
172
|
+
allow,
|
|
117
173
|
skills: shelf,
|
|
118
174
|
plugins,
|
|
119
175
|
dialog,
|
|
120
176
|
on,
|
|
121
177
|
text,
|
|
122
178
|
preparing: lit.length > 0,
|
|
123
|
-
|
|
179
|
+
design: designs,
|
|
180
|
+
// A design system a plugin made is attached at once, and the maker turns off: it made one.
|
|
181
|
+
prep: async (turn, signal) => {
|
|
182
|
+
// One plugin at a time, so each pause is its own question.
|
|
183
|
+
const denied = [];
|
|
184
|
+
for (const x of lit)
|
|
185
|
+
if (!signal.aborted && !(await allow({ id: x.id, name: x.name, note: x.note })))
|
|
186
|
+
denied.push(x.id);
|
|
187
|
+
const p = await prep(found.plugins, gear.gear, { ...turn, design }, composer, signal, denied);
|
|
188
|
+
if (p.design)
|
|
189
|
+
gear.set({ design: p.design, plugins: gear.gear.plugins.filter((x) => x !== 'design') });
|
|
190
|
+
return p;
|
|
191
|
+
},
|
|
124
192
|
};
|
|
125
193
|
}
|
package/lib/design.d.ts
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A design system as the composer handles it: drafted by a model, kept as a
|
|
3
|
+
* Studio brand (W3C design tokens plus `components` and `voice`), said to the
|
|
4
|
+
* model on every message of a conversation it is attached to, and drawn as a
|
|
5
|
+
* page to look at. Pure, so it is tested without a browser.
|
|
6
|
+
*/
|
|
7
|
+
import type { Brand } from './api/studio.ts';
|
|
8
|
+
/** What a model is asked to answer: the system in plain fields. */
|
|
9
|
+
export interface Draft {
|
|
10
|
+
name: string;
|
|
11
|
+
colors: Record<string, string>;
|
|
12
|
+
fonts: {
|
|
13
|
+
heading: string;
|
|
14
|
+
body: string;
|
|
15
|
+
mono: string;
|
|
16
|
+
};
|
|
17
|
+
/** Type sizes, px, smallest first. */
|
|
18
|
+
scale: number[];
|
|
19
|
+
/** Spacing steps, px. */
|
|
20
|
+
spacing: number[];
|
|
21
|
+
radius: Record<string, number>;
|
|
22
|
+
/** One line each: how a button, card, input, nav… looks and behaves. */
|
|
23
|
+
components: Record<string, string>;
|
|
24
|
+
voice: {
|
|
25
|
+
tone: string;
|
|
26
|
+
do: string[];
|
|
27
|
+
dont: string[];
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** The schema the drafting call decodes under. */
|
|
31
|
+
export declare const SCHEMA: {
|
|
32
|
+
readonly type: 'object';
|
|
33
|
+
readonly additionalProperties: false;
|
|
34
|
+
readonly required: readonly ['name', 'colors', 'fonts', 'scale', 'spacing', 'radius', 'components', 'voice'];
|
|
35
|
+
readonly properties: {
|
|
36
|
+
readonly name: {
|
|
37
|
+
readonly type: 'string';
|
|
38
|
+
};
|
|
39
|
+
readonly colors: {
|
|
40
|
+
readonly type: 'object';
|
|
41
|
+
readonly additionalProperties: false;
|
|
42
|
+
readonly required: readonly ['primary', 'secondary', 'accent', 'background', 'surface', 'text', 'muted', 'border'];
|
|
43
|
+
readonly properties: {
|
|
44
|
+
[k: string]: {
|
|
45
|
+
type: string;
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
readonly fonts: {
|
|
50
|
+
readonly type: 'object';
|
|
51
|
+
readonly additionalProperties: false;
|
|
52
|
+
readonly required: readonly ['heading', 'body', 'mono'];
|
|
53
|
+
readonly properties: {
|
|
54
|
+
readonly heading: {
|
|
55
|
+
readonly type: 'string';
|
|
56
|
+
};
|
|
57
|
+
readonly body: {
|
|
58
|
+
readonly type: 'string';
|
|
59
|
+
};
|
|
60
|
+
readonly mono: {
|
|
61
|
+
readonly type: 'string';
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
readonly scale: {
|
|
66
|
+
readonly type: 'array';
|
|
67
|
+
readonly items: {
|
|
68
|
+
readonly type: 'number';
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
readonly spacing: {
|
|
72
|
+
readonly type: 'array';
|
|
73
|
+
readonly items: {
|
|
74
|
+
readonly type: 'number';
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
readonly radius: {
|
|
78
|
+
readonly type: 'object';
|
|
79
|
+
readonly additionalProperties: false;
|
|
80
|
+
readonly required: readonly ['sm', 'md', 'lg'];
|
|
81
|
+
readonly properties: {
|
|
82
|
+
readonly sm: {
|
|
83
|
+
readonly type: 'number';
|
|
84
|
+
};
|
|
85
|
+
readonly md: {
|
|
86
|
+
readonly type: 'number';
|
|
87
|
+
};
|
|
88
|
+
readonly lg: {
|
|
89
|
+
readonly type: 'number';
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
};
|
|
93
|
+
readonly components: {
|
|
94
|
+
readonly type: 'object';
|
|
95
|
+
readonly additionalProperties: false;
|
|
96
|
+
readonly required: readonly ['button', 'card', 'input', 'nav', 'hero'];
|
|
97
|
+
readonly properties: {
|
|
98
|
+
[k: string]: {
|
|
99
|
+
type: string;
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
readonly voice: {
|
|
104
|
+
readonly type: 'object';
|
|
105
|
+
readonly additionalProperties: false;
|
|
106
|
+
readonly required: readonly ['tone', 'do', 'dont'];
|
|
107
|
+
readonly properties: {
|
|
108
|
+
readonly tone: {
|
|
109
|
+
readonly type: 'string';
|
|
110
|
+
};
|
|
111
|
+
readonly do: {
|
|
112
|
+
readonly type: 'array';
|
|
113
|
+
readonly items: {
|
|
114
|
+
readonly type: 'string';
|
|
115
|
+
};
|
|
116
|
+
};
|
|
117
|
+
readonly dont: {
|
|
118
|
+
readonly type: 'array';
|
|
119
|
+
readonly items: {
|
|
120
|
+
readonly type: 'string';
|
|
121
|
+
};
|
|
122
|
+
};
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
};
|
|
126
|
+
};
|
|
127
|
+
/** A model's answer as a Draft, or why it is not one. */
|
|
128
|
+
export declare function drafted(raw: unknown): Draft | string;
|
|
129
|
+
/** A Draft as W3C design tokens, with our two groups beside them. */
|
|
130
|
+
export declare function tokensOf(d: Draft): Record<string, unknown>;
|
|
131
|
+
/** What a model is told on every message of a conversation the system is attached to. */
|
|
132
|
+
export declare function said(b: Brand): string;
|
|
133
|
+
/** The system drawn as a page: its palette, type, a button, a card, an input, its voice. */
|
|
134
|
+
export declare function preview(b: Brand): string;
|
package/lib/design.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/** The schema the drafting call decodes under. */
|
|
2
|
+
export const SCHEMA = {
|
|
3
|
+
type: 'object',
|
|
4
|
+
additionalProperties: false,
|
|
5
|
+
required: ['name', 'colors', 'fonts', 'scale', 'spacing', 'radius', 'components', 'voice'],
|
|
6
|
+
properties: {
|
|
7
|
+
name: { type: 'string' },
|
|
8
|
+
colors: {
|
|
9
|
+
type: 'object',
|
|
10
|
+
additionalProperties: false,
|
|
11
|
+
required: ['primary', 'secondary', 'accent', 'background', 'surface', 'text', 'muted', 'border'],
|
|
12
|
+
properties: Object.fromEntries(['primary', 'secondary', 'accent', 'background', 'surface', 'text', 'muted', 'border'].map((k) => [k, { type: 'string' }])),
|
|
13
|
+
},
|
|
14
|
+
fonts: { type: 'object', additionalProperties: false, required: ['heading', 'body', 'mono'], properties: { heading: { type: 'string' }, body: { type: 'string' }, mono: { type: 'string' } } },
|
|
15
|
+
scale: { type: 'array', items: { type: 'number' } },
|
|
16
|
+
spacing: { type: 'array', items: { type: 'number' } },
|
|
17
|
+
radius: { type: 'object', additionalProperties: false, required: ['sm', 'md', 'lg'], properties: { sm: { type: 'number' }, md: { type: 'number' }, lg: { type: 'number' } } },
|
|
18
|
+
components: {
|
|
19
|
+
type: 'object',
|
|
20
|
+
additionalProperties: false,
|
|
21
|
+
required: ['button', 'card', 'input', 'nav', 'hero'],
|
|
22
|
+
properties: Object.fromEntries(['button', 'card', 'input', 'nav', 'hero'].map((k) => [k, { type: 'string' }])),
|
|
23
|
+
},
|
|
24
|
+
voice: { type: 'object', additionalProperties: false, required: ['tone', 'do', 'dont'], properties: { tone: { type: 'string' }, do: { type: 'array', items: { type: 'string' } }, dont: { type: 'array', items: { type: 'string' } } } },
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
const HEX = /^#[0-9a-f]{3}([0-9a-f]{3})?$/i;
|
|
28
|
+
const str = (v) => (typeof v === 'string' ? v.trim() : '');
|
|
29
|
+
const obj = (v) => (v && typeof v === 'object' && !Array.isArray(v) ? v : {});
|
|
30
|
+
const nums = (v, lo, hi) => (Array.isArray(v) ? v.filter((n) => typeof n === 'number' && n >= lo && n <= hi) : []);
|
|
31
|
+
const lines = (v) => (Array.isArray(v) ? v.map(str).filter(Boolean).slice(0, 8) : []);
|
|
32
|
+
/** A model's answer as a Draft, or why it is not one. */
|
|
33
|
+
export function drafted(raw) {
|
|
34
|
+
const o = obj(raw);
|
|
35
|
+
const name = str(o.name).slice(0, 80);
|
|
36
|
+
if (!name)
|
|
37
|
+
return 'The design system has no name.';
|
|
38
|
+
const colors = Object.fromEntries(Object.entries(obj(o.colors)).filter(([, v]) => HEX.test(str(v))).map(([k, v]) => [k, str(v)]));
|
|
39
|
+
if (!colors.primary || !colors.background || !colors.text)
|
|
40
|
+
return 'The design system needs at least a primary, a background and a text colour.';
|
|
41
|
+
const f = obj(o.fonts);
|
|
42
|
+
const fonts = { heading: str(f.heading) || 'Inter', body: str(f.body) || 'Inter', mono: str(f.mono) || 'ui-monospace' };
|
|
43
|
+
const r = obj(o.radius);
|
|
44
|
+
const radius = Object.fromEntries(Object.entries(r).filter(([, v]) => typeof v === 'number' && v >= 0 && v <= 64));
|
|
45
|
+
const v = obj(o.voice);
|
|
46
|
+
return {
|
|
47
|
+
name,
|
|
48
|
+
colors,
|
|
49
|
+
fonts,
|
|
50
|
+
scale: nums(o.scale, 8, 160).slice(0, 10),
|
|
51
|
+
spacing: nums(o.spacing, 0, 256).slice(0, 12),
|
|
52
|
+
radius,
|
|
53
|
+
components: Object.fromEntries(Object.entries(obj(o.components)).map(([k, d]) => [k, str(d)]).filter(([, d]) => d)),
|
|
54
|
+
voice: { tone: str(v.tone), do: lines(v.do), dont: lines(v.dont) },
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/** A Draft as W3C design tokens, with our two groups beside them. */
|
|
58
|
+
export function tokensOf(d) {
|
|
59
|
+
const group = (rec, type, unit = '') => Object.fromEntries(Object.entries(rec).map(([k, v]) => [k, { $value: unit ? `${v}${unit}` : v, $type: type }]));
|
|
60
|
+
return {
|
|
61
|
+
color: group(d.colors, 'color'),
|
|
62
|
+
font: group({ heading: d.fonts.heading, body: d.fonts.body, mono: d.fonts.mono }, 'fontFamily'),
|
|
63
|
+
type: group(Object.fromEntries(d.scale.map((n, i) => [`step${i}`, n])), 'dimension', 'px'),
|
|
64
|
+
space: group(Object.fromEntries(d.spacing.map((n, i) => [`s${i}`, n])), 'dimension', 'px'),
|
|
65
|
+
radius: group(d.radius, 'dimension', 'px'),
|
|
66
|
+
components: Object.fromEntries(Object.entries(d.components).map(([k, v]) => [k, { $value: v, $type: 'string' }])),
|
|
67
|
+
voice: { tone: { $value: d.voice.tone, $type: 'string' }, do: { $value: d.voice.do, $type: 'string' }, dont: { $value: d.voice.dont, $type: 'string' } },
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/** One token group as `name value` pairs. */
|
|
71
|
+
function pairs(tokens, group) {
|
|
72
|
+
return Object.entries(obj(tokens[group])).map(([k, v]) => {
|
|
73
|
+
const val = obj(v).$value;
|
|
74
|
+
return [k, Array.isArray(val) ? val.join('; ') : String(val ?? '')];
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
/** What a model is told on every message of a conversation the system is attached to. */
|
|
78
|
+
export function said(b) {
|
|
79
|
+
const t = b.tokens;
|
|
80
|
+
const row = (g) => pairs(t, g).map(([k, v]) => `${k} ${v}`).join(', ');
|
|
81
|
+
const parts = [
|
|
82
|
+
`Design system "${b.name}" is attached to this conversation. Everything you make — pages, apps, components, slides, images and copy — follows it exactly; never invent other colours, fonts or radii.`,
|
|
83
|
+
row('color') && `Colours: ${row('color')}.`,
|
|
84
|
+
row('font') && `Fonts: ${row('font')}.`,
|
|
85
|
+
row('type') && `Type scale: ${row('type')}.`,
|
|
86
|
+
row('space') && `Spacing: ${row('space')}.`,
|
|
87
|
+
row('radius') && `Radius: ${row('radius')}.`,
|
|
88
|
+
row('components') && `Components: ${pairs(t, 'components').map(([k, v]) => `${k} — ${v}`).join(' ')}`,
|
|
89
|
+
row('voice') && `Voice: ${pairs(t, 'voice').map(([k, v]) => `${k}: ${v}`).join(' | ')}`,
|
|
90
|
+
'In HTML, set these as CSS custom properties on :root and use them; load the fonts from Google Fonts when they are web fonts.',
|
|
91
|
+
];
|
|
92
|
+
return parts.filter(Boolean).join('\n');
|
|
93
|
+
}
|
|
94
|
+
const esc = (s) => s.replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' })[c]);
|
|
95
|
+
/** The system drawn as a page: its palette, type, a button, a card, an input, its voice. */
|
|
96
|
+
export function preview(b) {
|
|
97
|
+
const t = b.tokens;
|
|
98
|
+
const c = Object.fromEntries(pairs(t, 'color'));
|
|
99
|
+
const f = Object.fromEntries(pairs(t, 'font'));
|
|
100
|
+
const r = Object.fromEntries(pairs(t, 'radius'));
|
|
101
|
+
const sizes = pairs(t, 'type').map(([, v]) => v);
|
|
102
|
+
const voice = Object.fromEntries(pairs(t, 'voice'));
|
|
103
|
+
const comps = pairs(t, 'components');
|
|
104
|
+
const fonts = [f.heading, f.body].filter((x) => x && !/^ui-|system|sans-serif|serif$/i.test(x));
|
|
105
|
+
const link = fonts.length ? `<link rel="stylesheet" href="https://fonts.googleapis.com/css2?${fonts.map((x) => `family=${encodeURIComponent(x)}:wght@400;600;700`).join('&')}&display=swap">` : '';
|
|
106
|
+
const swatches = Object.entries(c).map(([k, v]) => `<div class="sw"><span style="background:${esc(v)}"></span><b>${esc(k)}</b><code>${esc(v)}</code></div>`).join('');
|
|
107
|
+
const scale = sizes.map((s) => `<div style="font-size:${esc(s)}">${esc(b.name)} — ${esc(s)}</div>`).join('');
|
|
108
|
+
return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${esc(b.name)}</title>${link}
|
|
109
|
+
<style>:root{--primary:${c.primary};--secondary:${c.secondary ?? c.primary};--accent:${c.accent ?? c.primary};--bg:${c.background};--surface:${c.surface ?? c.background};--text:${c.text};--muted:${c.muted ?? c.text};--border:${c.border ?? c.muted ?? c.text};--r-sm:${r.sm ?? '4px'};--r-md:${r.md ?? '8px'};--r-lg:${r.lg ?? '16px'}}
|
|
110
|
+
body{margin:0;background:var(--bg);color:var(--text);font-family:'${esc(f.body ?? 'Inter')}',system-ui,sans-serif;padding:40px;line-height:1.5}
|
|
111
|
+
h1,h2{font-family:'${esc(f.heading ?? 'Inter')}',system-ui,sans-serif;margin:0 0 12px}h2{margin-top:40px;font-size:14px;letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}
|
|
112
|
+
.row{display:flex;flex-wrap:wrap;gap:16px}.sw{display:flex;flex-direction:column;gap:4px;width:120px}.sw span{height:64px;border-radius:var(--r-md);border:1px solid var(--border)}.sw code{color:var(--muted);font-size:12px}
|
|
113
|
+
.btn{background:var(--primary);color:var(--bg);border:0;border-radius:var(--r-md);padding:12px 20px;font:inherit;font-weight:600}.btn.ghost{background:transparent;color:var(--text);border:1px solid var(--border)}
|
|
114
|
+
.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--r-lg);padding:20px;max-width:320px}input{background:var(--surface);color:var(--text);border:1px solid var(--border);border-radius:var(--r-sm);padding:10px 12px;font:inherit;width:260px}
|
|
115
|
+
li{color:var(--muted)}</style></head><body>
|
|
116
|
+
<h1 style="font-size:40px">${esc(b.name)}</h1><p style="color:var(--muted)">${esc(voice.tone ?? '')}</p>
|
|
117
|
+
<h2>Colour</h2><div class="row">${swatches}</div>
|
|
118
|
+
<h2>Type</h2>${scale}
|
|
119
|
+
<h2>Components</h2><div class="row" style="align-items:flex-start"><button class="btn">Primary action</button><button class="btn ghost">Secondary</button><input placeholder="An input"><div class="card"><b>A card</b><p style="color:var(--muted);margin:8px 0 0">${esc(comps.find(([k]) => k === 'card')?.[1] ?? '')}</p></div></div>
|
|
120
|
+
<ul>${comps.map(([k, v]) => `<li><b style="color:var(--text)">${esc(k)}</b> — ${esc(v)}</li>`).join('')}</ul>
|
|
121
|
+
<h2>Voice</h2><p><b>Do</b>: ${esc(voice.do ?? '')}</p><p><b>Don't</b>: ${esc(voice.dont ?? '')}</p>
|
|
122
|
+
</body></html>`;
|
|
123
|
+
}
|
package/lib/gear.d.ts
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
import { type Target } from './api/call.ts';
|
|
2
2
|
/** What a run or a turn may do on its own: the permission mode. */
|
|
3
|
-
export type Mode = 'auto' | 'plan';
|
|
3
|
+
export type Mode = 'auto' | 'manual' | 'plan';
|
|
4
4
|
/**
|
|
5
|
-
* The modes
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* The modes, one line each. Chat offers Auto and Manual; Dev adds Plan. Manual
|
|
6
|
+
* is a real pause: before a plugin runs for a message (a search, a site read, a
|
|
7
|
+
* deck), the person allows it once, always in this conversation, or not, and a
|
|
8
|
+
* Dev run is asked for before it starts.
|
|
8
9
|
*/
|
|
9
10
|
export declare const MODES: readonly {
|
|
10
11
|
id: Mode;
|
|
11
12
|
label: string;
|
|
12
13
|
note: string;
|
|
13
14
|
}[];
|
|
15
|
+
/** The modes each surface offers. */
|
|
16
|
+
export declare const CHAT_MODES: readonly Mode[];
|
|
17
|
+
export declare const DEV_MODES: readonly Mode[];
|
|
14
18
|
export interface Gear {
|
|
15
19
|
/** Skill names (`/v1/tool/skills`). */
|
|
16
20
|
skills: string[];
|
|
@@ -25,6 +29,8 @@ export interface Gear {
|
|
|
25
29
|
research: boolean;
|
|
26
30
|
memory: boolean;
|
|
27
31
|
mode: Mode;
|
|
32
|
+
/** In Manual, the plugins allowed for the rest of this conversation. */
|
|
33
|
+
allowed: string[];
|
|
28
34
|
}
|
|
29
35
|
export declare const BARE: Gear;
|
|
30
36
|
/** How many conversations keep a choice of their own: the document holds 16 KiB in all. */
|
package/lib/gear.js
CHANGED
|
@@ -18,14 +18,19 @@
|
|
|
18
18
|
import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
|
|
19
19
|
import { call } from './api/call.js';
|
|
20
20
|
/**
|
|
21
|
-
* The modes
|
|
22
|
-
*
|
|
23
|
-
*
|
|
21
|
+
* The modes, one line each. Chat offers Auto and Manual; Dev adds Plan. Manual
|
|
22
|
+
* is a real pause: before a plugin runs for a message (a search, a site read, a
|
|
23
|
+
* deck), the person allows it once, always in this conversation, or not, and a
|
|
24
|
+
* Dev run is asked for before it starts.
|
|
24
25
|
*/
|
|
25
26
|
export const MODES = [
|
|
26
|
-
{ id: 'auto', label: 'Auto', note: '
|
|
27
|
-
{ id: '
|
|
27
|
+
{ id: 'auto', label: 'Auto', note: 'Runs tools on its own; pauses only for anything risky.' },
|
|
28
|
+
{ id: 'manual', label: 'Manual', note: 'Asks before each tool, connector or site it opens.' },
|
|
29
|
+
{ id: 'plan', label: 'Plan', note: 'Writes the plan and waits for your go before changing anything.' },
|
|
28
30
|
];
|
|
31
|
+
/** The modes each surface offers. */
|
|
32
|
+
export const CHAT_MODES = ['auto', 'manual'];
|
|
33
|
+
export const DEV_MODES = ['auto', 'manual', 'plan'];
|
|
29
34
|
export const BARE = {
|
|
30
35
|
skills: [],
|
|
31
36
|
connectors: [],
|
|
@@ -35,6 +40,7 @@ export const BARE = {
|
|
|
35
40
|
research: false,
|
|
36
41
|
memory: true,
|
|
37
42
|
mode: 'auto',
|
|
43
|
+
allowed: [],
|
|
38
44
|
};
|
|
39
45
|
/** What a new conversation carries over from the last: the switches, never a pick. */
|
|
40
46
|
const STICKY = ['research', 'memory', 'mode'];
|
|
@@ -55,6 +61,7 @@ export function gearOf(raw, base = BARE) {
|
|
|
55
61
|
research: typeof o.research === 'boolean' ? o.research : base.research,
|
|
56
62
|
memory: typeof o.memory === 'boolean' ? o.memory : base.memory,
|
|
57
63
|
mode: MODES.some((m) => m.id === o.mode) ? o.mode : base.mode,
|
|
64
|
+
allowed: 'allowed' in o ? list(o.allowed) : base.allowed,
|
|
58
65
|
};
|
|
59
66
|
}
|
|
60
67
|
/** `gears` with `key`'s entry first, cut to `KEEP`. */
|
package/lib/gutter.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The grip between two panes, one for every edge: the sidebar's, the side
|
|
3
|
+
* panel's, a run's chat against its work. The kit's Grip with its `inline` bar
|
|
4
|
+
* (drawn only while it is reached for), its band centred across the gutter and
|
|
5
|
+
* the bar halfway down it, so each edge looks and moves the same.
|
|
6
|
+
*
|
|
7
|
+
* side the edge the box it sits in is pinned to (Grip's): `left` for a
|
|
8
|
+
* sidebar, whose free edge is its right, `right` for an aside
|
|
9
|
+
* gap the gutter's width; the frame's `--pane-gap` by default
|
|
10
|
+
* inset the box's own border, px: an absolute place is measured inside it
|
|
11
|
+
*/
|
|
12
|
+
import { type GripProps } from '@hanzo/ui';
|
|
13
|
+
export type GutterProps = Omit<GripProps, 'bar'> & {
|
|
14
|
+
gap?: string | number;
|
|
15
|
+
inset?: number;
|
|
16
|
+
};
|
|
17
|
+
export declare function Gutter({ side, gap, inset, ...rest }: GutterProps): import("react").JSX.Element;
|
package/lib/gutter.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* The grip between two panes, one for every edge: the sidebar's, the side
|
|
4
|
+
* panel's, a run's chat against its work. The kit's Grip with its `inline` bar
|
|
5
|
+
* (drawn only while it is reached for), its band centred across the gutter and
|
|
6
|
+
* the bar halfway down it, so each edge looks and moves the same.
|
|
7
|
+
*
|
|
8
|
+
* side the edge the box it sits in is pinned to (Grip's): `left` for a
|
|
9
|
+
* sidebar, whose free edge is its right, `right` for an aside
|
|
10
|
+
* gap the gutter's width; the frame's `--pane-gap` by default
|
|
11
|
+
* inset the box's own border, px: an absolute place is measured inside it
|
|
12
|
+
*/
|
|
13
|
+
import { Grip } from '@hanzo/ui';
|
|
14
|
+
import { gap as GAP } from './pane.js';
|
|
15
|
+
export function Gutter({ side, gap = GAP, inset = 0, ...rest }) {
|
|
16
|
+
const g = typeof gap === 'number' ? `${gap}px` : gap;
|
|
17
|
+
// A CSS length gui takes as written; its types name only tokens and numbers.
|
|
18
|
+
const band = `max(8px, ${g})`;
|
|
19
|
+
// The band's start, so its middle is the gutter's: half the gutter and half the band out from the edge.
|
|
20
|
+
const at = (w) => `calc(-1 * (${g} + ${w}) / 2 - ${inset}px)`;
|
|
21
|
+
const edge = side === 'left' ? 'r' : 'l';
|
|
22
|
+
return (_jsx(Grip, { side: side, bar: "inline", justify: "center", width: band, [edge]: at(band),
|
|
23
|
+
// A finger's band, centred on the same line.
|
|
24
|
+
$touchable: { width: 44, [edge]: at('44px') }, ...rest }));
|
|
25
|
+
}
|
package/lib/index.d.ts
CHANGED
|
@@ -30,6 +30,7 @@ export { DevSection, DOTS, useSessions } from './section.tsx';
|
|
|
30
30
|
export { HostProvider } from './host.tsx';
|
|
31
31
|
export { Slack, useWho, Who } from './foot.tsx';
|
|
32
32
|
export { Grip, type GripProps } from '@hanzo/ui';
|
|
33
|
+
export { Gutter, type GutterProps } from './gutter.tsx';
|
|
33
34
|
export { administers } from './claims.ts';
|
|
34
35
|
export { Meter, Title } from './meter.tsx';
|
|
35
36
|
export { Plan } from './settings/plan.tsx';
|
|
@@ -51,7 +52,7 @@ export { href, parse, path, PLACES, route, SESSION, SLUG, type Route, type Scree
|
|
|
51
52
|
export { catalog, EFFORTS, FIRST, FREE, grants, isFree, keep, kept, nameOf, reasons, useCatalog, useChoice, useMind, type Choice, type Effort, type Mind } from './mind.ts';
|
|
52
53
|
export { ASK, cut, GAP, MEASURE, measure, Prompt, prompt, ROUND, trigger, Tune, useEar, useMention, useUpload, type Chip, type PromptOptions } from './prompt.tsx';
|
|
53
54
|
export { Plus, type Link, type PlusProps, type Row as ShelfRow, type Shelf, type Switch } from './plus.tsx';
|
|
54
|
-
export { BARE, gearOf, MODES, useGear, useLanguage, type Gear, type GearState, type Mode as GearMode } from './gear.ts';
|
|
55
|
+
export { BARE, CHAT_MODES, DEV_MODES, gearOf, MODES, useGear, useLanguage, type Gear, type GearState, type Mode as GearMode } from './gear.ts';
|
|
55
56
|
export { useCarry, type Carry } from './carry.tsx';
|
|
56
57
|
export { Dictation } from './mic.tsx';
|
|
57
58
|
export { artifacts, blob, lead, renders, Reply, Served, speakable, speaker, type Artifact, type Listen } from './reply.tsx';
|
package/lib/index.js
CHANGED
|
@@ -30,6 +30,7 @@ export { DevSection, DOTS, useSessions } from './section.js';
|
|
|
30
30
|
export { HostProvider } from './host.js';
|
|
31
31
|
export { Slack, useWho, Who } from './foot.js';
|
|
32
32
|
export { Grip } from '@hanzo/ui';
|
|
33
|
+
export { Gutter } from './gutter.js';
|
|
33
34
|
export { administers } from './claims.js';
|
|
34
35
|
export { Meter, Title } from './meter.js';
|
|
35
36
|
export { Plan } from './settings/plan.js';
|
|
@@ -46,7 +47,7 @@ export { href, parse, path, PLACES, route, SESSION, SLUG } from './route.js';
|
|
|
46
47
|
export { catalog, EFFORTS, FIRST, FREE, grants, isFree, keep, kept, nameOf, reasons, useCatalog, useChoice, useMind } from './mind.js';
|
|
47
48
|
export { ASK, cut, GAP, MEASURE, measure, Prompt, prompt, ROUND, trigger, Tune, useEar, useMention, useUpload } from './prompt.js';
|
|
48
49
|
export { Plus } from './plus.js';
|
|
49
|
-
export { BARE, gearOf, MODES, useGear, useLanguage } from './gear.js';
|
|
50
|
+
export { BARE, CHAT_MODES, DEV_MODES, gearOf, MODES, useGear, useLanguage } from './gear.js';
|
|
50
51
|
export { useCarry } from './carry.js';
|
|
51
52
|
export { Dictation } from './mic.js';
|
|
52
53
|
export { artifacts, blob, lead, renders, Reply, Served, speakable, speaker } from './reply.js';
|
package/lib/landing.js
CHANGED
|
@@ -36,6 +36,7 @@ import { usePrefs } from './prefs.js';
|
|
|
36
36
|
import { Publish } from './publish.js';
|
|
37
37
|
import { path } from './route.js';
|
|
38
38
|
import { useCarry } from './carry.js';
|
|
39
|
+
import { DEV_MODES } from './gear.js';
|
|
39
40
|
import { Suggestions, useSuggestions } from './suggestions.js';
|
|
40
41
|
import { MEASURE, Prompt, useEar } from './prompt.js';
|
|
41
42
|
import { Monitor as Device } from '@hanzogui/lucide-icons-2';
|
|
@@ -134,8 +135,8 @@ export function Landing({ onStarted }) {
|
|
|
134
135
|
}
|
|
135
136
|
},
|
|
136
137
|
go: (href) => (href.startsWith('http') ? host.open(href) : host.go(href)),
|
|
137
|
-
});
|
|
138
|
-
const mode = carry.
|
|
138
|
+
}, DEV_MODES);
|
|
139
|
+
const mode = carry.mode.value === 'plan' ? 'plan' : 'build';
|
|
139
140
|
const hints = useSuggestions(t);
|
|
140
141
|
// The chosen codebase's environment, so New can say when it has none yet.
|
|
141
142
|
const codebase = isForge(kept.repo) ? kept.repo.name : '';
|
|
@@ -205,7 +206,13 @@ export function Landing({ onStarted }) {
|
|
|
205
206
|
const prepped = carry.preparing ? await carry.prep({ text: draft.trim(), files: [], thread: '' }, new AbortController().signal) : {};
|
|
206
207
|
// An answer of a plugin's own (a deck) is the answer: no run is started for it.
|
|
207
208
|
if (prepped.reply) {
|
|
208
|
-
|
|
209
|
+
// Its words, without the page it drew (Chat shows that in the side panel).
|
|
210
|
+
setNote(prepped.reply.split('```')[0].trim());
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
// Manual: the run is asked for before it starts; it edits, commits and pushes on its own once it has.
|
|
214
|
+
if (!(await carry.allow({ id: 'run', name: 'Dev to start this run', note: 'It edits, tests, commits and pushes its own branch' }))) {
|
|
215
|
+
setNote('Not started.');
|
|
209
216
|
return;
|
|
210
217
|
}
|
|
211
218
|
const run = await start(t, {
|
|
@@ -303,15 +310,16 @@ export function Landing({ onStarted }) {
|
|
|
303
310
|
loading: places.loading,
|
|
304
311
|
error: places.error?.message ?? null,
|
|
305
312
|
};
|
|
306
|
-
return (_jsxs(YStack, { flex: 1, minH: 0, minW: 0, items: "center", justify: "center", pb: "$10", overflowY: "auto", overflowX: "hidden", children: [_jsxs(YStack, { width: "100%", maxW: MEASURE, gap: "$
|
|
313
|
+
return (_jsxs(YStack, { flex: 1, minH: 0, minW: 0, items: "center", justify: "center", pb: "$10", overflowY: "auto", overflowX: "hidden", children: [_jsxs(YStack, { width: "100%", maxW: MEASURE, gap: "$3", children: [_jsx(SizableText, { render: "h1", fontSize: "$7", lineHeight: "$7", fontWeight: "700", color: "$ink", style: { textAlign: 'center' }, children: "Build what\u2019s next." }), _jsxs(YStack, { gap: "$2", children: [stranger ? (_jsxs(XStack, { items: "center", justify: "space-between", gap: "$3", px: "$3", py: "$2", rounded: "$10", borderWidth: 1, borderColor: "$borderColor", role: "alert", children: [_jsx(SizableText, { size: "$2", color: "$ink", flex: 1, minW: 0, children: `${codebase} is not one of this organization’s repositories.` }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => setPicking(true), children: "Choose a repository" })] })) : env.value && (env.value.state === 'proposed' || unset(env.value)) && !place.id ? (_jsxs(XStack, { items: "center", justify: "space-between", gap: "$3", px: "$3", py: "$2", rounded: "$10", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { size: "$2", color: "$ink", flex: 1, minW: 0, children: env.value.state === 'proposed'
|
|
307
314
|
? `A proposed environment for ${codebase} is waiting for review.`
|
|
308
315
|
: `${codebase} has no environment yet, so every run starts it bare.` }), env.value.state === 'proposed' && env.value.session ? (_jsx(Button, { size: "sm", variant: "outline", onPress: () => host.go(env.value.session), children: "Review" })) : (_jsx(Button, { size: "sm", onPress: () => setOffering(true), children: "Set up environment" }))] })) : null, note || unhonoured(mode, place.id) || mind.note ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: note || unhonoured(mode, place.id) || mind.note })) : null, _jsx(Prompt, { framed: true, value: draft, onChange: setDraft, onSend: () => void send(), busy: busy, label: "Describe a task or ask a question", head: head, plus: {
|
|
309
316
|
upload: () => pick(files, setFiles, setNote),
|
|
310
317
|
skills: carry.skills,
|
|
318
|
+
design: carry.design,
|
|
311
319
|
plugins: carry.plugins,
|
|
312
320
|
devices,
|
|
313
321
|
memory: { on: carry.gear.gear.memory, set: (on) => carry.gear.set({ memory: on }) },
|
|
314
|
-
}, on: carry.on, slash: carry.skills, at: carry.plugins, mode:
|
|
322
|
+
}, on: carry.on, slash: carry.skills, at: carry.plugins, mode: carry.mode, where: where, below: _jsx(Suggestions, { rows: hints.rows, onPick: setDraft, onDismiss: hints.dismiss }), mind: mind, limits: limits, onUpgrade: () => host.go(path({ kind: 'screen', screen: 'plans' })), voice: voice, language: prefs.language ?? null, onLanguage: (id) => void savePrefs({ language: id }), talk: talk })] })] }), _jsx(Publish, { source: adding, onClose: () => setAdding(null) }), carry.dialog, codebase ? (_jsx(SetupDialog, { repo: codebase, homes: homes, open: offering, onOpenChange: setOffering, onStart: setup, onRepo: choose, onSaved: () => {
|
|
315
323
|
setOffering(false);
|
|
316
324
|
env.reload();
|
|
317
325
|
} })) : null] }));
|
package/lib/plugin.d.ts
CHANGED
|
@@ -35,6 +35,8 @@ export interface Turn {
|
|
|
35
35
|
files: File[];
|
|
36
36
|
/** The conversation's id, or '' for one not begun. */
|
|
37
37
|
thread: string;
|
|
38
|
+
/** The design system attached to the conversation (a Studio brand id), or ''. */
|
|
39
|
+
design: string;
|
|
38
40
|
}
|
|
39
41
|
/** What a plugin adds to the message. */
|
|
40
42
|
export interface Prep {
|
|
@@ -44,6 +46,8 @@ export interface Prep {
|
|
|
44
46
|
files?: File[];
|
|
45
47
|
/** An answer of the plugin's own, in markdown; the model is not asked. */
|
|
46
48
|
reply?: string;
|
|
49
|
+
/** A design system the plugin made: attached to the conversation from then on. */
|
|
50
|
+
design?: string;
|
|
47
51
|
}
|
|
48
52
|
export interface Plugin {
|
|
49
53
|
id: string;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type Target } from '../api/call.ts';
|
|
2
|
+
import type { Plugin, Prep, Turn } from '../plugin.ts';
|
|
3
|
+
/** Hanzo's model that decodes under a strict schema. */
|
|
4
|
+
export declare const DRAFTER = "zen5-flash";
|
|
5
|
+
/** The site a brief points at, if it names one. */
|
|
6
|
+
export declare const siteOf: (text: string) => string;
|
|
7
|
+
export declare function make(turn: Turn, t: Target, signal: AbortSignal): Promise<Prep>;
|
|
8
|
+
export declare const designer: Plugin;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Design system: while it is on, a message is a brief for one — "make me a
|
|
4
|
+
* design system for X", or a site to take it from — and the answer is the
|
|
5
|
+
* system: drafted by a Hanzo model under a strict schema (palette, type,
|
|
6
|
+
* spacing, radii, components, voice), kept as a Studio brand (the one store for
|
|
7
|
+
* them), drawn as a page in the side panel, and attached to the conversation, so
|
|
8
|
+
* everything made after it follows it. From `+` › Design system it attaches to
|
|
9
|
+
* any later conversation, in Chat or in Dev.
|
|
10
|
+
*/
|
|
11
|
+
import { Palette } from '@hanzogui/lucide-icons-2';
|
|
12
|
+
import { call } from '../api/call.js';
|
|
13
|
+
import { keepBrand } from '../api/studio.js';
|
|
14
|
+
import { drafted, preview, SCHEMA, tokensOf } from '../design.js';
|
|
15
|
+
/** Hanzo's model that decodes under a strict schema. */
|
|
16
|
+
export const DRAFTER = 'zen5-flash';
|
|
17
|
+
/** The site a brief points at, if it names one. */
|
|
18
|
+
export const siteOf = (text) => text.match(/https?:\/\/[^\s)]+/)?.[0] ?? '';
|
|
19
|
+
const BRIEF = 'You design design systems. From the brief (and the site text, when given), answer one complete, coherent system: ' +
|
|
20
|
+
'a name; eight hex colours with enough contrast for text on background and on primary; heading, body and mono fonts (Google Fonts names where you can); ' +
|
|
21
|
+
'a type scale of 6-8 px sizes; a spacing scale of 6-10 px steps; sm, md and lg radii; one line each for button, card, input, nav and hero; ' +
|
|
22
|
+
'and a voice: its tone in a sentence, 3-5 do and 3-5 dont lines. Take colours, fonts and tone from the site when one is given.';
|
|
23
|
+
async function site(t, url, signal) {
|
|
24
|
+
if (!url)
|
|
25
|
+
return '';
|
|
26
|
+
const d = await call(t, 'POST', '/v1/crawl', { url }, { signal }).catch(() => null);
|
|
27
|
+
return d?.success ? (d.data?.markdown ?? '').slice(0, 6000) : '';
|
|
28
|
+
}
|
|
29
|
+
export async function make(turn, t, signal) {
|
|
30
|
+
const brief = turn.text.trim();
|
|
31
|
+
if (!brief)
|
|
32
|
+
return {};
|
|
33
|
+
const page = await site(t, siteOf(brief), signal);
|
|
34
|
+
const raw = await call(t, 'POST', '/v1/chat/completions', {
|
|
35
|
+
model: DRAFTER,
|
|
36
|
+
max_tokens: 2000,
|
|
37
|
+
messages: [
|
|
38
|
+
{ role: 'system', content: BRIEF },
|
|
39
|
+
{ role: 'user', content: page ? `${brief}\n\nThe site, as text:\n${page}` : brief },
|
|
40
|
+
],
|
|
41
|
+
response_format: { type: 'json_schema', json_schema: { name: 'design_system', strict: true, schema: SCHEMA } },
|
|
42
|
+
}, { signal });
|
|
43
|
+
const text = raw?.choices?.[0]?.message?.content;
|
|
44
|
+
let parsed = null;
|
|
45
|
+
try {
|
|
46
|
+
parsed = JSON.parse(typeof text === 'string' ? text : '');
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
throw new Error('The model did not answer a design system. Try the brief again.');
|
|
50
|
+
}
|
|
51
|
+
const d = drafted(parsed);
|
|
52
|
+
if (typeof d === 'string')
|
|
53
|
+
throw new Error(d);
|
|
54
|
+
const b = await keepBrand(t, { name: d.name, tokens: tokensOf(d) }, signal);
|
|
55
|
+
const file = `${d.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'design-system'}.html`;
|
|
56
|
+
const reply = [
|
|
57
|
+
`**${d.name}** is saved and attached to this conversation: what you build here follows it, and \`+\` › Design system attaches it anywhere else.`,
|
|
58
|
+
`Colours ${Object.values(d.colors).join(' ')} · ${d.fonts.heading} / ${d.fonts.body} · radius ${Object.values(d.radius).join('/')}px`,
|
|
59
|
+
d.voice.tone ? `Voice: ${d.voice.tone}` : '',
|
|
60
|
+
'```html ' + file + '\n' + preview(b) + '\n```',
|
|
61
|
+
];
|
|
62
|
+
return { reply: reply.filter(Boolean).join('\n\n'), design: b.id };
|
|
63
|
+
}
|
|
64
|
+
export const designer = {
|
|
65
|
+
id: 'design',
|
|
66
|
+
name: 'Design system',
|
|
67
|
+
note: 'Make one from a brief or a site',
|
|
68
|
+
icon: _jsx(Palette, { size: 16 }),
|
|
69
|
+
prepare: (turn, c, signal) => make(turn, c.target, signal),
|
|
70
|
+
};
|
package/lib/plugins/index.d.ts
CHANGED
|
@@ -24,4 +24,4 @@ export declare function merge(preps: readonly Prep[]): Prep;
|
|
|
24
24
|
* fails the message, with its name and its reason: a report asked for and not
|
|
25
25
|
* researched must not arrive looking researched.
|
|
26
26
|
*/
|
|
27
|
-
export declare function prep(plugins: readonly Plugin[], g: Gear, turn: Turn, c: Composer, signal: AbortSignal): Promise<Prep>;
|
|
27
|
+
export declare function prep(plugins: readonly Plugin[], g: Gear, turn: Turn, c: Composer, signal: AbortSignal, denied?: readonly string[]): Promise<Prep>;
|
package/lib/plugins/index.js
CHANGED
|
@@ -23,6 +23,7 @@ import { say } from '../customize/say.js';
|
|
|
23
23
|
import { path } from '../route.js';
|
|
24
24
|
import { pdf } from './pdf.js';
|
|
25
25
|
import { project } from './project.js';
|
|
26
|
+
import { designer } from './design.js';
|
|
26
27
|
import { deep } from './research.js';
|
|
27
28
|
import { sketch } from './sketch.js';
|
|
28
29
|
import { slides } from './slides.js';
|
|
@@ -46,6 +47,7 @@ export async function probe(t) {
|
|
|
46
47
|
sketch,
|
|
47
48
|
administers(t.token(), t.org) ? template : null,
|
|
48
49
|
runs.includes('slides') ? slides : null,
|
|
50
|
+
studio.status === 'fulfilled' ? designer : null,
|
|
49
51
|
pdf(runs.includes('document')),
|
|
50
52
|
].filter((p) => p !== null);
|
|
51
53
|
}
|
|
@@ -114,8 +116,10 @@ export function merge(preps) {
|
|
|
114
116
|
* fails the message, with its name and its reason: a report asked for and not
|
|
115
117
|
* researched must not arrive looking researched.
|
|
116
118
|
*/
|
|
117
|
-
export async function prep(plugins, g, turn, c, signal) {
|
|
118
|
-
const preps = await Promise.all(active(plugins, g)
|
|
119
|
+
export async function prep(plugins, g, turn, c, signal, denied = []) {
|
|
120
|
+
const preps = await Promise.all(active(plugins, g)
|
|
121
|
+
.filter((p) => !denied.includes(p.id))
|
|
122
|
+
.map((p) => p.prepare(turn, c, signal).catch((e) => {
|
|
119
123
|
if (signal.aborted)
|
|
120
124
|
throw e;
|
|
121
125
|
throw new Error(`${p.name}: ${say(e)}`);
|
package/lib/plugins/slides.js
CHANGED
|
@@ -40,7 +40,7 @@ export async function deck(turn, t, signal) {
|
|
|
40
40
|
if (!ask)
|
|
41
41
|
return {};
|
|
42
42
|
const last = turn.thread ? decks.get(turn.thread) : undefined;
|
|
43
|
-
const r = await make(t, { template: 'slides', title: ask.slice(0, 80), input: { brief: last ? revision(last, ask) : ask, format: 'pptx' } }, signal);
|
|
43
|
+
const r = await make(t, { template: 'slides', title: ask.slice(0, 80), input: { brief: last ? revision(last, ask) : ask, format: 'pptx' }, ...(turn.design ? { brand: turn.design } : {}) }, signal);
|
|
44
44
|
if (turn.thread)
|
|
45
45
|
decks.set(turn.thread, r);
|
|
46
46
|
return { reply: told(r) };
|
package/lib/project.js
CHANGED
|
@@ -42,7 +42,7 @@ import { SizableText, useMedia, XStack, YStack } from '@hanzo/gui';
|
|
|
42
42
|
import { Clock, ExternalLink, MousePointerClick, PanelLeft, RefreshCcw, Share2, Upload, } from '@hanzogui/lucide-icons-2';
|
|
43
43
|
import { Attachments, CHAT, Console, DEVICES, Feedback, FileTabs, FileTree, PageSelect, PreviewFrame, ProjectChip, VIEWS, Views, Workspace, fold, } from '@hanzo/ui/agents';
|
|
44
44
|
import { Message } from '@hanzo/ui/chat';
|
|
45
|
-
import { Button, Dialog, DialogContent, DialogTitle
|
|
45
|
+
import { Button, Dialog, DialogContent, DialogTitle } from '@hanzo/ui';
|
|
46
46
|
import { HanzoMark } from '@hanzo/ui/product';
|
|
47
47
|
import { useDictation, useVoice } from '@hanzo/voice';
|
|
48
48
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
@@ -57,6 +57,7 @@ import { decode, outcome, pull, said, scrub, who } from './api/turn.js';
|
|
|
57
57
|
import { verdict as record } from './api/verdict.js';
|
|
58
58
|
import { useKept, useProjects, useRead, useRun } from './data.js';
|
|
59
59
|
import { Merge } from './git.js';
|
|
60
|
+
import { Gutter } from './gutter.js';
|
|
60
61
|
import { useHost, useTarget } from './host.js';
|
|
61
62
|
import { useMind } from './mind.js';
|
|
62
63
|
import { Out } from './out.js';
|
|
@@ -416,7 +417,7 @@ export function Project({ at }) {
|
|
|
416
417
|
? `${named.org}/${named.name} is open on ${home}. Say what to change and it gets built there.`
|
|
417
418
|
: `${slug} is loaded. Say what to change and it gets built.` })) : (ordered.map((r) => (_jsxs(YStack, { gap: "$2", children: [_jsx(Turn, { run: r, open: r.id === current, onOpen: () => setChosen(r.id), project: slug }), r.id === current ? (_jsxs(YStack, { gap: "$1", pl: "$2", borderLeftWidth: 1, borderColor: "$borderColor", children: [fold(run.events.map((e) => ({ kind: e.kind, actor: who(e.actor), seq: e.seq, id: e.id, text: said(e, run.record?.mode) })).filter((b) => b.text)).map((b) => (_jsx(SizableText, { size: "$1", color: "$soft", children: b.text }, b.key))), pr.href ? (_jsxs(XStack, { items: "center", gap: "$2", flexWrap: "wrap", children: [_jsxs(Out, { href: pr.href, label: `Open pull request ${pr.label}`, children: [_jsxs(SizableText, { size: "$1", color: "$ink", textDecorationLine: "underline", children: ["Pull request ", pr.label] }), _jsx(ExternalLink, { size: 11 })] }), _jsx(Merge, { session: r.id, open: proposing, onMerged: merged })] })) : null] })) : null] }, r.id)))) }), _jsxs(YStack, { "data-slot": "project-ask", px: "$3", pb: "$3", gap: "$2", minW: 0, $md: { pr: 0 }, children: [note ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: note })) : null, picked.length ? _jsx(Attachments, { items: picked, onRemove: () => setPicked([]) }) : null, attached.length ? (_jsx(XStack, { gap: 6, flexWrap: "wrap", children: _jsx(Files, { files: attached, onFiles: setAttached }) })) : null, _jsx(Prompt, { framed: true, value: draft, onChange: setDraft, onSend: () => void (current && running ? steer(current) : send()), onStop: current
|
|
418
419
|
? () => void stop(t, current).then(() => setNote('Stop requested — the run keeps its work on its branch'), (e) => setNote(e.message))
|
|
419
|
-
: undefined, busy: busy || (running && !draft.trim()), placeholder: running ? 'Steer this run' : 'Ask Hanzo for edits', label: "Ask Hanzo for edits", plus: { upload: () => pick(attached, setAttached, setNote) }, mode: running ? null : { value: mode === 'plan' ? 'plan' : 'auto', set: (m) => setMode(m === 'plan' ? 'plan' : 'build') }, mind: running ? null : mind, voice: voice })] }), wide && !shut ? (_jsx(
|
|
420
|
+
: undefined, busy: busy || (running && !draft.trim()), placeholder: running ? 'Steer this run' : 'Ask Hanzo for edits', label: "Ask Hanzo for edits", plus: { upload: () => pick(attached, setAttached, setNote) }, mode: running ? null : { value: mode === 'plan' ? 'plan' : 'auto', set: (m) => setMode(m === 'plan' ? 'plan' : 'build'), items: ['auto', 'plan'] }, mind: running ? null : mind, voice: voice })] }), wide && !shut ? (_jsx(Gutter, { side: "left", span: across, floor: CHAT_FLOOR, ceil: most, reset: CHAT_WIDTH, onSpan: setSplit, label: "Resize the chat", gap: GUTTER })) : null] }));
|
|
420
421
|
// The preview stays loaded while another view is open, so coming back to it
|
|
421
422
|
// is the page as it was left.
|
|
422
423
|
const preview = (_jsxs(YStack, { "data-slot": "preview", flex: 1, minH: 0, minW: 0, gap: "$2", display: view === 'preview' ? 'flex' : 'none', children: [_jsxs(XStack, { "data-slot": "preview-bar", items: "center", gap: "$1.5", minW: 0, shrink: 0, "$max-md": { px: '$2' }, children: [_jsx(XStack, { render: "button", "aria-label": "Reload the preview", onPress: () => frame.current?.reload(), p: "$1.5", rounded: "$3", hoverStyle: { bg: '$hover' }, children: _jsx(RefreshCcw, { size: 16 }) }), _jsx(PageSelect, { pages: pages, value: page, onChange: setPage, shrink: 1 }), _jsx(XStack, { flex: 1 }), _jsx(Views, { views: DEVICES, value: device, onChange: (d) => setDevice(d), label: "Device", labels: "none" }), src ? (_jsx(Out, { href: src, label: "Open in a new tab", children: _jsx(XStack, { p: "$1.5", rounded: "$3", hoverStyle: { bg: '$hover' }, children: _jsx(ExternalLink, { size: 16 }) }) })) : null] }), _jsx(PreviewFrame, { ref: frame, src: src, title: `${title} preview`, device: device, onBridge: onBridge, empty: _jsxs(YStack, { items: "center", gap: "$2", p: "$6", children: [_jsx(SizableText, { size: "$3", color: "$ink", children: stalled
|
package/lib/prompt.d.ts
CHANGED
package/lib/prompt.js
CHANGED
|
@@ -113,7 +113,7 @@ export function Tune({ mind, mode, where, onUpgrade }) {
|
|
|
113
113
|
return (_jsxs(XStack, { "data-slot": "tune", width: "100%", items: "center", gap: 2, flexWrap: "wrap", rowGap: 4, children: [where ? (_jsx(XStack, { items: "center", gap: 2, flexWrap: "wrap", minW: 0, "$max-sm": { width: '100%' }, children: where })) : null, _jsx(XStack, { flex: 1, "$max-sm": { display: 'none' } }), _jsxs(XStack, { items: "center", gap: 2, ml: "auto", shrink: 0, children: [mind ? _jsx(Model, { mind: mind, onUpgrade: onUpgrade }) : null, mind && thinks && pace ? (_jsx(Opens, { label: `Effort: ${pace.label}`, word: pace.label, dim: true, children: (close) => (_jsx(Choices, { title: "Effort", items: EFFORTS, value: mind.effort, onPick: (e) => {
|
|
114
114
|
mind.pace(e);
|
|
115
115
|
close();
|
|
116
|
-
} })) })) : null, mode ? (_jsxs(Popover, { allowFlip: true, placement: "top-end", children: [_jsx(PopoverTrigger, { asChild: true, children: _jsx(XStack, { render: "button", "data-slot": "mode", "aria-label": `Mode: ${MODES.find((m) => m.id === mode.value)?.label}`, items: "center", height: 24, px: 8, ml: 4, rounded: 6, bg: "$hover", cursor: "pointer", hoverStyle: { bg: '$raised' }, focusVisibleStyle: { outlineColor: '$outlineColor', outlineWidth: 2, outlineStyle: 'solid' }, "$max-sm": { height: 44, px: 12 }, children: _jsx(SizableText, { fontSize: 14, color: "$ink", children: MODES.find((m) => m.id === mode.value)?.label }) }) }), _jsx(PopoverContent, { solid: true, p: 6, width: 288, maxW: "calc(100vw - 24px)", children: _jsx(Choices, { title: "Mode", items: MODES, value: mode.value, onPick: mode.set }) })] })) : null] })] }));
|
|
116
|
+
} })) })) : null, mode ? (_jsxs(Popover, { allowFlip: true, placement: "top-end", children: [_jsx(PopoverTrigger, { asChild: true, children: _jsx(XStack, { render: "button", "data-slot": "mode", "aria-label": `Mode: ${MODES.find((m) => m.id === mode.value)?.label}`, items: "center", height: 24, px: 8, ml: 4, rounded: 6, bg: "$hover", cursor: "pointer", hoverStyle: { bg: '$raised' }, focusVisibleStyle: { outlineColor: '$outlineColor', outlineWidth: 2, outlineStyle: 'solid' }, "$max-sm": { height: 44, px: 12 }, children: _jsx(SizableText, { fontSize: 14, color: "$ink", children: MODES.find((m) => m.id === mode.value)?.label }) }) }), _jsx(PopoverContent, { solid: true, p: 6, width: 288, maxW: "calc(100vw - 24px)", children: _jsx(Choices, { title: "Mode", items: mode.items ? MODES.filter((m) => mode.items.includes(m.id)) : MODES, value: mode.value, onPick: mode.set }) })] })) : null] })] }));
|
|
117
117
|
}
|
|
118
118
|
/** The `/` or `@` the caret ends on, and the words typed after it. */
|
|
119
119
|
export function trigger(value) {
|
package/lib/run.js
CHANGED
|
@@ -25,7 +25,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
25
25
|
*/
|
|
26
26
|
import { SizableText, useMedia, XStack, YStack } from '@hanzo/gui';
|
|
27
27
|
import { ChevronDown, ChevronRight, Clock, Copy, ExternalLink, GitPullRequest, Globe, Pencil, Share } from '@hanzogui/lucide-icons-2';
|
|
28
|
-
import { Button, Dialog, DialogContent, DialogTitle, DropdownMenu,
|
|
28
|
+
import { Button, Dialog, DialogContent, DialogTitle, DropdownMenu, Input } from '@hanzo/ui';
|
|
29
29
|
import { Steer } from '@hanzo/ui/agents';
|
|
30
30
|
import { Failure } from '@hanzo/ui/chat';
|
|
31
31
|
import { useLimits } from '@hanzo/ui/product/useLimits';
|
|
@@ -41,6 +41,7 @@ import { Desk, SEED } from './desk.js';
|
|
|
41
41
|
import { useHost, useTarget } from './host.js';
|
|
42
42
|
import { useMind } from './mind.js';
|
|
43
43
|
import { Out } from './out.js';
|
|
44
|
+
import { Gutter } from './gutter.js';
|
|
44
45
|
import { gap, pane } from './pane.js';
|
|
45
46
|
import { measure, Prompt, useEar } from './prompt.js';
|
|
46
47
|
import { useSpoken } from './voice.js';
|
|
@@ -372,15 +373,9 @@ export function Run({ id }) {
|
|
|
372
373
|
...(busy || waiting ? ['pause', 'resume', 'stop'] : []),
|
|
373
374
|
] }), _jsx(Button, { size: "sm", disabled: notify, onPress: () => void ask(), children: notify ? 'You will be notified' : 'Notify me' })] })) : null] }), _jsx(Prompt, { framed: true, value: draft, onChange: setDraft, onSend: send, ready: Boolean(draft.trim()), disabled: !signed || busy || waiting || !(running || finished), placeholder: placeholder, label: steering ? 'Steer this run' : 'Follow up on this run', head: files.length ? _jsx(XStack, { gap: "$1.5", flexWrap: "wrap", children: _jsx(Files, { files: files, onFiles: setFiles }) }) : undefined, plus: { upload: () => pick(files, setFiles, setNote) },
|
|
374
375
|
// What a new run from here is made with; words that steer this run use neither.
|
|
375
|
-
mind: signed && !steering ? mind : null, limits: limits, voice: voice
|
|
376
|
+
mind: signed && !steering ? mind : null, limits: limits, voice: voice })] })] }), desk && !wide ? desk_(true) : null, desk && wide ? (_jsxs(YStack, { "data-slot": "desk", ref: split.aside, ...pane,
|
|
376
377
|
// A pane of its own beside the run's, the gutter between them.
|
|
377
|
-
ml: gap, position: "relative", shrink: 0, minH: 0, style: split.asideStyle, children: [split.grip ? (_jsx(
|
|
378
|
-
// The handle halfway down the edge, where a hand looks for it.
|
|
379
|
-
justify: "center",
|
|
380
|
-
// In the gutter between the two panes; `left` is measured inside the pane's 1px edge.
|
|
381
|
-
l: `calc(-1 * ${gap} - ${pane.borderWidth}px)`, width: gap,
|
|
382
|
-
// A finger's band, centred on the gutter.
|
|
383
|
-
$touchable: { l: `calc(-22px - ${gap} / 2 - ${pane.borderWidth}px)`, width: 44 } })) : null, _jsx(SideProvider, { value: split.side, children: desk_(false) })] })) : null, _jsx(Dialog, { open: naming, onOpenChange: setNaming, children: _jsxs(DialogContent, { maxW: 480, children: [_jsx(DialogTitle, { children: "Rename this run" }), _jsx(Input, { autoFocus: true, value: name, onChangeText: setName, "aria-label": "The run\u2019s name", maxLength: 512, onKeyDown: (e) => {
|
|
378
|
+
ml: gap, position: "relative", shrink: 0, minH: 0, style: split.asideStyle, children: [split.grip ? (_jsx(Gutter, { ...split.grip, label: "Resize the side panel", inset: pane.borderWidth })) : null, _jsx(SideProvider, { value: split.side, children: desk_(false) })] })) : null, _jsx(Dialog, { open: naming, onOpenChange: setNaming, children: _jsxs(DialogContent, { maxW: 480, children: [_jsx(DialogTitle, { children: "Rename this run" }), _jsx(Input, { autoFocus: true, value: name, onChangeText: setName, "aria-label": "The run\u2019s name", maxLength: 512, onKeyDown: (e) => {
|
|
384
379
|
if (e.key === 'Enter')
|
|
385
380
|
save();
|
|
386
381
|
} }), _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "outline", onPress: () => setNaming(false), children: "Cancel" }), _jsx(Button, { size: "sm", disabled: busy || !name.trim(), onPress: save, children: "Save" })] })] }) })] }));
|
package/lib/suggest.d.ts
CHANGED
|
@@ -196,6 +196,10 @@ export declare function answer(raw: unknown, inputs: Input[]): Suggestion[];
|
|
|
196
196
|
export declare function kept(raw: unknown): Kept | undefined;
|
|
197
197
|
/** The rows to draw: those not dismissed. */
|
|
198
198
|
export declare const shown: (k: Kept | null | undefined) => Suggestion[];
|
|
199
|
+
/** What Hanzo does, three ways in: what fills the rows a person's activity and signup do not. */
|
|
200
|
+
export declare const STARTERS: readonly Suggestion[];
|
|
201
|
+
/** The kept rows, filled to `MOST` with the starters not dismissed: never one lonely row. */
|
|
202
|
+
export declare const fill: (k: Kept | null | undefined) => Suggestion[];
|
|
199
203
|
/** `k` with `id` dismissed: remembered first, the oldest forgotten past `REMEMBER`. */
|
|
200
204
|
export declare function dismissed(k: Kept, id: string): Kept;
|
|
201
205
|
/** The addresses and titles of dismissed rows the model is told to skip. */
|
package/lib/suggest.js
CHANGED
|
@@ -190,9 +190,9 @@ const SYSTEM = [
|
|
|
190
190
|
'- prompt: the message the person would send Hanzo to do the step, first person, complete on its own, naming the source and its address when it has one.',
|
|
191
191
|
'- href: copied exactly from the input the row is about, or "" when that input has none. Never write an address that is not in the input.',
|
|
192
192
|
'- icon: github for a github.com address, agent for a run or an automation, chat for a conversation, file for a file, task for any other issue or task.',
|
|
193
|
-
'
|
|
193
|
+
'Answer three rows. Take them from the activity first; fill the rest from what the person told us at signup (their role, their work): first things to ask Hanzo, with href "".',
|
|
194
194
|
'Never repeat an item listed under skip. Never suggest the same item twice.',
|
|
195
|
-
'
|
|
195
|
+
'With neither activity nor anything from signup worth a row, answer fewer; never pad with a weak one.',
|
|
196
196
|
].join('\n');
|
|
197
197
|
/** The chat completion that asks for rows, or null when there is nothing to make them from. */
|
|
198
198
|
export function request(inputs, seed, skip, now) {
|
|
@@ -309,6 +309,19 @@ export function kept(raw) {
|
|
|
309
309
|
}
|
|
310
310
|
/** The rows to draw: those not dismissed. */
|
|
311
311
|
export const shown = (k) => (k ? k.rows.filter((r) => !k.dismissed.includes(r.id)).slice(0, MOST) : []);
|
|
312
|
+
/** What Hanzo does, three ways in: what fills the rows a person's activity and signup do not. */
|
|
313
|
+
export const STARTERS = [
|
|
314
|
+
{ id: 'start:chat', icon: 'chat', title: 'Think something through', href: '', summary: '', action: 'Ask Hanzo', prompt: 'Help me think through ' },
|
|
315
|
+
{ id: 'start:dev', icon: 'agent', title: 'Build an app or a site', href: '', summary: '', action: 'Describe it', prompt: 'Build me ' },
|
|
316
|
+
{ id: 'start:research', icon: 'file', title: 'Research a topic', href: '', summary: '', action: 'Get a cited brief', prompt: 'Research this and give me a short brief with sources: ' },
|
|
317
|
+
];
|
|
318
|
+
/** The kept rows, filled to `MOST` with the starters not dismissed: never one lonely row. */
|
|
319
|
+
export const fill = (k) => {
|
|
320
|
+
if (!k)
|
|
321
|
+
return [];
|
|
322
|
+
const rows = shown(k);
|
|
323
|
+
return [...rows, ...STARTERS.filter((r) => !k.dismissed.includes(r.id))].slice(0, Math.max(rows.length, MOST));
|
|
324
|
+
};
|
|
312
325
|
/** `k` with `id` dismissed: remembered first, the oldest forgotten past `REMEMBER`. */
|
|
313
326
|
export function dismissed(k, id) {
|
|
314
327
|
return { ...k, dismissed: [id, ...k.dismissed.filter((x) => x !== id)].slice(0, REMEMBER) };
|
package/lib/suggestions.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type Target } from './api/call.ts';
|
|
2
2
|
import { type Suggestion } from './suggest.ts';
|
|
3
3
|
export interface Suggested {
|
|
4
|
-
/** The rows to draw:
|
|
4
|
+
/** The rows to draw: three when any are, none dismissed. */
|
|
5
5
|
rows: Suggestion[];
|
|
6
6
|
/** Dismiss one, remembered by the account. */
|
|
7
7
|
dismiss: (id: string) => void;
|
package/lib/suggestions.js
CHANGED
|
@@ -33,7 +33,7 @@ import { list } from './api/sessions.js';
|
|
|
33
33
|
import { everything } from './api/work.js';
|
|
34
34
|
import { claims } from './claims.js';
|
|
35
35
|
import { Out } from './out.js';
|
|
36
|
-
import { answer, dismissed, due, inputs, request, seed,
|
|
36
|
+
import { answer, dismissed, due, fill, inputs, request, seed, sig, skip, stale, TAKE } from './suggest.js';
|
|
37
37
|
/** A source that refuses this person (a 4xx other than an expired sign-in or a rate) has nothing for them; any other failure leaves it unknown. */
|
|
38
38
|
function soft(p, none) {
|
|
39
39
|
return p.catch((e) => {
|
|
@@ -120,7 +120,8 @@ export function useSuggestions(t) {
|
|
|
120
120
|
load(t).then(() => setError(null), (e) => setError(said(e)));
|
|
121
121
|
}, [t]);
|
|
122
122
|
const now = useSyncExternalStore(subscribe, () => doc, () => doc);
|
|
123
|
-
|
|
123
|
+
// Filled to three with what Hanzo does, so a new account and a quiet one never see one lonely row.
|
|
124
|
+
const rows = useMemo(() => fill(now), [now]);
|
|
124
125
|
const dismiss = useCallback((id) => {
|
|
125
126
|
if (!doc)
|
|
126
127
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanzo/build",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.70",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0 OR MIT",
|
|
6
6
|
"description": "Hanzo Build — say what you want, watch it built, deployed and running. The builder as a component, mounted by the Hanzo app at hanzo.ai.",
|