@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/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kanzo-tech/ai",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Kanzo AI surfaces —
|
|
3
|
+
"version": "0.17.0",
|
|
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",
|
|
7
7
|
"sideEffects": [
|
|
@@ -19,42 +19,42 @@
|
|
|
19
19
|
"types": "./dist/index.d.ts",
|
|
20
20
|
"import": "./dist/index.js"
|
|
21
21
|
},
|
|
22
|
-
"./markdown": {
|
|
23
|
-
"types": "./dist/markdown.d.ts",
|
|
24
|
-
"import": "./dist/markdown.js"
|
|
25
|
-
},
|
|
26
22
|
"./tailwind.css": "./src/tailwind.css",
|
|
27
23
|
"./package.json": "./package.json"
|
|
28
24
|
},
|
|
29
|
-
"//peers": "
|
|
25
|
+
"//peers": "@kanzo-tech/ui and Ark are required peers rather than dependencies — two copies of Ark would mean two portals and two focus traps. `ai` and `@ai-sdk/react` are REQUIRED peers for the reason @kanzo-tech/llm gives: the host's agent and these components must share one UIMessage, one transport and one useChat. streamdown is a dependency now: Chat renders markdown, and a conversation that draws plain text is not one this package offers.",
|
|
30
26
|
"peerDependencies": {
|
|
27
|
+
"@ai-sdk/react": "^4.0.0",
|
|
31
28
|
"@ark-ui/react": "^5.37.2",
|
|
29
|
+
"ai": "^7.0.0",
|
|
32
30
|
"lucide-react": ">=1",
|
|
33
31
|
"react": ">=19",
|
|
34
32
|
"react-dom": ">=19",
|
|
35
|
-
"
|
|
36
|
-
"@kanzo-tech/ui": "0.16.0"
|
|
33
|
+
"@kanzo-tech/ui": "0.17.0"
|
|
37
34
|
},
|
|
38
35
|
"dependencies": {
|
|
39
|
-
"
|
|
36
|
+
"streamdown": "^2.5.0",
|
|
37
|
+
"tailwind-variants": "^3.2.2",
|
|
38
|
+
"@kanzo-tech/llm": "0.17.0"
|
|
40
39
|
},
|
|
41
40
|
"devDependencies": {
|
|
41
|
+
"@ai-sdk/react": "^4.0.129",
|
|
42
42
|
"@ark-ui/react": "^5.37.2",
|
|
43
43
|
"@testing-library/dom": "^10.4.1",
|
|
44
44
|
"@testing-library/react": "^16.3.2",
|
|
45
45
|
"@testing-library/user-event": "^14.6.1",
|
|
46
46
|
"@types/react": "^19.0.0",
|
|
47
47
|
"@types/react-dom": "^19.0.0",
|
|
48
|
+
"ai": "^7.0.126",
|
|
48
49
|
"jsdom": "^29.1.1",
|
|
49
50
|
"lucide-react": "^1.23.0",
|
|
50
51
|
"react": "^19.0.0",
|
|
51
52
|
"react-dom": "^19.0.0",
|
|
52
53
|
"rollup-plugin-preserve-directives": "^0.4.0",
|
|
53
|
-
"
|
|
54
|
-
"@kanzo-tech/ui": "0.
|
|
55
|
-
"@kanzo-tech/theme": "0.16.0"
|
|
54
|
+
"@kanzo-tech/theme": "0.17.0",
|
|
55
|
+
"@kanzo-tech/ui": "0.17.0"
|
|
56
56
|
},
|
|
57
|
-
"//size-limit": "
|
|
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": [
|
|
59
59
|
{
|
|
60
60
|
"name": "root barrel (JS)",
|
|
@@ -66,31 +66,15 @@
|
|
|
66
66
|
"react/jsx-runtime",
|
|
67
67
|
"@ark-ui/react",
|
|
68
68
|
"@kanzo-tech/ui",
|
|
69
|
-
"
|
|
70
|
-
"tailwind-variants"
|
|
71
|
-
]
|
|
72
|
-
},
|
|
73
|
-
{
|
|
74
|
-
"name": "markdown subpath (JS)",
|
|
75
|
-
"path": "dist/markdown.js",
|
|
76
|
-
"limit": "3 kB",
|
|
77
|
-
"ignore": [
|
|
78
|
-
"react",
|
|
79
|
-
"react-dom",
|
|
80
|
-
"react/jsx-runtime",
|
|
81
|
-
"@ark-ui/react",
|
|
82
|
-
"@kanzo-tech/ui",
|
|
69
|
+
"@kanzo-tech/llm",
|
|
83
70
|
"lucide-react",
|
|
84
71
|
"tailwind-variants",
|
|
85
|
-
"streamdown"
|
|
72
|
+
"streamdown",
|
|
73
|
+
"ai",
|
|
74
|
+
"@ai-sdk/react"
|
|
86
75
|
]
|
|
87
76
|
}
|
|
88
77
|
],
|
|
89
|
-
"peerDependenciesMeta": {
|
|
90
|
-
"streamdown": {
|
|
91
|
-
"optional": true
|
|
92
|
-
}
|
|
93
|
-
},
|
|
94
78
|
"repository": {
|
|
95
79
|
"type": "git",
|
|
96
80
|
"url": "git+https://github.com/Kanzo-Tech/ui.git",
|
package/dist/complete.d.ts
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import { AiMarkProps } from './ai-mark.js';
|
|
2
|
-
import { InlineCompletionRequest } from './use-ai.js';
|
|
3
|
-
import * as React from "react";
|
|
4
|
-
export interface CompleteRootProps {
|
|
5
|
-
complete: (request: InlineCompletionRequest) => AsyncIterable<string>;
|
|
6
|
-
value: string;
|
|
7
|
-
onValueChange: (value: string) => void;
|
|
8
|
-
debounceMs?: number;
|
|
9
|
-
minLength?: number;
|
|
10
|
-
/**
|
|
11
|
-
* What a screen reader hears once, when a suggestion lands.
|
|
12
|
-
*
|
|
13
|
-
* @default "Suggestion ready. Press Tab to accept, Escape to dismiss."
|
|
14
|
-
*/
|
|
15
|
-
announcement?: string;
|
|
16
|
-
className?: string;
|
|
17
|
-
children: React.ReactNode;
|
|
18
|
-
}
|
|
19
|
-
export declare function CompleteRoot(props: CompleteRootProps): React.JSX.Element;
|
|
20
|
-
export declare function CompleteTextarea({ children }: {
|
|
21
|
-
children: React.ReactElement;
|
|
22
|
-
}): React.JSX.Element;
|
|
23
|
-
/**
|
|
24
|
-
* The ✨, bound to the stream — the same mark `SuggestTrigger` wears, so an assisted field looks
|
|
25
|
-
* assisted whichever of the two it has.
|
|
26
|
-
*
|
|
27
|
-
* Pressing it **takes what is on offer and asks when there is nothing**. That is one gesture with
|
|
28
|
-
* two readings on purpose: it is the only way to accept without a keyboard, and the only way back
|
|
29
|
-
* after Escape, which otherwise ends the field's assistance until the next keystroke.
|
|
30
|
-
*/
|
|
31
|
-
export declare function CompleteMark(props: Omit<AiMarkProps, "busy" | "offering"> & {
|
|
32
|
-
/**
|
|
33
|
-
* The accessible name while a suggestion is on offer, when `label` names the mark at rest.
|
|
34
|
-
*
|
|
35
|
-
* @default "Accept suggestion"
|
|
36
|
-
*/
|
|
37
|
-
offeringLabel?: string;
|
|
38
|
-
}): React.JSX.Element;
|
|
39
|
-
/**
|
|
40
|
-
* The continuation drawn where it will land, over the field itself. It wraps and scrolls with the
|
|
41
|
-
* field rather than measuring whether a line of it fits.
|
|
42
|
-
*/
|
|
43
|
-
export declare function CompleteGhost({ className }: {
|
|
44
|
-
className?: string;
|
|
45
|
-
}): React.JSX.Element;
|
|
46
|
-
/**
|
|
47
|
-
* The keys, while there is something to take.
|
|
48
|
-
*
|
|
49
|
-
* The compound already told one class of reader: the `sr-only` region announces *Press Tab to
|
|
50
|
-
* accept, Escape to dismiss* the moment an offer lands. A sighted keyboard user got nothing —
|
|
51
|
-
* `CompleteHint` carries the keys too, so a composition that uses the ghost taught the gesture
|
|
52
|
-
* nowhere. This is the visible twin of that sentence, and it is a part rather than a fixture
|
|
53
|
-
* because only the caller knows where it fits: beside the ✨ in a `block-end` addon, under the
|
|
54
|
-
* field, nowhere at all on a form of eleven where one line of instruction beats eleven.
|
|
55
|
-
*
|
|
56
|
-
* There is no reference to copy. Gmail taught Tab with a popup shown once ever; Copilot puts a
|
|
57
|
-
* toolbar under the pointer, which is a gesture a keyboard cannot make. Naming the key next to the
|
|
58
|
-
* offer is the version that works for both.
|
|
59
|
-
*/
|
|
60
|
-
export declare function CompleteKeys({ acceptLabel, className, dismissLabel, }: {
|
|
61
|
-
/** The word after the `Tab` key. */
|
|
62
|
-
acceptLabel?: string;
|
|
63
|
-
className?: string;
|
|
64
|
-
/** The word after the `Esc` key. */
|
|
65
|
-
dismissLabel?: string;
|
|
66
|
-
}): React.JSX.Element | null;
|
|
67
|
-
/** Below-field alternative to `CompleteGhost`, for a field the overlay cannot sit over. */
|
|
68
|
-
export declare function CompleteHint({ acceptLabel, className, dismissLabel, }: {
|
|
69
|
-
/** The word after the `Tab` key, as on `CompleteKeys`. */
|
|
70
|
-
acceptLabel?: string;
|
|
71
|
-
className?: string;
|
|
72
|
-
/** The word after the `Esc` key, as on `CompleteKeys`. */
|
|
73
|
-
dismissLabel?: string;
|
|
74
|
-
}): React.JSX.Element | null;
|
|
75
|
-
/**
|
|
76
|
-
* What went wrong, in place. Renders nothing when nothing did.
|
|
77
|
-
*
|
|
78
|
-
* `Suggest` has always shown its failures — the strip prints them where the candidates would be —
|
|
79
|
-
* and this side showed none at all: the ✨ stopped spinning and the field said nothing, because
|
|
80
|
-
* `error` was on the hook and no part read it. It is a separate part rather than a line inside
|
|
81
|
-
* `CompleteHint` because the ghost surface is usually `CompleteGhost`, an overlay with no room
|
|
82
|
-
* under it, and a failure has to be readable in both.
|
|
83
|
-
*/
|
|
84
|
-
export declare function CompleteError({ className }: {
|
|
85
|
-
className?: string;
|
|
86
|
-
}): React.JSX.Element | null;
|
|
87
|
-
//# sourceMappingURL=complete.d.ts.map
|
package/dist/complete.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"complete.d.ts","sourceRoot":"","sources":["../src/complete.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,EAAU,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AACxD,OAAO,EAEL,KAAK,uBAAuB,EAG7B,MAAM,aAAa,CAAC;AA+FrB,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,CAAC,OAAO,EAAE,uBAAuB,KAAK,aAAa,CAAC,MAAM,CAAC,CAAC;IACtE,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,iBAAiB,qBAyLpD;AAED,wBAAgB,gBAAgB,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,KAAK,CAAC,YAAY,CAAA;CAAE,qBAkB9E;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAC1B,KAAK,EAAE,IAAI,CAAC,WAAW,EAAE,MAAM,GAAG,UAAU,CAAC,GAAG;IAC9C;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,qBAmBF;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,EAAE,SAAS,EAAE,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,qBA8FlE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,YAAY,CAAC,EAC3B,WAAsB,EACtB,SAAS,EACT,YAAwB,GACzB,EAAE;IACD,oCAAoC;IACpC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oCAAoC;IACpC,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,4BAgBA;AAED,2FAA2F;AAC3F,wBAAgB,YAAY,CAAC,EAC3B,WAAW,EACX,SAAS,EACT,YAAY,GACb,EAAE;IACD,0DAA0D;IAC1D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,0DAA0D;IAC1D,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,4BAYA;AAGD;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,EAAE,SAAS,EAAE,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,4BAYlE"}
|
package/dist/complete.js
DELETED
|
@@ -1,278 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import { jsx as m, jsxs as v } from "react/jsx-runtime";
|
|
3
|
-
import { ark as f } from "@ark-ui/react/factory";
|
|
4
|
-
import * as c from "react";
|
|
5
|
-
import { cn as y, Kbd as H } from "@kanzo-tech/ui";
|
|
6
|
-
import { AiMark as _ } from "./ai-mark.js";
|
|
7
|
-
import { useInlineCompletion as G, MIN_COMPLETE_LENGTH as O } from "./use-ai.js";
|
|
8
|
-
const M = c.createContext(null), x = (r) => {
|
|
9
|
-
const e = c.useContext(M);
|
|
10
|
-
if (!e) throw new Error(`${r} must render inside <CompleteRoot>`);
|
|
11
|
-
return e;
|
|
12
|
-
}, z = [
|
|
13
|
-
"fontFamily",
|
|
14
|
-
"fontSize",
|
|
15
|
-
"fontWeight",
|
|
16
|
-
"fontStyle",
|
|
17
|
-
"letterSpacing",
|
|
18
|
-
"lineHeight",
|
|
19
|
-
"textTransform",
|
|
20
|
-
"paddingTop",
|
|
21
|
-
"paddingRight",
|
|
22
|
-
"paddingBottom",
|
|
23
|
-
"paddingLeft",
|
|
24
|
-
"borderTopWidth",
|
|
25
|
-
"borderRightWidth",
|
|
26
|
-
"borderBottomWidth",
|
|
27
|
-
"borderLeftWidth"
|
|
28
|
-
], S = (r) => Number.parseFloat(r) || 0, V = (r, e) => {
|
|
29
|
-
const i = getComputedStyle(r), s = i.direction === "rtl", l = r.getBoundingClientRect(), a = e.getBoundingClientRect(), b = r.offsetWidth - r.clientWidth - S(i.borderLeftWidth) - S(i.borderRightWidth), u = {
|
|
30
|
-
borderStyle: "solid",
|
|
31
|
-
borderColor: "transparent",
|
|
32
|
-
insetBlockStart: l.top - a.top,
|
|
33
|
-
insetBlockEnd: a.bottom - l.bottom,
|
|
34
|
-
insetInlineStart: s ? a.right - l.right : l.left - a.left,
|
|
35
|
-
insetInlineEnd: (s ? l.left - a.left : a.right - l.right) + (b > 0 ? b : 0)
|
|
36
|
-
};
|
|
37
|
-
for (const d of z) u[d] = i[d];
|
|
38
|
-
return { metrics: u, rtl: s };
|
|
39
|
-
}, j = { metrics: {}, rtl: !1 }, q = "[mask-image:linear-gradient(to_bottom,#000_calc(100%-0.9lh),transparent)]", $ = (r) => {
|
|
40
|
-
var e;
|
|
41
|
-
return ((e = /^\s*\S+/.exec(r)) == null ? void 0 : e[0]) ?? "";
|
|
42
|
-
};
|
|
43
|
-
function ee(r) {
|
|
44
|
-
const {
|
|
45
|
-
announcement: e = "Suggestion ready. Press Tab to accept, Escape to dismiss.",
|
|
46
|
-
complete: i,
|
|
47
|
-
value: s,
|
|
48
|
-
onValueChange: l,
|
|
49
|
-
debounceMs: a,
|
|
50
|
-
minLength: b,
|
|
51
|
-
className: u,
|
|
52
|
-
children: d
|
|
53
|
-
} = r, o = G({ complete: i, debounceMs: a, minLength: b }), p = c.useRef(null), W = c.useRef(null), [h, R] = c.useState(s.length), [k, A] = c.useState(j), g = o.ghost, E = c.useRef(null);
|
|
54
|
-
c.useLayoutEffect(() => {
|
|
55
|
-
const t = p.current, n = E.current;
|
|
56
|
-
!t || n === null || (E.current = null, t.setSelectionRange(n, n), R(n));
|
|
57
|
-
}, [s]), c.useLayoutEffect(() => {
|
|
58
|
-
const t = p.current, n = W.current;
|
|
59
|
-
if (!t || !n) return;
|
|
60
|
-
const C = () => A(V(t, n));
|
|
61
|
-
if (C(), typeof ResizeObserver > "u") return;
|
|
62
|
-
const N = new ResizeObserver(C);
|
|
63
|
-
return N.observe(t), N.observe(n), () => N.disconnect();
|
|
64
|
-
}, []);
|
|
65
|
-
const K = o.dismiss, L = c.useCallback(() => {
|
|
66
|
-
const t = p.current;
|
|
67
|
-
if (!t || E.current !== null) return;
|
|
68
|
-
const n = t.selectionStart ?? t.value.length;
|
|
69
|
-
if ((t.selectionEnd ?? n) !== n) {
|
|
70
|
-
K();
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
R(n);
|
|
74
|
-
}, [K]);
|
|
75
|
-
c.useEffect(() => {
|
|
76
|
-
const t = p.current;
|
|
77
|
-
if (!t) return;
|
|
78
|
-
const n = () => {
|
|
79
|
-
document.activeElement === t && L();
|
|
80
|
-
};
|
|
81
|
-
return document.addEventListener("selectionchange", n), () => document.removeEventListener("selectionchange", n);
|
|
82
|
-
}, [L]);
|
|
83
|
-
const D = c.useCallback(
|
|
84
|
-
(t) => {
|
|
85
|
-
const n = t.target, C = n.selectionStart ?? n.value.length;
|
|
86
|
-
R(C), l(n.value), o.setValue(n.value, C);
|
|
87
|
-
},
|
|
88
|
-
[o, l]
|
|
89
|
-
), w = c.useCallback(
|
|
90
|
-
(t) => {
|
|
91
|
-
const n = s.slice(0, h) + t + s.slice(h);
|
|
92
|
-
return E.current = h + t.length, l(n), { next: n, at: h + t.length };
|
|
93
|
-
},
|
|
94
|
-
[h, l, s]
|
|
95
|
-
), T = c.useCallback(() => {
|
|
96
|
-
var t;
|
|
97
|
-
g && (w(g), o.dismiss(), (t = p.current) == null || t.focus());
|
|
98
|
-
}, [o, g, w]), B = c.useCallback(() => {
|
|
99
|
-
const t = $(g);
|
|
100
|
-
if (!t) return;
|
|
101
|
-
const { next: n, at: C } = w(t);
|
|
102
|
-
o.setValue(n, C);
|
|
103
|
-
}, [o, g, w]), I = c.useCallback(() => {
|
|
104
|
-
const t = p.current, n = t && t === document.activeElement ? t.selectionStart ?? h : h;
|
|
105
|
-
R(n), o.ask(s, n), t == null || t.focus(), t == null || t.setSelectionRange(n, n);
|
|
106
|
-
}, [h, o, s]), P = c.useCallback(
|
|
107
|
-
(t) => {
|
|
108
|
-
if (t.defaultPrevented || !g) return;
|
|
109
|
-
const n = k.rtl ? "ArrowLeft" : "ArrowRight";
|
|
110
|
-
t.key === "Tab" ? (t.preventDefault(), T()) : t.key === n && (t.ctrlKey || t.metaKey) ? (t.preventDefault(), B()) : t.key === "Escape" && o.dismiss();
|
|
111
|
-
},
|
|
112
|
-
[T, B, o, k.rtl, g]
|
|
113
|
-
), F = {
|
|
114
|
-
fieldRef: p,
|
|
115
|
-
value: s,
|
|
116
|
-
ghost: g,
|
|
117
|
-
caret: h,
|
|
118
|
-
rtl: k.rtl,
|
|
119
|
-
status: o.status,
|
|
120
|
-
error: o.error,
|
|
121
|
-
askable: s.trim().length >= (b ?? O),
|
|
122
|
-
metrics: k.metrics,
|
|
123
|
-
onChange: D,
|
|
124
|
-
onKeyDown: P,
|
|
125
|
-
syncCaret: L,
|
|
126
|
-
accept: T,
|
|
127
|
-
request: I
|
|
128
|
-
};
|
|
129
|
-
return /* @__PURE__ */ m(M.Provider, { value: F, children: /* @__PURE__ */ v(f.div, { className: y("relative w-full min-w-0", u), "data-slot": "complete", ref: W, children: [
|
|
130
|
-
d,
|
|
131
|
-
/* @__PURE__ */ m(f.span, { "aria-live": "polite", className: "sr-only", "data-slot": "complete-status", children: g ? e : "" })
|
|
132
|
-
] }) });
|
|
133
|
-
}
|
|
134
|
-
function te({ children: r }) {
|
|
135
|
-
const e = x("CompleteTextarea");
|
|
136
|
-
return /* @__PURE__ */ m(
|
|
137
|
-
f.textarea,
|
|
138
|
-
{
|
|
139
|
-
"aria-keyshortcuts": e.ghost ? "Tab Escape" : void 0,
|
|
140
|
-
asChild: !0,
|
|
141
|
-
"data-slot": "complete-textarea",
|
|
142
|
-
onChange: e.onChange,
|
|
143
|
-
onClick: e.syncCaret,
|
|
144
|
-
onKeyDown: e.onKeyDown,
|
|
145
|
-
onKeyUp: e.syncCaret,
|
|
146
|
-
onSelect: e.syncCaret,
|
|
147
|
-
ref: e.fieldRef,
|
|
148
|
-
value: e.value,
|
|
149
|
-
children: r
|
|
150
|
-
}
|
|
151
|
-
);
|
|
152
|
-
}
|
|
153
|
-
function ne(r) {
|
|
154
|
-
const { label: e = "AI assist", offeringLabel: i = "Accept suggestion", ...s } = r, l = x("CompleteMark"), a = l.ghost.length > 0;
|
|
155
|
-
return /* @__PURE__ */ m(
|
|
156
|
-
_,
|
|
157
|
-
{
|
|
158
|
-
busy: l.status === "loading" && !a,
|
|
159
|
-
disabled: !a && !l.askable,
|
|
160
|
-
label: a ? i : e,
|
|
161
|
-
offering: a,
|
|
162
|
-
onClick: a ? l.accept : l.request,
|
|
163
|
-
...s
|
|
164
|
-
}
|
|
165
|
-
);
|
|
166
|
-
}
|
|
167
|
-
function re({ className: r }) {
|
|
168
|
-
const e = x("CompleteGhost"), i = c.useRef(null), [s, l] = c.useState(!1);
|
|
169
|
-
c.useLayoutEffect(() => {
|
|
170
|
-
const u = e.fieldRef.current, d = i.current;
|
|
171
|
-
if (!u || !d) return;
|
|
172
|
-
if (!e.ghost) {
|
|
173
|
-
u.style.removeProperty("min-height");
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
const o = getComputedStyle(u), p = d.scrollHeight + S(o.borderTopWidth) + S(o.borderBottomWidth);
|
|
177
|
-
p > u.getBoundingClientRect().height + 1 && u.style.setProperty("min-height", `${p}px`);
|
|
178
|
-
}, [e.fieldRef, e.ghost, e.value]), c.useLayoutEffect(() => {
|
|
179
|
-
const u = e.fieldRef.current, d = i.current;
|
|
180
|
-
if (!u || !d) return;
|
|
181
|
-
const o = () => {
|
|
182
|
-
d.scrollLeft = u.scrollLeft, d.scrollTop = u.scrollTop;
|
|
183
|
-
};
|
|
184
|
-
return o(), l(d.scrollHeight > d.clientHeight), u.addEventListener("scroll", o), () => u.removeEventListener("scroll", o);
|
|
185
|
-
}, [e.caret, e.fieldRef, e.value, e.ghost]);
|
|
186
|
-
const a = e.ghost.length > 0, b = Math.min(e.caret, e.value.length);
|
|
187
|
-
return /* @__PURE__ */ v(
|
|
188
|
-
f.div,
|
|
189
|
-
{
|
|
190
|
-
"aria-hidden": !0,
|
|
191
|
-
className: y(
|
|
192
|
-
"pointer-events-none absolute inset-0 overflow-hidden text-transparent",
|
|
193
|
-
"whitespace-pre-wrap break-words",
|
|
194
|
-
// **A continuation that does not fit fades; it is not guillotined.** The box is
|
|
195
|
-
// `overflow: hidden`, so without this a too-long ghost ends on a hard edge — mid-letter
|
|
196
|
-
// against the ✨. Applied only when the measurement above says it is genuinely cut, which
|
|
197
|
-
// is now only when a caller has pinned the field's height.
|
|
198
|
-
s && q,
|
|
199
|
-
!a && "invisible",
|
|
200
|
-
r
|
|
201
|
-
),
|
|
202
|
-
"data-slot": "complete-ghost",
|
|
203
|
-
ref: i,
|
|
204
|
-
style: e.metrics,
|
|
205
|
-
children: [
|
|
206
|
-
/* @__PURE__ */ m(f.span, { className: "invisible", children: e.value.slice(0, b) }),
|
|
207
|
-
/* @__PURE__ */ m(f.span, { className: "text-faint", children: e.ghost }),
|
|
208
|
-
/* @__PURE__ */ m(f.span, { className: "invisible", children: e.value.slice(b) })
|
|
209
|
-
]
|
|
210
|
-
}
|
|
211
|
-
);
|
|
212
|
-
}
|
|
213
|
-
function U({
|
|
214
|
-
acceptLabel: r = "accept",
|
|
215
|
-
className: e,
|
|
216
|
-
dismissLabel: i = "dismiss"
|
|
217
|
-
}) {
|
|
218
|
-
return x("CompleteKeys").ghost ? /* @__PURE__ */ v(
|
|
219
|
-
f.span,
|
|
220
|
-
{
|
|
221
|
-
className: y("inline-flex items-center gap-2 text-muted-foreground text-xs", e),
|
|
222
|
-
"data-slot": "complete-keys",
|
|
223
|
-
children: [
|
|
224
|
-
/* @__PURE__ */ v(f.span, { className: "inline-flex items-center gap-1 whitespace-nowrap", children: [
|
|
225
|
-
/* @__PURE__ */ m(H, { children: "Tab" }),
|
|
226
|
-
" ",
|
|
227
|
-
r
|
|
228
|
-
] }),
|
|
229
|
-
/* @__PURE__ */ v(f.span, { className: "inline-flex items-center gap-1 whitespace-nowrap", children: [
|
|
230
|
-
/* @__PURE__ */ m(H, { children: "Esc" }),
|
|
231
|
-
" ",
|
|
232
|
-
i
|
|
233
|
-
] })
|
|
234
|
-
]
|
|
235
|
-
}
|
|
236
|
-
) : null;
|
|
237
|
-
}
|
|
238
|
-
function se({
|
|
239
|
-
acceptLabel: r,
|
|
240
|
-
className: e,
|
|
241
|
-
dismissLabel: i
|
|
242
|
-
}) {
|
|
243
|
-
const s = x("CompleteHint");
|
|
244
|
-
return s.ghost ? /* @__PURE__ */ v(
|
|
245
|
-
f.p,
|
|
246
|
-
{
|
|
247
|
-
className: y("mt-1.5 whitespace-pre-wrap break-words text-muted-foreground text-sm", e),
|
|
248
|
-
"data-slot": "complete-hint",
|
|
249
|
-
children: [
|
|
250
|
-
/* @__PURE__ */ m(f.span, { className: "text-faint", children: s.ghost.replace(/^\s+/, "") }),
|
|
251
|
-
" ",
|
|
252
|
-
/* @__PURE__ */ m(U, { acceptLabel: r, dismissLabel: i })
|
|
253
|
-
]
|
|
254
|
-
}
|
|
255
|
-
) : null;
|
|
256
|
-
}
|
|
257
|
-
function oe({ className: r }) {
|
|
258
|
-
const e = x("CompleteError");
|
|
259
|
-
return e.error === null ? null : /* @__PURE__ */ m(
|
|
260
|
-
f.p,
|
|
261
|
-
{
|
|
262
|
-
className: y("mt-1.5 text-destructive-foreground text-sm", r),
|
|
263
|
-
"data-slot": "complete-error",
|
|
264
|
-
role: "alert",
|
|
265
|
-
children: e.error
|
|
266
|
-
}
|
|
267
|
-
);
|
|
268
|
-
}
|
|
269
|
-
export {
|
|
270
|
-
oe as CompleteError,
|
|
271
|
-
re as CompleteGhost,
|
|
272
|
-
se as CompleteHint,
|
|
273
|
-
U as CompleteKeys,
|
|
274
|
-
ne as CompleteMark,
|
|
275
|
-
ee as CompleteRoot,
|
|
276
|
-
te as CompleteTextarea
|
|
277
|
-
};
|
|
278
|
-
//# sourceMappingURL=complete.js.map
|
package/dist/complete.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"complete.js","sources":["../src/complete.tsx"],"sourcesContent":["\"use client\";\n\nimport { ark } from \"@ark-ui/react/factory\";\nimport * as React from \"react\";\nimport { cn, Kbd } from \"@kanzo-tech/ui\";\nimport { AiMark, type AiMarkProps } from \"./ai-mark.js\";\nimport {\n type AiStatus,\n type InlineCompletionRequest,\n MIN_COMPLETE_LENGTH,\n useInlineCompletion,\n} from \"./use-ai.js\";\n\n/**\n * A `<textarea>`, and there is no `<input>` branch any more.\n *\n * **A one-line field takes candidates, not a continuation.** A continuation drawn over an\n * `<input>` can only ever show what fits in the remaining width — the field's own `scrollLeft`\n * cannot reveal text that is not in its value — so a long offer was unreadable by any gesture and\n * accepting it meant accepting blind. No reference does it either: Gmail continues a body, Copilot\n * an editor, and every single-line field in the wild offers a *list*. That is `Suggest`, and it is\n * already here.\n */\ntype FieldEl = HTMLTextAreaElement;\n\ninterface CompleteCtx {\n fieldRef: React.RefObject<FieldEl | null>;\n value: string;\n /** What is on offer at the caret. Empty when there is nothing. */\n ghost: string;\n /** Where the offer goes. The ghost is drawn here, not at the end of the value. */\n caret: number;\n rtl: boolean;\n status: AiStatus;\n error: string | null;\n /** The value is long enough for `complete` to be asked at all. */\n askable: boolean;\n metrics: React.CSSProperties;\n onChange: (e: React.ChangeEvent<FieldEl>) => void;\n onKeyDown: (e: React.KeyboardEvent<FieldEl>) => void;\n syncCaret: () => void;\n accept: () => void;\n request: () => void;\n}\n\nconst Ctx = React.createContext<CompleteCtx | null>(null);\nconst useCtx = (part: string) => {\n const c = React.useContext(Ctx);\n if (!c) throw new Error(`${part} must render inside <CompleteRoot>`);\n return c;\n};\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\ninterface FieldShape {\n metrics: React.CSSProperties;\n rtl: boolean;\n}\n\n// The overlay is positioned against the FIELD, not against the root, because the two are not the\n// same box once the ✨ lives inside an `InputGroup`: the group carries the border the input no\n// longer has, and a mirror pinned to the root paints a character-width off.\nconst px = (v: string) => Number.parseFloat(v) || 0;\n\nconst readField = (el: FieldEl, root: HTMLElement): FieldShape => {\n const cs = getComputedStyle(el);\n const rtl = cs.direction === \"rtl\";\n const box = el.getBoundingClientRect();\n const outer = root.getBoundingClientRect();\n // A scrolling textarea takes its scrollbar out of the text's width, and it takes it from the\n // inline-END side in both directions. Without this the mirror wraps a word earlier or later than\n // the field does wherever scrollbars are not overlays — which is not macOS, so it is exactly the\n // kind of divergence nobody sees here.\n const gutter = el.offsetWidth - el.clientWidth - px(cs.borderLeftWidth) - px(cs.borderRightWidth);\n const metrics: Record<string, string | number> = {\n borderStyle: \"solid\",\n borderColor: \"transparent\",\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:\n (rtl ? box.left - outer.left : outer.right - box.right) + (gutter > 0 ? gutter : 0),\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\nconst REST: FieldShape = { metrics: {}, rtl: false };\n\n/**\n * A safety net rather than the answer. The field grows to fit its offer, so it runs out of height\n * only where a caller has pinned a `max-height` — and there a fade beats a slice.\n *\n * There were three of these. The two horizontal ones masked a continuation that overran a\n * single-line field, which is the case that no longer exists.\n */\nconst GHOST_FADE =\n \"[mask-image:linear-gradient(to_bottom,#000_calc(100%-0.9lh),transparent)]\";\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\nexport interface CompleteRootProps {\n complete: (request: InlineCompletionRequest) => AsyncIterable<string>;\n value: string;\n onValueChange: (value: string) => void;\n debounceMs?: number;\n minLength?: number;\n /**\n * What a screen reader hears once, when a suggestion lands.\n *\n * @default \"Suggestion ready. Press Tab to accept, Escape to dismiss.\"\n */\n announcement?: string;\n className?: string;\n children: React.ReactNode;\n}\n\nexport function CompleteRoot(props: CompleteRootProps) {\n const {\n announcement = \"Suggestion ready. Press Tab to accept, Escape to dismiss.\",\n complete,\n value,\n onValueChange,\n debounceMs,\n minLength,\n className,\n children,\n } = props;\n const completion = useInlineCompletion({ complete, debounceMs, minLength });\n const fieldRef = React.useRef<FieldEl | null>(null);\n const rootRef = React.useRef<HTMLDivElement>(null);\n const [caret, setCaret] = React.useState(value.length);\n const [field, setField] = React.useState<FieldShape>(REST);\n\n const ghost = completion.ghost;\n\n // Where the caret must land after WE changed the value. The browser puts it wherever it likes\n // when React writes a new `value`, and taking a word out of the middle of a sentence is the case\n // that makes it obvious.\n const landing = React.useRef<number | null>(null);\n\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 // The ghost mirror wears the field's real metrics, so it aligns whatever the size/className.\n React.useLayoutEffect(() => {\n const el = fieldRef.current;\n const root = rootRef.current;\n if (!el || !root) return;\n const sync = () => setField(readField(el, root));\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 const dismiss = completion.dismiss;\n\n /**\n * **A range is not an insertion point, so an offer cannot survive one.**\n *\n * `selectionStart` is the HEAD of a selection, not a caret. Selecting a sentence — or pressing\n * ⌘A — moved our caret to the start of it while the offer stayed live, and the continuation was\n * then drawn from there: on top of the words it was made to follow. Taking it with Tab spliced it\n * in at the same wrong place, because `insert` splits the value at this number too.\n *\n * The fix is not to draw it somewhere else. `InlineCompletionRequest` is explicitly the\n * degenerate range — an insertion point — so while text is selected there is no place the offer\n * *could* go, and the honest answer is to let it go. Collapsing the selection asks again, which\n * costs a debounce and no more.\n */\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 dismiss();\n return;\n }\n setCaret(start);\n }, [dismiss]);\n\n /**\n * The caret is where the ghost is **drawn**, so ours drifting from the field's is not a missed\n * update — it paints a continuation on top of the value. Seen live: a ghost starting at character\n * zero, over the sentence it was continuing.\n *\n * `selectionchange` is the only event that reports every way a caret moves — arrows, clicks,\n * drags, Home/End, and a script calling `setSelectionRange`. `click` and `keyup` catch most of\n * them and are kept because they fire first; this catches the rest.\n */\n React.useEffect(() => {\n const el = fieldRef.current;\n if (!el) return;\n const onSelectionChange = () => {\n if (document.activeElement === el) syncCaret();\n };\n document.addEventListener(\"selectionchange\", onSelectionChange);\n return () => document.removeEventListener(\"selectionchange\", onSelectionChange);\n }, [syncCaret]);\n\n const onChange = React.useCallback(\n (e: React.ChangeEvent<FieldEl>) => {\n const el = e.target;\n const at = el.selectionStart ?? el.value.length;\n setCaret(at);\n onValueChange(el.value);\n completion.setValue(el.value, at);\n },\n [completion, onValueChange],\n );\n\n /** Put `text` in at the caret and leave the caret after it. */\n const insert = React.useCallback(\n (text: string) => {\n const next = value.slice(0, caret) + text + value.slice(caret);\n landing.current = caret + text.length;\n onValueChange(next);\n return { next, at: caret + text.length };\n },\n [caret, onValueChange, value],\n );\n\n const accept = React.useCallback(() => {\n if (!ghost) return;\n insert(ghost);\n completion.dismiss();\n fieldRef.current?.focus();\n }, [completion, ghost, insert]);\n\n // Taking one word is not a new request and not a second machine: the word goes in, the caret\n // moves past it, and the offer survives by the same rule that keeps it alive when the reader\n // types that word themselves. There used to be a `taken` offset here doing it by hand.\n const acceptWord = React.useCallback(() => {\n const word = nextWord(ghost);\n if (!word) return;\n const { next, at } = insert(word);\n completion.setValue(next, at);\n }, [completion, ghost, insert]);\n\n const request = React.useCallback(() => {\n const el = fieldRef.current;\n // Where the offer goes, and the two have to agree. Focusing a field nobody has focused yet puts\n // the DOM caret at 0 while our own is still at the end, and the ghost would then be drawn in one\n // place and inserted in another.\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 }, [caret, completion, value]);\n\n const onKeyDown = React.useCallback(\n (e: React.KeyboardEvent<FieldEl>) => {\n if (e.defaultPrevented || !ghost) return;\n // Forward, not right: the key that means \"next word\" is mirrored under RTL, and this is the\n // one place in the compound where a key stands for a direction.\n const forward = field.rtl ? \"ArrowLeft\" : \"ArrowRight\";\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();\n }\n },\n [accept, acceptWord, completion, field.rtl, ghost],\n );\n\n const ctx: CompleteCtx = {\n fieldRef, value, ghost, caret,\n rtl: field.rtl,\n status: completion.status,\n error: completion.error,\n askable: value.trim().length >= (minLength ?? MIN_COMPLETE_LENGTH),\n metrics: field.metrics,\n onChange, onKeyDown, syncCaret, accept, request,\n };\n\n return (\n <Ctx.Provider value={ctx}>\n <ark.div className={cn(\"relative w-full min-w-0\", className)} data-slot=\"complete\" ref={rootRef}>\n {children}\n {/* The ghost itself is `aria-hidden` and streams a chunk a frame, so announcing IT would\n read the same sentence twenty times. This sentence changes once, when there is\n something to take. */}\n <ark.span aria-live=\"polite\" className=\"sr-only\" data-slot=\"complete-status\">\n {ghost ? announcement : \"\"}\n </ark.span>\n </ark.div>\n </Ctx.Provider>\n );\n}\n\nexport function CompleteTextarea({ children }: { children: React.ReactElement }) {\n const ctx = useCtx(\"CompleteTextarea\");\n return (\n <ark.textarea\n aria-keyshortcuts={ctx.ghost ? \"Tab Escape\" : undefined}\n asChild\n data-slot=\"complete-textarea\"\n onChange={ctx.onChange}\n onClick={ctx.syncCaret}\n onKeyDown={ctx.onKeyDown}\n onKeyUp={ctx.syncCaret}\n onSelect={ctx.syncCaret}\n ref={ctx.fieldRef as React.Ref<HTMLTextAreaElement>}\n value={ctx.value}\n >\n {children}\n </ark.textarea>\n );\n}\n\n/**\n * The ✨, bound to the stream — the same mark `SuggestTrigger` wears, so an assisted field looks\n * assisted whichever of the two it has.\n *\n * Pressing it **takes what is on offer and asks when there is nothing**. That is one gesture with\n * two readings on purpose: it is the only way to accept without a keyboard, and the only way back\n * after Escape, which otherwise ends the field's assistance until the next keystroke.\n */\nexport function CompleteMark(\n props: Omit<AiMarkProps, \"busy\" | \"offering\"> & {\n /**\n * The accessible name while a suggestion is on offer, when `label` names the mark at rest.\n *\n * @default \"Accept suggestion\"\n */\n offeringLabel?: string;\n }\n) {\n const { label = \"AI assist\", offeringLabel = \"Accept suggestion\", ...rest } = props;\n const ctx = useCtx(\"CompleteMark\");\n const offering = ctx.ghost.length > 0;\n\n return (\n <AiMark\n busy={ctx.status === \"loading\" && !offering}\n // The one binding that legitimately differs from `SuggestMark`, and it is domain and not\n // taste: there is nothing to continue below `minLength`, while a candidate source can answer\n // from an empty field. Everything else about the two marks is bound identically.\n disabled={!offering && !ctx.askable}\n label={offering ? offeringLabel : label}\n offering={offering}\n onClick={offering ? ctx.accept : ctx.request}\n {...rest}\n />\n );\n}\n\n/**\n * The continuation drawn where it will land, over the field itself. It wraps and scrolls with the\n * field rather than measuring whether a line of it fits.\n */\nexport function CompleteGhost({ className }: { className?: string }) {\n const ctx = useCtx(\"CompleteGhost\");\n const boxRef = React.useRef<HTMLDivElement>(null);\n const [clipped, setClipped] = React.useState(false);\n\n /**\n * **A continuation that does not fit makes the field taller. It is not cut.**\n *\n * The fade below was the old answer and it was the wrong one: a wrapping field runs out of\n * *height*, and a mask on the last line still reads as a sentence sliced through the middle of a\n * line of type — with, on a `block-end` addon, the ✨ and the keys painted over the slice. Seen on\n * `docs/ai/fields`. An editor gets away with clipping because it can push the following lines\n * down; a `<textarea>` cannot, because the ghost is not in its value. So the field takes the\n * height the offer needs while the offer is on the table, and gives it back when it is not — which\n * is what a textarea does anyway when you type into it. `rows` becomes a floor rather than a fixed\n * size, which is what `rows` already means.\n *\n * **It grows and never shrinks while one offer is live.** Taking a word makes the ghost shorter,\n * and re-measuring down on every word would make the box jitter under the reader mid-sentence for\n * no gain — somebody taking a word is about to take another. It resets the moment there is no\n * ghost, which is every accept, every dismiss and every keystroke that kills the offer.\n *\n */\n React.useLayoutEffect(() => {\n const el = ctx.fieldRef.current;\n const box = boxRef.current;\n if (!el || !box) return;\n if (!ctx.ghost) {\n el.style.removeProperty(\"min-height\");\n return;\n }\n // `minHeight` and not `height`, measured: a `block-end` addon turns the group into a column\n // flex container and the field is `flex: 1 1 0%` in it, so the flex algorithm computes the used\n // height and an explicit `height` is ignored outright — the inline `104px` was on the element\n // and the box stayed 64. Raising the floor is what the flex algorithm does honour, and it is\n // also what this means: `rows` is a floor, and the offer asks for a taller one.\n const cs = getComputedStyle(el);\n const needed = box.scrollHeight + px(cs.borderTopWidth) + px(cs.borderBottomWidth);\n if (needed > el.getBoundingClientRect().height + 1)\n el.style.setProperty(\"min-height\", `${needed}px`);\n }, [ctx.fieldRef, ctx.ghost, ctx.value]);\n\n // An `overflow: hidden` box is still scrollable from script, so the mirror can be parked exactly\n // where the field parked its own text — which is what lets a ghost longer than the remaining\n // line be clipped at the edge instead of hidden entirely.\n React.useLayoutEffect(() => {\n const el = ctx.fieldRef.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 // Does the continuation fit in the space the field actually has? Measured rather than\n // assumed, because the answer changes with the field's size, with the value, and with every\n // chunk that arrives — and after the effect above it should be `false` unless a caller pinned\n // the height.\n setClipped(box.scrollHeight > box.clientHeight);\n el.addEventListener(\"scroll\", sync);\n return () => el.removeEventListener(\"scroll\", sync);\n }, [ctx.caret, ctx.fieldRef, ctx.value, ctx.ghost]);\n\n\n const show = ctx.ghost.length > 0;\n // A caret past the end of the value would split it in the wrong place; the value is the authority.\n const at = Math.min(ctx.caret, ctx.value.length);\n\n return (\n <ark.div\n aria-hidden\n className={cn(\n \"pointer-events-none absolute inset-0 overflow-hidden text-transparent\",\n \"whitespace-pre-wrap break-words\",\n // **A continuation that does not fit fades; it is not guillotined.** The box is\n // `overflow: hidden`, so without this a too-long ghost ends on a hard edge — mid-letter\n // against the ✨. Applied only when the measurement above says it is genuinely cut, which\n // is now only when a caller has pinned the field's height.\n clipped && GHOST_FADE,\n !show && \"invisible\",\n className,\n )}\n data-slot=\"complete-ghost\"\n ref={boxRef}\n style={ctx.metrics}\n >\n {/* The value holds its own space so the ghost starts exactly at the caret — `hidden` would\n collapse it. Split in two rather than one leading run: an offer is made *at* the caret,\n and what comes after it has to keep its place or the tail of the sentence shifts. */}\n <ark.span className=\"invisible\">{ctx.value.slice(0, at)}</ark.span>\n <ark.span className=\"text-faint\">{ctx.ghost}</ark.span>\n <ark.span className=\"invisible\">{ctx.value.slice(at)}</ark.span>\n </ark.div>\n );\n}\n\n/**\n * The keys, while there is something to take.\n *\n * The compound already told one class of reader: the `sr-only` region announces *Press Tab to\n * accept, Escape to dismiss* the moment an offer lands. A sighted keyboard user got nothing —\n * `CompleteHint` carries the keys too, so a composition that uses the ghost taught the gesture\n * nowhere. This is the visible twin of that sentence, and it is a part rather than a fixture\n * because only the caller knows where it fits: beside the ✨ in a `block-end` addon, under the\n * field, nowhere at all on a form of eleven where one line of instruction beats eleven.\n *\n * There is no reference to copy. Gmail taught Tab with a popup shown once ever; Copilot puts a\n * toolbar under the pointer, which is a gesture a keyboard cannot make. Naming the key next to the\n * offer is the version that works for both.\n */\nexport function CompleteKeys({\n acceptLabel = \"accept\",\n className,\n dismissLabel = \"dismiss\",\n}: {\n /** The word after the `Tab` key. */\n acceptLabel?: string;\n className?: string;\n /** The word after the `Esc` key. */\n dismissLabel?: string;\n}) {\n const ctx = useCtx(\"CompleteKeys\");\n if (!ctx.ghost) return null;\n return (\n <ark.span\n className={cn(\"inline-flex items-center gap-2 text-muted-foreground text-xs\", className)}\n data-slot=\"complete-keys\"\n >\n <ark.span className=\"inline-flex items-center gap-1 whitespace-nowrap\">\n <Kbd>Tab</Kbd> {acceptLabel}\n </ark.span>\n <ark.span className=\"inline-flex items-center gap-1 whitespace-nowrap\">\n <Kbd>Esc</Kbd> {dismissLabel}\n </ark.span>\n </ark.span>\n );\n}\n\n/** Below-field alternative to `CompleteGhost`, for a field the overlay cannot sit over. */\nexport function CompleteHint({\n acceptLabel,\n className,\n dismissLabel,\n}: {\n /** The word after the `Tab` key, as on `CompleteKeys`. */\n acceptLabel?: string;\n className?: string;\n /** The word after the `Esc` key, as on `CompleteKeys`. */\n dismissLabel?: string;\n}) {\n const ctx = useCtx(\"CompleteHint\");\n if (!ctx.ghost) return null;\n return (\n <ark.p\n className={cn(\"mt-1.5 whitespace-pre-wrap break-words text-muted-foreground text-sm\", className)}\n data-slot=\"complete-hint\"\n >\n <ark.span className=\"text-faint\">{ctx.ghost.replace(/^\\s+/, \"\")}</ark.span>{\" \"}\n <CompleteKeys acceptLabel={acceptLabel} dismissLabel={dismissLabel} />\n </ark.p>\n );\n}\n\n\n/**\n * What went wrong, in place. Renders nothing when nothing did.\n *\n * `Suggest` has always shown its failures — the strip prints them where the candidates would be —\n * and this side showed none at all: the ✨ stopped spinning and the field said nothing, because\n * `error` was on the hook and no part read it. It is a separate part rather than a line inside\n * `CompleteHint` because the ghost surface is usually `CompleteGhost`, an overlay with no room\n * under it, and a failure has to be readable in both.\n */\nexport function CompleteError({ className }: { className?: string }) {\n const ctx = useCtx(\"CompleteError\");\n if (ctx.error === null) return null;\n return (\n <ark.p\n className={cn(\"mt-1.5 text-destructive-foreground text-sm\", className)}\n data-slot=\"complete-error\"\n role=\"alert\"\n >\n {ctx.error}\n </ark.p>\n );\n}\n"],"names":[],"mappings":";;;;;;;AA6CA;AAEE;AACA;AACA;AACF;AAEgB;AACd;AAAc;AAAY;AAAc;AAAa;AAAiB;AACtE;AAAiB;AAAc;AAAgB;AAAiB;AAChE;AAAkB;AAAoB;AACxC;AAaE;AASiD;AAClC;AACA;AACoB;AACC;AACiC;AAEgB;AAErF;AACA;AACF;;AAeoC;AAA8B;AAkB3D;AACL;AAAM;AACW;AACf;AACA;AACA;AACA;AACA;AACA;AACA;AAeF;AACE;AAEA;AAGW;AAKX;AAEA;AACA;AAEA;AACA;AACA;AAEgB;AAGlB;AAgBE;AACA;AACA;AACA;AACE;AACA;AAAA;AAEF;AAAc;AAYhB;AACE;AACA;AACA;AACE;AAAmC;AAErC;AAC8E;AAGhF;AAAuB;AAEnB;AAEA;AAEgC;AAClC;AAC0B;AAIP;AAEjB;AACA;AAEgC;AAClC;AAC4B;;AAI5B;AAGkB;AAOlB;AACA;AACA;AACA;AAA4B;AAI5B;AAKA;AAG0B;AAGJ;AAEpB;AAGA;AACA;AAOa;AAEf;AACiD;AAG1B;AACvB;AAAU;AAAO;AAAO;AACb;AACQ;AACD;AAC4B;AAC/B;AACf;AAAU;AAAW;AAAW;AAAQ;AAG1C;AAGO;AAAA;AAMD;AAIR;AAEO;AACL;AACA;AACE;AAAK;AAAJ;AAC+C;AACvC;AACG;AACI;AACD;AACE;AACF;AACC;AACL;AACE;AAEV;AAAA;AAGP;AAUO;AAUL;AAIA;AACE;AAAC;AAAA;AACoC;AAIP;AACM;AAClC;AACqC;AACjC;AAAA;AAGV;AAMO;AACL;AAsBA;AACE;AAEA;AACA;AACE;AACA;AAAA;AAOF;AAEA;AACkD;AAOlD;AAEA;AACA;AACE;AACmB;AAErB;AAOkD;AAIpD;AAIA;AACE;AAAK;AAAJ;AACY;AACA;AACT;AACA;AAAA;AAAA;AAAA;AAAA;AAKW;AACF;AACT;AAAA;AAEQ;AACL;AACM;AAKX;AAAwD;AACZ;AACS;AAAA;AAAA;AAG3D;AAgBO;AAAsB;AACb;AACd;AAEF;AAQE;AAEE;AAAK;AAAJ;AACwF;AAC7E;AAEV;AACE;AAAQ;AAAM;AAAE;AAClB;AAEE;AAAQ;AAAM;AAAE;AAClB;AAAA;AAAA;AAGN;AAGO;AAAsB;AAC3B;AACA;AAEF;AAOE;AACA;AAEE;AAAK;AAAJ;AACgG;AACrF;AAEV;AAAgE;AAAY;AACR;AAAA;AAAA;AAG1E;AAYO;AACL;AACA;AAEE;AAAK;AAAJ;AACsE;AAC3D;AACL;AAEA;AAAA;AAGX;;;;;;;;;;"}
|
package/dist/message-part.d.ts
DELETED
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import { MessageRole } from './message.js';
|
|
2
|
-
import { RunState } from './task.js';
|
|
3
|
-
/**
|
|
4
|
-
* A turn as a **list of parts**, and the join every component in this package was missing.
|
|
5
|
-
*
|
|
6
|
-
* `Message`, `MessageText`, `Reasoning`, `Tool` and `Task` were each correct and none of them was
|
|
7
|
-
* wired to the next: a host held its own shape and translated into five sets of props by hand.
|
|
8
|
-
* keasy's `AskMessage` is the worked example — it carries `sql`, `reasoning`, `explanation` and a
|
|
9
|
-
* `phase` of `'generating' | 'executing' | 'explaining' | 'done'`, which is a tool part, a reasoning
|
|
10
|
-
* part, a text part and a state, hand-rolled. **That is the second consumer, and it existed before
|
|
11
|
-
* this file did.** So this is not a protocol invented ahead of its need.
|
|
12
|
-
*
|
|
13
|
-
* The shape mirrors the AI SDK's `UIMessage`, deliberately and without depending on it — the four
|
|
14
|
-
* properties that make theirs work are the four copied here:
|
|
15
|
-
*
|
|
16
|
-
* 1. **A message is a list of parts, not a string.** One `.map` with one `switch` on `part.type`
|
|
17
|
-
* renders a whole turn.
|
|
18
|
-
* 2. **State lives on the part**, so a renderer is told what to draw rather than inferring it.
|
|
19
|
-
* 3. **Generic in metadata and tools**, so a product extends the union instead of forking it.
|
|
20
|
-
* 4. **Transport stays separate.** Ours already is, and is smaller than theirs: a caller passes a
|
|
21
|
-
* function returning an `AsyncIterable`. Nothing here replaces that seam.
|
|
22
|
-
*
|
|
23
|
-
* ## Four variants, not nine
|
|
24
|
-
*
|
|
25
|
-
* Theirs has nine. `file`, `data-*`, `step-start` and `dynamic-tool` have no call site here, and
|
|
26
|
-
* `source` is **declined rather than deferred**: discovery does not cite a document, it runs a
|
|
27
|
-
* query, and provenance that is a statement plus rows is a tool call. It arrives the day an answer
|
|
28
|
-
* rests on a text somebody wrote.
|
|
29
|
-
*
|
|
30
|
-
* ## The two state vocabularies are the ones already in the package
|
|
31
|
-
*
|
|
32
|
-
* `RunState` — `pending` · `running` · `done` · `failed` — is `Task`'s and `Tool`'s, from one table
|
|
33
|
-
* with one label map and one icon family. It is not restated here, and the reference's
|
|
34
|
-
* `input-streaming | input-available | output-available | output-error` is not adopted: four states
|
|
35
|
-
* under two spellings is the drift this repo keeps deleting.
|
|
36
|
-
*
|
|
37
|
-
* `StreamState` is the other one, and it is two values because text and reasoning have two.
|
|
38
|
-
*
|
|
39
|
-
* ## What this file is not
|
|
40
|
-
*
|
|
41
|
-
* **It renders nothing.** There is no `<Message parts={…} />` here and that is the shape rather than
|
|
42
|
-
* an omission: which part a product renders, in what order, with what chrome around it, is the
|
|
43
|
-
* thing that differs per product — and a component that owned the `switch` would own those answers
|
|
44
|
-
* too. The `switch` belongs to the host; what this gives it is a union exhaustive enough that
|
|
45
|
-
* TypeScript tells the host when a case is missing.
|
|
46
|
-
*/
|
|
47
|
-
/** Where a streamed part has got to. Two values, because text and reasoning have two. */
|
|
48
|
-
export type StreamState = "streaming" | "done";
|
|
49
|
-
/** Prose the model wrote. `MessageText` draws it. */
|
|
50
|
-
export interface AiTextPart {
|
|
51
|
-
type: "text";
|
|
52
|
-
text: string;
|
|
53
|
-
state?: StreamState;
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* The model's own working, folded away. `Reasoning` draws it.
|
|
57
|
-
*
|
|
58
|
-
* **No duration here**, and that is checked rather than assumed: `Reasoning` times itself off the
|
|
59
|
-
* `streaming` transition and renders *Thought for N seconds* from its own state. A `seconds` on the
|
|
60
|
-
* part would be a second measurement of one fact, and the one no component reads.
|
|
61
|
-
*/
|
|
62
|
-
export interface AiReasoningPart {
|
|
63
|
-
type: "reasoning";
|
|
64
|
-
text: string;
|
|
65
|
-
state?: StreamState;
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* One call the model made. `Tool` draws it.
|
|
69
|
-
*
|
|
70
|
-
* `input` and `output` are `unknown` here and **children** at the component: `ToolInput` and
|
|
71
|
-
* `ToolOutput` take what you render rather than a JSON blob, because we always know what the tool
|
|
72
|
-
* was. The part still carries the values, because a host that wants the JSON tree has to have them.
|
|
73
|
-
*
|
|
74
|
-
* `NAME` is the axis a product extends along. Give it a union of your tool names and a `switch` on
|
|
75
|
-
* `part.name` narrows `input` and `output` if you declare them per name.
|
|
76
|
-
*/
|
|
77
|
-
export interface AiToolPart<NAME extends string = string> {
|
|
78
|
-
type: "tool";
|
|
79
|
-
name: NAME;
|
|
80
|
-
state?: RunState;
|
|
81
|
-
input?: unknown;
|
|
82
|
-
output?: unknown;
|
|
83
|
-
/** Set when `state` is `failed`. */
|
|
84
|
-
errorText?: string;
|
|
85
|
-
}
|
|
86
|
-
/**
|
|
87
|
-
* One step of work, ours and with no equivalent in the reference. `Task` draws it.
|
|
88
|
-
*
|
|
89
|
-
* Two fields, because `Task` has two parts: `TaskTitle` takes the words and `TaskStatus` reads the
|
|
90
|
-
* state off the root and says it. There is no list of sub-lines on the part because there is none
|
|
91
|
-
* on the component — a step that wants children is a step whose host renders them.
|
|
92
|
-
*/
|
|
93
|
-
export interface AiTaskPart {
|
|
94
|
-
type: "task";
|
|
95
|
-
title: string;
|
|
96
|
-
state?: RunState;
|
|
97
|
-
}
|
|
98
|
-
export type AiMessagePart<NAME extends string = string> = AiTextPart | AiReasoningPart | AiToolPart<NAME> | AiTaskPart;
|
|
99
|
-
/**
|
|
100
|
-
* One turn.
|
|
101
|
-
*
|
|
102
|
-
* `METADATA` is where a product hangs what only it knows — a request id, a cost, who asked. It is
|
|
103
|
-
* unconstrained on purpose: constraining it is how a transport type ends up describing one product.
|
|
104
|
-
*/
|
|
105
|
-
export interface AiMessage<METADATA = unknown, NAME extends string = string> {
|
|
106
|
-
id: string;
|
|
107
|
-
role: MessageRole;
|
|
108
|
-
metadata?: METADATA;
|
|
109
|
-
parts: AiMessagePart<NAME>[];
|
|
110
|
-
}
|
|
111
|
-
/**
|
|
112
|
-
* The guards, because they are what make a host's `switch` readable.
|
|
113
|
-
*
|
|
114
|
-
* Each is a type predicate, so `parts.filter(isToolPart)` is an `AiToolPart[]` and not a cast. That
|
|
115
|
-
* is the whole reason they exist rather than the host writing `part.type === "tool"`: the equality
|
|
116
|
-
* narrows inside a `switch` and does not survive a `.filter`.
|
|
117
|
-
*/
|
|
118
|
-
export declare const isTextPart: (part: AiMessagePart) => part is AiTextPart;
|
|
119
|
-
export declare const isReasoningPart: (part: AiMessagePart) => part is AiReasoningPart;
|
|
120
|
-
export declare const isToolPart: <NAME extends string>(part: AiMessagePart<NAME>) => part is AiToolPart<NAME>;
|
|
121
|
-
export declare const isTaskPart: (part: AiMessagePart) => part is AiTaskPart;
|
|
122
|
-
//# sourceMappingURL=message-part.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"message-part.d.ts","sourceRoot":"","sources":["../src/message-part.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAE1C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AAEH,yFAAyF;AACzF,MAAM,MAAM,WAAW,GAAG,WAAW,GAAG,MAAM,CAAC;AAE/C,qDAAqD;AACrD,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,WAAW,CAAC;CACrB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,WAAW,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,WAAW,CAAC;CACrB;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,UAAU,CAAC,IAAI,SAAS,MAAM,GAAG,MAAM;IACtD,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,IAAI,CAAC;IACX,KAAK,CAAC,EAAE,QAAQ,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,oCAAoC;IACpC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,QAAQ,CAAC;CAClB;AAED,MAAM,MAAM,aAAa,CAAC,IAAI,SAAS,MAAM,GAAG,MAAM,IAClD,UAAU,GACV,eAAe,GACf,UAAU,CAAC,IAAI,CAAC,GAChB,UAAU,CAAC;AAEf;;;;;GAKG;AACH,MAAM,WAAW,SAAS,CAAC,QAAQ,GAAG,OAAO,EAAE,IAAI,SAAS,MAAM,GAAG,MAAM;IACzE,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,WAAW,CAAC;IAClB,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,KAAK,EAAE,aAAa,CAAC,IAAI,CAAC,EAAE,CAAC;CAC9B;AAED;;;;;;GAMG;AACH,eAAO,MAAM,UAAU,GAAI,MAAM,aAAa,KAAG,IAAI,IAAI,UAAkC,CAAC;AAE5F,eAAO,MAAM,eAAe,GAAI,MAAM,aAAa,KAAG,IAAI,IAAI,eACnC,CAAC;AAE5B,eAAO,MAAM,UAAU,GAAI,IAAI,SAAS,MAAM,EAC5C,MAAM,aAAa,CAAC,IAAI,CAAC,KACxB,IAAI,IAAI,UAAU,CAAC,IAAI,CAAyB,CAAC;AAEpD,eAAO,MAAM,UAAU,GAAI,MAAM,aAAa,KAAG,IAAI,IAAI,UAAkC,CAAC"}
|