@magicx-eng/ai-autocomplete-vanilla 0.6.0 → 0.8.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/README.md +148 -2
- package/dist/index.d.mts +161 -2
- package/dist/index.d.ts +161 -2
- package/dist/index.js +219 -15
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +219 -15
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -78,6 +78,30 @@ interface CompletedParamState extends CompletedParam {
|
|
|
78
78
|
options: SuggestionOption[];
|
|
79
79
|
metadata?: Record<string, unknown>;
|
|
80
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* A suggestion the user dismissed with the skip key (→) instead of filling.
|
|
83
|
+
*
|
|
84
|
+
* Client-only bookkeeping. A skipped suggestion has no text in the input, so
|
|
85
|
+
* it can't live in `completedParams` — that array is reconciled against the
|
|
86
|
+
* input on every keystroke and anything missing from the text is dropped.
|
|
87
|
+
* Skipped entries are folded into the wire `completed_params` array (with
|
|
88
|
+
* `text: "skipped"`, no placeholder) only when a request or a submit result is
|
|
89
|
+
* built. See `withSkippedParams`.
|
|
90
|
+
*
|
|
91
|
+
* The array is append-only until `reset()` — a skip is filtered out at
|
|
92
|
+
* build time when a param of its type ended up filled, not pruned here.
|
|
93
|
+
* That's deliberate: the filter self-heals if the user later deletes that
|
|
94
|
+
* param's text (the skip reappears in the payload), where pruning would have
|
|
95
|
+
* discarded the signal permanently. Consumers reading this array directly
|
|
96
|
+
* should apply the same filter — `withSkippedParams` is exported for it.
|
|
97
|
+
*/
|
|
98
|
+
interface SkippedParamState {
|
|
99
|
+
id: string;
|
|
100
|
+
/** The skipped suggestion's `type` (e.g. "goal"). */
|
|
101
|
+
type: string;
|
|
102
|
+
/** The suggestion's display text at skip time. Introspection only — never sent. */
|
|
103
|
+
suggestionPlaceholder: string;
|
|
104
|
+
}
|
|
81
105
|
/**
|
|
82
106
|
* Client-side state for an LLM-identified param. Tentative — replaced
|
|
83
107
|
* wholesale from each response (latest wins) and dropped when its text no
|
|
@@ -123,6 +147,66 @@ interface AccessTokenResult {
|
|
|
123
147
|
}
|
|
124
148
|
type APIConfig = APIKeyConfig | AccessTokenConfig;
|
|
125
149
|
type OptionOverrides = Record<string, (query: string) => SuggestionOption[]>;
|
|
150
|
+
/**
|
|
151
|
+
* A single product card in the dropdown's product strip.
|
|
152
|
+
*
|
|
153
|
+
* Platform-agnostic on purpose: every integration (Shopify today, others
|
|
154
|
+
* later) maps its own search response onto exactly these fields, so the SDK
|
|
155
|
+
* renders one strip regardless of where the results came from. Only `id`,
|
|
156
|
+
* `title` and `url` are required — the card reflows when any of the rest is
|
|
157
|
+
* missing. Do NOT grow this shape casually; a new field is a new contract
|
|
158
|
+
* every integration has to satisfy.
|
|
159
|
+
*/
|
|
160
|
+
interface Product {
|
|
161
|
+
/** Stable identity — used as the render key, so it must be unique per result set. */
|
|
162
|
+
id: string;
|
|
163
|
+
title: string;
|
|
164
|
+
/** Destination for the card's `href`. Selection does not navigate; see `onProductSelect`. */
|
|
165
|
+
url: string;
|
|
166
|
+
/** Absent/null renders the placeholder tile — plenty of catalogues have no images. */
|
|
167
|
+
imageUrl?: string | null;
|
|
168
|
+
/** Pre-formatted by the integration, which is the side that knows the currency. */
|
|
169
|
+
price?: string;
|
|
170
|
+
vendor?: string;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Opt-in product-search wiring. Absent (the default) means the SDK never
|
|
174
|
+
* fetches and never renders a strip — behaviour is byte-for-byte what it was
|
|
175
|
+
* before products existed.
|
|
176
|
+
*/
|
|
177
|
+
interface ProductsConfig {
|
|
178
|
+
/**
|
|
179
|
+
* Runs the platform's product search. The integration owns the request
|
|
180
|
+
* entirely — auth headers, GraphQL body, locale prefixes, whatever the
|
|
181
|
+
* platform needs — and must honour `signal`: the SDK aborts it the moment a
|
|
182
|
+
* newer query supersedes this one.
|
|
183
|
+
*
|
|
184
|
+
* Called on the SDK's own fetch cadence (the same debounce that drives
|
|
185
|
+
* `/suggest`), never on a timer of its own, and never with an empty query.
|
|
186
|
+
*/
|
|
187
|
+
fetch: (query: string, signal: AbortSignal) => Promise<unknown>;
|
|
188
|
+
/**
|
|
189
|
+
* Maps the platform's raw payload onto `Product[]`. Kept separate from
|
|
190
|
+
* `fetch` so integrations can unit-test the mapping without a network; fold
|
|
191
|
+
* it into `fetch` and return the mapped array if you prefer.
|
|
192
|
+
*
|
|
193
|
+
* A throw here is treated exactly like a rejected fetch: the strip clears,
|
|
194
|
+
* the suggestions half is untouched.
|
|
195
|
+
*
|
|
196
|
+
* The output is trusted: `Product.url` goes straight onto the card's `href`
|
|
197
|
+
* with no sanitisation, and modifier / middle clicks are deliberately left
|
|
198
|
+
* to the browser. If the platform response is not fully under your control,
|
|
199
|
+
* validate the URL inside `transform`.
|
|
200
|
+
*
|
|
201
|
+
* One cross-framework caveat: Angular's `[href]` binding runs its own URL
|
|
202
|
+
* sanitiser, so a scheme it doesn't recognise (`myapp://…`) is rewritten to
|
|
203
|
+
* `unsafe:myapp://…` there while vanilla and React emit it verbatim. Stick
|
|
204
|
+
* to http/https if the same integration has to serve all three packages.
|
|
205
|
+
*/
|
|
206
|
+
transform: (raw: unknown) => Product[];
|
|
207
|
+
/** Optional cap the SDK applies after `transform`. */
|
|
208
|
+
limit?: number;
|
|
209
|
+
}
|
|
126
210
|
interface AutocompleteResult {
|
|
127
211
|
query: string;
|
|
128
212
|
raw_query: string;
|
|
@@ -142,6 +226,15 @@ interface CoreInputState {
|
|
|
142
226
|
* override or overlap completed params.
|
|
143
227
|
*/
|
|
144
228
|
identifiedParams: IdentifiedParamState[];
|
|
229
|
+
/**
|
|
230
|
+
* Suggestions the user dismissed with the skip key (→). Held apart from
|
|
231
|
+
* `completedParams` because they have no text in the input; folded into the
|
|
232
|
+
* wire `completed_params` array (as `text: "skipped"`) on every request and
|
|
233
|
+
* on the submit result. Append-only until `reset()` — see
|
|
234
|
+
* {@link SkippedParamState} for why skips of a since-filled type are
|
|
235
|
+
* filtered at build time rather than pruned here.
|
|
236
|
+
*/
|
|
237
|
+
skippedParams: SkippedParamState[];
|
|
145
238
|
/**
|
|
146
239
|
* Open while the user has unresolved trailing text: anchored at the covered
|
|
147
240
|
* offset where they started typing, snapshotting the actionable suggestions
|
|
@@ -155,6 +248,13 @@ interface CoreInputState {
|
|
|
155
248
|
snapshot: Suggestion[];
|
|
156
249
|
} | null;
|
|
157
250
|
suggestions: Suggestion[];
|
|
251
|
+
/**
|
|
252
|
+
* Results of the latest product search, already transformed and capped by
|
|
253
|
+
* `opts.products.limit`. Always empty when `opts.products` is unconfigured.
|
|
254
|
+
* Cleared on an empty query and on a failed fetch/transform, so the strip
|
|
255
|
+
* never shows results belonging to an older query.
|
|
256
|
+
*/
|
|
257
|
+
products: Product[];
|
|
158
258
|
activeDropdownIndex: number;
|
|
159
259
|
newParamId: string | null;
|
|
160
260
|
isLoading: boolean;
|
|
@@ -256,6 +356,13 @@ interface CoreOptions {
|
|
|
256
356
|
* arrow button. Clicks on the provided element bubble up and trigger submit.
|
|
257
357
|
*/
|
|
258
358
|
submitButton?: HTMLElement | null;
|
|
359
|
+
/**
|
|
360
|
+
* Opt-in product strip. When set, the dropdown renders a horizontal row of
|
|
361
|
+
* product cards below the options grid, fed by the integration's own search
|
|
362
|
+
* endpoint on the SDK's existing fetch cadence. Omit it and nothing about
|
|
363
|
+
* the dropdown changes — no request, no markup, no layout shift.
|
|
364
|
+
*/
|
|
365
|
+
products?: ProductsConfig;
|
|
259
366
|
onSubmit?: (result: AutocompleteResult) => void;
|
|
260
367
|
onError?: (error: Error) => void;
|
|
261
368
|
onChange?: (text: string) => void;
|
|
@@ -265,6 +372,13 @@ interface CoreOptions {
|
|
|
265
372
|
onFocus?: () => void;
|
|
266
373
|
/** Called when the input loses focus (or `setFocused(false)` is called). */
|
|
267
374
|
onBlur?: () => void;
|
|
375
|
+
/**
|
|
376
|
+
* Called when the user activates a product card. The SDK does NOT navigate —
|
|
377
|
+
* the integration decides what a selection means (open the PDP, add to cart,
|
|
378
|
+
* fill the input, …). Modifier / middle clicks are left to the browser so
|
|
379
|
+
* cmd-click and "copy link address" keep working, and don't fire this.
|
|
380
|
+
*/
|
|
381
|
+
onProductSelect?: (product: Product) => void;
|
|
268
382
|
value?: string;
|
|
269
383
|
completedParams?: CompletedParamState[];
|
|
270
384
|
/**
|
|
@@ -285,8 +399,9 @@ type AIAutocompleteEvents = {
|
|
|
285
399
|
stateChange: [state: CoreState];
|
|
286
400
|
focus: [];
|
|
287
401
|
blur: [];
|
|
402
|
+
productSelect: [product: Product];
|
|
288
403
|
};
|
|
289
|
-
type CoreUpdateOptions = Partial<Omit<CoreOptions, "onSubmit" | "onError" | "onChange" | "onParamsChange" | "onStateChange" | "onFocus" | "onBlur">>;
|
|
404
|
+
type CoreUpdateOptions = Partial<Omit<CoreOptions, "onSubmit" | "onError" | "onChange" | "onParamsChange" | "onStateChange" | "onFocus" | "onBlur" | "onProductSelect">>;
|
|
290
405
|
declare class AIAutocomplete {
|
|
291
406
|
private inputStore;
|
|
292
407
|
private store;
|
|
@@ -295,6 +410,7 @@ declare class AIAutocomplete {
|
|
|
295
410
|
private fetchController;
|
|
296
411
|
private keyboardController;
|
|
297
412
|
private pillsController;
|
|
413
|
+
private productsController;
|
|
298
414
|
private reEdit;
|
|
299
415
|
private modeController;
|
|
300
416
|
private container;
|
|
@@ -345,6 +461,15 @@ declare class AIAutocomplete {
|
|
|
345
461
|
handleCaretAfterInput(offset: number | null): void;
|
|
346
462
|
handleCaretMove(offset: number | null): void;
|
|
347
463
|
setActiveDropdownIndex(index: number): void;
|
|
464
|
+
/**
|
|
465
|
+
* Announce a product selection. The rendered cards call this on activation;
|
|
466
|
+
* headless consumers rendering their own strip call it themselves.
|
|
467
|
+
*
|
|
468
|
+
* Emitting is the entire behaviour — the SDK deliberately does not navigate
|
|
469
|
+
* to `product.url`, because only the integration knows whether a selection
|
|
470
|
+
* means "open the PDP", "add to cart" or "drop the title into the input".
|
|
471
|
+
*/
|
|
472
|
+
selectProduct(product: Product): void;
|
|
348
473
|
handleTextChange(value: string): void;
|
|
349
474
|
handleKeyDown(e: KeyboardEvent): void;
|
|
350
475
|
setFocused(focused: boolean): void;
|
|
@@ -393,6 +518,8 @@ declare class AIAutocomplete {
|
|
|
393
518
|
* subscription early-returns on subsequent fires.
|
|
394
519
|
*/
|
|
395
520
|
private maybeExitReEditOnNoMatch;
|
|
521
|
+
/** Fire an immediate (undebounced) fetch for the current text + params. */
|
|
522
|
+
private fetchNow;
|
|
396
523
|
/**
|
|
397
524
|
* When the user has typed text that exactly matches (case-insensitive) one
|
|
398
525
|
* of the active suggestion's options, promote it to a completed param right
|
|
@@ -536,4 +663,36 @@ declare class ModeController {
|
|
|
536
663
|
private detachListener;
|
|
537
664
|
}
|
|
538
665
|
|
|
539
|
-
|
|
666
|
+
/**
|
|
667
|
+
* Sentinel `text` marking a `completed_params` entry the user skipped (→)
|
|
668
|
+
* rather than filled. Sent regardless of `maskCompletedText` — it's a fixed
|
|
669
|
+
* marker, never user-entered content.
|
|
670
|
+
*/
|
|
671
|
+
declare const SKIPPED_PARAM_TEXT = "skipped";
|
|
672
|
+
/**
|
|
673
|
+
* Folds skipped suggestions into a wire `completed_params` array so the server
|
|
674
|
+
* learns which parameters the user dismissed and can stop re-suggesting them.
|
|
675
|
+
*
|
|
676
|
+
* Skipped entries carry no placeholder: nothing was substituted into
|
|
677
|
+
* `raw_query`, so a `{{TYPE_N}}` token would point at text that doesn't exist.
|
|
678
|
+
* They're appended after the real params for the same reason — they have no
|
|
679
|
+
* position in the query.
|
|
680
|
+
*
|
|
681
|
+
* A skip is dropped when a param of the same type ends up filled anyway (the
|
|
682
|
+
* user skipped `goal`, then typed one): sending both would tell the server the
|
|
683
|
+
* parameter is simultaneously answered and declined.
|
|
684
|
+
*/
|
|
685
|
+
declare function withSkippedParams(completed: CompletedParam[], skipped: SkippedParamState[]): CompletedParam[];
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* Builds the `AutocompleteResult` handed to `onSubmit`: the placeholder-
|
|
689
|
+
* tokenized raw query plus the completed params, with skipped suggestions
|
|
690
|
+
* folded in (see {@link withSkippedParams}).
|
|
691
|
+
*
|
|
692
|
+
* Shared by every submit path — vanilla Enter / submit button, the React Tier 1
|
|
693
|
+
* component, the Angular Tier 1 component — so they can't drift on what a
|
|
694
|
+
* result contains.
|
|
695
|
+
*/
|
|
696
|
+
declare function buildSubmitResult(text: string, completedParams: CompletedParamState[], skippedParams?: SkippedParamState[]): AutocompleteResult;
|
|
697
|
+
|
|
698
|
+
export { AIAutocomplete, type APIConfig, type APIKeyConfig, ATTRIBUTION_URL, type AccessTokenConfig, type AccessTokenResult, type AppearanceMode, type AutocompleteRequest, type AutocompleteResponse, type AutocompleteResult, type CompletedParam, type CompletedParamState, type CoreOptions, type CoreState, type IdentifiedParam, type IdentifiedParamState, type InputItem, ModeController, type OptionOverrides, type Product, type ProductsConfig, type RecentlySuggested, type RenderMode, SKIPPED_PARAM_TEXT, type Segment, type SkippedParamState, type Store, type Suggestion, type SuggestionOption, type TaskKind, buildAttributionUrl, buildQuery, buildSubmitResult, createStore, cursorIsAtEnd, extractPlainText, getCursorOffset, getFooterHint, plainTextLength, previousGraphemeBoundary, renderEditableContent, setCursorOffset, withSkippedParams };
|