@magicx-eng/ai-autocomplete-vanilla 0.36.1 → 0.38.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
@@ -14,7 +14,7 @@ A framework-agnostic vanilla JS/TypeScript library that provides a guided AI-pow
14
14
  - **Access token auth** — short-lived tokens with automatic refresh, single-flight deduplication, and 401 retry
15
15
  - **Keyboard navigation** — arrow keys, enter to submit, tab to autocomplete, backspace to un-bold the last completed param
16
16
  - **Client-side filtering** — instant substring filtering on every keystroke
17
- - **Datepicker** — date parameters are answered with a calendar instead of an option list. Click a day or navigate with the arrow keys; the date is committed the way it would be written — `Tuesday` for a date inside the next week, `March 23` for one later this year, `March 23 2027` for another year. Tapping a committed date re-opens the calendar on the month that text names now — for a weekday name, that is the next such day, not the one originally picked. A parameter whose options are written as spans (`September 11 - October 13`) gets the same calendar answered in two clicks, committed as one range.
17
+ - **Datepicker** — date parameters are answered with a calendar instead of an option list. Click a day or navigate with the arrow keys; the date is committed the way it would be written — `Tuesday` for a date inside the next week, `March 23` for one later this year, `March 23 2027` for another year. Tapping a committed date re-opens the calendar on the month that text names now — for a weekday name, that is the next such day, not the one originally picked. A date-range parameter gets the same calendar answered in two clicks, committed as one range.
18
18
  - **Option overrides** — supply the options for a parameter yourself: a fixed list, a computed one, or one fetched from your own search endpoint as the user types
19
19
  - **Product strip** — the dropdown renders a horizontal row of product cards below the options, filled by default with the items the `/suggest` response reports as matching the query; plug in your own product search instead with `products`, or turn the strip off with `showProducts: false`
20
20
  - **Catalog report** — every response says which of the query's constraints the catalog applied, which it had to drop, and how many items are left, on `customFields` / `custom_fields`
