@gengage/assistant-fe 0.4.2 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -91,6 +91,8 @@ See [docs/native-mobile-sdk.md](docs/native-mobile-sdk.md) for iOS WKWebView, An
91
91
  | **SimBut** | `@gengage/assistant-fe/simbut` | PDP image-overlay pill that opens a `findSimilar` flow for the current product |
92
92
  | **Native** | `@gengage/assistant-fe/native` | Android/iOS WebView bridge + overlay bootstrap |
93
93
 
94
+ The chat widget includes a built-in history entry point in the header. Past chat sessions are stored per account/user in `localStorage`, while the active session restore cache continues to use IndexedDB for cross-navigation handoff flows.
95
+
94
96
  ---
95
97
 
96
98
  ## Customization
@@ -121,6 +123,7 @@ npm run koctascomtr # Merchant shortcut with the seed
121
123
  npm run trendyolcom # Another merchant shortcut using its default PDP SKU
122
124
  npm run typecheck # TypeScript strict check
123
125
  npm run test # Unit tests
126
+ npm run test:chat-history # Targeted history + restore regression suite
124
127
  npm run build # Build to dist/
125
128
  npm run catalog # Visual component catalog at :3002
126
129
  npm run docs:build # Build contributor docs with VitePress
@@ -365,6 +365,19 @@ export declare const HandoffNoticeSchema: z.ZodObject<{
365
365
  products_discussed: z.ZodOptional<z.ZodArray<z.ZodString>>;
366
366
  user_sentiment: z.ZodOptional<z.ZodString>;
367
367
  }, z.core.$strip>;
368
+ export declare const PanelRestoreCardSchema: z.ZodObject<{
369
+ eyebrow: z.ZodOptional<z.ZodString>;
370
+ title: z.ZodOptional<z.ZodString>;
371
+ showTitle: z.ZodOptional<z.ZodBoolean>;
372
+ ctaLabel: z.ZodOptional<z.ZodString>;
373
+ panelMessageId: z.ZodString;
374
+ highlightMessageId: z.ZodOptional<z.ZodString>;
375
+ threadId: z.ZodOptional<z.ZodString>;
376
+ products: z.ZodOptional<z.ZodArray<z.ZodObject<{
377
+ imageUrl: z.ZodOptional<z.ZodString>;
378
+ name: z.ZodOptional<z.ZodString>;
379
+ }, z.core.$strip>>>;
380
+ }, z.core.$strip>;
368
381
  export declare const PhotoAnalysisCardSchema: z.ZodObject<{
369
382
  summary: z.ZodString;
370
383
  strengths: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -769,6 +782,22 @@ export declare const chatCatalog: {
769
782
  }, z.core.$strip>;
770
783
  readonly description: "A notice shown when the conversation is escalated to a human agent.";
771
784
  };
