@athoscommerce/snap-store-mobx 1.5.1-beta.114 → 1.5.1

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.
Files changed (51) hide show
  1. package/dist/cjs/Autocomplete/Stores/AutocompleteFacetStore.js +1 -2
  2. package/dist/cjs/Search/Stores/SearchFacetStore.d.ts.map +1 -1
  3. package/dist/cjs/Search/Stores/SearchFacetStore.js +10 -13
  4. package/dist/cjs/Search/Stores/SearchResultStore.d.ts.map +1 -1
  5. package/dist/cjs/Search/Stores/SearchResultStore.js +7 -6
  6. package/dist/cjs/Search/Stores/index.d.ts +1 -1
  7. package/dist/cjs/Search/Stores/index.d.ts.map +1 -1
  8. package/dist/cjs/Search/Stores/index.js +1 -2
  9. package/dist/cjs/index.d.ts +0 -4
  10. package/dist/cjs/index.d.ts.map +1 -1
  11. package/dist/cjs/index.js +1 -5
  12. package/dist/cjs/types.d.ts +2 -24
  13. package/dist/cjs/types.d.ts.map +1 -1
  14. package/dist/esm/Autocomplete/Stores/AutocompleteFacetStore.js +1 -1
  15. package/dist/esm/Search/Stores/SearchFacetStore.d.ts.map +1 -1
  16. package/dist/esm/Search/Stores/SearchFacetStore.js +10 -13
  17. package/dist/esm/Search/Stores/SearchResultStore.d.ts.map +1 -1
  18. package/dist/esm/Search/Stores/SearchResultStore.js +7 -6
  19. package/dist/esm/Search/Stores/index.d.ts +1 -1
  20. package/dist/esm/Search/Stores/index.d.ts.map +1 -1
  21. package/dist/esm/Search/Stores/index.js +1 -1
  22. package/dist/esm/index.d.ts +0 -4
  23. package/dist/esm/index.d.ts.map +1 -1
  24. package/dist/esm/index.js +0 -2
  25. package/dist/esm/types.d.ts +2 -24
  26. package/dist/esm/types.d.ts.map +1 -1
  27. package/package.json +5 -5
  28. package/dist/cjs/Chat/ChatStore.d.ts +0 -91
  29. package/dist/cjs/Chat/ChatStore.d.ts.map +0 -1
  30. package/dist/cjs/Chat/ChatStore.js +0 -638
  31. package/dist/cjs/Chat/Stores/ChatAttachmentStore.d.ts +0 -98
  32. package/dist/cjs/Chat/Stores/ChatAttachmentStore.d.ts.map +0 -1
  33. package/dist/cjs/Chat/Stores/ChatAttachmentStore.js +0 -314
  34. package/dist/cjs/Chat/Stores/ChatCompareStore.d.ts +0 -16
  35. package/dist/cjs/Chat/Stores/ChatCompareStore.d.ts.map +0 -1
  36. package/dist/cjs/Chat/Stores/ChatCompareStore.js +0 -64
  37. package/dist/cjs/Chat/Stores/ChatSessionStore.d.ts +0 -107
  38. package/dist/cjs/Chat/Stores/ChatSessionStore.d.ts.map +0 -1
  39. package/dist/cjs/Chat/Stores/ChatSessionStore.js +0 -644
  40. package/dist/esm/Chat/ChatStore.d.ts +0 -91
  41. package/dist/esm/Chat/ChatStore.d.ts.map +0 -1
  42. package/dist/esm/Chat/ChatStore.js +0 -568
  43. package/dist/esm/Chat/Stores/ChatAttachmentStore.d.ts +0 -98
  44. package/dist/esm/Chat/Stores/ChatAttachmentStore.d.ts.map +0 -1
  45. package/dist/esm/Chat/Stores/ChatAttachmentStore.js +0 -222
  46. package/dist/esm/Chat/Stores/ChatCompareStore.d.ts +0 -16
  47. package/dist/esm/Chat/Stores/ChatCompareStore.d.ts.map +0 -1
  48. package/dist/esm/Chat/Stores/ChatCompareStore.js +0 -50
  49. package/dist/esm/Chat/Stores/ChatSessionStore.d.ts +0 -107
  50. package/dist/esm/Chat/Stores/ChatSessionStore.d.ts.map +0 -1
  51. package/dist/esm/Chat/Stores/ChatSessionStore.js +0 -609
