@kanzo-tech/ai 0.16.0 → 0.17.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/ai-mark.d.ts +1 -1
- package/dist/ai-mark.d.ts.map +1 -1
- package/dist/ai-mark.js +12 -12
- package/dist/ai-mark.js.map +1 -1
- package/dist/assist.d.ts +71 -0
- package/dist/assist.d.ts.map +1 -0
- package/dist/assist.js +394 -0
- package/dist/assist.js.map +1 -0
- package/dist/chat.d.ts +41 -0
- package/dist/chat.d.ts.map +1 -0
- package/dist/chat.js +77 -0
- package/dist/chat.js.map +1 -0
- package/dist/engine.d.ts +101 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +197 -0
- package/dist/engine.js.map +1 -0
- package/dist/index.d.ts +7 -24
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -56
- package/dist/index.js.map +1 -1
- package/dist/message.js +25 -66
- package/dist/message.js.map +1 -1
- package/dist/prompt-input.d.ts +2 -2
- package/dist/prompt-input.d.ts.map +1 -1
- package/dist/prompt-input.js +47 -49
- package/dist/prompt-input.js.map +1 -1
- package/dist/proposals.d.ts +28 -0
- package/dist/proposals.d.ts.map +1 -0
- package/dist/proposals.js +56 -0
- package/dist/proposals.js.map +1 -0
- package/dist/reasoning.d.ts +4 -3
- package/dist/reasoning.d.ts.map +1 -1
- package/dist/reasoning.js +60 -55
- package/dist/reasoning.js.map +1 -1
- package/dist/tool.d.ts +25 -50
- package/dist/tool.d.ts.map +1 -1
- package/dist/tool.js +83 -95
- package/dist/tool.js.map +1 -1
- package/package.json +18 -34
- package/dist/complete.d.ts +0 -87
- package/dist/complete.d.ts.map +0 -1
- package/dist/complete.js +0 -278
- package/dist/complete.js.map +0 -1
- package/dist/message-part.d.ts +0 -122
- package/dist/message-part.d.ts.map +0 -1
- package/dist/message-part.js +0 -8
- package/dist/message-part.js.map +0 -1
- package/dist/message-text.d.ts +0 -44
- package/dist/message-text.d.ts.map +0 -1
- package/dist/message-text.js +0 -49
- package/dist/message-text.js.map +0 -1
- package/dist/suggest.d.ts +0 -82
- package/dist/suggest.d.ts.map +0 -1
- package/dist/suggest.js +0 -123
- package/dist/suggest.js.map +0 -1
- package/dist/task.d.ts +0 -48
- package/dist/task.d.ts.map +0 -1
- package/dist/task.js +0 -91
- package/dist/task.js.map +0 -1
- package/dist/types.d.ts +0 -10
- package/dist/types.d.ts.map +0 -1
- package/dist/use-ai.d.ts +0 -170
- package/dist/use-ai.d.ts.map +0 -1
- package/dist/use-ai.js +0 -186
- package/dist/use-ai.js.map +0 -1
package/dist/ai-mark.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { InputGroupButton } from '@kanzo-tech/ui';
|
|
2
2
|
import type * as React from "react";
|
|
3
|
-
export interface AiMarkProps extends Omit<React.ComponentProps<typeof InputGroupButton>, "
|
|
3
|
+
export interface AiMarkProps extends Omit<React.ComponentProps<typeof InputGroupButton>, "size"> {
|
|
4
4
|
/** Something is on offer right now — paints only; see {@link AiMarkProps.label}. */
|
|
5
5
|
offering?: boolean;
|
|
6
6
|
/** A request is in flight. */
|
package/dist/ai-mark.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ai-mark.d.ts","sourceRoot":"","sources":["../src/ai-mark.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,KAAK,MAAM,OAAO,CAAC;AAEpC,OAAO,EAAM,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AA8CtD,MAAM,WAAW,WACf,SAAQ,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,OAAO,gBAAgB,CAAC,EAAE,
|
|
1
|
+
{"version":3,"file":"ai-mark.d.ts","sourceRoot":"","sources":["../src/ai-mark.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,KAAK,MAAM,OAAO,CAAC;AAEpC,OAAO,EAAM,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AA8CtD,MAAM,WAAW,WACf,SAAQ,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,OAAO,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnE,oFAAoF;IACpF,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,8BAA8B;IAC9B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,wBAAgB,MAAM,CAAC,KAAK,EAAE,WAAW,qBA0BxC"}
|
package/dist/ai-mark.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { jsx as o } from "react/jsx-runtime";
|
|
2
|
-
import { SparklesIcon as
|
|
3
|
-
import { tv as
|
|
4
|
-
import { InputGroupButton as p, cn as
|
|
5
|
-
const
|
|
2
|
+
import { SparklesIcon as l } from "lucide-react";
|
|
3
|
+
import { tv as c } from "tailwind-variants";
|
|
4
|
+
import { InputGroupButton as p, cn as u } from "@kanzo-tech/ui";
|
|
5
|
+
const d = c({
|
|
6
6
|
base: [
|
|
7
7
|
"text-muted-foreground",
|
|
8
8
|
"transition-colors",
|
|
@@ -45,24 +45,24 @@ const u = l({
|
|
|
45
45
|
},
|
|
46
46
|
defaultVariants: { tone: "rest" }
|
|
47
47
|
});
|
|
48
|
-
function
|
|
49
|
-
const { busy: a = !1,
|
|
48
|
+
function y(e) {
|
|
49
|
+
const { busy: a = !1, children: i, className: s, label: r = "AI assist", offering: t = !1, slot: n, title: m, ...f } = e;
|
|
50
50
|
return /* @__PURE__ */ o(
|
|
51
51
|
p,
|
|
52
52
|
{
|
|
53
53
|
"aria-label": r,
|
|
54
|
-
className:
|
|
54
|
+
className: u(d({ tone: t ? "offer" : "rest" }), s),
|
|
55
55
|
"data-offering": t || void 0,
|
|
56
56
|
isLoading: a,
|
|
57
57
|
size: "icon-sm",
|
|
58
|
-
title:
|
|
59
|
-
...
|
|
60
|
-
slot:
|
|
61
|
-
children: /* @__PURE__ */ o(
|
|
58
|
+
title: m ?? r,
|
|
59
|
+
...f,
|
|
60
|
+
slot: n ?? "ai-mark",
|
|
61
|
+
children: i ?? /* @__PURE__ */ o(l, {})
|
|
62
62
|
}
|
|
63
63
|
);
|
|
64
64
|
}
|
|
65
65
|
export {
|
|
66
|
-
|
|
66
|
+
y as AiMark
|
|
67
67
|
};
|
|
68
68
|
//# sourceMappingURL=ai-mark.js.map
|
package/dist/ai-mark.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ai-mark.js","sources":["../src/ai-mark.tsx"],"sourcesContent":["import { SparklesIcon } from \"lucide-react\";\nimport type * as React from \"react\";\nimport { tv } from \"tailwind-variants\";\nimport { cn, InputGroupButton } from \"@kanzo-tech/ui\";\n\nconst aiMarkVariants = tv({\n base: [\n \"text-muted-foreground\",\n \"transition-colors\",\n \"motion-reduce:transition-none!\",\n ],\n variants: {\n tone: {\n rest: \"\",\n /**\n * Lit, not merely recoloured.\n *\n * `text-primary` alone was the whole offering state, and at 16px against a resting\n * `text-muted-foreground` it is a change a reader has to be looking for. The mark is the one\n * thing on screen that says *there is something here* to somebody whose eyes are on the field\n * rather than on the ghost, so it carries the tenant's brand the way every other \"this one is\n * chosen\" surface in the library does — `RadioGroupCard`, `Questionnaire` and `CodeEditor`'s\n * selection all spell it `bg-primary/17`.\n *\n * **No border, and that is geometry rather than taste.** `RadioGroupCard` pairs this wash with\n * `border-primary` and it is the right pair — but there is nowhere to draw it here. Measured\n * on `docs/ai/fields`: the group is 32px *including* its 1px border, so its content box is\n * 30px, and `InputGroupButton` is 32px because 24px failed WCAG 2.5.8 at compact density. The\n * mark therefore overhangs the content box by exactly 1px top and bottom, which is precisely\n * where the group's own border is drawn — so a border on the mark lands on top of the field's.\n * Fixing that is `InputGroupAddon`'s to fix, and it is filed.\n *\n * What this costs: on a palette whose brand is neutral, `--brand-a5` resolves to `#0000001f`,\n * black at 12%, and the lit state reads closer to hover than to lit. That is the palette\n * having no hue to light up with rather than this rule being wrong, and it comes right the\n * moment a tenant chooses one.\n *\n * The hover pair is repeated on purpose: `Button`'s ghost variant sets `hover:bg-accent`, and\n * without these the mark would go *out* under the pointer at the exact moment it is being\n * reached for. No animation of its own — the base already carries `transition-colors` and the\n * `motion-reduce` escape, so it comes up rather than snapping, and stays still for a reader\n * who asked for that.\n */\n offer: \"bg-primary/17 text-primary hover:bg-primary/30 hover:text-primary\",\n },\n },\n defaultVariants: { tone: \"rest\" },\n});\n\nexport interface AiMarkProps\n extends Omit<React.ComponentProps<typeof InputGroupButton>, \"
|
|
1
|
+
{"version":3,"file":"ai-mark.js","sources":["../src/ai-mark.tsx"],"sourcesContent":["import { SparklesIcon } from \"lucide-react\";\nimport type * as React from \"react\";\nimport { tv } from \"tailwind-variants\";\nimport { cn, InputGroupButton } from \"@kanzo-tech/ui\";\n\nconst aiMarkVariants = tv({\n base: [\n \"text-muted-foreground\",\n \"transition-colors\",\n \"motion-reduce:transition-none!\",\n ],\n variants: {\n tone: {\n rest: \"\",\n /**\n * Lit, not merely recoloured.\n *\n * `text-primary` alone was the whole offering state, and at 16px against a resting\n * `text-muted-foreground` it is a change a reader has to be looking for. The mark is the one\n * thing on screen that says *there is something here* to somebody whose eyes are on the field\n * rather than on the ghost, so it carries the tenant's brand the way every other \"this one is\n * chosen\" surface in the library does — `RadioGroupCard`, `Questionnaire` and `CodeEditor`'s\n * selection all spell it `bg-primary/17`.\n *\n * **No border, and that is geometry rather than taste.** `RadioGroupCard` pairs this wash with\n * `border-primary` and it is the right pair — but there is nowhere to draw it here. Measured\n * on `docs/ai/fields`: the group is 32px *including* its 1px border, so its content box is\n * 30px, and `InputGroupButton` is 32px because 24px failed WCAG 2.5.8 at compact density. The\n * mark therefore overhangs the content box by exactly 1px top and bottom, which is precisely\n * where the group's own border is drawn — so a border on the mark lands on top of the field's.\n * Fixing that is `InputGroupAddon`'s to fix, and it is filed.\n *\n * What this costs: on a palette whose brand is neutral, `--brand-a5` resolves to `#0000001f`,\n * black at 12%, and the lit state reads closer to hover than to lit. That is the palette\n * having no hue to light up with rather than this rule being wrong, and it comes right the\n * moment a tenant chooses one.\n *\n * The hover pair is repeated on purpose: `Button`'s ghost variant sets `hover:bg-accent`, and\n * without these the mark would go *out* under the pointer at the exact moment it is being\n * reached for. No animation of its own — the base already carries `transition-colors` and the\n * `motion-reduce` escape, so it comes up rather than snapping, and stays still for a reader\n * who asked for that.\n */\n offer: \"bg-primary/17 text-primary hover:bg-primary/30 hover:text-primary\",\n },\n },\n defaultVariants: { tone: \"rest\" },\n});\n\nexport interface AiMarkProps\n extends Omit<React.ComponentProps<typeof InputGroupButton>, \"size\"> {\n /** Something is on offer right now — paints only; see {@link AiMarkProps.label}. */\n offering?: boolean;\n /** A request is in flight. */\n busy?: boolean;\n /**\n * The whole accessible name, and the only thing that carries state to a reader who cannot see\n * the colour — so a caller that sets `offering` names the offer too, the way `CompleteMark`\n * does.\n */\n label?: string;\n}\n\n/**\n * The ✨ that says a field is model-assisted.\n *\n * It is a **statement before it is a button**: quiet at rest, so a reader can tell an assisted\n * field from a plain one without touching it, brighter while something is on offer, a spinner\n * while a request is out.\n *\n * ## Both compounds bind it the same way, and that is a rule\n *\n * A mark that paints one way over a ghost and another over a candidate strip reads as two products\n * rather than one library, so `CompleteMark` and `SuggestMark` derive their props identically:\n *\n * - `offering` — *there is something on offer*: a ghost, or candidates on screen. It used to be set\n * only over a ghost, so a strip full of candidates left the mark at rest.\n * - `busy` — *a request is out and nothing has arrived yet*, `loading && !offering`. A bare\n * `loading` spun on top of candidates that were already there.\n * - The press means **give me the assistance**, and each compound reads that in its own domain: a\n * ghost is one offer that needs a gesture, so pressing takes it; a strip is N offers that carry\n * their own buttons, so pressing asks for a different set. Neither is ever inert.\n *\n * The only binding that differs is `disabled`, and it differs for a reason rather than by accident:\n * there is nothing to continue below a minimum length, while a candidate source can answer from an\n * empty field.\n *\n * **Nothing here keys off `:hover`.** A hover-revealed mark is invisible to touch and to the\n * keyboard, and a hover-fired request bills a model for a pointer crossing the field. The state\n * comes from the caller's own knowledge of the stream; focus and a press are the gestures.\n *\n * An `InputGroupButton` rather than a bare `Button` because the mark belongs **inside** the field,\n * in an `InputGroupAddon`: what it marks is the control, so it sits in the control's box and stays\n * with it whether the row above is a label, a legend or nothing — and the addon's coarse-pointer\n * hit area already applies there. It renders standalone too; the group selectors simply never\n * match.\n *\n * There was a `data-[state=open]:text-primary` rule here for the days when pressing it opened a\n * candidate popover and `PopoverTrigger asChild` wrote `data-state` onto it. Nothing opens now, so\n * nothing writes that attribute and the rule matched nothing.\n */\nexport function AiMark(props: AiMarkProps) {\n const { busy = false, children, className, label = \"AI assist\", offering = false, slot, title, ...rest } = props;\n\n return (\n <InputGroupButton\n aria-label={label}\n className={cn(aiMarkVariants({ tone: offering ? \"offer\" : \"rest\" }), className)}\n data-offering={offering || undefined}\n // `Button`'s own loading contract, not a spinner of ours. It used to render `<Spinner />` in\n // place of the glyph and write its own `aria-busy` — which never reached the DOM, because\n // `Button` writes `aria-busy={isLoading}` AFTER spreading props and always won. So the mark\n // spun on screen while telling a screen reader it was idle, and stayed pressable while it\n // did. One machine now: the spinner, `aria-busy`, `aria-disabled` and `data-state` all come\n // from the primitive, and a spinner you can press stops being a lie.\n isLoading={busy}\n size=\"icon-sm\"\n // The browser's own tooltip, for the same reason `SuggestList` gives a candidate's rationale\n // one: it reaches the keyboard through focus and costs no box. A 16px glyph with an\n // accessible name and no visible one tells a pointer reader nothing at all.\n title={title ?? label}\n {...rest}\n slot={slot ?? \"ai-mark\"}\n >\n {children ?? <SparklesIcon />}\n </InputGroupButton>\n );\n}\n"],"names":["aiMarkVariants","tv","AiMark","props","busy","children","className","label","offering","slot","title","rest","jsx","InputGroupButton","cn","SparklesIcon"],"mappings":";;;;AAKA,MAAMA,IAAiBC,EAAG;AAAA,EACxB,MAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,EAAA;AAAA,EAEF,UAAU;AAAA,IACR,MAAM;AAAA,MACJ,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MA8BN,OAAO;AAAA,IAAA;AAAA,EACT;AAAA,EAEF,iBAAiB,EAAE,MAAM,OAAA;AAC3B,CAAC;AAsDM,SAASC,EAAOC,GAAoB;AACzC,QAAM,EAAE,MAAAC,IAAO,IAAO,UAAAC,GAAU,WAAAC,GAAW,OAAAC,IAAQ,aAAa,UAAAC,IAAW,IAAO,MAAAC,GAAM,OAAAC,GAAO,GAAGC,MAASR;AAE3G,SACE,gBAAAS;AAAA,IAACC;AAAA,IAAA;AAAA,MACC,cAAYN;AAAA,MACZ,WAAWO,EAAGd,EAAe,EAAE,MAAMQ,IAAW,UAAU,QAAQ,GAAGF,CAAS;AAAA,MAC9E,iBAAeE,KAAY;AAAA,MAO3B,WAAWJ;AAAA,MACX,MAAK;AAAA,MAIL,OAAOM,KAASH;AAAA,MACf,GAAGI;AAAA,MACJ,MAAMF,KAAQ;AAAA,MAEb,UAAAJ,uBAAaU,GAAA,CAAA,CAAa;AAAA,IAAA;AAAA,EAAA;AAGjC;"}
|
package/dist/assist.d.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { LanguageModel } from '@kanzo-tech/llm';
|
|
2
|
+
import { AssistEvent } from './engine.js';
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
/** Every word `Assist` draws or announces. English by default; a host sets them once, on the provider. */
|
|
5
|
+
export interface AssistTranslations {
|
|
6
|
+
/** The ✨ at rest. */
|
|
7
|
+
assist: string;
|
|
8
|
+
/** The ✨ while a continuation is on offer — pressing it takes it. */
|
|
9
|
+
accept: string;
|
|
10
|
+
/** The ✨ while candidates are on offer — pressing it asks for others. */
|
|
11
|
+
others: string;
|
|
12
|
+
/** The ✨ right after a value was taken — pressing it puts the old one back. */
|
|
13
|
+
revert: string;
|
|
14
|
+
/** Beside the `Tab` key, under a continuation. */
|
|
15
|
+
acceptKey: string;
|
|
16
|
+
/** Beside the `Esc` key. */
|
|
17
|
+
dismissKey: string;
|
|
18
|
+
/** Beside `Alt ]`. */
|
|
19
|
+
nextKey: string;
|
|
20
|
+
/** Read once, by a screen reader, when a continuation lands. */
|
|
21
|
+
announcement: string;
|
|
22
|
+
/** While candidates are on their way. */
|
|
23
|
+
thinking: string;
|
|
24
|
+
/** When the model had nothing to offer. */
|
|
25
|
+
empty: string;
|
|
26
|
+
/** A candidate's ✕, given the candidate's text. */
|
|
27
|
+
dismiss: (text: string) => string;
|
|
28
|
+
}
|
|
29
|
+
export interface AssistProviderProps {
|
|
30
|
+
/** The model every assisted field below asks — `kanzo("kanzo-complete")`. */
|
|
31
|
+
model: LanguageModel;
|
|
32
|
+
/**
|
|
33
|
+
* What the rest of the form says, for every field below: read at the moment a field asks, so it
|
|
34
|
+
* is never stale. A string, or a function returning one.
|
|
35
|
+
*/
|
|
36
|
+
context?: string | (() => string | undefined);
|
|
37
|
+
/** Each proposal's life — shown, accepted, partly accepted, rejected, ignored. */
|
|
38
|
+
onEvent?: (event: AssistEvent) => void;
|
|
39
|
+
translations?: Partial<AssistTranslations>;
|
|
40
|
+
children: React.ReactNode;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The one place a product decides how its fields are assisted: which model, what the form as a
|
|
44
|
+
* whole is about, where the telemetry goes, and in what words. Renders no element.
|
|
45
|
+
*/
|
|
46
|
+
export declare function AssistProvider(props: AssistProviderProps): React.JSX.Element;
|
|
47
|
+
export interface AssistProps<V extends string | string[]> {
|
|
48
|
+
/**
|
|
49
|
+
* The field's value. A string is a text field — a `Textarea` gets a continuation at the caret, an
|
|
50
|
+
* `Input` gets whole values to replace it with; a list is a tags field, and gets values to add.
|
|
51
|
+
*/
|
|
52
|
+
value: V;
|
|
53
|
+
onValueChange: (value: V) => void;
|
|
54
|
+
/** One sentence for the model about this field, beyond its label and description. */
|
|
55
|
+
instructions?: string;
|
|
56
|
+
/** The control, from `@kanzo-tech/ui`: `Textarea`, `Input` or `TagsInput`. `Assist` wires it. */
|
|
57
|
+
children: React.ReactElement;
|
|
58
|
+
className?: string;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* A model-assisted field. Wrap the control; that is all.
|
|
62
|
+
*
|
|
63
|
+
* What it offers is one thing — a **proposal** for the field's value — drawn the way the control
|
|
64
|
+
* calls for: a `Textarea` holds prose, so the proposal is a continuation drawn as ghost text at the
|
|
65
|
+
* caret (Tab takes it, Ctrl/⌘+→ a word, Alt+] / Alt+[ the alternatives, Esc lets it go); a one-line
|
|
66
|
+
* `Input` cannot show text beyond its width, so it gets whole values as a strip of candidates under
|
|
67
|
+
* it; a `TagsInput` gets candidates to add. The ✨ inside the control marks it as assisted, asks,
|
|
68
|
+
* takes, and — right after something was taken — puts the old value back.
|
|
69
|
+
*/
|
|
70
|
+
export declare function Assist<V extends string | string[]>(props: AssistProps<V>): React.JSX.Element;
|
|
71
|
+
//# sourceMappingURL=assist.d.ts.map
|
|
@@ -0,0 +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,mDAAmD;IACnD,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;CACnC;AA2BD,MAAM,WAAW,mBAAmB;IAClC,6EAA6E;IAC7E,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,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,qBAYxD;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"}
|
package/dist/assist.js
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as i, jsxs as E, Fragment as ge } from "react/jsx-runtime";
|
|
3
|
+
import { ark as R } from "@ark-ui/react/factory";
|
|
4
|
+
import { Undo2Icon as pe, XIcon as ve } from "lucide-react";
|
|
5
|
+
import * as l from "react";
|
|
6
|
+
import { cn as M, Kbd as j, ButtonGroup as ye, Suggestion as be, Button as xe, Spinner as ke, Suggestions as Ee } from "@kanzo-tech/ui";
|
|
7
|
+
import { AiMark as U } from "./ai-mark.js";
|
|
8
|
+
import { useContinuation as Se, useCandidates as Q, MIN_CONTINUE_LENGTH as Ce } from "./engine.js";
|
|
9
|
+
import { continuation as Re, candidates as Y } from "./proposals.js";
|
|
10
|
+
const we = {
|
|
11
|
+
assist: "AI assist",
|
|
12
|
+
accept: "Accept suggestion",
|
|
13
|
+
others: "Suggest different values",
|
|
14
|
+
revert: "Undo AI suggestion",
|
|
15
|
+
acceptKey: "accept",
|
|
16
|
+
dismissKey: "dismiss",
|
|
17
|
+
nextKey: "next",
|
|
18
|
+
announcement: "Suggestion ready. Press Tab to accept, Escape to dismiss.",
|
|
19
|
+
thinking: "Thinking…",
|
|
20
|
+
empty: "Nothing to suggest.",
|
|
21
|
+
dismiss: (n) => `Dismiss ${n}`
|
|
22
|
+
}, Z = l.createContext(null);
|
|
23
|
+
function je(n) {
|
|
24
|
+
const { model: t, context: s, onEvent: r, translations: c, children: d } = n, a = l.useMemo(
|
|
25
|
+
() => ({
|
|
26
|
+
model: t,
|
|
27
|
+
context: typeof s == "function" ? s : () => s,
|
|
28
|
+
onEvent: r,
|
|
29
|
+
t: { ...we, ...c }
|
|
30
|
+
}),
|
|
31
|
+
[s, t, r, c]
|
|
32
|
+
);
|
|
33
|
+
return /* @__PURE__ */ i(Z.Provider, { value: a, children: d });
|
|
34
|
+
}
|
|
35
|
+
const J = (n) => (n ?? "").split(/\s+/).map((t) => {
|
|
36
|
+
var s, r;
|
|
37
|
+
return t ? (r = (s = document.getElementById(t)) == null ? void 0 : s.textContent) == null ? void 0 : r.trim() : "";
|
|
38
|
+
}).filter(Boolean).join(" ");
|
|
39
|
+
function G(n, t, s) {
|
|
40
|
+
return n ? { name: J(n.getAttribute("aria-labelledby")) || [...n.labels ?? []].map((c) => {
|
|
41
|
+
var d;
|
|
42
|
+
return (d = c.textContent) == null ? void 0 : d.trim();
|
|
43
|
+
}).filter(Boolean).join(" ") || n.getAttribute("aria-label") || n.getAttribute("placeholder") || "", description: J(n.getAttribute("aria-describedby")) || void 0, instructions: t, context: s } : { name: "", instructions: t, context: s };
|
|
44
|
+
}
|
|
45
|
+
const Ne = [
|
|
46
|
+
"fontFamily",
|
|
47
|
+
"fontSize",
|
|
48
|
+
"fontWeight",
|
|
49
|
+
"fontStyle",
|
|
50
|
+
"letterSpacing",
|
|
51
|
+
"lineHeight",
|
|
52
|
+
"textTransform",
|
|
53
|
+
"paddingTop",
|
|
54
|
+
"paddingRight",
|
|
55
|
+
"paddingBottom",
|
|
56
|
+
"paddingLeft",
|
|
57
|
+
"borderTopWidth",
|
|
58
|
+
"borderRightWidth",
|
|
59
|
+
"borderBottomWidth",
|
|
60
|
+
"borderLeftWidth"
|
|
61
|
+
], F = (n) => Number.parseFloat(n) || 0;
|
|
62
|
+
function q(n, t, s) {
|
|
63
|
+
const r = n.getBoundingClientRect(), c = t.getBoundingClientRect();
|
|
64
|
+
return {
|
|
65
|
+
insetBlockStart: r.top - c.top,
|
|
66
|
+
insetBlockEnd: c.bottom - r.bottom,
|
|
67
|
+
insetInlineStart: s ? c.right - r.right : r.left - c.left,
|
|
68
|
+
insetInlineEnd: s ? r.left - c.left : c.right - r.right
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function Ae(n, t) {
|
|
72
|
+
const s = getComputedStyle(n), r = s.direction === "rtl", c = n.offsetWidth - n.clientWidth - F(s.borderLeftWidth) - F(s.borderRightWidth), d = q(n, t, r), a = {
|
|
73
|
+
...d,
|
|
74
|
+
insetInlineEnd: d.insetInlineEnd + (c > 0 ? c : 0),
|
|
75
|
+
borderStyle: "solid",
|
|
76
|
+
borderColor: "transparent"
|
|
77
|
+
};
|
|
78
|
+
for (const g of Ne) a[g] = s[g];
|
|
79
|
+
return { metrics: a, rtl: r };
|
|
80
|
+
}
|
|
81
|
+
const ee = "2.25rem", Be = (n) => {
|
|
82
|
+
var t;
|
|
83
|
+
return ((t = /^\s*\S+/.exec(n)) == null ? void 0 : t[0]) ?? "";
|
|
84
|
+
}, I = (...n) => (t) => {
|
|
85
|
+
for (const s of n) s == null || s(t);
|
|
86
|
+
}, Ie = (...n) => (t) => {
|
|
87
|
+
for (const s of n)
|
|
88
|
+
typeof s == "function" ? s(t) : s && (s.current = t);
|
|
89
|
+
};
|
|
90
|
+
function Ue(n) {
|
|
91
|
+
const t = l.useContext(Z);
|
|
92
|
+
if (!t) throw new Error("<Assist> must render inside <AssistProvider>");
|
|
93
|
+
return Array.isArray(n.value) ? /* @__PURE__ */ i(Me, { ...n, settings: t }) : /* @__PURE__ */ i(Le, { ...n, settings: t });
|
|
94
|
+
}
|
|
95
|
+
const Te = (n) => (n == null ? void 0 : n.querySelector("textarea, input:not([type=hidden])")) ?? null;
|
|
96
|
+
function te(n, t) {
|
|
97
|
+
return l.useCallback(
|
|
98
|
+
(s, r) => {
|
|
99
|
+
var c;
|
|
100
|
+
return (c = n.onEvent) == null ? void 0 : c.call(n, { kind: s, proposal: r, field: G(t(), void 0, void 0).name });
|
|
101
|
+
},
|
|
102
|
+
[t, n]
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
function Le(n) {
|
|
106
|
+
const { value: t, onValueChange: s, instructions: r, children: c, className: d, settings: a } = n, { t: g } = a, h = l.useRef(null), f = l.useRef(null), [m, C] = l.useState(!1), [w, P] = l.useState(
|
|
107
|
+
{ metrics: {}, rtl: !1, mark: {} }
|
|
108
|
+
), [v, x] = l.useState(t.length), [H, O] = l.useState(!1), [N, S] = l.useState(null), u = l.useRef(null), y = l.useCallback(() => f.current, []), T = te(a, y), L = () => {
|
|
109
|
+
var e;
|
|
110
|
+
return G(f.current, r, (e = a.context) == null ? void 0 : e.call(a));
|
|
111
|
+
}, p = Se({
|
|
112
|
+
source: (e) => Re(a.model, L(), e),
|
|
113
|
+
onEvent: T
|
|
114
|
+
}), k = Q({
|
|
115
|
+
source: ({ signal: e }) => Y(a.model, L(), { value: t, existing: [t], signal: e, count: 6, list: !1 }),
|
|
116
|
+
existing: [t],
|
|
117
|
+
onEvent: T
|
|
118
|
+
}), b = m ? p.ghost : "";
|
|
119
|
+
l.useLayoutEffect(() => {
|
|
120
|
+
const e = f.current, o = h.current;
|
|
121
|
+
if (!e || !o) return;
|
|
122
|
+
C(e instanceof HTMLTextAreaElement), e.style.paddingInlineEnd = ee;
|
|
123
|
+
const W = () => {
|
|
124
|
+
const X = Ae(e, o), z = q(e, o, X.rtl), he = e instanceof HTMLTextAreaElement ? { insetInlineEnd: z.insetInlineEnd + 4, insetBlockEnd: z.insetBlockEnd + 4 } : { insetInlineEnd: z.insetInlineEnd + 2, insetBlockStart: z.insetBlockStart, height: e.offsetHeight };
|
|
125
|
+
P({ ...X, mark: he });
|
|
126
|
+
};
|
|
127
|
+
if (W(), typeof ResizeObserver > "u") return;
|
|
128
|
+
const V = new ResizeObserver(W);
|
|
129
|
+
return V.observe(e), V.observe(o), () => V.disconnect();
|
|
130
|
+
}, []), l.useLayoutEffect(() => {
|
|
131
|
+
const e = f.current, o = u.current;
|
|
132
|
+
!e || o === null || (u.current = null, e.setSelectionRange(o, o), x(o));
|
|
133
|
+
}, [t]);
|
|
134
|
+
const K = l.useCallback(() => {
|
|
135
|
+
const e = f.current;
|
|
136
|
+
if (!e || u.current !== null) return;
|
|
137
|
+
const o = e.selectionStart ?? e.value.length;
|
|
138
|
+
if ((e.selectionEnd ?? o) !== o) {
|
|
139
|
+
p.dismiss("ignored");
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
x(o);
|
|
143
|
+
}, [p]);
|
|
144
|
+
l.useEffect(() => {
|
|
145
|
+
const e = () => {
|
|
146
|
+
document.activeElement === f.current && K();
|
|
147
|
+
};
|
|
148
|
+
return document.addEventListener("selectionchange", e), () => document.removeEventListener("selectionchange", e);
|
|
149
|
+
}, [K]);
|
|
150
|
+
const D = (e, o) => {
|
|
151
|
+
u.current = o, s(e);
|
|
152
|
+
}, _ = () => {
|
|
153
|
+
var e;
|
|
154
|
+
b && (S(t), D(t.slice(0, v) + b + t.slice(v), v + b.length), p.settle("accepted", b), (e = f.current) == null || e.focus());
|
|
155
|
+
}, re = () => {
|
|
156
|
+
const e = Be(b);
|
|
157
|
+
if (!e) return;
|
|
158
|
+
const o = t.slice(0, v) + e + t.slice(v);
|
|
159
|
+
D(o, v + e.length), p.settle("partial", e), p.setValue(o, v + e.length);
|
|
160
|
+
}, oe = (e) => {
|
|
161
|
+
S(t), D(e, e.length), k.take(e);
|
|
162
|
+
}, ie = () => {
|
|
163
|
+
var e;
|
|
164
|
+
N !== null && (D(N, N.length), S(null), (e = f.current) == null || e.focus());
|
|
165
|
+
}, ce = () => {
|
|
166
|
+
const e = f.current, o = e && e === document.activeElement ? e.selectionStart ?? v : v;
|
|
167
|
+
x(o), p.ask(t, o), e == null || e.focus(), e == null || e.setSelectionRange(o, o);
|
|
168
|
+
}, le = (e) => {
|
|
169
|
+
const o = e.target, W = o.selectionStart ?? o.value.length;
|
|
170
|
+
x(W), S(null), s(o.value), m && p.setValue(o.value, W);
|
|
171
|
+
}, ae = (e) => {
|
|
172
|
+
if (e.defaultPrevented) return;
|
|
173
|
+
if (!m) {
|
|
174
|
+
e.key === "Escape" && k.items.length > 0 && k.cancel();
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const o = w.rtl ? "ArrowLeft" : "ArrowRight";
|
|
178
|
+
if (e.altKey && (e.key === "]" || e.key === "[" || e.code === "BracketRight" || e.code === "BracketLeft")) {
|
|
179
|
+
e.preventDefault(), p.cycle(e.key === "]" || e.code === "BracketRight" ? 1 : -1);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
b && (e.key === "Tab" ? (e.preventDefault(), _()) : e.key === o && (e.ctrlKey || e.metaKey) ? (e.preventDefault(), re()) : e.key === "Escape" && p.dismiss("rejected"));
|
|
183
|
+
}, $ = c, A = $.props, ue = l.cloneElement($, {
|
|
184
|
+
value: t,
|
|
185
|
+
onChange: I(A.onChange, le),
|
|
186
|
+
onKeyDown: I(A.onKeyDown, ae),
|
|
187
|
+
onClick: I(A.onClick, K),
|
|
188
|
+
onKeyUp: I(A.onKeyUp, K),
|
|
189
|
+
onSelect: I(A.onSelect, K),
|
|
190
|
+
ref: Ie(A.ref, f),
|
|
191
|
+
"aria-keyshortcuts": b ? "Tab Escape Alt+]" : void 0
|
|
192
|
+
}), B = m ? b.length > 0 : k.items.length > 0, de = (m ? p.status : k.status) === "loading" && !B, fe = N !== null && !B, me = !m || t.trim().length >= Ce;
|
|
193
|
+
return /* @__PURE__ */ E(
|
|
194
|
+
R.div,
|
|
195
|
+
{
|
|
196
|
+
className: M("relative flex w-full min-w-0 flex-col gap-2", d),
|
|
197
|
+
"data-slot": "assist",
|
|
198
|
+
onBlur: (e) => {
|
|
199
|
+
e.currentTarget.contains(e.relatedTarget) || O(!1);
|
|
200
|
+
},
|
|
201
|
+
onFocus: () => O(!0),
|
|
202
|
+
ref: h,
|
|
203
|
+
children: [
|
|
204
|
+
ue,
|
|
205
|
+
m && /* @__PURE__ */ i(Ke, { caret: v, field: f, ghost: b, metrics: w.metrics, value: t }),
|
|
206
|
+
/* @__PURE__ */ i(
|
|
207
|
+
R.span,
|
|
208
|
+
{
|
|
209
|
+
className: M("absolute flex items-center", w.mark.insetInlineEnd === void 0 && "invisible"),
|
|
210
|
+
"data-slot": "assist-mark",
|
|
211
|
+
style: w.mark,
|
|
212
|
+
children: fe ? /* @__PURE__ */ i(U, { label: g.revert, onClick: ie, children: /* @__PURE__ */ i(pe, {}) }) : /* @__PURE__ */ i(
|
|
213
|
+
U,
|
|
214
|
+
{
|
|
215
|
+
busy: de,
|
|
216
|
+
disabled: !B && !me,
|
|
217
|
+
label: B ? m ? g.accept : g.others : g.assist,
|
|
218
|
+
offering: B,
|
|
219
|
+
onClick: m ? B ? _ : ce : k.press
|
|
220
|
+
}
|
|
221
|
+
)
|
|
222
|
+
}
|
|
223
|
+
),
|
|
224
|
+
m ? /* @__PURE__ */ E(ge, { children: [
|
|
225
|
+
b && /* @__PURE__ */ i(We, { t: g }),
|
|
226
|
+
/* @__PURE__ */ i("span", { "aria-live": "polite", className: "sr-only", "data-slot": "assist-status", children: b ? g.announcement : "" }),
|
|
227
|
+
/* @__PURE__ */ i(ne, { error: p.error })
|
|
228
|
+
] }) : H && /* @__PURE__ */ i(se, { list: k, onPick: oe, t: g })
|
|
229
|
+
]
|
|
230
|
+
}
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
function Ke(n) {
|
|
234
|
+
const { field: t, ghost: s, value: r, caret: c, metrics: d } = n, a = l.useRef(null);
|
|
235
|
+
l.useLayoutEffect(() => {
|
|
236
|
+
const h = t.current, f = a.current;
|
|
237
|
+
if (!h || !f) return;
|
|
238
|
+
if (!s) {
|
|
239
|
+
h.style.removeProperty("min-height");
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
const m = getComputedStyle(h), C = f.scrollHeight + F(m.borderTopWidth) + F(m.borderBottomWidth);
|
|
243
|
+
C > h.getBoundingClientRect().height + 1 && h.style.setProperty("min-height", `${C}px`);
|
|
244
|
+
}, [t, s, r]), l.useLayoutEffect(() => {
|
|
245
|
+
const h = t.current, f = a.current;
|
|
246
|
+
if (!h || !f) return;
|
|
247
|
+
const m = () => {
|
|
248
|
+
f.scrollLeft = h.scrollLeft, f.scrollTop = h.scrollTop;
|
|
249
|
+
};
|
|
250
|
+
return m(), h.addEventListener("scroll", m), () => h.removeEventListener("scroll", m);
|
|
251
|
+
}, [c, t, s, r]);
|
|
252
|
+
const g = Math.min(c, r.length);
|
|
253
|
+
return /* @__PURE__ */ E(
|
|
254
|
+
R.div,
|
|
255
|
+
{
|
|
256
|
+
"aria-hidden": !0,
|
|
257
|
+
className: M(
|
|
258
|
+
"pointer-events-none absolute overflow-hidden whitespace-pre-wrap break-words text-transparent",
|
|
259
|
+
!s && "invisible"
|
|
260
|
+
),
|
|
261
|
+
"data-slot": "assist-ghost",
|
|
262
|
+
ref: a,
|
|
263
|
+
style: d,
|
|
264
|
+
children: [
|
|
265
|
+
/* @__PURE__ */ i("span", { className: "invisible", children: r.slice(0, g) }),
|
|
266
|
+
/* @__PURE__ */ i("span", { className: "text-faint", children: s }),
|
|
267
|
+
/* @__PURE__ */ i("span", { className: "invisible", children: r.slice(g) })
|
|
268
|
+
]
|
|
269
|
+
}
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
function We({ t: n }) {
|
|
273
|
+
return /* @__PURE__ */ E(R.span, { className: "inline-flex flex-wrap items-center gap-3 text-muted-foreground text-xs", "data-slot": "assist-keys", children: [
|
|
274
|
+
/* @__PURE__ */ E("span", { className: "inline-flex items-center gap-1 whitespace-nowrap", children: [
|
|
275
|
+
/* @__PURE__ */ i(j, { children: "Tab" }),
|
|
276
|
+
" ",
|
|
277
|
+
n.acceptKey
|
|
278
|
+
] }),
|
|
279
|
+
/* @__PURE__ */ E("span", { className: "inline-flex items-center gap-1 whitespace-nowrap", children: [
|
|
280
|
+
/* @__PURE__ */ i(j, { children: "Alt ]" }),
|
|
281
|
+
" ",
|
|
282
|
+
n.nextKey
|
|
283
|
+
] }),
|
|
284
|
+
/* @__PURE__ */ E("span", { className: "inline-flex items-center gap-1 whitespace-nowrap", children: [
|
|
285
|
+
/* @__PURE__ */ i(j, { children: "Esc" }),
|
|
286
|
+
" ",
|
|
287
|
+
n.dismissKey
|
|
288
|
+
] })
|
|
289
|
+
] });
|
|
290
|
+
}
|
|
291
|
+
function ne({ error: n }) {
|
|
292
|
+
return n === null ? null : /* @__PURE__ */ i(R.p, { className: "text-destructive-foreground text-sm", "data-slot": "assist-error", role: "alert", children: n });
|
|
293
|
+
}
|
|
294
|
+
function se(n) {
|
|
295
|
+
const { list: t, onPick: s, t: r } = n, c = t.error !== null ? /* @__PURE__ */ i(ne, { error: t.error }) : t.items.length > 0 ? t.items.map((d) => /* @__PURE__ */ E(ye, { "aria-label": d.text, slot: "assist-candidate", children: [
|
|
296
|
+
/* @__PURE__ */ i(be, { onSelect: s, title: d.rationale, value: d.text, children: d.text }),
|
|
297
|
+
/* @__PURE__ */ i(
|
|
298
|
+
xe,
|
|
299
|
+
{
|
|
300
|
+
"aria-label": r.dismiss(d.text),
|
|
301
|
+
className: "h-auto min-h-[24px] self-stretch px-2 py-1",
|
|
302
|
+
onClick: () => t.dismiss(d.text),
|
|
303
|
+
pill: !0,
|
|
304
|
+
size: "sm",
|
|
305
|
+
slot: "assist-dismiss",
|
|
306
|
+
variant: "outline",
|
|
307
|
+
children: /* @__PURE__ */ i(ve, {})
|
|
308
|
+
}
|
|
309
|
+
)
|
|
310
|
+
] }, 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: [
|
|
311
|
+
/* @__PURE__ */ i(ke, { "aria-hidden": !0 }),
|
|
312
|
+
r.thinking
|
|
313
|
+
] }) : t.status === "ready" ? /* @__PURE__ */ i("span", { className: "text-muted-foreground text-sm", "data-slot": "assist-empty", children: r.empty }) : null;
|
|
314
|
+
return c === null ? null : /* @__PURE__ */ i(Ee, { className: "min-h-8", slot: "assist-candidates", children: c });
|
|
315
|
+
}
|
|
316
|
+
function Me(n) {
|
|
317
|
+
const { value: t, onValueChange: s, instructions: r, children: c, className: d, settings: a } = n, { t: g } = a, h = l.useRef(null), [f, m] = l.useState(!1), [C, w] = l.useState({}), P = l.useCallback(() => Te(h.current), []), v = te(a, P), x = Q({
|
|
318
|
+
source: ({ signal: u }) => {
|
|
319
|
+
var y;
|
|
320
|
+
return Y(a.model, G(P(), r, (y = a.context) == null ? void 0 : y.call(a)), {
|
|
321
|
+
value: "",
|
|
322
|
+
existing: t,
|
|
323
|
+
signal: u,
|
|
324
|
+
count: 6,
|
|
325
|
+
list: !0
|
|
326
|
+
});
|
|
327
|
+
},
|
|
328
|
+
existing: t,
|
|
329
|
+
onEvent: v
|
|
330
|
+
});
|
|
331
|
+
l.useLayoutEffect(() => {
|
|
332
|
+
const u = h.current, y = (u == null ? void 0 : u.querySelector("[data-part=control]")) ?? (u == null ? void 0 : u.firstElementChild);
|
|
333
|
+
if (!u || !y) return;
|
|
334
|
+
y.style.paddingInlineEnd = ee;
|
|
335
|
+
const T = () => {
|
|
336
|
+
const p = getComputedStyle(y).direction === "rtl", k = q(y, u, p);
|
|
337
|
+
w({ insetInlineEnd: k.insetInlineEnd + 2, insetBlockStart: k.insetBlockStart, height: y.offsetHeight });
|
|
338
|
+
};
|
|
339
|
+
if (T(), typeof ResizeObserver > "u") return;
|
|
340
|
+
const L = new ResizeObserver(T);
|
|
341
|
+
return L.observe(y), () => L.disconnect();
|
|
342
|
+
}, []);
|
|
343
|
+
const H = c, O = H.props, N = l.cloneElement(H, {
|
|
344
|
+
value: t,
|
|
345
|
+
onValueChange: I(O.onValueChange, (u) => s(u.value))
|
|
346
|
+
}), S = x.items.length > 0;
|
|
347
|
+
return /* @__PURE__ */ E(
|
|
348
|
+
R.div,
|
|
349
|
+
{
|
|
350
|
+
className: M("relative flex w-full min-w-0 flex-col gap-2", d),
|
|
351
|
+
"data-slot": "assist",
|
|
352
|
+
onBlur: (u) => {
|
|
353
|
+
u.currentTarget.contains(u.relatedTarget) || m(!1);
|
|
354
|
+
},
|
|
355
|
+
onFocus: () => m(!0),
|
|
356
|
+
ref: h,
|
|
357
|
+
children: [
|
|
358
|
+
N,
|
|
359
|
+
/* @__PURE__ */ i(
|
|
360
|
+
R.span,
|
|
361
|
+
{
|
|
362
|
+
className: M("absolute flex items-center", C.insetInlineEnd === void 0 && "invisible"),
|
|
363
|
+
"data-slot": "assist-mark",
|
|
364
|
+
style: C,
|
|
365
|
+
children: /* @__PURE__ */ i(
|
|
366
|
+
U,
|
|
367
|
+
{
|
|
368
|
+
busy: x.status === "loading" && !S,
|
|
369
|
+
label: S ? g.others : g.assist,
|
|
370
|
+
offering: S,
|
|
371
|
+
onClick: x.press
|
|
372
|
+
}
|
|
373
|
+
)
|
|
374
|
+
}
|
|
375
|
+
),
|
|
376
|
+
f && /* @__PURE__ */ i(
|
|
377
|
+
se,
|
|
378
|
+
{
|
|
379
|
+
list: x,
|
|
380
|
+
onPick: (u) => {
|
|
381
|
+
s([...t, u]), x.take(u);
|
|
382
|
+
},
|
|
383
|
+
t: g
|
|
384
|
+
}
|
|
385
|
+
)
|
|
386
|
+
]
|
|
387
|
+
}
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
export {
|
|
391
|
+
Ue as Assist,
|
|
392
|
+
je as AssistProvider
|
|
393
|
+
};
|
|
394
|
+
//# sourceMappingURL=assist.js.map
|
|
@@ -0,0 +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 /** 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 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 t: AssistTranslations;\n}\n\nconst Settings = React.createContext<AssistSettings | null>(null);\n\nexport interface AssistProviderProps {\n /** The model every assisted field below asks — `kanzo(\"kanzo-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 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, translations, children } = props;\n const value = React.useMemo<AssistSettings>(\n () => ({\n model,\n context: typeof context === \"function\" ? context : () => context,\n onEvent,\n t: { ...ENGLISH, ...translations },\n }),\n [context, model, onEvent, 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 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} />\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\nfunction Failure({ error }: { error: string | null }) {\n if (error === null) return null;\n return (\n <ark.p className=\"text-destructive-foreground text-sm\" data-slot=\"assist-error\" role=\"alert\">\n {error}\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.error !== null ? (\n <Failure error={list.error} />\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\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":";;;;;;;;;AA6CA;AAAoC;AAC1B;AACA;AACA;AACA;AACG;AACC;AACH;AACK;AACJ;AACH;AAET;AA+BO;AACL;AACoB;AACX;AACL;AACyD;AACzD;AACoB;AAAa;AAEG;AAExC;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;AAIX;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;AACkC;AAGkB;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;AAEA;AACE;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;AAIX;AACE;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;;;;;"}
|