@hanzo/build 0.2.62 → 0.2.64

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.
@@ -0,0 +1,54 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * Presentations: while it is on, a message makes a deck in Hanzo Studio (the
4
+ * `slides` template, a PowerPoint file) and the answer is the deck: its outline,
5
+ * the file, and the run in Studio, where it is edited. A later message in the
6
+ * same conversation revises the last deck rather than starting over.
7
+ */
8
+ import { Presentation } from '@hanzogui/lucide-icons-2';
9
+ import { json } from '../api/chat.js';
10
+ import { make } from '../api/studio.js';
11
+ /** The last deck each conversation made: what a later message revises. */
12
+ const decks = new Map();
13
+ /** What a deck's writing step said: its title and its slides' titles. */
14
+ export function outline(text) {
15
+ const o = json(text) ?? {};
16
+ const slides = (Array.isArray(o.slides) ? o.slides : [])
17
+ .map((s) => (s && typeof s === 'object' ? s.title : ''))
18
+ .map((t) => (typeof t === 'string' ? t.trim() : ''))
19
+ .filter(Boolean);
20
+ return { title: typeof o.title === 'string' ? o.title.trim() : '', slides };
21
+ }
22
+ /** The brief a revision is made from: the deck as it stands, then the change asked for. */
23
+ export function revision(last, ask) {
24
+ return `Revise this deck. Keep what the change does not touch.\n\nThe deck as it stands:\n${last.text.trim()}\n\nThe change:\n${ask}`;
25
+ }
26
+ /** The answer a made deck is: its outline, the file, and where it is edited. */
27
+ export function told(r) {
28
+ const { title, slides } = outline(r.text);
29
+ const file = r.outputs[0];
30
+ const head = `**${title || r.title || 'Your deck'}**${slides.length ? ` — ${slides.length} slides` : ''}`;
31
+ const links = [
32
+ file?.url ? `[Download the deck](${file.url}) (PowerPoint; the link works for an hour)` : '',
33
+ file?.origin ? `[Open it in Studio](${file.origin}) to edit it` : '',
34
+ ].filter(Boolean);
35
+ const list = slides.map((s, i) => `${i + 1}. ${s}`).join('\n');
36
+ return [head, links.join(' · '), list, 'It is kept in your Library.'].filter(Boolean).join('\n\n');
37
+ }
38
+ export async function deck(turn, t, signal) {
39
+ const ask = turn.text.trim();
40
+ if (!ask)
41
+ return {};
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);
44
+ if (turn.thread)
45
+ decks.set(turn.thread, r);
46
+ return { reply: told(r) };
47
+ }
48
+ export const slides = {
49
+ id: 'slides',
50
+ name: 'Presentations',
51
+ note: 'Create and edit decks',
52
+ icon: _jsx(Presentation, { size: 16 }),
53
+ prepare: (turn, c, signal) => deck(turn, c.target, signal),
54
+ };
@@ -0,0 +1,13 @@
1
+ import type { Plugin } from '../plugin.ts';
2
+ export interface Draft {
3
+ name: string;
4
+ description: string;
5
+ body: string;
6
+ }
7
+ /** A skill's name from any words: one lowercase segment of letters, digits and hyphens. */
8
+ export declare function slug(text: string): string;
9
+ /** What a model's draft says, each part the ask's own where the draft has none. */
10
+ export declare function drafted(said: string, draft: string): Draft;
11
+ /** The SKILL.md a template is saved as: its name and description up front, then the template. */
12
+ export declare function document(d: Draft): string;
13
+ export declare const template: Plugin;
@@ -0,0 +1,92 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Template Creator: the ask in the composer, saved as a reusable template — one
4
+ * of the org's own skills (POST /v1/tool/skills), switched on as it is saved so
5
+ * it is under `+` › Skills from then on. A Hanzo model drafts the template from
6
+ * the ask (its inputs as {{placeholders}}); the person reads and edits it before
7
+ * it is saved. Writing a skill takes an org admin, so only an admin is offered it.
8
+ */
9
+ import { SizableText, XStack, YStack } from '@hanzo/gui';
10
+ import { LayoutTemplate } from '@hanzogui/lucide-icons-2';
11
+ import { Button, Dialog, DialogContent, DialogDescription, DialogTitle, Input, Textarea } from '@hanzo/ui';
12
+ import { useEffect, useState } from 'react';
13
+ import { ask, json } from '../api/chat.js';
14
+ import { NAME, refuse, tool, write } from '../api/skills.js';
15
+ import { toggle } from '../api/tools.js';
16
+ import { say } from '../customize/say.js';
17
+ const DRAFT = `Turn the person's request into a reusable template, saved as a skill an agent follows. Answer only JSON: {"name": "a short lowercase-hyphenated name", "description": "one line: when to use it", "body": "markdown, under 250 words: what it is for, the inputs it needs written as {{placeholders}}, then the steps"}.`;
18
+ /** A skill's name from any words: one lowercase segment of letters, digits and hyphens. */
19
+ export function slug(text) {
20
+ const s = text
21
+ .toLowerCase()
22
+ .normalize('NFKD')
23
+ .replace(/[̀-ͯ]/g, '')
24
+ .replace(/[^a-z0-9]+/g, '-')
25
+ .replace(/^-+|-+$/g, '')
26
+ .slice(0, 48)
27
+ .replace(/-+$/, '');
28
+ return NAME.test(s) ? s : 'template';
29
+ }
30
+ /** The first line of a text, cut to a line's length. */
31
+ const first = (text) => (text.trim().split('\n')[0] ?? '').slice(0, 160).trim();
32
+ /** What a model's draft says, each part the ask's own where the draft has none. */
33
+ export function drafted(said, draft) {
34
+ const o = json(said) ?? {};
35
+ const str = (v) => (typeof v === 'string' ? v.trim() : '');
36
+ return {
37
+ name: slug(str(o.name) || draft.split(/\s+/).slice(0, 6).join(' ')),
38
+ description: first(str(o.description) || draft),
39
+ body: str(o.body) || draft.trim(),
40
+ };
41
+ }
42
+ /** The SKILL.md a template is saved as: its name and description up front, then the template. */
43
+ export function document(d) {
44
+ return `---\nname: ${d.name}\ndescription: ${JSON.stringify(d.description.replace(/\s+/g, ' ').trim())}\n---\n\n${d.body.trim()}\n`;
45
+ }
46
+ function Creator({ c }) {
47
+ const from = c.draft.trim();
48
+ const [d, setD] = useState({ name: from ? slug(from.split(/\s+/).slice(0, 6).join(' ')) : '', description: first(from), body: from });
49
+ const [drafting, setDrafting] = useState(Boolean(from));
50
+ const [busy, setBusy] = useState(false);
51
+ const [note, setNote] = useState('');
52
+ const [saved, setSaved] = useState('');
53
+ useEffect(() => {
54
+ if (!from)
55
+ return;
56
+ const stop = new AbortController();
57
+ ask(c.target, DRAFT, from, stop.signal, 2000)
58
+ .then((said) => setD(drafted(said, from)))
59
+ .catch(() => undefined)
60
+ .finally(() => setDrafting(false));
61
+ return () => stop.abort();
62
+ }, [c.target, from]);
63
+ const save = async () => {
64
+ const content = document(d);
65
+ const why = refuse({ name: d.name, content });
66
+ if (why) {
67
+ setNote(why);
68
+ return;
69
+ }
70
+ setBusy(true);
71
+ setNote('');
72
+ try {
73
+ const s = await write(c.target, { name: d.name, description: d.description, content });
74
+ await toggle(c.target, [tool(s.name)]);
75
+ setSaved(s.name);
76
+ }
77
+ catch (e) {
78
+ setNote(say(e));
79
+ }
80
+ finally {
81
+ setBusy(false);
82
+ }
83
+ };
84
+ return (_jsx(Dialog, { open: true, onOpenChange: (o) => !o && c.show(null), children: _jsxs(DialogContent, { maxW: 640, width: "100%", children: [_jsx(DialogTitle, { children: "Template Creator" }), _jsx(DialogDescription, { children: saved ? `Saved as ${saved}. It is under + › Skills.` : drafting ? 'Hanzo is drafting the template from your message…' : 'A reusable template, saved as one of your skills.' }), saved ? null : (_jsxs(YStack, { gap: "$3", children: [_jsx(Input, { value: d.name, onChangeText: (name) => setD({ ...d, name }), placeholder: "weekly-update", "aria-label": "Name", autoCapitalize: "none" }), _jsx(Input, { value: d.description, onChangeText: (description) => setD({ ...d, description }), placeholder: "When to use it", "aria-label": "Description" }), _jsx(Textarea, { value: d.body, onChangeText: (body) => setD({ ...d, body }), placeholder: '# Weekly update\n\nInputs: {{team}}, {{week}}\n\n1. …', "aria-label": "Template", rows: 12, style: { maxHeight: '40vh', overflowY: 'auto' } })] })), note ? (_jsx(SizableText, { size: "$2", color: "$soft", role: "status", children: note })) : null, _jsx(XStack, { gap: "$2", justify: "flex-end", children: saved ? (_jsx(Button, { size: "sm", onPress: () => c.show(null), children: "Done" })) : (_jsxs(_Fragment, { children: [_jsx(Button, { size: "sm", variant: "ghost", onPress: () => c.show(null), children: "Cancel" }), _jsx(Button, { size: "sm", disabled: busy || drafting || !d.name || !d.body.trim(), onPress: () => void save(), children: "Save" })] })) })] }) }));
85
+ }
86
+ export const template = {
87
+ id: 'template',
88
+ name: 'Template Creator',
89
+ note: 'Templates saved as skills',
90
+ icon: _jsx(LayoutTemplate, { size: 16 }),
91
+ open: (c) => c.show(_jsx(Creator, { c: c })),
92
+ };
package/lib/prompt.d.ts CHANGED
@@ -16,7 +16,7 @@ export declare const measure: {
16
16
  readonly maxW: "calc(var(--container-prose, 48rem) - 1.5rem)";
17
17
  readonly mx: 'auto';
18
18
  };