@@ -78,6 +78,7 @@ const ac = new AIAutocomplete(container, {
78
78
  optionOverrides: { location: async (query, signal) => [...] }, // see "Option Overrides"
79
79
  columns: 2,
80
80
  maskCompletedText: false, // when true, omits completed params' text from API requests (PII masking)
81
+ commitOnWordEnd: false, // true = a word still being typed never becomes a chip until a space, Enter, or Tab ends it (exact matches and server identified params). Picking an option is unchanged.
81
82
  additionalContext: { tier: "gold" }, // optional user context, to personalize suggestions and options
82
83
 
83
84
  // Appearance
@@ -431,11 +432,9 @@ new AIAutocomplete(container, { ..., submitButton: null });
431
432
  ### Datepicker
432
433
 
433
434
  When the next parameter is a date, the dropdown shows a calendar instead of a
434
- list of options. A parameter counts as a date when its name says so, or when at
435
- least three of the options it offers are themselves written as dates — so a
436
- parameter called `when` or `arrival` still gets a calendar. Options written as
437
- relative phrases (`today`, `next week`) are not read as dates; a parameter
438
- offering those needs a name that says date.
435
+ list of options. Which parameters are dates is the server's call: one whose
436
+ `type` is `date` gets a calendar, and one without a date type keeps its option
437
+ list, however date-like its name or options look.
439
438
 
440
439
  Nothing is required to switch it on — a date parameter renders this way
441
440
  automatically, and every other parameter is unaffected. Tier 1 and Tier 2 render
@@ -503,19 +502,15 @@ say which, and phrases like `next friday`. Those still open the calendar, just
503
502
  on the current month with nothing marked, so the user picks rather than being
504
503
  shown a guess that might be wrong.
505
504
 
506
- **Date ranges.** A parameter whose options are written as spans — `September 11
507
- - October 13`, `September 11 to November 16` — is answered by the same calendar
508
- in two clicks: the first sets the start, the second the end, and the pair
505
+ **Date ranges.** A parameter whose `type` is `date_range` is answered by the
506
+ same calendar in two clicks: the first sets the start, the second the end, and the pair
509
507
  commits as one answer. The days between them band as the user moves across the
510
508
  calendar, so the span being chosen is visible before it is committed. Paging the
511
509
  month between the two clicks is expected — that is how a span crosses months —
512
510
  and <kbd>Esc</kbd> drops the start and lets them begin again. Clicking the end
513
511
  before the start is fine; the two are ordered on the way in.
514
512
 
515
- The span is read off the options the same way a single date is read off its own:
516
- either separator (`-`, `–`, `—`, `to`, `through`, `until`), either half in any
517
- date format the SDK reads, and a name that says so (`date_range`, `dateRange`)
518
- counts too. It commits as one parameter, written the way it was offered —
513
+ It commits as one parameter, written as a span —
519
514
  `September 11 - October 13`, with the year on whichever end needs it — and both
520
515
  ends are also recorded on the completed parameter's `metadata` as `YYYY-MM-DD`,
521
516
  under the `DATE_RANGE_META_START` / `DATE_RANGE_META_END` keys this package
package/dist/index.d.mts CHANGED
@@ -57,8 +57,9 @@ interface SuggestionOption {
57
57
  parameter?: string;
58
58
  }
59
59
  /**
60
- * The value type of a suggestion's parameter — which picker it needs. Present
61
- * only for a date or date-range parameter.
60
+ * The value type of a parameter — which picker it needs. The server sends it
61
+ * for every date or date-range parameter; a parameter without one is answered
62
+ * from its options.
62
63
  */
63
64
  interface SuggestionType {
64
65
  kind: "string" | "numeric" | "date" | "date_range" | "boolean";
@@ -81,6 +82,11 @@ interface InputItem {
81
82
  state: "completed" | "in_progress";
82
83
  /** Absent or "selected" = echo of client params; "identified" = LLM-inferred. */
83
84
  source?: "selected" | "identified";
85
+ /**
86
+ * The value type of the parameter this span was identified as. A span whose
87
+ * type names a date or date range opens a calendar when tapped.
88
+ */
89
+ type?: SuggestionType;
84
90
  }
85
91
  /** Wire shape for an LLM-identified param echoed back to the server. */
86
92
  interface IdentifiedParam {
@@ -295,6 +301,8 @@ interface IdentifiedParamState {
295
301
  id: string;
296
302
  parameter: string;
297
303
  text: string;
304
+ /** The span's value type, from its `input` item. */
305
+ type?: SuggestionType;
298
306
  }
299
307
  type Segment = {
300
308
  type: "text";
@@ -592,30 +600,20 @@ type FormatType = "options" | "date" | "date-range";
592
600
  /** Whether this format is answered by a calendar rather than by a list. */
593
601
  declare function isCalendarFormat(format: FormatType): boolean;
594
602
  /**
595
- * Decide how a suggestion should be answered.
596
- *
597
- * The server cannot send a `formatType` yet, so a suggestion that holds a date
598
- * is detected here — by the options it offers, and failing that by its name —
599
- * and rendered as a calendar with those options ignored.
603
+ * Decide how a parameter should be answered — from the value type the server
604
+ * names on it, and nothing else.
600
605
  *
601
- * The detection is deliberately the LAST resort. A `formatType` that *is* on
602
- * the wire always wins, so the day the server starts sending one this function
603
- * needs a single block deleted — the datepicker itself, and every gate that
604
- * consults this, keep working untouched.
606
+ * The server sets `type` on every date and date-range parameter, so one
607
+ * without it is an option list, whatever its name, options or text look like.
608
+ * The SDK never guesses: guessing from options read a "Ring size" offering
609
+ * "4.75", "7.25" and "8.5" as three dates and replaced its sizes with a
610
+ * calendar.
605
611
  *
606
- * Options are read before the name because they are the stronger evidence:
607
- * they are the values themselves, while a name is what someone chose to call
608
- * them. A parameter called `travel_date` whose options are all spans is a span
609
- * parameter, whatever its name says.
610
- *
611
- * Accepts anything with a `parameter`, not a `Suggestion`, so re-edit can ask
612
- * the same question about a completed param via its `suggestionParameter`.
613
- * A suggestion whose `type` names a date or date-range parameter is that,
614
- * whatever its options and name say.
612
+ * Accepts anything with a `type`, not a `Suggestion`, so re-edit can ask the
613
+ * same question about a completed param via its `suggestionValueType`, and an
614
+ * identified span via its own `type`.
615
615
  */
616
616
  declare function resolveFormatType(source: {
617
- parameter: string;
618
- options?: SuggestionOption[];
619
617
  type?: SuggestionType;
620
618
  } | undefined | null): FormatType;
621
619
 
@@ -925,6 +923,8 @@ interface CoreOptions {
925
923
  apiConfig?: APIConfig;
926
924
  optionOverrides?: OptionOverrides;
927
925
  maskCompletedText?: boolean;
926
+ /** When true, a word the user is still typing never becomes a chip. The word becomes a chip when it ends: a space, Enter, or Tab. Default: false. */
927
+ commitOnWordEnd?: boolean;
928
928
  /**
929
929
  * Optional user context. Include whatever you know about the user — a
930
930
  * profile, preferences, workspace, anything — to personalize suggested
@@ -1136,6 +1136,10 @@ declare class AIAutocomplete {
1136
1136
  private funnelTelemetry;
1137
1137
  /** A `search_started` that happened before any response said whether to send it. */
1138
1138
  private pendingSearchStarted;
1139
+ /** The text at the last user keystroke, while its last word is still open. */
1140
+ private openWordText;
1141
+ /** Identified params held back until the word they end ends. */
1142
+ private heldIdentified;
1139
1143
  /** What the last `impression` showed, so an identical response doesn't resend it. */
1140
1144
  private lastImpressionKey;
1141
1145
  private readonly emitSubmit;
@@ -1231,8 +1235,8 @@ declare class AIAutocomplete {
1231
1235
  /**
1232
1236
  * Open the calendar for a span the server identified as a date.
1233
1237
  *
1234
- * Returns false for anything else — an identified span whose type isn't a
1235
- * date stays inert, exactly as every identified span did before.
1238
+ * Returns false for anything else — an identified span whose `type` the
1239
+ * server didn't name as a date or date range stays inert.
1236
1240
  *
1237
1241
  * The span's own text is read for a starting month (`parseLooseDate`), but
1238
1242
  * failing to read it is not a failure: the calendar opens either way, and
@@ -1382,6 +1386,14 @@ declare class AIAutocomplete {
1382
1386
  private subscribeSkeletonMirror;
1383
1387
  private subscribeNewParamTimer;
1384
1388
  private handleChange;
1389
+ /** True while commitOnWordEnd is on and the user still types the last word. */
1390
+ private isWordOpen;
1391
+ private admitIdentified;
1392
+ /**
1393
+ * Ends the word the user is typing. Makes the chip for an exact match and
1394
+ * admits the held identified params. Returns true when it changed anything.
1395
+ */
1396
+ private endTypedWord;
1385
1397
  /**
1386
1398
  * Opens the pending span on the first keystroke past covered text: there is
1387
1399
  * unresolved trailing text beyond the covered offset (filterBase / last pill
package/dist/index.d.ts CHANGED
@@ -57,8 +57,9 @@ interface SuggestionOption {
57
57
  parameter?: string;
58
58
  }
59
59
  /**
60
- * The value type of a suggestion's parameter — which picker it needs. Present
61
- * only for a date or date-range parameter.
60
+ * The value type of a parameter — which picker it needs. The server sends it
61
+ * for every date or date-range parameter; a parameter without one is answered
62
+ * from its options.
62
63
  */
63
64
  interface SuggestionType {
64
65
  kind: "string" | "numeric" | "date" | "date_range" | "boolean";
@@ -81,6 +82,11 @@ interface InputItem {
81
82
  state: "completed" | "in_progress";
82
83
  /** Absent or "selected" = echo of client params; "identified" = LLM-inferred. */
83
84
  source?: "selected" | "identified";
85
+ /**
86
+ * The value type of the parameter this span was identified as. A span whose
87
+ * type names a date or date range opens a calendar when tapped.
88
+ */
89
+ type?: SuggestionType;
84
90
  }
85
91
  /** Wire shape for an LLM-identified param echoed back to the server. */
86
92
  interface IdentifiedParam {
@@ -295,6 +301,8 @@ interface IdentifiedParamState {
295
301
  id: string;
296
302
  parameter: string;
297
303
  text: string;
304
+ /** The span's value type, from its `input` item. */
305
+ type?: SuggestionType;
298
306
  }
299
307
  type Segment = {
300
308
  type: "text";
@@ -592,30 +600,20 @@ type FormatType = "options" | "date" | "date-range";
592
600
  /** Whether this format is answered by a calendar rather than by a list. */
593
601
  declare function isCalendarFormat(format: FormatType): boolean;
594
602
  /**
595
- * Decide how a suggestion should be answered.
596
- *
597
- * The server cannot send a `formatType` yet, so a suggestion that holds a date
598
- * is detected here — by the options it offers, and failing that by its name —
599
- * and rendered as a calendar with those options ignored.
603
+ * Decide how a parameter should be answered — from the value type the server
604
+ * names on it, and nothing else.
600
605
  *
601
- * The detection is deliberately the LAST resort. A `formatType` that *is* on
602
- * the wire always wins, so the day the server starts sending one this function
603
- * needs a single block deleted — the datepicker itself, and every gate that
604
- * consults this, keep working untouched.
606
+ * The server sets `type` on every date and date-range parameter, so one
607
+ * without it is an option list, whatever its name, options or text look like.
608
+ * The SDK never guesses: guessing from options read a "Ring size" offering
609
+ * "4.75", "7.25" and "8.5" as three dates and replaced its sizes with a
610
+ * calendar.
605
611
  *
606
- * Options are read before the name because they are the stronger evidence:
607
- * they are the values themselves, while a name is what someone chose to call
608
- * them. A parameter called `travel_date` whose options are all spans is a span
609
- * parameter, whatever its name says.
610
- *
611
- * Accepts anything with a `parameter`, not a `Suggestion`, so re-edit can ask
612
- * the same question about a completed param via its `suggestionParameter`.
613
- * A suggestion whose `type` names a date or date-range parameter is that,
614
- * whatever its options and name say.
612
+ * Accepts anything with a `type`, not a `Suggestion`, so re-edit can ask the
613
+ * same question about a completed param via its `suggestionValueType`, and an
614
+ * identified span via its own `type`.
615
615
  */
616
616
  declare function resolveFormatType(source: {
617
- parameter: string;
618
- options?: SuggestionOption[];
619
617
  type?: SuggestionType;
620
618
  } | undefined | null): FormatType;
621
619
 
@@ -925,6 +923,8 @@ interface CoreOptions {
925
923
  apiConfig?: APIConfig;
926
924
  optionOverrides?: OptionOverrides;
927
925
  maskCompletedText?: boolean;
926
+ /** When true, a word the user is still typing never becomes a chip. The word becomes a chip when it ends: a space, Enter, or Tab. Default: false. */
927
+ commitOnWordEnd?: boolean;
928
928
  /**
929
929
  * Optional user context. Include whatever you know about the user — a
930
930
  * profile, preferences, workspace, anything — to personalize suggested
@@ -1136,6 +1136,10 @@ declare class AIAutocomplete {
1136
1136
  private funnelTelemetry;
1137
1137
  /** A `search_started` that happened before any response said whether to send it. */
1138
1138
  private pendingSearchStarted;
1139
+ /** The text at the last user keystroke, while its last word is still open. */
1140
+ private openWordText;
1141
+ /** Identified params held back until the word they end ends. */
1142
+ private heldIdentified;
1139
1143
  /** What the last `impression` showed, so an identical response doesn't resend it. */
1140
1144
  private lastImpressionKey;
1141
1145
  private readonly emitSubmit;
@@ -1231,8 +1235,8 @@ declare class AIAutocomplete {
1231
1235
  /**
1232
1236
  * Open the calendar for a span the server identified as a date.
1233
1237
  *
1234
- * Returns false for anything else — an identified span whose type isn't a
1235
- * date stays inert, exactly as every identified span did before.
1238
+ * Returns false for anything else — an identified span whose `type` the
1239
+ * server didn't name as a date or date range stays inert.
1236
1240
  *
1237
1241
  * The span's own text is read for a starting month (`parseLooseDate`), but
1238
1242
  * failing to read it is not a failure: the calendar opens either way, and
@@ -1382,6 +1386,14 @@ declare class AIAutocomplete {
1382
1386
  private subscribeSkeletonMirror;
1383
1387
  private subscribeNewParamTimer;
1384
1388
  private handleChange;
1389
+ /** True while commitOnWordEnd is on and the user still types the last word. */
1390
+ private isWordOpen;
1391
+ private admitIdentified;
1392
+ /**
1393
+ * Ends the word the user is typing. Makes the chip for an exact match and
1394
+ * admits the held identified params. Returns true when it changed anything.
1395
+ */
1396
+ private endTypedWord;
1385
1397
  /**
1386
1398
  * Opens the pending span on the first keystroke past covered text: there is
1387
1399
  * unresolved trailing text beyond the covered offset (filterBase / last pill