@kanzo-tech/ai 0.24.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assist.d.ts +6 -0
- package/dist/assist.d.ts.map +1 -1
- package/dist/assist.js +166 -165
- package/dist/assist.js.map +1 -1
- package/dist/proposals.d.ts +2 -0
- package/dist/proposals.d.ts.map +1 -1
- package/dist/proposals.js +6 -4
- package/dist/proposals.js.map +1 -1
- package/package.json +5 -5
package/dist/assist.d.ts
CHANGED
|
@@ -61,6 +61,12 @@ export interface AssistProps<V extends string | string[]> {
|
|
|
61
61
|
onValueChange: (value: V) => void;
|
|
62
62
|
/** One sentence for the model about this field, beyond its label and description. */
|
|
63
63
|
instructions?: string;
|
|
64
|
+
/**
|
|
65
|
+
* What the host knows about this field that its label and description do not say — the values it
|
|
66
|
+
* accepts, its limits, what its neighbours hold. Facts rather than an instruction, so as many lines
|
|
67
|
+
* as it takes; a function is read when the field asks, so it sees the form as it is then.
|
|
68
|
+
*/
|
|
69
|
+
context?: string | (() => string | undefined);
|
|
64
70
|
/** The control, from `@kanzo-tech/ui`: `Textarea`, `Input` or `TagsInput`. `Assist` wires it. */
|
|
65
71
|
children: React.ReactElement;
|
|
66
72
|
className?: string;
|
package/dist/assist.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"assist.d.ts","sourceRoot":"","sources":["../src/assist.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAGrD,OAAO,EACL,KAAK,WAAW,EAKjB,MAAM,aAAa,CAAC;AAKrB,0GAA0G;AAC1G,MAAM,WAAW,kBAAkB;IACjC,qBAAqB;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,qEAAqE;IACrE,MAAM,EAAE,MAAM,CAAC;IACf,yEAAyE;IACzE,MAAM,EAAE,MAAM,CAAC;IACf,+EAA+E;IAC/E,MAAM,EAAE,MAAM,CAAC;IACf,kDAAkD;IAClD,SAAS,EAAE,MAAM,CAAC;IAClB,4BAA4B;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,sBAAsB;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,gEAAgE;IAChE,YAAY,EAAE,MAAM,CAAC;IACrB,yCAAyC;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,2CAA2C;IAC3C,KAAK,EAAE,MAAM,CAAC;IACd,4EAA4E;IAC5E,MAAM,EAAE,MAAM,CAAC;IACf,mDAAmD;IACnD,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;CACnC;AA6BD,MAAM,WAAW,mBAAmB;IAClC,yEAAyE;IACzE,KAAK,EAAE,aAAa,CAAC;IACrB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,GAAG,SAAS,CAAC,CAAC;IAC9C,kFAAkF;IAClF,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,CAAC;IACvC;;;;OAIG;IACH,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IACrC,YAAY,CAAC,EAAE,OAAO,CAAC,kBAAkB,CAAC,CAAC;IAC3C,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,mBAAmB,qBAaxD;AA4FD,MAAM,WAAW,WAAW,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,EAAE;IACtD;;;OAGG;IACH,KAAK,EAAE,CAAC,CAAC;IACT,aAAa,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,CAAC;IAClC,qFAAqF;IACrF,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iGAAiG;IACjG,QAAQ,EAAE,KAAK,CAAC,YAAY,CAAC;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;GASG;AACH,wBAAgB,MAAM,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,EAAE,EAAE,KAAK,EAAE,WAAW,CAAC,CAAC,CAAC,qBAOxE"}
|
|
1
|
+
{"version":3,"file":"assist.d.ts","sourceRoot":"","sources":["../src/assist.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAGrD,OAAO,EACL,KAAK,WAAW,EAKjB,MAAM,aAAa,CAAC;AAKrB,0GAA0G;AAC1G,MAAM,WAAW,kBAAkB;IACjC,qBAAqB;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,qEAAqE;IACrE,MAAM,EAAE,MAAM,CAAC;IACf,yEAAyE;IACzE,MAAM,EAAE,MAAM,CAAC;IACf,+EAA+E;IAC/E,MAAM,EAAE,MAAM,CAAC;IACf,kDAAkD;IAClD,SAAS,EAAE,MAAM,CAAC;IAClB,4BAA4B;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,sBAAsB;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,gEAAgE;IAChE,YAAY,EAAE,MAAM,CAAC;IACrB,yCAAyC;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,2CAA2C;IAC3C,KAAK,EAAE,MAAM,CAAC;IACd,4EAA4E;IAC5E,MAAM,EAAE,MAAM,CAAC;IACf,mDAAmD;IACnD,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;CACnC;AA6BD,MAAM,WAAW,mBAAmB;IAClC,yEAAyE;IACzE,KAAK,EAAE,aAAa,CAAC;IACrB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,GAAG,SAAS,CAAC,CAAC;IAC9C,kFAAkF;IAClF,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,CAAC;IACvC;;;;OAIG;IACH,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IACrC,YAAY,CAAC,EAAE,OAAO,CAAC,kBAAkB,CAAC,CAAC;IAC3C,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,mBAAmB,qBAaxD;AA4FD,MAAM,WAAW,WAAW,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,EAAE;IACtD;;;OAGG;IACH,KAAK,EAAE,CAAC,CAAC;IACT,aAAa,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,CAAC;IAClC,qFAAqF;IACrF,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,GAAG,SAAS,CAAC,CAAC;IAC9C,iGAAiG;IACjG,QAAQ,EAAE,KAAK,CAAC,YAAY,CAAC;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;GASG;AACH,wBAAgB,MAAM,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,EAAE,EAAE,KAAK,EAAE,WAAW,CAAC,CAAC,CAAC,qBAOxE"}
|
package/dist/assist.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx as c, jsxs as E, Fragment as
|
|
2
|
+
import { jsx as c, jsxs as E, Fragment as be } from "react/jsx-runtime";
|
|
3
3
|
import { ark as R } from "@ark-ui/react/factory";
|
|
4
|
-
import { Undo2Icon as
|
|
4
|
+
import { Undo2Icon as ve, XIcon as xe } from "lucide-react";
|
|
5
5
|
import * as l from "react";
|
|
6
|
-
import { cn as M, Kbd as
|
|
7
|
-
import { AiMark as
|
|
8
|
-
import { useContinuation as
|
|
9
|
-
import { continuation as
|
|
10
|
-
const
|
|
6
|
+
import { cn as M, Kbd as U, ButtonGroup as ke, Suggestion as Ee, Button as Se, Spinner as Ce, Suggestions as Re } from "@kanzo-tech/ui";
|
|
7
|
+
import { AiMark as G } from "./ai-mark.js";
|
|
8
|
+
import { useContinuation as we, useCandidates as Z, MIN_CONTINUE_LENGTH as Ne } from "./engine.js";
|
|
9
|
+
import { continuation as Ae, candidates as ee } from "./proposals.js";
|
|
10
|
+
const Be = {
|
|
11
11
|
assist: "AI assist",
|
|
12
12
|
accept: "Accept suggestion",
|
|
13
13
|
others: "Suggest different values",
|
|
@@ -20,31 +20,31 @@ const Ne = {
|
|
|
20
20
|
empty: "Nothing to suggest.",
|
|
21
21
|
failed: "Couldn’t suggest anything.",
|
|
22
22
|
dismiss: (n) => `Dismiss ${n}`
|
|
23
|
-
},
|
|
24
|
-
function
|
|
25
|
-
const { model: t, context: s, onEvent: r, onFailure: o, translations:
|
|
23
|
+
}, te = l.createContext(null);
|
|
24
|
+
function qe(n) {
|
|
25
|
+
const { model: t, context: s, onEvent: r, onFailure: o, translations: m, children: y } = n, d = l.useMemo(
|
|
26
26
|
() => ({
|
|
27
27
|
model: t,
|
|
28
28
|
context: typeof s == "function" ? s : () => s,
|
|
29
29
|
onEvent: r,
|
|
30
30
|
onFailure: o,
|
|
31
|
-
t: { ...
|
|
31
|
+
t: { ...Be, ...m }
|
|
32
32
|
}),
|
|
33
|
-
[s, t, r, o,
|
|
33
|
+
[s, t, r, o, m]
|
|
34
34
|
);
|
|
35
|
-
return /* @__PURE__ */ c(
|
|
35
|
+
return /* @__PURE__ */ c(te.Provider, { value: d, children: y });
|
|
36
36
|
}
|
|
37
|
-
const
|
|
37
|
+
const Y = (n) => (n ?? "").split(/\s+/).map((t) => {
|
|
38
38
|
var s, r;
|
|
39
39
|
return t ? (r = (s = document.getElementById(t)) == null ? void 0 : s.textContent) == null ? void 0 : r.trim() : "";
|
|
40
40
|
}).filter(Boolean).join(" ");
|
|
41
|
-
function
|
|
42
|
-
return n ? { name:
|
|
43
|
-
var
|
|
44
|
-
return (
|
|
45
|
-
}).filter(Boolean).join(" ") || n.getAttribute("aria-label") || n.getAttribute("placeholder") || "", description:
|
|
41
|
+
function _(n, t) {
|
|
42
|
+
return n ? { name: Y(n.getAttribute("aria-labelledby")) || [...n.labels ?? []].map((r) => {
|
|
43
|
+
var o;
|
|
44
|
+
return (o = r.textContent) == null ? void 0 : o.trim();
|
|
45
|
+
}).filter(Boolean).join(" ") || n.getAttribute("aria-label") || n.getAttribute("placeholder") || "", description: Y(n.getAttribute("aria-describedby")) || void 0, ...t } : { name: "", ...t };
|
|
46
46
|
}
|
|
47
|
-
const
|
|
47
|
+
const Ie = [
|
|
48
48
|
"fontFamily",
|
|
49
49
|
"fontSize",
|
|
50
50
|
"fontWeight",
|
|
@@ -60,8 +60,8 @@ const Ae = [
|
|
|
60
60
|
"borderRightWidth",
|
|
61
61
|
"borderBottomWidth",
|
|
62
62
|
"borderLeftWidth"
|
|
63
|
-
],
|
|
64
|
-
function
|
|
63
|
+
], V = (n) => Number.parseFloat(n) || 0;
|
|
64
|
+
function $(n, t, s) {
|
|
65
65
|
const r = n.getBoundingClientRect(), o = t.getBoundingClientRect();
|
|
66
66
|
return {
|
|
67
67
|
insetBlockStart: r.top - o.top,
|
|
@@ -70,109 +70,113 @@ function _(n, t, s) {
|
|
|
70
70
|
insetInlineEnd: s ? r.left - o.left : o.right - r.right
|
|
71
71
|
};
|
|
72
72
|
}
|
|
73
|
-
function
|
|
74
|
-
const s = getComputedStyle(n), r = s.direction === "rtl", o = n.offsetWidth - n.clientWidth -
|
|
75
|
-
...
|
|
76
|
-
insetInlineEnd:
|
|
73
|
+
function Te(n, t) {
|
|
74
|
+
const s = getComputedStyle(n), r = s.direction === "rtl", o = n.offsetWidth - n.clientWidth - V(s.borderLeftWidth) - V(s.borderRightWidth), m = $(n, t, r), y = {
|
|
75
|
+
...m,
|
|
76
|
+
insetInlineEnd: m.insetInlineEnd + (o > 0 ? o : 0),
|
|
77
77
|
borderStyle: "solid",
|
|
78
78
|
borderColor: "transparent"
|
|
79
79
|
};
|
|
80
|
-
for (const
|
|
81
|
-
return { metrics:
|
|
80
|
+
for (const d of Ie) y[d] = s[d];
|
|
81
|
+
return { metrics: y, rtl: r };
|
|
82
82
|
}
|
|
83
|
-
const
|
|
83
|
+
const ne = "2.25rem", Le = (n) => {
|
|
84
84
|
var t;
|
|
85
85
|
return ((t = /^\s*\S+/.exec(n)) == null ? void 0 : t[0]) ?? "";
|
|
86
86
|
}, I = (...n) => (t) => {
|
|
87
87
|
for (const s of n) s == null || s(t);
|
|
88
|
-
},
|
|
88
|
+
}, Ke = (...n) => (t) => {
|
|
89
89
|
for (const s of n)
|
|
90
90
|
typeof s == "function" ? s(t) : s && (s.current = t);
|
|
91
91
|
};
|
|
92
|
-
function
|
|
93
|
-
const t = l.useContext(
|
|
92
|
+
function _e(n) {
|
|
93
|
+
const t = l.useContext(te);
|
|
94
94
|
if (!t) throw new Error("<Assist> must render inside <AssistProvider>");
|
|
95
|
-
return Array.isArray(n.value) ? /* @__PURE__ */ c(
|
|
95
|
+
return Array.isArray(n.value) ? /* @__PURE__ */ c(He, { ...n, settings: t }) : /* @__PURE__ */ c(Me, { ...n, settings: t });
|
|
96
96
|
}
|
|
97
|
-
const
|
|
98
|
-
|
|
97
|
+
const se = (n, t, s) => {
|
|
98
|
+
var r;
|
|
99
|
+
return {
|
|
100
|
+
instructions: n,
|
|
101
|
+
fieldContext: typeof t == "function" ? t() : t,
|
|
102
|
+
context: (r = s.context) == null ? void 0 : r.call(s)
|
|
103
|
+
};
|
|
104
|
+
}, We = (n) => (n == null ? void 0 : n.querySelector("textarea, input:not([type=hidden])")) ?? null;
|
|
105
|
+
function re(n, t) {
|
|
99
106
|
return l.useCallback(
|
|
100
107
|
(s, r) => {
|
|
101
108
|
var o;
|
|
102
|
-
return (o = n.onEvent) == null ? void 0 : o.call(n, { kind: s, proposal: r, field:
|
|
109
|
+
return (o = n.onEvent) == null ? void 0 : o.call(n, { kind: s, proposal: r, field: _(t(), {}).name });
|
|
103
110
|
},
|
|
104
111
|
[t, n]
|
|
105
112
|
);
|
|
106
113
|
}
|
|
107
|
-
function
|
|
108
|
-
const { value: t, onValueChange: s, instructions: r,
|
|
114
|
+
function Me(n) {
|
|
115
|
+
const { value: t, onValueChange: s, instructions: r, context: o, children: m, className: y, settings: d } = n, { t: a } = d, p = l.useRef(null), f = l.useRef(null), [g, P] = l.useState(!1), [w, O] = l.useState(
|
|
109
116
|
{ metrics: {}, rtl: !1, mark: {} }
|
|
110
|
-
), [
|
|
111
|
-
|
|
112
|
-
return q(m.current, r, (e = a.context) == null ? void 0 : e.call(a));
|
|
113
|
-
}, p = Ce({
|
|
114
|
-
source: (e) => we(a.model, L(), e),
|
|
117
|
+
), [b, v] = l.useState(t.length), [H, D] = l.useState(!1), [N, S] = l.useState(null), u = l.useRef(null), C = l.useCallback(() => f.current, []), T = re(d, C), L = () => _(f.current, se(r, o, d)), h = we({
|
|
118
|
+
source: (e) => Ae(d.model, L(), e),
|
|
115
119
|
onEvent: T
|
|
116
|
-
}), x =
|
|
117
|
-
source: ({ signal: e }) =>
|
|
120
|
+
}), x = Z({
|
|
121
|
+
source: ({ signal: e }) => ee(d.model, L(), { value: t, existing: [t], signal: e, count: 6, list: !1 }),
|
|
118
122
|
existing: [t],
|
|
119
123
|
onEvent: T
|
|
120
124
|
});
|
|
121
|
-
|
|
122
|
-
const k = g ?
|
|
125
|
+
q(d, h), q(d, x);
|
|
126
|
+
const k = g ? h.ghost : "";
|
|
123
127
|
l.useLayoutEffect(() => {
|
|
124
|
-
const e =
|
|
128
|
+
const e = f.current, i = p.current;
|
|
125
129
|
if (!e || !i) return;
|
|
126
|
-
|
|
130
|
+
P(e instanceof HTMLTextAreaElement), e.style.paddingInlineEnd = ne;
|
|
127
131
|
const W = () => {
|
|
128
|
-
const
|
|
129
|
-
|
|
132
|
+
const Q = Te(e, i), F = $(e, i, Q.rtl), ye = e instanceof HTMLTextAreaElement ? { insetInlineEnd: F.insetInlineEnd + 4, insetBlockEnd: F.insetBlockEnd + 4 } : { insetInlineEnd: F.insetInlineEnd + 2, insetBlockStart: F.insetBlockStart, height: e.offsetHeight };
|
|
133
|
+
O({ ...Q, mark: ye });
|
|
130
134
|
};
|
|
131
135
|
if (W(), typeof ResizeObserver > "u") return;
|
|
132
|
-
const
|
|
133
|
-
return
|
|
136
|
+
const j = new ResizeObserver(W);
|
|
137
|
+
return j.observe(e), j.observe(i), () => j.disconnect();
|
|
134
138
|
}, []), l.useLayoutEffect(() => {
|
|
135
|
-
const e =
|
|
139
|
+
const e = f.current, i = u.current;
|
|
136
140
|
!e || i === null || (u.current = null, e.setSelectionRange(i, i), v(i));
|
|
137
141
|
}, [t]);
|
|
138
142
|
const K = l.useCallback(() => {
|
|
139
|
-
const e =
|
|
143
|
+
const e = f.current;
|
|
140
144
|
if (!e || u.current !== null) return;
|
|
141
145
|
const i = e.selectionStart ?? e.value.length;
|
|
142
146
|
if ((e.selectionEnd ?? i) !== i) {
|
|
143
|
-
|
|
147
|
+
h.dismiss("ignored");
|
|
144
148
|
return;
|
|
145
149
|
}
|
|
146
150
|
v(i);
|
|
147
|
-
}, [
|
|
151
|
+
}, [h]);
|
|
148
152
|
l.useEffect(() => {
|
|
149
153
|
const e = () => {
|
|
150
|
-
document.activeElement ===
|
|
154
|
+
document.activeElement === f.current && K();
|
|
151
155
|
};
|
|
152
156
|
return document.addEventListener("selectionchange", e), () => document.removeEventListener("selectionchange", e);
|
|
153
157
|
}, [K]);
|
|
154
|
-
const
|
|
158
|
+
const z = (e, i) => {
|
|
155
159
|
u.current = i, s(e);
|
|
156
|
-
},
|
|
160
|
+
}, X = () => {
|
|
157
161
|
var e;
|
|
158
|
-
k && (S(t),
|
|
159
|
-
}, oe = () => {
|
|
160
|
-
const e = Ie(k);
|
|
161
|
-
if (!e) return;
|
|
162
|
-
const i = t.slice(0, y) + e + t.slice(y);
|
|
163
|
-
D(i, y + e.length), p.settle("partial", e), p.setValue(i, y + e.length);
|
|
164
|
-
}, ie = (e) => {
|
|
165
|
-
S(t), D(e, e.length), x.take(e);
|
|
162
|
+
k && (S(t), z(t.slice(0, b) + k + t.slice(b), b + k.length), h.settle("accepted", k), (e = f.current) == null || e.focus());
|
|
166
163
|
}, ce = () => {
|
|
164
|
+
const e = Le(k);
|
|
165
|
+
if (!e) return;
|
|
166
|
+
const i = t.slice(0, b) + e + t.slice(b);
|
|
167
|
+
z(i, b + e.length), h.settle("partial", e), h.setValue(i, b + e.length);
|
|
168
|
+
}, le = (e) => {
|
|
169
|
+
S(t), z(e, e.length), x.take(e);
|
|
170
|
+
}, ae = () => {
|
|
167
171
|
var e;
|
|
168
|
-
N !== null && (
|
|
169
|
-
},
|
|
170
|
-
const e =
|
|
171
|
-
v(i),
|
|
172
|
-
},
|
|
172
|
+
N !== null && (z(N, N.length), S(null), (e = f.current) == null || e.focus());
|
|
173
|
+
}, ue = () => {
|
|
174
|
+
const e = f.current, i = e && e === document.activeElement ? e.selectionStart ?? b : b;
|
|
175
|
+
v(i), h.ask(t, i), e == null || e.focus(), e == null || e.setSelectionRange(i, i);
|
|
176
|
+
}, de = (e) => {
|
|
173
177
|
const i = e.target, W = i.selectionStart ?? i.value.length;
|
|
174
|
-
v(W), S(null), s(i.value), g &&
|
|
175
|
-
},
|
|
178
|
+
v(W), S(null), s(i.value), g && h.setValue(i.value, W);
|
|
179
|
+
}, fe = (e) => {
|
|
176
180
|
if (e.defaultPrevented) return;
|
|
177
181
|
if (!g) {
|
|
178
182
|
e.key === "Escape" && x.items.length > 0 && x.cancel();
|
|
@@ -180,80 +184,80 @@ function Ke(n) {
|
|
|
180
184
|
}
|
|
181
185
|
const i = w.rtl ? "ArrowLeft" : "ArrowRight";
|
|
182
186
|
if (e.altKey && (e.key === "]" || e.key === "[" || e.code === "BracketRight" || e.code === "BracketLeft")) {
|
|
183
|
-
e.preventDefault(),
|
|
187
|
+
e.preventDefault(), h.cycle(e.key === "]" || e.code === "BracketRight" ? 1 : -1);
|
|
184
188
|
return;
|
|
185
189
|
}
|
|
186
|
-
k && (e.key === "Tab" ? (e.preventDefault(),
|
|
187
|
-
},
|
|
190
|
+
k && (e.key === "Tab" ? (e.preventDefault(), X()) : e.key === i && (e.ctrlKey || e.metaKey) ? (e.preventDefault(), ce()) : e.key === "Escape" && h.dismiss("rejected"));
|
|
191
|
+
}, J = m, A = J.props, me = l.cloneElement(J, {
|
|
188
192
|
value: t,
|
|
189
|
-
onChange: I(A.onChange,
|
|
190
|
-
onKeyDown: I(A.onKeyDown,
|
|
193
|
+
onChange: I(A.onChange, de),
|
|
194
|
+
onKeyDown: I(A.onKeyDown, fe),
|
|
191
195
|
onClick: I(A.onClick, K),
|
|
192
196
|
onKeyUp: I(A.onKeyUp, K),
|
|
193
197
|
onSelect: I(A.onSelect, K),
|
|
194
|
-
ref:
|
|
198
|
+
ref: Ke(A.ref, f),
|
|
195
199
|
"aria-keyshortcuts": k ? "Tab Escape Alt+]" : void 0
|
|
196
|
-
}), B = g ? k.length > 0 : x.items.length > 0,
|
|
200
|
+
}), B = g ? k.length > 0 : x.items.length > 0, ge = (g ? h.status : x.status) === "loading" && !B, he = N !== null && !B, pe = !g || t.trim().length >= Ne;
|
|
197
201
|
return /* @__PURE__ */ E(
|
|
198
202
|
R.div,
|
|
199
203
|
{
|
|
200
|
-
className: M("relative flex w-full min-w-0 flex-col gap-2",
|
|
204
|
+
className: M("relative flex w-full min-w-0 flex-col gap-2", y),
|
|
201
205
|
"data-slot": "assist",
|
|
202
206
|
onBlur: (e) => {
|
|
203
|
-
e.currentTarget.contains(e.relatedTarget) ||
|
|
207
|
+
e.currentTarget.contains(e.relatedTarget) || D(!1);
|
|
204
208
|
},
|
|
205
|
-
onFocus: () =>
|
|
206
|
-
ref:
|
|
209
|
+
onFocus: () => D(!0),
|
|
210
|
+
ref: p,
|
|
207
211
|
children: [
|
|
208
|
-
|
|
209
|
-
g && /* @__PURE__ */ c(
|
|
212
|
+
me,
|
|
213
|
+
g && /* @__PURE__ */ c(Pe, { caret: b, field: f, ghost: k, metrics: w.metrics, value: t }),
|
|
210
214
|
/* @__PURE__ */ c(
|
|
211
215
|
R.span,
|
|
212
216
|
{
|
|
213
217
|
className: M("absolute flex items-center", w.mark.insetInlineEnd === void 0 && "invisible"),
|
|
214
218
|
"data-slot": "assist-mark",
|
|
215
219
|
style: w.mark,
|
|
216
|
-
children:
|
|
217
|
-
|
|
220
|
+
children: he ? /* @__PURE__ */ c(G, { label: a.revert, onClick: ae, children: /* @__PURE__ */ c(ve, {}) }) : /* @__PURE__ */ c(
|
|
221
|
+
G,
|
|
218
222
|
{
|
|
219
|
-
busy:
|
|
220
|
-
disabled: !B && !
|
|
221
|
-
label: B ? g ?
|
|
223
|
+
busy: ge,
|
|
224
|
+
disabled: !B && !pe,
|
|
225
|
+
label: B ? g ? a.accept : a.others : a.assist,
|
|
222
226
|
offering: B,
|
|
223
|
-
onClick: g ? B ?
|
|
227
|
+
onClick: g ? B ? X : ue : x.press
|
|
224
228
|
}
|
|
225
229
|
)
|
|
226
230
|
}
|
|
227
231
|
),
|
|
228
|
-
g ? /* @__PURE__ */ E(
|
|
229
|
-
k && /* @__PURE__ */ c(
|
|
230
|
-
/* @__PURE__ */ c("span", { "aria-live": "polite", className: "sr-only", "data-slot": "assist-status", children: k ?
|
|
231
|
-
/* @__PURE__ */ c(
|
|
232
|
-
] }) : H && /* @__PURE__ */ c(
|
|
232
|
+
g ? /* @__PURE__ */ E(be, { children: [
|
|
233
|
+
k && /* @__PURE__ */ c(Oe, { t: a }),
|
|
234
|
+
/* @__PURE__ */ c("span", { "aria-live": "polite", className: "sr-only", "data-slot": "assist-status", children: k ? a.announcement : "" }),
|
|
235
|
+
/* @__PURE__ */ c(oe, { error: h.error, status: h.status, t: a })
|
|
236
|
+
] }) : H && /* @__PURE__ */ c(ie, { list: x, onPick: le, t: a })
|
|
233
237
|
]
|
|
234
238
|
}
|
|
235
239
|
);
|
|
236
240
|
}
|
|
237
|
-
function
|
|
238
|
-
const { field: t, ghost: s, value: r, caret: o, metrics:
|
|
241
|
+
function Pe(n) {
|
|
242
|
+
const { field: t, ghost: s, value: r, caret: o, metrics: m } = n, y = l.useRef(null);
|
|
239
243
|
l.useLayoutEffect(() => {
|
|
240
|
-
const
|
|
241
|
-
if (!
|
|
244
|
+
const a = t.current, p = y.current;
|
|
245
|
+
if (!a || !p) return;
|
|
242
246
|
if (!s) {
|
|
243
|
-
|
|
247
|
+
a.style.removeProperty("min-height");
|
|
244
248
|
return;
|
|
245
249
|
}
|
|
246
|
-
const
|
|
247
|
-
|
|
250
|
+
const f = getComputedStyle(a), g = p.scrollHeight + V(f.borderTopWidth) + V(f.borderBottomWidth);
|
|
251
|
+
g > a.getBoundingClientRect().height + 1 && a.style.setProperty("min-height", `${g}px`);
|
|
248
252
|
}, [t, s, r]), l.useLayoutEffect(() => {
|
|
249
|
-
const
|
|
250
|
-
if (!
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
+
const a = t.current, p = y.current;
|
|
254
|
+
if (!a || !p) return;
|
|
255
|
+
const f = () => {
|
|
256
|
+
p.scrollLeft = a.scrollLeft, p.scrollTop = a.scrollTop;
|
|
253
257
|
};
|
|
254
|
-
return
|
|
258
|
+
return f(), a.addEventListener("scroll", f), () => a.removeEventListener("scroll", f);
|
|
255
259
|
}, [o, t, s, r]);
|
|
256
|
-
const
|
|
260
|
+
const d = Math.min(o, r.length);
|
|
257
261
|
return /* @__PURE__ */ E(
|
|
258
262
|
R.div,
|
|
259
263
|
{
|
|
@@ -263,135 +267,132 @@ function We(n) {
|
|
|
263
267
|
!s && "invisible"
|
|
264
268
|
),
|
|
265
269
|
"data-slot": "assist-ghost",
|
|
266
|
-
ref:
|
|
267
|
-
style:
|
|
270
|
+
ref: y,
|
|
271
|
+
style: m,
|
|
268
272
|
children: [
|
|
269
|
-
/* @__PURE__ */ c("span", { className: "invisible", children: r.slice(0,
|
|
273
|
+
/* @__PURE__ */ c("span", { className: "invisible", children: r.slice(0, d) }),
|
|
270
274
|
/* @__PURE__ */ c("span", { className: "text-faint", children: s }),
|
|
271
|
-
/* @__PURE__ */ c("span", { className: "invisible", children: r.slice(
|
|
275
|
+
/* @__PURE__ */ c("span", { className: "invisible", children: r.slice(d) })
|
|
272
276
|
]
|
|
273
277
|
}
|
|
274
278
|
);
|
|
275
279
|
}
|
|
276
|
-
function
|
|
280
|
+
function Oe({ t: n }) {
|
|
277
281
|
return /* @__PURE__ */ E(R.span, { className: "inline-flex flex-wrap items-center gap-3 text-muted-foreground text-xs", "data-slot": "assist-keys", children: [
|
|
278
282
|
/* @__PURE__ */ E("span", { className: "inline-flex items-center gap-1 whitespace-nowrap", children: [
|
|
279
|
-
/* @__PURE__ */ c(
|
|
283
|
+
/* @__PURE__ */ c(U, { children: "Tab" }),
|
|
280
284
|
" ",
|
|
281
285
|
n.acceptKey
|
|
282
286
|
] }),
|
|
283
287
|
/* @__PURE__ */ E("span", { className: "inline-flex items-center gap-1 whitespace-nowrap", children: [
|
|
284
|
-
/* @__PURE__ */ c(
|
|
288
|
+
/* @__PURE__ */ c(U, { children: "Alt ]" }),
|
|
285
289
|
" ",
|
|
286
290
|
n.nextKey
|
|
287
291
|
] }),
|
|
288
292
|
/* @__PURE__ */ E("span", { className: "inline-flex items-center gap-1 whitespace-nowrap", children: [
|
|
289
|
-
/* @__PURE__ */ c(
|
|
293
|
+
/* @__PURE__ */ c(U, { children: "Esc" }),
|
|
290
294
|
" ",
|
|
291
295
|
n.dismissKey
|
|
292
296
|
] })
|
|
293
297
|
] });
|
|
294
298
|
}
|
|
295
|
-
function
|
|
299
|
+
function q(n, t) {
|
|
296
300
|
const { onFailure: s } = n, { status: r, error: o } = t;
|
|
297
301
|
l.useEffect(() => {
|
|
298
302
|
r === "error" && (s == null || s(o));
|
|
299
303
|
}, [r, o, s]);
|
|
300
304
|
}
|
|
301
|
-
function
|
|
305
|
+
function oe(n) {
|
|
302
306
|
const { error: t, status: s, t: r } = n;
|
|
303
307
|
return s !== "error" ? null : /* @__PURE__ */ c(R.p, { className: "text-destructive-foreground text-sm", "data-slot": "assist-error", role: "alert", children: t instanceof Error && t.message ? t.message : r.failed });
|
|
304
308
|
}
|
|
305
|
-
function
|
|
306
|
-
const { list: t, onPick: s, t: r } = n, o = t.status === "error" ? /* @__PURE__ */ c(
|
|
307
|
-
/* @__PURE__ */ c(
|
|
309
|
+
function ie(n) {
|
|
310
|
+
const { list: t, onPick: s, t: r } = n, o = t.status === "error" ? /* @__PURE__ */ c(oe, { error: t.error, status: t.status, t: r }) : t.items.length > 0 ? t.items.map((m) => /* @__PURE__ */ E(ke, { "aria-label": m.text, slot: "assist-candidate", children: [
|
|
311
|
+
/* @__PURE__ */ c(Ee, { onSelect: s, title: m.rationale, value: m.text, children: m.text }),
|
|
308
312
|
/* @__PURE__ */ c(
|
|
309
|
-
|
|
313
|
+
Se,
|
|
310
314
|
{
|
|
311
|
-
"aria-label": r.dismiss(
|
|
315
|
+
"aria-label": r.dismiss(m.text),
|
|
312
316
|
className: "h-auto min-h-[24px] self-stretch px-2 py-1",
|
|
313
|
-
onClick: () => t.dismiss(
|
|
317
|
+
onClick: () => t.dismiss(m.text),
|
|
314
318
|
pill: !0,
|
|
315
319
|
size: "sm",
|
|
316
320
|
slot: "assist-dismiss",
|
|
317
321
|
variant: "outline",
|
|
318
|
-
children: /* @__PURE__ */ c(
|
|
322
|
+
children: /* @__PURE__ */ c(xe, {})
|
|
319
323
|
}
|
|
320
324
|
)
|
|
321
|
-
] },
|
|
322
|
-
/* @__PURE__ */ c(
|
|
325
|
+
] }, m.text)) : t.status === "loading" ? /* @__PURE__ */ E("span", { className: "inline-flex items-center gap-2 text-muted-foreground text-sm", "data-slot": "assist-pending", children: [
|
|
326
|
+
/* @__PURE__ */ c(Ce, { "aria-hidden": !0 }),
|
|
323
327
|
r.thinking
|
|
324
328
|
] }) : t.status === "ready" ? /* @__PURE__ */ c("span", { className: "text-muted-foreground text-sm", "data-slot": "assist-empty", children: r.empty }) : null;
|
|
325
|
-
return o === null ? null : /* @__PURE__ */ c(
|
|
329
|
+
return o === null ? null : /* @__PURE__ */ c(Re, { className: "min-h-8", slot: "assist-candidates", children: o });
|
|
326
330
|
}
|
|
327
|
-
function
|
|
328
|
-
const { value: t, onValueChange: s, instructions: r,
|
|
329
|
-
source: ({ signal: u }) => {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
list: !0
|
|
337
|
-
});
|
|
338
|
-
},
|
|
331
|
+
function He(n) {
|
|
332
|
+
const { value: t, onValueChange: s, instructions: r, context: o, children: m, className: y, settings: d } = n, { t: a } = d, p = l.useRef(null), [f, g] = l.useState(!1), [P, w] = l.useState({}), O = l.useCallback(() => We(p.current), []), b = re(d, O), v = Z({
|
|
333
|
+
source: ({ signal: u }) => ee(d.model, _(O(), se(r, o, d)), {
|
|
334
|
+
value: "",
|
|
335
|
+
existing: t,
|
|
336
|
+
signal: u,
|
|
337
|
+
count: 6,
|
|
338
|
+
list: !0
|
|
339
|
+
}),
|
|
339
340
|
existing: t,
|
|
340
|
-
onEvent:
|
|
341
|
+
onEvent: b
|
|
341
342
|
});
|
|
342
|
-
|
|
343
|
-
const u =
|
|
344
|
-
if (!u || !
|
|
345
|
-
|
|
343
|
+
q(d, v), l.useLayoutEffect(() => {
|
|
344
|
+
const u = p.current, C = (u == null ? void 0 : u.querySelector("[data-part=control]")) ?? (u == null ? void 0 : u.firstElementChild);
|
|
345
|
+
if (!u || !C) return;
|
|
346
|
+
C.style.paddingInlineEnd = ne;
|
|
346
347
|
const T = () => {
|
|
347
|
-
const
|
|
348
|
-
w({ insetInlineEnd: x.insetInlineEnd + 2, insetBlockStart: x.insetBlockStart, height:
|
|
348
|
+
const h = getComputedStyle(C).direction === "rtl", x = $(C, u, h);
|
|
349
|
+
w({ insetInlineEnd: x.insetInlineEnd + 2, insetBlockStart: x.insetBlockStart, height: C.offsetHeight });
|
|
349
350
|
};
|
|
350
351
|
if (T(), typeof ResizeObserver > "u") return;
|
|
351
352
|
const L = new ResizeObserver(T);
|
|
352
|
-
return L.observe(
|
|
353
|
+
return L.observe(C), () => L.disconnect();
|
|
353
354
|
}, []);
|
|
354
|
-
const H =
|
|
355
|
+
const H = m, D = H.props, N = l.cloneElement(H, {
|
|
355
356
|
value: t,
|
|
356
|
-
onValueChange: I(
|
|
357
|
+
onValueChange: I(D.onValueChange, (u) => s(u.value))
|
|
357
358
|
}), S = v.items.length > 0;
|
|
358
359
|
return /* @__PURE__ */ E(
|
|
359
360
|
R.div,
|
|
360
361
|
{
|
|
361
|
-
className: M("relative flex w-full min-w-0 flex-col gap-2",
|
|
362
|
+
className: M("relative flex w-full min-w-0 flex-col gap-2", y),
|
|
362
363
|
"data-slot": "assist",
|
|
363
364
|
onBlur: (u) => {
|
|
364
365
|
u.currentTarget.contains(u.relatedTarget) || g(!1);
|
|
365
366
|
},
|
|
366
367
|
onFocus: () => g(!0),
|
|
367
|
-
ref:
|
|
368
|
+
ref: p,
|
|
368
369
|
children: [
|
|
369
370
|
N,
|
|
370
371
|
/* @__PURE__ */ c(
|
|
371
372
|
R.span,
|
|
372
373
|
{
|
|
373
|
-
className: M("absolute flex items-center",
|
|
374
|
+
className: M("absolute flex items-center", P.insetInlineEnd === void 0 && "invisible"),
|
|
374
375
|
"data-slot": "assist-mark",
|
|
375
|
-
style:
|
|
376
|
+
style: P,
|
|
376
377
|
children: /* @__PURE__ */ c(
|
|
377
|
-
|
|
378
|
+
G,
|
|
378
379
|
{
|
|
379
380
|
busy: v.status === "loading" && !S,
|
|
380
|
-
label: S ?
|
|
381
|
+
label: S ? a.others : a.assist,
|
|
381
382
|
offering: S,
|
|
382
383
|
onClick: v.press
|
|
383
384
|
}
|
|
384
385
|
)
|
|
385
386
|
}
|
|
386
387
|
),
|
|
387
|
-
|
|
388
|
-
|
|
388
|
+
f && /* @__PURE__ */ c(
|
|
389
|
+
ie,
|
|
389
390
|
{
|
|
390
391
|
list: v,
|
|
391
392
|
onPick: (u) => {
|
|
392
393
|
s([...t, u]), v.take(u);
|
|
393
394
|
},
|
|
394
|
-
t:
|
|
395
|
+
t: a
|
|
395
396
|
}
|
|
396
397
|
)
|
|
397
398
|
]
|
|
@@ -399,7 +400,7 @@ function Pe(n) {
|
|
|
399
400
|
);
|
|
400
401
|
}
|
|
401
402
|
export {
|
|
402
|
-
|
|
403
|
-
|
|
403
|
+
_e as Assist,
|
|
404
|
+
qe as AssistProvider
|
|
404
405
|
};
|
|
405
406
|
//# sourceMappingURL=assist.js.map
|
package/dist/assist.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"assist.js","sources":["../src/assist.tsx"],"sourcesContent":["\"use client\";\n\nimport { ark } from \"@ark-ui/react/factory\";\nimport { Undo2Icon, XIcon } from \"lucide-react\";\nimport * as React from \"react\";\nimport type { LanguageModel } from \"@kanzo-tech/llm\";\nimport { Button, ButtonGroup, cn, Kbd, Spinner, Suggestion, Suggestions } from \"@kanzo-tech/ui\";\nimport { AiMark } from \"./ai-mark.js\";\nimport {\n type AssistEvent,\n MIN_CONTINUE_LENGTH,\n type Proposal,\n useCandidates,\n useContinuation,\n} from \"./engine.js\";\nimport { type FieldBrief, candidates, continuation } from \"./proposals.js\";\n\n// ── The words ────────────────────────────────────────────────────────────────\n\n/** Every word `Assist` draws or announces. English by default; a host sets them once, on the provider. */\nexport interface AssistTranslations {\n /** The ✨ at rest. */\n assist: string;\n /** The ✨ while a continuation is on offer — pressing it takes it. */\n accept: string;\n /** The ✨ while candidates are on offer — pressing it asks for others. */\n others: string;\n /** The ✨ right after a value was taken — pressing it puts the old one back. */\n revert: string;\n /** Beside the `Tab` key, under a continuation. */\n acceptKey: string;\n /** Beside the `Esc` key. */\n dismissKey: string;\n /** Beside `Alt ]`. */\n nextKey: string;\n /** Read once, by a screen reader, when a continuation lands. */\n announcement: string;\n /** While candidates are on their way. */\n thinking: string;\n /** When the model had nothing to offer. */\n empty: string;\n /** When asking failed and what was thrown carries no message of its own. */\n failed: string;\n /** A candidate's ✕, given the candidate's text. */\n dismiss: (text: string) => string;\n}\n\nconst ENGLISH: AssistTranslations = {\n assist: \"AI assist\",\n accept: \"Accept suggestion\",\n others: \"Suggest different values\",\n revert: \"Undo AI suggestion\",\n acceptKey: \"accept\",\n dismissKey: \"dismiss\",\n nextKey: \"next\",\n announcement: \"Suggestion ready. Press Tab to accept, Escape to dismiss.\",\n thinking: \"Thinking…\",\n empty: \"Nothing to suggest.\",\n failed: \"Couldn’t suggest anything.\",\n dismiss: (text) => `Dismiss ${text}`,\n};\n\n// ── The provider ─────────────────────────────────────────────────────────────\n\ninterface AssistSettings {\n model: LanguageModel;\n context?: () => string | undefined;\n onEvent?: (event: AssistEvent) => void;\n onFailure?: (error: unknown) => void;\n t: AssistTranslations;\n}\n\nconst Settings = React.createContext<AssistSettings | null>(null);\n\nexport interface AssistProviderProps {\n /** The model every assisted field below asks — `gateway(\"complete\")`. */\n model: LanguageModel;\n /**\n * What the rest of the form says, for every field below: read at the moment a field asks, so it\n * is never stale. A string, or a function returning one.\n */\n context?: string | (() => string | undefined);\n /** Each proposal's life — shown, accepted, partly accepted, rejected, ignored. */\n onEvent?: (event: AssistEvent) => void;\n /**\n * Called with what the model's stream threw, whole, when a field's ask fails — on the first\n * attempt, never retried. A model from `createGateway` that goes silent arrives as its `AiError`\n * coded `ai/silent`. The field also says so under itself.\n */\n onFailure?: (error: unknown) => void;\n translations?: Partial<AssistTranslations>;\n children: React.ReactNode;\n}\n\n/**\n * The one place a product decides how its fields are assisted: which model, what the form as a\n * whole is about, where the telemetry goes, and in what words. Renders no element.\n */\nexport function AssistProvider(props: AssistProviderProps) {\n const { model, context, onEvent, onFailure, translations, children } = props;\n const value = React.useMemo<AssistSettings>(\n () => ({\n model,\n context: typeof context === \"function\" ? context : () => context,\n onEvent,\n onFailure,\n t: { ...ENGLISH, ...translations },\n }),\n [context, model, onEvent, onFailure, translations],\n );\n return <Settings.Provider value={value}>{children}</Settings.Provider>;\n}\n\n// ── Reading the field ────────────────────────────────────────────────────────\n\ntype Control = HTMLTextAreaElement | HTMLInputElement;\n\nconst textOf = (ids: string | null) =>\n (ids ?? \"\")\n .split(/\\s+/)\n .map((id) => (id ? document.getElementById(id)?.textContent?.trim() : \"\"))\n .filter(Boolean)\n .join(\" \");\n\n/**\n * What the field is, as a person filling it in is told: its accessible name and description. Ark's\n * `Field` wires the label and helper text to the control with `aria-labelledby`/`aria-describedby`,\n * so an accessible field needs nothing more — and a field without a name is a field a screen reader\n * cannot fill either.\n */\nfunction describe(el: Control | null, instructions: string | undefined, context: string | undefined): FieldBrief {\n if (!el) return { name: \"\", instructions, context };\n const name =\n textOf(el.getAttribute(\"aria-labelledby\")) ||\n [...(el.labels ?? [])].map((l) => l.textContent?.trim()).filter(Boolean).join(\" \") ||\n el.getAttribute(\"aria-label\") ||\n el.getAttribute(\"placeholder\") ||\n \"\";\n return { name, description: textOf(el.getAttribute(\"aria-describedby\")) || undefined, instructions, context };\n}\n\n// ── Geometry ─────────────────────────────────────────────────────────────────\n\nconst METRICS = [\n \"fontFamily\", \"fontSize\", \"fontWeight\", \"fontStyle\", \"letterSpacing\", \"lineHeight\",\n \"textTransform\", \"paddingTop\", \"paddingRight\", \"paddingBottom\", \"paddingLeft\",\n \"borderTopWidth\", \"borderRightWidth\", \"borderBottomWidth\", \"borderLeftWidth\",\n] as const;\n\nconst px = (v: string) => Number.parseFloat(v) || 0;\n\n/** The box `el` occupies inside `root`, as logical insets. */\nfunction insets(el: HTMLElement, root: HTMLElement, rtl: boolean) {\n const box = el.getBoundingClientRect();\n const outer = root.getBoundingClientRect();\n return {\n insetBlockStart: box.top - outer.top,\n insetBlockEnd: outer.bottom - box.bottom,\n insetInlineStart: rtl ? outer.right - box.right : box.left - outer.left,\n insetInlineEnd: rtl ? box.left - outer.left : outer.right - box.right,\n };\n}\n\n/** The ghost mirror wears the field's real metrics, so it aligns whatever the size or className. */\nfunction mirror(el: Control, root: HTMLElement) {\n const cs = getComputedStyle(el);\n const rtl = cs.direction === \"rtl\";\n // A scrolling textarea takes its scrollbar from the inline-end side in both directions.\n const gutter = el.offsetWidth - el.clientWidth - px(cs.borderLeftWidth) - px(cs.borderRightWidth);\n const box = insets(el, root, rtl);\n const metrics: Record<string, string | number> = {\n ...box,\n insetInlineEnd: box.insetInlineEnd + (gutter > 0 ? gutter : 0),\n borderStyle: \"solid\",\n borderColor: \"transparent\",\n };\n for (const k of METRICS) metrics[k] = cs[k as keyof CSSStyleDeclaration] as string;\n return { metrics: metrics as React.CSSProperties, rtl };\n}\n\n/** Room for the ✨ inside the control's box, so typed text never runs under it. */\nconst MARK_ROOM = \"2.25rem\";\n\n/** Leading space plus the next word — what one press of the accept-a-word key takes. */\nconst nextWord = (ghost: string) => /^\\s*\\S+/.exec(ghost)?.[0] ?? \"\";\n\nconst merge =\n <E,>(...handlers: (((e: E) => void) | undefined)[]) =>\n (e: E) => {\n for (const h of handlers) h?.(e);\n };\n\nconst mergeRefs =\n <T,>(...refs: (React.Ref<T> | undefined)[]) =>\n (node: T | null) => {\n for (const ref of refs) {\n if (typeof ref === \"function\") ref(node);\n else if (ref) (ref as React.RefObject<T | null>).current = node;\n }\n };\n\n// ── Assist ───────────────────────────────────────────────────────────────────\n\nexport interface AssistProps<V extends string | string[]> {\n /**\n * The field's value. A string is a text field — a `Textarea` gets a continuation at the caret, an\n * `Input` gets whole values to replace it with; a list is a tags field, and gets values to add.\n */\n value: V;\n onValueChange: (value: V) => void;\n /** One sentence for the model about this field, beyond its label and description. */\n instructions?: string;\n /** The control, from `@kanzo-tech/ui`: `Textarea`, `Input` or `TagsInput`. `Assist` wires it. */\n children: React.ReactElement;\n className?: string;\n}\n\n/**\n * A model-assisted field. Wrap the control; that is all.\n *\n * What it offers is one thing — a **proposal** for the field's value — drawn the way the control\n * calls for: a `Textarea` holds prose, so the proposal is a continuation drawn as ghost text at the\n * caret (Tab takes it, Ctrl/⌘+→ a word, Alt+] / Alt+[ the alternatives, Esc lets it go); a one-line\n * `Input` cannot show text beyond its width, so it gets whole values as a strip of candidates under\n * it; a `TagsInput` gets candidates to add. The ✨ inside the control marks it as assisted, asks,\n * takes, and — right after something was taken — puts the old value back.\n */\nexport function Assist<V extends string | string[]>(props: AssistProps<V>) {\n const settings = React.useContext(Settings);\n if (!settings) throw new Error(\"<Assist> must render inside <AssistProvider>\");\n if (Array.isArray(props.value)) {\n return <ListAssist {...(props as unknown as AssistProps<string[]>)} settings={settings} />;\n }\n return <TextAssist {...(props as unknown as AssistProps<string>)} settings={settings} />;\n}\n\ntype Inner<V extends string | string[]> = AssistProps<V> & { settings: AssistSettings };\n\n/** The control the field reads and writes — the child itself, or the input inside a compound. */\nconst controlIn = (root: HTMLElement | null) =>\n root?.querySelector<Control>(\"textarea, input:not([type=hidden])\") ?? null;\n\nfunction useEvents(settings: AssistSettings, control: () => Control | null) {\n return React.useCallback(\n (kind: AssistEvent[\"kind\"], proposal: Proposal) =>\n settings.onEvent?.({ kind, proposal, field: describe(control(), undefined, undefined).name }),\n [control, settings],\n );\n}\n\n// ── A text field: continuation (Textarea) or candidates (Input) ──────────────\n\nfunction TextAssist(props: Inner<string>) {\n const { value, onValueChange, instructions, children, className, settings } = props;\n const { t } = settings;\n const rootRef = React.useRef<HTMLDivElement>(null);\n const fieldRef = React.useRef<Control | null>(null);\n const [multiline, setMultiline] = React.useState(false);\n const [shape, setShape] = React.useState<{ metrics: React.CSSProperties; rtl: boolean; mark: React.CSSProperties }>(\n { metrics: {}, rtl: false, mark: {} },\n );\n const [caret, setCaret] = React.useState(value.length);\n const [active, setActive] = React.useState(false);\n /** The value before the last proposal was taken; the ✨ puts it back until the reader edits. */\n const [before, setBefore] = React.useState<string | null>(null);\n const landing = React.useRef<number | null>(null);\n\n const control = React.useCallback(() => fieldRef.current, []);\n const report = useEvents(settings, control);\n const brief = () => describe(fieldRef.current, instructions, settings.context?.());\n\n const completion = useContinuation({\n source: (request) => continuation(settings.model, brief(), request),\n onEvent: report,\n });\n const list = useCandidates({\n source: ({ signal }) => candidates(settings.model, brief(), { value, existing: [value], signal, count: 6, list: false }),\n existing: [value],\n onEvent: report,\n });\n useReported(settings, completion);\n useReported(settings, list);\n const ghost = multiline ? completion.ghost : \"\";\n\n React.useLayoutEffect(() => {\n const el = fieldRef.current;\n const root = rootRef.current;\n if (!el || !root) return;\n setMultiline(el instanceof HTMLTextAreaElement);\n el.style.paddingInlineEnd = MARK_ROOM;\n const sync = () => {\n const m = mirror(el, root);\n const box = insets(el, root, m.rtl);\n // Bottom-end in a textarea, where the eye lands after a paragraph; vertically centred in a\n // one-line field, as an input's own addons are.\n const mark: React.CSSProperties =\n el instanceof HTMLTextAreaElement\n ? { insetInlineEnd: box.insetInlineEnd + 4, insetBlockEnd: box.insetBlockEnd + 4 }\n : { insetInlineEnd: box.insetInlineEnd + 2, insetBlockStart: box.insetBlockStart, height: el.offsetHeight };\n setShape({ ...m, mark });\n };\n sync();\n if (typeof ResizeObserver === \"undefined\") return;\n const ro = new ResizeObserver(sync);\n ro.observe(el);\n ro.observe(root);\n return () => ro.disconnect();\n }, []);\n\n // Where the caret must land after WE changed the value; the browser puts it anywhere otherwise.\n React.useLayoutEffect(() => {\n const el = fieldRef.current;\n const at = landing.current;\n if (!el || at === null) return;\n landing.current = null;\n el.setSelectionRange(at, at);\n setCaret(at);\n }, [value]);\n\n /** A selection is not an insertion point, so an offer cannot survive one. */\n const syncCaret = React.useCallback(() => {\n const el = fieldRef.current;\n if (!el || landing.current !== null) return;\n const start = el.selectionStart ?? el.value.length;\n if ((el.selectionEnd ?? start) !== start) {\n completion.dismiss(\"ignored\");\n return;\n }\n setCaret(start);\n }, [completion]);\n\n React.useEffect(() => {\n const onSelectionChange = () => {\n if (document.activeElement === fieldRef.current) syncCaret();\n };\n document.addEventListener(\"selectionchange\", onSelectionChange);\n return () => document.removeEventListener(\"selectionchange\", onSelectionChange);\n }, [syncCaret]);\n\n const put = (next: string, at: number) => {\n landing.current = at;\n onValueChange(next);\n };\n\n const accept = () => {\n if (!ghost) return;\n setBefore(value);\n put(value.slice(0, caret) + ghost + value.slice(caret), caret + ghost.length);\n completion.settle(\"accepted\", ghost);\n fieldRef.current?.focus();\n };\n\n const acceptWord = () => {\n const word = nextWord(ghost);\n if (!word) return;\n const next = value.slice(0, caret) + word + value.slice(caret);\n put(next, caret + word.length);\n completion.settle(\"partial\", word);\n completion.setValue(next, caret + word.length);\n };\n\n const pick = (text: string) => {\n setBefore(value);\n put(text, text.length);\n list.take(text);\n };\n\n const revert = () => {\n if (before === null) return;\n put(before, before.length);\n setBefore(null);\n fieldRef.current?.focus();\n };\n\n const request = () => {\n const el = fieldRef.current;\n const at = el && el === document.activeElement ? (el.selectionStart ?? caret) : caret;\n setCaret(at);\n completion.ask(value, at);\n el?.focus();\n el?.setSelectionRange(at, at);\n };\n\n const onChange = (e: React.ChangeEvent<Control>) => {\n const el = e.target;\n const at = el.selectionStart ?? el.value.length;\n setCaret(at);\n setBefore(null);\n onValueChange(el.value);\n if (multiline) completion.setValue(el.value, at);\n };\n\n const onKeyDown = (e: React.KeyboardEvent<Control>) => {\n if (e.defaultPrevented) return;\n if (!multiline) {\n if (e.key === \"Escape\" && list.items.length > 0) list.cancel();\n return;\n }\n // Forward, not right: \"next word\" is mirrored under RTL.\n const forward = shape.rtl ? \"ArrowLeft\" : \"ArrowRight\";\n if (e.altKey && (e.key === \"]\" || e.key === \"[\" || e.code === \"BracketRight\" || e.code === \"BracketLeft\")) {\n e.preventDefault();\n completion.cycle(e.key === \"]\" || e.code === \"BracketRight\" ? 1 : -1);\n return;\n }\n if (!ghost) return;\n if (e.key === \"Tab\") {\n e.preventDefault();\n accept();\n } else if (e.key === forward && (e.ctrlKey || e.metaKey)) {\n e.preventDefault();\n acceptWord();\n } else if (e.key === \"Escape\") {\n completion.dismiss(\"rejected\");\n }\n };\n\n const child = children as React.ReactElement<Record<string, unknown>>;\n const own = child.props;\n const field = React.cloneElement(child, {\n value,\n onChange: merge(own.onChange as never, onChange),\n onKeyDown: merge(own.onKeyDown as never, onKeyDown),\n onClick: merge(own.onClick as never, syncCaret),\n onKeyUp: merge(own.onKeyUp as never, syncCaret),\n onSelect: merge(own.onSelect as never, syncCaret),\n ref: mergeRefs(own.ref as React.Ref<Control>, fieldRef),\n \"aria-keyshortcuts\": ghost ? \"Tab Escape Alt+]\" : undefined,\n });\n\n const offering = multiline ? ghost.length > 0 : list.items.length > 0;\n const busy = (multiline ? completion.status : list.status) === \"loading\" && !offering;\n const reverting = before !== null && !offering;\n const askable = !multiline || value.trim().length >= MIN_CONTINUE_LENGTH;\n\n return (\n <ark.div\n className={cn(\"relative flex w-full min-w-0 flex-col gap-2\", className)}\n data-slot=\"assist\"\n onBlur={(e) => {\n if (!e.currentTarget.contains(e.relatedTarget)) setActive(false);\n }}\n onFocus={() => setActive(true)}\n ref={rootRef}\n >\n {field}\n {multiline && <Ghost caret={caret} field={fieldRef} ghost={ghost} metrics={shape.metrics} value={value} />}\n <ark.span\n className={cn(\"absolute flex items-center\", shape.mark.insetInlineEnd === undefined && \"invisible\")}\n data-slot=\"assist-mark\"\n style={shape.mark}\n >\n {reverting ? (\n <AiMark label={t.revert} onClick={revert}>\n <Undo2Icon />\n </AiMark>\n ) : (\n <AiMark\n busy={busy}\n disabled={!offering && !askable}\n label={offering ? (multiline ? t.accept : t.others) : t.assist}\n offering={offering}\n onClick={multiline ? (offering ? accept : request) : list.press}\n />\n )}\n </ark.span>\n {multiline ? (\n <>\n {ghost && <Keys t={t} />}\n <span aria-live=\"polite\" className=\"sr-only\" data-slot=\"assist-status\">\n {ghost ? t.announcement : \"\"}\n </span>\n <Failure error={completion.error} status={completion.status} t={t} />\n </>\n ) : (\n active && <Candidates list={list} onPick={pick} t={t} />\n )}\n </ark.div>\n );\n}\n\n/** The continuation drawn where it lands, over the field; the field grows to hold it. */\nfunction Ghost(props: {\n field: React.RefObject<Control | null>;\n ghost: string;\n value: string;\n caret: number;\n metrics: React.CSSProperties;\n}) {\n const { field, ghost, value, caret, metrics } = props;\n const boxRef = React.useRef<HTMLDivElement>(null);\n\n // `rows` is a floor: while an offer is on the table the field takes the height it needs, and\n // gives it back when there is none — what a textarea does anyway when you type into it.\n React.useLayoutEffect(() => {\n const el = field.current;\n const box = boxRef.current;\n if (!el || !box) return;\n if (!ghost) {\n el.style.removeProperty(\"min-height\");\n return;\n }\n const cs = getComputedStyle(el);\n const needed = box.scrollHeight + px(cs.borderTopWidth) + px(cs.borderBottomWidth);\n if (needed > el.getBoundingClientRect().height + 1) el.style.setProperty(\"min-height\", `${needed}px`);\n }, [field, ghost, value]);\n\n React.useLayoutEffect(() => {\n const el = field.current;\n const box = boxRef.current;\n if (!el || !box) return;\n const sync = () => {\n box.scrollLeft = el.scrollLeft;\n box.scrollTop = el.scrollTop;\n };\n sync();\n el.addEventListener(\"scroll\", sync);\n return () => el.removeEventListener(\"scroll\", sync);\n }, [caret, field, ghost, value]);\n\n const at = Math.min(caret, value.length);\n return (\n <ark.div\n aria-hidden\n className={cn(\n \"pointer-events-none absolute overflow-hidden whitespace-pre-wrap break-words text-transparent\",\n !ghost && \"invisible\",\n )}\n data-slot=\"assist-ghost\"\n ref={boxRef}\n style={metrics}\n >\n <span className=\"invisible\">{value.slice(0, at)}</span>\n <span className=\"text-faint\">{ghost}</span>\n <span className=\"invisible\">{value.slice(at)}</span>\n </ark.div>\n );\n}\n\n/** The keys, while there is something to take: the visible twin of the screen-reader announcement. */\nfunction Keys({ t }: { t: AssistTranslations }) {\n return (\n <ark.span className=\"inline-flex flex-wrap items-center gap-3 text-muted-foreground text-xs\" data-slot=\"assist-keys\">\n <span className=\"inline-flex items-center gap-1 whitespace-nowrap\">\n <Kbd>Tab</Kbd> {t.acceptKey}\n </span>\n <span className=\"inline-flex items-center gap-1 whitespace-nowrap\">\n <Kbd>Alt ]</Kbd> {t.nextKey}\n </span>\n <span className=\"inline-flex items-center gap-1 whitespace-nowrap\">\n <Kbd>Esc</Kbd> {t.dismissKey}\n </span>\n </ark.span>\n );\n}\n\n/** Hands the provider's `onFailure` each failure once, whether or not the field is showing it. */\nfunction useReported(settings: AssistSettings, stream: { status: string; error: unknown }) {\n const { onFailure } = settings;\n const { status, error } = stream;\n React.useEffect(() => {\n if (status === \"error\") onFailure?.(error);\n }, [status, error, onFailure]);\n}\n\nfunction Failure(props: { error: unknown; status: string; t: AssistTranslations }) {\n const { error, status, t } = props;\n if (status !== \"error\") return null;\n return (\n <ark.p className=\"text-destructive-foreground text-sm\" data-slot=\"assist-error\" role=\"alert\">\n {error instanceof Error && error.message ? error.message : t.failed}\n </ark.p>\n );\n}\n\n/**\n * The strip, in the flow under the field, while it has focus. Two buttons per candidate in a group —\n * a `<button>` cannot hold a button — and the ✕ always drawn: a hover-only control is unreachable by\n * keyboard and absent on touch. The rationale is the pill's `title`: it reaches the keyboard through\n * focus and costs no box.\n */\nfunction Candidates(props: {\n list: ReturnType<typeof useCandidates>;\n onPick: (text: string) => void;\n t: AssistTranslations;\n}) {\n const { list, onPick, t } = props;\n const body =\n list.status === \"error\" ? (\n <Failure error={list.error} status={list.status} t={t} />\n ) : list.items.length > 0 ? (\n list.items.map((item) => (\n <ButtonGroup aria-label={item.text} key={item.text} slot=\"assist-candidate\">\n <Suggestion onSelect={onPick} title={item.rationale} value={item.text}>\n {item.text}\n </Suggestion>\n <Button\n aria-label={t.dismiss(item.text)}\n className=\"h-auto min-h-[24px] self-stretch px-2 py-1\"\n onClick={() => list.dismiss(item.text)}\n pill\n size=\"sm\"\n slot=\"assist-dismiss\"\n variant=\"outline\"\n >\n <XIcon />\n </Button>\n </ButtonGroup>\n ))\n ) : list.status === \"loading\" ? (\n <span className=\"inline-flex items-center gap-2 text-muted-foreground text-sm\" data-slot=\"assist-pending\">\n <Spinner aria-hidden />\n {t.thinking}\n </span>\n ) : list.status === \"ready\" ? (\n <span className=\"text-muted-foreground text-sm\" data-slot=\"assist-empty\">\n {t.empty}\n </span>\n ) : null;\n\n if (body === null) return null;\n return (\n <Suggestions className=\"min-h-8\" slot=\"assist-candidates\">\n {body}\n </Suggestions>\n );\n}\n\n// ── A list field: candidates to add ──────────────────────────────────────────\n\nfunction ListAssist(props: Inner<string[]>) {\n const { value, onValueChange, instructions, children, className, settings } = props;\n const { t } = settings;\n const rootRef = React.useRef<HTMLDivElement>(null);\n const [active, setActive] = React.useState(false);\n const [mark, setMark] = React.useState<React.CSSProperties>({});\n\n const control = React.useCallback(() => controlIn(rootRef.current), []);\n const report = useEvents(settings, control);\n const list = useCandidates({\n source: ({ signal }) =>\n candidates(settings.model, describe(control(), instructions, settings.context?.()), {\n value: \"\",\n existing: value,\n signal,\n count: 6,\n list: true,\n }),\n existing: value,\n onEvent: report,\n });\n useReported(settings, list);\n\n // The ✨ sits at the end of the control's own box — Ark marks it `data-part=\"control\"`.\n React.useLayoutEffect(() => {\n const root = rootRef.current;\n const box = root?.querySelector<HTMLElement>(\"[data-part=control]\") ?? (root?.firstElementChild as HTMLElement | null);\n if (!root || !box) return;\n box.style.paddingInlineEnd = MARK_ROOM;\n const sync = () => {\n const rtl = getComputedStyle(box).direction === \"rtl\";\n const b = insets(box, root, rtl);\n setMark({ insetInlineEnd: b.insetInlineEnd + 2, insetBlockStart: b.insetBlockStart, height: box.offsetHeight });\n };\n sync();\n if (typeof ResizeObserver === \"undefined\") return;\n const ro = new ResizeObserver(sync);\n ro.observe(box);\n return () => ro.disconnect();\n }, []);\n\n const child = children as React.ReactElement<Record<string, unknown>>;\n const own = child.props;\n const field = React.cloneElement(child, {\n value,\n onValueChange: merge(own.onValueChange as never, (details: { value: string[] }) => onValueChange(details.value)),\n });\n\n const offering = list.items.length > 0;\n return (\n <ark.div\n className={cn(\"relative flex w-full min-w-0 flex-col gap-2\", className)}\n data-slot=\"assist\"\n onBlur={(e) => {\n if (!e.currentTarget.contains(e.relatedTarget)) setActive(false);\n }}\n onFocus={() => setActive(true)}\n ref={rootRef}\n >\n {field}\n <ark.span\n className={cn(\"absolute flex items-center\", mark.insetInlineEnd === undefined && \"invisible\")}\n data-slot=\"assist-mark\"\n style={mark}\n >\n <AiMark\n busy={list.status === \"loading\" && !offering}\n label={offering ? t.others : t.assist}\n offering={offering}\n onClick={list.press}\n />\n </ark.span>\n {active && (\n <Candidates\n list={list}\n onPick={(text) => {\n onValueChange([...value, text]);\n list.take(text);\n }}\n t={t}\n />\n )}\n </ark.div>\n );\n}\n"],"names":[],"mappings":";;;;;;;;;AA+CA;AAAoC;AAC1B;AACA;AACA;AACA;AACG;AACC;AACH;AACK;AACJ;AACH;AACC;AAEV;AAsCO;AACL;AACoB;AACX;AACL;AACyD;AACzD;AACA;AACoB;AAAa;AAEc;AAEnD;AACF;AAMA;;AAGkB;AAAwD;AAU1E;AACE;;AAGoC;AAAe;AAKrD;AAIA;AAAgB;AACd;AAAc;AAAY;AAAc;AAAa;AAAiB;AACtE;AAAiB;AAAc;AAAgB;AAAiB;AAChE;AAAkB;AAAoB;AACxC;AAKA;AACE;AAEA;AAAO;AAC4B;AACC;AACiC;AACH;AAEpE;AAGA;AACE;AAKiD;AAC5C;AACyD;AAC/C;AACA;AAEf;AACA;AACF;AAGA;;AAGoC;AAA8B;AAK9D;AACF;AAKE;AACE;AAGJ;AA4BK;AACL;AACA;AACA;AAIF;AAKA;AAGA;AACE;AAAa;;AAET;;;AACgB;AAEtB;AAIA;AACE;AAKgC;AACI;;AAUhB;AAA6D;AAE9C;AACiC;AACzD;AAEgB;AAC8F;AACvG;AACP;AAEX;AAEA;AAEA;AACE;AAEA;AACA;AAEA;AACE;AAQA;AAAuB;AAGzB;AACA;AACA;AAEgB;AAKhB;AAEA;AAGW;AAIb;AACE;AACA;AACA;AACA;AACE;AACA;AAAA;AAEF;AAAc;AAGhB;AACE;AACE;AAAiD;AAEnD;AAC8E;AAGhF;AACE;AACkB;;AAIlB;AAIkB;AAIlB;AACA;AACA;AACA;AAE6C;AAI7C;AAEc;;AAId;AAGkB;AAIlB;AAEA;AAG0B;AAI1B;AAEA;AAG+C;AAI/C;AACA;AACE;AACA;AAAA;AAGF;AACA;AACE;AAEA;AAAA;AAEF;AAQ+B;AAMO;AACtC;AAC+C;AACG;AACJ;AACA;AACE;AACM;AACJ;AAQpD;AACE;AAAK;AAAJ;AACuE;AAC5D;AAER;AAA+D;AACjE;AAC6B;AACxB;AAEJ;AAAA;AACuG;AACxG;AAAK;AAAJ;AACmG;AACxF;AACG;AAOX;AAAC;AAAA;AACC;AACwB;AACgC;AACxD;AAC0D;AAAA;AAAA;AAC5D;AAAA;AAKC;AAAqB;AAGtB;AACmE;AAGf;AAAA;AAAA;AAI9D;AAGA;AAOE;AAKA;AACE;AAEA;AACA;AACE;AACA;AAAA;AAEF;AAEA;AAAoG;AAIpG;AAEA;AACA;AACE;AACmB;AAErB;AAEkD;AAGpD;AACA;AACE;AAAK;AAAJ;AACY;AACA;AACT;AACU;AAAA;AAEF;AACL;AACE;AAEP;AAAgD;AACZ;AACS;AAAA;AAAA;AAGnD;AAGA;AACE;AAEI;AACE;AAAQ;AAAM;AAAI;AACpB;AAEE;AAAU;AAAM;AAAI;AACtB;AAEE;AAAQ;AAAM;AAAI;AACpB;AAGN;AAGA;AACE;AAEA;AACE;AAAoC;AAExC;AAEA;AACE;AACA;AAMF;AAQA;AAKE;AAOQ;AAEA;AACA;AAAC;AAAA;AACgC;AACrB;AAC2B;AACjC;AACC;AACA;AACG;AAED;AAAA;AAAA;AAMX;AAAqB;AAClB;AAQT;AAMF;AAIA;AACE;AAQ2B;;AAEvB;AAAoF;AAC3E;AACG;AACV;AACO;AACD;AACP;AAAA;AACO;AACD;AAEX;AAIE;AAEA;AACA;AACA;AACE;AAEA;AAA8G;AAGhH;AACA;AACA;AACgB;AAGlB;AAEwC;AACtC;AAC+G;AAIjH;AACE;AAAK;AAAJ;AACuE;AAC5D;AAER;AAA+D;AACjE;AAC6B;AACxB;AAEJ;AAAA;AACD;AAAK;AAAJ;AAC6F;AAClF;AACH;AAEP;AAAC;AAAA;AACqC;AACL;AAC/B;AACc;AAAA;AAAA;AAChB;AAAA;AAGA;AAAC;AAAA;AACC;AAEE;AACc;AAChB;AACA;AAAA;AAAA;AACF;AAAA;AAIR;;;;;"}
|
|
1
|
+
{"version":3,"file":"assist.js","sources":["../src/assist.tsx"],"sourcesContent":["\"use client\";\n\nimport { ark } from \"@ark-ui/react/factory\";\nimport { Undo2Icon, XIcon } from \"lucide-react\";\nimport * as React from \"react\";\nimport type { LanguageModel } from \"@kanzo-tech/llm\";\nimport { Button, ButtonGroup, cn, Kbd, Spinner, Suggestion, Suggestions } from \"@kanzo-tech/ui\";\nimport { AiMark } from \"./ai-mark.js\";\nimport {\n type AssistEvent,\n MIN_CONTINUE_LENGTH,\n type Proposal,\n useCandidates,\n useContinuation,\n} from \"./engine.js\";\nimport { type FieldBrief, candidates, continuation } from \"./proposals.js\";\n\n// ── The words ────────────────────────────────────────────────────────────────\n\n/** Every word `Assist` draws or announces. English by default; a host sets them once, on the provider. */\nexport interface AssistTranslations {\n /** The ✨ at rest. */\n assist: string;\n /** The ✨ while a continuation is on offer — pressing it takes it. */\n accept: string;\n /** The ✨ while candidates are on offer — pressing it asks for others. */\n others: string;\n /** The ✨ right after a value was taken — pressing it puts the old one back. */\n revert: string;\n /** Beside the `Tab` key, under a continuation. */\n acceptKey: string;\n /** Beside the `Esc` key. */\n dismissKey: string;\n /** Beside `Alt ]`. */\n nextKey: string;\n /** Read once, by a screen reader, when a continuation lands. */\n announcement: string;\n /** While candidates are on their way. */\n thinking: string;\n /** When the model had nothing to offer. */\n empty: string;\n /** When asking failed and what was thrown carries no message of its own. */\n failed: string;\n /** A candidate's ✕, given the candidate's text. */\n dismiss: (text: string) => string;\n}\n\nconst ENGLISH: AssistTranslations = {\n assist: \"AI assist\",\n accept: \"Accept suggestion\",\n others: \"Suggest different values\",\n revert: \"Undo AI suggestion\",\n acceptKey: \"accept\",\n dismissKey: \"dismiss\",\n nextKey: \"next\",\n announcement: \"Suggestion ready. Press Tab to accept, Escape to dismiss.\",\n thinking: \"Thinking…\",\n empty: \"Nothing to suggest.\",\n failed: \"Couldn’t suggest anything.\",\n dismiss: (text) => `Dismiss ${text}`,\n};\n\n// ── The provider ─────────────────────────────────────────────────────────────\n\ninterface AssistSettings {\n model: LanguageModel;\n context?: () => string | undefined;\n onEvent?: (event: AssistEvent) => void;\n onFailure?: (error: unknown) => void;\n t: AssistTranslations;\n}\n\nconst Settings = React.createContext<AssistSettings | null>(null);\n\nexport interface AssistProviderProps {\n /** The model every assisted field below asks — `gateway(\"complete\")`. */\n model: LanguageModel;\n /**\n * What the rest of the form says, for every field below: read at the moment a field asks, so it\n * is never stale. A string, or a function returning one.\n */\n context?: string | (() => string | undefined);\n /** Each proposal's life — shown, accepted, partly accepted, rejected, ignored. */\n onEvent?: (event: AssistEvent) => void;\n /**\n * Called with what the model's stream threw, whole, when a field's ask fails — on the first\n * attempt, never retried. A model from `createGateway` that goes silent arrives as its `AiError`\n * coded `ai/silent`. The field also says so under itself.\n */\n onFailure?: (error: unknown) => void;\n translations?: Partial<AssistTranslations>;\n children: React.ReactNode;\n}\n\n/**\n * The one place a product decides how its fields are assisted: which model, what the form as a\n * whole is about, where the telemetry goes, and in what words. Renders no element.\n */\nexport function AssistProvider(props: AssistProviderProps) {\n const { model, context, onEvent, onFailure, translations, children } = props;\n const value = React.useMemo<AssistSettings>(\n () => ({\n model,\n context: typeof context === \"function\" ? context : () => context,\n onEvent,\n onFailure,\n t: { ...ENGLISH, ...translations },\n }),\n [context, model, onEvent, onFailure, translations],\n );\n return <Settings.Provider value={value}>{children}</Settings.Provider>;\n}\n\n// ── Reading the field ────────────────────────────────────────────────────────\n\ntype Control = HTMLTextAreaElement | HTMLInputElement;\n\nconst textOf = (ids: string | null) =>\n (ids ?? \"\")\n .split(/\\s+/)\n .map((id) => (id ? document.getElementById(id)?.textContent?.trim() : \"\"))\n .filter(Boolean)\n .join(\" \");\n\n/**\n * What the field is, as a person filling it in is told: its accessible name and description. Ark's\n * `Field` wires the label and helper text to the control with `aria-labelledby`/`aria-describedby`,\n * so an accessible field needs nothing more — and a field without a name is a field a screen reader\n * cannot fill either.\n */\nfunction describe(el: Control | null, given: Omit<FieldBrief, \"name\" | \"description\">): FieldBrief {\n if (!el) return { name: \"\", ...given };\n const name =\n textOf(el.getAttribute(\"aria-labelledby\")) ||\n [...(el.labels ?? [])].map((l) => l.textContent?.trim()).filter(Boolean).join(\" \") ||\n el.getAttribute(\"aria-label\") ||\n el.getAttribute(\"placeholder\") ||\n \"\";\n return { name, description: textOf(el.getAttribute(\"aria-describedby\")) || undefined, ...given };\n}\n\n// ── Geometry ─────────────────────────────────────────────────────────────────\n\nconst METRICS = [\n \"fontFamily\", \"fontSize\", \"fontWeight\", \"fontStyle\", \"letterSpacing\", \"lineHeight\",\n \"textTransform\", \"paddingTop\", \"paddingRight\", \"paddingBottom\", \"paddingLeft\",\n \"borderTopWidth\", \"borderRightWidth\", \"borderBottomWidth\", \"borderLeftWidth\",\n] as const;\n\nconst px = (v: string) => Number.parseFloat(v) || 0;\n\n/** The box `el` occupies inside `root`, as logical insets. */\nfunction insets(el: HTMLElement, root: HTMLElement, rtl: boolean) {\n const box = el.getBoundingClientRect();\n const outer = root.getBoundingClientRect();\n return {\n insetBlockStart: box.top - outer.top,\n insetBlockEnd: outer.bottom - box.bottom,\n insetInlineStart: rtl ? outer.right - box.right : box.left - outer.left,\n insetInlineEnd: rtl ? box.left - outer.left : outer.right - box.right,\n };\n}\n\n/** The ghost mirror wears the field's real metrics, so it aligns whatever the size or className. */\nfunction mirror(el: Control, root: HTMLElement) {\n const cs = getComputedStyle(el);\n const rtl = cs.direction === \"rtl\";\n // A scrolling textarea takes its scrollbar from the inline-end side in both directions.\n const gutter = el.offsetWidth - el.clientWidth - px(cs.borderLeftWidth) - px(cs.borderRightWidth);\n const box = insets(el, root, rtl);\n const metrics: Record<string, string | number> = {\n ...box,\n insetInlineEnd: box.insetInlineEnd + (gutter > 0 ? gutter : 0),\n borderStyle: \"solid\",\n borderColor: \"transparent\",\n };\n for (const k of METRICS) metrics[k] = cs[k as keyof CSSStyleDeclaration] as string;\n return { metrics: metrics as React.CSSProperties, rtl };\n}\n\n/** Room for the ✨ inside the control's box, so typed text never runs under it. */\nconst MARK_ROOM = \"2.25rem\";\n\n/** Leading space plus the next word — what one press of the accept-a-word key takes. */\nconst nextWord = (ghost: string) => /^\\s*\\S+/.exec(ghost)?.[0] ?? \"\";\n\nconst merge =\n <E,>(...handlers: (((e: E) => void) | undefined)[]) =>\n (e: E) => {\n for (const h of handlers) h?.(e);\n };\n\nconst mergeRefs =\n <T,>(...refs: (React.Ref<T> | undefined)[]) =>\n (node: T | null) => {\n for (const ref of refs) {\n if (typeof ref === \"function\") ref(node);\n else if (ref) (ref as React.RefObject<T | null>).current = node;\n }\n };\n\n// ── Assist ───────────────────────────────────────────────────────────────────\n\nexport interface AssistProps<V extends string | string[]> {\n /**\n * The field's value. A string is a text field — a `Textarea` gets a continuation at the caret, an\n * `Input` gets whole values to replace it with; a list is a tags field, and gets values to add.\n */\n value: V;\n onValueChange: (value: V) => void;\n /** One sentence for the model about this field, beyond its label and description. */\n instructions?: string;\n /**\n * What the host knows about this field that its label and description do not say — the values it\n * accepts, its limits, what its neighbours hold. Facts rather than an instruction, so as many lines\n * as it takes; a function is read when the field asks, so it sees the form as it is then.\n */\n context?: string | (() => string | undefined);\n /** The control, from `@kanzo-tech/ui`: `Textarea`, `Input` or `TagsInput`. `Assist` wires it. */\n children: React.ReactElement;\n className?: string;\n}\n\n/**\n * A model-assisted field. Wrap the control; that is all.\n *\n * What it offers is one thing — a **proposal** for the field's value — drawn the way the control\n * calls for: a `Textarea` holds prose, so the proposal is a continuation drawn as ghost text at the\n * caret (Tab takes it, Ctrl/⌘+→ a word, Alt+] / Alt+[ the alternatives, Esc lets it go); a one-line\n * `Input` cannot show text beyond its width, so it gets whole values as a strip of candidates under\n * it; a `TagsInput` gets candidates to add. The ✨ inside the control marks it as assisted, asks,\n * takes, and — right after something was taken — puts the old value back.\n */\nexport function Assist<V extends string | string[]>(props: AssistProps<V>) {\n const settings = React.useContext(Settings);\n if (!settings) throw new Error(\"<Assist> must render inside <AssistProvider>\");\n if (Array.isArray(props.value)) {\n return <ListAssist {...(props as unknown as AssistProps<string[]>)} settings={settings} />;\n }\n return <TextAssist {...(props as unknown as AssistProps<string>)} settings={settings} />;\n}\n\ntype Inner<V extends string | string[]> = AssistProps<V> & { settings: AssistSettings };\n\n/** What the host tells the model about a field, read at the moment the field asks. */\nconst briefOf = (\n instructions: string | undefined,\n context: AssistProps<string>[\"context\"],\n settings: AssistSettings,\n) => ({\n instructions,\n fieldContext: typeof context === \"function\" ? context() : context,\n context: settings.context?.(),\n});\n\n/** The control the field reads and writes — the child itself, or the input inside a compound. */\nconst controlIn = (root: HTMLElement | null) =>\n root?.querySelector<Control>(\"textarea, input:not([type=hidden])\") ?? null;\n\nfunction useEvents(settings: AssistSettings, control: () => Control | null) {\n return React.useCallback(\n (kind: AssistEvent[\"kind\"], proposal: Proposal) =>\n settings.onEvent?.({ kind, proposal, field: describe(control(), {}).name }),\n [control, settings],\n );\n}\n\n// ── A text field: continuation (Textarea) or candidates (Input) ──────────────\n\nfunction TextAssist(props: Inner<string>) {\n const { value, onValueChange, instructions, context, children, className, settings } = props;\n const { t } = settings;\n const rootRef = React.useRef<HTMLDivElement>(null);\n const fieldRef = React.useRef<Control | null>(null);\n const [multiline, setMultiline] = React.useState(false);\n const [shape, setShape] = React.useState<{ metrics: React.CSSProperties; rtl: boolean; mark: React.CSSProperties }>(\n { metrics: {}, rtl: false, mark: {} },\n );\n const [caret, setCaret] = React.useState(value.length);\n const [active, setActive] = React.useState(false);\n /** The value before the last proposal was taken; the ✨ puts it back until the reader edits. */\n const [before, setBefore] = React.useState<string | null>(null);\n const landing = React.useRef<number | null>(null);\n\n const control = React.useCallback(() => fieldRef.current, []);\n const report = useEvents(settings, control);\n const brief = () => describe(fieldRef.current, briefOf(instructions, context, settings));\n\n const completion = useContinuation({\n source: (request) => continuation(settings.model, brief(), request),\n onEvent: report,\n });\n const list = useCandidates({\n source: ({ signal }) => candidates(settings.model, brief(), { value, existing: [value], signal, count: 6, list: false }),\n existing: [value],\n onEvent: report,\n });\n useReported(settings, completion);\n useReported(settings, list);\n const ghost = multiline ? completion.ghost : \"\";\n\n React.useLayoutEffect(() => {\n const el = fieldRef.current;\n const root = rootRef.current;\n if (!el || !root) return;\n setMultiline(el instanceof HTMLTextAreaElement);\n el.style.paddingInlineEnd = MARK_ROOM;\n const sync = () => {\n const m = mirror(el, root);\n const box = insets(el, root, m.rtl);\n // Bottom-end in a textarea, where the eye lands after a paragraph; vertically centred in a\n // one-line field, as an input's own addons are.\n const mark: React.CSSProperties =\n el instanceof HTMLTextAreaElement\n ? { insetInlineEnd: box.insetInlineEnd + 4, insetBlockEnd: box.insetBlockEnd + 4 }\n : { insetInlineEnd: box.insetInlineEnd + 2, insetBlockStart: box.insetBlockStart, height: el.offsetHeight };\n setShape({ ...m, mark });\n };\n sync();\n if (typeof ResizeObserver === \"undefined\") return;\n const ro = new ResizeObserver(sync);\n ro.observe(el);\n ro.observe(root);\n return () => ro.disconnect();\n }, []);\n\n // Where the caret must land after WE changed the value; the browser puts it anywhere otherwise.\n React.useLayoutEffect(() => {\n const el = fieldRef.current;\n const at = landing.current;\n if (!el || at === null) return;\n landing.current = null;\n el.setSelectionRange(at, at);\n setCaret(at);\n }, [value]);\n\n /** A selection is not an insertion point, so an offer cannot survive one. */\n const syncCaret = React.useCallback(() => {\n const el = fieldRef.current;\n if (!el || landing.current !== null) return;\n const start = el.selectionStart ?? el.value.length;\n if ((el.selectionEnd ?? start) !== start) {\n completion.dismiss(\"ignored\");\n return;\n }\n setCaret(start);\n }, [completion]);\n\n React.useEffect(() => {\n const onSelectionChange = () => {\n if (document.activeElement === fieldRef.current) syncCaret();\n };\n document.addEventListener(\"selectionchange\", onSelectionChange);\n return () => document.removeEventListener(\"selectionchange\", onSelectionChange);\n }, [syncCaret]);\n\n const put = (next: string, at: number) => {\n landing.current = at;\n onValueChange(next);\n };\n\n const accept = () => {\n if (!ghost) return;\n setBefore(value);\n put(value.slice(0, caret) + ghost + value.slice(caret), caret + ghost.length);\n completion.settle(\"accepted\", ghost);\n fieldRef.current?.focus();\n };\n\n const acceptWord = () => {\n const word = nextWord(ghost);\n if (!word) return;\n const next = value.slice(0, caret) + word + value.slice(caret);\n put(next, caret + word.length);\n completion.settle(\"partial\", word);\n completion.setValue(next, caret + word.length);\n };\n\n const pick = (text: string) => {\n setBefore(value);\n put(text, text.length);\n list.take(text);\n };\n\n const revert = () => {\n if (before === null) return;\n put(before, before.length);\n setBefore(null);\n fieldRef.current?.focus();\n };\n\n const request = () => {\n const el = fieldRef.current;\n const at = el && el === document.activeElement ? (el.selectionStart ?? caret) : caret;\n setCaret(at);\n completion.ask(value, at);\n el?.focus();\n el?.setSelectionRange(at, at);\n };\n\n const onChange = (e: React.ChangeEvent<Control>) => {\n const el = e.target;\n const at = el.selectionStart ?? el.value.length;\n setCaret(at);\n setBefore(null);\n onValueChange(el.value);\n if (multiline) completion.setValue(el.value, at);\n };\n\n const onKeyDown = (e: React.KeyboardEvent<Control>) => {\n if (e.defaultPrevented) return;\n if (!multiline) {\n if (e.key === \"Escape\" && list.items.length > 0) list.cancel();\n return;\n }\n // Forward, not right: \"next word\" is mirrored under RTL.\n const forward = shape.rtl ? \"ArrowLeft\" : \"ArrowRight\";\n if (e.altKey && (e.key === \"]\" || e.key === \"[\" || e.code === \"BracketRight\" || e.code === \"BracketLeft\")) {\n e.preventDefault();\n completion.cycle(e.key === \"]\" || e.code === \"BracketRight\" ? 1 : -1);\n return;\n }\n if (!ghost) return;\n if (e.key === \"Tab\") {\n e.preventDefault();\n accept();\n } else if (e.key === forward && (e.ctrlKey || e.metaKey)) {\n e.preventDefault();\n acceptWord();\n } else if (e.key === \"Escape\") {\n completion.dismiss(\"rejected\");\n }\n };\n\n const child = children as React.ReactElement<Record<string, unknown>>;\n const own = child.props;\n const field = React.cloneElement(child, {\n value,\n onChange: merge(own.onChange as never, onChange),\n onKeyDown: merge(own.onKeyDown as never, onKeyDown),\n onClick: merge(own.onClick as never, syncCaret),\n onKeyUp: merge(own.onKeyUp as never, syncCaret),\n onSelect: merge(own.onSelect as never, syncCaret),\n ref: mergeRefs(own.ref as React.Ref<Control>, fieldRef),\n \"aria-keyshortcuts\": ghost ? \"Tab Escape Alt+]\" : undefined,\n });\n\n const offering = multiline ? ghost.length > 0 : list.items.length > 0;\n const busy = (multiline ? completion.status : list.status) === \"loading\" && !offering;\n const reverting = before !== null && !offering;\n const askable = !multiline || value.trim().length >= MIN_CONTINUE_LENGTH;\n\n return (\n <ark.div\n className={cn(\"relative flex w-full min-w-0 flex-col gap-2\", className)}\n data-slot=\"assist\"\n onBlur={(e) => {\n if (!e.currentTarget.contains(e.relatedTarget)) setActive(false);\n }}\n onFocus={() => setActive(true)}\n ref={rootRef}\n >\n {field}\n {multiline && <Ghost caret={caret} field={fieldRef} ghost={ghost} metrics={shape.metrics} value={value} />}\n <ark.span\n className={cn(\"absolute flex items-center\", shape.mark.insetInlineEnd === undefined && \"invisible\")}\n data-slot=\"assist-mark\"\n style={shape.mark}\n >\n {reverting ? (\n <AiMark label={t.revert} onClick={revert}>\n <Undo2Icon />\n </AiMark>\n ) : (\n <AiMark\n busy={busy}\n disabled={!offering && !askable}\n label={offering ? (multiline ? t.accept : t.others) : t.assist}\n offering={offering}\n onClick={multiline ? (offering ? accept : request) : list.press}\n />\n )}\n </ark.span>\n {multiline ? (\n <>\n {ghost && <Keys t={t} />}\n <span aria-live=\"polite\" className=\"sr-only\" data-slot=\"assist-status\">\n {ghost ? t.announcement : \"\"}\n </span>\n <Failure error={completion.error} status={completion.status} t={t} />\n </>\n ) : (\n active && <Candidates list={list} onPick={pick} t={t} />\n )}\n </ark.div>\n );\n}\n\n/** The continuation drawn where it lands, over the field; the field grows to hold it. */\nfunction Ghost(props: {\n field: React.RefObject<Control | null>;\n ghost: string;\n value: string;\n caret: number;\n metrics: React.CSSProperties;\n}) {\n const { field, ghost, value, caret, metrics } = props;\n const boxRef = React.useRef<HTMLDivElement>(null);\n\n // `rows` is a floor: while an offer is on the table the field takes the height it needs, and\n // gives it back when there is none — what a textarea does anyway when you type into it.\n React.useLayoutEffect(() => {\n const el = field.current;\n const box = boxRef.current;\n if (!el || !box) return;\n if (!ghost) {\n el.style.removeProperty(\"min-height\");\n return;\n }\n const cs = getComputedStyle(el);\n const needed = box.scrollHeight + px(cs.borderTopWidth) + px(cs.borderBottomWidth);\n if (needed > el.getBoundingClientRect().height + 1) el.style.setProperty(\"min-height\", `${needed}px`);\n }, [field, ghost, value]);\n\n React.useLayoutEffect(() => {\n const el = field.current;\n const box = boxRef.current;\n if (!el || !box) return;\n const sync = () => {\n box.scrollLeft = el.scrollLeft;\n box.scrollTop = el.scrollTop;\n };\n sync();\n el.addEventListener(\"scroll\", sync);\n return () => el.removeEventListener(\"scroll\", sync);\n }, [caret, field, ghost, value]);\n\n const at = Math.min(caret, value.length);\n return (\n <ark.div\n aria-hidden\n className={cn(\n \"pointer-events-none absolute overflow-hidden whitespace-pre-wrap break-words text-transparent\",\n !ghost && \"invisible\",\n )}\n data-slot=\"assist-ghost\"\n ref={boxRef}\n style={metrics}\n >\n <span className=\"invisible\">{value.slice(0, at)}</span>\n <span className=\"text-faint\">{ghost}</span>\n <span className=\"invisible\">{value.slice(at)}</span>\n </ark.div>\n );\n}\n\n/** The keys, while there is something to take: the visible twin of the screen-reader announcement. */\nfunction Keys({ t }: { t: AssistTranslations }) {\n return (\n <ark.span className=\"inline-flex flex-wrap items-center gap-3 text-muted-foreground text-xs\" data-slot=\"assist-keys\">\n <span className=\"inline-flex items-center gap-1 whitespace-nowrap\">\n <Kbd>Tab</Kbd> {t.acceptKey}\n </span>\n <span className=\"inline-flex items-center gap-1 whitespace-nowrap\">\n <Kbd>Alt ]</Kbd> {t.nextKey}\n </span>\n <span className=\"inline-flex items-center gap-1 whitespace-nowrap\">\n <Kbd>Esc</Kbd> {t.dismissKey}\n </span>\n </ark.span>\n );\n}\n\n/** Hands the provider's `onFailure` each failure once, whether or not the field is showing it. */\nfunction useReported(settings: AssistSettings, stream: { status: string; error: unknown }) {\n const { onFailure } = settings;\n const { status, error } = stream;\n React.useEffect(() => {\n if (status === \"error\") onFailure?.(error);\n }, [status, error, onFailure]);\n}\n\nfunction Failure(props: { error: unknown; status: string; t: AssistTranslations }) {\n const { error, status, t } = props;\n if (status !== \"error\") return null;\n return (\n <ark.p className=\"text-destructive-foreground text-sm\" data-slot=\"assist-error\" role=\"alert\">\n {error instanceof Error && error.message ? error.message : t.failed}\n </ark.p>\n );\n}\n\n/**\n * The strip, in the flow under the field, while it has focus. Two buttons per candidate in a group —\n * a `<button>` cannot hold a button — and the ✕ always drawn: a hover-only control is unreachable by\n * keyboard and absent on touch. The rationale is the pill's `title`: it reaches the keyboard through\n * focus and costs no box.\n */\nfunction Candidates(props: {\n list: ReturnType<typeof useCandidates>;\n onPick: (text: string) => void;\n t: AssistTranslations;\n}) {\n const { list, onPick, t } = props;\n const body =\n list.status === \"error\" ? (\n <Failure error={list.error} status={list.status} t={t} />\n ) : list.items.length > 0 ? (\n list.items.map((item) => (\n <ButtonGroup aria-label={item.text} key={item.text} slot=\"assist-candidate\">\n <Suggestion onSelect={onPick} title={item.rationale} value={item.text}>\n {item.text}\n </Suggestion>\n <Button\n aria-label={t.dismiss(item.text)}\n className=\"h-auto min-h-[24px] self-stretch px-2 py-1\"\n onClick={() => list.dismiss(item.text)}\n pill\n size=\"sm\"\n slot=\"assist-dismiss\"\n variant=\"outline\"\n >\n <XIcon />\n </Button>\n </ButtonGroup>\n ))\n ) : list.status === \"loading\" ? (\n <span className=\"inline-flex items-center gap-2 text-muted-foreground text-sm\" data-slot=\"assist-pending\">\n <Spinner aria-hidden />\n {t.thinking}\n </span>\n ) : list.status === \"ready\" ? (\n <span className=\"text-muted-foreground text-sm\" data-slot=\"assist-empty\">\n {t.empty}\n </span>\n ) : null;\n\n if (body === null) return null;\n return (\n <Suggestions className=\"min-h-8\" slot=\"assist-candidates\">\n {body}\n </Suggestions>\n );\n}\n\n// ── A list field: candidates to add ──────────────────────────────────────────\n\nfunction ListAssist(props: Inner<string[]>) {\n const { value, onValueChange, instructions, context, children, className, settings } = props;\n const { t } = settings;\n const rootRef = React.useRef<HTMLDivElement>(null);\n const [active, setActive] = React.useState(false);\n const [mark, setMark] = React.useState<React.CSSProperties>({});\n\n const control = React.useCallback(() => controlIn(rootRef.current), []);\n const report = useEvents(settings, control);\n const list = useCandidates({\n source: ({ signal }) =>\n candidates(settings.model, describe(control(), briefOf(instructions, context, settings)), {\n value: \"\",\n existing: value,\n signal,\n count: 6,\n list: true,\n }),\n existing: value,\n onEvent: report,\n });\n useReported(settings, list);\n\n // The ✨ sits at the end of the control's own box — Ark marks it `data-part=\"control\"`.\n React.useLayoutEffect(() => {\n const root = rootRef.current;\n const box = root?.querySelector<HTMLElement>(\"[data-part=control]\") ?? (root?.firstElementChild as HTMLElement | null);\n if (!root || !box) return;\n box.style.paddingInlineEnd = MARK_ROOM;\n const sync = () => {\n const rtl = getComputedStyle(box).direction === \"rtl\";\n const b = insets(box, root, rtl);\n setMark({ insetInlineEnd: b.insetInlineEnd + 2, insetBlockStart: b.insetBlockStart, height: box.offsetHeight });\n };\n sync();\n if (typeof ResizeObserver === \"undefined\") return;\n const ro = new ResizeObserver(sync);\n ro.observe(box);\n return () => ro.disconnect();\n }, []);\n\n const child = children as React.ReactElement<Record<string, unknown>>;\n const own = child.props;\n const field = React.cloneElement(child, {\n value,\n onValueChange: merge(own.onValueChange as never, (details: { value: string[] }) => onValueChange(details.value)),\n });\n\n const offering = list.items.length > 0;\n return (\n <ark.div\n className={cn(\"relative flex w-full min-w-0 flex-col gap-2\", className)}\n data-slot=\"assist\"\n onBlur={(e) => {\n if (!e.currentTarget.contains(e.relatedTarget)) setActive(false);\n }}\n onFocus={() => setActive(true)}\n ref={rootRef}\n >\n {field}\n <ark.span\n className={cn(\"absolute flex items-center\", mark.insetInlineEnd === undefined && \"invisible\")}\n data-slot=\"assist-mark\"\n style={mark}\n >\n <AiMark\n busy={list.status === \"loading\" && !offering}\n label={offering ? t.others : t.assist}\n offering={offering}\n onClick={list.press}\n />\n </ark.span>\n {active && (\n <Candidates\n list={list}\n onPick={(text) => {\n onValueChange([...value, text]);\n list.take(text);\n }}\n t={t}\n />\n )}\n </ark.div>\n );\n}\n"],"names":[],"mappings":"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"}
|
package/dist/proposals.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ export interface FieldBrief {
|
|
|
8
8
|
description?: string;
|
|
9
9
|
/** The host's own instruction for this field (`<Assist instructions>`). */
|
|
10
10
|
instructions?: string;
|
|
11
|
+
/** What the host knows about this field beyond its label — its rules, its neighbours (`<Assist context>`). */
|
|
12
|
+
fieldContext?: string;
|
|
11
13
|
/** What the rest of the form says (`<AssistProvider context>`). */
|
|
12
14
|
context?: string;
|
|
13
15
|
}
|
package/dist/proposals.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"proposals.d.ts","sourceRoot":"","sources":["../src/proposals.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,aAAa,EAAkC,MAAM,iBAAiB,CAAC;AACrF,OAAO,KAAK,EAAE,mBAAmB,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEjE,4DAA4D;AAC5D,MAAM,WAAW,UAAU;IACzB,sDAAsD;IACtD,IAAI,EAAE,MAAM,CAAC;IACb,6DAA6D;IAC7D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,2EAA2E;IAC3E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mEAAmE;IACnE,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;
|
|
1
|
+
{"version":3,"file":"proposals.d.ts","sourceRoot":"","sources":["../src/proposals.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,aAAa,EAAkC,MAAM,iBAAiB,CAAC;AACrF,OAAO,KAAK,EAAE,mBAAmB,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEjE,4DAA4D;AAC5D,MAAM,WAAW,UAAU;IACzB,sDAAsD;IACtD,IAAI,EAAE,MAAM,CAAC;IACb,6DAA6D;IAC7D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,2EAA2E;IAC3E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,8GAA8G;IAC9G,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mEAAmE;IACnE,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAgCD,+CAA+C;AAC/C,wBAAuB,YAAY,CACjC,KAAK,EAAE,aAAa,EACpB,KAAK,EAAE,UAAU,EACjB,EAAE,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,mBAAmB,GAC/D,aAAa,CAAC,MAAM,CAAC,CAevB;AAcD;;;GAGG;AACH,wBAAuB,UAAU,CAC/B,KAAK,EAAE,aAAa,EACpB,KAAK,EAAE,UAAU,EACjB,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE;IACxC,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,MAAM,EAAE,WAAW,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,sFAAsF;IACtF,IAAI,EAAE,OAAO,CAAC;CACf,GACA,aAAa,CAAC,QAAQ,CAAC,CAoBzB"}
|
package/dist/proposals.js
CHANGED
|
@@ -3,6 +3,8 @@ const p = (e) => [
|
|
|
3
3
|
`Field: ${e.name || "(unnamed)"}`,
|
|
4
4
|
e.description && `Field description: ${e.description}`,
|
|
5
5
|
e.instructions && `Instructions: ${e.instructions}`,
|
|
6
|
+
e.fieldContext && `Field context:
|
|
7
|
+
${e.fieldContext}`,
|
|
6
8
|
e.context && `Form context:
|
|
7
9
|
${e.context}`
|
|
8
10
|
].filter(Boolean).join(`
|
|
@@ -36,7 +38,7 @@ ${u}`,
|
|
|
36
38
|
onError: l.onError
|
|
37
39
|
}).textStream, l.rethrow();
|
|
38
40
|
}
|
|
39
|
-
const
|
|
41
|
+
const x = f({
|
|
40
42
|
type: "object",
|
|
41
43
|
properties: {
|
|
42
44
|
value: { type: "string", description: "The whole value, exactly as it would be entered." },
|
|
@@ -44,18 +46,18 @@ const y = f({
|
|
|
44
46
|
},
|
|
45
47
|
required: ["value", "rationale"],
|
|
46
48
|
additionalProperties: !1
|
|
47
|
-
}),
|
|
49
|
+
}), y = "You suggest values for a form field. Each value must be complete and ready to enter as-is, in the language of the form.";
|
|
48
50
|
async function* w(e, n, { value: t, existing: o, signal: c, count: a, list: i }) {
|
|
49
51
|
const u = i ? `Current items: ${o.length ? o.join(", ") : "(none)"}
|
|
50
52
|
Suggest NEW items, not already present.` : `Current value: ${t || "(empty)"}
|
|
51
53
|
Suggest alternatives to replace it${t ? ", different from it" : ""}.`, s = d(), l = m({
|
|
52
54
|
model: e,
|
|
53
55
|
onError: s.onError,
|
|
54
|
-
system:
|
|
56
|
+
system: y,
|
|
55
57
|
prompt: `${p(n)}
|
|
56
58
|
${u}
|
|
57
59
|
Give ${a}.`,
|
|
58
|
-
output: h.array({ element:
|
|
60
|
+
output: h.array({ element: x }),
|
|
59
61
|
abortSignal: c,
|
|
60
62
|
maxRetries: 0
|
|
61
63
|
});
|
package/dist/proposals.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"proposals.js","sources":["../src/proposals.ts"],"sourcesContent":["// What a field asks the model, written once. A host names the model and, at most, a sentence of\n// instructions; the field describes itself through its accessible name and description, which is\n// what a person filling it in reads too.\n\nimport { type LanguageModel, Output, jsonSchema, streamText } from \"@kanzo-tech/llm\";\nimport type { ContinuationRequest, Proposal } from \"./engine.js\";\n\n/** What the model is told about the field it is filling. */\nexport interface FieldBrief {\n /** The accessible name — the label a person reads. */\n name: string;\n /** The accessible description — the helper text under it. */\n description?: string;\n /** The host's own instruction for this field (`<Assist instructions>`). */\n instructions?: string;\n /** What the rest of the form says (`<AssistProvider context>`). */\n context?: string;\n}\n\nconst brief = (field: FieldBrief) =>\n [\n `Field: ${field.name || \"(unnamed)\"}`,\n field.description && `Field description: ${field.description}`,\n field.instructions && `Instructions: ${field.instructions}`,\n field.context && `Form context:\\n${field.context}`,\n ]\n .filter(Boolean)\n .join(\"\\n\");\n\nconst CONTINUE = `You complete text inside a form field, like an editor's inline completion.\nReply with ONLY the text to insert at <caret/>: no quotes, no commentary, never repeat text that is already there.\nKeep the language, tone and format of the existing text. Stop at a natural point: the end of the sentence for an automatic suggestion; up to a short paragraph when explicitly asked.`;\n\n/**\n * The AI SDK reports a failed stream to `onError` and then ends the stream as if it had finished, so\n * a refused or broken call would read as a model with nothing to say. This keeps what was reported\n * and throws it, whole, once the stream ends.\n */\nfunction failures() {\n let failed: { error: unknown } | undefined;\n return {\n onError: ({ error }: { error: unknown }) => void (failed ??= { error }),\n rethrow: () => {\n if (failed) throw failed.error;\n },\n };\n}\n\n/** One continuation at the caret, streamed. */\nexport async function* continuation(\n model: LanguageModel,\n field: FieldBrief,\n { value, position, trigger, avoid, signal }: ContinuationRequest,\n): AsyncIterable<string> {\n const text = `${value.slice(0, position)}<caret/>${value.slice(position)}`;\n const differ = avoid.length\n ? `\\nOffer something different from these earlier suggestions:\\n${avoid.map((a) => `- ${a.trim()}`).join(\"\\n\")}`\n : \"\";\n const reported = failures();\n yield* streamText({\n model,\n system: CONTINUE,\n prompt: `${brief(field)}\\nRequest: ${trigger}${differ}\\n\\nText:\\n${text}`,\n abortSignal: signal,\n maxRetries: 0,\n onError: reported.onError,\n }).textStream;\n reported.rethrow();\n}\n\nconst CANDIDATE = jsonSchema<{ value: string; rationale: string }>({\n type: \"object\",\n properties: {\n value: { type: \"string\", description: \"The whole value, exactly as it would be entered.\" },\n rationale: { type: \"string\", description: \"Why it fits, in one short sentence.\" },\n },\n required: [\"value\", \"rationale\"],\n additionalProperties: false,\n});\n\nconst CANDIDATES = `You suggest values for a form field. Each value must be complete and ready to enter as-is, in the language of the form.`;\n\n/**\n * Whole values for the field, each arriving as soon as it is complete (`elementStream`), so the\n * first candidate is on screen while the model writes the rest.\n */\nexport async function* candidates(\n model: LanguageModel,\n field: FieldBrief,\n { value, existing, signal, count, list }: {\n value: string;\n existing: string[];\n signal: AbortSignal;\n count: number;\n /** The field holds a list (tags): a candidate is one more item, not a replacement. */\n list: boolean;\n },\n): AsyncIterable<Proposal> {\n const holds = list\n ? `Current items: ${existing.length ? existing.join(\", \") : \"(none)\"}\\nSuggest NEW items, not already present.`\n : `Current value: ${value || \"(empty)\"}\\nSuggest alternatives to replace it${value ? \", different from it\" : \"\"}.`;\n const reported = failures();\n const result = streamText({\n model,\n onError: reported.onError,\n system: CANDIDATES,\n prompt: `${brief(field)}\\n${holds}\\nGive ${count}.`,\n output: Output.array({ element: CANDIDATE }),\n abortSignal: signal,\n maxRetries: 0,\n });\n for await (const item of result.elementStream) {\n yield list\n ? { text: item.value, rationale: item.rationale }\n : { text: item.value, rationale: item.rationale, range: [0, value.length] };\n }\n reported.rethrow();\n}\n"],"names":["brief","field","CONTINUE","failures","failed","error","continuation","model","value","position","trigger","avoid","signal","text","differ","a","reported","streamText","CANDIDATE","jsonSchema","CANDIDATES","candidates","existing","count","list","holds","result","Output","item"],"mappings":";
|
|
1
|
+
{"version":3,"file":"proposals.js","sources":["../src/proposals.ts"],"sourcesContent":["// What a field asks the model, written once. A host names the model and, at most, a sentence of\n// instructions; the field describes itself through its accessible name and description, which is\n// what a person filling it in reads too.\n\nimport { type LanguageModel, Output, jsonSchema, streamText } from \"@kanzo-tech/llm\";\nimport type { ContinuationRequest, Proposal } from \"./engine.js\";\n\n/** What the model is told about the field it is filling. */\nexport interface FieldBrief {\n /** The accessible name — the label a person reads. */\n name: string;\n /** The accessible description — the helper text under it. */\n description?: string;\n /** The host's own instruction for this field (`<Assist instructions>`). */\n instructions?: string;\n /** What the host knows about this field beyond its label — its rules, its neighbours (`<Assist context>`). */\n fieldContext?: string;\n /** What the rest of the form says (`<AssistProvider context>`). */\n context?: string;\n}\n\nconst brief = (field: FieldBrief) =>\n [\n `Field: ${field.name || \"(unnamed)\"}`,\n field.description && `Field description: ${field.description}`,\n field.instructions && `Instructions: ${field.instructions}`,\n field.fieldContext && `Field context:\\n${field.fieldContext}`,\n field.context && `Form context:\\n${field.context}`,\n ]\n .filter(Boolean)\n .join(\"\\n\");\n\nconst CONTINUE = `You complete text inside a form field, like an editor's inline completion.\nReply with ONLY the text to insert at <caret/>: no quotes, no commentary, never repeat text that is already there.\nKeep the language, tone and format of the existing text. Stop at a natural point: the end of the sentence for an automatic suggestion; up to a short paragraph when explicitly asked.`;\n\n/**\n * The AI SDK reports a failed stream to `onError` and then ends the stream as if it had finished, so\n * a refused or broken call would read as a model with nothing to say. This keeps what was reported\n * and throws it, whole, once the stream ends.\n */\nfunction failures() {\n let failed: { error: unknown } | undefined;\n return {\n onError: ({ error }: { error: unknown }) => void (failed ??= { error }),\n rethrow: () => {\n if (failed) throw failed.error;\n },\n };\n}\n\n/** One continuation at the caret, streamed. */\nexport async function* continuation(\n model: LanguageModel,\n field: FieldBrief,\n { value, position, trigger, avoid, signal }: ContinuationRequest,\n): AsyncIterable<string> {\n const text = `${value.slice(0, position)}<caret/>${value.slice(position)}`;\n const differ = avoid.length\n ? `\\nOffer something different from these earlier suggestions:\\n${avoid.map((a) => `- ${a.trim()}`).join(\"\\n\")}`\n : \"\";\n const reported = failures();\n yield* streamText({\n model,\n system: CONTINUE,\n prompt: `${brief(field)}\\nRequest: ${trigger}${differ}\\n\\nText:\\n${text}`,\n abortSignal: signal,\n maxRetries: 0,\n onError: reported.onError,\n }).textStream;\n reported.rethrow();\n}\n\nconst CANDIDATE = jsonSchema<{ value: string; rationale: string }>({\n type: \"object\",\n properties: {\n value: { type: \"string\", description: \"The whole value, exactly as it would be entered.\" },\n rationale: { type: \"string\", description: \"Why it fits, in one short sentence.\" },\n },\n required: [\"value\", \"rationale\"],\n additionalProperties: false,\n});\n\nconst CANDIDATES = `You suggest values for a form field. Each value must be complete and ready to enter as-is, in the language of the form.`;\n\n/**\n * Whole values for the field, each arriving as soon as it is complete (`elementStream`), so the\n * first candidate is on screen while the model writes the rest.\n */\nexport async function* candidates(\n model: LanguageModel,\n field: FieldBrief,\n { value, existing, signal, count, list }: {\n value: string;\n existing: string[];\n signal: AbortSignal;\n count: number;\n /** The field holds a list (tags): a candidate is one more item, not a replacement. */\n list: boolean;\n },\n): AsyncIterable<Proposal> {\n const holds = list\n ? `Current items: ${existing.length ? existing.join(\", \") : \"(none)\"}\\nSuggest NEW items, not already present.`\n : `Current value: ${value || \"(empty)\"}\\nSuggest alternatives to replace it${value ? \", different from it\" : \"\"}.`;\n const reported = failures();\n const result = streamText({\n model,\n onError: reported.onError,\n system: CANDIDATES,\n prompt: `${brief(field)}\\n${holds}\\nGive ${count}.`,\n output: Output.array({ element: CANDIDATE }),\n abortSignal: signal,\n maxRetries: 0,\n });\n for await (const item of result.elementStream) {\n yield list\n ? { text: item.value, rationale: item.rationale }\n : { text: item.value, rationale: item.rationale, range: [0, value.length] };\n }\n reported.rethrow();\n}\n"],"names":["brief","field","CONTINUE","failures","failed","error","continuation","model","value","position","trigger","avoid","signal","text","differ","a","reported","streamText","CANDIDATE","jsonSchema","CANDIDATES","candidates","existing","count","list","holds","result","Output","item"],"mappings":";AAqBA,MAAMA,IAAQ,CAACC,MACb;AAAA,EACE,UAAUA,EAAM,QAAQ,WAAW;AAAA,EACnCA,EAAM,eAAe,sBAAsBA,EAAM,WAAW;AAAA,EAC5DA,EAAM,gBAAgB,iBAAiBA,EAAM,YAAY;AAAA,EACzDA,EAAM,gBAAgB;AAAA,EAAmBA,EAAM,YAAY;AAAA,EAC3DA,EAAM,WAAW;AAAA,EAAkBA,EAAM,OAAO;AAClD,EACG,OAAO,OAAO,EACd,KAAK;AAAA,CAAI,GAERC,IAAW;AAAA;AAAA;AASjB,SAASC,IAAW;AAClB,MAAIC;AACJ,SAAO;AAAA,IACL,SAAS,CAAC,EAAE,OAAAC,EAAA,MAAgC,MAAMD,UAAW,EAAE,OAAAC;IAC/D,SAAS,MAAM;AACb,UAAID,SAAcA,EAAO;AAAA,IAC3B;AAAA,EAAA;AAEJ;AAGA,gBAAuBE,EACrBC,GACAN,GACA,EAAE,OAAAO,GAAO,UAAAC,GAAU,SAAAC,GAAS,OAAAC,GAAO,QAAAC,KACZ;AACvB,QAAMC,IAAO,GAAGL,EAAM,MAAM,GAAGC,CAAQ,CAAC,WAAWD,EAAM,MAAMC,CAAQ,CAAC,IAClEK,IAASH,EAAM,SACjB;AAAA;AAAA,EAAgEA,EAAM,IAAI,CAACI,MAAM,KAAKA,EAAE,KAAA,CAAM,EAAE,EAAE,KAAK;AAAA,CAAI,CAAC,KAC5G,IACEC,IAAWb,EAAA;AACjB,SAAOc,EAAW;AAAA,IAChB,OAAAV;AAAA,IACA,QAAQL;AAAA,IACR,QAAQ,GAAGF,EAAMC,CAAK,CAAC;AAAA,WAAcS,CAAO,GAAGI,CAAM;AAAA;AAAA;AAAA,EAAcD,CAAI;AAAA,IACvE,aAAaD;AAAA,IACb,YAAY;AAAA,IACZ,SAASI,EAAS;AAAA,EAAA,CACnB,EAAE,YACHA,EAAS,QAAA;AACX;AAEA,MAAME,IAAYC,EAAiD;AAAA,EACjE,MAAM;AAAA,EACN,YAAY;AAAA,IACV,OAAO,EAAE,MAAM,UAAU,aAAa,mDAAA;AAAA,IACtC,WAAW,EAAE,MAAM,UAAU,aAAa,sCAAA;AAAA,EAAsC;AAAA,EAElF,UAAU,CAAC,SAAS,WAAW;AAAA,EAC/B,sBAAsB;AACxB,CAAC,GAEKC,IAAa;AAMnB,gBAAuBC,EACrBd,GACAN,GACA,EAAE,OAAAO,GAAO,UAAAc,GAAU,QAAAV,GAAQ,OAAAW,GAAO,MAAAC,KAQT;AACzB,QAAMC,IAAQD,IACV,kBAAkBF,EAAS,SAASA,EAAS,KAAK,IAAI,IAAI,QAAQ;AAAA,2CAClE,kBAAkBd,KAAS,SAAS;AAAA,oCAAuCA,IAAQ,wBAAwB,EAAE,KAC3GQ,IAAWb,EAAA,GACXuB,IAAST,EAAW;AAAA,IACxB,OAAAV;AAAA,IACA,SAASS,EAAS;AAAA,IAClB,QAAQI;AAAA,IACR,QAAQ,GAAGpB,EAAMC,CAAK,CAAC;AAAA,EAAKwB,CAAK;AAAA,OAAUF,CAAK;AAAA,IAChD,QAAQI,EAAO,MAAM,EAAE,SAAST,GAAW;AAAA,IAC3C,aAAaN;AAAA,IACb,YAAY;AAAA,EAAA,CACb;AACD,mBAAiBgB,KAAQF,EAAO;AAC9B,UAAMF,IACF,EAAE,MAAMI,EAAK,OAAO,WAAWA,EAAK,cACpC,EAAE,MAAMA,EAAK,OAAO,WAAWA,EAAK,WAAW,OAAO,CAAC,GAAGpB,EAAM,MAAM,EAAA;AAE5E,EAAAQ,EAAS,QAAA;AACX;"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kanzo-tech/ai",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
4
4
|
"description": "Kanzo AI surfaces — a field the model helps fill (Assist) and a conversation with one (Chat), over the AI SDK through @kanzo-tech/llm. Sibling of @kanzo-tech/ui, not part of it.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -30,12 +30,12 @@
|
|
|
30
30
|
"lucide-react": ">=1",
|
|
31
31
|
"react": ">=19",
|
|
32
32
|
"react-dom": ">=19",
|
|
33
|
-
"@kanzo-tech/ui": "0.
|
|
33
|
+
"@kanzo-tech/ui": "0.26.0"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"streamdown": "^2.5.0",
|
|
37
37
|
"tailwind-variants": "^3.2.2",
|
|
38
|
-
"@kanzo-tech/llm": "0.
|
|
38
|
+
"@kanzo-tech/llm": "0.26.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@ai-sdk/react": "^4.0.129",
|
|
@@ -51,8 +51,8 @@
|
|
|
51
51
|
"react": "^19.0.0",
|
|
52
52
|
"react-dom": "^19.0.0",
|
|
53
53
|
"rollup-plugin-preserve-directives": "^0.4.0",
|
|
54
|
-
"@kanzo-tech/
|
|
55
|
-
"@kanzo-tech/
|
|
54
|
+
"@kanzo-tech/theme": "0.26.0",
|
|
55
|
+
"@kanzo-tech/ui": "0.26.0"
|
|
56
56
|
},
|
|
57
57
|
"//size-limit": "The root barrel, measured without the peers and dependencies a host already has. Assist and Chat tree-shake apart: a host that only assists fields does not ship the transcript, and the markdown parser comes with Chat alone.",
|
|
58
58
|
"size-limit": [
|