@hanzo/build 0.2.60 → 0.2.62
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 -3
- package/lib/api/conversations.d.ts +20 -0
- package/lib/api/conversations.js +23 -0
- package/lib/api/pref.d.ts +10 -0
- package/lib/api/pref.js +7 -0
- package/lib/builder.js +2 -2
- package/lib/carry.d.ts +17 -0
- package/lib/carry.js +95 -0
- package/lib/gear.d.ts +53 -0
- package/lib/gear.js +141 -0
- package/lib/index.d.ts +22 -5
- package/lib/index.js +20 -5
- package/lib/landing.js +30 -10
- package/lib/mic.d.ts +10 -0
- package/lib/mic.js +95 -0
- package/lib/mind.d.ts +18 -1
- package/lib/mind.js +48 -5
- package/lib/panel.js +14 -7
- package/lib/plugin.d.ts +56 -0
- package/lib/plugin.js +1 -0
- package/lib/plus.d.ts +46 -0
- package/lib/plus.js +58 -0
- package/lib/prefs.d.ts +14 -1
- package/lib/prefs.js +13 -6
- package/lib/project.js +4 -7
- package/lib/prompt.d.ts +68 -19
- package/lib/prompt.js +172 -95
- package/lib/run.js +14 -44
- package/lib/side.d.ts +68 -0
- package/lib/side.js +195 -0
- package/lib/suggest.d.ts +202 -0
- package/lib/suggest.js +329 -0
- package/lib/suggestions.d.ts +20 -0
- package/lib/suggestions.js +170 -0
- package/package.json +3 -3
- package/lib/grip.d.ts +0 -40
- package/lib/grip.js +0 -108
package/lib/prefs.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
3
|
* The person's own settings, read once for the whole builder and shared by
|
|
4
4
|
* everything that follows them: Settings writes them, New starts from the
|
|
@@ -8,6 +8,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
8
8
|
* The last document read is kept in this browser, per org, so the page is
|
|
9
9
|
* drawn the person's way before the platform answers. A save shows at once and
|
|
10
10
|
* is taken back, key by key, if the platform refuses it.
|
|
11
|
+
*
|
|
12
|
+
* A host mounts it too, above the builder (the Hanzo app, for Chat's side
|
|
13
|
+
* panel), and the builder's own then reads through that one: one document, one
|
|
14
|
+
* read. The look (`Look`) is the builder's alone: a host has its own.
|
|
11
15
|
*/
|
|
12
16
|
import { createContext, useCallback, useContext, useEffect, useMemo, useRef } from 'react';
|
|
13
17
|
import { merge, read, save } from './api/pref.js';
|
|
@@ -15,6 +19,9 @@ import { useKept, useRead } from './data.js';
|
|
|
15
19
|
import { useHost, useTarget } from './host.js';
|
|
16
20
|
const Context = createContext(null);
|
|
17
21
|
export function PrefsProvider({ children }) {
|
|
22
|
+
return useContext(Context) ? children : _jsx(Keep, { children: children });
|
|
23
|
+
}
|
|
24
|
+
function Keep({ children }) {
|
|
18
25
|
const host = useHost();
|
|
19
26
|
const t = useTarget();
|
|
20
27
|
const signed = Boolean(host.person);
|
|
@@ -48,11 +55,11 @@ export function PrefsProvider({ children }) {
|
|
|
48
55
|
}
|
|
49
56
|
}, [signed, t, keep]);
|
|
50
57
|
const value = useMemo(() => ({ prefs: kept, loading: doc.loading, error: doc.error, save: write }), [kept, doc.loading, doc.error, write]);
|
|
51
|
-
return (
|
|
58
|
+
return _jsx(Context.Provider, { value: value, children: children });
|
|
52
59
|
}
|
|
53
|
-
export function usePrefs() {
|
|
60
|
+
export function usePrefs(need = true) {
|
|
54
61
|
const p = useContext(Context);
|
|
55
|
-
if (!p)
|
|
62
|
+
if (!p && need)
|
|
56
63
|
throw new Error('The builder was rendered without its settings');
|
|
57
64
|
return p;
|
|
58
65
|
}
|
|
@@ -66,9 +73,9 @@ const STILL = '*,*::before,*::after{animation-duration:.01ms!important;animation
|
|
|
66
73
|
* root re-applies its appearance on mount, and a rule marked important outlasts
|
|
67
74
|
* that, while the builder is on the page and no longer.
|
|
68
75
|
*/
|
|
69
|
-
function Look(
|
|
76
|
+
export function Look() {
|
|
70
77
|
const host = useHost();
|
|
71
|
-
const { theme, text, motion } = prefs;
|
|
78
|
+
const { theme, text, motion } = usePrefs().prefs;
|
|
72
79
|
useEffect(() => {
|
|
73
80
|
if (theme && theme !== host.theme)
|
|
74
81
|
host.chooseTheme?.(theme);
|
package/lib/project.js
CHANGED
|
@@ -40,9 +40,9 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
40
40
|
*/
|
|
41
41
|
import { SizableText, useMedia, XStack, YStack } from '@hanzo/gui';
|
|
42
42
|
import { Clock, ExternalLink, MousePointerClick, PanelLeft, RefreshCcw, Share2, Upload, } from '@hanzogui/lucide-icons-2';
|
|
43
|
-
import { Attachments, CHAT, Console, DEVICES, Feedback, FileTabs, FileTree,
|
|
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 } from '@hanzo/ui';
|
|
45
|
+
import { Button, Dialog, DialogContent, DialogTitle, Grip } 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,9 +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 { Grip } from './grip.js';
|
|
61
60
|
import { useHost, useTarget } from './host.js';
|
|
62
|
-
import { MODES } from './landing.js';
|
|
63
61
|
import { useMind } from './mind.js';
|
|
64
62
|
import { Out } from './out.js';
|
|
65
63
|
import { Prompt, useEar } from './prompt.js';
|
|
@@ -192,7 +190,6 @@ export function Project({ at }) {
|
|
|
192
190
|
const [asked, setAsked] = useState(false);
|
|
193
191
|
const shut = cramped ? !asked : collapsed;
|
|
194
192
|
const [dock, setDock] = useKept('hanzo.build.dock', 36);
|
|
195
|
-
const [dismissed, setDismissed] = useKept(`hanzo.build.hints.${slug}`, false);
|
|
196
193
|
const [mode, setMode] = useKept('hanzo.build.mode', 'build');
|
|
197
194
|
const [page, setPage] = useState('/');
|
|
198
195
|
const [draft, setDraft] = useState('');
|
|
@@ -417,9 +414,9 @@ export function Project({ at }) {
|
|
|
417
414
|
? `${project.name} is loaded and nothing is published yet. Say what to change and it gets built.`
|
|
418
415
|
: named
|
|
419
416
|
? `${named.org}/${named.name} is open on ${home}. Say what to change and it gets built there.`
|
|
420
|
-
: `${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: [
|
|
417
|
+
: `${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
|
|
421
418
|
? () => void stop(t, current).then(() => setNote('Stop requested — the run keeps its work on its branch'), (e) => setNote(e.message))
|
|
422
|
-
: undefined, busy: busy || (running && !draft.trim()), placeholder: running ? 'Steer this run' : 'Ask Hanzo for edits', label: "Ask Hanzo for edits",
|
|
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(Grip, { side: "left", span: across, floor: CHAT_FLOOR, ceil: most, reset: CHAT_WIDTH, onSpan: setSplit, label: "Resize the chat", r: -GUTTER, width: GUTTER })) : null] }));
|
|
423
420
|
// The preview stays loaded while another view is open, so coming back to it
|
|
424
421
|
// is the page as it was left.
|
|
425
422
|
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
|
@@ -3,7 +3,9 @@ import { type PauseSource } from '@hanzo/ui/models';
|
|
|
3
3
|
import { type Machine, type Speech } from '@hanzo/voice';
|
|
4
4
|
import { type ReactNode } from 'react';
|
|
5
5
|
import type { Target } from './api/call.ts';
|
|
6
|
+
import { type Mode } from './gear.ts';
|
|
6
7
|
import { type Mind } from './mind.ts';
|
|
8
|
+
import { type PlusProps, type Shelf } from './plus.tsx';
|
|
7
9
|
/** The reading measure: @hanzo/design's prose width, which Width (appearance) retunes. */
|
|
8
10
|
export declare const MEASURE = "var(--container-prose, 48rem)";
|
|
9
11
|
/** The space between turns, px: one rhythm for Chat and Dev. */
|
|
@@ -14,12 +16,13 @@ export declare const measure: {
|
|
|
14
16
|
readonly maxW: "calc(var(--container-prose, 48rem) - 1.5rem)";
|
|
15
17
|
readonly mx: 'auto';
|
|
16
18
|
};
|
|
17
|
-
/** The round controls' box, px
|
|
19
|
+
/** The round controls' box, px. */
|
|
18
20
|
export declare const ROUND = 32;
|
|
21
|
+
/** What every Composer placeholder says, unless a surface names who is listening. */
|
|
22
|
+
export declare const ASK = "Type / for skills";
|
|
19
23
|
/**
|
|
20
24
|
* The frame as a pane of its own, floating on the pane it is drawn in: the
|
|
21
|
-
* `--pane-*` cut on design's first paper rung
|
|
22
|
-
* gradient through a translucent pane).
|
|
25
|
+
* `--pane-*` cut on design's first paper rung.
|
|
23
26
|
*/
|
|
24
27
|
export declare const cut: {
|
|
25
28
|
readonly bg: 'var(--pane-fill, var(--sheet-1))';
|
|
@@ -31,6 +34,13 @@ export declare const cut: {
|
|
|
31
34
|
};
|
|
32
35
|
/** The platform's ear and voice for a target: dictation, talk and Listen all hear through it. */
|
|
33
36
|
export declare function useEar(t: Target): Speech;
|
|
37
|
+
/** One thing the conversation carries, shown after `+`. */
|
|
38
|
+
export interface Chip {
|
|
39
|
+
id: string;
|
|
40
|
+
label: string;
|
|
41
|
+
icon?: ReactNode;
|
|
42
|
+
onRemove: () => void;
|
|
43
|
+
}
|
|
34
44
|
export interface PromptOptions {
|
|
35
45
|
value: string;
|
|
36
46
|
onChange: (value: string) => void;
|
|
@@ -49,31 +59,70 @@ export interface PromptOptions {
|
|
|
49
59
|
/** The model and effort. Null where the next send uses neither (steering a run). */
|
|
50
60
|
mind?: Mind | null;
|
|
51
61
|
limits?: PauseSource | null;
|
|
52
|
-
/**
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
|
|
62
|
+
/** A model the plan does not grant was asked for: the way to the plan that does. */
|
|
63
|
+
onUpgrade?: (model: string) => void;
|
|
64
|
+
/** The `+` menu. Absent, there is none. */
|
|
65
|
+
plus?: PlusProps;
|
|
66
|
+
/** What the conversation carries, as chips after `+`. */
|
|
67
|
+
on?: Chip[];
|
|
68
|
+
/** `/` lists these (the skills) above the frame. */
|
|
69
|
+
slash?: Shelf;
|
|
70
|
+
/** `@` lists these (the plugins) above the frame. */
|
|
71
|
+
at?: Shelf;
|
|
72
|
+
/** The mode, as the pill under the frame. */
|
|
73
|
+
mode?: {
|
|
74
|
+
value: Mode;
|
|
75
|
+
set: (m: Mode) => void;
|
|
76
|
+
} | null;
|
|
77
|
+
/** At the start of the row under the frame: Dev's where and which repository. */
|
|
78
|
+
where?: ReactNode;
|
|
79
|
+
/** Under the row (above the frame on a phone): the person's suggestions. */
|
|
80
|
+
below?: ReactNode;
|
|
56
81
|
/** Dictation into the draft (@hanzo/voice `useVoice` + `useDictation`). */
|
|
57
82
|
voice?: Machine;
|
|
58
|
-
/**
|
|
83
|
+
/** The language dictation listens in, null for Auto, and how to change it. */
|
|
84
|
+
language?: string | null;
|
|
85
|
+
onLanguage?: (id: string | null) => void;
|
|
86
|
+
/** A spoken conversation (@hanzo/voice `useTalk`): the round button while the draft is empty. */
|
|
59
87
|
talk?: Machine;
|
|
60
88
|
/** Drawn on a pane: the frame is cut as a pane of its own (`cut`). */
|
|
61
89
|
framed?: boolean;
|
|
62
|
-
/** Centred just above the
|
|
90
|
+
/** Centred just above the frame: what every message is sent under. */
|
|
63
91
|
note?: ReactNode;
|
|
64
|
-
/** A phone:
|
|
92
|
+
/** A phone: suggestions go above the frame and every target is 44px. */
|
|
65
93
|
narrow?: boolean;
|
|
66
94
|
}
|
|
67
|
-
/**
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
limits?: PauseSource | null;
|
|
74
|
-
disabled?: boolean;
|
|
95
|
+
/** The row under the frame: where (Dev), then the model, the effort and the mode. */
|
|
96
|
+
export declare function Tune({ mind, mode, where, onUpgrade }: {
|
|
97
|
+
mind?: Mind | null;
|
|
98
|
+
mode?: PromptOptions['mode'];
|
|
99
|
+
where?: ReactNode;
|
|
100
|
+
onUpgrade?: (model: string) => void;
|
|
75
101
|
}): import("react").JSX.Element;
|
|
102
|
+
/** The `/` or `@` the caret ends on, and the words typed after it. */
|
|
103
|
+
export declare function trigger(value: string): {
|
|
104
|
+
char: '/' | '@';
|
|
105
|
+
query: string;
|
|
106
|
+
at: number;
|
|
107
|
+
} | null;
|
|
76
108
|
/** @hanzo/ui `Composer` props for the one composer. */
|
|
77
|
-
export declare function prompt(o: PromptOptions
|
|
109
|
+
export declare function prompt(o: PromptOptions, state?: {
|
|
110
|
+
index: number;
|
|
111
|
+
closed: string | null;
|
|
112
|
+
}): ComposerProps;
|
|
113
|
+
/** Ctrl+U (⌘U) uploads, while a composer with an upload is on the page. */
|
|
114
|
+
export declare function useUpload(upload: (() => void) | undefined): void;
|
|
115
|
+
/**
|
|
116
|
+
* The composer's own state — which row of a `/` or `@` list is lit — and the
|
|
117
|
+
* keys that move it. A surface that hands `prompt` to `Chat` calls this and
|
|
118
|
+
* passes its answer through; `<Prompt>` does it for itself.
|
|
119
|
+
*/
|
|
120
|
+
export declare function useMention(o: PromptOptions): {
|
|
121
|
+
state: {
|
|
122
|
+
index: number;
|
|
123
|
+
closed: string | null;
|
|
124
|
+
};
|
|
125
|
+
field: ComposerProps['field'];
|
|
126
|
+
};
|
|
78
127
|
/** The composer drawn on its own. */
|
|
79
128
|
export declare function Prompt(o: PromptOptions): import("react").JSX.Element;
|
package/lib/prompt.js
CHANGED
|
@@ -1,48 +1,56 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
|
-
* The composer, once, for Chat and Dev:
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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, the effort (dim) and
|
|
10
|
+
* the mode as a pill, each opening its choices; Dev puts where the run goes
|
|
11
|
+
* and its repository at the row's start (`where`). Under that, the person's
|
|
12
|
+
* suggestions (`below`; above the frame on a phone).
|
|
13
|
+
*
|
|
14
|
+
* `/` opens the skills and `@` the plugins, in a list just above the frame.
|
|
9
15
|
*
|
|
10
16
|
* `prompt` builds @hanzo/ui's `Composer` props, so a surface that draws the
|
|
11
17
|
* composer through `Chat` (its `composer` prop) and one that draws it directly
|
|
12
|
-
* (`<Prompt>`) draw the same thing.
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* The frame is as wide as the text above it: the reading measure, less the
|
|
16
|
-
* thread column's own gutters, centred (`measure`). A transcript reads at
|
|
17
|
-
* `MEASURE` and this lines up with its words.
|
|
18
|
+
* (`<Prompt>`) draw the same thing. The frame is as wide as the text above it
|
|
19
|
+
* (`measure`).
|
|
18
20
|
*/
|
|
19
21
|
import { SizableText, useMedia, XStack, YStack } from '@hanzo/gui';
|
|
20
|
-
import { ArrowUp, AudioLines, Check,
|
|
22
|
+
import { ArrowUp, AudioLines, Check, Square, X } from '@hanzogui/lucide-icons-2';
|
|
21
23
|
import { Control } from '@hanzo/composer';
|
|
22
|
-
import {
|
|
24
|
+
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSearch, DropdownMenuTrigger, Popover, PopoverContent, PopoverTrigger } from '@hanzo/ui';
|
|
23
25
|
import { Composer } from '@hanzo/ui/chat';
|
|
24
|
-
import {
|
|
26
|
+
import { groupModels, matchesModel } from '@hanzo/ui/models';
|
|
25
27
|
import { speech, Voice } from '@hanzo/voice';
|
|
26
|
-
import { useMemo, useState } from 'react';
|
|
28
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
27
29
|
import { ENSO } from './api/models.js';
|
|
28
|
-
import {
|
|
30
|
+
import { MODES } from './gear.js';
|
|
31
|
+
import { Dictation } from './mic.js';
|
|
32
|
+
import { EFFORTS, grants, nameOf, reasons, ROUTER_IDS } from './mind.js';
|
|
33
|
+
import { Plus } from './plus.js';
|
|
29
34
|
/** The reading measure: @hanzo/design's prose width, which Width (appearance) retunes. */
|
|
30
35
|
export const MEASURE = 'var(--container-prose, 48rem)';
|
|
31
36
|
/** The space between turns, px: one rhythm for Chat and Dev. */
|
|
32
37
|
export const GAP = 20;
|
|
33
38
|
/** The composer's width: the measure less the thread column's 0.75rem gutters, centred, so its edges are the text's. */
|
|
34
39
|
export const measure = { width: 'calc(100% - 1.5rem)', maxW: `calc(${MEASURE} - 1.5rem)`, mx: 'auto' };
|
|
35
|
-
/** The round controls' box, px
|
|
40
|
+
/** The round controls' box, px. */
|
|
36
41
|
export const ROUND = 32;
|
|
37
|
-
/** The
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
-
/**
|
|
41
|
-
const
|
|
42
|
+
/** The frame's corner, px: 12 on a laptop, 22 on a phone. */
|
|
43
|
+
const CORNER = 12;
|
|
44
|
+
const CORNER_PHONE = 22;
|
|
45
|
+
/** The ink on a filled round: the accent's own foreground (gui's icons default to the theme's text). */
|
|
46
|
+
const ON = 'var(--primary-foreground)';
|
|
47
|
+
/** A round control with no ground of its own until it is pointed at. */
|
|
48
|
+
const QUIET = { bg: 'transparent', color: 'var(--foreground)', hoverStyle: { bg: '$hover' } };
|
|
49
|
+
/** What every Composer placeholder says, unless a surface names who is listening. */
|
|
50
|
+
export const ASK = 'Type / for skills';
|
|
42
51
|
/**
|
|
43
52
|
* The frame as a pane of its own, floating on the pane it is drawn in: the
|
|
44
|
-
* `--pane-*` cut on design's first paper rung
|
|
45
|
-
* gradient through a translucent pane).
|
|
53
|
+
* `--pane-*` cut on design's first paper rung.
|
|
46
54
|
*/
|
|
47
55
|
export const cut = {
|
|
48
56
|
bg: 'var(--pane-fill, var(--sheet-1))',
|
|
@@ -56,104 +64,173 @@ export const cut = {
|
|
|
56
64
|
export function useEar(t) {
|
|
57
65
|
return useMemo(() => speech({ baseUrl: t.api, token: t.token, ...(t.org ? { org: t.org } : {}) }), [t]);
|
|
58
66
|
}
|
|
59
|
-
/**
|
|
60
|
-
function
|
|
61
|
-
return _jsx(
|
|
67
|
+
/** A text button in the row under the frame. */
|
|
68
|
+
function Word({ children, dim = false, label, open }) {
|
|
69
|
+
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 }) }));
|
|
62
70
|
}
|
|
63
|
-
/**
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
71
|
+
/** A titled list of choices, each with a line saying what it does and a check on the one in force. */
|
|
72
|
+
function Choices({ title, items, value, onPick, }) {
|
|
73
|
+
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", style: { textAlign: 'left' }, children: c.label }), _jsx(SizableText, { fontSize: 13, color: "$hint", style: { textAlign: 'left' }, children: c.note })] }), c.id === value ? _jsx(Check, { size: 16, color: "var(--primary)", mt: 2, "aria-hidden": true }) : null] }, c.id)))] }));
|
|
74
|
+
}
|
|
75
|
+
/** A word in the row that opens a panel of choices. */
|
|
76
|
+
function Opens({ label, word, dim, children }) {
|
|
77
|
+
const [open, setOpen] = useState(false);
|
|
78
|
+
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)) })] }));
|
|
79
|
+
}
|
|
80
|
+
/** The model's name, and the catalog behind it: Free's lane to pick from, the rest an upgrade. */
|
|
81
|
+
function Model({ mind, onUpgrade }) {
|
|
68
82
|
const [open, setOpen] = useState(false);
|
|
69
|
-
const
|
|
70
|
-
const
|
|
71
|
-
|
|
83
|
+
const [q, setQ] = useState('');
|
|
84
|
+
const groups = useMemo(() => {
|
|
85
|
+
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);
|
|
87
|
+
}, [mind.models, q]);
|
|
72
88
|
const routed = mind.model === ENSO;
|
|
73
|
-
return (_jsxs(
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
mind.pick(ENSO);
|
|
87
|
-
setOpen(false);
|
|
88
|
-
}, items: "center", gap: "$2", px: "$2", height: 28, rounded: "$3", self: "flex-start", borderWidth: 1, borderColor: "$borderColor", cursor: "pointer", hoverStyle: { bg: '$hover' }, children: [_jsx(Ring, {}), _jsx(SizableText, { size: "$1", color: "$ink", children: "Use Enso" })] })), _jsx(ModelPicker, { size: "sm", name: "Model", models: mind.models, limits: limits,
|
|
89
|
-
// The router is named on the chip; a catalog that does not list it leaves the picker open.
|
|
90
|
-
value: routed && !mind.models.some((m) => m.id === ENSO) ? undefined : mind.model, placeholder: "Pick a model", onChange: (id) => {
|
|
91
|
-
mind.pick(id);
|
|
92
|
-
setOpen(false);
|
|
93
|
-
}, loading: mind.loading, error: mind.error })] })] }) })] }));
|
|
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)))] })] }));
|
|
93
|
+
}
|
|
94
|
+
/** The row under the frame: where (Dev), then the model, the effort and the mode. */
|
|
95
|
+
export function Tune({ mind, mode, where, onUpgrade }) {
|
|
96
|
+
const thinks = mind ? reasons(mind.model, mind.models) : false;
|
|
97
|
+
const pace = mind ? EFFORTS.find((e) => e.id === mind.effort) : null;
|
|
98
|
+
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) => {
|
|
99
|
+
mind.pace(e);
|
|
100
|
+
close();
|
|
101
|
+
} })) })) : 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] })] }));
|
|
94
102
|
}
|
|
95
|
-
/** The
|
|
96
|
-
function
|
|
97
|
-
|
|
103
|
+
/** The `/` or `@` the caret ends on, and the words typed after it. */
|
|
104
|
+
export function trigger(value) {
|
|
105
|
+
const m = /(^|\s)([/@])([^\s/@]*)$/.exec(value);
|
|
106
|
+
return m ? { char: m[2], query: m[3].toLowerCase(), at: m.index + m[1].length } : null;
|
|
107
|
+
}
|
|
108
|
+
/** The list `/` or `@` opens above the frame. */
|
|
109
|
+
function Mention({ shelf, rows, index, onPick, tag, name }) {
|
|
110
|
+
return (_jsxs(YStack, { role: "listbox", "aria-label": name, "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 ?? `Type to search ${name.toLowerCase()}` }), 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, children: r.label }), _jsx(SizableText, { fontSize: 13, color: "$hint", children: r.on ? 'On' : tag })] }, r.id)))) : (_jsx(SizableText, { fontSize: 14, color: "$hint", px: 8, py: 8, children: shelf.loading ? 'Reading…' : 'Nothing matches' }))] }));
|
|
98
111
|
}
|
|
99
112
|
/** @hanzo/ui `Composer` props for the one composer. */
|
|
100
|
-
export function prompt(o) {
|
|
113
|
+
export function prompt(o, state) {
|
|
101
114
|
const lit = o.ready ?? Boolean(o.value.trim());
|
|
102
115
|
const busy = Boolean(o.busy);
|
|
103
116
|
const disabled = Boolean(o.disabled);
|
|
104
117
|
const live = busy ? Boolean(o.onStop) : lit && !disabled;
|
|
105
|
-
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
const
|
|
109
|
-
const
|
|
118
|
+
const narrow = Boolean(o.narrow);
|
|
119
|
+
// `/` and `@`: the shelf the caret's word opens, filtered by what follows it.
|
|
120
|
+
const typed = trigger(o.value);
|
|
121
|
+
const shelf = typed ? (typed.char === '/' ? o.slash : o.at) : undefined;
|
|
122
|
+
const listed = shelf && typed && state?.closed !== o.value ? shelf.rows.filter((r) => r.label.toLowerCase().includes(typed.query)).slice(0, 8) : null;
|
|
123
|
+
const index = Math.min(state?.index ?? 0, Math.max(0, (listed?.length ?? 1) - 1));
|
|
124
|
+
const choose = (id) => {
|
|
125
|
+
if (!typed || !shelf)
|
|
126
|
+
return;
|
|
127
|
+
o.onChange(o.value.slice(0, typed.at).trimEnd() + (typed.at > 0 ? ' ' : ''));
|
|
128
|
+
shelf.pick(id);
|
|
129
|
+
};
|
|
130
|
+
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
|
+
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
|
+
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: () => {
|
|
134
|
+
if (!live)
|
|
135
|
+
return;
|
|
136
|
+
if (busy)
|
|
137
|
+
o.onStop?.();
|
|
138
|
+
else
|
|
139
|
+
o.onSend();
|
|
140
|
+
}, children: busy ? _jsx(Square, { size: 13, color: ON, fill: ON }) : _jsx(ArrowUp, { size: 16, strokeWidth: 2.5, color: ON }) }));
|
|
110
141
|
return {
|
|
111
142
|
value: o.value,
|
|
112
143
|
onChange: o.onChange,
|
|
113
|
-
onSend:
|
|
144
|
+
onSend: () => {
|
|
145
|
+
if (listed?.length)
|
|
146
|
+
return choose(listed[index].id);
|
|
147
|
+
o.onSend();
|
|
148
|
+
},
|
|
114
149
|
...(o.onStop ? { onStop: o.onStop } : {}),
|
|
115
150
|
busy,
|
|
116
151
|
disabled,
|
|
117
|
-
|
|
152
|
+
placeholder: o.placeholder ?? ASK,
|
|
118
153
|
...(o.label ? { label: o.label } : {}),
|
|
119
|
-
|
|
120
|
-
// inline-style: flex `order`, which no gui prop carries; the field keeps its node either way.
|
|
121
|
-
// An empty field keeps its placeholder to the line, cut short rather than wrapped inside the pill.
|
|
154
|
+
rows: 2,
|
|
122
155
|
field: {
|
|
123
156
|
...o.field,
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
157
|
+
fontSize: 16,
|
|
158
|
+
lineHeight: 24,
|
|
159
|
+
px: 8,
|
|
160
|
+
pt: 2,
|
|
161
|
+
minH: 48,
|
|
162
|
+
bg: 'transparent',
|
|
128
163
|
},
|
|
129
|
-
// Above the pill and at its width, so a chip lines up with the field's edge.
|
|
130
164
|
head,
|
|
131
|
-
|
|
165
|
+
foot,
|
|
132
166
|
...measure,
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
gap: 4,
|
|
138
|
-
items: tall ? 'flex-end' : 'center',
|
|
139
|
-
flexWrap: tall ? 'wrap' : 'nowrap',
|
|
140
|
-
'$max-sm': { flexWrap: 'wrap', rounded: CORNER },
|
|
167
|
+
rounded: CORNER,
|
|
168
|
+
p: 8,
|
|
169
|
+
pt: 12,
|
|
170
|
+
gap: 6,
|
|
141
171
|
borderColor: '$borderColor',
|
|
142
172
|
bg: '$edge',
|
|
173
|
+
'$max-sm': { rounded: CORNER_PHONE },
|
|
143
174
|
...(o.framed ? { boxShadow: 'var(--shadow-sheet-1)' } : null),
|
|
144
|
-
children: (_jsxs(XStack, { items: "center", gap:
|
|
145
|
-
send: (_jsxs(XStack, { items: "center", gap:
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
175
|
+
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)] })),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
/** Ctrl+U (⌘U) uploads, while a composer with an upload is on the page. */
|
|
180
|
+
export function useUpload(upload) {
|
|
181
|
+
const now = useRef(upload);
|
|
182
|
+
now.current = upload;
|
|
183
|
+
useEffect(() => {
|
|
184
|
+
if (!upload)
|
|
185
|
+
return;
|
|
186
|
+
const key = (e) => {
|
|
187
|
+
if ((e.ctrlKey || e.metaKey) && !e.altKey && !e.shiftKey && e.key.toLowerCase() === 'u') {
|
|
188
|
+
e.preventDefault();
|
|
189
|
+
now.current?.();
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
window.addEventListener('keydown', key);
|
|
193
|
+
return () => window.removeEventListener('keydown', key);
|
|
194
|
+
}, [Boolean(upload)]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* The composer's own state — which row of a `/` or `@` list is lit — and the
|
|
198
|
+
* keys that move it. A surface that hands `prompt` to `Chat` calls this and
|
|
199
|
+
* passes its answer through; `<Prompt>` does it for itself.
|
|
200
|
+
*/
|
|
201
|
+
export function useMention(o) {
|
|
202
|
+
const [index, setIndex] = useState(0);
|
|
203
|
+
const [closed, setClosed] = useState(null);
|
|
204
|
+
useUpload(o.plus?.upload);
|
|
205
|
+
const typed = trigger(o.value);
|
|
206
|
+
const shelf = typed ? (typed.char === '/' ? o.slash : o.at) : undefined;
|
|
207
|
+
const open = Boolean(shelf && closed !== o.value);
|
|
208
|
+
useEffect(() => setIndex(0), [typed?.char, typed?.query]);
|
|
209
|
+
const field = {
|
|
210
|
+
...o.field,
|
|
211
|
+
onKeyDown: (e) => {
|
|
212
|
+
const key = e.key ?? e.nativeEvent?.key;
|
|
213
|
+
if (open && key === 'ArrowDown') {
|
|
214
|
+
e.preventDefault?.();
|
|
215
|
+
setIndex((i) => i + 1);
|
|
216
|
+
}
|
|
217
|
+
else if (open && key === 'ArrowUp') {
|
|
218
|
+
e.preventDefault?.();
|
|
219
|
+
setIndex((i) => Math.max(0, i - 1));
|
|
220
|
+
}
|
|
221
|
+
else if (open && key === 'Escape') {
|
|
222
|
+
e.preventDefault?.();
|
|
223
|
+
setClosed(o.value);
|
|
224
|
+
}
|
|
225
|
+
else
|
|
226
|
+
o.field?.onKeyDown?.(e);
|
|
227
|
+
},
|
|
153
228
|
};
|
|
229
|
+
return { state: { index, closed }, field };
|
|
154
230
|
}
|
|
155
231
|
/** The composer drawn on its own. */
|
|
156
232
|
export function Prompt(o) {
|
|
157
233
|
const media = useMedia();
|
|
158
|
-
|
|
234
|
+
const { state, field } = useMention(o);
|
|
235
|
+
return _jsx(Composer, { ...prompt({ ...o, field, narrow: o.narrow ?? !media.sm }, state) });
|
|
159
236
|
}
|