@@ -1,91 +0,0 @@
1
- import { ChatStoreConfig, StoreServices } from '../types';
2
- import { MetaStore } from '../Meta/MetaStore';
3
- import type { MetaResponseModel, SearchResponseModelFacet } from '@athoscommerce/snapi-types';
4
- import { AbstractStore } from '../Abstract/AbstractStore';
5
- import type { ChatResponseModel, ChatRequestModel, ProductsResponseModel } from '@athoscommerce/snap-client';
6
- import type { UrlManager } from '@athoscommerce/snap-url-manager';
7
- import { StorageStore } from '@athoscommerce/snap-toolbox';
8
- import { ChatSessionStore } from './Stores/ChatSessionStore';
9
- import { ChatStatusResponse } from '@athoscommerce/snap-client';
10
- import { Product } from '../Search/Stores/SearchResultStore';
11
- import { SearchFacetStore } from '../Search/Stores/SearchFacetStore';
12
- export declare class ChatStore extends AbstractStore<ChatStoreConfig> {
13
- meta?: MetaStore;
14
- inputValue: string;
15
- open: boolean;
16
- storage: StorageStore;
17
- services: StoreServices;
18
- /** Detached UrlManager — holds the in-progress facet selection for the active facets display.
19
- * Detached so .go() updates state without navigating the page. */
20
- urlManager: UrlManager;
21
- /** The single facet store rendered in chat — backed by the active message's facets. */
22
- facets: SearchFacetStore;
23
- chats: ChatSessionStore[];
24
- currentChatId: string;
25
- chatEnabled: boolean | null;
26
- initChatLoading: boolean;
27
- suggestedQuestions: string[];
28
- welcomeMessage: string;
29
- features: ChatStatusResponse['features'];
30
- productQuickview: Product | null;
31
- productQuickviewError: string | null;
32
- /** Raw meta kept for lazy hydration of inactive chat sessions. */
33
- private storedMetaData;
34
- /** Tracks which message currently owns the displayed facets — guards against redundant rebuilds. */
35
- private activeFacetsMessageId;
36
- /** Bumps on every detached-urlManager change so observers re-evaluate isFacetSelected. */
37
- urlVersion: number;
38
- /** Snapshot of the applied filter state captured the last time the active message was seeded.
39
- * Compared against the live urlManager state to decide whether there are pending changes.
40
- * `null` means no facets have been seeded yet — there are no pending changes in that case. */
41
- private appliedFilterSnapshot;
42
- constructor(config: ChatStoreConfig, services: StoreServices);
43
- /** Build a SearchFacetStore from raw facet data using the current detached urlManager.
44
- * Synthesizes meta entries for each facet so SearchFacetStore's display/meta filter
45
- * doesn't drop chat facets (the chat backend already decided what to send). */
46
- private buildFacetStore;
47
- /** Reset the detached urlManager and seed it with this message's filtered facet values,
48
- * then rebuild the root SearchFacetStore so the UI shows the matching display. */
49
- setActiveFacets(facets: SearchResponseModelFacet[], messageId: string | null): void;
50
- /** Total number of selected facet values in the in-progress urlManager state, summed
51
- * across all filter fields. Each value in a value facet, each range in a range-bucket
52
- * facet, and the single range on a slider facet each count as one. */
53
- get pendingFacetCount(): number;
54
- /** True when the in-progress facet selection differs from the filters that were applied
55
- * by the active productSearchResult — i.e. the user has selected/removed something that
56
- * hasn't been sent yet. Used to decide whether to show the Apply button. */
57
- get hasPendingFacetChanges(): boolean;
58
- get currentChat(): ChatSessionStore | undefined;
59
- get chatsIds(): string[];
60
- get blocked(): boolean;
61
- private applyChatStatusResponse;
62
- handleChatStatusResponse(response: ChatStatusResponse): boolean;
63
- reset(): void;
64
- setProductQuickview(product: Product): void;
65
- updateProductQuickview(response: ProductsResponseModel): void;
66
- setProductQuickviewError(message: string): void;
67
- clearProductQuickview(): void;
68
- clearHistory(): void;
69
- createChat(data?: {
70
- sessionId: string;
71
- sessionEndTime?: Date;
72
- }): ChatSessionStore;
73
- switchChat(id: string): void;
74
- sendProductQuery(result: any, options: {
75
- requestType: 'productQuery' | 'productSimilar' | 'productComparison';
76
- }): void;
77
- compareProduct(result: any): void;
78
- addFacet(facet: {
79
- key: string;
80
- value: string;
81
- }): void;
82
- removeFacet(key: string, value: string): void;
83
- clearPendingFacets(): void;
84
- isFacetSelected(key: string, value: string): boolean;
85
- request(request: ChatRequestModel): void;
86
- update(data: {
87
- chat: ChatResponseModel;
88
- meta: MetaResponseModel;
89
- }): void;
90
- }
91
- //# sourceMappingURL=ChatStore.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"ChatStore.d.ts","sourceRoot":"","sources":["../../../src/Chat/ChatStore.ts"],"names":[],"mappings":"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"}
@@ -1,568 +0,0 @@
1
- import { makeObservable, observable, computed, reaction } from 'mobx';
2
- import { MetaStore } from '../Meta/MetaStore';
3
- import { AbstractStore } from '../Abstract/AbstractStore';
4
- import { StorageStore } from '@athoscommerce/snap-toolbox';
5
- import { ChatSessionStore } from './Stores/ChatSessionStore';
6
- import { Product, Variants } from '../Search/Stores/SearchResultStore';
7
- import { SearchFacetStore } from '../Search/Stores/SearchFacetStore';
8
- const CHAT_STATUS_EXPIRATION_TIME = 1000 * 60 * 60 * 12; // 12 hours
9
- export class ChatStore extends AbstractStore {
10
- constructor(config, services) {
11
- super(config);
12
- this.meta = undefined;
13
- this.inputValue = '';
14
- this.open = false;
15
- this.chats = [];
16
- this.chatEnabled = null;
17
- this.initChatLoading = false;
18
- this.suggestedQuestions = [];
19
- this.welcomeMessage = '';
20
- this.features = { imageSearch: { enabled: false }, similarProducts: { enabled: false } };
21
- this.productQuickview = null;
22
- this.productQuickviewError = null;
23
- /** Raw meta kept for lazy hydration of inactive chat sessions. */
24
- this.storedMetaData = null;
25
- /** Tracks which message currently owns the displayed facets — guards against redundant rebuilds. */
26
- this.activeFacetsMessageId = null;
27
- /** Bumps on every detached-urlManager change so observers re-evaluate isFacetSelected. */
28
- this.urlVersion = 0;
29
- /** Snapshot of the applied filter state captured the last time the active message was seeded.
30
- * Compared against the live urlManager state to decide whether there are pending changes.
31
- * `null` means no facets have been seeded yet — there are no pending changes in that case. */
32
- this.appliedFilterSnapshot = null;
33
- this.services = services;
34
- this.urlManager = services.urlManager.detach(true);
35
- this.urlManager.subscribe(() => {
36
- this.urlVersion++;
37
- });
38
- const legacyKey = `ss-${this.config.id}`;
39
- const storageKey = this.config.siteId ? `ss-${this.config.siteId}-${this.config.id}` : legacyKey;
40
- this.storage = new StorageStore({
41
- type: 'local',
42
- key: storageKey,
43
- });
44
- this.facets = this.buildFacetStore();
45
- const storedChatStatusResponse = this.storage.get('chatStatusResponse');
46
- if (storedChatStatusResponse) {
47
- try {
48
- const storedChatStatus = JSON.parse(storedChatStatusResponse);
49
- if (storedChatStatus.checkTime && Date.now() - storedChatStatus.checkTime > CHAT_STATUS_EXPIRATION_TIME) {
50
- // chat status is expired, remove from storage to trigger a new check
51
- this.storage.set('chatStatusResponse', null);
52
- }
53
- else {
54
- // Apply the stored response without persisting, so the 12-hour
55
- // checkTime keeps counting from the last real API call rather
56
- // than resetting on every page load.
57
- this.applyChatStatusResponse(storedChatStatus.response);
58
- }
59
- }
60
- catch {
61
- this.storage.set('chatStatusResponse', null);
62
- }
63
- }
64
- // check for entries in storage
65
- let latestChatId = '';
66
- const storedChats = this.storage.get('chats');
67
- storedChats &&
68
- Object.keys(storedChats || {}).forEach((chatId) => {
69
- const chatData = storedChats[chatId];
70
- if (chatData) {
71
- const restoredChat = new ChatSessionStore({
72
- data: {
73
- ...chatData,
74
- id: chatId,
75
- },
76
- stores: {
77
- storage: this.storage,
78
- },
79
- });
80
- // Mark as unhydrated — results are still raw JSON from storage.
81
- // Only the active session will be hydrated below.
82
- restoredChat.hydrated = false;
83
- this.chats.push(restoredChat);
84
- latestChatId = chatId;
85
- }
86
- });
87
- // Prefer the persisted active chat ID so switching chats survives page reloads.
88
- // Fall back to the most recently created chat if the stored ID is missing or stale.
89
- const storedCurrentChatId = this.storage.get('currentChatId');
90
- const activeChatId = storedCurrentChatId && this.chats.some((chat) => chat.id === storedCurrentChatId) ? storedCurrentChatId : latestChatId;
91
- const storedMeta = this.storage.get('meta');
92
- if (storedMeta) {
93
- try {
94
- const metaData = JSON.parse(storedMeta);
95
- this.meta = new MetaStore({
96
- data: {
97
- meta: metaData,
98
- },
99
- });
100
- // Keep raw meta for lazy hydration of inactive sessions
101
- this.storedMetaData = metaData;
102
- // Only hydrate the active chat session — inactive sessions will be
103
- // hydrated lazily when switched to via switchChat()
104
- const activeChat = this.chats.find((chat) => chat.id === activeChatId);
105
- if (activeChat) {
106
- activeChat.hydrateResults(metaData);
107
- activeChat.hydrated = true;
108
- }
109
- }
110
- catch {
111
- this.storage.set('meta', null);
112
- }
113
- }
114
- this.currentChatId = activeChatId;
115
- makeObservable(this, {
116
- meta: observable,
117
- inputValue: observable,
118
- open: observable,
119
- chats: observable,
120
- currentChatId: observable,
121
- chatEnabled: observable,
122
- initChatLoading: observable,
123
- blocked: computed,
124
- currentChat: computed,
125
- chatsIds: computed,
126
- features: observable,
127
- suggestedQuestions: observable,
128
- welcomeMessage: observable,
129
- productQuickview: observable,
130
- productQuickviewError: observable,
131
- facets: observable,
132
- urlVersion: observable,
133
- hasPendingFacetChanges: computed,
134
- pendingFacetCount: computed,
135
- });
136
- // Sync the root facets display with the active message. Only productSearchResult
137
- // messages carry facets — for any other active message, clear the display so a
138
- // stale facet bar from a previous response doesn't linger.
139
- // Exception: when the user clicks "discuss product" on a carousel result, the
140
- // active message becomes the productQuery side-chat, but the carousel is still
141
- // the last real message — keep its facets visible alongside the product context.
142
- reaction(() => {
143
- const chat = this.currentChat;
144
- const active = chat?.activeMessage;
145
- if (active?.messageType === 'productSearchResult')
146
- return active;
147
- if (active?.messageType === 'productQuery' && chat) {
148
- const messages = chat.chat;
149
- const activeIdx = messages.findIndex((m) => m.id === active.id);
150
- if (activeIdx > 0 && messages[activeIdx - 1].messageType === 'productSearchResult') {
151
- return messages[activeIdx - 1];
152
- }
153
- }
154
- return null;
155
- }, (active) => {
156
- if (active && active.id !== this.activeFacetsMessageId) {
157
- const facets = active.facets;
158
- this.setActiveFacets(facets || [], active.id);
159
- }
160
- else if (!active && this.activeFacetsMessageId !== null) {
161
- this.setActiveFacets([], null);
162
- }
163
- }, { fireImmediately: true });
164
- // Mirror the urlManager's pending range filters into each RangeFacet's `active`.
165
- // Slider facets are remounted whenever their dropdown closes/reopens and the
166
- // FacetSlider seeds its state from facet.active — without this sync the slider
167
- // would snap back to the server-provided active on every reopen, losing the
168
- // user's pending selection until Apply is clicked.
169
- // Note: range filters round-trip through the detached urlManager's URL form,
170
- // so they come back as `[{low, high}]` arrays even though they were set as objects.
171
- const readRangeFilter = (raw) => {
172
- const entry = Array.isArray(raw) ? raw[raw.length - 1] : raw;
173
- if (!entry || typeof entry !== 'object')
174
- return null;
175
- if (typeof entry.low === 'undefined' && typeof entry.high === 'undefined')
176
- return null;
177
- return {
178
- low: entry.low === null || typeof entry.low === 'undefined' ? undefined : Number(entry.low),
179
- high: entry.high === null || typeof entry.high === 'undefined' ? undefined : Number(entry.high),
180
- };
181
- };
182
- reaction(() => {
183
- void this.urlVersion;
184
- const filterState = this.urlManager.state?.filter || {};
185
- return this.facets
186
- .filter((f) => f.type === 'range')
187
- .map((f) => {
188
- const pending = readRangeFilter(filterState[f.field]);
189
- return `${f.field}:${pending?.low ?? '*'}:${pending?.high ?? '*'}`;
190
- })
191
- .join('|');
192
- }, () => {
193
- const filterState = this.urlManager.state?.filter || {};
194
- for (const facet of this.facets) {
195
- if (facet.type !== 'range')
196
- continue;
197
- const rangeFacet = facet;
198
- const pending = readRangeFilter(filterState[rangeFacet.field]);
199
- const desired = pending
200
- ? {
201
- low: typeof pending.low !== 'undefined' ? pending.low : rangeFacet.range?.low,
202
- high: typeof pending.high !== 'undefined' ? pending.high : rangeFacet.range?.high,
203
- }
204
- : rangeFacet.range
205
- ? { low: rangeFacet.range.low, high: rangeFacet.range.high }
206
- : null;
207
- if (!desired)
208
- continue;
209
- if (rangeFacet.active?.low !== desired.low || rangeFacet.active?.high !== desired.high) {
210
- rangeFacet.active = desired;
211
- }
212
- }
213
- }, { fireImmediately: true });
214
- }
215
- /** Build a SearchFacetStore from raw facet data using the current detached urlManager.
216
- * Synthesizes meta entries for each facet so SearchFacetStore's display/meta filter
217
- * doesn't drop chat facets (the chat backend already decided what to send). */
218
- buildFacetStore(facets = []) {
219
- const baseMeta = this.meta?.data || {};
220
- const baseFacetMeta = baseMeta.facets || {};
221
- const facetMeta = { ...baseFacetMeta };
222
- facets.forEach((facet) => {
223
- if (!facet.field)
224
- return;
225
- const expectedDisplay = facet.type === 'range' ? 'slider' : 'list';
226
- const existing = facetMeta[facet.field];
227
- if (!existing || existing.display !== expectedDisplay) {
228
- facetMeta[facet.field] = {
229
- ...(existing || {}),
230
- field: facet.field,
231
- label: facet.label,
232
- display: expectedDisplay,
233
- };
234
- }
235
- });
236
- return new SearchFacetStore({
237
- config: {},
238
- services: { ...this.services, urlManager: this.urlManager },
239
- stores: { storage: this.storage },
240
- data: {
241
- search: { facets },
242
- meta: { ...baseMeta, facets: facetMeta },
243
- },
244
- });
245
- }
246
- /** Reset the detached urlManager and seed it with this message's filtered facet values,
247
- * then rebuild the root SearchFacetStore so the UI shows the matching display. */
248
- setActiveFacets(facets, messageId) {
249
- // reset url state, then seed with the API-reported filtered values
250
- this.urlManager.reset().go();
251
- facets.forEach((facet) => {
252
- // Slider/range facets carry the active selection on the facet itself rather
253
- // than on individual bucket values.
254
- if (facet.type === 'range') {
255
- const active = facet.active;
256
- const range = facet.range;
257
- if (active && range && (active.low !== range.low || active.high !== range.high)) {
258
- this.urlManager.set(`filter.${facet.field}`, { low: active.low, high: active.high }).go();
259
- }
260
- return;
261
- }
262
- const filteredValues = (facet.values || []).filter((v) => v?.filtered);
263
- filteredValues.forEach((v) => {
264
- if (typeof v.low !== 'undefined' || typeof v.high !== 'undefined') {
265
- this.urlManager.merge(`filter.${facet.field}`, [{ low: v.low, high: v.high }]).go();
266
- }
267
- else if (typeof v.value !== 'undefined') {
268
- this.urlManager.merge(`filter.${facet.field}`, v.value).go();
269
- }
270
- });
271
- });
272
- this.facets = this.buildFacetStore(facets);
273
- this.activeFacetsMessageId = messageId;
274
- this.appliedFilterSnapshot = stableStringify(this.urlManager.state?.filter);
275
- }
276
- /** Total number of selected facet values in the in-progress urlManager state, summed
277
- * across all filter fields. Each value in a value facet, each range in a range-bucket
278
- * facet, and the single range on a slider facet each count as one. */
279
- get pendingFacetCount() {
280
- void this.urlVersion;
281
- const filterState = this.urlManager.state?.filter;
282
- if (!filterState)
283
- return 0;
284
- let count = 0;
285
- for (const key of Object.keys(filterState)) {
286
- const value = filterState[key];
287
- if (Array.isArray(value)) {
288
- count += value.length;
289
- }
290
- else if (typeof value !== 'undefined') {
291
- count += 1;
292
- }
293
- }
294
- return count;
295
- }
296
- /** True when the in-progress facet selection differs from the filters that were applied
297
- * by the active productSearchResult — i.e. the user has selected/removed something that
298
- * hasn't been sent yet. Used to decide whether to show the Apply button. */
299
- get hasPendingFacetChanges() {
300
- // touch the version so mobx re-runs this when urlManager state changes
301
- void this.urlVersion;
302
- const filterState = this.urlManager.state?.filter;
303
- if (this.appliedFilterSnapshot === null) {
304
- // No facets have been seeded — pending changes exist only if
305
- // the user has manually added filters via addFacet.
306
- return filterState !== undefined && Object.keys(filterState).length > 0;
307
- }
308
- return stableStringify(filterState) !== this.appliedFilterSnapshot;
309
- }
310
- get currentChat() {
311
- return this.chats.find((chat) => chat.id === this.currentChatId);
312
- }
313
- get chatsIds() {
314
- return this.chats.map((chat) => chat.id);
315
- }
316
- get blocked() {
317
- let isBlocked = false;
318
- const blockedAttachments = this.currentChat?.attachments.items.some((item) => item.type === 'image' && item.state === 'loading');
319
- if (this.loading || blockedAttachments) {
320
- isBlocked = true;
321
- }
322
- return isBlocked;
323
- }
324
- applyChatStatusResponse(response) {
325
- const { chatbot, features } = response;
326
- const { status, suggestedQuestions, welcomeMessage } = chatbot;
327
- this.chatEnabled = status.enabled === true;
328
- this.features = features;
329
- this.suggestedQuestions = suggestedQuestions || [];
330
- this.welcomeMessage = welcomeMessage || '';
331
- return this.chatEnabled;
332
- }
333
- handleChatStatusResponse(response) {
334
- const enabled = this.applyChatStatusResponse(response);
335
- // Always persist on a real API response — restarts the 12-hour expiration
336
- // so each new chat session refreshes the cache.
337
- this.storage.set('chatStatusResponse', JSON.stringify({ response, checkTime: Date.now() }));
338
- return enabled;
339
- }
340
- reset() {
341
- this.chats = [];
342
- this.clearProductQuickview();
343
- this.storage.clear();
344
- this.createChat();
345
- }
346
- setProductQuickview(product) {
347
- // Clone the product so variant selections (and the parent-mappings merge done
348
- // in updateProductQuickview) only affect the quickview — the carousel and any
349
- // other surfaces still hold the original instance.
350
- this.productQuickview = cloneProductForQuickview(product, this.meta?.data);
351
- this.productQuickviewError = null;
352
- }
353
- updateProductQuickview(response) {
354
- if (!this.productQuickview) {
355
- return;
356
- }
357
- // merge parent-level mappings from the API response into the Product
358
- this.productQuickview.mappings = {
359
- ...this.productQuickview.mappings,
360
- core: { ...this.productQuickview.mappings.core, ...response.mappings.core },
361
- };
362
- const meta = this.meta?.data || {};
363
- if (this.productQuickview.variants) {
364
- // update existing Variants with new data from the Products API
365
- this.productQuickview.variants.optionConfig = response.variants.optionConfig;
366
- this.productQuickview.variants.update(response.variants.data);
367
- }
368
- else {
369
- // create Variants for the first time using the Products API data
370
- this.productQuickview.variants = new Variants({
371
- data: {
372
- mask: this.productQuickview.mask,
373
- variants: response.variants.data,
374
- optionConfig: response.variants.optionConfig,
375
- meta,
376
- },
377
- });
378
- }
379
- }
380
- setProductQuickviewError(message) {
381
- this.productQuickviewError = message;
382
- }
383
- clearProductQuickview() {
384
- this.productQuickview = null;
385
- this.productQuickviewError = null;
386
- }
387
- clearHistory() {
388
- // drop previous chats while keeping the conversation the user is currently in;
389
- // if there is no current chat, fall back to a full reset
390
- const currentChat = this.currentChat;
391
- if (!currentChat) {
392
- this.reset();
393
- return;
394
- }
395
- const storedChats = this.storage.get('chats') || {};
396
- const filtered = {};
397
- if (storedChats[currentChat.id]) {
398
- filtered[currentChat.id] = storedChats[currentChat.id];
399
- }
400
- this.storage.set('chats', filtered);
401
- this.chats = [currentChat];
402
- }
403
- createChat(data) {
404
- // abandon any in-flight request state from the previous chat — its
405
- // response will be discarded by the controller, so the UI shouldn't
406
- // keep showing its loading spinner or stale error
407
- this.loading = false;
408
- this.error = undefined;
409
- // Prune old sessions before creating a new one to keep storage bounded
410
- ChatSessionStore.pruneStoredSessions(this.storage);
411
- const newChat = new ChatSessionStore({
412
- data: {
413
- sessionId: data?.sessionId,
414
- sessionEndTime: data?.sessionEndTime,
415
- },
416
- stores: {
417
- storage: this.storage,
418
- },
419
- });
420
- this.currentChatId = newChat.id;
421
- this.storage.set('currentChatId', newChat.id);
422
- this.chats.push(newChat);
423
- return newChat;
424
- }
425
- switchChat(id) {
426
- const chatExists = this.chats.find((chat) => chat.id === id);
427
- if (chatExists) {
428
- // Lazily hydrate results when switching to a session for the first time
429
- if (!chatExists.hydrated && this.storedMetaData) {
430
- chatExists.hydrateResults(this.storedMetaData);
431
- chatExists.hydrated = true;
432
- }
433
- this.currentChatId = id;
434
- this.storage.set('currentChatId', id);
435
- }
436
- }
437
- sendProductQuery(result, options) {
438
- const display = result?.display || result;
439
- this.currentChat?.attachments.add({
440
- type: 'product',
441
- requestType: options.requestType,
442
- productId: result.id,
443
- name: display.mappings?.core?.name,
444
- thumbnailUrl: display.mappings?.core?.thumbnailImageUrl || display.mappings?.core?.imageUrl || display.mappings?.core?.parentImageUrl,
445
- });
446
- // for the productQuery flow we want the secondary chat to immediately
447
- // show the product details panel — push a productQuery message so it
448
- // becomes the active side-chat message until a productAnswer arrives
449
- if (options.requestType === 'productQuery' && this.currentChat) {
450
- this.currentChat.pushProductQueryMessage(result);
451
- }
452
- }
453
- compareProduct(result) {
454
- this.currentChat?.comparisons.add({
455
- result,
456
- });
457
- }
458
- addFacet(facet) {
459
- const value = parseFacetValue(facet.value);
460
- this.urlManager.merge(`filter.${facet.key}`, Array.isArray(value) ? value : value).go();
461
- }
462
- removeFacet(key, value) {
463
- const parsed = parseFacetValue(value);
464
- this.urlManager.remove(`filter.${key}`, parsed).go();
465
- }
466
- clearPendingFacets() {
467
- this.urlManager.remove('filter').go();
468
- }
469
- isFacetSelected(key, value) {
470
- // touch the version so mobx re-runs this when urlManager state changes (the
471
- // underlying UrlManager isn't a mobx observable, so we mirror its updates here)
472
- void this.urlVersion;
473
- const filterState = this.urlManager.state?.filter;
474
- if (!filterState || !filterState[key])
475
- return false;
476
- const stored = Array.isArray(filterState[key]) ? filterState[key] : [filterState[key]];
477
- const parsed = parseFacetValue(value);
478
- return stored.some((entry) => facetValueEquals(entry, parsed));
479
- }
480
- request(request) {
481
- // snapshot facet labels (field + per-value) from the active facets display so the
482
- // outgoing user message can render "Filter by Color Red" rather than raw keys
483
- const filterLabels = {};
484
- for (const facet of this.facets) {
485
- const valueFacet = facet;
486
- const values = {};
487
- (valueFacet.values || []).forEach((v) => {
488
- if (typeof v.low !== 'undefined' || typeof v.high !== 'undefined') {
489
- values[`${v.low ?? '*'}:${v.high ?? '*'}`] = v.label;
490
- }
491
- else if (typeof v.value !== 'undefined') {
492
- values[v.value] = v.label;
493
- }
494
- });
495
- filterLabels[valueFacet.field] = { facetLabel: valueFacet.label || valueFacet.field, values };
496
- }
497
- this.currentChat?.request(request, filterLabels);
498
- // remove any productSimilar attachments after request
499
- const productSimilarAttachments = this.currentChat?.attachments.attached.filter((item) => item.type === 'product' && item.requestType === 'productSimilar') || [];
500
- productSimilarAttachments.forEach((item) => {
501
- this.currentChat?.attachments.remove(item.id);
502
- });
503
- }
504
- update(data) {
505
- // TODO: handle error
506
- // if(err?.responseBody?.errorMessage || err?.responseBody?.errorCode) {
507
- // const errorMessage = err?.responseBody?.errorMessage || 'An unknown error has occurred.';
508
- // }
509
- this.currentChat?.update(data);
510
- if (!this.meta) {
511
- this.storage.set('meta', JSON.stringify(data.meta));
512
- }
513
- this.meta = new MetaStore({
514
- data: {
515
- meta: data.meta,
516
- },
517
- });
518
- // Keep raw meta in sync for lazy hydration of inactive sessions
519
- this.storedMetaData = data.meta;
520
- }
521
- }
522
- /** Deterministic JSON.stringify — sorts object keys recursively so equality comparisons
523
- * aren't sensitive to insertion order. */
524
- function stableStringify(value) {
525
- if (value === null || value === undefined)
526
- return 'null';
527
- if (typeof value !== 'object')
528
- return JSON.stringify(value);
529
- if (Array.isArray(value)) {
530
- return `[${value.map((v) => stableStringify(v)).join(',')}]`;
531
- }
532
- const keys = Object.keys(value).sort();
533
- return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(value[k])}`).join(',')}}`;
534
- }
535
- /** Convert a chat-facet UI value into the URL-state shape — range buckets arrive as "low:high". */
536
- function parseFacetValue(value) {
537
- if (typeof value === 'string' && value.includes(':')) {
538
- const [low, high] = value.split(':');
539
- return {
540
- low: low === '*' ? undefined : Number(low),
541
- high: high === '*' ? undefined : Number(high),
542
- };
543
- }
544
- return value;
545
- }
546
- function facetValueEquals(a, b) {
547
- if (typeof a === 'object' && a !== null && typeof b === 'object' && b !== null) {
548
- return a.low === b.low && a.high === b.high;
549
- }
550
- return a === b;
551
- }
552
- /** Build a fresh observable Product from an existing one. Variants are intentionally
553
- * omitted — `updateProductQuickview` populates them from the products API response. */
554
- function cloneProductForQuickview(product, meta) {
555
- const result = {
556
- id: product.id,
557
- responseId: product.responseId,
558
- mappings: JSON.parse(JSON.stringify(product.mappings || {})),
559
- attributes: JSON.parse(JSON.stringify(product.attributes || {})),
560
- badges: product.badges?.all?.map((b) => ({ tag: b.tag })) || [],
561
- };
562
- return new Product({
563
- config: {},
564
- data: { result, meta: meta || {} },
565
- position: product.position ?? 0,
566
- responseId: product.responseId,
567
- });
568
- }