19
- /** The round controls' box, px. */
19
+ /** The round controls' box, px, and a phone's, a finger's width. */
20
20
  export declare const ROUND = 32;
21
21
  /** What every Composer placeholder says, unless a surface names who is listening. */
22
22
  export declare const ASK = "Type / for skills";
package/lib/prompt.js CHANGED
@@ -37,8 +37,9 @@ export const MEASURE = 'var(--container-prose, 48rem)';
37
37
  export const GAP = 20;
38
38
  /** The composer's width: the measure less the thread column's 0.75rem gutters, centred, so its edges are the text's. */
39
39
  export const measure = { width: 'calc(100% - 1.5rem)', maxW: `calc(${MEASURE} - 1.5rem)`, mx: 'auto' };
40
- /** The round controls' box, px. */
40
+ /** The round controls' box, px, and a phone's, a finger's width. */
41
41
  export const ROUND = 32;
42
+ const TAP = 44;
42
43
  /** The frame's corner, px: 12 on a laptop, 22 on a phone. */
43
44
  const CORNER = 12;
44
45
  const CORNER_PHONE = 22;
@@ -46,6 +47,8 @@ const CORNER_PHONE = 22;
46
47
  const ON = 'var(--primary-foreground)';
47
48
  /** A round control with no ground of its own until it is pointed at. */
