@hanzo/build 0.2.83 → 0.2.84
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/README.md +13 -10
- package/lib/carry.d.ts +3 -4
- package/lib/carry.js +2 -2
- package/lib/host.d.ts +0 -2
- package/lib/index.d.ts +5 -7
- package/lib/index.js +5 -7
- package/lib/landing.js +51 -45
- package/lib/mind.d.ts +28 -5
- package/lib/mind.js +5 -30
- package/lib/plugins/index.d.ts +1 -1
- package/lib/project.js +7 -7
- package/lib/run.js +6 -7
- package/lib/settings/general.js +2 -1
- package/lib/tools.d.ts +9 -6
- package/lib/tools.js +5 -14
- package/lib/transcript.js +1 -2
- package/lib/voice.d.ts +4 -4
- package/lib/voice.js +11 -11
- package/package.json +3 -3
- package/lib/mic.d.ts +0 -12
- package/lib/mic.js +0 -139
- package/lib/plus.d.ts +0 -46
- package/lib/plus.js +0 -72
- package/lib/prompt.d.ts +0 -134
- package/lib/prompt.js +0 -272
package/lib/prompt.js
DELETED
|
@@ -1,272 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
/**
|
|
3
|
-
* The composer, once, for Chat and Dev. A frame: the field on top ("Type / for
|
|
4
|
-
* skills"), and under it one row — `+` at the start, what the conversation
|
|
5
|
-
* carries as small chips, then at the end dictation (the mic and its menu) and
|
|
6
|
-
* one round button that is voice mode while the draft is empty, send once it
|
|
7
|
-
* has words, and stop while a turn runs. Nothing else is in the frame.
|
|
8
|
-
*
|
|
9
|
-
* Under the frame, at its end, one quiet row: the model (only where the host
|
|
10
|
-
* shows it), the effort (dim) and the mode as a pill, each opening its choices;
|
|
11
|
-
* Dev puts where the run goes and its repository at the row's start (`where`).
|
|
12
|
-
* Under that, the person's suggestions (`below`; above the frame on a phone).
|
|
13
|
-
*
|
|
14
|
-
* `/` opens the skills and `@` the plugins, in a list just above the frame.
|
|
15
|
-
*
|
|
16
|
-
* `prompt` builds @hanzo/ui's `Composer` props, so a surface that draws the
|
|
17
|
-
* composer through `Chat` (its `composer` prop) and one that draws it directly
|
|
18
|
-
* (`<Prompt>`) draw the same thing. The frame is as wide as the text above it
|
|
19
|
-
* (`measure`).
|
|
20
|
-
*
|
|
21
|
-
* Its words are the reader's language (@hanzo/i18n): the components here ask
|
|
22
|
-
* `useI18n` themselves, and `prompt`, which is not a component, takes `t` from
|
|
23
|
-
* `useMention`'s state, English without one. Model names, a host's own words
|
|
24
|
-
* (its placeholder, a shelf's rows and links) and what a person typed are
|
|
25
|
-
* drawn as given.
|
|
26
|
-
*/
|
|
27
|
-
import { SizableText, useMedia, XStack, YStack } from '@hanzo/gui';
|
|
28
|
-
import { phrase } from '@hanzo/i18n';
|
|
29
|
-
import { useI18n } from '@hanzo/i18n/react';
|
|
30
|
-
import { ArrowUp, AudioLines, Check, Square, X } from '@hanzogui/lucide-icons-2';
|
|
31
|
-
import { Control } from '@hanzo/composer';
|
|
32
|
-
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSearch, DropdownMenuTrigger, Popover, PopoverContent, PopoverTrigger } from '@hanzo/ui';
|
|
33
|
-
import { Composer } from '@hanzo/ui/chat';
|
|
34
|
-
import { groupModels, isPaused, matchesModel } from '@hanzo/ui/models';
|
|
35
|
-
import { speech, Voice } from '@hanzo/voice';
|
|
36
|
-
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
37
|
-
import { ENSO } from './api/models.js';
|
|
38
|
-
import { MODES } from './gear.js';
|
|
39
|
-
import { Dictation } from './mic.js';
|
|
40
|
-
import { EFFORTS, grants, nameOf, ROUTER_IDS } from './mind.js';
|
|
41
|
-
import { Plus } from './plus.js';
|
|
42
|
-
import { english } from './say.js';
|
|
43
|
-
/** The reading measure: @hanzo/design's prose width, which Width (appearance) retunes. */
|
|
44
|
-
export const MEASURE = 'var(--container-prose, 48rem)';
|
|
45
|
-
/** The space between turns, px: one rhythm for Chat and Dev. */
|
|
46
|
-
export const GAP = 20;
|
|
47
|
-
/** The composer's width: the measure less the thread column's 0.75rem gutters, centred, so its edges are the text's. */
|
|
48
|
-
export const measure = { width: 'calc(100% - 1.5rem)', maxW: `calc(${MEASURE} - 1.5rem)`, mx: 'auto' };
|
|
49
|
-
/** The round controls' box, px, and a phone's, a finger's width. */
|
|
50
|
-
export const ROUND = 32;
|
|
51
|
-
const TAP = 44;
|
|
52
|
-
/** The frame's corner, px: 12 on a laptop, 22 on a phone. */
|
|
53
|
-
const CORNER = 12;
|
|
54
|
-
const CORNER_PHONE = 22;
|
|
55
|
-
/** The ink on a filled round: the accent's own foreground (gui's icons default to the theme's text). */
|
|
56
|
-
const ON = 'var(--primary-foreground)';
|
|
57
|
-
/** A round control with no ground of its own until it is pointed at. */
|
|
58
|
-
const QUIET = { bg: 'transparent', color: 'var(--foreground)', hoverStyle: { bg: '$hover' } };
|
|
59
|
-
/** The model rows the menu draws before anything is typed. */
|
|
60
|
-
const SHOWN = 40;
|
|
61
|
-
/** What every Composer placeholder says, unless a surface names who is listening. */
|
|
62
|
-
export const ASK = phrase('Type / for skills');
|
|
63
|
-
/**
|
|
64
|
-
* The frame as a pane of its own, floating on the pane it is drawn in: the
|
|
65
|
-
* `--pane-*` cut on design's first paper rung.
|
|
66
|
-
*/
|
|
67
|
-
export const cut = {
|
|
68
|
-
bg: 'var(--pane-fill, var(--sheet-1))',
|
|
69
|
-
rounded: 'var(--pane-round, var(--radius-xl))',
|
|
70
|
-
borderWidth: 1,
|
|
71
|
-
borderColor: 'var(--pane-edge, var(--white-08))',
|
|
72
|
-
backdropFilter: 'var(--pane-blur, blur(20px) saturate(1.8))',
|
|
73
|
-
boxShadow: 'var(--shadow-sheet-1)',
|
|
74
|
-
};
|
|
75
|
-
/** The platform's ear and voice for a target: dictation, talk and Listen all hear through it. */
|
|
76
|
-
export function useEar(t) {
|
|
77
|
-
return useMemo(() => speech({ baseUrl: t.api, token: t.token, ...(t.org ? { org: t.org } : {}) }), [t]);
|
|
78
|
-
}
|
|
79
|
-
/** A text button in the row under the frame. */
|
|
80
|
-
function Word({ children, dim = false, label, open }) {
|
|
81
|
-
return (_jsx(XStack, { render: "button", "aria-label": label, "aria-expanded": open, items: "center", height: 24, px: 6, rounded: 6, cursor: "pointer", bg: open ? '$hover' : 'transparent', hoverStyle: { bg: '$hover' }, focusVisibleStyle: { outlineColor: '$outlineColor', outlineWidth: 2, outlineStyle: 'solid' }, "$max-sm": { height: 44, px: 8 }, children: _jsx(SizableText, { fontSize: 14, fontWeight: dim ? '400' : '500', color: dim ? '$hint' : '$ink', numberOfLines: 1, children: children }) }));
|
|
82
|
-
}
|
|
83
|
-
/** A titled list of choices, each with a line saying what it does and a check on the one in force. The items' words are marked (`phrase`); the title comes translated. */
|
|
84
|
-
function Choices({ title, items, value, onPick, }) {
|
|
85
|
-
const { t } = useI18n();
|
|
86
|
-
return (_jsxs(YStack, { role: "listbox", "aria-label": title, gap: 2, "data-slot": "choices", children: [_jsx(SizableText, { fontSize: 13, color: "$hint", px: 10, pt: 6, pb: 4, children: title }), items.map((c) => (_jsxs(XStack, { render: "button", role: "option", "aria-selected": c.id === value, "aria-disabled": c.off || undefined, onPress: () => !c.off && onPick(c.id), items: "flex-start", gap: 12, px: 10, py: 8, rounded: 8, width: "100%", cursor: c.off ? 'default' : 'pointer', opacity: c.off ? 0.5 : 1, hoverStyle: c.off ? undefined : { bg: '$hover' }, children: [_jsxs(YStack, { flex: 1, minW: 0, items: "flex-start", gap: 2, children: [_jsx(SizableText, { fontSize: 14, color: "$ink", text: "start", children: t(c.label) }), _jsx(SizableText, { fontSize: 13, color: "$hint", text: "start", children: t(c.note) })] }), c.id === value ? _jsx(Check, { size: 16, color: "var(--primary)", mt: 2, "aria-hidden": true }) : null] }, c.id)))] }));
|
|
87
|
-
}
|
|
88
|
-
/** A word in the row that opens a panel of choices. */
|
|
89
|
-
function Opens({ label, word, dim, children }) {
|
|
90
|
-
const [open, setOpen] = useState(false);
|
|
91
|
-
return (_jsxs(Popover, { open: open, onOpenChange: setOpen, allowFlip: true, placement: "top-end", children: [_jsx(PopoverTrigger, { asChild: true, children: _jsx(XStack, { children: _jsx(Word, { label: label, dim: dim, open: open, children: word }) }) }), _jsx(PopoverContent, { solid: true, p: 6, width: 288, maxW: "calc(100vw - 24px)", children: children(() => setOpen(false)) })] }));
|
|
92
|
-
}
|
|
93
|
-
/** The model's name, and the catalog behind it: Free's lane to pick from, the rest an upgrade. */
|
|
94
|
-
function Model({ mind, onUpgrade, limits }) {
|
|
95
|
-
const { t } = useI18n();
|
|
96
|
-
const [open, setOpen] = useState(false);
|
|
97
|
-
const [q, setQ] = useState('');
|
|
98
|
-
// The catalog is hundreds long, and drawing all of it is what made the menu slow
|
|
99
|
-
// to open: until something is typed, the first `SHOWN` rows, Hanzo's families
|
|
100
|
-
// first, and a line saying the search reaches the rest.
|
|
101
|
-
const { groups, more } = useMemo(() => {
|
|
102
|
-
const list = mind.models.filter((m) => !ROUTER_IDS.has(m.id) && (m.outputs?.includes('text') ?? true) && (!q.trim() || matchesModel(m, q)));
|
|
103
|
-
const all = groupModels(list);
|
|
104
|
-
let room = q.trim() ? Infinity : SHOWN;
|
|
105
|
-
const cut = all
|
|
106
|
-
.map((g) => {
|
|
107
|
-
const models = g.models.slice(0, Math.max(0, room));
|
|
108
|
-
room -= models.length;
|
|
109
|
-
return { ...g, models };
|
|
110
|
-
})
|
|
111
|
-
.filter((g) => g.models.length);
|
|
112
|
-
return { groups: cut, more: list.length - cut.reduce((n, g) => n + g.models.length, 0) };
|
|
113
|
-
}, [mind.models, q]);
|
|
114
|
-
const routed = mind.model === ENSO;
|
|
115
|
-
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: t('Model: {name}', { name: 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: t('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: t('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) => {
|
|
116
|
-
const ok = grants(mind.free, m.id, mind.models);
|
|
117
|
-
// Paused by the plan's usage: still listed and still picked; the gateway answers from Enso.
|
|
118
|
-
const paused = isPaused(m, limits);
|
|
119
|
-
return (_jsx(DropdownMenuItem, { "data-slot": "model-row", "data-model": m.id, "data-paused": paused ? 'true' : undefined, 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: t('Upgrade') })), ok && paused ? (_jsx(SizableText, { fontSize: 12, color: "$hint", children: t('Paused') })) : null, m.id === mind.model ? _jsx(Check, { size: 14, color: "var(--primary)", "aria-hidden": true }) : null] }) }, m.id));
|
|
120
|
-
})] }, g.key))), more > 0 ? (_jsx(SizableText, { fontSize: 12, color: "$hint", px: 8, py: 8, children: t('Type to search {count} more models', { count: more }) })) : null] })] })] }));
|
|
121
|
-
}
|
|
122
|
-
/** The row under the frame: where (Dev), then the model where the host shows it, the effort and the mode. */
|
|
123
|
-
export function Tune({ model, effort, mode, where, onUpgrade, limits, }) {
|
|
124
|
-
const { t } = useI18n();
|
|
125
|
-
const pace = effort ? EFFORTS.find((e) => e.id === effort.effort) : null;
|
|
126
|
-
const now = mode ? MODES.find((m) => m.id === mode.value) : undefined;
|
|
127
|
-
const way = now ? t(now.label) : '';
|
|
128
|
-
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, marginInlineStart: "auto", shrink: 0, children: [model ? _jsx(Model, { mind: model, onUpgrade: onUpgrade, limits: limits }) : null, effort && pace ? (_jsx(Opens, { label: t('Effort: {level}', { level: t(pace.label) }), word: t(pace.label), dim: true, children: (close) => (_jsx(Choices, { title: t('Effort'), items: EFFORTS, value: effort.effort, onPick: (e) => {
|
|
129
|
-
effort.pace(e);
|
|
130
|
-
close();
|
|
131
|
-
} })) })) : null, mode ? (_jsxs(Popover, { allowFlip: true, placement: "top-end", children: [_jsx(PopoverTrigger, { asChild: true, children: _jsx(XStack, { render: "button", "data-slot": "mode", "aria-label": t('Mode: {mode}', { mode: way }), items: "center", height: 24, px: 8, marginInlineStart: 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: way }) }) }), _jsx(PopoverContent, { solid: true, p: 6, width: 288, maxW: "calc(100vw - 24px)", children: _jsx(Choices, { title: t('Mode'), items: mode.items ? MODES.filter((m) => mode.items.includes(m.id)) : MODES, value: mode.value, onPick: mode.set }) })] })) : null] })] }));
|
|
132
|
-
}
|
|
133
|
-
/** The `/` or `@` the caret ends on, and the words typed after it. */
|
|
134
|
-
export function trigger(value) {
|
|
135
|
-
const m = /(^|\s)([/@])([^\s/@]*)$/.exec(value);
|
|
136
|
-
return m ? { char: m[2], query: m[3].toLowerCase(), at: m.index + m[1].length } : null;
|
|
137
|
-
}
|
|
138
|
-
/** The list `/` (the skills) or `@` (the plugins) opens above the frame. */
|
|
139
|
-
function Mention({ shelf, rows, index, onPick, skills }) {
|
|
140
|
-
const { t } = useI18n();
|
|
141
|
-
const tag = t(skills ? 'Skill' : 'Plugin');
|
|
142
|
-
return (_jsxs(YStack, { role: "listbox", "aria-label": t(skills ? 'Skills' : 'Plugins'), "data-slot": "mention", width: "100%", maxH: 240, overflowY: "auto", overflowX: "hidden", p: 6, rounded: CORNER, borderWidth: 1, borderColor: "$borderColor", bg: "$background", "$max-sm": { rounded: CORNER_PHONE }, children: [_jsx(SizableText, { fontSize: 13, color: "$hint", px: 8, pt: 4, pb: 6, children: shelf.search ?? t(skills ? 'Type to search skills' : 'Type to search plugins') }), rows.length ? (rows.map((r, i) => (_jsxs(XStack, { render: "button", role: "option", "aria-selected": i === index, onPress: () => onPick(r.id), items: "center", gap: 10, px: 8, height: 36, rounded: 8, bg: i === index ? '$hover' : 'transparent', hoverStyle: { bg: '$hover' }, cursor: "pointer", "$max-sm": { height: 46 }, children: [r.icon ? (_jsx(XStack, { width: 16, items: "center", justify: "center", "aria-hidden": true, children: r.icon })) : null, _jsx(SizableText, { fontSize: 15, color: "$ink", numberOfLines: 1, flex: 1, text: "start", children: r.label }), _jsx(SizableText, { fontSize: 13, color: "$hint", children: r.on ? t('On') : tag })] }, r.id)))) : (_jsx(SizableText, { fontSize: 14, color: "$hint", px: 8, py: 8, children: t(shelf.loading ? 'Reading…' : 'Nothing matches') }))] }));
|
|
143
|
-
}
|
|
144
|
-
/** @hanzo/ui `Composer` props for the one composer. */
|
|
145
|
-
export function prompt(o, state) {
|
|
146
|
-
const t = state?.t ?? english;
|
|
147
|
-
const lit = o.ready ?? Boolean(o.value.trim());
|
|
148
|
-
const busy = Boolean(o.busy);
|
|
149
|
-
const disabled = Boolean(o.disabled);
|
|
150
|
-
const live = busy ? Boolean(o.onStop) : lit && !disabled;
|
|
151
|
-
const narrow = Boolean(o.narrow);
|
|
152
|
-
// `/` and `@`: the shelf the caret's word opens, filtered by what follows it.
|
|
153
|
-
const typed = trigger(o.value);
|
|
154
|
-
const shelf = typed ? (typed.char === '/' ? o.slash : o.at) : undefined;
|
|
155
|
-
const listed = shelf && typed && state?.closed !== o.value ? shelf.rows.filter((r) => r.label.toLowerCase().includes(typed.query)).slice(0, 8) : null;
|
|
156
|
-
const index = Math.min(state?.index ?? 0, Math.max(0, (listed?.length ?? 1) - 1));
|
|
157
|
-
const choose = (id) => {
|
|
158
|
-
if (!typed || !shelf)
|
|
159
|
-
return;
|
|
160
|
-
o.onChange(o.value.slice(0, typed.at).trimEnd() + (typed.at > 0 ? ' ' : ''));
|
|
161
|
-
shelf.pick(id);
|
|
162
|
-
};
|
|
163
|
-
const mention = listed && shelf && typed ? (_jsx(Mention, { shelf: shelf, rows: listed, index: index, onPick: choose, skills: typed.char === '/' })) : null;
|
|
164
|
-
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;
|
|
165
|
-
const foot = o.model || o.effort || o.mode || o.where || (!narrow && o.below) ? (_jsxs(YStack, { ...measure, gap: 10, "data-slot": "prompt-foot", children: [_jsx(Tune, { model: o.model, effort: o.effort, mode: o.mode, where: o.where, onUpgrade: o.onUpgrade, limits: o.limits }), narrow ? null : o.below] })) : undefined;
|
|
166
|
-
const round = (_jsx(Control, { type: "button", size: narrow ? TAP : ROUND, fill: true, rounded: 999, "data-slot": "composer-send", "aria-label": t(busy ? 'Stop' : 'Send'), "aria-disabled": !live || undefined, opacity: live ? 1 : 0.4, onClick: () => {
|
|
167
|
-
if (!live)
|
|
168
|
-
return;
|
|
169
|
-
if (busy)
|
|
170
|
-
o.onStop?.();
|
|
171
|
-
else
|
|
172
|
-
o.onSend();
|
|
173
|
-
}, children: busy ? _jsx(Square, { size: 13, color: ON, fill: ON }) : _jsx(ArrowUp, { size: 16, strokeWidth: 2.5, color: ON }) }));
|
|
174
|
-
return {
|
|
175
|
-
value: o.value,
|
|
176
|
-
onChange: o.onChange,
|
|
177
|
-
onSend: () => {
|
|
178
|
-
if (listed?.length)
|
|
179
|
-
return choose(listed[index].id);
|
|
180
|
-
o.onSend();
|
|
181
|
-
},
|
|
182
|
-
...(o.onStop ? { onStop: o.onStop } : {}),
|
|
183
|
-
busy,
|
|
184
|
-
disabled,
|
|
185
|
-
placeholder: o.placeholder ?? t(ASK),
|
|
186
|
-
...(o.label ? { label: o.label } : {}),
|
|
187
|
-
// At rest two rows: one line of text, then the tools. It grows with the text to the kit's ceiling, then scrolls.
|
|
188
|
-
rows: 1,
|
|
189
|
-
field: {
|
|
190
|
-
...o.field,
|
|
191
|
-
fontSize: 16,
|
|
192
|
-
lineHeight: 24,
|
|
193
|
-
px: 8,
|
|
194
|
-
pt: 2,
|
|
195
|
-
pb: 0,
|
|
196
|
-
minH: 26,
|
|
197
|
-
bg: 'transparent',
|
|
198
|
-
},
|
|
199
|
-
head,
|
|
200
|
-
foot,
|
|
201
|
-
...measure,
|
|
202
|
-
rounded: CORNER,
|
|
203
|
-
p: 8,
|
|
204
|
-
pt: 12,
|
|
205
|
-
gap: 6,
|
|
206
|
-
borderColor: '$borderColor',
|
|
207
|
-
bg: '$edge',
|
|
208
|
-
'$max-sm': { rounded: CORNER_PHONE },
|
|
209
|
-
...(o.framed ? { boxShadow: 'var(--shadow-sheet-1)' } : null),
|
|
210
|
-
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, paddingInlineStart: 8, paddingInlineEnd: 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": t('Remove {name}', { name: 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)))] })),
|
|
211
|
-
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: t('Voice mode'), listening: t('In conversation — click to hang up'), speaking: t('Hanzo is speaking — talk to interrupt') }, children: () => _jsx(AudioLines, { size: 16, color: narrow ? ON : 'var(--foreground)' }) }) })) : (round)] })),
|
|
212
|
-
};
|
|
213
|
-
}
|
|
214
|
-
/** Ctrl+U (⌘U) uploads, while a composer with an upload is on the page. */
|
|
215
|
-
export function useUpload(upload) {
|
|
216
|
-
const now = useRef(upload);
|
|
217
|
-
now.current = upload;
|
|
218
|
-
useEffect(() => {
|
|
219
|
-
if (!upload)
|
|
220
|
-
return;
|
|
221
|
-
const key = (e) => {
|
|
222
|
-
if ((e.ctrlKey || e.metaKey) && !e.altKey && !e.shiftKey && e.key.toLowerCase() === 'u') {
|
|
223
|
-
e.preventDefault();
|
|
224
|
-
now.current?.();
|
|
225
|
-
}
|
|
226
|
-
};
|
|
227
|
-
window.addEventListener('keydown', key);
|
|
228
|
-
return () => window.removeEventListener('keydown', key);
|
|
229
|
-
}, [Boolean(upload)]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
230
|
-
}
|
|
231
|
-
/**
|
|
232
|
-
* The composer's own state — which row of a `/` or `@` list is lit, and the
|
|
233
|
-
* reader's `t` — and the keys that move it. A surface that hands `prompt` to
|
|
234
|
-
* `Chat` calls this and passes its answer through; `<Prompt>` does it for itself.
|
|
235
|
-
*/
|
|
236
|
-
export function useMention(o) {
|
|
237
|
-
const { t } = useI18n();
|
|
238
|
-
const [index, setIndex] = useState(0);
|
|
239
|
-
const [closed, setClosed] = useState(null);
|
|
240
|
-
useUpload(o.plus?.upload);
|
|
241
|
-
const typed = trigger(o.value);
|
|
242
|
-
const shelf = typed ? (typed.char === '/' ? o.slash : o.at) : undefined;
|
|
243
|
-
const open = Boolean(shelf && closed !== o.value);
|
|
244
|
-
useEffect(() => setIndex(0), [typed?.char, typed?.query]);
|
|
245
|
-
const field = {
|
|
246
|
-
...o.field,
|
|
247
|
-
onKeyDown: (e) => {
|
|
248
|
-
const key = e.key ?? e.nativeEvent?.key;
|
|
249
|
-
if (open && key === 'ArrowDown') {
|
|
250
|
-
e.preventDefault?.();
|
|
251
|
-
setIndex((i) => i + 1);
|
|
252
|
-
}
|
|
253
|
-
else if (open && key === 'ArrowUp') {
|
|
254
|
-
e.preventDefault?.();
|
|
255
|
-
setIndex((i) => Math.max(0, i - 1));
|
|
256
|
-
}
|
|
257
|
-
else if (open && key === 'Escape') {
|
|
258
|
-
e.preventDefault?.();
|
|
259
|
-
setClosed(o.value);
|
|
260
|
-
}
|
|
261
|
-
else
|
|
262
|
-
o.field?.onKeyDown?.(e);
|
|
263
|
-
},
|
|
264
|
-
};
|
|
265
|
-
return { state: { index, closed, t }, field };
|
|
266
|
-
}
|
|
267
|
-
/** The composer drawn on its own. */
|
|
268
|
-
export function Prompt(o) {
|
|
269
|
-
const media = useMedia();
|
|
270
|
-
const { state, field } = useMention(o);
|
|
271
|
-
return _jsx(Composer, { ...prompt({ ...o, field, narrow: o.narrow ?? !media.sm }, state) });
|
|
272
|
-
}
|