@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 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;
@@ -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 pe } from "react/jsx-runtime";
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 ye, XIcon as ve } from "lucide-react";
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 j, ButtonGroup as be, Suggestion as xe, Button as ke, Spinner as Ee, Suggestions as Se } from "@kanzo-tech/ui";
7
- import { AiMark as U } from "./ai-mark.js";
8
- import { useContinuation as Ce, useCandidates as Y, MIN_CONTINUE_LENGTH as Re } from "./engine.js";
9
- import { continuation as we, candidates as Z } from "./proposals.js";
10
- const Ne = {
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
- }, ee = l.createContext(null);
24
- function Ue(n) {
25
- const { model: t, context: s, onEvent: r, onFailure: o, translations: d, children: a } = n, f = l.useMemo(
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: { ...Ne, ...d }
31
+ t: { ...Be, ...m }
32
32
  }),
33
- [s, t, r, o, d]
33
+ [s, t, r, o, m]
34
34
  );
35
- return /* @__PURE__ */ c(ee.Provider, { value: f, children: a });
35
+ return /* @__PURE__ */ c(te.Provider, { value: d, children: y });
36
36
  }
37
- const Q = (n) => (n ?? "").split(/\s+/).map((t) => {
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 q(n, t, s) {
42
- return n ? { name: Q(n.getAttribute("aria-labelledby")) || [...n.labels ?? []].map((o) => {
43
- var d;
44
- return (d = o.textContent) == null ? void 0 : d.trim();
45
- }).filter(Boolean).join(" ") || n.getAttribute("aria-label") || n.getAttribute("placeholder") || "", description: Q(n.getAttribute("aria-describedby")) || void 0, instructions: t, context: s } : { name: "", instructions: t, context: s };
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 Ae = [
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
- ], F = (n) => Number.parseFloat(n) || 0;
64
- function _(n, t, s) {
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 Be(n, t) {
74
- const s = getComputedStyle(n), r = s.direction === "rtl", o = n.offsetWidth - n.clientWidth - F(s.borderLeftWidth) - F(s.borderRightWidth), d = _(n, t, r), a = {
75
- ...d,
76
- insetInlineEnd: d.insetInlineEnd + (o > 0 ? o : 0),
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 f of Ae) a[f] = s[f];
81
- return { metrics: a, rtl: r };
80
+ for (const d of Ie) y[d] = s[d];
81
+ return { metrics: y, rtl: r };
82
82
  }
83
- const te = "2.25rem", Ie = (n) => {
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
- }, Te = (...n) => (t) => {
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 Ge(n) {
93
- const t = l.useContext(ee);
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(Pe, { ...n, settings: t }) : /* @__PURE__ */ c(Ke, { ...n, settings: t });
95
+ return Array.isArray(n.value) ? /* @__PURE__ */ c(He, { ...n, settings: t }) : /* @__PURE__ */ c(Me, { ...n, settings: t });
96
96
  }
97
- const Le = (n) => (n == null ? void 0 : n.querySelector("textarea, input:not([type=hidden])")) ?? null;
98
- function ne(n, t) {
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: q(t(), void 0, void 0).name });
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 Ke(n) {
108
- const { value: t, onValueChange: s, instructions: r, children: o, className: d, settings: a } = n, { t: f } = a, h = l.useRef(null), m = l.useRef(null), [g, C] = l.useState(!1), [w, P] = l.useState(
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
- ), [y, v] = l.useState(t.length), [H, O] = l.useState(!1), [N, S] = l.useState(null), u = l.useRef(null), b = l.useCallback(() => m.current, []), T = ne(a, b), L = () => {
111
- var e;
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 = Y({
117
- source: ({ signal: e }) => Z(a.model, L(), { value: t, existing: [t], signal: e, count: 6, list: !1 }),
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
- G(a, p), G(a, x);
122
- const k = g ? p.ghost : "";
125
+ q(d, h), q(d, x);
126
+ const k = g ? h.ghost : "";
123
127
  l.useLayoutEffect(() => {
124
- const e = m.current, i = h.current;
128
+ const e = f.current, i = p.current;
125
129
  if (!e || !i) return;
126
- C(e instanceof HTMLTextAreaElement), e.style.paddingInlineEnd = te;
130
+ P(e instanceof HTMLTextAreaElement), e.style.paddingInlineEnd = ne;
127
131
  const W = () => {
128
- const J = Be(e, i), z = _(e, i, J.rtl), he = e instanceof HTMLTextAreaElement ? { insetInlineEnd: z.insetInlineEnd + 4, insetBlockEnd: z.insetBlockEnd + 4 } : { insetInlineEnd: z.insetInlineEnd + 2, insetBlockStart: z.insetBlockStart, height: e.offsetHeight };
129
- P({ ...J, mark: he });
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 V = new ResizeObserver(W);
133
- return V.observe(e), V.observe(i), () => V.disconnect();
136
+ const j = new ResizeObserver(W);
137
+ return j.observe(e), j.observe(i), () => j.disconnect();
134
138
  }, []), l.useLayoutEffect(() => {
135
- const e = m.current, i = u.current;
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 = m.current;
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
- p.dismiss("ignored");
147
+ h.dismiss("ignored");
144
148
  return;
145
149
  }
146
150
  v(i);
147
- }, [p]);
151
+ }, [h]);
148
152
  l.useEffect(() => {
149
153
  const e = () => {
150
- document.activeElement === m.current && K();
154
+ document.activeElement === f.current && K();
151
155
  };
152
156
  return document.addEventListener("selectionchange", e), () => document.removeEventListener("selectionchange", e);
153
157
  }, [K]);
154
- const D = (e, i) => {
158
+ const z = (e, i) => {
155
159
  u.current = i, s(e);
156
- }, $ = () => {
160
+ }, X = () => {
157
161
  var e;
158
- k && (S(t), D(t.slice(0, y) + k + t.slice(y), y + k.length), p.settle("accepted", k), (e = m.current) == null || e.focus());
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 && (D(N, N.length), S(null), (e = m.current) == null || e.focus());
169
- }, le = () => {
170
- const e = m.current, i = e && e === document.activeElement ? e.selectionStart ?? y : y;
171
- v(i), p.ask(t, i), e == null || e.focus(), e == null || e.setSelectionRange(i, i);
172
- }, ae = (e) => {
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 && p.setValue(i.value, W);
175
- }, ue = (e) => {
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(), p.cycle(e.key === "]" || e.code === "BracketRight" ? 1 : -1);
187
+ e.preventDefault(), h.cycle(e.key === "]" || e.code === "BracketRight" ? 1 : -1);
184
188
  return;
185
189
  }
186
- k && (e.key === "Tab" ? (e.preventDefault(), $()) : e.key === i && (e.ctrlKey || e.metaKey) ? (e.preventDefault(), oe()) : e.key === "Escape" && p.dismiss("rejected"));
187
- }, X = o, A = X.props, de = l.cloneElement(X, {
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, ae),
190
- onKeyDown: I(A.onKeyDown, ue),
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: Te(A.ref, m),
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, fe = (g ? p.status : x.status) === "loading" && !B, me = N !== null && !B, ge = !g || t.trim().length >= Re;
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", d),
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) || O(!1);
207
+ e.currentTarget.contains(e.relatedTarget) || D(!1);
204
208
  },
205
- onFocus: () => O(!0),
206
- ref: h,
209
+ onFocus: () => D(!0),
210
+ ref: p,
207
211
  children: [
208
- de,
209
- g && /* @__PURE__ */ c(We, { caret: y, field: m, ghost: k, metrics: w.metrics, value: t }),
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: me ? /* @__PURE__ */ c(U, { label: f.revert, onClick: ce, children: /* @__PURE__ */ c(ye, {}) }) : /* @__PURE__ */ c(
217
- U,
220
+ children: he ? /* @__PURE__ */ c(G, { label: a.revert, onClick: ae, children: /* @__PURE__ */ c(ve, {}) }) : /* @__PURE__ */ c(
221
+ G,
218
222
  {
219
- busy: fe,
220
- disabled: !B && !ge,
221
- label: B ? g ? f.accept : f.others : f.assist,
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 ? $ : le : x.press
227
+ onClick: g ? B ? X : ue : x.press
224
228
  }
225
229
  )
226
230
  }
227
231
  ),
228
- g ? /* @__PURE__ */ E(pe, { children: [
229
- k && /* @__PURE__ */ c(Me, { t: f }),
230
- /* @__PURE__ */ c("span", { "aria-live": "polite", className: "sr-only", "data-slot": "assist-status", children: k ? f.announcement : "" }),
231
- /* @__PURE__ */ c(se, { error: p.error, status: p.status, t: f })
232
- ] }) : H && /* @__PURE__ */ c(re, { list: x, onPick: ie, t: f })
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 We(n) {
238
- const { field: t, ghost: s, value: r, caret: o, metrics: d } = n, a = l.useRef(null);
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 h = t.current, m = a.current;
241
- if (!h || !m) return;
244
+ const a = t.current, p = y.current;
245
+ if (!a || !p) return;
242
246
  if (!s) {
243
- h.style.removeProperty("min-height");
247
+ a.style.removeProperty("min-height");
244
248
  return;
245
249
  }
246
- const g = getComputedStyle(h), C = m.scrollHeight + F(g.borderTopWidth) + F(g.borderBottomWidth);
247
- C > h.getBoundingClientRect().height + 1 && h.style.setProperty("min-height", `${C}px`);
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 h = t.current, m = a.current;
250
- if (!h || !m) return;
251
- const g = () => {
252
- m.scrollLeft = h.scrollLeft, m.scrollTop = h.scrollTop;
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 g(), h.addEventListener("scroll", g), () => h.removeEventListener("scroll", g);
258
+ return f(), a.addEventListener("scroll", f), () => a.removeEventListener("scroll", f);
255
259
  }, [o, t, s, r]);
256
- const f = Math.min(o, r.length);
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: a,
267
- style: d,
270
+ ref: y,
271
+ style: m,
268
272
  children: [
269
- /* @__PURE__ */ c("span", { className: "invisible", children: r.slice(0, f) }),
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(f) })
275
+ /* @__PURE__ */ c("span", { className: "invisible", children: r.slice(d) })
272
276
  ]
273
277
  }
274
278
  );
275
279
  }