48
49
  const QUIET = { bg: 'transparent', color: 'var(--foreground)', hoverStyle: { bg: '$hover' } };
50
+ /** The model rows the menu draws before anything is typed. */
51
+ const SHOWN = 40;
49
52
  /** What every Composer placeholder says, unless a surface names who is listening. */
50
53
  export const ASK = 'Type / for skills';
51
54
  /**
@@ -81,15 +84,27 @@ function Opens({ label, word, dim, children }) {
81
84
  function Model({ mind, onUpgrade }) {
82
85
  const [open, setOpen] = useState(false);
83
86
  const [q, setQ] = useState('');
84
- const groups = useMemo(() => {
87
+ // The catalog is hundreds long, and drawing all of it is what made the menu slow
88
+ // to open: until something is typed, the first `SHOWN` rows, Hanzo's families
89
+ // first, and a line saying the search reaches the rest.
90
+ const { groups, more } = useMemo(() => {
85
91
  const list = mind.models.filter((m) => !ROUTER_IDS.has(m.id) && (m.outputs?.includes('text') ?? true) && (!q.trim() || matchesModel(m, q)));
86
- return groupModels(list);
92
+ const all = groupModels(list);
93
+ let room = q.trim() ? Infinity : SHOWN;
94
+ const cut = all
95
+ .map((g) => {
96
+ const models = g.models.slice(0, Math.max(0, room));
97
+ room -= models.length;
98
+ return { ...g, models };
99
+ })
100
+ .filter((g) => g.models.length);
101
+ return { groups: cut, more: list.length - cut.reduce((n, g) => n + g.models.length, 0) };
87
102
  }, [mind.models, q]);
88
103
  const routed = mind.model === ENSO;
89
- return (_jsxs(DropdownMenu, { open: open, onOpenChange: setOpen, placement: "top-end", children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(XStack, { "data-slot": "mind", children: _jsx(Word, { label: `Model: ${nameOf(mind.model, mind.models)}`, open: open, children: nameOf(mind.model, mind.models) }) }) }), _jsxs(DropdownMenuContent, { align: "end", minW: 280, maxH: 420, "data-slot": "model-menu", children: [_jsx(DropdownMenuSearch, { value: q, onValueChange: setQ, placeholder: "Search models" }), !mind.free && !q.trim() ? (_jsx(DropdownMenuItem, { onSelect: () => mind.pick(ENSO), children: _jsxs(XStack, { flex: 1, items: "center", gap: 8, children: [_jsxs(YStack, { flex: 1, children: [_jsx(SizableText, { fontSize: 14, color: "$ink", children: "Enso" }), _jsx(SizableText, { fontSize: 12, color: "$hint", children: "Picks the model for each message and step" })] }), routed ? _jsx(Check, { size: 14, color: "var(--primary)", "aria-hidden": true }) : null] }) })) : null, groups.map((g) => (_jsxs(YStack, { role: "group", "aria-label": g.label, children: [_jsx(SizableText, { fontSize: 12, color: "$hint", px: 8, pt: 8, pb: 2, children: g.label }), g.models.map((m) => {
90
- const ok = grants(mind.free, m.id, mind.models);
91
- return (_jsx(DropdownMenuItem, { "data-slot": "model-row", onSelect: () => (ok ? mind.pick(m.id) : onUpgrade?.(m.id)), children: _jsxs(XStack, { flex: 1, items: "center", gap: 8, children: [_jsx(SizableText, { fontSize: 14, color: ok ? '$ink' : '$hint', numberOfLines: 1, flex: 1, children: nameOf(m.id, mind.models) }), ok ? null : (_jsx(SizableText, { fontSize: 12, color: "$hint", px: 6, py: 1, rounded: 6, borderWidth: 1, borderColor: "$borderColor", children: "Upgrade" })), m.id === mind.model ? _jsx(Check, { size: 14, color: "var(--primary)", "aria-hidden": true }) : null] }) }, m.id));
92
- })] }, g.key)))] })] }));
104
+ return (_jsxs(DropdownMenu, { open: open, onOpenChange: setOpen, placement: "top-end", children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(XStack, { "data-slot": "mind", children: _jsx(Word, { label: `Model: ${nameOf(mind.model, mind.models)}`, open: open, children: nameOf(mind.model, mind.models) }) }) }), _jsxs(DropdownMenuContent, { align: "end", minW: 280, "data-slot": "model-menu", children: [_jsx(DropdownMenuSearch, { value: q, onValueChange: setQ, placeholder: "Search models" }), _jsxs(YStack, { maxH: "min(420px, 60vh)", overflowY: "auto", overflowX: "hidden", "data-slot": "model-rows", children: [!mind.free && !q.trim() ? (_jsx(DropdownMenuItem, { onSelect: () => mind.pick(ENSO), children: _jsxs(XStack, { flex: 1, items: "center", gap: 8, children: [_jsxs(YStack, { flex: 1, children: [_jsx(SizableText, { fontSize: 14, color: "$ink", children: "Enso" }), _jsx(SizableText, { fontSize: 12, color: "$hint", children: "Picks the model for each message and step" })] }), routed ? _jsx(Check, { size: 14, color: "var(--primary)", "aria-hidden": true }) : null] }) })) : null, groups.map((g) => (_jsxs(YStack, { role: "group", "aria-label": g.label, children: [_jsx(SizableText, { fontSize: 12, color: "$hint", px: 8, pt: 8, pb: 2, children: g.label }), g.models.map((m) => {
105
+ const ok = grants(mind.free, m.id, mind.models);
106
+ return (_jsx(DropdownMenuItem, { "data-slot": "model-row", onSelect: () => (ok ? mind.pick(m.id) : onUpgrade?.(m.id)), children: _jsxs(XStack, { flex: 1, items: "center", gap: 8, children: [_jsx(SizableText, { fontSize: 14, color: ok ? '$ink' : '$hint', numberOfLines: 1, flex: 1, children: nameOf(m.id, mind.models) }), ok ? null : (_jsx(SizableText, { fontSize: 12, color: "$hint", px: 6, py: 1, rounded: 6, borderWidth: 1, borderColor: "$borderColor", children: "Upgrade" })), m.id === mind.model ? _jsx(Check, { size: 14, color: "var(--primary)", "aria-hidden": true }) : null] }) }, m.id));
107
+ })] }, g.key))), more > 0 ? (_jsx(SizableText, { fontSize: 12, color: "$hint", px: 8, py: 8, children: `Type to search ${more} more models` })) : null] })] })] }));
93
108
  }
94
109
  /** The row under the frame: where (Dev), then the model, the effort and the mode. */
