@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 CHANGED
@@ -87,6 +87,13 @@ const ac = new AIAutocomplete(container, {
87
87
  // Focus
88
88
  autoFocus: true, // focus the input on mount (Tier 1 only)
89
89
 
90
+ // Product strip (opt-in). Omit it and nothing changes: no request, no markup.
91
+ products: {
92
+ fetch: (query, signal) => myPlatform.search(query, { signal }),
93
+ transform: (raw) => mapToProducts(raw),
94
+ limit: 8,
95
+ },
96
+
90
97
  // Custom submit button (Tier 1 only)
91
98
  // - undefined (default): renders the built-in arrow button
92
99
  // - null: no submit button at all
@@ -100,6 +107,7 @@ const ac = new AIAutocomplete(container, {
100
107
  onParamsChange: (params) => { ... },
101
108
  onFocus: () => { ... },
102
109
  onBlur: () => { ... },
110
+ onProductSelect: (product) => { ... }, // see "Product strip"
103
111
 
104
112
  // Controlled mode
105
113
  value: "initial text",
@@ -116,6 +124,7 @@ ac.reset(); // Clear everything, re-fetch, and start a new se
116
124
  ac.destroy(); // Remove DOM, listeners, timers
117
125
  ac.setMode("dark"); // Switch color mode
118
126
  ac.update({ animations: false, optionsPosition: "above" });
127
+ ac.selectProduct(product); // Emit onProductSelect (Tier 3 / custom strips)
119
128
  ```
120
129
 
121
130
  > **Tier 1 auto-resets** after both Enter key and built-in submit-button clicks — your `onSubmit` runs, then the SDK clears the input and starts a new session. You don't need to call `ac.reset()` yourself in Tier 1.
@@ -128,6 +137,109 @@ ac.update({ animations: false, optionsPosition: "above" });
128
137
 
129
138
  > **Caret placement after a completion** — whenever a completed param is added (by any means — option click, exact-match typing, or re-edit), the caret lands right after the trailing space following the bold so typing can continue immediately. A space is inserted if one wasn't already there.
130
139
 
140
+ ### Product strip
141
+
142
+ Opt in with `products` and the dropdown renders a horizontal row of product
143
+ cards below the options grid. Every platform (Shopify today, others later) has
144
+ its own search endpoint and its own response shape, so the SDK owns the UI and
145
+ the integration owns only the fetching and the mapping.
146
+
147
+ ```ts
148
+ import { AIAutocomplete, type Product } from "@magicx-eng/ai-autocomplete-vanilla";
149
+
150
+ new AIAutocomplete(container, {
151
+ apiConfig: { apiKey: "..." },
152
+
153
+ products: {
154
+ // You own the request entirely — auth headers, GraphQL body, locale
155
+ // prefixes, whatever the platform needs. Honour the signal: the SDK aborts
156
+ // it as soon as a newer query supersedes this one.
157
+ fetch: (query, signal) =>
158
+ fetch("/api/2024-10/graphql.json", {
159
+ method: "POST",
160
+ headers: { "X-Shopify-Storefront-Access-Token": token },
161
+ body: JSON.stringify({ query: SEARCH_QUERY, variables: { q: query } }),
162
+ signal,
163
+ }).then((r) => r.json()),
164
+
165
+ // Pure mapping, kept out of `fetch` so you can unit-test it without a
166
+ // network. Fold it into `fetch` and return the array if you prefer.
167
+ transform: (raw) =>
168
+ raw.data.products.nodes.map((node) => ({
169
+ id: node.id,
170
+ title: node.title,
171
+ url: node.onlineStoreUrl,
172
+ imageUrl: node.featuredImage?.url ?? null,
173
+ price: formatMoney(node.priceRange.minVariantPrice), // you know the currency
174
+ vendor: node.vendor,
175
+ })),
176
+
177
+ limit: 8, // applied by the SDK, after transform
178
+ },
179
+
180
+ // Selection emits — the SDK never navigates.
181
+ onProductSelect: (product) => {
182
+ window.location.assign(product.url); // …or add to cart, or fill the input
183
+ },
184
+ });
185
+ ```
186
+
187
+ `Product` is exactly six fields; only `id`, `title` and `url` are required:
188
+
189
+ ```ts
190
+ type Product = {
191
+ id: string;
192
+ title: string;
193
+ url: string;
194
+ imageUrl?: string | null; // null renders the placeholder tile
195
+ price?: string; // pre-formatted by you
196
+ vendor?: string;
197
+ };
198
+ ```
199
+
200
+ **What the SDK guarantees**
201
+
202
+ - **One fetch cadence.** The product search rides the same debounce, the same
203
+ `AbortController` and the same version guard as `/suggest` — there is no
204
+ second timer to drift out of step. A query that never triggers a suggestions
205
+ request (e.g. one still narrowing the current option list) doesn't trigger a
206
+ product search either.
207
+ - **Empty queries never reach you.** On mount and after `reset()` the strip
208
+ simply clears.
209
+ - **Out-of-order responses are dropped.** A slow response for an older query is
210
+ never rendered over a newer one.
211
+ - **Failure is silent.** A rejected `fetch` or a throwing `transform` clears the
212
+ strip, logs once, and leaves the suggestions half untouched — no `error`
213
+ state, no `onError`, no effect on `isLoading`.
214
+ - **The two halves are independent.** The panel opens if *either* has content,
215
+ so products keep it open when option filtering empties the list, and
216
+ suggestions render normally when there are no products. `dropdownTrigger`
217
+ (`manual` / `hidden`) still gates the panel as before — products don't
218
+ overrule an explicit opt-out of an auto-opening panel.
219
+
220
+ **Selection and links.** Cards are real `<a href>` elements, so cmd/ctrl-click,
221
+ middle-click and "copy link address" behave natively. A plain left click is
222
+ intercepted (`preventDefault`) and emits `onProductSelect` instead — the SDK
223
+ never navigates on your behalf.
224
+
225
+ `product.url` is used verbatim — the SDK trusts your `transform` output and
226
+ does not sanitise it, so validate the URL there if the platform response isn't
227
+ fully under your control. (Angular additionally runs its own `[href]`
228
+ sanitiser, so unrecognised schemes like `myapp://…` are rewritten to
229
+ `unsafe:…` in that package only.)
230
+
231
+ **Accessibility.** The strip is a `role="group"` labelled "Products"; each card
232
+ is a `role="option"` in the tab order, activated with Enter or Space, with a
233
+ visible focus ring. Tabbing into the strip does not close the panel. The row
234
+ scrolls horizontally by trackpad, wheel, touch and keyboard; the page never
235
+ scrolls sideways and the scrollbar chrome is hidden in all engines.
236
+
237
+ `update({ products })` swaps the integration (and clears the current cards);
238
+ `update({ products: undefined })` turns the strip off. Either way the strip
239
+ repopulates on the *next* `/suggest` request rather than immediately — the
240
+ product search rides that one scheduler, and changing the config doesn't itself
241
+ change the query.
242
+
131
243
  #### Custom submit button
132
244
 
133
245
  ```ts
@@ -176,6 +288,8 @@ input.addEventListener("blur", () => ac.setFocused(false));
176
288
 
177
289
  Pills always render inside the dropdown in this mode. The library creates the dropdown DOM inside your container — you just provide the element and wire up your input's events.
178
290
 
291
+ The [product strip](#product-strip) works here too: pass `products` / `onProductSelect` exactly as in Tier 1 and the strip renders inside the dropdown the library owns.
292
+
179
293
  > Focus/blur wiring is required when `dropdownTrigger` is `"auto"` (the default) — the dropdown only opens while the input is focused. Without `setFocused()`, the dropdown will never open.
180
294
 
181
295
  ---
@@ -244,6 +358,7 @@ unsub();
244
358
  | `isLoading` | `boolean` | Fetch in progress. The Tier 1 / Tier 2 renderers gate the loading skeleton additionally on `!inSelectionAnimation` and `!editingParam`. |
245
359
  | `inSelectionAnimation` | `boolean` | True for the 500 ms after a user-initiated option tap so the streak animation can finish before the dropdown switches to the loading skeleton. |
246
360
  | `editingParam` | `CompletedParamState \| null` | When non-null, the user is re-editing a bold completed param; cached options remain visible and the loading skeleton is suppressed. |
361
+ | `products` | `Product[]` | Results of the latest product search (empty unless `products` is configured). See [Product strip](#product-strip) — Tier 3 consumers render their own cards and call `selectProduct(product)` to emit `onProductSelect`. |
247
362
  | `isReady` | `boolean` | Server indicates query is complete |
248
363
  | `error` | `Error \| null` | Last fetch error |
249
364
 
@@ -323,7 +438,19 @@ The object passed to `onSubmit`:
323
438
  |---|---|---|
324
439
  | `query` | `string` | Plain text as the user sees it. |
325
440
  | `raw_query` | `string` | Text with placeholder tokens (e.g. `"Create a {{TASK_1}}"`). |
326
- | `completed_params` | `CompletedParam[]` | Array of filled parameter values. |
441
+ | `completed_params` | `CompletedParam[]` | Array of filled parameter values, followed by any the user skipped (see below). |
442
+
443
+ #### Skipped parameters
444
+
445
+ Pressing <kbd>→</kbd> at the end of the input dismisses the active pill. The dismissal is reported to the server — and included in `completed_params` here — as an entry with no placeholder and the sentinel text `"skipped"`:
446
+
447
+ ```ts
448
+ { placeholder: "", type: "goal", text: "skipped", kind: null }
449
+ ```
450
+
451
+ Skipped entries are appended after the filled params (they have no position in the query) and are deduped by type. A skip is dropped if a param of the same type ends up filled anyway. Skipping the last available pill triggers an immediate request so the server can suggest something else; earlier skips ride along on the next request. `reset()` clears them.
452
+
453
+ > **Reading `state.skippedParams` directly:** the array is append-only until `reset()`. The "drop a skip whose type got filled" rule is applied when the payload is built, not by pruning the array — so if the user skips `goal` and later fills one, the raw array still holds the `goal` entry. That's deliberate: the filter self-heals if they then delete that param's text, where pruning would discard the signal for good. Apply the same rule yourself with the exported `withSkippedParams(completedParams, skippedParams)`.
327
454
 
328
455
  ### Event Subscription
329
456
 
@@ -379,6 +506,21 @@ Override these on the container element. All built-in defaults use `:where()` (z
379
506
  | `--aia-scrollbar-thumb` | `rgba(0, 0, 0, 0.3)` | `rgba(0, 0, 0, 0.3)` | Color of the option list's scrollbar thumb (Firefox + WebKit). |
380
507
  | `--aia-streak-rgb` | `99, 102, 241` | `255, 255, 255` | Comma-separated RGB triplet used to tint the option-selection streak animation (consumed via `rgba(var(--aia-streak-rgb), …)`). |
381
508
  | `--aia-streak-glass-bg` | `rgba(99, 102, 241, 0.1)` | `rgba(255, 255, 255, 0.1)` | Background fill for the streak's glass-pill effect. |
509
+ | `--aia-product-card-width` | `116px` | `116px` | Width of a product card in the strip. The media tile is square, so this also sets its height. |
510
+ | `--aia-product-gap` | `8px` | `8px` | Gap between product cards. |
511
+ | `--aia-product-bg` | `transparent` | `transparent` | Product card background. |
512
+ | `--aia-product-bg-active` | `--aia-option-bg` | `--aia-option-bg` | Product card background on hover. |
513
+ | `--aia-product-media-bg` | `--aia-skeleton-bg` | `--aia-skeleton-bg` | Fill behind the product image, and of the placeholder tile when a product has no image. |
514
+ | `--aia-product-placeholder-color` | `--aia-option-color` | `--aia-option-color` | Glyph color of the no-image placeholder tile. |
515
+ | `--aia-product-title-color` | `--aia-option-color-selected` | `--aia-option-color-selected` | Product title text. Follows the option colors by default, so theming the panel moves suggestions and products together. |
516
+ | `--aia-product-price-color` | `--aia-option-color-selected` | `--aia-option-color-selected` | Product price text. |
517
+ | `--aia-product-vendor-color` | `--aia-option-color` | `--aia-option-color` | Product vendor line. |
518
+ | `--aia-product-focus-ring` | `--aia-option-color-selected` | `--aia-option-color-selected` | Focus ring drawn on a keyboard-focused card. |
519
+ | `--aia-products-label-color` | `--aia-option-color` | `--aia-option-color` | "Products" section label. |
520
+ | `--aia-product-title-font-size` | `12px` | `12px` | Product title font size. |
521
+ | `--aia-product-price-font-size` | `11px` | `11px` | Product price font size. |
522
+ | `--aia-product-vendor-font-size` | `10px` | `10px` | Product vendor font size. |
523
+ | `--aia-products-label-font-size` | `11px` | `11px` | Section label font size. |
382
524
  | `--aia-skeleton-bg` | `rgba(189, 189, 189, 0.25)` | `#1a1b1d` | Fill color for the loading skeleton bars and the masked text in cached pills/options. |
383
525
 
384
526
  ### Per-mode Overrides
@@ -412,7 +554,11 @@ For styling beyond the CSS variables, target these stable `data-aia-*` attribute
412
554
  | `[data-aia-pill]` | Each unfilled-suggestion pill |
413
555
  | `[data-aia-pillbar]` | Pill bar container inside the dropdown |
414
556
  | `[data-aia-option]` | Each suggestion option |
415
- | `[data-aia-dropdown]` | The dropdown root (listbox) |
557
+ | `[data-aia-dropdown]` | The dropdown root (listbox). Carries `data-aia-has-products` while the product strip has cards. |
558
+ | `[data-aia-products]` | Product strip section (label + row) |
559
+ | `[data-aia-products-row]` | The horizontally scrolling row of cards |
560
+ | `[data-aia-product]` | Each product card |
561
+ | `[data-aia-product-placeholder]` | Media tile of a card whose product has no image |
416
562
 
417
563
  Completed params render as inline `<strong>` elements inside the editor. Override their weight with `[data-aia-input] strong { font-weight: 700; }` (the built-in style uses `:where()` so any consumer selector wins without `!important`).
418
564
 
package/dist/index.d.mts 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
- 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 RecentlySuggested, type RenderMode, type Segment, type Store, type Suggestion, type SuggestionOption, type TaskKind, buildAttributionUrl, buildQuery, createStore, cursorIsAtEnd, extractPlainText, getCursorOffset, getFooterHint, plainTextLength, previousGraphemeBoundary, renderEditableContent, setCursorOffset };
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 };