@kanzo-tech/ai 0.1.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 +54 -0
- package/dist/ai-mark.d.ts.map +1 -0
- package/dist/ai-mark.js +68 -0
- package/dist/ai-mark.js.map +1 -0
- package/dist/complete.d.ts +66 -0
- package/dist/complete.d.ts.map +1 -0
- package/dist/complete.js +259 -0
- package/dist/complete.js.map +1 -0
- package/dist/conversation.d.ts +38 -0
- package/dist/conversation.d.ts.map +1 -0
- package/dist/conversation.js +118 -0
- package/dist/conversation.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +60 -0
- package/dist/index.js.map +1 -0
- package/dist/markdown.d.ts +50 -0
- package/dist/markdown.d.ts.map +1 -0
- package/dist/markdown.js +36 -0
- package/dist/markdown.js.map +1 -0
- package/dist/message-part.d.ts +122 -0
- package/dist/message-part.d.ts.map +1 -0
- package/dist/message-part.js +8 -0
- package/dist/message-part.js.map +1 -0
- package/dist/message-text.d.ts +44 -0
- package/dist/message-text.d.ts.map +1 -0
- package/dist/message-text.js +49 -0
- package/dist/message-text.js.map +1 -0
- package/dist/message.d.ts +73 -0
- package/dist/message.d.ts.map +1 -0
- package/dist/message.js +120 -0
- package/dist/message.js.map +1 -0
- package/dist/prompt-input.d.ts +30 -0
- package/dist/prompt-input.d.ts.map +1 -0
- package/dist/prompt-input.js +113 -0
- package/dist/prompt-input.js.map +1 -0
- package/dist/reasoning.d.ts +32 -0
- package/dist/reasoning.d.ts.map +1 -0
- package/dist/reasoning.js +94 -0
- package/dist/reasoning.js.map +1 -0
- package/dist/styles.css +2 -0
- package/dist/suggest.d.ts +75 -0
- package/dist/suggest.d.ts.map +1 -0
- package/dist/suggest.js +123 -0
- package/dist/suggest.js.map +1 -0
- package/dist/task.d.ts +48 -0
- package/dist/task.d.ts.map +1 -0
- package/dist/task.js +91 -0
- package/dist/task.js.map +1 -0
- package/dist/tool.d.ts +56 -0
- package/dist/tool.d.ts.map +1 -0
- package/dist/tool.js +122 -0
- package/dist/tool.js.map +1 -0
- package/dist/types.d.ts +10 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/use-ai.d.ts +170 -0
- package/dist/use-ai.d.ts.map +1 -0
- package/dist/use-ai.js +186 -0
- package/dist/use-ai.js.map +1 -0
- package/package.json +111 -0
package/dist/use-ai.d.ts
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { Candidate } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* The continuation is meant to be appended verbatim. Two safety nets, and no character-level
|
|
4
|
+
* surgery — that mangled coincidental overlaps and was removed.
|
|
5
|
+
*
|
|
6
|
+
* 1. **The finished echo.** A model that restated the whole value before continuing gets that one
|
|
7
|
+
* unambiguous prefix dropped.
|
|
8
|
+
* 2. **The echo still arriving**, which is the one you can see. `cleanGhost` runs on the
|
|
9
|
+
* *accumulated* text once a frame, so while an echo is streaming in, `cont` is a strict prefix
|
|
10
|
+
* of the value and rule 1 cannot fire yet — the field painted the sentence it already contained,
|
|
11
|
+
* a second time, until the echo completed. Measured live on the Complete page: a value of
|
|
12
|
+
* `Three hounds seen at the ford, in threes as they go` with a ghost reading `Three hou`.
|
|
13
|
+
*
|
|
14
|
+
* So an unresolved echo renders as nothing. What it costs is a genuine continuation that happens
|
|
15
|
+
* to begin with the value's own opening characters — hidden for the few frames until it diverges,
|
|
16
|
+
* which is invisible next to painting the value twice.
|
|
17
|
+
*/
|
|
18
|
+
export declare function cleanGhost(base: string, cont: string): string;
|
|
19
|
+
/**
|
|
20
|
+
* Where a request has got to. **One union for the engine and both hooks**, because a status plus a
|
|
21
|
+
* boolean is two spellings of one fact and they drift: this file carried three vocabularies at
|
|
22
|
+
* once, and the pair `status` + `loading` could not tell *never asked* from *asked, here they are*.
|
|
23
|
+
*
|
|
24
|
+
* `ready` means a run finished — with something or with nothing. An empty `ready` is the honest
|
|
25
|
+
* answer "the source had nothing", which a surface should say rather than sit blank.
|
|
26
|
+
*/
|
|
27
|
+
export type AiStatus = "idle" | "loading" | "ready" | "error";
|
|
28
|
+
export interface AiStream<T> {
|
|
29
|
+
/**
|
|
30
|
+
* Open `source` and pull it to the end, handing every value to `each`. Return `false` from `each`
|
|
31
|
+
* to stop early — a budget, a match, whatever the consumer's rule is.
|
|
32
|
+
*
|
|
33
|
+
* **The engine owns the loop**, and that is the point of this shape. When it was `start` +
|
|
34
|
+
* `next` + `idle` every consumer wrote the same twenty lines and the same abort-race protocol —
|
|
35
|
+
* capture the signal, pull, check it twice — four times over, because `next()` read the *current*
|
|
36
|
+
* iterator and a superseded loop could otherwise steal a chunk from the run that replaced it.
|
|
37
|
+
* Here the controller is a local of this closure, so a superseded run cannot see the new one.
|
|
38
|
+
*
|
|
39
|
+
* Supersedes anything in flight. Resolves to what happened to **this** run: `ready`, `error`, or
|
|
40
|
+
* `idle` if it was cancelled or superseded. That return is why there is no `peek()`: a loop that
|
|
41
|
+
* outlives the render that started it used to read `error` from a stale closure — the value from
|
|
42
|
+
* before the failure — so it needed a second way in.
|
|
43
|
+
*/
|
|
44
|
+
run: (source: (signal: AbortSignal) => AsyncIterable<T>, each: (value: T) => boolean | void) => Promise<AiStatus>;
|
|
45
|
+
/** Abort anything in flight and go back to idle, clearing any error. */
|
|
46
|
+
cancel: () => void;
|
|
47
|
+
/** Forget a finished answer. A live stream is untouched — there is nothing to forget yet. */
|
|
48
|
+
reset: () => void;
|
|
49
|
+
status: AiStatus;
|
|
50
|
+
error: string | null;
|
|
51
|
+
}
|
|
52
|
+
/** The shared engine: one iterator, one controller, one status. */
|
|
53
|
+
export declare function useAiStream<T>(errorText?: string): AiStream<T>;
|
|
54
|
+
/** Below this there is not enough text to continue, so no request is made. `Complete` reads it to
|
|
55
|
+
* keep its ✨ from being a button that does nothing. */
|
|
56
|
+
export declare const MIN_COMPLETE_LENGTH = 4;
|
|
57
|
+
/**
|
|
58
|
+
* Why a request is being made. LSP 3.18's `InlineCompletionTriggerKind` minus the `Kind`, and it
|
|
59
|
+
* exists for the reason the spec has it: a source may reasonably answer an explicit ask with
|
|
60
|
+
* something longer and dearer than it answers a keystroke.
|
|
61
|
+
*/
|
|
62
|
+
export type InlineCompletionTrigger = "invoked" | "automatic";
|
|
63
|
+
/**
|
|
64
|
+
* What the source is asked.
|
|
65
|
+
*
|
|
66
|
+
* **`position`, not just `value`.** A completion is offered *at the caret* — it was an append at
|
|
67
|
+
* the end of the value, which is why the ghost used to vanish the moment the caret was not there.
|
|
68
|
+
* LSP calls this a `range` and prefers a replacement over an insertion; ours is the degenerate
|
|
69
|
+
* range, an insertion point, because our fields hold prose and not code.
|
|
70
|
+
*/
|
|
71
|
+
export interface InlineCompletionRequest {
|
|
72
|
+
/** The whole field value. A source that only wants what comes before the caret slices it. */
|
|
73
|
+
value: string;
|
|
74
|
+
/** Where the continuation goes. */
|
|
75
|
+
position: number;
|
|
76
|
+
trigger: InlineCompletionTrigger;
|
|
77
|
+
signal?: AbortSignal;
|
|
78
|
+
}
|
|
79
|
+
export interface UseInlineCompletionOptions {
|
|
80
|
+
/** Streaming inline completion — yields continuation chunks; honour the `AbortSignal`. */
|
|
81
|
+
complete: (request: InlineCompletionRequest) => AsyncIterable<string>;
|
|
82
|
+
debounceMs?: number;
|
|
83
|
+
minLength?: number;
|
|
84
|
+
}
|
|
85
|
+
export interface InlineCompletion {
|
|
86
|
+
/**
|
|
87
|
+
* What is left to insert **at the caret**, empty when there is nothing.
|
|
88
|
+
*
|
|
89
|
+
* There is no `hasGhost`, for the reason `useSuggestions` returns `items` and no `hasItems`: a
|
|
90
|
+
* derived boolean beside the thing it is derived from is a second spelling of one fact.
|
|
91
|
+
*/
|
|
92
|
+
ghost: string;
|
|
93
|
+
/** `ready` with an empty `ghost` is "asked, and the model had nothing to add". */
|
|
94
|
+
status: AiStatus;
|
|
95
|
+
error: string | null;
|
|
96
|
+
/**
|
|
97
|
+
* The field changed. Ask after a debounce — **unless the offer already on the table survives**,
|
|
98
|
+
* which is the whole reason this takes a caret.
|
|
99
|
+
*/
|
|
100
|
+
setValue: (value: string, position?: number) => void;
|
|
101
|
+
/** Ask now, as `"invoked"` — the ✨ path. */
|
|
102
|
+
ask: (value: string, position?: number) => void;
|
|
103
|
+
/**
|
|
104
|
+
* Forget the offer, and stop anything in flight.
|
|
105
|
+
*
|
|
106
|
+
* **There is no `accept` and no `clear`.** `clear` was a second name bound to this same function.
|
|
107
|
+
* And `accept` returned the ghost for the caller to insert and then did exactly this — but the
|
|
108
|
+
* caller already holds `ghost` and already owns the value, so the return was unread by the only
|
|
109
|
+
* consumer there has ever been. **The hook offers; the caller accepts.**
|
|
110
|
+
*/
|
|
111
|
+
dismiss: () => void;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Ghost-text completion that does NOT own the input value — the caller's field owns the text and
|
|
115
|
+
* performs the insertion; this only offers a continuation and forgets it on `dismiss`.
|
|
116
|
+
*
|
|
117
|
+
* ## The offer survives typing that agrees with it
|
|
118
|
+
*
|
|
119
|
+
* This is the difference a reader feels, and it is one rule: an offer made at `from` stays on the
|
|
120
|
+
* table while what has been typed since is a prefix of it, and the ghost is simply what is left.
|
|
121
|
+
* Type the word the model was going to write and the ghost shortens; **no request is made at all.**
|
|
122
|
+
*
|
|
123
|
+
* It threw the whole offer away on every keystroke and started a fresh debounce, so the fastest a
|
|
124
|
+
* suggestion could reappear was 350 ms plus a model. Smart Compose's budget is 60 ms at p90, and
|
|
125
|
+
* you cannot get there by asking again — you get there by not asking. LSP 3.18 spells the same rule
|
|
126
|
+
* `filterText` ("an inline completion is shown if the text to replace is a prefix of the filter
|
|
127
|
+
* text") and Monaco spells it `inlineSuggest.mode: "prefix"`.
|
|
128
|
+
*/
|
|
129
|
+
export declare function useInlineCompletion(options: UseInlineCompletionOptions): InlineCompletion;
|
|
130
|
+
export interface UseSuggestionsOptions {
|
|
131
|
+
suggest: (signal?: AbortSignal) => AsyncIterable<Candidate>;
|
|
132
|
+
/** Current values — a candidate equal to one of these (case-insensitively) never appears. */
|
|
133
|
+
existing?: string[];
|
|
134
|
+
/**
|
|
135
|
+
* How many to take from the stream. A strip wraps, so this is a budget rather than a window:
|
|
136
|
+
* nothing refills when one is dismissed.
|
|
137
|
+
*
|
|
138
|
+
* @default 6
|
|
139
|
+
*/
|
|
140
|
+
limit?: number;
|
|
141
|
+
}
|
|
142
|
+
export interface SuggestionsController {
|
|
143
|
+
items: Candidate[];
|
|
144
|
+
/** `ready` with no items means the source had nothing — not that its answer was consumed. */
|
|
145
|
+
status: AiStatus;
|
|
146
|
+
error: string | null;
|
|
147
|
+
/** Ask the source. A no-op unless idle: in flight, already answered, or failed, it does nothing
|
|
148
|
+
* and `refresh` is the way through. */
|
|
149
|
+
ask: () => void;
|
|
150
|
+
/** Ask again from scratch, discarding what is shown. Also the retry after an error. */
|
|
151
|
+
refresh: () => void;
|
|
152
|
+
/** Abort in flight, and forget. */
|
|
153
|
+
cancel: () => void;
|
|
154
|
+
/**
|
|
155
|
+
* Drop one candidate, **by value**.
|
|
156
|
+
*
|
|
157
|
+
* By value and not by index: an index-addressed list forces every caller to keep a parallel
|
|
158
|
+
* lookup, and it is wrong the moment anything else mutates the array.
|
|
159
|
+
*/
|
|
160
|
+
dismiss: (value: string) => void;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* A streamed list of candidate values, deduped against what the field already holds.
|
|
164
|
+
*
|
|
165
|
+
* The engine is shared with `useInlineCompletion`; what is here is the part specific to a list:
|
|
166
|
+
* dedup, a budget, and the rule for when asking again is free. **No windowing and no refill loop**
|
|
167
|
+
* — those existed to keep exactly three rows alive inside a popover, and the popover is gone.
|
|
168
|
+
*/
|
|
169
|
+
export declare function useSuggestions(options: UseSuggestionsOptions): SuggestionsController;
|
|
170
|
+
//# sourceMappingURL=use-ai.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-ai.d.ts","sourceRoot":"","sources":["../src/use-ai.ts"],"names":[],"mappings":"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"}
|
package/dist/use-ai.js
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useState as W, useRef as C, useCallback as l, useEffect as R, useMemo as A } from "react";
|
|
3
|
+
function N(r, e) {
|
|
4
|
+
const u = r.trimEnd();
|
|
5
|
+
if (!u || !e) return e;
|
|
6
|
+
const s = { base: u.toLowerCase(), cont: e.toLowerCase() };
|
|
7
|
+
return s.cont.startsWith(s.base) ? e.slice(u.length) : s.base.startsWith(s.cont) ? "" : e;
|
|
8
|
+
}
|
|
9
|
+
const G = (r, e) => r instanceof Error && r.message ? r.message : e;
|
|
10
|
+
function q(r = "Something went wrong") {
|
|
11
|
+
const [e, u] = W("idle"), [s, g] = W(null), E = C("idle"), h = C(void 0), k = C(r);
|
|
12
|
+
k.current = r;
|
|
13
|
+
const n = l((i, o = null) => {
|
|
14
|
+
E.current = i, u(i), g(o);
|
|
15
|
+
}, []);
|
|
16
|
+
R(() => () => {
|
|
17
|
+
var i;
|
|
18
|
+
return (i = h.current) == null ? void 0 : i.abort();
|
|
19
|
+
}, []);
|
|
20
|
+
const v = l(
|
|
21
|
+
(i, o) => {
|
|
22
|
+
var d;
|
|
23
|
+
(d = h.current) == null || d.abort();
|
|
24
|
+
const a = new AbortController();
|
|
25
|
+
return h.current = a, n("loading"), (async () => {
|
|
26
|
+
let x;
|
|
27
|
+
try {
|
|
28
|
+
x = i(a.signal)[Symbol.asyncIterator]();
|
|
29
|
+
} catch (m) {
|
|
30
|
+
return n("error", G(m, k.current)), "error";
|
|
31
|
+
}
|
|
32
|
+
for (; ; ) {
|
|
33
|
+
let m;
|
|
34
|
+
try {
|
|
35
|
+
m = await x.next();
|
|
36
|
+
} catch (p) {
|
|
37
|
+
return a.signal.aborted ? "idle" : (n("error", G(p, k.current)), "error");
|
|
38
|
+
}
|
|
39
|
+
if (a.signal.aborted) return "idle";
|
|
40
|
+
if (m.done) break;
|
|
41
|
+
if (o(m.value) === !1) {
|
|
42
|
+
a.abort();
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return n("ready"), "ready";
|
|
47
|
+
})();
|
|
48
|
+
},
|
|
49
|
+
[n]
|
|
50
|
+
), f = l(() => {
|
|
51
|
+
var i;
|
|
52
|
+
(i = h.current) == null || i.abort(), n("idle");
|
|
53
|
+
}, [n]), b = l(() => {
|
|
54
|
+
E.current !== "loading" && n("idle");
|
|
55
|
+
}, [n]);
|
|
56
|
+
return A(
|
|
57
|
+
() => ({ run: v, cancel: f, reset: b, status: e, error: s }),
|
|
58
|
+
[f, s, b, v, e]
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
function _(r) {
|
|
62
|
+
let e, u = !1;
|
|
63
|
+
const s = () => {
|
|
64
|
+
u = !0, e === void 0 && (e = requestAnimationFrame(() => {
|
|
65
|
+
e = void 0, u = !1, r();
|
|
66
|
+
}));
|
|
67
|
+
};
|
|
68
|
+
return s.flush = () => {
|
|
69
|
+
const g = u;
|
|
70
|
+
s.cancel(), g && r();
|
|
71
|
+
}, s.cancel = () => {
|
|
72
|
+
e !== void 0 && cancelAnimationFrame(e), e = void 0, u = !1;
|
|
73
|
+
}, s;
|
|
74
|
+
}
|
|
75
|
+
const B = 4;
|
|
76
|
+
function O(r) {
|
|
77
|
+
const { run: e, cancel: u, status: s, error: g } = q("Couldn’t complete"), [E, h] = W(""), k = C(r);
|
|
78
|
+
k.current = r;
|
|
79
|
+
const n = C(null), v = C({ value: "", position: 0 }), f = C(void 0), b = l(() => {
|
|
80
|
+
const t = n.current;
|
|
81
|
+
if (!t) return "";
|
|
82
|
+
const { value: c, position: y } = v.current;
|
|
83
|
+
if (y < t.from) return "";
|
|
84
|
+
const w = c.slice(t.from, y);
|
|
85
|
+
return t.text.startsWith(w) ? t.text.slice(w.length) : "";
|
|
86
|
+
}, []), i = l(() => {
|
|
87
|
+
const t = n.current;
|
|
88
|
+
if (!t) return !1;
|
|
89
|
+
const { value: c, position: y } = v.current;
|
|
90
|
+
if (y < t.from) return !1;
|
|
91
|
+
const w = c.slice(t.from, y);
|
|
92
|
+
return t.text.startsWith(w) || !t.done && w.startsWith(t.text);
|
|
93
|
+
}, []), o = C(void 0);
|
|
94
|
+
o.current ?? (o.current = _(() => h(b())));
|
|
95
|
+
const a = l(() => {
|
|
96
|
+
var t;
|
|
97
|
+
clearTimeout(f.current), (t = o.current) == null || t.cancel(), n.current = null, u(), h("");
|
|
98
|
+
}, [u]), d = l(
|
|
99
|
+
(t, c, y) => {
|
|
100
|
+
const w = { from: c, text: "", done: !1 };
|
|
101
|
+
n.current = w;
|
|
102
|
+
const F = t.slice(0, c);
|
|
103
|
+
let I = "";
|
|
104
|
+
e(
|
|
105
|
+
(S) => k.current.complete({ value: t, position: c, trigger: y, signal: S }),
|
|
106
|
+
(S) => {
|
|
107
|
+
var L;
|
|
108
|
+
if (n.current !== w) return !1;
|
|
109
|
+
I += S, w.text = N(F, I), (L = o.current) == null || L.call(o);
|
|
110
|
+
}
|
|
111
|
+
).then((S) => {
|
|
112
|
+
var L, M;
|
|
113
|
+
n.current === w && (w.done = !0, S === "ready" ? (L = o.current) == null || L.flush() : (M = o.current) == null || M.cancel());
|
|
114
|
+
});
|
|
115
|
+
},
|
|
116
|
+
[e]
|
|
117
|
+
), x = (t) => t.trim().length < (k.current.minLength ?? B), m = l(
|
|
118
|
+
(t, c = t.length) => {
|
|
119
|
+
if (v.current = { value: t, position: c }, i()) {
|
|
120
|
+
h(b());
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
a(), !x(t) && (f.current = setTimeout(
|
|
124
|
+
() => d(t, c, "automatic"),
|
|
125
|
+
k.current.debounceMs ?? 350
|
|
126
|
+
));
|
|
127
|
+
},
|
|
128
|
+
[i, a, b, d]
|
|
129
|
+
), p = l(
|
|
130
|
+
(t, c = t.length) => {
|
|
131
|
+
v.current = { value: t, position: c }, a(), !x(t) && d(t, c, "invoked");
|
|
132
|
+
},
|
|
133
|
+
[a, d]
|
|
134
|
+
);
|
|
135
|
+
return R(
|
|
136
|
+
() => () => {
|
|
137
|
+
var t;
|
|
138
|
+
clearTimeout(f.current), (t = o.current) == null || t.cancel();
|
|
139
|
+
},
|
|
140
|
+
[]
|
|
141
|
+
), A(
|
|
142
|
+
() => ({ ghost: E, status: s, error: g, setValue: m, ask: p, dismiss: a }),
|
|
143
|
+
[p, a, g, E, m, s]
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
const T = (r) => r.trim().toLowerCase();
|
|
147
|
+
function P(r) {
|
|
148
|
+
const { run: e, cancel: u, reset: s, status: g, error: E } = q(
|
|
149
|
+
"Couldn’t load suggestions"
|
|
150
|
+
), [h, k] = W([]), n = C([]), v = C(r);
|
|
151
|
+
v.current = r;
|
|
152
|
+
const f = l((d) => {
|
|
153
|
+
n.current = d, k(d);
|
|
154
|
+
}, []), b = l(() => {
|
|
155
|
+
const { existing: d = [], limit: x = 6, suggest: m } = v.current, p = new Set(d.map(T).filter(Boolean)), t = [];
|
|
156
|
+
f([]), e(
|
|
157
|
+
(c) => m(c),
|
|
158
|
+
(c) => {
|
|
159
|
+
const y = T(c.value);
|
|
160
|
+
return !y || p.has(y) ? !0 : (p.add(y), t.push(c), f([...t]), t.length < x);
|
|
161
|
+
}
|
|
162
|
+
);
|
|
163
|
+
}, [f, e]), i = l(() => {
|
|
164
|
+
g === "idle" && b();
|
|
165
|
+
}, [b, g]), o = l(() => {
|
|
166
|
+
u(), f([]);
|
|
167
|
+
}, [u, f]), a = l(
|
|
168
|
+
(d) => {
|
|
169
|
+
const x = T(d), m = n.current.filter((p) => T(p.value) !== x);
|
|
170
|
+
f(m), m.length === 0 && s();
|
|
171
|
+
},
|
|
172
|
+
[f, s]
|
|
173
|
+
);
|
|
174
|
+
return A(
|
|
175
|
+
() => ({ items: h, status: g, error: E, ask: i, refresh: b, cancel: o, dismiss: a }),
|
|
176
|
+
[i, o, a, E, h, b, g]
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
export {
|
|
180
|
+
B as MIN_COMPLETE_LENGTH,
|
|
181
|
+
N as cleanGhost,
|
|
182
|
+
q as useAiStream,
|
|
183
|
+
O as useInlineCompletion,
|
|
184
|
+
P as useSuggestions
|
|
185
|
+
};
|
|
186
|
+
//# sourceMappingURL=use-ai.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-ai.js","sources":["../src/use-ai.ts"],"sourcesContent":["\"use client\";\n\n// Headless AI-assist engine — the streaming machinery behind inline ghost completion (`Complete`,\n// via `useInlineCompletion`) and a candidate strip (`Suggest`, via `useSuggestions`). Domain-free\n// and source-agnostic: a consumer passes a function returning an async iterable and cancels it\n// with an `AbortSignal`. It imports nothing of `@codemirror/*` — `CodeEditor` has no completion\n// prop and never had one, and this package must not grow one, because `ui/editor` importing from\n// here would make `ui` depend on `ai` and `ai` already depends on `ui`.\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport type { Candidate } from \"./types.js\";\n\n/**\n * The continuation is meant to be appended verbatim. Two safety nets, and no character-level\n * surgery — that mangled coincidental overlaps and was removed.\n *\n * 1. **The finished echo.** A model that restated the whole value before continuing gets that one\n * unambiguous prefix dropped.\n * 2. **The echo still arriving**, which is the one you can see. `cleanGhost` runs on the\n * *accumulated* text once a frame, so while an echo is streaming in, `cont` is a strict prefix\n * of the value and rule 1 cannot fire yet — the field painted the sentence it already contained,\n * a second time, until the echo completed. Measured live on the Complete page: a value of\n * `Three hounds seen at the ford, in threes as they go` with a ghost reading `Three hou`.\n *\n * So an unresolved echo renders as nothing. What it costs is a genuine continuation that happens\n * to begin with the value's own opening characters — hidden for the few frames until it diverges,\n * which is invisible next to painting the value twice.\n */\nexport function cleanGhost(base: string, cont: string): string {\n const b = base.trimEnd();\n if (!b || !cont) return cont;\n const lower = { base: b.toLowerCase(), cont: cont.toLowerCase() };\n if (lower.cont.startsWith(lower.base)) return cont.slice(b.length);\n if (lower.base.startsWith(lower.cont)) return \"\";\n return cont;\n}\n\nconst message = (e: unknown, fallback: string) =>\n e instanceof Error && e.message ? e.message : fallback;\n\n/**\n * Where a request has got to. **One union for the engine and both hooks**, because a status plus a\n * boolean is two spellings of one fact and they drift: this file carried three vocabularies at\n * once, and the pair `status` + `loading` could not tell *never asked* from *asked, here they are*.\n *\n * `ready` means a run finished — with something or with nothing. An empty `ready` is the honest\n * answer \"the source had nothing\", which a surface should say rather than sit blank.\n */\nexport type AiStatus = \"idle\" | \"loading\" | \"ready\" | \"error\";\n\nexport interface AiStream<T> {\n /**\n * Open `source` and pull it to the end, handing every value to `each`. Return `false` from `each`\n * to stop early — a budget, a match, whatever the consumer's rule is.\n *\n * **The engine owns the loop**, and that is the point of this shape. When it was `start` +\n * `next` + `idle` every consumer wrote the same twenty lines and the same abort-race protocol —\n * capture the signal, pull, check it twice — four times over, because `next()` read the *current*\n * iterator and a superseded loop could otherwise steal a chunk from the run that replaced it.\n * Here the controller is a local of this closure, so a superseded run cannot see the new one.\n *\n * Supersedes anything in flight. Resolves to what happened to **this** run: `ready`, `error`, or\n * `idle` if it was cancelled or superseded. That return is why there is no `peek()`: a loop that\n * outlives the render that started it used to read `error` from a stale closure — the value from\n * before the failure — so it needed a second way in.\n */\n run: (\n source: (signal: AbortSignal) => AsyncIterable<T>,\n each: (value: T) => boolean | void,\n ) => Promise<AiStatus>;\n /** Abort anything in flight and go back to idle, clearing any error. */\n cancel: () => void;\n /** Forget a finished answer. A live stream is untouched — there is nothing to forget yet. */\n reset: () => void;\n status: AiStatus;\n error: string | null;\n}\n\n/** The shared engine: one iterator, one controller, one status. */\nexport function useAiStream<T>(errorText = \"Something went wrong\"): AiStream<T> {\n const [status, setStatus] = useState<AiStatus>(\"idle\");\n const [error, setError] = useState<string | null>(null);\n\n // Beside the state, and read only where a decision is synchronous (`reset`). Anything a surface\n // renders has to be state: this was a ref first, and the strip stayed on whatever status it\n // happened to paint with.\n const statusRef = useRef<AiStatus>(\"idle\");\n const ctrl = useRef<AbortController>(undefined);\n const errRef = useRef(errorText);\n errRef.current = errorText;\n\n const set = useCallback((s: AiStatus, e: string | null = null) => {\n statusRef.current = s;\n setStatus(s);\n setError(e);\n }, []);\n\n useEffect(() => () => ctrl.current?.abort(), []);\n\n const run = useCallback(\n (src: (signal: AbortSignal) => AsyncIterable<T>, each: (value: T) => boolean | void) => {\n ctrl.current?.abort();\n const c = new AbortController();\n ctrl.current = c;\n set(\"loading\");\n\n return (async (): Promise<AiStatus> => {\n let it: AsyncIterator<T>;\n try {\n it = src(c.signal)[Symbol.asyncIterator]();\n } catch (e) {\n set(\"error\", message(e, errRef.current));\n return \"error\";\n }\n for (;;) {\n let res: IteratorResult<T>;\n try {\n res = await it.next();\n } catch (e) {\n // A throw on the way out is not a failure to report — the caller asked for it.\n if (c.signal.aborted) return \"idle\";\n set(\"error\", message(e, errRef.current));\n return \"error\";\n }\n if (c.signal.aborted) return \"idle\";\n if (res.done) break;\n if (each(res.value) === false) {\n // Enough. The source is told, because it is the one holding the socket.\n c.abort();\n break;\n }\n }\n set(\"ready\");\n return \"ready\";\n })();\n },\n [set],\n );\n\n const cancel = useCallback(() => {\n ctrl.current?.abort();\n set(\"idle\");\n }, [set]);\n\n const reset = useCallback(() => {\n if (statusRef.current !== \"loading\") set(\"idle\");\n }, [set]);\n\n return useMemo(\n () => ({ run, cancel, reset, status, error }),\n [cancel, error, reset, run, status],\n );\n}\n\n// ── frame coalescing ─────────────────────────────────────────────────────────\n\ninterface Coalesced {\n (): void;\n flush: () => void;\n cancel: () => void;\n}\n\n/**\n * Run `paint` at most once a frame, and once more on `flush()` so the last chunk is never the one\n * that got dropped. `requestAnimationFrame` rather than a millisecond interval because the thing\n * being matched is the display, and because a hidden tab stops firing it — which is the correct\n * behaviour for a paint nobody is looking at.\n *\n * It throttles a **call**, not a value. Carrying the pending value meant a superseded run could\n * flush a string the newer one had already replaced; a repaint that derives what to show from the\n * current offer cannot be stale, so there is nothing to guard.\n */\nfunction coalesce(paint: () => void): Coalesced {\n let frame: number | undefined;\n let queued = false;\n\n const run: Coalesced = () => {\n queued = true;\n if (frame !== undefined) return;\n frame = requestAnimationFrame(() => {\n frame = undefined;\n queued = false;\n paint();\n });\n };\n\n run.flush = () => {\n const pending = queued;\n run.cancel();\n if (pending) paint();\n };\n\n run.cancel = () => {\n if (frame !== undefined) cancelAnimationFrame(frame);\n frame = undefined;\n queued = false;\n };\n\n return run;\n}\n\n// ── useInlineCompletion — inline continuation (bring your own input) ─────────\n\n/** Below this there is not enough text to continue, so no request is made. `Complete` reads it to\n * keep its ✨ from being a button that does nothing. */\nexport const MIN_COMPLETE_LENGTH = 4;\n\n/**\n * Why a request is being made. LSP 3.18's `InlineCompletionTriggerKind` minus the `Kind`, and it\n * exists for the reason the spec has it: a source may reasonably answer an explicit ask with\n * something longer and dearer than it answers a keystroke.\n */\nexport type InlineCompletionTrigger = \"invoked\" | \"automatic\";\n\n/**\n * What the source is asked.\n *\n * **`position`, not just `value`.** A completion is offered *at the caret* — it was an append at\n * the end of the value, which is why the ghost used to vanish the moment the caret was not there.\n * LSP calls this a `range` and prefers a replacement over an insertion; ours is the degenerate\n * range, an insertion point, because our fields hold prose and not code.\n */\nexport interface InlineCompletionRequest {\n /** The whole field value. A source that only wants what comes before the caret slices it. */\n value: string;\n /** Where the continuation goes. */\n position: number;\n trigger: InlineCompletionTrigger;\n signal?: AbortSignal;\n}\n\nexport interface UseInlineCompletionOptions {\n /** Streaming inline completion — yields continuation chunks; honour the `AbortSignal`. */\n complete: (request: InlineCompletionRequest) => AsyncIterable<string>;\n debounceMs?: number;\n minLength?: number;\n}\n\nexport interface InlineCompletion {\n /**\n * What is left to insert **at the caret**, empty when there is nothing.\n *\n * There is no `hasGhost`, for the reason `useSuggestions` returns `items` and no `hasItems`: a\n * derived boolean beside the thing it is derived from is a second spelling of one fact.\n */\n ghost: string;\n /** `ready` with an empty `ghost` is \"asked, and the model had nothing to add\". */\n status: AiStatus;\n error: string | null;\n /**\n * The field changed. Ask after a debounce — **unless the offer already on the table survives**,\n * which is the whole reason this takes a caret.\n */\n setValue: (value: string, position?: number) => void;\n /** Ask now, as `\"invoked\"` — the ✨ path. */\n ask: (value: string, position?: number) => void;\n /**\n * Forget the offer, and stop anything in flight.\n *\n * **There is no `accept` and no `clear`.** `clear` was a second name bound to this same function.\n * And `accept` returned the ghost for the caller to insert and then did exactly this — but the\n * caller already holds `ghost` and already owns the value, so the return was unread by the only\n * consumer there has ever been. **The hook offers; the caller accepts.**\n */\n dismiss: () => void;\n}\n\n/** An offer on the table: a continuation, and the point in the value it was made at. */\ninterface Offer {\n from: number;\n /** Everything received so far, with the echo rule already applied. */\n text: string;\n /** The stream ended, so a `typed` longer than `text` is a mismatch rather than a wait. */\n done: boolean;\n}\n\n/**\n * Ghost-text completion that does NOT own the input value — the caller's field owns the text and\n * performs the insertion; this only offers a continuation and forgets it on `dismiss`.\n *\n * ## The offer survives typing that agrees with it\n *\n * This is the difference a reader feels, and it is one rule: an offer made at `from` stays on the\n * table while what has been typed since is a prefix of it, and the ghost is simply what is left.\n * Type the word the model was going to write and the ghost shortens; **no request is made at all.**\n *\n * It threw the whole offer away on every keystroke and started a fresh debounce, so the fastest a\n * suggestion could reappear was 350 ms plus a model. Smart Compose's budget is 60 ms at p90, and\n * you cannot get there by asking again — you get there by not asking. LSP 3.18 spells the same rule\n * `filterText` (\"an inline completion is shown if the text to replace is a prefix of the filter\n * text\") and Monaco spells it `inlineSuggest.mode: \"prefix\"`.\n */\nexport function useInlineCompletion(options: UseInlineCompletionOptions): InlineCompletion {\n const { run, cancel, status, error } = useAiStream<string>(\"Couldn’t complete\");\n const [ghost, setGhost] = useState(\"\");\n\n const opts = useRef(options);\n opts.current = options;\n\n const offer = useRef<Offer | null>(null);\n /** The field as of the last thing the caller told us. `rendered` is a function of this. */\n const field = useRef({ value: \"\", position: 0 });\n const askTimer = useRef<ReturnType<typeof setTimeout>>(undefined);\n\n /** What is on offer at the caret right now — \"\" when nothing is, which is not the same as dead. */\n const rendered = useCallback((): string => {\n const o = offer.current;\n if (!o) return \"\";\n const { value, position } = field.current;\n if (position < o.from) return \"\";\n const typed = value.slice(o.from, position);\n return o.text.startsWith(typed) ? o.text.slice(typed.length) : \"\";\n }, []);\n\n /** Is the offer still about the text in the field? */\n const alive = useCallback((): boolean => {\n const o = offer.current;\n if (!o) return false;\n const { value, position } = field.current;\n if (position < o.from) return false;\n const typed = value.slice(o.from, position);\n // A `typed` longer than what has arrived is only a mismatch once the stream has ended: mid-flight\n // it is the reader typing ahead of the model, and the next chunk may well agree with them.\n return o.text.startsWith(typed) || (!o.done && typed.startsWith(o.text));\n }, []);\n\n const painter = useRef<Coalesced>(undefined);\n painter.current ??= coalesce(() => setGhost(rendered()));\n\n const drop = useCallback(() => {\n clearTimeout(askTimer.current);\n painter.current?.cancel();\n offer.current = null;\n cancel();\n setGhost(\"\");\n }, [cancel]);\n\n // The write is coalesced to a frame rather than done per chunk: a token stream arrives faster than\n // the display refreshes, so a `setState` per chunk renders work nobody sees. keasy's Ask panel hit\n // this and grew its own `rafPending` guard around the same loop.\n const start = useCallback(\n (value: string, position: number, trigger: InlineCompletionTrigger) => {\n const mine: Offer = { from: position, text: \"\", done: false };\n offer.current = mine;\n const base = value.slice(0, position);\n let raw = \"\";\n void run(\n (signal) => opts.current.complete({ value, position, trigger, signal }),\n (chunk) => {\n // Superseded or dismissed: stop pulling rather than write into somebody else's offer.\n if (offer.current !== mine) return false;\n raw += chunk;\n mine.text = cleanGhost(base, raw);\n painter.current?.();\n },\n ).then((outcome) => {\n if (offer.current !== mine) return;\n mine.done = true;\n if (outcome === \"ready\") painter.current?.flush();\n else painter.current?.cancel();\n });\n },\n [run],\n );\n\n const short = (value: string) =>\n value.trim().length < (opts.current.minLength ?? MIN_COMPLETE_LENGTH);\n\n const setValue = useCallback(\n (value: string, position = value.length) => {\n field.current = { value, position };\n if (alive()) {\n // The offer covers this keystroke. Repaint and ask nothing — this is the whole feature.\n setGhost(rendered());\n return;\n }\n drop();\n if (short(value)) return;\n askTimer.current = setTimeout(\n () => start(value, position, \"automatic\"),\n opts.current.debounceMs ?? 350,\n );\n },\n [alive, drop, rendered, start],\n );\n\n const ask = useCallback(\n (value: string, position = value.length) => {\n field.current = { value, position };\n drop();\n if (short(value)) return;\n start(value, position, \"invoked\");\n },\n [drop, start],\n );\n\n useEffect(\n () => () => {\n clearTimeout(askTimer.current);\n painter.current?.cancel();\n },\n [],\n );\n\n return useMemo(\n () => ({ ghost, status, error, setValue, ask, dismiss: drop }),\n [ask, drop, error, ghost, setValue, status],\n );\n}\n\n// ── useSuggestions — a deduped candidate strip ───────────────────────────────\n\n/** Case-insensitive key for deduping suggestion values. */\nconst norm = (v: string) => v.trim().toLowerCase();\n\nexport interface UseSuggestionsOptions {\n suggest: (signal?: AbortSignal) => AsyncIterable<Candidate>;\n /** Current values — a candidate equal to one of these (case-insensitively) never appears. */\n existing?: string[];\n /**\n * How many to take from the stream. A strip wraps, so this is a budget rather than a window:\n * nothing refills when one is dismissed.\n *\n * @default 6\n */\n limit?: number;\n}\n\nexport interface SuggestionsController {\n items: Candidate[];\n /** `ready` with no items means the source had nothing — not that its answer was consumed. */\n status: AiStatus;\n error: string | null;\n /** Ask the source. A no-op unless idle: in flight, already answered, or failed, it does nothing\n * and `refresh` is the way through. */\n ask: () => void;\n /** Ask again from scratch, discarding what is shown. Also the retry after an error. */\n refresh: () => void;\n /** Abort in flight, and forget. */\n cancel: () => void;\n /**\n * Drop one candidate, **by value**.\n *\n * By value and not by index: an index-addressed list forces every caller to keep a parallel\n * lookup, and it is wrong the moment anything else mutates the array.\n */\n dismiss: (value: string) => void;\n}\n\n/**\n * A streamed list of candidate values, deduped against what the field already holds.\n *\n * The engine is shared with `useInlineCompletion`; what is here is the part specific to a list:\n * dedup, a budget, and the rule for when asking again is free. **No windowing and no refill loop**\n * — those existed to keep exactly three rows alive inside a popover, and the popover is gone.\n */\nexport function useSuggestions(options: UseSuggestionsOptions): SuggestionsController {\n const { run, cancel: abort, reset, status, error } = useAiStream<Candidate>(\n \"Couldn’t load suggestions\",\n );\n const [items, setItems] = useState<Candidate[]>([]);\n // A ref beside the state so `dismiss` can filter what is on screen *now* without taking `items`\n // as a dependency — which would hand every consumer a new callback on every render.\n const shown = useRef<Candidate[]>([]);\n\n const opts = useRef(options);\n opts.current = options;\n\n const put = useCallback((next: Candidate[]) => {\n shown.current = next;\n setItems(next);\n }, []);\n\n const refresh = useCallback(() => {\n const { existing = [], limit = 6, suggest } = opts.current;\n const seen = new Set(existing.map(norm).filter(Boolean));\n const taken: Candidate[] = [];\n put([]);\n void run(\n (signal) => suggest(signal),\n (item) => {\n const key = norm(item.value);\n if (!key || seen.has(key)) return true;\n seen.add(key);\n taken.push(item);\n put([...taken]);\n return taken.length < limit;\n },\n );\n }, [put, run]);\n\n // Gated on the status alone, and that one line replaced an `asked` ref that latched forever:\n // take every candidate and the ✨ became a button that did nothing, because \"has been asked\" is\n // not the question. The question is whether anything is in flight or on screen, and the status\n // answers it — `dismiss` puts it back to idle when nothing is left of the answer.\n const ask = useCallback(() => {\n if (status !== \"idle\") return;\n refresh();\n }, [refresh, status]);\n\n const cancel = useCallback(() => {\n abort();\n put([]);\n }, [abort, put]);\n\n const dismiss = useCallback(\n (value: string) => {\n const key = norm(value);\n const next = shown.current.filter((item) => norm(item.value) !== key);\n put(next);\n // Nothing left of an answer is not the same as an answer with nothing in it. This is what\n // keeps a strip from saying \"Nothing to suggest.\" at the reader who just took the last one.\n if (next.length === 0) reset();\n },\n [put, reset],\n );\n\n return useMemo(\n () => ({ items, status, error, ask, refresh, cancel, dismiss }),\n [ask, cancel, dismiss, error, items, refresh, status],\n );\n}\n"],"names":[],"mappings":";;AA4BO;AACL;AACA;AACA;AACA;AAGF;AAEA;AA0CO;AACL;AASA;AAEA;AACE;AAEU;AAGZ;;AAAsB;AAAc;AAEpC;AAAY;;AAER;AACA;AACA;AAIE;AACA;AACE;AAAuC;AAEvC;AACO;AAET;AACE;AACA;AACE;AAAe;AAGf;AAEO;AAET;AACA;AACA;AAEE;AACA;AAAA;AACF;AAEF;AACO;AACT;AACF;AACI;;AAIJ;AACU;AAIV;AAA+C;AAGjD;AAAO;AACgC;AACH;AAEtC;AAoBA;AACE;AAGA;AAEE;AAEE;AAEA;AACD;AAGH;AACE;AACA;AACa;AAIb;AAES;AAIb;AAMO;AAuFA;AACL;AAIA;AAEA;AAOE;AACA;AACA;AACA;AACA;AACA;AAA+D;AAK/D;AACA;AACA;AACA;AACA;AAGA;AAAsE;AAIxE;AAEA;;AACE;AAIW;AAMC;AAEV;AACA;AACA;AACA;AACA;AAAK;AACmE;;AAGpE;AACA;AAEA;AACF;;AAEA;AAGsB;AACvB;AACH;AACI;AAMW;AAGb;AAEE;AACA;AAAA;AAGF;AACmB;AACuB;AACb;AAAA;AAE/B;AAC6B;AAGnB;AAIR;AACgC;AAClC;AACY;AAGd;AAAA;;AAEI;AACiB;AACnB;AACA;AAGK;;AAEqC;AAE9C;AAKA;AA2CO;AACL;AAAqD;AACnD;AAQF;AAEA;AACE;AACa;AAIb;AAGA;AACK;AACuB;AAExB;AACA;AAIsB;AACxB;AAAA;AASF;AACA;AAIA;AACM;AAGQ;AAEZ;AAEA;AAGuB;AACzB;AACW;AAGb;AAAO;;AAE+C;AAExD;;;;;;;;"}
|
package/package.json
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kanzo-tech/ai",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Kanzo AI surfaces — the transcript, the composer, a model's reasoning and the calls it makes, plus the field affordances that stream into a value. Sibling of @kanzo-tech/ui, not part of it: a Message has a role and one of the roles is `assistant`, which is domain knowledge the generic vocabulary does not carry.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"sideEffects": [
|
|
8
|
+
"**/*.css"
|
|
9
|
+
],
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"module": "./dist/index.js",
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"import": "./dist/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./markdown": {
|
|
22
|
+
"types": "./dist/markdown.d.ts",
|
|
23
|
+
"import": "./dist/markdown.js"
|
|
24
|
+
},
|
|
25
|
+
"./package.json": "./package.json",
|
|
26
|
+
"./styles.css": "./dist/styles.css"
|
|
27
|
+
},
|
|
28
|
+
"//peers": "Everything on the root barrel draws with @kanzo-tech/ui's parts and Ark's machines, so both are required peers rather than dependencies — two copies of Ark would mean two portals and two focus traps. streamdown is the one OPTIONAL peer, and it is why ./markdown exists: it measures 495 kB minified and 128 kB brotli on its own, against a 20 kB budget for the whole root barrel, so a host that renders plain text must not pay for a markdown parser. Same door as @kanzo-tech/ui/editor and @kanzo-tech/graph/duckdb.",
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"@ark-ui/react": "^5.37.2",
|
|
31
|
+
"lucide-react": ">=1",
|
|
32
|
+
"react": ">=19",
|
|
33
|
+
"react-dom": ">=19",
|
|
34
|
+
"streamdown": ">=2",
|
|
35
|
+
"@kanzo-tech/ui": "0.1.0"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"tailwind-variants": "^3.2.2"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@ark-ui/react": "^5.37.2",
|
|
42
|
+
"@tailwindcss/cli": "4",
|
|
43
|
+
"@testing-library/dom": "^10.4.1",
|
|
44
|
+
"@testing-library/react": "^16.3.2",
|
|
45
|
+
"@testing-library/user-event": "^14.6.1",
|
|
46
|
+
"@types/react": "^19.0.0",
|
|
47
|
+
"@types/react-dom": "^19.0.0",
|
|
48
|
+
"jsdom": "^29.1.1",
|
|
49
|
+
"lucide-react": "^1.23.0",
|
|
50
|
+
"react": "^19.0.0",
|
|
51
|
+
"react-dom": "^19.0.0",
|
|
52
|
+
"rollup-plugin-preserve-directives": "^0.4.0",
|
|
53
|
+
"streamdown": "^2.5.0",
|
|
54
|
+
"tailwindcss": "4",
|
|
55
|
+
"@kanzo-tech/ui": "0.1.0",
|
|
56
|
+
"@kanzo-tech/theme": "0.1.0"
|
|
57
|
+
},
|
|
58
|
+
"//size-limit": "First measurement taken at the split, so this budget is a change detector from day one rather than a number inherited from somewhere. The four AI-assist modules that moved here cost @kanzo-tech/ui's root barrel about 4 kB when they arrived (38 -> 42 kB, recorded in that package's own note); the barrel is rebaselined down by the same move.",
|
|
59
|
+
"size-limit": [
|
|
60
|
+
{
|
|
61
|
+
"name": "root barrel (JS)",
|
|
62
|
+
"path": "dist/index.js",
|
|
63
|
+
"limit": "20 kB",
|
|
64
|
+
"ignore": [
|
|
65
|
+
"react",
|
|
66
|
+
"react-dom",
|
|
67
|
+
"react/jsx-runtime",
|
|
68
|
+
"@ark-ui/react",
|
|
69
|
+
"@kanzo-tech/ui",
|
|
70
|
+
"lucide-react",
|
|
71
|
+
"tailwind-variants"
|
|
72
|
+
]
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"name": "markdown subpath (JS)",
|
|
76
|
+
"path": "dist/markdown.js",
|
|
77
|
+
"limit": "3 kB",
|
|
78
|
+
"ignore": [
|
|
79
|
+
"react",
|
|
80
|
+
"react-dom",
|
|
81
|
+
"react/jsx-runtime",
|
|
82
|
+
"@ark-ui/react",
|
|
83
|
+
"@kanzo-tech/ui",
|
|
84
|
+
"lucide-react",
|
|
85
|
+
"tailwind-variants",
|
|
86
|
+
"streamdown"
|
|
87
|
+
]
|
|
88
|
+
}
|
|
89
|
+
],
|
|
90
|
+
"peerDependenciesMeta": {
|
|
91
|
+
"streamdown": {
|
|
92
|
+
"optional": true
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
"repository": {
|
|
96
|
+
"type": "git",
|
|
97
|
+
"url": "git+https://github.com/Kanzo-Tech/ui.git",
|
|
98
|
+
"directory": "packages/ai"
|
|
99
|
+
},
|
|
100
|
+
"homepage": "https://kanzo-tech.github.io/ui",
|
|
101
|
+
"bugs": {
|
|
102
|
+
"url": "https://github.com/Kanzo-Tech/ui/issues"
|
|
103
|
+
},
|
|
104
|
+
"scripts": {
|
|
105
|
+
"build": "vite build && pnpm build:css",
|
|
106
|
+
"typecheck": "tsc --noEmit",
|
|
107
|
+
"test": "vitest run",
|
|
108
|
+
"size": "size-limit",
|
|
109
|
+
"build:css": "tailwindcss -i src/styles.css -o dist/styles.css --minify"
|
|
110
|
+
}
|
|
111
|
+
}
|