@blotoutio/providers-evo-search-sdk 1.66.2 → 1.68.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 +116 -0
- package/core.cjs.js +242 -5
- package/core.js +242 -5
- package/core.mjs +242 -5
- package/hooks.cjs.js +320 -3
- package/hooks.d.ts +58 -1
- package/hooks.mjs +318 -4
- package/index.cjs.js +2202 -81
- package/index.js +2202 -81
- package/index.mjs +2202 -81
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -54,6 +54,122 @@ function HeaderSearch() {
|
|
|
54
54
|
}
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
+
## Image search
|
|
58
|
+
|
|
59
|
+
Shoppers can search with a photo instead of words. The hook exposes it as one extra action that writes the same `results` / `loading` / `error` state as the text path, so you render one result list:
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
import { useEvoSearch } from '@blotoutio/providers-evo-search-sdk/hooks'
|
|
63
|
+
|
|
64
|
+
function ImageSearch() {
|
|
65
|
+
const { query, searchByImage, results, loading, error } = useEvoSearch()
|
|
66
|
+
return (
|
|
67
|
+
<>
|
|
68
|
+
<input
|
|
69
|
+
type='file'
|
|
70
|
+
accept='image/*'
|
|
71
|
+
onChange={(e) => {
|
|
72
|
+
const file = e.target.files?.[0]
|
|
73
|
+
if (file) {
|
|
74
|
+
// Second argument is an optional text refinement ("in blue").
|
|
75
|
+
void searchByImage(file, query)
|
|
76
|
+
}
|
|
77
|
+
}}
|
|
78
|
+
/>
|
|
79
|
+
{loading && <span>Searching…</span>}
|
|
80
|
+
{error && <span>Image search failed. Try again.</span>}
|
|
81
|
+
<ul>
|
|
82
|
+
{results.map((p) => (
|
|
83
|
+
<li key={p.id}>{p.title}</li>
|
|
84
|
+
))}
|
|
85
|
+
</ul>
|
|
86
|
+
</>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Pass a `File` or `Blob` straight from the file input (or a drop / paste handler) — it is downscaled and JPEG-encoded for you. A `string` is taken as an already-prepared `data:` URI and sent as-is.
|
|
92
|
+
|
|
93
|
+
### Check the merchant gate first
|
|
94
|
+
|
|
95
|
+
Image search is off unless the merchant enabled it, exactly like Ask AI. Read it from the API config and hide the affordance when it is not enabled:
|
|
96
|
+
|
|
97
|
+
```tsx
|
|
98
|
+
import { useEffect, useState } from 'react'
|
|
99
|
+
import { useEvoSearchAPI } from '@blotoutio/providers-evo-search-sdk/hooks'
|
|
100
|
+
|
|
101
|
+
function useVisualSearchEnabled() {
|
|
102
|
+
const { api, ready } = useEvoSearchAPI()
|
|
103
|
+
const [enabled, setEnabled] = useState(false)
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
if (!ready || !api) {
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
void api
|
|
109
|
+
// Older workers omit the field, so compare strictly — a missing value
|
|
110
|
+
// means off, never on.
|
|
111
|
+
.config()
|
|
112
|
+
.then((config) => setEnabled(config.visualSearchEnabled === true))
|
|
113
|
+
.catch(() => setEnabled(false))
|
|
114
|
+
}, [api, ready])
|
|
115
|
+
return enabled
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
If the merchant turns it off between that check and a search, the request fails with `403` and the rejection carries `disabled: true`. Treat that as "hide the entry point", not as an error to show:
|
|
120
|
+
|
|
121
|
+
```tsx
|
|
122
|
+
import type { EvoSearchAPIError } from '@blotoutio/providers-evo-search-sdk/hooks'
|
|
123
|
+
|
|
124
|
+
if ((error as EvoSearchAPIError | null)?.disabled) {
|
|
125
|
+
// Hide the camera button — nothing the shopper can retry.
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### The raw API (non-React, or your own state)
|
|
130
|
+
|
|
131
|
+
In React, `useEvoSearchAPI()` hands you the API object:
|
|
132
|
+
|
|
133
|
+
```tsx
|
|
134
|
+
const { api } = useEvoSearchAPI()
|
|
135
|
+
const { mode, products } = await api.searchImage(file, 'in blue')
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
It is the same object the registry holds, so a vanilla storefront can reach it without the hook:
|
|
139
|
+
|
|
140
|
+
```js
|
|
141
|
+
const api = window[Symbol.for('evo-search')]?.api
|
|
142
|
+
const { mode, products } = await api.searchImage(file, 'in blue')
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`searchImage` returns the API product shape, not the normalized one `useEvoSearch().results` yields. Map it if you want both paths to render through the same components:
|
|
146
|
+
|
|
147
|
+
```js
|
|
148
|
+
import { mapAPIProductToSDKProduct } from '@blotoutio/providers-evo-search-sdk/hooks'
|
|
149
|
+
|
|
150
|
+
const normalized = products.map(mapAPIProductToSDKProduct)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Timeouts are your responsibility on the raw API
|
|
154
|
+
|
|
155
|
+
`searchImage` has no built-in deadline: pass an `AbortSignal` so a stalled embedding can't hang your UI forever. Do not retry — every attempt costs a multimodal embedding, and there is no response cache on this route. `useEvoSearch().searchByImage` already applies this policy (no retries, 15s budget):
|
|
156
|
+
|
|
157
|
+
```js
|
|
158
|
+
const controller = new AbortController()
|
|
159
|
+
const timer = setTimeout(() => controller.abort(), 15_000)
|
|
160
|
+
try {
|
|
161
|
+
await api.searchImage(file, undefined, undefined, controller.signal)
|
|
162
|
+
} finally {
|
|
163
|
+
clearTimeout(timer)
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Debounce and abort rather than firing per keystroke when a text refinement is attached to an image.
|
|
168
|
+
|
|
169
|
+
### Why 768px
|
|
170
|
+
|
|
171
|
+
Uploads are downscaled to a 768px long edge before they leave the browser: retrieval quality was tuned at that rendition, and the endpoint caps the decoded payload (~1.5MB), which a phone photo blows past. If you want to prepare or preview the data URI yourself, `downscaleImage(file)` and `MAX_IMAGE_EDGE` are exported from the same entry point — it rejects anything that isn't an image.
|
|
172
|
+
|
|
57
173
|
## Tracking interactions
|
|
58
174
|
|
|
59
175
|
EvoSearch needs three interaction signals to power the dashboard: searches, result clicks, and cart/purchase events. **In a headless integration, only `trackClick` requires code on your side.**
|
package/core.cjs.js
CHANGED
|
@@ -598,6 +598,19 @@ const hasPreviewKey = () => {
|
|
|
598
598
|
return false;
|
|
599
599
|
}
|
|
600
600
|
};
|
|
601
|
+
// Session-sticky opt-out for internal QA: ?evs=off suppresses the widget even
|
|
602
|
+
// when the manifest has it enabled, so the merchant's default site can be
|
|
603
|
+
// previewed. Takes precedence over both `enabled` and the preview flag.
|
|
604
|
+
const disableKeyName = 'disableEvoSearch';
|
|
605
|
+
const hasDisableKey = () => {
|
|
606
|
+
var _a;
|
|
607
|
+
try {
|
|
608
|
+
return ((_a = sessionStorage.getItem(disableKeyName)) !== null && _a !== void 0 ? _a : '0') === '1';
|
|
609
|
+
}
|
|
610
|
+
catch {
|
|
611
|
+
return false;
|
|
612
|
+
}
|
|
613
|
+
};
|
|
601
614
|
|
|
602
615
|
var _a;
|
|
603
616
|
const registryKey = Symbol.for('evo-search');
|
|
@@ -640,6 +653,10 @@ const notifyReady = () => {
|
|
|
640
653
|
// caller-superseded (aborted) request. Shared by the widget and the headless
|
|
641
654
|
// hook. See callers: search-widget.ts (performAutocomplete) and
|
|
642
655
|
// hooks/use-evo-search.ts.
|
|
656
|
+
// Per-attempt budget for /search/image, shared by the widget and the headless
|
|
657
|
+
// hook so the two can't drift. Wider than the text search's 7s: the request
|
|
658
|
+
// carries an upload and waits on a multimodal embedding.
|
|
659
|
+
const IMAGE_SEARCH_TIMEOUT_MS = 15000;
|
|
643
660
|
const DEFAULTS = {
|
|
644
661
|
timeoutMs: 7000,
|
|
645
662
|
retries: 2,
|
|
@@ -743,6 +760,126 @@ async function withSearchResilience(attempt, callerSignal, options = {}) {
|
|
|
743
760
|
throw lastError;
|
|
744
761
|
}
|
|
745
762
|
|
|
763
|
+
// Client-side downscale for image search. The worker caps the decoded upload at
|
|
764
|
+
// ~1.5MB and every request costs a multimodal embed, so shrinking a phone photo
|
|
765
|
+
// here (a 12MP JPEG is ~4MB) is what keeps the request inside the cap and the
|
|
766
|
+
// upload quick on mobile data. Matches the sync-time `imageEmbedWidth` (see
|
|
767
|
+
// search-tuning.ts) so query and index renditions land in the same Gemini
|
|
768
|
+
// tiling bracket — a JPEG at this edge is still two orders of magnitude below
|
|
769
|
+
// the worker's decoded-size cap.
|
|
770
|
+
const MAX_IMAGE_EDGE = 768;
|
|
771
|
+
const JPEG_QUALITY = 0.85;
|
|
772
|
+
// Fallback decode for engines without createImageBitmap. Orientation here is
|
|
773
|
+
// whatever the UA's `image-orientation` default is — see decodeImage.
|
|
774
|
+
const loadImage = (source) => new Promise((resolve, reject) => {
|
|
775
|
+
const objectUrl = URL.createObjectURL(source);
|
|
776
|
+
const image = new Image();
|
|
777
|
+
image.onload = () => {
|
|
778
|
+
URL.revokeObjectURL(objectUrl);
|
|
779
|
+
resolve(image);
|
|
780
|
+
};
|
|
781
|
+
image.onerror = () => {
|
|
782
|
+
URL.revokeObjectURL(objectUrl);
|
|
783
|
+
reject(new Error('Image could not be decoded'));
|
|
784
|
+
};
|
|
785
|
+
image.src = objectUrl;
|
|
786
|
+
});
|
|
787
|
+
// createImageBitmap pins EXIF orientation explicitly, so a portrait phone photo
|
|
788
|
+
// is embedded upright even on engines that predate the
|
|
789
|
+
// `image-orientation: from-image` default (older iOS Safari, the embedded
|
|
790
|
+
// Android WebViews storefront traffic includes) — a sideways embed degrades
|
|
791
|
+
// retrieval with no error to show for it. It also skips the full-size <img>
|
|
792
|
+
// decode. So it goes first, and the <img> path catches everything it can't do:
|
|
793
|
+
// engines that lack the API, and the sources it rejects outright but <img>
|
|
794
|
+
// renders (an SVG with no intrinsic size is the one that shows up).
|
|
795
|
+
// A very large source can still yield a blank canvas on old iOS; if blank
|
|
796
|
+
// embeds are ever observed, add a source pixel-count ceiling here.
|
|
797
|
+
const decodeImage = async (source) => {
|
|
798
|
+
if (typeof createImageBitmap === 'function') {
|
|
799
|
+
try {
|
|
800
|
+
const bitmap = await createImageBitmap(source, {
|
|
801
|
+
imageOrientation: 'from-image',
|
|
802
|
+
});
|
|
803
|
+
return {
|
|
804
|
+
image: bitmap,
|
|
805
|
+
width: bitmap.width,
|
|
806
|
+
height: bitmap.height,
|
|
807
|
+
// Phone memory doesn't wait for GC to reclaim a decoded bitmap.
|
|
808
|
+
release: () => bitmap.close(),
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
catch {
|
|
812
|
+
// Fall through: a rejection here means "this decoder can't", not "this
|
|
813
|
+
// isn't an image". The legacy path decides, and only its failure is the
|
|
814
|
+
// shopper's error.
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
const image = await loadImage(source);
|
|
818
|
+
return {
|
|
819
|
+
image,
|
|
820
|
+
width: image.naturalWidth,
|
|
821
|
+
height: image.naturalHeight,
|
|
822
|
+
release: () => undefined,
|
|
823
|
+
};
|
|
824
|
+
};
|
|
825
|
+
// Draw `image` — or just `region` of it — into a fresh canvas whose longest
|
|
826
|
+
// edge is at most `maxEdge`, on a white ground. A cropped region is scaled to
|
|
827
|
+
// the same edge cap as the whole frame, so a selection is embedded at the
|
|
828
|
+
// resolution the index renditions were built at rather than at whatever
|
|
829
|
+
// fraction of the frame it happened to occupy.
|
|
830
|
+
const renderToCanvas = (image, sourceWidth, sourceHeight, maxEdge, region) => {
|
|
831
|
+
const sx = region ? Math.round(region.x * sourceWidth) : 0;
|
|
832
|
+
const sy = region ? Math.round(region.y * sourceHeight) : 0;
|
|
833
|
+
const sw = region
|
|
834
|
+
? Math.max(1, Math.round(region.w * sourceWidth))
|
|
835
|
+
: sourceWidth;
|
|
836
|
+
const sh = region
|
|
837
|
+
? Math.max(1, Math.round(region.h * sourceHeight))
|
|
838
|
+
: sourceHeight;
|
|
839
|
+
const longestEdge = Math.max(sw, sh);
|
|
840
|
+
const scale = longestEdge > maxEdge ? maxEdge / longestEdge : 1;
|
|
841
|
+
const canvas = document.createElement('canvas');
|
|
842
|
+
canvas.width = Math.max(1, Math.round(sw * scale));
|
|
843
|
+
canvas.height = Math.max(1, Math.round(sh * scale));
|
|
844
|
+
const context = canvas.getContext('2d');
|
|
845
|
+
if (!context) {
|
|
846
|
+
throw new Error('Canvas is unavailable');
|
|
847
|
+
}
|
|
848
|
+
// JPEG has no alpha, so a transparent source (a PNG/WebP cutout, the common
|
|
849
|
+
// paste case) would otherwise composite onto the canvas's transparent-black
|
|
850
|
+
// default and be embedded as a black-backed product. Catalog shots are
|
|
851
|
+
// typically white-background, so white is the closer prior.
|
|
852
|
+
context.fillStyle = '#ffffff';
|
|
853
|
+
context.fillRect(0, 0, canvas.width, canvas.height);
|
|
854
|
+
if (region) {
|
|
855
|
+
context.drawImage(image, sx, sy, sw, sh, 0, 0, canvas.width, canvas.height);
|
|
856
|
+
}
|
|
857
|
+
else {
|
|
858
|
+
context.drawImage(image, 0, 0, canvas.width, canvas.height);
|
|
859
|
+
}
|
|
860
|
+
return canvas;
|
|
861
|
+
};
|
|
862
|
+
/**
|
|
863
|
+
* Downscale `source` to a JPEG data URI whose longest edge is at most
|
|
864
|
+
* `MAX_IMAGE_EDGE`, upright regardless of the source's EXIF orientation.
|
|
865
|
+
* Rejects anything that isn't an image, and anything the browser can't decode
|
|
866
|
+
* (HEIC off iOS, corrupt files) — every input path (file picker, drop, paste) is
|
|
867
|
+
* shopper-controlled, so this is the trust boundary that keeps a stray PDF or
|
|
868
|
+
* text drop off the wire.
|
|
869
|
+
*/
|
|
870
|
+
const downscaleImage = async (source) => {
|
|
871
|
+
if (!source.type.startsWith('image/')) {
|
|
872
|
+
throw new Error('Only image files can be searched');
|
|
873
|
+
}
|
|
874
|
+
const { image, width, height, release } = await decodeImage(source);
|
|
875
|
+
try {
|
|
876
|
+
return renderToCanvas(image, width, height, MAX_IMAGE_EDGE).toDataURL('image/jpeg', JPEG_QUALITY);
|
|
877
|
+
}
|
|
878
|
+
finally {
|
|
879
|
+
release();
|
|
880
|
+
}
|
|
881
|
+
};
|
|
882
|
+
|
|
746
883
|
// Ask can't reuse withSearchResilience: a half-consumed stream isn't retryable.
|
|
747
884
|
// Idle timeout resets per chunk; both reject with TimeoutError so submitAsk
|
|
748
885
|
// routes them to the search fallback rather than treating them as cancellation.
|
|
@@ -792,6 +929,11 @@ const createEvoSearchAPI = ({ fetch: fetchImpl = window.fetch, baseURL, userId,
|
|
|
792
929
|
headers.set('EdgeTagSessionId', sessionId);
|
|
793
930
|
}
|
|
794
931
|
headers.set('X-EvoSearch-Channel', getChannel());
|
|
932
|
+
// Only sent when the merchant is actually previewing; these routes preflight
|
|
933
|
+
// anyway, so the extra header costs no round-trip.
|
|
934
|
+
if (hasPreviewKey()) {
|
|
935
|
+
headers.set('X-EvoSearch-Preview', '1');
|
|
936
|
+
}
|
|
795
937
|
return headers;
|
|
796
938
|
};
|
|
797
939
|
// Identity for the public read endpoints travels as query params instead of
|
|
@@ -806,6 +948,12 @@ const createEvoSearchAPI = ({ fetch: fetchImpl = window.fetch, baseURL, userId,
|
|
|
806
948
|
url.searchParams.set('sid', sessionId);
|
|
807
949
|
}
|
|
808
950
|
url.searchParams.set('channel', getChannel());
|
|
951
|
+
// Emitted only when previewing: a live-traffic search URL has to stay
|
|
952
|
+
// byte-identical to today's or the edge cache fragments and the hit rate
|
|
953
|
+
// regresses on the per-keystroke hot path.
|
|
954
|
+
if (hasPreviewKey()) {
|
|
955
|
+
url.searchParams.set('preview', '1');
|
|
956
|
+
}
|
|
809
957
|
};
|
|
810
958
|
// Parsing `baseURL` once at API construction avoids re-parsing the host on
|
|
811
959
|
// every keystroke when `getURL` runs inside the autocomplete hot path.
|
|
@@ -918,7 +1066,7 @@ const createEvoSearchAPI = ({ fetch: fetchImpl = window.fetch, baseURL, userId,
|
|
|
918
1066
|
const clearRecents = () => sendMutation('/user/recents', 'ClearRecents', 'DELETE');
|
|
919
1067
|
// Resilience lives here, not at each call site, so no consumer can opt out —
|
|
920
1068
|
// the headless channel previously had no deadline and no retry.
|
|
921
|
-
const search = async (query, limit =
|
|
1069
|
+
const search = async (query, limit = 24, signal) => {
|
|
922
1070
|
logger.info(`EvoSearch API: Search request - query length: ${query.length}, limit: ${limit}`);
|
|
923
1071
|
const data = await withSearchResilience((attemptSignal) => getJSON('/search', 'Search', { q: query, limit: String(limit) }, attemptSignal),
|
|
924
1072
|
// Shared stand-in is safe: the wrapper removes its abort listener per attempt.
|
|
@@ -926,6 +1074,62 @@ const createEvoSearchAPI = ({ fetch: fetchImpl = window.fetch, baseURL, userId,
|
|
|
926
1074
|
logger.info(`EvoSearch API: Search success - ${data.llmSuggestions.length} suggestions, ${data.products.length} products`);
|
|
927
1075
|
return data;
|
|
928
1076
|
};
|
|
1077
|
+
// Image search. Unlike `/search` there is no response cache on the worker
|
|
1078
|
+
// side and every call costs a multimodal embedding, so callers must debounce
|
|
1079
|
+
// and abort rather than fire per keystroke. A 403 `{disabled:true}` means the
|
|
1080
|
+
// merchant turned visual search off — surfaced as `disabled` on the thrown
|
|
1081
|
+
// error so the widget can hide the affordance instead of showing an error.
|
|
1082
|
+
//
|
|
1083
|
+
// A File/Blob (the common case headlessly: straight off an `<input
|
|
1084
|
+
// type="file">`) is downscaled and JPEG-encoded here, which is both the size
|
|
1085
|
+
// cap the worker enforces and the trust boundary that keeps a non-image off
|
|
1086
|
+
// the wire. A string is assumed to be a prepared data URI and passes through.
|
|
1087
|
+
const searchImage = async (image, text, limit, signal) => {
|
|
1088
|
+
var _a;
|
|
1089
|
+
const dataUri = typeof image === 'string' ? image : await downscaleImage(image);
|
|
1090
|
+
logger.info(`EvoSearch API: Image search request - text length: ${(_a = text === null || text === void 0 ? void 0 : text.length) !== null && _a !== void 0 ? _a : 0}`);
|
|
1091
|
+
const headers = getHeaders();
|
|
1092
|
+
headers.set('Content-Type', 'application/json');
|
|
1093
|
+
// The widget and the hook wrap this in a no-retry resilience budget; a raw
|
|
1094
|
+
// consumer that passes no signal gets the same deadline here rather than a
|
|
1095
|
+
// request that can hang indefinitely.
|
|
1096
|
+
const controller = new AbortController();
|
|
1097
|
+
const onCallerAbort = () => controller.abort();
|
|
1098
|
+
signal === null || signal === void 0 ? void 0 : signal.addEventListener('abort', onCallerAbort);
|
|
1099
|
+
if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
|
|
1100
|
+
controller.abort();
|
|
1101
|
+
}
|
|
1102
|
+
const timer = setTimeout(() => controller.abort(), IMAGE_SEARCH_TIMEOUT_MS);
|
|
1103
|
+
let response;
|
|
1104
|
+
try {
|
|
1105
|
+
response = await fetchImpl(getURL('/search/image'), {
|
|
1106
|
+
method: 'POST',
|
|
1107
|
+
headers,
|
|
1108
|
+
body: JSON.stringify({
|
|
1109
|
+
image: dataUri,
|
|
1110
|
+
...(text ? { text } : {}),
|
|
1111
|
+
...(limit === undefined ? {} : { limit }),
|
|
1112
|
+
}),
|
|
1113
|
+
credentials: 'omit',
|
|
1114
|
+
signal: controller.signal,
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1117
|
+
finally {
|
|
1118
|
+
clearTimeout(timer);
|
|
1119
|
+
signal === null || signal === void 0 ? void 0 : signal.removeEventListener('abort', onCallerAbort);
|
|
1120
|
+
}
|
|
1121
|
+
if (!response.ok) {
|
|
1122
|
+
const errorText = await response.text().catch(() => '');
|
|
1123
|
+
logger.error(`EvoSearch API: ImageSearch failed - ${response.status}: ${errorText}`);
|
|
1124
|
+
const error = new Error(`ImageSearch failed - ${response.status}`);
|
|
1125
|
+
error.status = response.status;
|
|
1126
|
+
if (response.status === 403 && isDisabledBody(errorText)) {
|
|
1127
|
+
error.disabled = true;
|
|
1128
|
+
}
|
|
1129
|
+
throw error;
|
|
1130
|
+
}
|
|
1131
|
+
return (await response.json());
|
|
1132
|
+
};
|
|
929
1133
|
// SSE consumer for Ask AI. Decodes `event: <type>\ndata: <json>` frames
|
|
930
1134
|
// from the worker stream and yields typed events as they arrive. Caller
|
|
931
1135
|
// aborts via the AbortSignal — fetch teardown closes the underlying
|
|
@@ -1091,10 +1295,19 @@ const createEvoSearchAPI = ({ fetch: fetchImpl = window.fetch, baseURL, userId,
|
|
|
1091
1295
|
clearRecents,
|
|
1092
1296
|
},
|
|
1093
1297
|
search,
|
|
1298
|
+
searchImage,
|
|
1094
1299
|
askStream,
|
|
1095
1300
|
sendEvent,
|
|
1096
1301
|
};
|
|
1097
1302
|
};
|
|
1303
|
+
const isDisabledBody = (body) => {
|
|
1304
|
+
try {
|
|
1305
|
+
return JSON.parse(body).disabled === true;
|
|
1306
|
+
}
|
|
1307
|
+
catch {
|
|
1308
|
+
return false;
|
|
1309
|
+
}
|
|
1310
|
+
};
|
|
1098
1311
|
const parseSseFrame = (frame) => {
|
|
1099
1312
|
let eventType = '';
|
|
1100
1313
|
const dataLines = [];
|
|
@@ -1161,8 +1374,10 @@ const init = (params) => {
|
|
|
1161
1374
|
// A re-init can land on a different channel, or bail out entirely, so drop
|
|
1162
1375
|
// any previous cadence before deciding which one this run owns.
|
|
1163
1376
|
disposeKeepWarm === null || disposeKeepWarm === void 0 ? void 0 : disposeKeepWarm();
|
|
1164
|
-
|
|
1377
|
+
const evsParam = new URLSearchParams(window.location.search).get('evs');
|
|
1378
|
+
if (evsParam === 'preview') {
|
|
1165
1379
|
try {
|
|
1380
|
+
sessionStorage.removeItem(disableKeyName);
|
|
1166
1381
|
sessionStorage.setItem(previewKeyName, '1');
|
|
1167
1382
|
logger.info('EvoSearch SDK: Preview mode enabled via ?evs=preview URL param');
|
|
1168
1383
|
}
|
|
@@ -1170,11 +1385,32 @@ const init = (params) => {
|
|
|
1170
1385
|
error('EvoSearch SDK: Failed to set previewEvoSearch sessionStorage');
|
|
1171
1386
|
}
|
|
1172
1387
|
}
|
|
1388
|
+
else if (evsParam === 'off') {
|
|
1389
|
+
try {
|
|
1390
|
+
sessionStorage.removeItem(previewKeyName);
|
|
1391
|
+
sessionStorage.setItem(disableKeyName, '1');
|
|
1392
|
+
logger.info('EvoSearch SDK: Disabled via ?evs=off URL param');
|
|
1393
|
+
}
|
|
1394
|
+
catch {
|
|
1395
|
+
error('EvoSearch SDK: Failed to set disableEvoSearch sessionStorage');
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
else if (evsParam === 'reset') {
|
|
1399
|
+
try {
|
|
1400
|
+
sessionStorage.removeItem(previewKeyName);
|
|
1401
|
+
sessionStorage.removeItem(disableKeyName);
|
|
1402
|
+
logger.info('EvoSearch SDK: Reset to configured state via ?evs=reset URL param');
|
|
1403
|
+
}
|
|
1404
|
+
catch {
|
|
1405
|
+
error('EvoSearch SDK: Failed to clear evoSearch sessionStorage');
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1173
1408
|
const { enabled, uiPreferences, askAi } = (_c = (_b = params.manifest) === null || _b === void 0 ? void 0 : _b.variables) !== null && _c !== void 0 ? _c : {};
|
|
1174
1409
|
const mode = uiPreferences === null || uiPreferences === void 0 ? void 0 : uiPreferences.mode;
|
|
1175
1410
|
const askAiEnabled = (askAi === null || askAi === void 0 ? void 0 : askAi.enabled) === true;
|
|
1411
|
+
const visualSearchEnabled = (uiPreferences === null || uiPreferences === void 0 ? void 0 : uiPreferences.visualSearch) === true;
|
|
1176
1412
|
const hasPreview = hasPreviewKey();
|
|
1177
|
-
const shouldEnable = enabled || hasPreview;
|
|
1413
|
+
const shouldEnable = !hasDisableKey() && (enabled || hasPreview);
|
|
1178
1414
|
if (!shouldEnable) {
|
|
1179
1415
|
logger.info('EvoSearch SDK: Mode is disabled, skipping initialization');
|
|
1180
1416
|
return;
|
|
@@ -1222,6 +1458,7 @@ const init = (params) => {
|
|
|
1222
1458
|
api: evoSearchAPI,
|
|
1223
1459
|
uiPreferences,
|
|
1224
1460
|
askAiEnabled,
|
|
1461
|
+
visualSearchEnabled,
|
|
1225
1462
|
});
|
|
1226
1463
|
params.sendTag({
|
|
1227
1464
|
eventId: crypto.randomUUID(),
|
|
@@ -1244,7 +1481,7 @@ const init = (params) => {
|
|
|
1244
1481
|
const tag = ({ data }) => {
|
|
1245
1482
|
try {
|
|
1246
1483
|
return {
|
|
1247
|
-
sdkVersion: "1.
|
|
1484
|
+
sdkVersion: "1.68.0" || '',
|
|
1248
1485
|
searchQuery: data['searchQuery'],
|
|
1249
1486
|
searchResults: data['searchResults'],
|
|
1250
1487
|
timestamp: Date.now(),
|
|
@@ -1253,7 +1490,7 @@ const tag = ({ data }) => {
|
|
|
1253
1490
|
catch (err) {
|
|
1254
1491
|
console.error(`EvoSearch SDK: Tagging failed - ${err}`);
|
|
1255
1492
|
return {
|
|
1256
|
-
sdkVersion: "1.
|
|
1493
|
+
sdkVersion: "1.68.0" ,
|
|
1257
1494
|
timestamp: Date.now(),
|
|
1258
1495
|
};
|
|
1259
1496
|
}
|