@kanzo-tech/ai 0.18.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assist.d.ts +7 -0
- package/dist/assist.d.ts.map +1 -1
- package/dist/assist.js +168 -157
- package/dist/assist.js.map +1 -1
- package/dist/engine.d.ts +15 -4
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +179 -141
- package/dist/engine.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -7
- package/dist/index.js.map +1 -1
- package/dist/proposals.d.ts.map +1 -1
- package/dist/proposals.js +40 -28
- package/dist/proposals.js.map +1 -1
- package/package.json +5 -5
package/dist/engine.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"engine.js","sources":["../src/engine.ts"],"sourcesContent":["\"use client\";\n\n// The machinery behind `Assist`, and none of it is public. A field offers a **proposal** — text the\n// reader may take — and this file is everything about getting proposals out of a stream and keeping\n// them honest while the reader types: one engine, the inline continuation, and the candidate list.\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\n\n/** A value the model offers a field. LSP 3.18's `InlineCompletionItem`, as a field needs it. */\nexport interface Proposal {\n text: string;\n /**\n * What taking it replaces, as `[from, to)` in the field's value: an empty range at the caret is a\n * continuation, the whole value is a replacement. Absent for a new item in a list field (tags),\n * where taking it adds rather than edits.\n */\n range?: [from: number, to: number];\n /** Why the model offered it. Shown as the candidate's tooltip. */\n rationale?: string;\n}\n\n/**\n * What happened to a proposal, for a host that learns from it (HAX G13). The lifecycle VS Code's\n * inline completions report, in one callback: it was **shown**, taken whole (**accepted**) or a word\n * at a time (**partial**), turned down by the reader (**rejected**: Esc, or a candidate's ✕), or\n * left to die because they typed something else (**ignored**).\n */\nexport interface AssistEvent {\n kind: \"shown\" | \"accepted\" | \"partial\" | \"rejected\" | \"ignored\";\n proposal: Proposal;\n /** The field's accessible name — which field it was. */\n field: string;\n}\n\n/** Where a request has got to. Internal: no surface exposes it, they draw from it. */\nexport type Status = \"idle\" | \"loading\" | \"ready\" | \"error\";\n\nconst message = (e: unknown, fallback: string) =>\n e instanceof Error && e.message ? e.message : fallback;\n\n// ── The engine ───────────────────────────────────────────────────────────────\n\n/**\n * Open a source and pull it to the end, handing each value to `each`; `false` from `each` stops it.\n * A new run supersedes the one in flight, and resolves to what happened to **this** run, so a loop\n * that outlives its render never reads a stale status.\n */\nexport function useStream<T>(errorText: string) {\n const [status, setStatus] = useState<Status>(\"idle\");\n const [error, setError] = useState<string | null>(null);\n const statusRef = useRef<Status>(\"idle\");\n const ctrl = useRef<AbortController>(undefined);\n\n const set = useCallback((s: Status, 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<Status> => {\n try {\n for await (const value of src(c.signal)) {\n if (c.signal.aborted) return \"idle\";\n if (each(value) === false) {\n c.abort();\n break;\n }\n }\n } catch (e) {\n if (c.signal.aborted) return \"idle\";\n set(\"error\", message(e, errorText));\n return \"error\";\n }\n if (ctrl.current === c) set(\"ready\");\n return \"ready\";\n })();\n },\n [errorText, 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(() => ({ run, cancel, reset, status, error }), [cancel, error, reset, run, status]);\n}\n\n/**\n * Run `paint` at most once a frame, and once more on `flush()`. A token stream arrives faster than\n * the display refreshes, so a `setState` per chunk renders work nobody sees.\n */\nfunction coalesce(paint: () => void) {\n let frame: number | undefined;\n let queued = false;\n const run = () => {\n queued = true;\n if (frame !== undefined) return;\n frame = requestAnimationFrame(() => {\n frame = undefined;\n queued = false;\n paint();\n });\n };\n const cancel = () => {\n if (frame !== undefined) cancelAnimationFrame(frame);\n frame = undefined;\n queued = false;\n };\n const flush = () => {\n const pending = queued;\n cancel();\n if (pending) paint();\n };\n return Object.assign(run, { flush, cancel });\n}\n\n/**\n * The continuation is appended verbatim, with one safety net: a model that restated the value\n * before continuing gets that prefix dropped, and an echo still arriving renders as nothing until it\n * diverges — painting the value twice is worse than a few frames of nothing.\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\n// ── The continuation: one offer at the caret, with alternatives ──────────────\n\n/** Below this there is not enough text to continue, so nothing is asked. */\nexport const MIN_CONTINUE_LENGTH = 4;\n\nexport type Trigger = \"invoked\" | \"automatic\";\n\nexport interface ContinuationRequest {\n value: string;\n position: number;\n trigger: Trigger;\n /** Continuations already offered at this point; an alternative must differ from them. */\n avoid: string[];\n signal: AbortSignal;\n}\n\ninterface Offer {\n from: number;\n text: string;\n done: boolean;\n}\n\n/**\n * Ghost text that does not own the value: the field owns it and performs the insertion.\n *\n * **The offer survives typing that agrees with it.** An offer made at `from` stays on the table\n * while what was typed since is a prefix of it, and the ghost is what is left — no request at all.\n * That is LSP's `filterText` rule and the only way to Smart Compose's 60 ms budget: not asking.\n *\n * **Alternatives** are LSP's `Invoked` case: asking again at the same point appends a different\n * offer rather than replacing the one on the table, and `cycle` moves between them (Alt+] / Alt+[).\n */\nexport function useContinuation(options: {\n source: (request: ContinuationRequest) => AsyncIterable<string>;\n onEvent: (kind: AssistEvent[\"kind\"], proposal: Proposal) => void;\n debounceMs?: number;\n}) {\n const { run, cancel, status, error } = useStream<string>(\"Couldn’t complete\");\n const [ghost, setGhost] = useState(\"\");\n const opts = useRef(options);\n opts.current = options;\n\n const offers = useRef<Offer[]>([]);\n const index = useRef(0);\n const field = useRef({ value: \"\", position: 0 });\n const timer = useRef<ReturnType<typeof setTimeout>>(undefined);\n const shown = useRef<Offer | null>(null);\n\n const current = () => offers.current[index.current] ?? null;\n\n const rendered = useCallback((): string => {\n const o = 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 const alive = useCallback((): boolean => {\n const o = 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 // Typing ahead of a stream still arriving is not a mismatch yet: the next chunk may agree.\n return o.text.startsWith(typed) || (!o.done && typed.startsWith(o.text));\n }, []);\n\n const proposal = (o: Offer): Proposal => ({ text: o.text, range: [o.from, o.from] });\n\n const painter = useRef<ReturnType<typeof coalesce>>(undefined);\n painter.current ??= coalesce(() => {\n const next = rendered();\n setGhost(next);\n const o = current();\n if (next && o && shown.current !== o) {\n shown.current = o;\n opts.current.onEvent(\"shown\", proposal(o));\n }\n });\n\n /** Forget every offer. `why` is reported once, for the offer on screen. */\n const drop = useCallback(\n (why?: \"rejected\" | \"ignored\") => {\n clearTimeout(timer.current);\n painter.current?.cancel();\n const o = current();\n if (why && o && shown.current === o) opts.current.onEvent(why, proposal(o));\n offers.current = [];\n index.current = 0;\n shown.current = null;\n cancel();\n setGhost(\"\");\n },\n [cancel],\n );\n\n const start = useCallback(\n (value: string, position: number, trigger: Trigger) => {\n const avoid = offers.current.filter((o) => o.from === position).map((o) => o.text);\n const mine: Offer = { from: position, text: \"\", done: false };\n offers.current = [...offers.current.filter((o) => o.from === position), mine];\n index.current = offers.current.length - 1;\n const base = value.slice(0, position);\n let raw = \"\";\n void run(\n (signal) => opts.current.source({ value, position, trigger, avoid, signal }),\n (chunk) => {\n if (current() !== mine) return false;\n raw += chunk;\n mine.text = cleanGhost(base, raw);\n painter.current?.();\n },\n ).then((outcome) => {\n mine.done = true;\n if (current() !== mine) return;\n if (outcome === \"ready\") painter.current?.flush();\n else painter.current?.cancel();\n });\n },\n [run],\n );\n\n const short = (value: string) => value.trim().length < MIN_CONTINUE_LENGTH;\n\n /** The field changed. Ask after a debounce — unless the offer on the table survives it. */\n const setValue = useCallback(\n (value: string, position = value.length) => {\n field.current = { value, position };\n if (alive()) {\n setGhost(rendered());\n return;\n }\n drop(\"ignored\");\n if (short(value)) return;\n timer.current = setTimeout(\n () => start(value, position, \"automatic\"),\n opts.current.debounceMs ?? 350,\n );\n },\n [alive, drop, rendered, start],\n );\n\n /** Ask now — the ✨, and Alt+] past the last alternative. */\n const ask = useCallback(\n (value: string, position = value.length) => {\n const same = field.current.value === value && field.current.position === position;\n field.current = { value, position };\n if (!same) drop(\"ignored\");\n clearTimeout(timer.current);\n if (short(value)) return;\n start(value, position, \"invoked\");\n },\n [drop, start],\n );\n\n /** Move between alternatives; past the last one, ask for another. */\n const cycle = useCallback(\n (step: 1 | -1) => {\n const next = index.current + step;\n if (next < 0) return;\n if (next >= offers.current.length) {\n const { value, position } = field.current;\n ask(value, position);\n return;\n }\n index.current = next;\n setGhost(rendered());\n painter.current?.();\n },\n [ask, rendered],\n );\n\n const settle = useCallback(\n (kind: \"accepted\" | \"partial\", text: string) => {\n const o = current();\n if (o) opts.current.onEvent(kind, { text, range: [field.current.position, field.current.position] });\n if (kind === \"accepted\") {\n offers.current = [];\n index.current = 0;\n shown.current = null;\n clearTimeout(timer.current);\n painter.current?.cancel();\n cancel();\n setGhost(\"\");\n }\n },\n [cancel],\n );\n\n useEffect(\n () => () => {\n clearTimeout(timer.current);\n painter.current?.cancel();\n },\n [],\n );\n\n return { ghost, status, error, setValue, ask, cycle, settle, dismiss: drop };\n}\n\n// ── The candidates: N whole values, deduped ──────────────────────────────────\n\nconst norm = (v: string) => v.trim().toLowerCase();\n\n/**\n * A streamed list of whole values, deduped against what the field already holds. A budget rather\n * than a window: nothing refills when one is taken or dismissed.\n */\nexport function useCandidates(options: {\n source: (request: { existing: string[]; signal: AbortSignal }) => AsyncIterable<Proposal>;\n existing: string[];\n onEvent: (kind: AssistEvent[\"kind\"], proposal: Proposal) => void;\n limit?: number;\n}) {\n const { run, cancel: abort, reset, status, error } = useStream<Proposal>(\"Couldn’t load suggestions\");\n const [items, setItems] = useState<Proposal[]>([]);\n const shown = useRef<Proposal[]>([]);\n const opts = useRef(options);\n opts.current = options;\n\n const put = useCallback((next: Proposal[]) => {\n shown.current = next;\n setItems(next);\n }, []);\n\n const refresh = useCallback(() => {\n const { existing, limit = 6, source } = opts.current;\n const seen = new Set(existing.map(norm).filter(Boolean));\n const taken: Proposal[] = [];\n put([]);\n void run(\n (signal) => source({ existing, signal }),\n (item) => {\n const key = norm(item.text);\n if (!key || seen.has(key)) return true;\n seen.add(key);\n taken.push(item);\n put([...taken]);\n opts.current.onEvent(\"shown\", item);\n return taken.length < limit;\n },\n );\n }, [put, run]);\n\n /** The ✨: a first set, a different set, or a retry — never inert, except while loading. */\n const press = useCallback(() => {\n if (status === \"loading\") return;\n refresh();\n }, [refresh, status]);\n\n const remove = useCallback(\n (text: string, kind: \"accepted\" | \"rejected\") => {\n const key = norm(text);\n const item = shown.current.find((p) => norm(p.text) === key);\n if (item) opts.current.onEvent(kind, item);\n const next = shown.current.filter((p) => norm(p.text) !== key);\n put(next);\n // Nothing left of an answer is not an answer with nothing in it.\n if (next.length === 0) reset();\n },\n [put, reset],\n );\n\n const cancel = useCallback(() => {\n abort();\n put([]);\n }, [abort, put]);\n\n return { items, status, error, press, take: (t: string) => remove(t, \"accepted\"), dismiss: (t: string) => remove(t, \"rejected\"), cancel };\n}\n"],"names":[],"mappings":";;AAqCA;AAUO;AACL;AAME;AAEU;AAGZ;;AAAsB;AAAc;AAEpC;AAAY;;AAER;AACA;AACA;AAIE;AACE;AACE;AACA;AACE;AACA;AAAA;AACF;AACF;AAEA;AAEO;AAET;AACO;AACT;AACF;AACe;;AAIf;AACU;AAIV;AAA+C;AAGjD;AACF;AAMA;AACE;AAEA;AAEE;AAEE;AAEA;AACD;AAGD;AAES;AAOX;AAJE;AACA;AACa;AAGjB;AAOO;AACL;AACA;AACA;AACA;AAGF;AAKO;AA6BA;AAKL;AAGA;AAEA;AASE;AACA;AACA;AACA;AACA;AACA;AAA+D;AAI/D;AACA;AACA;AACA;AACA;AAEA;AAAsE;AAMxE;AACE;AACA;AACA;AACA;AAE2C;AAK7C;AAAa;;AAET;AAEA;AACA;AAKW;AACb;AACO;AAGK;AAEV;AAEA;AAEA;AACA;AACA;AAAK;AACwE;;AAEzE;AACA;AAEA;AACF;;AAGA;AAEsB;AACvB;AACH;AACI;AAMW;AAGb;AACE;AACA;AAAA;AAGF;AACgB;AAC0B;AACb;AAAA;AAE/B;AAC6B;AAInB;AAER;AAIA;AACgC;AAClC;AACY;AAIA;;AAEV;AACA;AACA;AACE;AACA;AACA;AAAA;AAEF;AAEA;AAAA;AACF;AACc;AAGD;;AAGX;AAQa;AAEf;AACO;AAGT;AAAA;;AAEI;AACiB;AACnB;AACA;AAIJ;AAIA;AAMO;AAML;AAIA;AAEA;AACE;AACa;AAIb;AAGA;AACK;AACoC;AAErC;AACA;AAKsB;AACxB;AAAA;AAMF;AACA;AAGa;AAEX;AAEA;AACA;AACA;AAEuB;AACzB;AACW;AAIX;AACM;AAGR;AACF;;;;;;;;"}
|
|
1
|
+
{"version":3,"file":"engine.js","sources":["../src/engine.ts"],"sourcesContent":["\"use client\";\n\n// The machinery behind `Assist`, and none of it is public. A field offers a **proposal** — text the\n// reader may take — and this file is everything about getting proposals out of a stream and keeping\n// them honest while the reader types: one engine, the inline continuation, and the candidate list.\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\n\n/** A value the model offers a field. LSP 3.18's `InlineCompletionItem`, as a field needs it. */\nexport interface Proposal {\n text: string;\n /**\n * What taking it replaces, as `[from, to)` in the field's value: an empty range at the caret is a\n * continuation, the whole value is a replacement. Absent for a new item in a list field (tags),\n * where taking it adds rather than edits.\n */\n range?: [from: number, to: number];\n /** Why the model offered it. Shown as the candidate's tooltip. */\n rationale?: string;\n}\n\n/**\n * What happened to a proposal, for a host that learns from it (HAX G13). The lifecycle VS Code's\n * inline completions report, in one callback: it was **shown**, taken whole (**accepted**) or a word\n * at a time (**partial**), turned down by the reader (**rejected**: Esc, or a candidate's ✕), or\n * left to die because they typed something else (**ignored**).\n */\nexport interface AssistEvent {\n kind: \"shown\" | \"accepted\" | \"partial\" | \"rejected\" | \"ignored\";\n proposal: Proposal;\n /** The field's accessible name — which field it was. */\n field: string;\n}\n\n/** Where a request has got to. Internal: no surface exposes it, they draw from it. */\nexport type Status = \"idle\" | \"loading\" | \"ready\" | \"error\";\n\n/** How long a stream may go without sending anything, the first chunk included. */\nconst SILENT_AFTER = 30_000;\n\n/** The one failure this package names: the model stopped sending without closing the stream. */\nexport class AiError extends Error {\n override readonly name = \"AiError\";\n constructor(\n readonly code: \"ai/silent\",\n message: string,\n readonly data: { readonly after?: number } = {},\n options?: ErrorOptions,\n ) {\n super(message, options);\n }\n}\n\n/**\n * `pending`, unless `c` aborts first or nothing arrives within {@link SILENT_AFTER} — then `c` is\n * aborted with an `ai/silent` `AiError`, which this rejects with.\n */\nfunction heard<T>(pending: Promise<T>, c: AbortController): Promise<T> {\n return new Promise<T>((resolve, reject) => {\n const settle = () => {\n clearTimeout(timer);\n c.signal.removeEventListener(\"abort\", aborted);\n };\n const aborted = () => {\n settle();\n reject(c.signal.reason);\n };\n const timer = setTimeout(\n () => c.abort(new AiError(\"ai/silent\", `The model sent nothing for ${SILENT_AFTER} ms`, { after: SILENT_AFTER })),\n SILENT_AFTER,\n );\n if (c.signal.aborted) return aborted();\n c.signal.addEventListener(\"abort\", aborted);\n pending.then(\n (value) => {\n settle();\n resolve(value);\n },\n (e: unknown) => {\n settle();\n reject(e);\n },\n );\n });\n}\n\n// ── The engine ───────────────────────────────────────────────────────────────\n\n/**\n * Open a source and pull it to the end, handing each value to `each`; `false` from `each` stops it.\n * A new run supersedes the one in flight, and resolves to what happened to **this** run, so a loop\n * that outlives its render never reads a stale status.\n */\nexport function useStream<T>() {\n const [status, setStatus] = useState<Status>(\"idle\");\n /** What the source threw, as thrown, while `status` is `\"error\"`. */\n const [error, setError] = useState<unknown>(undefined);\n const statusRef = useRef<Status>(\"idle\");\n const ctrl = useRef<AbortController>(undefined);\n\n const set = useCallback((s: Status, e?: unknown) => {\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<Status> => {\n try {\n const it = src(c.signal)[Symbol.asyncIterator]();\n for (;;) {\n const res = await heard(it.next(), c);\n if (c.signal.aborted) return \"idle\";\n if (res.done) break;\n if (each(res.value) === false) {\n c.abort();\n break;\n }\n }\n } catch (e) {\n // A cancellation is not a failure to report — the caller asked for it. Silence is.\n if (c.signal.aborted && !(c.signal.reason instanceof AiError)) return \"idle\";\n if (!c.signal.aborted) c.abort();\n set(\"error\", e);\n return \"error\";\n }\n if (ctrl.current === c) 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(() => ({ run, cancel, reset, status, error }), [cancel, error, reset, run, status]);\n}\n\n/**\n * Run `paint` at most once a frame, and once more on `flush()`. A token stream arrives faster than\n * the display refreshes, so a `setState` per chunk renders work nobody sees.\n */\nfunction coalesce(paint: () => void) {\n let frame: number | undefined;\n let queued = false;\n const run = () => {\n queued = true;\n if (frame !== undefined) return;\n frame = requestAnimationFrame(() => {\n frame = undefined;\n queued = false;\n paint();\n });\n };\n const cancel = () => {\n if (frame !== undefined) cancelAnimationFrame(frame);\n frame = undefined;\n queued = false;\n };\n const flush = () => {\n const pending = queued;\n cancel();\n if (pending) paint();\n };\n return Object.assign(run, { flush, cancel });\n}\n\n/**\n * The continuation is appended verbatim, with one safety net: a model that restated the value\n * before continuing gets that prefix dropped, and an echo still arriving renders as nothing until it\n * diverges — painting the value twice is worse than a few frames of nothing.\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\n// ── The continuation: one offer at the caret, with alternatives ──────────────\n\n/** Below this there is not enough text to continue, so nothing is asked. */\nexport const MIN_CONTINUE_LENGTH = 4;\n\nexport type Trigger = \"invoked\" | \"automatic\";\n\nexport interface ContinuationRequest {\n value: string;\n position: number;\n trigger: Trigger;\n /** Continuations already offered at this point; an alternative must differ from them. */\n avoid: string[];\n signal: AbortSignal;\n}\n\ninterface Offer {\n from: number;\n text: string;\n done: boolean;\n}\n\n/**\n * Ghost text that does not own the value: the field owns it and performs the insertion.\n *\n * **The offer survives typing that agrees with it.** An offer made at `from` stays on the table\n * while what was typed since is a prefix of it, and the ghost is what is left — no request at all.\n * That is LSP's `filterText` rule and the only way to Smart Compose's 60 ms budget: not asking.\n *\n * **Alternatives** are LSP's `Invoked` case: asking again at the same point appends a different\n * offer rather than replacing the one on the table, and `cycle` moves between them (Alt+] / Alt+[).\n */\nexport function useContinuation(options: {\n source: (request: ContinuationRequest) => AsyncIterable<string>;\n onEvent: (kind: AssistEvent[\"kind\"], proposal: Proposal) => void;\n debounceMs?: number;\n}) {\n const { run, cancel, status, error } = useStream<string>();\n const [ghost, setGhost] = useState(\"\");\n const opts = useRef(options);\n opts.current = options;\n\n const offers = useRef<Offer[]>([]);\n const index = useRef(0);\n const field = useRef({ value: \"\", position: 0 });\n const timer = useRef<ReturnType<typeof setTimeout>>(undefined);\n const shown = useRef<Offer | null>(null);\n\n const current = () => offers.current[index.current] ?? null;\n\n const rendered = useCallback((): string => {\n const o = 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 const alive = useCallback((): boolean => {\n const o = 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 // Typing ahead of a stream still arriving is not a mismatch yet: the next chunk may agree.\n return o.text.startsWith(typed) || (!o.done && typed.startsWith(o.text));\n }, []);\n\n const proposal = (o: Offer): Proposal => ({ text: o.text, range: [o.from, o.from] });\n\n const painter = useRef<ReturnType<typeof coalesce>>(undefined);\n painter.current ??= coalesce(() => {\n const next = rendered();\n setGhost(next);\n const o = current();\n if (next && o && shown.current !== o) {\n shown.current = o;\n opts.current.onEvent(\"shown\", proposal(o));\n }\n });\n\n /** Forget every offer. `why` is reported once, for the offer on screen. */\n const drop = useCallback(\n (why?: \"rejected\" | \"ignored\") => {\n clearTimeout(timer.current);\n painter.current?.cancel();\n const o = current();\n if (why && o && shown.current === o) opts.current.onEvent(why, proposal(o));\n offers.current = [];\n index.current = 0;\n shown.current = null;\n cancel();\n setGhost(\"\");\n },\n [cancel],\n );\n\n const start = useCallback(\n (value: string, position: number, trigger: Trigger) => {\n const avoid = offers.current.filter((o) => o.from === position).map((o) => o.text);\n const mine: Offer = { from: position, text: \"\", done: false };\n offers.current = [...offers.current.filter((o) => o.from === position), mine];\n index.current = offers.current.length - 1;\n const base = value.slice(0, position);\n let raw = \"\";\n void run(\n (signal) => opts.current.source({ value, position, trigger, avoid, signal }),\n (chunk) => {\n if (current() !== mine) return false;\n raw += chunk;\n mine.text = cleanGhost(base, raw);\n painter.current?.();\n },\n ).then((outcome) => {\n mine.done = true;\n if (current() !== mine) return;\n if (outcome === \"ready\") return painter.current?.flush();\n painter.current?.cancel();\n // A failed stream's half a continuation is not an offer; the failure is `error`.\n if (outcome === \"error\") {\n mine.text = \"\";\n setGhost(\"\");\n }\n });\n },\n [run],\n );\n\n const short = (value: string) => value.trim().length < MIN_CONTINUE_LENGTH;\n\n /** The field changed. Ask after a debounce — unless the offer on the table survives it. */\n const setValue = useCallback(\n (value: string, position = value.length) => {\n field.current = { value, position };\n if (alive()) {\n setGhost(rendered());\n return;\n }\n drop(\"ignored\");\n if (short(value)) return;\n timer.current = setTimeout(\n () => start(value, position, \"automatic\"),\n opts.current.debounceMs ?? 350,\n );\n },\n [alive, drop, rendered, start],\n );\n\n /** Ask now — the ✨, and Alt+] past the last alternative. */\n const ask = useCallback(\n (value: string, position = value.length) => {\n const same = field.current.value === value && field.current.position === position;\n field.current = { value, position };\n if (!same) drop(\"ignored\");\n clearTimeout(timer.current);\n if (short(value)) return;\n start(value, position, \"invoked\");\n },\n [drop, start],\n );\n\n /** Move between alternatives; past the last one, ask for another. */\n const cycle = useCallback(\n (step: 1 | -1) => {\n const next = index.current + step;\n if (next < 0) return;\n if (next >= offers.current.length) {\n const { value, position } = field.current;\n ask(value, position);\n return;\n }\n index.current = next;\n setGhost(rendered());\n painter.current?.();\n },\n [ask, rendered],\n );\n\n const settle = useCallback(\n (kind: \"accepted\" | \"partial\", text: string) => {\n const o = current();\n if (o) opts.current.onEvent(kind, { text, range: [field.current.position, field.current.position] });\n if (kind === \"accepted\") {\n offers.current = [];\n index.current = 0;\n shown.current = null;\n clearTimeout(timer.current);\n painter.current?.cancel();\n cancel();\n setGhost(\"\");\n }\n },\n [cancel],\n );\n\n useEffect(\n () => () => {\n clearTimeout(timer.current);\n painter.current?.cancel();\n },\n [],\n );\n\n return { ghost, status, error, setValue, ask, cycle, settle, dismiss: drop };\n}\n\n// ── The candidates: N whole values, deduped ──────────────────────────────────\n\nconst norm = (v: string) => v.trim().toLowerCase();\n\n/**\n * A streamed list of whole values, deduped against what the field already holds. A budget rather\n * than a window: nothing refills when one is taken or dismissed.\n */\nexport function useCandidates(options: {\n source: (request: { existing: string[]; signal: AbortSignal }) => AsyncIterable<Proposal>;\n existing: string[];\n onEvent: (kind: AssistEvent[\"kind\"], proposal: Proposal) => void;\n limit?: number;\n}) {\n const { run, cancel: abort, reset, status, error } = useStream<Proposal>();\n const [items, setItems] = useState<Proposal[]>([]);\n const shown = useRef<Proposal[]>([]);\n const opts = useRef(options);\n opts.current = options;\n\n const put = useCallback((next: Proposal[]) => {\n shown.current = next;\n setItems(next);\n }, []);\n\n const refresh = useCallback(() => {\n const { existing, limit = 6, source } = opts.current;\n const seen = new Set(existing.map(norm).filter(Boolean));\n const taken: Proposal[] = [];\n put([]);\n void run(\n (signal) => source({ existing, signal }),\n (item) => {\n const key = norm(item.text);\n if (!key || seen.has(key)) return true;\n seen.add(key);\n taken.push(item);\n put([...taken]);\n opts.current.onEvent(\"shown\", item);\n return taken.length < limit;\n },\n );\n }, [put, run]);\n\n /** The ✨: a first set, a different set, or a retry — never inert, except while loading. */\n const press = useCallback(() => {\n if (status === \"loading\") return;\n refresh();\n }, [refresh, status]);\n\n const remove = useCallback(\n (text: string, kind: \"accepted\" | \"rejected\") => {\n const key = norm(text);\n const item = shown.current.find((p) => norm(p.text) === key);\n if (item) opts.current.onEvent(kind, item);\n const next = shown.current.filter((p) => norm(p.text) !== key);\n put(next);\n // Nothing left of an answer is not an answer with nothing in it.\n if (next.length === 0) reset();\n },\n [put, reset],\n );\n\n const cancel = useCallback(() => {\n abort();\n put([]);\n }, [abort, put]);\n\n return { items, status, error, press, take: (t: string) => remove(t, \"accepted\"), dismiss: (t: string) => remove(t, \"rejected\"), cancel };\n}\n"],"names":[],"mappings":";;;;;AAsCA;AAGO;AAA4B;AAQ/B;AAPgB;AAEP;AAEA;AAKb;AAMA;AACE;AACE;AACE;AAC6C;AAG7C;AACsB;AAEV;AACoG;AAChH;AAEF;AACA;AACQ;AAEJ;AACa;AACf;AAEE;AACQ;AACV;AAAA;AAGN;AASO;AACL;AAOE;AAEU;AAGZ;;AAAsB;AAAc;AAEpC;AAAY;;AAER;AACA;AACA;AAIE;AACE;AACA;AACE;AACA;AACA;AACA;AACE;AACA;AAAA;AACF;AACF;AAGA;AAGO;AAET;AACO;AACT;AACF;AACI;;AAIJ;AACU;AAIV;AAA+C;AAGjD;AACF;AAMA;AACE;AAEA;AAEE;AAEE;AAEA;AACD;AAGD;AAES;AAOX;AAJE;AACA;AACa;AAGjB;AAOO;AACL;AACA;AACA;AACA;AAGF;AAKO;AA6BA;AAKL;AAGA;AAEA;AASE;AACA;AACA;AACA;AACA;AACA;AAA+D;AAI/D;AACA;AACA;AACA;AACA;AAEA;AAAsE;AAMxE;AACE;AACA;AACA;AACA;AAE2C;AAK7C;AAAa;;AAET;AAEA;AACA;AAKW;AACb;AACO;AAGK;AAEV;AAEA;AAEA;AACA;AACA;AAAK;AACwE;;AAEzE;AACA;AAEA;AACF;;AAGA;AACA;AACA;AAIa;AAAA;AAEd;AACH;AACI;AAMW;AAGb;AACE;AACA;AAAA;AAGF;AACgB;AAC0B;AACb;AAAA;AAE/B;AAC6B;AAInB;AAER;AAIA;AACgC;AAClC;AACY;AAIA;;AAEV;AACA;AACA;AACE;AACA;AACA;AAAA;AAEF;AAEA;AAAA;AACF;AACc;AAGD;;AAGX;AAQa;AAEf;AACO;AAGT;AAAA;;AAEI;AACiB;AACnB;AACA;AAIJ;AAIA;AAMO;AAML;AAIA;AAEA;AACE;AACa;AAIb;AAGA;AACK;AACoC;AAErC;AACA;AAKsB;AACxB;AAAA;AAMF;AACA;AAGa;AAEX;AAEA;AACA;AACA;AAEuB;AACzB;AACW;AAIX;AACM;AAGR;AACF;;;;;;;;;"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { AssistProvider, Assist } from './assist.js';
|
|
2
2
|
export type { AssistProviderProps, AssistProps, AssistTranslations } from './assist.js';
|
|
3
|
+
export { AiError } from './engine.js';
|
|
3
4
|
export type { AssistEvent, Proposal } from './engine.js';
|
|
4
5
|
export { Chat } from './chat.js';
|
|
5
6
|
export type { ChatProps, ChatToolRenderers, ChatTranslations } from './chat.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrD,YAAY,EAAE,mBAAmB,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACxF,YAAY,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEzD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,YAAY,EAAE,SAAS,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAChF,YAAY,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAE1C,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrD,YAAY,EAAE,mBAAmB,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACxF,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AACtC,YAAY,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEzD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,YAAY,EAAE,SAAS,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAChF,YAAY,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAE1C,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { Assist as
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { Assist as t, AssistProvider as s } from "./assist.js";
|
|
2
|
+
import { AiError as f } from "./engine.js";
|
|
3
|
+
import { Chat as m } from "./chat.js";
|
|
4
|
+
import { useChat as x } from "@ai-sdk/react";
|
|
4
5
|
export {
|
|
5
|
-
|
|
6
|
-
t as
|
|
7
|
-
|
|
8
|
-
m as
|
|
6
|
+
f as AiError,
|
|
7
|
+
t as Assist,
|
|
8
|
+
s as AssistProvider,
|
|
9
|
+
m as Chat,
|
|
10
|
+
x as useChat
|
|
9
11
|
};
|
|
10
12
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;"}
|
package/dist/proposals.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"proposals.d.ts","sourceRoot":"","sources":["../src/proposals.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,aAAa,EAAkC,MAAM,iBAAiB,CAAC;AACrF,OAAO,KAAK,EAAE,mBAAmB,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEjE,4DAA4D;AAC5D,MAAM,WAAW,UAAU;IACzB,sDAAsD;IACtD,IAAI,EAAE,MAAM,CAAC;IACb,6DAA6D;IAC7D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,2EAA2E;IAC3E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mEAAmE;IACnE,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;
|
|
1
|
+
{"version":3,"file":"proposals.d.ts","sourceRoot":"","sources":["../src/proposals.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,aAAa,EAAkC,MAAM,iBAAiB,CAAC;AACrF,OAAO,KAAK,EAAE,mBAAmB,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEjE,4DAA4D;AAC5D,MAAM,WAAW,UAAU;IACzB,sDAAsD;IACtD,IAAI,EAAE,MAAM,CAAC;IACb,6DAA6D;IAC7D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,2EAA2E;IAC3E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mEAAmE;IACnE,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AA+BD,+CAA+C;AAC/C,wBAAuB,YAAY,CACjC,KAAK,EAAE,aAAa,EACpB,KAAK,EAAE,UAAU,EACjB,EAAE,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,mBAAmB,GAC/D,aAAa,CAAC,MAAM,CAAC,CAcvB;AAcD;;;GAGG;AACH,wBAAuB,UAAU,CAC/B,KAAK,EAAE,aAAa,EACpB,KAAK,EAAE,UAAU,EACjB,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE;IACxC,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,MAAM,EAAE,WAAW,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,sFAAsF;IACtF,IAAI,EAAE,OAAO,CAAC;CACf,GACA,aAAa,CAAC,QAAQ,CAAC,CAmBzB"}
|
package/dist/proposals.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { streamText as
|
|
1
|
+
import { streamText as p, Output as h, jsonSchema as f } from "@kanzo-tech/llm";
|
|
2
2
|
const m = (e) => [
|
|
3
3
|
`Field: ${e.name || "(unnamed)"}`,
|
|
4
4
|
e.description && `Field description: ${e.description}`,
|
|
@@ -6,26 +6,36 @@ const m = (e) => [
|
|
|
6
6
|
e.context && `Form context:
|
|
7
7
|
${e.context}`
|
|
8
8
|
].filter(Boolean).join(`
|
|
9
|
-
`),
|
|
9
|
+
`), g = `You complete text inside a form field, like an editor's inline completion.
|
|
10
10
|
Reply with ONLY the text to insert at <caret/>: no quotes, no commentary, never repeat text that is already there.
|
|
11
11
|
Keep the language, tone and format of the existing text. Stop at a natural point: the end of the sentence for an automatic suggestion; up to a short paragraph when explicitly asked.`;
|
|
12
|
-
function
|
|
13
|
-
|
|
12
|
+
function d() {
|
|
13
|
+
let e;
|
|
14
|
+
return {
|
|
15
|
+
onError: ({ error: n }) => void (e ?? (e = { error: n })),
|
|
16
|
+
rethrow: () => {
|
|
17
|
+
if (e) throw e.error;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
async function* E(e, n, { value: t, position: o, trigger: c, avoid: a, signal: i }) {
|
|
22
|
+
const u = `${t.slice(0, o)}<caret/>${t.slice(o)}`, s = a.length ? `
|
|
14
23
|
Offer something different from these earlier suggestions:
|
|
15
|
-
${
|
|
16
|
-
`)}` : "";
|
|
17
|
-
|
|
24
|
+
${a.map((r) => `- ${r.trim()}`).join(`
|
|
25
|
+
`)}` : "", l = d();
|
|
26
|
+
yield* p({
|
|
18
27
|
model: e,
|
|
19
|
-
system:
|
|
20
|
-
prompt: `${m(
|
|
21
|
-
Request: ${
|
|
28
|
+
system: g,
|
|
29
|
+
prompt: `${m(n)}
|
|
30
|
+
Request: ${c}${s}
|
|
22
31
|
|
|
23
32
|
Text:
|
|
24
|
-
${
|
|
25
|
-
abortSignal:
|
|
26
|
-
|
|
33
|
+
${u}`,
|
|
34
|
+
abortSignal: i,
|
|
35
|
+
onError: l.onError
|
|
36
|
+
}).textStream, l.rethrow();
|
|
27
37
|
}
|
|
28
|
-
const
|
|
38
|
+
const y = f({
|
|
29
39
|
type: "object",
|
|
30
40
|
properties: {
|
|
31
41
|
value: { type: "string", description: "The whole value, exactly as it would be entered." },
|
|
@@ -33,24 +43,26 @@ const f = h({
|
|
|
33
43
|
},
|
|
34
44
|
required: ["value", "rationale"],
|
|
35
45
|
additionalProperties: !1
|
|
36
|
-
}),
|
|
37
|
-
async function*
|
|
38
|
-
const
|
|
46
|
+
}), $ = "You suggest values for a form field. Each value must be complete and ready to enter as-is, in the language of the form.";
|
|
47
|
+
async function* w(e, n, { value: t, existing: o, signal: c, count: a, list: i }) {
|
|
48
|
+
const u = i ? `Current items: ${o.length ? o.join(", ") : "(none)"}
|
|
39
49
|
Suggest NEW items, not already present.` : `Current value: ${t || "(empty)"}
|
|
40
|
-
Suggest alternatives to replace it${t ? ", different from it" : ""}.`,
|
|
50
|
+
Suggest alternatives to replace it${t ? ", different from it" : ""}.`, s = d(), l = p({
|
|
41
51
|
model: e,
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
52
|
+
onError: s.onError,
|
|
53
|
+
system: $,
|
|
54
|
+
prompt: `${m(n)}
|
|
55
|
+
${u}
|
|
56
|
+
Give ${a}.`,
|
|
57
|
+
output: h.array({ element: y }),
|
|
58
|
+
abortSignal: c
|
|
48
59
|
});
|
|
49
|
-
for await (const
|
|
50
|
-
yield
|
|
60
|
+
for await (const r of l.elementStream)
|
|
61
|
+
yield i ? { text: r.value, rationale: r.rationale } : { text: r.value, rationale: r.rationale, range: [0, t.length] };
|
|
62
|
+
s.rethrow();
|
|
51
63
|
}
|
|
52
64
|
export {
|
|
53
|
-
|
|
54
|
-
|
|
65
|
+
w as candidates,
|
|
66
|
+
E as continuation
|
|
55
67
|
};
|
|
56
68
|
//# sourceMappingURL=proposals.js.map
|
package/dist/proposals.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"proposals.js","sources":["../src/proposals.ts"],"sourcesContent":["// What a field asks the model, written once. A host names the model and, at most, a sentence of\n// instructions; the field describes itself through its accessible name and description, which is\n// what a person filling it in reads too.\n\nimport { type LanguageModel, Output, jsonSchema, streamText } from \"@kanzo-tech/llm\";\nimport type { ContinuationRequest, Proposal } from \"./engine.js\";\n\n/** What the model is told about the field it is filling. */\nexport interface FieldBrief {\n /** The accessible name — the label a person reads. */\n name: string;\n /** The accessible description — the helper text under it. */\n description?: string;\n /** The host's own instruction for this field (`<Assist instructions>`). */\n instructions?: string;\n /** What the rest of the form says (`<AssistProvider context>`). */\n context?: string;\n}\n\nconst brief = (field: FieldBrief) =>\n [\n `Field: ${field.name || \"(unnamed)\"}`,\n field.description && `Field description: ${field.description}`,\n field.instructions && `Instructions: ${field.instructions}`,\n field.context && `Form context:\\n${field.context}`,\n ]\n .filter(Boolean)\n .join(\"\\n\");\n\nconst CONTINUE = `You complete text inside a form field, like an editor's inline completion.\nReply with ONLY the text to insert at <caret/>: no quotes, no commentary, never repeat text that is already there.\nKeep the language, tone and format of the existing text. Stop at a natural point: the end of the sentence for an automatic suggestion; up to a short paragraph when explicitly asked.`;\n\n/** One continuation at the caret, streamed. */\nexport function continuation(\n model: LanguageModel,\n field: FieldBrief,\n { value, position, trigger, avoid, signal }: ContinuationRequest,\n): AsyncIterable<string> {\n const text = `${value.slice(0, position)}<caret/>${value.slice(position)}`;\n const differ = avoid.length\n ? `\\nOffer something different from these earlier suggestions:\\n${avoid.map((a) => `- ${a.trim()}`).join(\"\\n\")}`\n : \"\";\n
|
|
1
|
+
{"version":3,"file":"proposals.js","sources":["../src/proposals.ts"],"sourcesContent":["// What a field asks the model, written once. A host names the model and, at most, a sentence of\n// instructions; the field describes itself through its accessible name and description, which is\n// what a person filling it in reads too.\n\nimport { type LanguageModel, Output, jsonSchema, streamText } from \"@kanzo-tech/llm\";\nimport type { ContinuationRequest, Proposal } from \"./engine.js\";\n\n/** What the model is told about the field it is filling. */\nexport interface FieldBrief {\n /** The accessible name — the label a person reads. */\n name: string;\n /** The accessible description — the helper text under it. */\n description?: string;\n /** The host's own instruction for this field (`<Assist instructions>`). */\n instructions?: string;\n /** What the rest of the form says (`<AssistProvider context>`). */\n context?: string;\n}\n\nconst brief = (field: FieldBrief) =>\n [\n `Field: ${field.name || \"(unnamed)\"}`,\n field.description && `Field description: ${field.description}`,\n field.instructions && `Instructions: ${field.instructions}`,\n field.context && `Form context:\\n${field.context}`,\n ]\n .filter(Boolean)\n .join(\"\\n\");\n\nconst CONTINUE = `You complete text inside a form field, like an editor's inline completion.\nReply with ONLY the text to insert at <caret/>: no quotes, no commentary, never repeat text that is already there.\nKeep the language, tone and format of the existing text. Stop at a natural point: the end of the sentence for an automatic suggestion; up to a short paragraph when explicitly asked.`;\n\n/**\n * The AI SDK reports a failed stream to `onError` and then ends the stream as if it had finished, so\n * a refused or broken call would read as a model with nothing to say. This keeps what was reported\n * and throws it, whole, once the stream ends.\n */\nfunction failures() {\n let failed: { error: unknown } | undefined;\n return {\n onError: ({ error }: { error: unknown }) => void (failed ??= { error }),\n rethrow: () => {\n if (failed) throw failed.error;\n },\n };\n}\n\n/** One continuation at the caret, streamed. */\nexport async function* continuation(\n model: LanguageModel,\n field: FieldBrief,\n { value, position, trigger, avoid, signal }: ContinuationRequest,\n): AsyncIterable<string> {\n const text = `${value.slice(0, position)}<caret/>${value.slice(position)}`;\n const differ = avoid.length\n ? `\\nOffer something different from these earlier suggestions:\\n${avoid.map((a) => `- ${a.trim()}`).join(\"\\n\")}`\n : \"\";\n const reported = failures();\n yield* streamText({\n model,\n system: CONTINUE,\n prompt: `${brief(field)}\\nRequest: ${trigger}${differ}\\n\\nText:\\n${text}`,\n abortSignal: signal,\n onError: reported.onError,\n }).textStream;\n reported.rethrow();\n}\n\nconst CANDIDATE = jsonSchema<{ value: string; rationale: string }>({\n type: \"object\",\n properties: {\n value: { type: \"string\", description: \"The whole value, exactly as it would be entered.\" },\n rationale: { type: \"string\", description: \"Why it fits, in one short sentence.\" },\n },\n required: [\"value\", \"rationale\"],\n additionalProperties: false,\n});\n\nconst CANDIDATES = `You suggest values for a form field. Each value must be complete and ready to enter as-is, in the language of the form.`;\n\n/**\n * Whole values for the field, each arriving as soon as it is complete (`elementStream`), so the\n * first candidate is on screen while the model writes the rest.\n */\nexport async function* candidates(\n model: LanguageModel,\n field: FieldBrief,\n { value, existing, signal, count, list }: {\n value: string;\n existing: string[];\n signal: AbortSignal;\n count: number;\n /** The field holds a list (tags): a candidate is one more item, not a replacement. */\n list: boolean;\n },\n): AsyncIterable<Proposal> {\n const holds = list\n ? `Current items: ${existing.length ? existing.join(\", \") : \"(none)\"}\\nSuggest NEW items, not already present.`\n : `Current value: ${value || \"(empty)\"}\\nSuggest alternatives to replace it${value ? \", different from it\" : \"\"}.`;\n const reported = failures();\n const result = streamText({\n model,\n onError: reported.onError,\n system: CANDIDATES,\n prompt: `${brief(field)}\\n${holds}\\nGive ${count}.`,\n output: Output.array({ element: CANDIDATE }),\n abortSignal: signal,\n });\n for await (const item of result.elementStream) {\n yield list\n ? { text: item.value, rationale: item.rationale }\n : { text: item.value, rationale: item.rationale, range: [0, value.length] };\n }\n reported.rethrow();\n}\n"],"names":["brief","field","CONTINUE","failures","failed","error","continuation","model","value","position","trigger","avoid","signal","text","differ","a","reported","streamText","CANDIDATE","jsonSchema","CANDIDATES","candidates","existing","count","list","holds","result","Output","item"],"mappings":";AAmBA,MAAMA,IAAQ,CAACC,MACb;AAAA,EACE,UAAUA,EAAM,QAAQ,WAAW;AAAA,EACnCA,EAAM,eAAe,sBAAsBA,EAAM,WAAW;AAAA,EAC5DA,EAAM,gBAAgB,iBAAiBA,EAAM,YAAY;AAAA,EACzDA,EAAM,WAAW;AAAA,EAAkBA,EAAM,OAAO;AAClD,EACG,OAAO,OAAO,EACd,KAAK;AAAA,CAAI,GAERC,IAAW;AAAA;AAAA;AASjB,SAASC,IAAW;AAClB,MAAIC;AACJ,SAAO;AAAA,IACL,SAAS,CAAC,EAAE,OAAAC,EAAA,MAAgC,MAAMD,UAAW,EAAE,OAAAC;IAC/D,SAAS,MAAM;AACb,UAAID,SAAcA,EAAO;AAAA,IAC3B;AAAA,EAAA;AAEJ;AAGA,gBAAuBE,EACrBC,GACAN,GACA,EAAE,OAAAO,GAAO,UAAAC,GAAU,SAAAC,GAAS,OAAAC,GAAO,QAAAC,KACZ;AACvB,QAAMC,IAAO,GAAGL,EAAM,MAAM,GAAGC,CAAQ,CAAC,WAAWD,EAAM,MAAMC,CAAQ,CAAC,IAClEK,IAASH,EAAM,SACjB;AAAA;AAAA,EAAgEA,EAAM,IAAI,CAACI,MAAM,KAAKA,EAAE,KAAA,CAAM,EAAE,EAAE,KAAK;AAAA,CAAI,CAAC,KAC5G,IACEC,IAAWb,EAAA;AACjB,SAAOc,EAAW;AAAA,IAChB,OAAAV;AAAA,IACA,QAAQL;AAAA,IACR,QAAQ,GAAGF,EAAMC,CAAK,CAAC;AAAA,WAAcS,CAAO,GAAGI,CAAM;AAAA;AAAA;AAAA,EAAcD,CAAI;AAAA,IACvE,aAAaD;AAAA,IACb,SAASI,EAAS;AAAA,EAAA,CACnB,EAAE,YACHA,EAAS,QAAA;AACX;AAEA,MAAME,IAAYC,EAAiD;AAAA,EACjE,MAAM;AAAA,EACN,YAAY;AAAA,IACV,OAAO,EAAE,MAAM,UAAU,aAAa,mDAAA;AAAA,IACtC,WAAW,EAAE,MAAM,UAAU,aAAa,sCAAA;AAAA,EAAsC;AAAA,EAElF,UAAU,CAAC,SAAS,WAAW;AAAA,EAC/B,sBAAsB;AACxB,CAAC,GAEKC,IAAa;AAMnB,gBAAuBC,EACrBd,GACAN,GACA,EAAE,OAAAO,GAAO,UAAAc,GAAU,QAAAV,GAAQ,OAAAW,GAAO,MAAAC,KAQT;AACzB,QAAMC,IAAQD,IACV,kBAAkBF,EAAS,SAASA,EAAS,KAAK,IAAI,IAAI,QAAQ;AAAA,2CAClE,kBAAkBd,KAAS,SAAS;AAAA,oCAAuCA,IAAQ,wBAAwB,EAAE,KAC3GQ,IAAWb,EAAA,GACXuB,IAAST,EAAW;AAAA,IACxB,OAAAV;AAAA,IACA,SAASS,EAAS;AAAA,IAClB,QAAQI;AAAA,IACR,QAAQ,GAAGpB,EAAMC,CAAK,CAAC;AAAA,EAAKwB,CAAK;AAAA,OAAUF,CAAK;AAAA,IAChD,QAAQI,EAAO,MAAM,EAAE,SAAST,GAAW;AAAA,IAC3C,aAAaN;AAAA,EAAA,CACd;AACD,mBAAiBgB,KAAQF,EAAO;AAC9B,UAAMF,IACF,EAAE,MAAMI,EAAK,OAAO,WAAWA,EAAK,cACpC,EAAE,MAAMA,EAAK,OAAO,WAAWA,EAAK,WAAW,OAAO,CAAC,GAAGpB,EAAM,MAAM,EAAA;AAE5E,EAAAQ,EAAS,QAAA;AACX;"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kanzo-tech/ai",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"description": "Kanzo AI surfaces — a field the model helps fill (Assist) and a conversation with one (Chat), over the AI SDK through @kanzo-tech/llm. Sibling of @kanzo-tech/ui, not part of it.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -30,12 +30,12 @@
|
|
|
30
30
|
"lucide-react": ">=1",
|
|
31
31
|
"react": ">=19",
|
|
32
32
|
"react-dom": ">=19",
|
|
33
|
-
"@kanzo-tech/ui": "0.
|
|
33
|
+
"@kanzo-tech/ui": "0.20.0"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"streamdown": "^2.5.0",
|
|
37
37
|
"tailwind-variants": "^3.2.2",
|
|
38
|
-
"@kanzo-tech/llm": "0.
|
|
38
|
+
"@kanzo-tech/llm": "0.20.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@ai-sdk/react": "^4.0.129",
|
|
@@ -51,8 +51,8 @@
|
|
|
51
51
|
"react": "^19.0.0",
|
|
52
52
|
"react-dom": "^19.0.0",
|
|
53
53
|
"rollup-plugin-preserve-directives": "^0.4.0",
|
|
54
|
-
"@kanzo-tech/theme": "0.
|
|
55
|
-
"@kanzo-tech/ui": "0.
|
|
54
|
+
"@kanzo-tech/theme": "0.20.0",
|
|
55
|
+
"@kanzo-tech/ui": "0.20.0"
|
|
56
56
|
},
|
|
57
57
|
"//size-limit": "The root barrel, measured without the peers and dependencies a host already has. Assist and Chat tree-shake apart: a host that only assists fields does not ship the transcript, and the markdown parser comes with Chat alone.",
|
|
58
58
|
"size-limit": [
|