276
- function Me({ t: n }) {
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(j, { children: "Tab" }),
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(j, { children: "Alt ]" }),
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(j, { children: "Esc" }),
293
+ /* @__PURE__ */ c(U, { children: "Esc" }),
290
294
  " ",
291
295
  n.dismissKey
292
296
  ] })
293
297
  ] });
294
298
  }
295
- function G(n, t) {
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 se(n) {
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 re(n) {
306
- const { list: t, onPick: s, t: r } = n, o = t.status === "error" ? /* @__PURE__ */ c(se, { error: t.error, status: t.status, t: r }) : t.items.length > 0 ? t.items.map((d) => /* @__PURE__ */ E(be, { "aria-label": d.text, slot: "assist-candidate", children: [
307
- /* @__PURE__ */ c(xe, { onSelect: s, title: d.rationale, value: d.text, children: d.text }),
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
- ke,
313
+ Se,
310
314
  {
311
- "aria-label": r.dismiss(d.text),
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(d.text),
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(ve, {})
322
+ children: /* @__PURE__ */ c(xe, {})
319
323
  }
320
324
  )
321
- ] }, d.text)) : t.status === "loading" ? /* @__PURE__ */ E("span", { className: "inline-flex items-center gap-2 text-muted-foreground text-sm", "data-slot": "assist-pending", children: [
322
- /* @__PURE__ */ c(Ee, { "aria-hidden": !0 }),
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(Se, { className: "min-h-8", slot: "assist-candidates", children: o });
329
+ return o === null ? null : /* @__PURE__ */ c(Re, { className: "min-h-8", slot: "assist-candidates", children: o });
326
330
  }
327
- function Pe(n) {
328
- const { value: t, onValueChange: s, instructions: r, children: o, className: d, settings: a } = n, { t: f } = a, h = l.useRef(null), [m, g] = l.useState(!1), [C, w] = l.useState({}), P = l.useCallback(() => Le(h.current), []), y = ne(a, P), v = Y({
329
- source: ({ signal: u }) => {
330
- var b;
331
- return Z(a.model, q(P(), r, (b = a.context) == null ? void 0 : b.call(a)), {
332
- value: "",
333
- existing: t,
334
- signal: u,
335
- count: 6,
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: y
341
+ onEvent: b
341
342
  });
342
- G(a, v), l.useLayoutEffect(() => {
343
- const u = h.current, b = (u == null ? void 0 : u.querySelector("[data-part=control]")) ?? (u == null ? void 0 : u.firstElementChild);
344
- if (!u || !b) return;
345
- b.style.paddingInlineEnd = te;
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 p = getComputedStyle(b).direction === "rtl", x = _(b, u, p);
348
- w({ insetInlineEnd: x.insetInlineEnd + 2, insetBlockStart: x.insetBlockStart, height: b.offsetHeight });
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(b), () => L.disconnect();
353
+ return L.observe(C), () => L.disconnect();
353
354
  }, []);
354
- const H = o, O = H.props, N = l.cloneElement(H, {
355
+ const H = m, D = H.props, N = l.cloneElement(H, {
355
356
  value: t,
356
- onValueChange: I(O.onValueChange, (u) => s(u.value))
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", d),
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: h,
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", C.insetInlineEnd === void 0 && "invisible"),
374
+ className: M("absolute flex items-center", P.insetInlineEnd === void 0 && "invisible"),
374
375
  "data-slot": "assist-mark",
375
- style: C,
376
+ style: P,
376
377
  children: /* @__PURE__ */ c(
377
- U,
378
+ G,
378
379
  {
379
380
  busy: v.status === "loading" && !S,
380
- label: S ? f.others : f.assist,
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
- m && /* @__PURE__ */ c(
388
- re,
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: f
395
+ t: a
395
396
  }
396
397
  )
397
398
  ]
@@ -399,7 +400,7 @@ function Pe(n) {
399
400
  );
400
401
  }
401
402
  export {
402
- Ge as Assist,
403
- Ue as AssistProvider
403
+ _e as Assist,
404
+ qe as AssistProvider
404
405
  };
405
406
  //# sourceMappingURL=assist.js.map
@@ -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":";;;;;;;;;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;AAkCK;AACL;AACA;AACA;AAIF;AAKA;;AAIM;AAAA;AACJ;AAC0D;AACjD;AACX;AAMA;AACE;AAAa;;AAET;;;AACgB;AAEtB;AAIA;AACE;AAKgC;AACI;AAYD;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;AAEmE;AACjF;AACG;AACV;AACO;AACD;AACP;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;;;;;"}
@@ -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
  }
@@ -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;AA+BD,+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"}
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 y = f({
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
- }), x = "You suggest values for a form field. Each value must be complete and ready to enter as-is, in the language of the form.";
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: x,
56
+ system: y,
55
57
  prompt: `${p(n)}
56
58
  ${u}
57
59
  Give ${a}.`,
58
- output: h.array({ element: y }),
60
+ output: h.array({ element: x }),
59
61
  abortSignal: c,
60
62
  maxRetries: 0
61
63
  });
@@ -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":";AAmBA,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,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;"}
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.24.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.24.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.24.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/ui": "0.24.0",
55
- "@kanzo-tech/theme": "0.24.0"
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": [