785
+ readonly PanelRestoreCard: {
786
+ readonly schema: z.ZodObject<{
787
+ eyebrow: z.ZodOptional<z.ZodString>;
788
+ title: z.ZodOptional<z.ZodString>;
789
+ showTitle: z.ZodOptional<z.ZodBoolean>;
790
+ ctaLabel: z.ZodOptional<z.ZodString>;
791
+ panelMessageId: z.ZodString;
792
+ highlightMessageId: z.ZodOptional<z.ZodString>;
793
+ threadId: z.ZodOptional<z.ZodString>;
794
+ products: z.ZodOptional<z.ZodArray<z.ZodObject<{
795
+ imageUrl: z.ZodOptional<z.ZodString>;
796
+ name: z.ZodOptional<z.ZodString>;
797
+ }, z.core.$strip>>>;
798
+ }, z.core.$strip>;
799
+ readonly description: "A frontend-owned transcript card that reopens a related panel snapshot.";
800
+ };
772
801
  readonly PhotoAnalysisCard: {
773
802
  readonly schema: z.ZodObject<{
774
803
  summary: z.ZodString;
@@ -1,5 +1,4 @@
1
1
  import { ChatI18n, ChatMessage } from '../types.js';
2
- import { ThumbnailEntry } from './ThumbnailsColumn.js';
3
2
  /** Generic fallback icon (right-arrow) used when a pill specifies an icon name not in the map. */
4
3
  declare const DEFAULT_ACTION_ICON = "<svg viewBox=\"0 0 16 16\" class=\"gengage-chat-icon\"><path d=\"M3 8h10M9 4l4 4-4 4\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>";
5
4
  /** SVG icon map for suggested action chips/pills. Keys match backend icon names. */
@@ -31,6 +30,8 @@ export interface ChatDrawerOptions {
31
30
  /** When true, render the header favorites (heart) button. */
32
31
  showHeaderFavorites?: boolean | undefined;
33
32
  onFavoritesClick?: (() => void) | undefined;
33
+ /** Callback fired when the history button in the header is clicked. */
34
+ onHistoryClick?: (() => void) | undefined;
34
35
  /** Callback fired when the panel back button is clicked. */
35
36
  onPanelBack?: (() => void) | undefined;
36
37
  /** Callback fired when the panel forward button is clicked. */
@@ -48,10 +49,10 @@ export interface ChatDrawerOptions {
48
49
  getMobileState?: (() => 'half' | 'full') | undefined;
49
50
  /** Returns true when the chat is displayed as a mobile bottom-sheet. Used to keep the side-panel back button always enabled. */
50
51
  getMobileViewport?: (() => boolean) | undefined;
51
- /** Callback fired when a product thumbnail is clicked (for thread rollback). */
52
- onThumbnailClick?: ((threadId: string) => void) | undefined;
53
52
  /** Callback fired when a link in bot HTML is clicked. */
54
53
  onLinkClick?: ((url: string) => void) | undefined;
54
+ /** Render a frontend-owned message UI payload inside the bubble, when present. */
55
+ renderMessageUISpec?: ((message: ChatMessage) => HTMLElement | null) | undefined;
55
56
  /** Enable voice input (Web Speech API STT). Default: false. */
56
57
  voiceEnabled?: boolean | undefined;
57
58
  /** BCP 47 language for speech recognition. Default: 'tr-TR'. */
@@ -87,9 +88,7 @@ export declare class ChatDrawer {
87
88
  private _panelEl;
88
89
  private _panelVisible;
89
90
  private _panelCollapsed;
90
- private _dividerPreviewEnabled;
91
91
  private _dividerEl;
92
- private _dividerPreviewEl;
93
92
  private _onPanelToggle;
94
93
  private _onHostShellSync;
95
94
  private _pendingAttachment;
@@ -105,7 +104,6 @@ export declare class ChatDrawer {
105
104
  private _userScrolledUp;
106
105
  private _scrollLockedUntil;
107
106
  private _inputChipsEl;
108
- private _thumbnailsColumn;
109
107
  private _panelFloatingEl;
110
108
  /** Mobile: overlay host for comparison dock (above panel scroll; avoids transformed panel containing block). */
111
109
  private _comparisonDockSlotEl;
@@ -227,7 +225,7 @@ export declare class ChatDrawer {
227
225
  /**
228
226
  * Targeted content swap for an already-visible panel. Replaces the skeleton
229
227
  * or the current content element with `el`, leaving topbar / AI zone /
230
- * thumbnails column / floating element attached. Also resets topbar actions
228
+ * floating element attached. Also resets topbar actions
231
229
  * because the incoming content provides its own toolbar.
232
230
  */
233
231
  private _swapPanelContent;
@@ -242,7 +240,7 @@ export declare class ChatDrawer {
242
240
  resyncPanelTopBarFromCurrentContent(): void;
243
241
  /** Whether the panel is currently visible (may be empty). */
244
242
  isPanelVisible(): boolean;
245
- /** Whether the panel is currently visible and has rendered content (beyond topbar + thumbnails column). */
243
+ /** Whether the panel is currently visible and has rendered content beyond chrome elements. */
246
244
  hasPanelContent(): boolean;
247
245
  /** Whether panel currently shows loading skeleton blocks. */
248
246
  isPanelLoading(): boolean;
@@ -260,6 +258,7 @@ export declare class ChatDrawer {
260
258
  * Keeps `_panelCollapsed` untouched so user collapse preference survives future panel renders.
261
259
  */
262
260
  clearPanel(): void;
261
+ private _syncComparisonDockVisibility;
263
262
  /**
264
263
  * Mobile-only slot (see CSS): pins the comparison dock above panel scroll.
265
264
  * Pass null to clear.
@@ -271,6 +270,7 @@ export declare class ChatDrawer {
271
270
  */
272
271
  hideMobilePanel(): void;
273
272
  private _showMobilePanelFromBtn;
273
+ showMobilePanel(): void;
274
274
  /** Expand panel without locking — user can still toggle via divider. */
275
275
  expandPanel(): void;
276
276
  /**
@@ -284,6 +284,7 @@ export declare class ChatDrawer {
284
284
  * case subsequent DOM mutations (appended children, layout) shift it.
285
285
  */
286
286
  private _smoothScrollPanelListToTop;
287
+ private _getChoicePrompterRevealScrollThreshold;
287
288
  /** Update scroll affordance (bottom fade gradient) on the panel. */
288
289
  private _updateScrollAffordance;
289
290
  /** Horizontal swipe on conversation/panel areas to toggle the panel (mobile only). */
@@ -294,7 +295,6 @@ export declare class ChatDrawer {
294
295
  isPanelCollapsed(): boolean;
295
296
  /** Programmatically set panel collapsed state. */
296
297
  setPanelCollapsed(collapsed: boolean): void;
297
- setDividerPreviewEnabled(enabled: boolean): void;
298
298
  /** Save panel collapsed state to sessionStorage. */
299
299
  persistPanelState(accountId: string): void;
300
300
  /** Restore panel collapsed state from sessionStorage. Returns true when restored as collapsed. */
@@ -347,10 +347,6 @@ export declare class ChatDrawer {
347
347
  }>): void;
348
348
  /** Clear input-area chips. */
349
349
  clearInputAreaChips(): void;
350
- setThumbnails(entries: ThumbnailEntry[]): void;
351
- hideThumbnails(): void;
352
- private _renderDividerPreview;
353
- private _syncDividerPreview;
354
350
  /** Activate focus trap — Tab/Shift+Tab cycles within the drawer. */
355
351
  trapFocus(): void;
356
352
  /** Release the focus trap and restore previously focused element. */
@@ -0,0 +1,4 @@
1
+ import { UIElement } from '../../common/types.js';
2
+ import { ChatUISpecRenderContext } from '../types.js';
3
+ export declare const FRONTEND_PANEL_RESTORE_ACTION = "frontend_restore_panel_message";
4
+ export declare function renderPanelRestoreCard(element: UIElement, ctx: ChatUISpecRenderContext): HTMLElement;
@@ -0,0 +1,85 @@
1
+ import { BackendContext, UISpec } from '../common/types.js';
2
+ import { ChatMessage } from './types.js';
3
+ type LegacyThumbnailEntry = {
4
+ sku: string;
5
+ imageUrl: string;
6
+ threadId: string;
7
+ };
8
+ export interface StoredHistoryMessage {
9
+ id: string;
10
+ threadId?: string | undefined;
11
+ role: 'user' | 'assistant';
12
+ content?: string | undefined;
13
+ uiSpec?: UISpec | undefined;
14
+ frontendOnly?: boolean | undefined;
15
+ silent?: boolean | undefined;
16
+ renderHint?: string | undefined;
17
+ photoAnalysis?: {
18
+ summary: string;
19
+ strengths?: string[];
20
+ focusPoints?: string[];
21
+ celebStyle?: string;
22
+ celebStyleReason?: string;
23
+ nextQuestion?: string;
24
+ } | undefined;
25
+ timestamp: number;
26
+ status: 'done' | 'error';
27
+ }
28
+ export interface ChatHistoryIndexEntry {
29
+ id: string;
30
+ title: string;
31
+ preview: string;
32
+ createdAt: string;
33
+ updatedAt: string;
34
+ messageCount: number;
35
+ pageType?: string | undefined;
36
+ sku?: string | undefined;
37
+ sourceSessionId: string;
38
+ }
39
+ export interface ChatHistorySessionRecord extends ChatHistoryIndexEntry {
40
+ version: number;
41
+ userId: string;
42
+ appId: string;
43
+ messages: StoredHistoryMessage[];
44
+ currentThreadId: string | null;
45
+ lastThreadId: string | null;
46
+ panelThreads?: string[] | undefined;
47
+ /** Legacy thumbnail-strip data kept optional for backward compatibility with older local history. */
48
+ thumbnailEntries?: LegacyThumbnailEntry[] | undefined;
49
+ panelSnapshotHtml?: Record<string, string> | undefined;
50
+ panelSnapshotTypes?: Record<string, string> | undefined;
51
+ panelAiSpecByThread?: Record<string, UISpec> | undefined;
52
+ lastBackendContext: BackendContext | null;
53
+ }
54
+ export interface SaveHistorySessionParams {
55
+ historyId: string;
56
+ userId: string;
57
+ appId: string;
58
+ sourceSessionId: string;
59
+ messages: ChatMessage[];
60
+ currentThreadId: string | null;
61
+ lastThreadId: string | null;
62
+ chatCreatedAt: string;
63
+ panelSnapshots: Map<string, HTMLElement>;
64
+ panelSnapshotTypes: Map<string, string>;
65
+ panelThreads: string[];
66
+ panelAiSpecByThread?: Map<string, UISpec> | undefined;
67
+ lastBackendContext: BackendContext | null;
68
+ pageType?: string | undefined;
69
+ sku?: string | undefined;
70
+ }
71
+ export declare class ChatHistoryStorage {
72
+ private readonly _appId;
73
+ private readonly _userId;
74
+ private readonly _storage;
75
+ private readonly _scope;
76
+ constructor(_appId: string, _userId: string, _storage?: Storage | null);
77
+ createHistoryId(): string;
78
+ findSessionId(sourceSessionId: string, createdAt: string): string | null;
79
+ listSessions(): ChatHistoryIndexEntry[];
80
+ loadSession(historyId: string): ChatHistorySessionRecord | null;
81
+ saveSession(params: SaveHistorySessionParams): ChatHistorySessionRecord | null;
82
+ deleteSession(historyId: string): void;
83
+ private _commitRecord;
84
+ }
85
+ export {};
@@ -63,9 +63,11 @@ export declare class GengageChat extends BaseWidget<ChatWidgetConfig> {
63
63
  private _comparisonSelectedSkus;
64
64
  private _comparisonSelectionWarning;
65
65
  private _comparisonRefreshRafId;
66
+ private _lastAppliedKeyboardInset;
66
67
  /** SKUs of products the user has viewed across panel product grids. */
67
68
  private _viewedProductSkus;
68
- private _thumbnailEntries;
69
+ /** Latest AI analysis UISpec (AITopPicks/AIGroupingCards) per thread for panel restore. */
70
+ private _panelAiSpecByThread;
69
71
  private _choicePrompterEl;
70
72
  private _openState;
71
73
  private _mobileBreakpoint;
@@ -113,6 +115,10 @@ export declare class GengageChat extends BaseWidget<ChatWidgetConfig> {
113
115
  private _currentPanelSource;
114
116
  /** IndexedDB session persistence manager. */
115
117
  private _session;
118
+ /** LocalStorage-backed chat history manager. */
119
+ private _historyStorage;
120
+ /** Active local history session ID for the current conversation. */
121
+ private _activeHistorySessionId;
116
122
  /** Transcript focus, pin-to-bottom, and scroll request coordination. */
117
123
  private readonly _presentation;
118
124
  /** Registered event callbacks (GA4 event hooks). Key = event name, value = set of callbacks. */
@@ -169,8 +175,12 @@ export declare class GengageChat extends BaseWidget<ChatWidgetConfig> {
169
175
  * delay or distort the next response.
170
176
  */
171
177
  private _pruneEmptyStreamingAssistantPlaceholders;
178
+ /** Clear all runtime conversation state while keeping the widget mounted and ready. */
179
+ private _clearConversationState;
172
180
  /** Reset all chat state when navigating to a different SKU/page. */
173
181
  private _resetForNewPage;
182
+ /** Start a brand-new local conversation on the current page context. */
183
+ private _startNewChat;
174
184
  /**
175
185
  * Same side effects as stream UISpec `clearPanel`: hide/clear the assistant panel without
176
186
  * `restoreOrClearPanel()`. Used for `clearPanel` chunks and PDP chat layout (`productDetailsExtended` off).
@@ -262,9 +272,20 @@ export declare class GengageChat extends BaseWidget<ChatWidgetConfig> {
262
272
  private _applyPanelListHeadingToContext;
263
273
  private _renderPanelFromSource;
264
274
  private _handleRollback;
275
+ private _ensureMobileFullSheetForPanel;
276
+ private _ensureMobilePanelOverlayVisible;
265
277
  private _ensurePdpPrimeSuggestedUiIfNeeded;
266
278
  /** Rewind the conversation to the given thread. */
267
279
  private _rollbackToThread;
280
+ private _persistHistoryToLocalStorage;
281
+ private _persistConversationState;
282
+ private _persistConversationStateForNavigation;
283
+ private _formatHistoryTimestamp;
284
+ private _openHistoryPanel;
285
+ private _buildHistoryPageEl;
286
+ private _deleteHistorySession;
287
+ private _loadHistorySession;
288
+ private _restoreFromHistoryRecord;
268
289
  private _persistToIndexedDB;
269
290
  private _isSameOriginUrl;
270
291
  private _markUnavailableProductContext;
@@ -289,8 +310,6 @@ export declare class GengageChat extends BaseWidget<ChatWidgetConfig> {
289
310
  * then fall back to thread-level history.
290
311
  */
291
312
  private _navigatePanelBack;
292
- private _shouldUseDividerPreviewForSpec;
293
- private _shouldUseDividerPreviewForSource;
294
313
  private _toggleComparisonSku;
295
314
  /**
296
315
  * Refresh the panel DOM to reflect the current comparison state without
@@ -318,6 +337,15 @@ export declare class GengageChat extends BaseWidget<ChatWidgetConfig> {
318
337
  private _toggleProductFavorite;
319
338
  private _openFavoritesPanel;
320
339
  private _buildFavoritesPageEl;
340
+ private _isMessageHostedUISpec;
341
+ private _isPanelAiSpec;
342
+ private _rememberPanelAiSpec;
343
+ private _restorePanelAiSpecMap;
344
+ private _hydrateRestoredUISpecMetadata;
345
+ private _renderMessageUISpec;
346
+ private _maybeAddPanelRestoreMessage;
347
+ private _restorePanelFromFrontendMessage;
348
+ private _restorePanelAiZoneForThread;
321
349
  /**
322
350
  * Run registered callbacks for a GA4 event.
323
351
  * If any callback returns false or throws, handle the failure (e.g. show error for cart-add).
@@ -42,7 +42,7 @@ export declare class PanelManager {
42
42
  * Also restores the panel topbar title for the snapshot's component type.
43
43
  * Returns true if the snapshot was found and restored.
44
44
  */
45
- restoreForMessage(messageId: string): boolean;
45
+ restoreForMessage(messageId: string, highlightMessageId?: string): boolean;
46
46
  /**
47
47
  * Send maximize-pdp / minify-pdp bridge messages with production-matching delays.
48
48
  * Called by the extended mode manager when panel extension state changes.
@@ -2,7 +2,6 @@ import { GengageIndexedDB, FavoriteData } from '../common/indexed-db.js';
2
2
  import { CommunicationBridge } from '../common/communication-bridge.js';
3
3
  import { BackendContext, UISpec } from '../common/types.js';
4
4
  import { ChatMessage } from './types.js';
5
- import { ThumbnailEntry } from './components/ThumbnailsColumn.js';
6
5
  export type { FavoriteData };
7
6
  type NavigateFn = (url: string) => void;
8
7
  export interface PersistSessionParams {
@@ -14,8 +13,9 @@ export interface PersistSessionParams {
14
13
  lastThreadId: string | null;
15
14
  chatCreatedAt: string;
16
15
  panelSnapshots: Map<string, HTMLElement>;
16
+ panelSnapshotTypes?: Map<string, string> | undefined;
17
17
  panelThreads: string[];
18
- thumbnailEntries: ThumbnailEntry[];
18
+ panelAiSpecByThread?: Map<string, UISpec> | undefined;
19
19
  lastBackendContext: BackendContext | null;
20
20
  sku?: string | undefined;
21
21
  }
@@ -239,6 +239,11 @@ export interface ChatI18n {
239
239
  offlineMessage: string;
240
240
  cartAriaLabel: string;
241
241
  favoritesAriaLabel: string;
242
+ historyAriaLabel: string;
243
+ historyPageTitle: string;
244
+ emptyHistoryMessage: string;
245
+ historyDeleteLabel: string;
246
+ historyDeleteConfirmMessage: string;
242
247
  showPanelAriaLabel: string;
243
248
  addToFavoritesLabel: string;
244
249
  customerReviewsTitle: string;
@@ -352,7 +357,7 @@ export interface ChatUISpecRenderContext {
352
357
  hideProductDiscountBadge?: boolean | undefined;
353
358
  hideUserReviews?: boolean | undefined;
354
359
  hideStockStatus?: boolean | undefined;
355
- i18n?: Pick<ChatI18n, 'productCtaLabel' | 'viewOnSiteLabel' | 'aiTopPicksTitle' | 'roleWinner' | 'roleBestValue' | 'roleBestAlternative' | 'viewDetails' | 'groundingReviewCta' | 'groundingReviewSubtitle' | 'variantsLabel' | 'sortRelated' | 'sortPriceAsc' | 'sortPriceDesc' | 'sortToolbarAriaLabel' | 'compareSelected' | 'compareMinHint' | 'comparisonSelectLabel' | 'comparisonSelectedLabel' | 'comparisonSelectCardHint' | 'panelTitleProductDetails' | 'panelTitleSimilarProducts' | 'panelTitleComparisonResults' | 'panelTitleCategories' | 'panelTitleSearchResults' | 'inStockLabel' | 'outOfStockLabel' | 'findSimilarLabel' | 'galleryPrevAriaLabel' | 'galleryNextAriaLabel' | 'beautyStylesPreparedTitle' | 'watchStylesPreparedTitle' | 'consultingOtherCompatibleProductsLabel' | 'consultingFallbackGroupLabel' | 'consultingFallbackStyleLabel' | 'consultingStyleLoadingDescription' | 'consultingStyleUnavailableDescription' | 'consultingStyleLoadingBadge' | 'consultingStyleUnavailableBadge' | 'viewMoreLabel' | 'similarProductsLabel' | 'addToCartButton' | 'shareButton' | 'productInfoTab' | 'specificationsTab' | 'recommendedChoiceLabel' | 'highlightsLabel' | 'keyDifferencesLabel' | 'specialCasesLabel' | 'emptyReviewsMessage' | 'closeAriaLabel' | 'dismissAriaLabel' | 'startChatLabel' | 'handoffHeading' | 'customerReviewsTitle' | 'addToFavoritesLabel' | 'reviewFilterPositive' | 'reviewFilterNegative' | 'decreaseLabel' | 'increaseLabel' | 'reviewCustomersMentionSingular' | 'reviewCustomersMentionPlural' | 'reviewSubjectsHeading' | 'aiBrowseCategoriesTitle' | 'photoAnalysisBadge' | 'photoAnalysisStrengthsLabel' | 'photoAnalysisFocusLabel' | 'photoAnalysisCelebStyleLabel' | 'beautyPhotoStepTitle' | 'beautyPhotoStepDescription' | 'beautyPhotoStepUpload' | 'beautyPhotoStepProcessing' | 'beautyPhotoStepSkip'>;
360
+ i18n?: Pick<ChatI18n, 'productCtaLabel' | 'viewOnSiteLabel' | 'aiTopPicksTitle' | 'roleWinner' | 'roleBestValue' | 'roleBestAlternative' | 'viewDetails' | 'groundingReviewCta' | 'groundingReviewSubtitle' | 'variantsLabel' | 'sortRelated' | 'sortPriceAsc' | 'sortPriceDesc' | 'sortToolbarAriaLabel' | 'compareSelected' | 'compareMinHint' | 'comparisonSelectLabel' | 'comparisonSelectedLabel' | 'comparisonSelectCardHint' | 'panelTitleProductDetails' | 'panelTitleSimilarProducts' | 'panelTitleComparisonResults' | 'panelTitleCategories' | 'panelTitleSearchResults' | 'inStockLabel' | 'outOfStockLabel' | 'findSimilarLabel' | 'galleryPrevAriaLabel' | 'galleryNextAriaLabel' | 'beautyStylesPreparedTitle' | 'watchStylesPreparedTitle' | 'consultingOtherCompatibleProductsLabel' | 'consultingFallbackGroupLabel' | 'consultingFallbackStyleLabel' | 'consultingStyleLoadingDescription' | 'consultingStyleUnavailableDescription' | 'consultingStyleLoadingBadge' | 'consultingStyleUnavailableBadge' | 'viewMoreLabel' | 'similarProductsLabel' | 'addToCartButton' | 'shareButton' | 'productInfoTab' | 'specificationsTab' | 'recommendedChoiceLabel' | 'highlightsLabel' | 'keyDifferencesLabel' | 'specialCasesLabel' | 'emptyReviewsMessage' | 'closeAriaLabel' | 'dismissAriaLabel' | 'startChatLabel' | 'handoffHeading' | 'customerReviewsTitle' | 'addToFavoritesLabel' | 'showPanelAriaLabel' | 'reviewFilterPositive' | 'reviewFilterNegative' | 'decreaseLabel' | 'increaseLabel' | 'reviewCustomersMentionSingular' | 'reviewCustomersMentionPlural' | 'reviewSubjectsHeading' | 'aiBrowseCategoriesTitle' | 'photoAnalysisBadge' | 'photoAnalysisStrengthsLabel' | 'photoAnalysisFocusLabel' | 'photoAnalysisCelebStyleLabel' | 'beautyPhotoStepTitle' | 'beautyPhotoStepDescription' | 'beautyPhotoStepUpload' | 'beautyPhotoStepProcessing' | 'beautyPhotoStepSkip'>;
356
361
  productSort?: ProductSortState | undefined;
357
362
  onSortChange?: ((sort: ProductSortState) => void) | undefined;
358
363
  comparisonSelectMode?: boolean | undefined;
@@ -384,6 +389,8 @@ export interface ChatMessage {
384
389
  content?: string;
385
390
  /** json-render UI spec attached to this message (e.g. product cards). */
386
391
  uiSpec?: import('../common/types.js').UISpec;
392
+ /** Frontend-synthesized visible message; exclude from backend chat history. */
393
+ frontendOnly?: boolean;
387
394
  /** Image attachment (user-uploaded). Stored in-memory until send. */
388
395
  attachment?: File;
389
396
  /** Snapshot of panel DOM at time this message's stream completed. */
@@ -420,6 +427,7 @@ export interface SerializableChatMessage {
420
427
  threadId?: string | undefined;
421
428
  role: 'user' | 'assistant';
422
429
  content?: string | undefined;
430
+ frontendOnly?: boolean | undefined;
423
431
  silent?: boolean | undefined;
424
432
  timestamp: number;
425
433
  status: 'streaming' | 'done' | 'error';