@blotoutio/providers-evo-search-sdk 1.62.1 → 1.63.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/hooks.d.ts CHANGED
@@ -65,6 +65,10 @@ declare module '@blotoutio/providers-evo-search-sdk/hooks' {
65
65
  export type AskConfigResponse = {
66
66
  askAiEnabled: boolean
67
67
  askStarters?: string[]
68
+ brandName?: string
69
+ brandVoice?: string
70
+ storeDescription?: string
71
+ outOfStock: 'show' | 'hide' | 'demote'
68
72
  }
69
73
 
70
74
  export type EvoSearchUserAPI = {
package/index.cjs.js CHANGED
@@ -606,7 +606,7 @@ const setStoredSearchMode = (mode) => {
606
606
  const searchSessionKeyName = 'evoSearchSession';
607
607
  const SEARCH_SESSION_VERSION = 1;
608
608
  const getStoredSearchSession = () => {
609
- var _a, _b, _c, _d, _e, _f;
609
+ var _a, _b, _c, _d, _e, _f, _g;
610
610
  try {
611
611
  const raw = sessionStorage.getItem(searchSessionKeyName);
612
612
  if (!raw) {
@@ -631,6 +631,7 @@ const getStoredSearchSession = () => {
631
631
  askAnswer: (_d = parsed.askAnswer) !== null && _d !== void 0 ? _d : '',
632
632
  askEvidence: (_e = parsed.askEvidence) !== null && _e !== void 0 ? _e : null,
633
633
  askFollowUps: (_f = parsed.askFollowUps) !== null && _f !== void 0 ? _f : [],
634
+ askCitedTitles: (_g = parsed.askCitedTitles) !== null && _g !== void 0 ? _g : [],
634
635
  };
635
636
  }
636
637
  catch {
@@ -6345,7 +6346,7 @@ const u=(e,s,t)=>{const r=new Map;for(let l=s;l<=t;l++)r.set(e[l],l);return r},c
6345
6346
  // Pure tokenizer kept out of `highlight.ts` so Jest can spec it without
6346
6347
  // pulling in `lit`'s ESM bundle. The renderer wraps these tokens in
6347
6348
  // <mark> via a Lit template — see `highlight.ts`.
6348
- const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
6349
+ const escapeRegex$1 = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
6349
6350
  // Compiling a `RegExp` per call is hot under autocomplete (every result row
6350
6351
  // + collection card + suggestion item, all sharing the same query). Keep a
6351
6352
  // single-slot memo keyed on the trimmed query.
@@ -6356,7 +6357,7 @@ const patternFor = (needle) => {
6356
6357
  return cachedPattern;
6357
6358
  }
6358
6359
  cachedNeedle = needle;
6359
- cachedPattern = new RegExp(escapeRegex(needle), 'ig');
6360
+ cachedPattern = new RegExp(escapeRegex$1(needle), 'ig');
6360
6361
  return cachedPattern;
6361
6362
  };
6362
6363
  const splitMatches = (text, query) => {
@@ -7422,6 +7423,71 @@ const buildRailSections = ({ searchQuery, suggestions, trendingSearches, recents
7422
7423
  return sections;
7423
7424
  };
7424
7425
 
7426
+ // Pure tokenizer for the Ask AI answer, kept out of the Lit component so Jest
7427
+ // can spec it without pulling in `lit`'s ESM bundle. The widget wraps the
7428
+ // `bold` segments in <strong> — see `renderAskAnswerText`.
7429
+ //
7430
+ // Highlighting is driven by the cited product/collection titles (the items the
7431
+ // model actually selected), not by whether the model remembered to wrap them
7432
+ // in `**` markers. We still honor any server-validated `**bold**` spans, then
7433
+ // additionally bold every cited title the model named in plain prose — so a
7434
+ // referred product is highlighted even when the model omits the markers.
7435
+ const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
7436
+ // One alternation of all cited titles, longest-first so the most specific
7437
+ // title wins when one is a prefix of another (regex alternation is leftmost,
7438
+ // not longest). Wrapped in Unicode-aware boundary lookarounds so a short title
7439
+ // never matches inside a larger word. Null when there are no titles to
7440
+ // highlight.
7441
+ const buildTitlePattern = (titles) => {
7442
+ const needles = Array.from(new Set(titles.map((title) => title.trim()).filter((title) => title.length))).sort((a, b) => b.length - a.length);
7443
+ if (!needles.length) {
7444
+ return null;
7445
+ }
7446
+ const alternation = needles.map(escapeRegex).join('|');
7447
+ return new RegExp(`(?<![\\p{L}\\p{N}])(?:${alternation})(?![\\p{L}\\p{N}])`, 'giu');
7448
+ };
7449
+ const pushTitleMatches = (segment, pattern, out) => {
7450
+ var _a;
7451
+ if (!pattern) {
7452
+ out.push({ text: segment, bold: false });
7453
+ return;
7454
+ }
7455
+ pattern.lastIndex = 0;
7456
+ let cursor = 0;
7457
+ for (const match of segment.matchAll(pattern)) {
7458
+ const start = (_a = match.index) !== null && _a !== void 0 ? _a : cursor;
7459
+ if (start > cursor) {
7460
+ out.push({ text: segment.slice(cursor, start), bold: false });
7461
+ }
7462
+ out.push({ text: match[0], bold: true });
7463
+ cursor = start + match[0].length;
7464
+ }
7465
+ if (cursor < segment.length) {
7466
+ out.push({ text: segment.slice(cursor), bold: false });
7467
+ }
7468
+ };
7469
+ const splitAnswerHighlights = (text, titles) => {
7470
+ const haystack = (text !== null && text !== void 0 ? text : '').trim();
7471
+ if (!haystack) {
7472
+ return [];
7473
+ }
7474
+ const pattern = buildTitlePattern(titles);
7475
+ const out = [];
7476
+ // Odd-indexed split parts are the inside of a model-emitted `**…**` span.
7477
+ haystack.split('**').forEach((segment, i) => {
7478
+ if (!segment) {
7479
+ return;
7480
+ }
7481
+ if (i % 2 === 1) {
7482
+ out.push({ text: segment, bold: true });
7483
+ }
7484
+ else {
7485
+ pushTitleMatches(segment, pattern, out);
7486
+ }
7487
+ });
7488
+ return out;
7489
+ };
7490
+
7425
7491
  const renderErrorState = (css, onAction) => b `
7426
7492
  <evs-state-block
7427
7493
  .css=${css}
@@ -8233,11 +8299,9 @@ const isCancelled = (error) => error instanceof Error &&
8233
8299
  // Default "Try asking" prompts shown on the Ask AI empty state when the
8234
8300
  // merchant hasn't configured their own via `uiPreferences.askStarters`.
8235
8301
  const FALLBACK_ASK_STARTERS = [
8236
- "What's popular right now?",
8302
+ 'Help me find a gift',
8237
8303
  'Gift ideas under $50',
8238
- 'What to wear to a wedding',
8239
- 'Recommend something for my mom',
8240
- 'Show me your bestsellers',
8304
+ "I'm not sure what to get",
8241
8305
  ];
8242
8306
  class SearchWidget extends ElementWithStylesheet {
8243
8307
  constructor() {
@@ -8260,6 +8324,10 @@ class SearchWidget extends ElementWithStylesheet {
8260
8324
  this.lastAskedQuery = '';
8261
8325
  this.askAnswer = '';
8262
8326
  this.askEvidence = null;
8327
+ // Titles of the products/collections the model actually cited, used to
8328
+ // highlight their names in the answer prose regardless of whether the model
8329
+ // wrapped them in `**` markers. Populated from the `citations` event.
8330
+ this.askCitedTitles = [];
8263
8331
  this.askFollowUps = [];
8264
8332
  this.askLoading = false;
8265
8333
  this.askAbort = null;
@@ -8451,6 +8519,7 @@ class SearchWidget extends ElementWithStylesheet {
8451
8519
  this.answerFlushHandle = null;
8452
8520
  }
8453
8521
  this.pendingAnswerChunk = '';
8522
+ this.askCitedTitles = [];
8454
8523
  }
8455
8524
  async submitAsk(query) {
8456
8525
  var _a, _b;
@@ -8478,6 +8547,7 @@ class SearchWidget extends ElementWithStylesheet {
8478
8547
  this.scheduleAnswerFlush();
8479
8548
  }
8480
8549
  else if (event.type === 'citations') {
8550
+ this.askCitedTitles = event.citations.resolved.map((c) => c.title);
8481
8551
  this.reorderEvidenceByCitations(event.citations);
8482
8552
  }
8483
8553
  else if (event.type === 'follow-ups') {
@@ -8591,8 +8661,8 @@ class SearchWidget extends ElementWithStylesheet {
8591
8661
  // just submitted — same affordance as typing it themselves.
8592
8662
  this.searchQuery = trimmed;
8593
8663
  this.lastAskedQuery = trimmed;
8594
- // The answer is the surface now — collapse the typeahead hints that were
8595
- // shown while the shopper was composing the question.
8664
+ // The answer is the surface now — collapse the pre-submit typeahead hints.
8665
+ // (Search-mode suggestions stay visible; this is ask-only.)
8596
8666
  this.showSuggestions = false;
8597
8667
  // Treat ask submissions as query history so the Recent rail surfaces
8598
8668
  // them on the next visit, identical to how typed searches behave.
@@ -8692,9 +8762,8 @@ class SearchWidget extends ElementWithStylesheet {
8692
8762
  if (this.hasError || !this.debouncedQuery || this.mode !== 'search') {
8693
8763
  return;
8694
8764
  }
8695
- // Results have settled — collapse the typeahead so it stops occupying the
8696
- // column above the products (the chip strip is a pre-result affordance).
8697
- this.showSuggestions = false;
8765
+ // Suggestions stay visible above the results until the query is cleared
8766
+ // (no settle-time collapse).
8698
8767
  // Collections are first-class results on the unified surface — a
8699
8768
  // collections-only match must not be reported as zero-result.
8700
8769
  this.markQueryFinalized(this.debouncedQuery, this.searchProducts.length + this.searchCollections.length);
@@ -8869,6 +8938,9 @@ class SearchWidget extends ElementWithStylesheet {
8869
8938
  // a terminal action, though: finalize the query now (or as soon as the
8870
8939
  // in-flight debounce/fetch renders). The pending debounce timer must NOT
8871
8940
  // be cleared here — cancelling it would strand `loading = true` forever.
8941
+ // Enter also collapses the typeahead — a terminal action, unlike the
8942
+ // settle pause which now leaves the suggestions in place.
8943
+ this.showSuggestions = false;
8872
8944
  const trimmed = query.trim();
8873
8945
  if (trimmed.length >= MIN_QUERY_LENGTH) {
8874
8946
  if (this.hasError) {
@@ -9040,6 +9112,7 @@ class SearchWidget extends ElementWithStylesheet {
9040
9112
  askAnswer: this.askAnswer,
9041
9113
  askEvidence: this.askEvidence,
9042
9114
  askFollowUps: this.askFollowUps,
9115
+ askCitedTitles: this.askCitedTitles,
9043
9116
  });
9044
9117
  }
9045
9118
  // Restore the last snapshot on mount. Sets state directly — no network — so
@@ -9069,6 +9142,7 @@ class SearchWidget extends ElementWithStylesheet {
9069
9142
  this.askAnswer = snap.askAnswer;
9070
9143
  this.askEvidence = snap.askEvidence;
9071
9144
  this.askFollowUps = snap.askFollowUps;
9145
+ this.askCitedTitles = snap.askCitedTitles;
9072
9146
  }
9073
9147
  this.mode = snap.mode === 'ask' && hasAnswer ? 'ask' : 'search';
9074
9148
  }
@@ -9117,14 +9191,17 @@ class SearchWidget extends ElementWithStylesheet {
9117
9191
  }
9118
9192
  return b ``;
9119
9193
  }
9120
- // Bold the predicted (un-typed) completion, mute what the shopper typed.
9194
+ // Bold the part of the suggestion that matches the shopper's query;
9195
+ // leave the rest unstyled. Matches anywhere in the phrase, not just the
9196
+ // prefix, since completions aren't always prefix matches of the query.
9121
9197
  renderPredicted(value) {
9122
9198
  const q = this.searchQuery.trim();
9123
- if (q && value.toLowerCase().startsWith(q.toLowerCase())) {
9124
- return b `<span class="typed">${value.slice(0, q.length)}</span
9125
- ><b>${value.slice(q.length)}</b>`;
9199
+ if (!q) {
9200
+ return b `${value}`;
9126
9201
  }
9127
- return b `<b>${value}</b>`;
9202
+ const escaped = q.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
9203
+ const parts = value.split(new RegExp(`(${escaped})`, 'gi'));
9204
+ return b `${parts.map((part, i) => i % 2 === 1 ? b `<b>${part}</b>` : b `${part}`)}`;
9128
9205
  }
9129
9206
  renderSuggestions(section) {
9130
9207
  if (!section.items.length) {
@@ -9339,8 +9416,8 @@ class SearchWidget extends ElementWithStylesheet {
9339
9416
  `;
9340
9417
  }
9341
9418
  renderAskAnswerText(text) {
9342
- const parts = text.trim().split('**');
9343
- return b `${parts.map((part, i) => i % 2 === 1 ? b `<strong>${part}</strong>` : part)}`;
9419
+ const segments = splitAnswerHighlights(text, this.askCitedTitles);
9420
+ return b `${segments.map((segment) => segment.bold ? b `<strong>${segment.text}</strong>` : segment.text)}`;
9344
9421
  }
9345
9422
  renderAskPane() {
9346
9423
  // Once the LLM has finished talking, show the centered starter chips
@@ -9747,6 +9824,10 @@ __decorate([
9747
9824
  r$1(),
9748
9825
  __metadata("design:type", Object)
9749
9826
  ], SearchWidget.prototype, "askEvidence", void 0);
9827
+ __decorate([
9828
+ r$1(),
9829
+ __metadata("design:type", Array)
9830
+ ], SearchWidget.prototype, "askCitedTitles", void 0);
9750
9831
  __decorate([
9751
9832
  r$1(),
9752
9833
  __metadata("design:type", Array)
package/index.js CHANGED
@@ -607,7 +607,7 @@ var ProvidersEvoSearchSdk = (function () {
607
607
  const searchSessionKeyName = 'evoSearchSession';
608
608
  const SEARCH_SESSION_VERSION = 1;
609
609
  const getStoredSearchSession = () => {
610
- var _a, _b, _c, _d, _e, _f;
610
+ var _a, _b, _c, _d, _e, _f, _g;
611
611
  try {
612
612
  const raw = sessionStorage.getItem(searchSessionKeyName);
613
613
  if (!raw) {
@@ -632,6 +632,7 @@ var ProvidersEvoSearchSdk = (function () {
632
632
  askAnswer: (_d = parsed.askAnswer) !== null && _d !== void 0 ? _d : '',
633
633
  askEvidence: (_e = parsed.askEvidence) !== null && _e !== void 0 ? _e : null,
634
634
  askFollowUps: (_f = parsed.askFollowUps) !== null && _f !== void 0 ? _f : [],
635
+ askCitedTitles: (_g = parsed.askCitedTitles) !== null && _g !== void 0 ? _g : [],
635
636
  };
636
637
  }
637
638
  catch {
@@ -6346,7 +6347,7 @@ var ProvidersEvoSearchSdk = (function () {
6346
6347
  // Pure tokenizer kept out of `highlight.ts` so Jest can spec it without
6347
6348
  // pulling in `lit`'s ESM bundle. The renderer wraps these tokens in
6348
6349
  // <mark> via a Lit template — see `highlight.ts`.
6349
- const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
6350
+ const escapeRegex$1 = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
6350
6351
  // Compiling a `RegExp` per call is hot under autocomplete (every result row
6351
6352
  // + collection card + suggestion item, all sharing the same query). Keep a
6352
6353
  // single-slot memo keyed on the trimmed query.
@@ -6357,7 +6358,7 @@ var ProvidersEvoSearchSdk = (function () {
6357
6358
  return cachedPattern;
6358
6359
  }
6359
6360
  cachedNeedle = needle;
6360
- cachedPattern = new RegExp(escapeRegex(needle), 'ig');
6361
+ cachedPattern = new RegExp(escapeRegex$1(needle), 'ig');
6361
6362
  return cachedPattern;
6362
6363
  };
6363
6364
  const splitMatches = (text, query) => {
@@ -7423,6 +7424,71 @@ var ProvidersEvoSearchSdk = (function () {
7423
7424
  return sections;
7424
7425
  };
7425
7426
 
7427
+ // Pure tokenizer for the Ask AI answer, kept out of the Lit component so Jest
7428
+ // can spec it without pulling in `lit`'s ESM bundle. The widget wraps the
7429
+ // `bold` segments in <strong> — see `renderAskAnswerText`.
7430
+ //
7431
+ // Highlighting is driven by the cited product/collection titles (the items the
7432
+ // model actually selected), not by whether the model remembered to wrap them
7433
+ // in `**` markers. We still honor any server-validated `**bold**` spans, then
7434
+ // additionally bold every cited title the model named in plain prose — so a
7435
+ // referred product is highlighted even when the model omits the markers.
7436
+ const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
7437
+ // One alternation of all cited titles, longest-first so the most specific
7438
+ // title wins when one is a prefix of another (regex alternation is leftmost,
7439
+ // not longest). Wrapped in Unicode-aware boundary lookarounds so a short title
7440
+ // never matches inside a larger word. Null when there are no titles to
7441
+ // highlight.
7442
+ const buildTitlePattern = (titles) => {
7443
+ const needles = Array.from(new Set(titles.map((title) => title.trim()).filter((title) => title.length))).sort((a, b) => b.length - a.length);
7444
+ if (!needles.length) {
7445
+ return null;
7446
+ }
7447
+ const alternation = needles.map(escapeRegex).join('|');
7448
+ return new RegExp(`(?<![\\p{L}\\p{N}])(?:${alternation})(?![\\p{L}\\p{N}])`, 'giu');
7449
+ };
7450
+ const pushTitleMatches = (segment, pattern, out) => {
7451
+ var _a;
7452
+ if (!pattern) {
7453
+ out.push({ text: segment, bold: false });
7454
+ return;
7455
+ }
7456
+ pattern.lastIndex = 0;
7457
+ let cursor = 0;
7458
+ for (const match of segment.matchAll(pattern)) {
7459
+ const start = (_a = match.index) !== null && _a !== void 0 ? _a : cursor;
7460
+ if (start > cursor) {
7461
+ out.push({ text: segment.slice(cursor, start), bold: false });
7462
+ }
7463
+ out.push({ text: match[0], bold: true });
7464
+ cursor = start + match[0].length;
7465
+ }
7466
+ if (cursor < segment.length) {
7467
+ out.push({ text: segment.slice(cursor), bold: false });
7468
+ }
7469
+ };
7470
+ const splitAnswerHighlights = (text, titles) => {
7471
+ const haystack = (text !== null && text !== void 0 ? text : '').trim();
7472
+ if (!haystack) {
7473
+ return [];
7474
+ }
7475
+ const pattern = buildTitlePattern(titles);
7476
+ const out = [];
7477
+ // Odd-indexed split parts are the inside of a model-emitted `**…**` span.
7478
+ haystack.split('**').forEach((segment, i) => {
7479
+ if (!segment) {
7480
+ return;
7481
+ }
7482
+ if (i % 2 === 1) {
7483
+ out.push({ text: segment, bold: true });
7484
+ }
7485
+ else {
7486
+ pushTitleMatches(segment, pattern, out);
7487
+ }
7488
+ });
7489
+ return out;
7490
+ };
7491
+
7426
7492
  const renderErrorState = (css, onAction) => b `
7427
7493
  <evs-state-block
7428
7494
  .css=${css}
@@ -8234,11 +8300,9 @@ var ProvidersEvoSearchSdk = (function () {
8234
8300
  // Default "Try asking" prompts shown on the Ask AI empty state when the
8235
8301
  // merchant hasn't configured their own via `uiPreferences.askStarters`.
8236
8302
  const FALLBACK_ASK_STARTERS = [
8237
- "What's popular right now?",
8303
+ 'Help me find a gift',
8238
8304
  'Gift ideas under $50',
8239
- 'What to wear to a wedding',
8240
- 'Recommend something for my mom',
8241
- 'Show me your bestsellers',
8305
+ "I'm not sure what to get",
8242
8306
  ];
8243
8307
  class SearchWidget extends ElementWithStylesheet {
8244
8308
  constructor() {
@@ -8261,6 +8325,10 @@ var ProvidersEvoSearchSdk = (function () {
8261
8325
  this.lastAskedQuery = '';
8262
8326
  this.askAnswer = '';
8263
8327
  this.askEvidence = null;
8328
+ // Titles of the products/collections the model actually cited, used to
8329
+ // highlight their names in the answer prose regardless of whether the model
8330
+ // wrapped them in `**` markers. Populated from the `citations` event.
8331
+ this.askCitedTitles = [];
8264
8332
  this.askFollowUps = [];
8265
8333
  this.askLoading = false;
8266
8334
  this.askAbort = null;
@@ -8452,6 +8520,7 @@ var ProvidersEvoSearchSdk = (function () {
8452
8520
  this.answerFlushHandle = null;
8453
8521
  }
8454
8522
  this.pendingAnswerChunk = '';
8523
+ this.askCitedTitles = [];
8455
8524
  }
8456
8525
  async submitAsk(query) {
8457
8526
  var _a, _b;
@@ -8479,6 +8548,7 @@ var ProvidersEvoSearchSdk = (function () {
8479
8548
  this.scheduleAnswerFlush();
8480
8549
  }
8481
8550
  else if (event.type === 'citations') {
8551
+ this.askCitedTitles = event.citations.resolved.map((c) => c.title);
8482
8552
  this.reorderEvidenceByCitations(event.citations);
8483
8553
  }
8484
8554
  else if (event.type === 'follow-ups') {
@@ -8592,8 +8662,8 @@ var ProvidersEvoSearchSdk = (function () {
8592
8662
  // just submitted — same affordance as typing it themselves.
8593
8663
  this.searchQuery = trimmed;
8594
8664
  this.lastAskedQuery = trimmed;
8595
- // The answer is the surface now — collapse the typeahead hints that were
8596
- // shown while the shopper was composing the question.
8665
+ // The answer is the surface now — collapse the pre-submit typeahead hints.
8666
+ // (Search-mode suggestions stay visible; this is ask-only.)
8597
8667
  this.showSuggestions = false;
8598
8668
  // Treat ask submissions as query history so the Recent rail surfaces
8599
8669
  // them on the next visit, identical to how typed searches behave.
@@ -8693,9 +8763,8 @@ var ProvidersEvoSearchSdk = (function () {
8693
8763
  if (this.hasError || !this.debouncedQuery || this.mode !== 'search') {
8694
8764
  return;
8695
8765
  }
8696
- // Results have settled — collapse the typeahead so it stops occupying the
8697
- // column above the products (the chip strip is a pre-result affordance).
8698
- this.showSuggestions = false;
8766
+ // Suggestions stay visible above the results until the query is cleared
8767
+ // (no settle-time collapse).
8699
8768
  // Collections are first-class results on the unified surface — a
8700
8769
  // collections-only match must not be reported as zero-result.
8701
8770
  this.markQueryFinalized(this.debouncedQuery, this.searchProducts.length + this.searchCollections.length);
@@ -8870,6 +8939,9 @@ var ProvidersEvoSearchSdk = (function () {
8870
8939
  // a terminal action, though: finalize the query now (or as soon as the
8871
8940
  // in-flight debounce/fetch renders). The pending debounce timer must NOT
8872
8941
  // be cleared here — cancelling it would strand `loading = true` forever.
8942
+ // Enter also collapses the typeahead — a terminal action, unlike the
8943
+ // settle pause which now leaves the suggestions in place.
8944
+ this.showSuggestions = false;
8873
8945
  const trimmed = query.trim();
8874
8946
  if (trimmed.length >= MIN_QUERY_LENGTH) {
8875
8947
  if (this.hasError) {
@@ -9041,6 +9113,7 @@ var ProvidersEvoSearchSdk = (function () {
9041
9113
  askAnswer: this.askAnswer,
9042
9114
  askEvidence: this.askEvidence,
9043
9115
  askFollowUps: this.askFollowUps,
9116
+ askCitedTitles: this.askCitedTitles,
9044
9117
  });
9045
9118
  }
9046
9119
  // Restore the last snapshot on mount. Sets state directly — no network — so
@@ -9070,6 +9143,7 @@ var ProvidersEvoSearchSdk = (function () {
9070
9143
  this.askAnswer = snap.askAnswer;
9071
9144
  this.askEvidence = snap.askEvidence;
9072
9145
  this.askFollowUps = snap.askFollowUps;
9146
+ this.askCitedTitles = snap.askCitedTitles;
9073
9147
  }
9074
9148
  this.mode = snap.mode === 'ask' && hasAnswer ? 'ask' : 'search';
9075
9149
  }
@@ -9118,14 +9192,17 @@ var ProvidersEvoSearchSdk = (function () {
9118
9192
  }
9119
9193
  return b ``;
9120
9194
  }
9121
- // Bold the predicted (un-typed) completion, mute what the shopper typed.
9195
+ // Bold the part of the suggestion that matches the shopper's query;
9196
+ // leave the rest unstyled. Matches anywhere in the phrase, not just the
9197
+ // prefix, since completions aren't always prefix matches of the query.
9122
9198
  renderPredicted(value) {
9123
9199
  const q = this.searchQuery.trim();
9124
- if (q && value.toLowerCase().startsWith(q.toLowerCase())) {
9125
- return b `<span class="typed">${value.slice(0, q.length)}</span
9126
- ><b>${value.slice(q.length)}</b>`;
9200
+ if (!q) {
9201
+ return b `${value}`;
9127
9202
  }
9128
- return b `<b>${value}</b>`;
9203
+ const escaped = q.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
9204
+ const parts = value.split(new RegExp(`(${escaped})`, 'gi'));
9205
+ return b `${parts.map((part, i) => i % 2 === 1 ? b `<b>${part}</b>` : b `${part}`)}`;
9129
9206
  }
9130
9207
  renderSuggestions(section) {
9131
9208
  if (!section.items.length) {
@@ -9340,8 +9417,8 @@ var ProvidersEvoSearchSdk = (function () {
9340
9417
  `;
9341
9418
  }
9342
9419
  renderAskAnswerText(text) {
9343
- const parts = text.trim().split('**');
9344
- return b `${parts.map((part, i) => i % 2 === 1 ? b `<strong>${part}</strong>` : part)}`;
9420
+ const segments = splitAnswerHighlights(text, this.askCitedTitles);
9421
+ return b `${segments.map((segment) => segment.bold ? b `<strong>${segment.text}</strong>` : segment.text)}`;
9345
9422
  }
9346
9423
  renderAskPane() {
9347
9424
  // Once the LLM has finished talking, show the centered starter chips
@@ -9748,6 +9825,10 @@ var ProvidersEvoSearchSdk = (function () {
9748
9825
  r$1(),
9749
9826
  __metadata("design:type", Object)
9750
9827
  ], SearchWidget.prototype, "askEvidence", void 0);
9828
+ __decorate([
9829
+ r$1(),
9830
+ __metadata("design:type", Array)
9831
+ ], SearchWidget.prototype, "askCitedTitles", void 0);
9751
9832
  __decorate([
9752
9833
  r$1(),
9753
9834
  __metadata("design:type", Array)
package/index.mjs CHANGED
@@ -604,7 +604,7 @@ const setStoredSearchMode = (mode) => {
604
604
  const searchSessionKeyName = 'evoSearchSession';
605
605
  const SEARCH_SESSION_VERSION = 1;
606
606
  const getStoredSearchSession = () => {
607
- var _a, _b, _c, _d, _e, _f;
607
+ var _a, _b, _c, _d, _e, _f, _g;
608
608
  try {
609
609
  const raw = sessionStorage.getItem(searchSessionKeyName);
610
610
  if (!raw) {
@@ -629,6 +629,7 @@ const getStoredSearchSession = () => {
629
629
  askAnswer: (_d = parsed.askAnswer) !== null && _d !== void 0 ? _d : '',
630
630
  askEvidence: (_e = parsed.askEvidence) !== null && _e !== void 0 ? _e : null,
631
631
  askFollowUps: (_f = parsed.askFollowUps) !== null && _f !== void 0 ? _f : [],
632
+ askCitedTitles: (_g = parsed.askCitedTitles) !== null && _g !== void 0 ? _g : [],
632
633
  };
633
634
  }
634
635
  catch {
@@ -6343,7 +6344,7 @@ const u=(e,s,t)=>{const r=new Map;for(let l=s;l<=t;l++)r.set(e[l],l);return r},c
6343
6344
  // Pure tokenizer kept out of `highlight.ts` so Jest can spec it without
6344
6345
  // pulling in `lit`'s ESM bundle. The renderer wraps these tokens in
6345
6346
  // <mark> via a Lit template — see `highlight.ts`.
6346
- const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
6347
+ const escapeRegex$1 = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
6347
6348
  // Compiling a `RegExp` per call is hot under autocomplete (every result row
6348
6349
  // + collection card + suggestion item, all sharing the same query). Keep a
6349
6350
  // single-slot memo keyed on the trimmed query.
@@ -6354,7 +6355,7 @@ const patternFor = (needle) => {
6354
6355
  return cachedPattern;
6355
6356
  }
6356
6357
  cachedNeedle = needle;
6357
- cachedPattern = new RegExp(escapeRegex(needle), 'ig');
6358
+ cachedPattern = new RegExp(escapeRegex$1(needle), 'ig');
6358
6359
  return cachedPattern;
6359
6360
  };
6360
6361
  const splitMatches = (text, query) => {
@@ -7420,6 +7421,71 @@ const buildRailSections = ({ searchQuery, suggestions, trendingSearches, recents
7420
7421
  return sections;
7421
7422
  };
7422
7423
 
7424
+ // Pure tokenizer for the Ask AI answer, kept out of the Lit component so Jest
7425
+ // can spec it without pulling in `lit`'s ESM bundle. The widget wraps the
7426
+ // `bold` segments in <strong> — see `renderAskAnswerText`.
7427
+ //
7428
+ // Highlighting is driven by the cited product/collection titles (the items the
7429
+ // model actually selected), not by whether the model remembered to wrap them
7430
+ // in `**` markers. We still honor any server-validated `**bold**` spans, then
7431
+ // additionally bold every cited title the model named in plain prose — so a
7432
+ // referred product is highlighted even when the model omits the markers.
7433
+ const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
7434
+ // One alternation of all cited titles, longest-first so the most specific
7435
+ // title wins when one is a prefix of another (regex alternation is leftmost,
7436
+ // not longest). Wrapped in Unicode-aware boundary lookarounds so a short title
7437
+ // never matches inside a larger word. Null when there are no titles to
7438
+ // highlight.
7439
+ const buildTitlePattern = (titles) => {
7440
+ const needles = Array.from(new Set(titles.map((title) => title.trim()).filter((title) => title.length))).sort((a, b) => b.length - a.length);
7441
+ if (!needles.length) {
7442
+ return null;
7443
+ }
7444
+ const alternation = needles.map(escapeRegex).join('|');
7445
+ return new RegExp(`(?<![\\p{L}\\p{N}])(?:${alternation})(?![\\p{L}\\p{N}])`, 'giu');
7446
+ };
7447
+ const pushTitleMatches = (segment, pattern, out) => {
7448
+ var _a;
7449
+ if (!pattern) {
7450
+ out.push({ text: segment, bold: false });
7451
+ return;
7452
+ }
7453
+ pattern.lastIndex = 0;
7454
+ let cursor = 0;
7455
+ for (const match of segment.matchAll(pattern)) {
7456
+ const start = (_a = match.index) !== null && _a !== void 0 ? _a : cursor;
7457
+ if (start > cursor) {
7458
+ out.push({ text: segment.slice(cursor, start), bold: false });
7459
+ }
7460
+ out.push({ text: match[0], bold: true });
7461
+ cursor = start + match[0].length;
7462
+ }
7463
+ if (cursor < segment.length) {
7464
+ out.push({ text: segment.slice(cursor), bold: false });
7465
+ }
7466
+ };
7467
+ const splitAnswerHighlights = (text, titles) => {
7468
+ const haystack = (text !== null && text !== void 0 ? text : '').trim();
7469
+ if (!haystack) {
7470
+ return [];
7471
+ }
7472
+ const pattern = buildTitlePattern(titles);
7473
+ const out = [];
7474
+ // Odd-indexed split parts are the inside of a model-emitted `**…**` span.
7475
+ haystack.split('**').forEach((segment, i) => {
7476
+ if (!segment) {
7477
+ return;
7478
+ }
7479
+ if (i % 2 === 1) {
7480
+ out.push({ text: segment, bold: true });
7481
+ }
7482
+ else {
7483
+ pushTitleMatches(segment, pattern, out);
7484
+ }
7485
+ });
7486
+ return out;
7487
+ };
7488
+
7423
7489
  const renderErrorState = (css, onAction) => b `
7424
7490
  <evs-state-block
7425
7491
  .css=${css}
@@ -8231,11 +8297,9 @@ const isCancelled = (error) => error instanceof Error &&
8231
8297
  // Default "Try asking" prompts shown on the Ask AI empty state when the
8232
8298
  // merchant hasn't configured their own via `uiPreferences.askStarters`.
8233
8299
  const FALLBACK_ASK_STARTERS = [
8234
- "What's popular right now?",
8300
+ 'Help me find a gift',
8235
8301
  'Gift ideas under $50',
8236
- 'What to wear to a wedding',
8237
- 'Recommend something for my mom',
8238
- 'Show me your bestsellers',
8302
+ "I'm not sure what to get",
8239
8303
  ];
8240
8304
  class SearchWidget extends ElementWithStylesheet {
8241
8305
  constructor() {
@@ -8258,6 +8322,10 @@ class SearchWidget extends ElementWithStylesheet {
8258
8322
  this.lastAskedQuery = '';
8259
8323
  this.askAnswer = '';
8260
8324
  this.askEvidence = null;
8325
+ // Titles of the products/collections the model actually cited, used to
8326
+ // highlight their names in the answer prose regardless of whether the model
8327
+ // wrapped them in `**` markers. Populated from the `citations` event.
8328
+ this.askCitedTitles = [];
8261
8329
  this.askFollowUps = [];
8262
8330
  this.askLoading = false;
8263
8331
  this.askAbort = null;
@@ -8449,6 +8517,7 @@ class SearchWidget extends ElementWithStylesheet {
8449
8517
  this.answerFlushHandle = null;
8450
8518
  }
8451
8519
  this.pendingAnswerChunk = '';
8520
+ this.askCitedTitles = [];
8452
8521
  }
8453
8522
  async submitAsk(query) {
8454
8523
  var _a, _b;
@@ -8476,6 +8545,7 @@ class SearchWidget extends ElementWithStylesheet {
8476
8545
  this.scheduleAnswerFlush();
8477
8546
  }
8478
8547
  else if (event.type === 'citations') {
8548
+ this.askCitedTitles = event.citations.resolved.map((c) => c.title);
8479
8549
  this.reorderEvidenceByCitations(event.citations);
8480
8550
  }
8481
8551
  else if (event.type === 'follow-ups') {
@@ -8589,8 +8659,8 @@ class SearchWidget extends ElementWithStylesheet {
8589
8659
  // just submitted — same affordance as typing it themselves.
8590
8660
  this.searchQuery = trimmed;
8591
8661
  this.lastAskedQuery = trimmed;
8592
- // The answer is the surface now — collapse the typeahead hints that were
8593
- // shown while the shopper was composing the question.
8662
+ // The answer is the surface now — collapse the pre-submit typeahead hints.
8663
+ // (Search-mode suggestions stay visible; this is ask-only.)
8594
8664
  this.showSuggestions = false;
8595
8665
  // Treat ask submissions as query history so the Recent rail surfaces
8596
8666
  // them on the next visit, identical to how typed searches behave.
@@ -8690,9 +8760,8 @@ class SearchWidget extends ElementWithStylesheet {
8690
8760
  if (this.hasError || !this.debouncedQuery || this.mode !== 'search') {
8691
8761
  return;
8692
8762
  }
8693
- // Results have settled — collapse the typeahead so it stops occupying the
8694
- // column above the products (the chip strip is a pre-result affordance).
8695
- this.showSuggestions = false;
8763
+ // Suggestions stay visible above the results until the query is cleared
8764
+ // (no settle-time collapse).
8696
8765
  // Collections are first-class results on the unified surface — a
8697
8766
  // collections-only match must not be reported as zero-result.
8698
8767
  this.markQueryFinalized(this.debouncedQuery, this.searchProducts.length + this.searchCollections.length);
@@ -8867,6 +8936,9 @@ class SearchWidget extends ElementWithStylesheet {
8867
8936
  // a terminal action, though: finalize the query now (or as soon as the
8868
8937
  // in-flight debounce/fetch renders). The pending debounce timer must NOT
8869
8938
  // be cleared here — cancelling it would strand `loading = true` forever.
8939
+ // Enter also collapses the typeahead — a terminal action, unlike the
8940
+ // settle pause which now leaves the suggestions in place.
8941
+ this.showSuggestions = false;
8870
8942
  const trimmed = query.trim();
8871
8943
  if (trimmed.length >= MIN_QUERY_LENGTH) {
8872
8944
  if (this.hasError) {
@@ -9038,6 +9110,7 @@ class SearchWidget extends ElementWithStylesheet {
9038
9110
  askAnswer: this.askAnswer,
9039
9111
  askEvidence: this.askEvidence,
9040
9112
  askFollowUps: this.askFollowUps,
9113
+ askCitedTitles: this.askCitedTitles,
9041
9114
  });
9042
9115
  }
9043
9116
  // Restore the last snapshot on mount. Sets state directly — no network — so
@@ -9067,6 +9140,7 @@ class SearchWidget extends ElementWithStylesheet {
9067
9140
  this.askAnswer = snap.askAnswer;
9068
9141
  this.askEvidence = snap.askEvidence;
9069
9142
  this.askFollowUps = snap.askFollowUps;
9143
+ this.askCitedTitles = snap.askCitedTitles;
9070
9144
  }
9071
9145
  this.mode = snap.mode === 'ask' && hasAnswer ? 'ask' : 'search';
9072
9146
  }
@@ -9115,14 +9189,17 @@ class SearchWidget extends ElementWithStylesheet {
9115
9189
  }
9116
9190
  return b ``;
9117
9191
  }
9118
- // Bold the predicted (un-typed) completion, mute what the shopper typed.
9192
+ // Bold the part of the suggestion that matches the shopper's query;
9193
+ // leave the rest unstyled. Matches anywhere in the phrase, not just the
9194
+ // prefix, since completions aren't always prefix matches of the query.
9119
9195
  renderPredicted(value) {
9120
9196
  const q = this.searchQuery.trim();
9121
- if (q && value.toLowerCase().startsWith(q.toLowerCase())) {
9122
- return b `<span class="typed">${value.slice(0, q.length)}</span
9123
- ><b>${value.slice(q.length)}</b>`;
9197
+ if (!q) {
9198
+ return b `${value}`;
9124
9199
  }
9125
- return b `<b>${value}</b>`;
9200
+ const escaped = q.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
9201
+ const parts = value.split(new RegExp(`(${escaped})`, 'gi'));
9202
+ return b `${parts.map((part, i) => i % 2 === 1 ? b `<b>${part}</b>` : b `${part}`)}`;
9126
9203
  }
9127
9204
  renderSuggestions(section) {
9128
9205
  if (!section.items.length) {
@@ -9337,8 +9414,8 @@ class SearchWidget extends ElementWithStylesheet {
9337
9414
  `;
9338
9415
  }
9339
9416
  renderAskAnswerText(text) {
9340
- const parts = text.trim().split('**');
9341
- return b `${parts.map((part, i) => i % 2 === 1 ? b `<strong>${part}</strong>` : part)}`;
9417
+ const segments = splitAnswerHighlights(text, this.askCitedTitles);
9418
+ return b `${segments.map((segment) => segment.bold ? b `<strong>${segment.text}</strong>` : segment.text)}`;
9342
9419
  }
9343
9420
  renderAskPane() {
9344
9421
  // Once the LLM has finished talking, show the centered starter chips
@@ -9745,6 +9822,10 @@ __decorate([
9745
9822
  r$1(),
9746
9823
  __metadata("design:type", Object)
9747
9824
  ], SearchWidget.prototype, "askEvidence", void 0);
9825
+ __decorate([
9826
+ r$1(),
9827
+ __metadata("design:type", Array)
9828
+ ], SearchWidget.prototype, "askCitedTitles", void 0);
9748
9829
  __decorate([
9749
9830
  r$1(),
9750
9831
  __metadata("design:type", Array)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blotoutio/providers-evo-search-sdk",
3
- "version": "1.62.1",
3
+ "version": "1.63.0",
4
4
  "description": "Evo Search SDK for EdgeTag",
5
5
  "author": "Blotout",
6
6
  "license": "MIT",