95
110
  export function Tune({ mind, mode, where, onUpgrade }) {
@@ -130,7 +145,7 @@ export function prompt(o, state) {
130
145
  const mention = listed && shelf && typed ? (_jsx(Mention, { shelf: shelf, rows: listed, index: index, onPick: choose, tag: typed.char === '/' ? 'Skill' : 'Plugin', name: typed.char === '/' ? 'Skills' : 'Plugins' })) : null;
131
146
  const head = o.head || o.note || mention || (narrow && o.below) ? (_jsxs(YStack, { ...measure, gap: "$1.5", "data-slot": "prompt-head", children: [narrow ? o.below : null, o.head, mention, o.note ? (_jsx(SizableText, { "data-slot": "prompt-note", fontSize: 12, color: "$soft", text: "center", self: "center", children: o.note })) : null] })) : undefined;
132
147
  const foot = o.mind || o.mode || o.where || (!narrow && o.below) ? (_jsxs(YStack, { ...measure, gap: 10, "data-slot": "prompt-foot", children: [_jsx(Tune, { mind: o.mind, mode: o.mode, where: o.where, onUpgrade: o.onUpgrade }), narrow ? null : o.below] })) : undefined;
133
- const round = (_jsx(Control, { type: "button", size: ROUND, fill: true, rounded: 999, "data-slot": "composer-send", "aria-label": busy ? 'Stop' : 'Send', "aria-disabled": !live || undefined, opacity: live ? 1 : 0.4, onClick: () => {
148
+ const round = (_jsx(Control, { type: "button", size: narrow ? TAP : ROUND, fill: true, rounded: 999, "data-slot": "composer-send", "aria-label": busy ? 'Stop' : 'Send', "aria-disabled": !live || undefined, opacity: live ? 1 : 0.4, onClick: () => {
134
149
  if (!live)
135
150
  return;
136
151
  if (busy)
@@ -173,7 +188,7 @@ export function prompt(o, state) {
173
188
  '$max-sm': { rounded: CORNER_PHONE },
174
189
  ...(o.framed ? { boxShadow: 'var(--shadow-sheet-1)' } : null),
175
190
  children: (_jsxs(XStack, { items: "center", gap: 4, "data-slot": "prompt-start", flex: 1, minW: 0, flexWrap: "wrap", children: [o.plus ? _jsx(Plus, { ...o.plus, disabled: disabled || o.plus.disabled }) : null, (o.on ?? []).map((c) => (_jsxs(XStack, { "data-slot": "prompt-chip", items: "center", gap: 6, pl: 8, pr: 4, height: 28, rounded: 8, bg: "$hover", maxW: 180, children: [c.icon ? _jsx(XStack, { "aria-hidden": true, children: c.icon }) : null, _jsx(SizableText, { fontSize: 13, color: "$ink", numberOfLines: 1, shrink: 1, children: c.label }), _jsx(XStack, { render: "button", "aria-label": `Remove ${c.label}`, onPress: c.onRemove, width: 20, height: 20, rounded: 6, items: "center", justify: "center", hoverStyle: { bg: '$raised' }, cursor: "pointer", children: _jsx(X, { size: 12, color: "$hint" }) })] }, c.id)))] })),
176
- send: (_jsxs(XStack, { items: "center", gap: 6, "data-slot": "prompt-end", shrink: 0, children: [o.voice ? _jsx(Dictation, { voice: o.voice, disabled: disabled, language: o.language ?? null, ...(o.onLanguage ? { onLanguage: o.onLanguage } : {}) }) : null, !busy && !lit && o.talk ? (_jsx(Control, { asChild: true, size: ROUND, fill: narrow, rounded: 999, ...(narrow ? null : QUIET), children: _jsx(Voice, { voice: o.talk, disabled: disabled, "data-slot": "composer-talk", says: { idle: 'Voice mode', listening: 'In conversation — click to hang up', speaking: 'Hanzo is speaking — talk to interrupt' }, children: () => _jsx(AudioLines, { size: 16, color: narrow ? ON : 'var(--foreground)' }) }) })) : (round)] })),
191
+ send: (_jsxs(XStack, { items: "center", gap: 6, "data-slot": "prompt-end", shrink: 0, children: [o.voice ? _jsx(Dictation, { voice: o.voice, disabled: disabled, narrow: narrow, language: o.language ?? null, ...(o.onLanguage ? { onLanguage: o.onLanguage } : {}) }) : null, !busy && !lit && o.talk ? (_jsx(Control, { asChild: true, size: narrow ? TAP : ROUND, fill: narrow, rounded: 999, ...(narrow ? null : QUIET), children: _jsx(Voice, { voice: o.talk, disabled: disabled, "data-slot": "composer-talk", says: { idle: 'Voice mode', listening: 'In conversation — click to hang up', speaking: 'Hanzo is speaking — talk to interrupt' }, children: () => _jsx(AudioLines, { size: 16, color: narrow ? ON : 'var(--foreground)' }) }) })) : (round)] })),
177
192
  };
178
193
  }
179
194
  /** Ctrl+U (⌘U) uploads, while a composer with an upload is on the page. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzo/build",
3
- "version": "0.2.62",
3
+ "version": "0.2.64",
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.",
@@ -14,7 +14,7 @@
14
14
  "@hanzo/iam": "0.21.28",
15
15
  "@hanzo/ui": "8.29.0",
16
16
  "@hanzo/vite": "0.1.4",
17
- "@hanzo/voice": "0.1.18",
17
+ "@hanzo/voice": "0.1.19",
18
18
  "@hanzogui/lucide-icons-2": "8.3.5",
19
19
  "@hanzogui/next-theme": "8.3.5",
20
20
  "@playwright/test": "1.63.0",
@@ -75,7 +75,7 @@
75
75
  "@hanzo/ui": ">=8.29.0",
76
76
  "@hanzogui/lucide-icons-2": ">=8.3.5",
77
77
  "react": ">=19.2.8",
78
- "@hanzo/voice": ">=0.1.16",
78
+ "@hanzo/voice": ">=0.1.19",
79
79
  "@hanzo/composer": ">=0.2.0"
80
80
  },
81
81
  "scripts": {