@constructor-io/constructorio-ui-asa 1.7.2 → 1.9.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.
Files changed (76) hide show
  1. package/README.md +21 -17
  2. package/dist/constructorio-ui-asa-bundled.js +17 -17
  3. package/lib/cjs/components/Button/Button.js +22 -3
  4. package/lib/cjs/components/Chat/AiMessage.js +2 -3
  5. package/lib/cjs/components/Chat/Chat.js +50 -9
  6. package/lib/cjs/components/CioAsa/CioAsa.js +6 -2
  7. package/lib/cjs/components/CioAsaProvider/CioAsaProvider.js +44 -3
  8. package/lib/cjs/components/ResultsBlock/ResultsGroup.js +5 -5
  9. package/lib/cjs/hooks/agentStream.js +113 -0
  10. package/lib/cjs/hooks/asaStreamHandlers.js +2 -1
  11. package/lib/cjs/hooks/persistence/backgroundTurns.js +121 -0
  12. package/lib/cjs/hooks/persistence/useChatPersistence.js +485 -0
  13. package/lib/cjs/hooks/persistence/useForeignStream.js +34 -0
  14. package/lib/cjs/hooks/persistence/useThreadList.js +45 -0
  15. package/lib/cjs/hooks/persistence/useWriteQueue.js +17 -0
  16. package/lib/cjs/hooks/useAsaResults.js +119 -159
  17. package/lib/cjs/hooks/useAsaTracking.js +33 -18
  18. package/lib/cjs/hooks/useIsMounted.js +15 -0
  19. package/lib/cjs/hooks/useLatest.js +10 -0
  20. package/lib/cjs/hooks/useViewportTracking.js +4 -3
  21. package/lib/cjs/index.js +3 -1
  22. package/lib/cjs/utils/chatRecordValidation.js +54 -0
  23. package/lib/cjs/utils/chatSession.js +98 -0
  24. package/lib/cjs/utils/chatThreads.js +248 -0
  25. package/lib/cjs/utils/clientOptions.js +11 -0
  26. package/lib/cjs/utils/localStoragePersistence.js +391 -0
  27. package/lib/cjs/utils/translate.js +1 -0
  28. package/lib/cjs/version.js +1 -1
  29. package/lib/mjs/components/Button/Button.js +26 -3
  30. package/lib/mjs/components/Chat/AiMessage.js +2 -3
  31. package/lib/mjs/components/Chat/Chat.js +48 -9
  32. package/lib/mjs/components/CioAsa/CioAsa.js +6 -2
  33. package/lib/mjs/components/CioAsaProvider/CioAsaProvider.js +44 -4
  34. package/lib/mjs/components/ResultsBlock/ResultsGroup.js +2 -1
  35. package/lib/mjs/hooks/agentStream.js +106 -0
  36. package/lib/mjs/hooks/asaStreamHandlers.js +1 -1
  37. package/lib/mjs/hooks/persistence/backgroundTurns.js +120 -0
  38. package/lib/mjs/hooks/persistence/useChatPersistence.js +474 -0
  39. package/lib/mjs/hooks/persistence/useForeignStream.js +31 -0
  40. package/lib/mjs/hooks/persistence/useThreadList.js +41 -0
  41. package/lib/mjs/hooks/persistence/useWriteQueue.js +14 -0
  42. package/lib/mjs/hooks/useAsaResults.js +113 -156
  43. package/lib/mjs/hooks/useAsaTracking.js +42 -17
  44. package/lib/mjs/hooks/useIsMounted.js +12 -0
  45. package/lib/mjs/hooks/useLatest.js +7 -0
  46. package/lib/mjs/hooks/useViewportTracking.js +3 -3
  47. package/lib/mjs/index.js +1 -0
  48. package/lib/mjs/utils/chatRecordValidation.js +51 -0
  49. package/lib/mjs/utils/chatSession.js +86 -0
  50. package/lib/mjs/utils/chatThreads.js +219 -0
  51. package/lib/mjs/utils/clientOptions.js +7 -0
  52. package/lib/mjs/utils/localStoragePersistence.js +373 -0
  53. package/lib/mjs/utils/translate.js +1 -0
  54. package/lib/mjs/version.js +1 -1
  55. package/lib/types/components/Button/Button.d.ts +3 -2
  56. package/lib/types/components/Chat/Chat.d.ts +20 -2
  57. package/lib/types/components/CioAsa/CioAsa.d.ts +1 -1
  58. package/lib/types/hooks/agentStream.d.ts +48 -0
  59. package/lib/types/hooks/asaStreamHandlers.d.ts +1 -0
  60. package/lib/types/hooks/persistence/backgroundTurns.d.ts +43 -0
  61. package/lib/types/hooks/persistence/useChatPersistence.d.ts +34 -0
  62. package/lib/types/hooks/persistence/useForeignStream.d.ts +9 -0
  63. package/lib/types/hooks/persistence/useThreadList.d.ts +9 -0
  64. package/lib/types/hooks/persistence/useWriteQueue.d.ts +5 -0
  65. package/lib/types/hooks/useAsaTracking.d.ts +6 -2
  66. package/lib/types/hooks/useIsMounted.d.ts +3 -0
  67. package/lib/types/hooks/useLatest.d.ts +3 -0
  68. package/lib/types/index.d.ts +3 -2
  69. package/lib/types/types.d.ts +170 -5
  70. package/lib/types/utils/chatRecordValidation.d.ts +2 -0
  71. package/lib/types/utils/chatSession.d.ts +45 -0
  72. package/lib/types/utils/chatThreads.d.ts +63 -0
  73. package/lib/types/utils/clientOptions.d.ts +5 -0
  74. package/lib/types/utils/localStoragePersistence.d.ts +26 -0
  75. package/lib/types/version.d.ts +1 -1
  76. package/package.json +2 -2
@@ -0,0 +1,48 @@
1
+ import type { Dispatch, SetStateAction } from 'react';
2
+ import { ChatMessage } from '../types';
3
+ type SetMessages = Dispatch<SetStateAction<ChatMessage[]>>;
4
+ export interface AgentStreamCallbacks {
5
+ /** The `start` event arrived, possibly carrying the server thread id. */
6
+ onStart: (threadId?: string) => void;
7
+ /** Fires once, on the first event with content, for load-started tracking. */
8
+ onLoadStart: (intentResultId?: string) => void;
9
+ /** The stream ended normally (not on a server error, a failure, or a cancel). */
10
+ onFinish: (result: {
11
+ searchResultCount: number;
12
+ intentResultId?: string;
13
+ }) => void;
14
+ }
15
+ export interface AgentStreamHandle {
16
+ /** The assistant message this stream writes into, so a cancel can settle the right one. */
17
+ assistantId: string;
18
+ /** Stop reading; the pending `done` settles without touching the assistant message. */
19
+ cancel: () => void;
20
+ done: Promise<void>;
21
+ }
22
+ /** Where a running answer goes: the chat showing its conversation, or the background. */
23
+ export interface TurnOwner {
24
+ setMessages: SetMessages;
25
+ onStart: (threadId?: string) => void;
26
+ /** The stream ended, failed or was cancelled. */
27
+ onDone: () => void;
28
+ }
29
+ /** An answer being read; its owner can change mid-stream, so leaving the chat does not stop it. */
30
+ export interface LiveTurn extends AgentStreamHandle {
31
+ owner: TurnOwner;
32
+ /** The assistant message as streamed so far, whoever shows it. */
33
+ assistant: ChatMessage;
34
+ /** From the `start` event. */
35
+ serverThreadId?: string;
36
+ }
37
+ /** Drains an agent stream into the assistant message identified by `assistantId`. */
38
+ export declare function readAgentStream(stream: ReadableStream, assistantId: string, setMessages: SetMessages, callbacks: AgentStreamCallbacks): AgentStreamHandle;
39
+ export interface TurnCallbacks {
40
+ onLoadStart: AgentStreamCallbacks['onLoadStart'];
41
+ /** As `AgentStreamCallbacks.onFinish`, with the server thread the answer belongs to. */
42
+ onFinish: (result: Parameters<AgentStreamCallbacks['onFinish']>[0] & {
43
+ threadId?: string;
44
+ }) => void;
45
+ }
46
+ /** Streams into `assistant`, reporting to the owner `createOwner` returns for the new turn. */
47
+ export declare function startTurn(stream: ReadableStream, assistant: ChatMessage, createOwner: (turn: LiveTurn) => TurnOwner, callbacks: TurnCallbacks): LiveTurn;
48
+ export {};
@@ -1,6 +1,7 @@
1
1
  import { Dispatch, SetStateAction } from 'react';
2
2
  import { ResultGroupMeta, ChatMessage } from '../types';
3
3
  export type MessageUpdater = Dispatch<SetStateAction<ChatMessage[]>>;
4
+ export declare function updateMessageById(setMessages: MessageUpdater, id: string, patch: (msg: ChatMessage) => ChatMessage): void;
4
5
  export declare function handleSearchResult(data: any, pendingGroup: ResultGroupMeta | null, assistantId: string, setMessages: MessageUpdater): ResultGroupMeta | null;
5
6
  export declare function handleMessage(data: any, assistantId: string, setMessages: MessageUpdater): void;
6
7
  export declare function handleFollowUpRefinement(data: any, assistantId: string, setMessages: MessageUpdater): void;
@@ -0,0 +1,43 @@
1
+ import type { ChatMessage, ChatPersistence } from '../../types';
2
+ import type { LiveTurn, TurnOwner } from '../agentStream';
3
+ import { ChatSession } from '../../utils/chatSession';
4
+ /** An answer that went on after its conversation left the screen, until it is stored or shown again. */
5
+ export declare class BackgroundTurn {
6
+ readonly turn: LiveTurn;
7
+ store: ChatPersistence;
8
+ /** The conversation's own session, forked from the chat that sent the message. */
9
+ readonly session: ChatSession;
10
+ messages: ChatMessage[];
11
+ streaming: boolean;
12
+ /** Whether the latest snapshot is known to be stored. */
13
+ saved: boolean;
14
+ /** Every id the conversation was stored under, so a list read before a re-key still finds it. */
15
+ readonly ids: Set<string>;
16
+ /** Its writes, in order; a chat that takes the turn back writes after them. */
17
+ writes: Promise<void>;
18
+ constructor(turn: LiveTurn, params: {
19
+ store: ChatPersistence;
20
+ session: ChatSession;
21
+ messages: ChatMessage[];
22
+ after: Promise<void>;
23
+ });
24
+ save(): Promise<void>;
25
+ /** Later writes go into `store`. */
26
+ moveTo(store: ChatPersistence): void;
27
+ /** Stores the conversation settled, without awaiting: the page is going away. */
28
+ flush(): void;
29
+ }
30
+ /**
31
+ * Keeps `turn` streaming with no chat showing it. It is saved into `store` at once, on `start`
32
+ * and when done, each write queued behind `after`, and is dropped from here once the last one landed.
33
+ */
34
+ export declare function continueInBackground(turn: LiveTurn, params: ConstructorParameters<typeof BackgroundTurn>[1]): void;
35
+ /**
36
+ * Takes back the background turn stored in `store` under `threadId` for a chat to show, handing an
37
+ * unfinished one to `createOwner`; `null` if there is none.
38
+ */
39
+ export declare function resumeBackgroundTurn(store: ChatPersistence, threadId: string, createOwner: (turn: LiveTurn) => TurnOwner): BackgroundTurn | null;
40
+ /** Points the background turns of `from` at `to`; resolves once their writes into `from` settled. */
41
+ export declare function moveBackgroundTurns(from: ChatPersistence, to: ChatPersistence): Promise<void>;
42
+ /** Called whenever a background turn is stored or finishes. */
43
+ export declare function subscribeBackgroundTurns(listener: () => void): () => void;
@@ -0,0 +1,34 @@
1
+ import { Dispatch, MutableRefObject, SetStateAction } from 'react';
2
+ import { ChatMessage, ChatPersistence, PersistenceScope } from '../../types';
3
+ import type { LiveTurn, TurnOwner } from '../agentStream';
4
+ import { ChatSession } from '../../utils/chatSession';
5
+ interface Params {
6
+ store: ChatPersistence | undefined;
7
+ /** Whose store it is; a switch from `'guest'` to `'user'` carries the conversation over. */
8
+ scope?: PersistenceScope;
9
+ /** Api key and domain of `store`; a login into another index does not carry anything over. */
10
+ index?: string;
11
+ session: ChatSession;
12
+ initialThreadId?: string;
13
+ messages: ChatMessage[];
14
+ setMessages: Dispatch<SetStateAction<ChatMessage[]>>;
15
+ isStreaming: boolean;
16
+ setIsStreaming: (value: boolean) => void;
17
+ /** This tab's answer streaming into the conversation on screen, if any. */
18
+ turnRef: MutableRefObject<LiveTurn | null>;
19
+ /** Stop the answer currently streaming in this tab, if any. */
20
+ cancelStream: () => void;
21
+ }
22
+ /** Restores, saves and syncs the conversation with `store`; inert when `store` is undefined. */
23
+ export default function useChatPersistence(params: Params): {
24
+ isHydrating: boolean;
25
+ threads: import("../../types").ThreadSummary[];
26
+ activeThreadId: string | null;
27
+ foreignInFlight: boolean;
28
+ beginTurn: () => void;
29
+ screenOwner: (turn: LiveTurn) => TurnOwner;
30
+ clearHistory: () => void;
31
+ newThread: () => void;
32
+ switchThread: (threadId: string) => Promise<void>;
33
+ };
34
+ export {};
@@ -0,0 +1,9 @@
1
+ import { ChatSession } from '../../utils/chatSession';
2
+ /** An answer streaming in another tab: blocks sending and settles after the grace period. */
3
+ export default function useForeignStream(session: ChatSession): {
4
+ foreignInFlight: boolean;
5
+ setForeignInFlight: import("react").Dispatch<import("react").SetStateAction<boolean>>;
6
+ watch: (remainingMs: number, onExpire: () => void) => void;
7
+ stop: () => void;
8
+ clearTimer: () => void;
9
+ };
@@ -0,0 +1,9 @@
1
+ import { MutableRefObject } from 'react';
2
+ import { ChatPersistence, ThreadSummary } from '../../types';
3
+ /** The stored thread list, re-read on demand; only the latest read may land. */
4
+ export default function useThreadList(storeRef: MutableRefObject<ChatPersistence | undefined>): {
5
+ threads: ThreadSummary[];
6
+ setThreads: import("react").Dispatch<import("react").SetStateAction<ThreadSummary[]>>;
7
+ refreshThreads: () => Promise<void>;
8
+ invalidate: () => void;
9
+ };
@@ -0,0 +1,5 @@
1
+ /** Runs storage writes one after another, in the order they were issued. */
2
+ export default function useWriteQueue(): {
3
+ enqueue: (run: () => Promise<void>) => Promise<void>;
4
+ reset: () => void;
5
+ };
@@ -1,5 +1,5 @@
1
1
  import { Tracker } from '@constructor-io/constructorio-client-javascript/lib/types/constructorio';
2
- import { AssistantTrackedItem } from '../types';
2
+ import { AgentButtonClickPayload, AssistantSubmitSource, AssistantTrackedItem } from '../types';
3
3
  export interface UseAsaTrackingProps {
4
4
  tracker?: Tracker;
5
5
  section?: string;
@@ -14,6 +14,8 @@ export interface TrackResultLoadFinishedArgs {
14
14
  intent: string;
15
15
  searchResultCount: number;
16
16
  intentResultId?: string;
17
+ /** Thread the answer belongs to, when it can differ from the hook's `threadId`. */
18
+ threadId?: string;
17
19
  }
18
20
  export interface TrackResultClickArgs {
19
21
  intent: string;
@@ -38,8 +40,10 @@ export interface TrackSearchSubmitArgs {
38
40
  intentResultId?: string;
39
41
  groupId?: string;
40
42
  }
43
+ export type TrackAgentButtonClickArgs = AgentButtonClickPayload;
41
44
  export interface UseAsaTrackingReturn {
42
- trackSubmit: (intent: string) => void;
45
+ trackAgentButtonClick: (args: TrackAgentButtonClickArgs) => void;
46
+ trackSubmit: (intent: string, source?: AssistantSubmitSource) => void;
43
47
  trackResultLoadStarted: (args: TrackResultLoadStartedArgs) => void;
44
48
  trackResultLoadFinished: (args: TrackResultLoadFinishedArgs) => void;
45
49
  trackResultClick: (args: TrackResultClickArgs) => void;
@@ -0,0 +1,3 @@
1
+ /// <reference types="react" />
2
+ /** A ref that is `true` while mounted, for async work that must not land after unmount. */
3
+ export default function useIsMounted(): import("react").MutableRefObject<boolean>;
@@ -0,0 +1,3 @@
1
+ /// <reference types="react" />
2
+ /** A ref that always holds the latest `value`, for async code that must not read stale props. */
3
+ export default function useLatest<T>(value: T): import("react").MutableRefObject<T>;
@@ -10,8 +10,9 @@ export type { ButtonProps } from './components/Button/Button';
10
10
  export { default as useAsaResults } from './hooks/useAsaResults';
11
11
  export { default as useAsaTracking } from './hooks/useAsaTracking';
12
12
  export { default as useCioAsaContext } from './hooks/useCioAsaContext';
13
- export type { UseAsaTrackingProps, UseAsaTrackingReturn } from './hooks/useAsaTracking';
13
+ export type { UseAsaTrackingProps, UseAsaTrackingReturn, TrackAgentButtonClickArgs, } from './hooks/useAsaTracking';
14
14
  export { normalizeItemToProduct } from './utils/productNormalizer';
15
+ export { clearPersistedConversations } from './utils/localStoragePersistence';
15
16
  export type { Product, NormalizeOptions } from './utils/productNormalizer';
16
17
  export { ConstructorIOClient } from './types';
17
- export type { Nullable, CioAsaProviderProps, AsaContextValue, CioClientOptions, RequestConfigs, Formatters, UrlHelpers, QueryParamEncodingOptions, DefaultQueryStringMap, Translations, ChatMessage, ChatMessageStatus, FollowUpRefinement, ResultGroup, ResultGroupMeta, SearchResultEventRequest, UseAsaResultsOptions, UseChatReturn, AsaCallbacks, AssistantSubmitSource, AssistantTrackedItem, ChatComponentOverrides, WelcomeScreenOverrides, ChatInputOverrides, AiMessageOverrides, ResultsBlockOverrides, ComponentOverrideProps, IncludeComponentOverrides, ChatHeaderRenderProps, ChatInputRenderProps, WelcomeScreenTitleRenderProps, WelcomeScreenInputRenderProps, SuggestedQuestionsRenderProps, UserMessageRenderProps, AiMessageLoaderRenderProps, AiMessageTextRenderProps, FollowUpRefinementRenderProps, ResultsGroupTitleRenderProps, ResultsViewMoreRenderProps, } from './types';
18
+ export type { Nullable, CioAsaProviderProps, PersistConversationOptions, AsaContextValue, CioClientOptions, RequestConfigs, Formatters, UrlHelpers, QueryParamEncodingOptions, DefaultQueryStringMap, Translations, ChatMessage, ChatMessageStatus, FollowUpRefinement, ResultGroup, ResultGroupMeta, SearchResultEventRequest, UseAsaResultsOptions, UseChatReturn, SendMessageOptions, ClearPersistedConversationsOptions, ThreadSummary, AsaCallbacks, AssistantSubmitSource, AssistantTrackedItem, AgentButtonClickMode, AgentButtonClickPayload, AgentButtonClickPageType, AgentButtonClickPlacement, ChatComponentOverrides, WelcomeScreenOverrides, ChatInputOverrides, AiMessageOverrides, ResultsBlockOverrides, ComponentOverrideProps, IncludeComponentOverrides, ChatHeaderRenderProps, ChatInputRenderProps, WelcomeScreenTitleRenderProps, WelcomeScreenInputRenderProps, SuggestedQuestionsRenderProps, UserMessageRenderProps, AiMessageLoaderRenderProps, AiMessageTextRenderProps, FollowUpRefinementRenderProps, ResultsGroupTitleRenderProps, ResultsViewMoreRenderProps, } from './types';
@@ -21,6 +21,12 @@ export interface AsaContextValue {
21
21
  urlHelpers: UrlHelpers;
22
22
  callbacks?: AsaCallbacks;
23
23
  section?: string;
24
+ /** Resolved chat persistence store, or `undefined` when persistence is off. */
25
+ persistence?: ChatPersistence;
26
+ /** Whose store `persistence` is: the guest's (per tab) or a signed-in shopper's (per browser). */
27
+ persistenceScope?: PersistenceScope;
28
+ /** Api key and domain `persistence` is scoped to; a login carries a conversation over only within one. */
29
+ persistenceIndex?: string;
24
30
  }
25
31
  export interface RequestConfigs extends IAgentParameters {
26
32
  intent?: string;
@@ -35,7 +41,7 @@ export interface UrlHelpers {
35
41
  getUrlFromState: (state: RequestConfigs, options: QueryParamEncodingOptions) => string;
36
42
  defaultQueryStringMap: Readonly<DefaultQueryStringMap>;
37
43
  }
38
- export interface CioAsaProviderProps extends Omit<Partial<AsaContextValue>, 'setCioClientOptions' | 'cioClientOptions'> {
44
+ interface CioAsaProviderBaseProps extends Omit<Partial<AsaContextValue>, 'setCioClientOptions' | 'cioClientOptions' | 'persistence' | 'persistenceScope' | 'persistenceIndex'> {
39
45
  apiKey?: string;
40
46
  /**
41
47
  * A/B test cells to attach to tracking events, as `{ [testName]: cellName }`. Each entry is
@@ -49,6 +55,32 @@ export interface CioAsaProviderProps extends Omit<Partial<AsaContextValue>, 'set
49
55
  */
50
56
  testCells?: Record<string, string>;
51
57
  }
58
+ /** How conversations are kept in the browser. */
59
+ export interface PersistConversationOptions {
60
+ /**
61
+ * Keep conversations across page loads: a signed-in shopper's in `localStorage` for 7 days,
62
+ * keyed by api key + domain + user id; a guest's in `sessionStorage`, ending with the tab.
63
+ */
64
+ enabled: boolean;
65
+ }
66
+ interface PersistentProviderProps {
67
+ persistConversation: PersistConversationOptions;
68
+ /**
69
+ * Whose history it is: the signed-in shopper's stable, non-personal id, or `null` for a guest.
70
+ * Change it on login and logout. With `apiKey` it is also set on the client.
71
+ */
72
+ userId: string | null;
73
+ }
74
+ interface EphemeralProviderProps {
75
+ /** Off, or omitted: the conversation lives in memory and is gone on the next page load. */
76
+ persistConversation?: PersistConversationOptions & {
77
+ enabled: false;
78
+ };
79
+ /** The shopper's id; with `apiKey` it is set on the client. `null` for a guest. */
80
+ userId?: string | null;
81
+ }
82
+ export type CioAsaPersistenceProps = PersistentProviderProps | EphemeralProviderProps;
83
+ export type CioAsaProviderProps = CioAsaProviderBaseProps & CioAsaPersistenceProps;
52
84
  export interface UseCioClientProps {
53
85
  apiKey?: string;
54
86
  cioClient?: Nullable<ConstructorIOClient>;
@@ -84,6 +116,8 @@ export interface ChatMessage {
84
116
  threadId?: string;
85
117
  /** Narrowing question the agent asked at the end of this turn, from a `follow_up_refinement` event. */
86
118
  refinement?: FollowUpRefinement;
119
+ /** The answer never arrived: the page was left or the shopper logged out mid-answer. Unset on a failure. */
120
+ interrupted?: boolean;
87
121
  }
88
122
  /** A narrowing question with selectable options, emitted by the agent as a `follow_up_refinement` event. */
89
123
  export interface FollowUpRefinement {
@@ -138,14 +172,29 @@ export interface ResultGroupMeta {
138
172
  }
139
173
  export interface ResultGroup {
140
174
  group: ResultGroupMeta;
175
+ /** Raw items as the agent sent them; empty on a conversation restored from storage. */
141
176
  searchResults: Record<string, unknown>[];
177
+ /** The cards as `normalizeItem` made them; set on a conversation restored from storage. */
178
+ products?: Product[];
142
179
  searchResultId?: string;
143
180
  intentResultId?: string;
144
181
  }
182
+ export interface SendMessageOptions {
183
+ /**
184
+ * Conversation to send into, e.g. one started elsewhere on the page. The chat moves to it
185
+ * first: loaded from storage when persistence has it, otherwise continued on the server
186
+ * with an empty screen. Omit to send into the conversation on screen.
187
+ */
188
+ threadId?: string;
189
+ }
145
190
  export interface UseChatReturn {
146
191
  messages: ChatMessage[];
147
- sendMessage: (text: string, source?: AssistantSubmitSource) => void;
192
+ /** Skipped while another answer streams. */
193
+ sendMessage: (text: string, source?: AssistantSubmitSource, options?: SendMessageOptions) => void;
194
+ /** True while an answer is streaming, here or, with persistence on, in another tab on the same thread. */
148
195
  isStreaming: boolean;
196
+ /** True while this tab's own answer is streaming, the one `abort` can stop. */
197
+ canAbort: boolean;
149
198
  /**
150
199
  * Cancel the in-flight request, keeping the conversation. The partial reply is settled
151
200
  * as `done` and the thread id is kept, so the next message continues the same
@@ -153,25 +202,140 @@ export interface UseChatReturn {
153
202
  */
154
203
  abort: () => void;
155
204
  clearHistory: () => void;
205
+ /** True while a persisted conversation is being loaded. Always `false` when persistence is off. */
206
+ isHydrating: boolean;
207
+ /** Stored conversations, most recent first. Empty when persistence is off. */
208
+ threads: ThreadSummary[];
209
+ /** Id of the stored conversation currently shown, or `null` for an unsaved one. */
210
+ activeThreadId: string | null;
211
+ /** Start an empty conversation, keeping the current one in storage when persistence is on. */
212
+ newThread: () => void;
213
+ /** Load a stored conversation and continue it. No-op when persistence is off. */
214
+ switchThread: (threadId: string) => Promise<void>;
156
215
  }
157
216
  export interface UseAsaResultsOptions {
158
- /** Seed the thread id (e.g. loaded from browser storage) to resume a prior conversation. Read once on mount. */
217
+ /**
218
+ * Seed the thread id to resume a prior conversation. Read once on mount. With persistence
219
+ * enabled, the matching stored transcript is restored too when one exists.
220
+ */
159
221
  initialThreadId?: string;
222
+ /** Makes the card stored for each product: the function `Chat` renders with. Defaults to `normalizeItemToProduct`. */
223
+ normalizeItem?: (item: any) => Product;
224
+ }
225
+ /** Where conversations live: `'session'` ends with the tab, `'local'` survives it. */
226
+ export type StorageArea = 'local' | 'session';
227
+ /** Whose conversations a store holds. */
228
+ export type PersistenceScope = 'guest' | 'user';
229
+ /** A stored conversation. `threadId` is the server thread id, or a `local-` id for non-conversational domains. */
230
+ export interface PersistedChat {
231
+ version: 1;
232
+ threadId: string;
233
+ messages: ChatMessage[];
234
+ createdAt: number;
235
+ updatedAt: number;
236
+ /**
237
+ * Id of the browser tab that last wrote the record. Lets a reloaded tab tell its own
238
+ * interrupted answer (settled at once) from one still streaming in another tab.
239
+ */
240
+ owner?: string;
241
+ }
242
+ export interface ThreadSummary {
243
+ threadId: string;
244
+ /** First user message, truncated. */
245
+ title: string;
246
+ createdAt: number;
247
+ updatedAt: number;
248
+ /** True while the latest answer is still streaming (possibly in another tab). */
249
+ inFlight: boolean;
250
+ }
251
+ /** Storage contract the chat hook talks to; implemented by `createLocalStoragePersistence`. */
252
+ export interface ChatPersistence {
253
+ /** Most recently updated first. */
254
+ listThreads(): Promise<ThreadSummary[]>;
255
+ getThread(threadId: string): Promise<PersistedChat | null>;
256
+ saveThread(chat: PersistedChat): Promise<void>;
257
+ deleteThread(threadId: string): Promise<void>;
258
+ /**
259
+ * Optional. Whether `threadId` was explicitly deleted (or the whole store was cleared), as
260
+ * opposed to expired or evicted. Without it, a thread that has gone missing is treated as deleted.
261
+ */
262
+ isThreadDeleted?(threadId: string): Promise<boolean>;
263
+ /**
264
+ * Optional. Write `chat` synchronously, retiring `staleIds` in the same step, for use while the
265
+ * page is unloading: an async write cannot finish there. Falls back to `saveThread` when absent.
266
+ */
267
+ saveThreadSync?(chat: PersistedChat, staleIds?: string[]): void;
268
+ /**
269
+ * Optional. Notify when stored threads change outside this hook instance (another tab).
270
+ * Returns an unsubscribe function.
271
+ */
272
+ subscribe?(listener: () => void): () => void;
273
+ }
274
+ /** Which stored history `clearPersistedConversations` deletes; mirror what the provider was given. */
275
+ export interface ClearPersistedConversationsOptions {
276
+ /** The provider's `apiKey`, or the api key of the `cioClient` given to it. */
277
+ apiKey: string;
278
+ /** The `domain` from `staticRequestConfigs`. Default `'chatbot'`, the provider's default. */
279
+ domain?: string;
280
+ /** The shopper whose history to delete. Omit or pass `null` for the guest history. */
281
+ userId?: string | null;
282
+ /** Storage to clear instead of the default: `localStorage` for a shopper, `sessionStorage` for the guest. */
283
+ storage?: Storage;
284
+ }
285
+ export interface LocalStoragePersistenceOptions {
286
+ /** Storage key prefix. Default `'cio-asa:chat'`. */
287
+ key?: string;
288
+ /** Appended to the key to isolate conversations, e.g. per api key + domain. */
289
+ namespace?: string;
290
+ /** Threads idle longer than this are dropped on read. Default 7 days, matching server retention. */
291
+ ttlMs?: number;
292
+ /** Cap on user/assistant turns kept per thread. Unlimited by default. */
293
+ maxTurns?: number;
294
+ /** Cap on threads kept. Unlimited by default. */
295
+ maxThreads?: number;
296
+ /** Which browser storage to use. Default `'local'`. */
297
+ storageArea?: StorageArea;
298
+ /** Storage to use instead of the browser's; takes precedence over `storageArea`. */
299
+ storage?: Storage;
160
300
  }
161
- /** How an intent was submitted: typed input, a welcome-screen suggestion chip, or a refinement chip. */
162
- export type AssistantSubmitSource = 'input' | 'suggestion' | 'refinement';
301
+ /**
302
+ * How an intent was submitted: typed input, a welcome-screen suggestion chip, a refinement chip,
303
+ * or the host page (`ChatHandle.sendMessage` or the `initialPrompt` prop).
304
+ */
305
+ export type AssistantSubmitSource = 'input' | 'suggestion' | 'refinement' | 'external';
163
306
  /** An item within a viewed/clicked search_result pod. */
164
307
  export interface AssistantTrackedItem {
165
308
  itemId?: string;
166
309
  itemName?: string;
167
310
  variationId?: string;
168
311
  }
312
+ /** Agent mode a CTA opens or starts. */
313
+ export type AgentButtonClickMode = 'chat' | 'search' | 'qna' | 'recommendations' | 'query_refinement';
314
+ /** Page surface an agent CTA was clicked on. */
315
+ export type AgentButtonClickPageType = 'home' | 'plp' | 'pdp' | 'search' | 'collection' | 'email_campaign' | 'cart';
316
+ /** Where an agent CTA sits on the page. All fields are optional. */
317
+ export interface AgentButtonClickPlacement {
318
+ /** Stable label describing where the CTA is placed, e.g. `'header'` or `'search_bar'`. */
319
+ positionOnPage?: string;
320
+ /** Page surface where the CTA was clicked. */
321
+ pageType?: AgentButtonClickPageType;
322
+ /** 1-based index distinguishing CTA instances with the same mode and position during one page view. */
323
+ instanceId?: number;
324
+ }
325
+ /** Fields sent with `ai_agent_button_click` and passed to `onAgentButtonClick`. */
326
+ export interface AgentButtonClickPayload extends AgentButtonClickPlacement {
327
+ mode: AgentButtonClickMode;
328
+ /** Agent experience the CTA opens, sent as `agent_domain`. */
329
+ agentDomain: string;
330
+ }
169
331
  /**
170
332
  * Optional consumer callbacks fired alongside the built-in behavioral tracking.
171
333
  * Each fires immediately after its corresponding `trackAssistant*` beacon is sent,
172
334
  * so consumers can mirror ASA analytics into their own systems. All are optional.
173
335
  */
174
336
  export interface AsaCallbacks {
337
+ /** A CTA that opens the assistant (the built-in `Button`) was clicked. */
338
+ onAgentButtonClick?: (payload: AgentButtonClickPayload) => void;
175
339
  /** User submitted an intent (typed) or clicked a suggestion / refinement chip. */
176
340
  onAssistantSubmit?: (payload: {
177
341
  intent: string;
@@ -240,6 +404,7 @@ export type Translations = {
240
404
  'CioAsa.results.saleBadge'?: string;
241
405
  'CioAsa.refinement.ariaLabel'?: string;
242
406
  'CioAsa.error.message'?: string;
407
+ 'CioAsa.error.interrupted'?: string;
243
408
  };
244
409
  export interface ChatHeaderRenderProps {
245
410
  title: string;
@@ -0,0 +1,2 @@
1
+ import type { PersistedChat } from '../types';
2
+ export default function isPersistedChat(value: unknown): value is PersistedChat;
@@ -0,0 +1,45 @@
1
+ import { ChatMessage, PersistedChat } from '../types';
2
+ import { ItemNormalizer } from './chatThreads';
3
+ /** Conversation state that async work must always see current; one object so a reset is one call. */
4
+ export interface ChatSession {
5
+ /** Server thread id, sent with the next request. Never a `local-` id. */
6
+ serverThreadId: string | null;
7
+ /** Id the conversation is stored under: the server id, or a `local-` one before it exists. */
8
+ storageThreadId: string | null;
9
+ /** Local ids whose re-keyed replacement could not be verified yet; deleted on a later save. */
10
+ orphanIds: string[];
11
+ createdAt: number | null;
12
+ /** `updatedAt` of the last snapshot written or applied; newer stored records win over it. */
13
+ lastSyncedAt: number;
14
+ /** There are changes since the last write. */
15
+ dirty: boolean;
16
+ /** The user acted; a load that was still running must not overwrite the result. */
17
+ interacted: boolean;
18
+ /** Bumped to invalidate loads started before the conversation changed. */
19
+ loadRequest: number;
20
+ isStreaming: boolean;
21
+ /** The thread is streaming in another tab; sending is blocked meanwhile. */
22
+ foreignInFlight: boolean;
23
+ idCounter: number;
24
+ /** Per-instance suffix so two tabs cannot mint the same message id in the same millisecond. */
25
+ idSuffix: string;
26
+ /** Makes the card stored for a raw item: the one the conversation is rendered with. */
27
+ normalizeItem: ItemNormalizer;
28
+ }
29
+ export declare function createChatSession(initialThreadId?: string, normalizeItem?: ItemNormalizer): ChatSession;
30
+ /** Forget the current conversation. Returns the id it was stored under, if any. */
31
+ export declare function resetConversation(session: ChatSession): string | null;
32
+ export declare function nextMessageId(session: ChatSession): string;
33
+ /** Whether `messageId` was minted by this session rather than by another tab. */
34
+ export declare function isOwnMessage(session: ChatSession, messageId: string): boolean;
35
+ /** Makes a stored conversation the one this session continues. */
36
+ export declare function adoptStoredChat(session: ChatSession, chat: PersistedChat): void;
37
+ /** A copy that keeps the conversation `session` holds while `session` itself moves on. */
38
+ export declare function forkSession(session: ChatSession): ChatSession;
39
+ /** Makes `session` continue the conversation `from` kept, showing `messages`. */
40
+ export declare function adoptSession(session: ChatSession, from: ChatSession, messages: ChatMessage[]): void;
41
+ /** Builds the record to write and moves the session onto its key; `staleIds` are the records to retire once stored. */
42
+ export declare function prepareSnapshot(session: ChatSession, messages: ChatMessage[], now?: number): {
43
+ snapshot: PersistedChat;
44
+ staleIds: string[];
45
+ };
@@ -0,0 +1,63 @@
1
+ import type { ChatMessage, ChatPersistence, PersistedChat } from '../types';
2
+ import type { Product } from './productNormalizer';
3
+ /** Stored record format; records with another version are ignored on read. */
4
+ export declare const PERSISTED_CHAT_VERSION = 1;
5
+ /** Prefix of the `localStorage` key; the api key, domain and user id are appended to it. */
6
+ export declare const DEFAULT_PERSISTENCE_KEY = "cio-asa:chat";
7
+ /** One week, matching how long the agent itself remembers a thread. */
8
+ export declare const DEFAULT_PERSISTENCE_TTL_MS: number;
9
+ /** How long a stored answer written by another tab is still shown as "typing" before it is settled. */
10
+ export declare const IN_FLIGHT_GRACE_MS: number;
11
+ /** Random token for the ids that must not collide between tabs: threads, tabs and messages. */
12
+ export declare function randomId(): string;
13
+ /** Client-side thread id for a conversation the server has not named yet. */
14
+ export declare function createLocalThreadId(): string;
15
+ /**
16
+ * Per-tab id: survives a reload, differs between tabs, `undefined` on the server. A duplicated tab
17
+ * copies `sessionStorage`, so the id is read back on a reload only, and is kept there only while
18
+ * the page is hidden.
19
+ */
20
+ export declare function getTabId(): string | undefined;
21
+ /** Whether `threadId` was made by `createLocalThreadId` rather than by the server. */
22
+ export declare function isLocalThreadId(threadId: string | null | undefined): boolean;
23
+ /** Title for a thread list: the first user message, truncated with an ellipsis. */
24
+ export declare function getThreadTitle(messages: ChatMessage[]): string;
25
+ /** Counter to continue message ids from after a restore, so new ids never collide with restored ones. */
26
+ export declare function nextMessageCounter(messages: ChatMessage[]): number;
27
+ /** Whether the latest answer in `messages` has not finished streaming. */
28
+ export declare function isInFlight(messages: ChatMessage[]): boolean;
29
+ /** Settles answers stored mid-stream: `done` when they have content, an interrupted `error` otherwise. */
30
+ export declare function normalizeHydratedMessages(messages: ChatMessage[]): ChatMessage[];
31
+ /** Makes the product card for a raw search-result item. */
32
+ export type ItemNormalizer = (item: Record<string, unknown>) => Product;
33
+ /** Messages as stored: raw items replaced by the cards `normalize` makes; a restored group keeps its cards. */
34
+ export declare function compactMessages(messages: ChatMessage[], normalize: ItemNormalizer): ChatMessage[];
35
+ /**
36
+ * Both lists in stored order; a shared message takes the incoming version unless `preferStored`.
37
+ * A settled incoming version still replaces a stored one mid-stream: it is the later one by definition.
38
+ */
39
+ export declare function mergeMessages(stored: ChatMessage[], incoming: ChatMessage[], preferStored?: boolean): ChatMessage[];
40
+ /**
41
+ * Saves `snapshot`, then deletes `staleIds` once the new record is read back. Stores may fail
42
+ * without throwing, so both steps are checked by reading: `saved` is whether the snapshot landed,
43
+ * `leftover` the stale ids still stored.
44
+ */
45
+ export declare function saveThreadAndRetireStale(store: ChatPersistence, snapshot: PersistedChat, staleIds: string[]): Promise<{
46
+ saved: boolean;
47
+ leftover: string[];
48
+ }>;
49
+ /** Finds the server-keyed record another tab re-keyed a local thread into, by its first message. */
50
+ export declare function findRekeyedThread(store: ChatPersistence, firstMessageId: string | undefined): Promise<PersistedChat | null>;
51
+ /** Whether a stored thread still reads as streaming: this tab's own always, another tab's only within the grace. */
52
+ export declare function isThreadStreaming(chat: PersistedChat, now?: number): boolean;
53
+ /** Ms left to show a stored answer as streaming in another tab; `0` when it should be settled. */
54
+ export declare function foreignStreamRemainingMs(chat: PersistedChat, now?: number): number;
55
+ /**
56
+ * Moves every thread of `from` into `to`; copies of the chat on screen are not copied, its owner
57
+ * saves it. A source record is deleted only once `to` holds it after every copy was made, so one
58
+ * copy evicting another, or a save that did not land, leaves the source in place.
59
+ */
60
+ export declare function moveThreads(from: ChatPersistence, to: ChatPersistence, onScreen?: {
61
+ threadIds?: string[];
62
+ firstMessageId?: string;
63
+ }): Promise<void>;
@@ -0,0 +1,5 @@
1
+ import type ConstructorIOClient from '@constructor-io/constructorio-client-javascript';
2
+ import type { ConstructorClientOptions } from '@constructor-io/constructorio-client-javascript/lib/types';
3
+ export type ClientIdentity = Pick<ConstructorClientOptions, 'apiKey' | 'userId'>;
4
+ /** `options` is private in the client's typings; read it here only, so a client bump fails a test, not storage keys. */
5
+ export declare function readClientOptions(client: ConstructorIOClient | null | undefined): ClientIdentity | undefined;
@@ -0,0 +1,26 @@
1
+ import type { ChatPersistence, ClearPersistedConversationsOptions, LocalStoragePersistenceOptions, StorageArea } from '../types';
2
+ /** No user id, or an empty one: the conversation belongs to a guest. */
3
+ export declare const isGuest: (userId: string | number | null | undefined) => boolean;
4
+ /** The id the client and storage use: `undefined` for a guest (no id, `null` or empty). */
5
+ export declare const shopperId: (userId: string | number | null | undefined) => string | undefined;
6
+ /** Guests live in `sessionStorage`, so their history ends with the tab; shoppers keep theirs in `localStorage`. */
7
+ export declare function storageAreaFor(userId: string | null | undefined): StorageArea;
8
+ /** Namespace the provider stores under: api key, domain and, for a signed-in shopper, the user id. */
9
+ export declare function persistenceNamespace(parts: {
10
+ apiKey: string;
11
+ domain?: string;
12
+ userId?: string | null;
13
+ }): string;
14
+ /**
15
+ * Deletes every stored conversation of one shopper, or of the guest without `userId`; open tabs
16
+ * follow. Resolves once the deletion is recorded.
17
+ */
18
+ export declare function clearPersistedConversations(options: ClearPersistedConversationsOptions): Promise<void>;
19
+ /** Conversation store on top of `localStorage`; one key per namespace, merged across tabs. */
20
+ export declare function createLocalStoragePersistence(options?: LocalStoragePersistenceOptions): ChatPersistence;
21
+ /**
22
+ * One store per namespace and area for the page, so a chat mounted again finds the answers still
23
+ * streaming into it. Not shared on the server, where the map would outlive the request.
24
+ */
25
+ export declare function sharedLocalStoragePersistence(namespace: string, storageArea: StorageArea): ChatPersistence;
26
+ export default createLocalStoragePersistence;
@@ -1,2 +1,2 @@
1
- declare const _default: "1.7.2";
1
+ declare const _default: "1.9.0";
2
2
  export default _default;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@constructor-io/constructorio-ui-asa",
3
- "version": "1.7.2",
3
+ "version": "1.9.0",
4
4
  "description": "Constructor.io AI Shopping Agent UI library for web applications",
5
5
  "author": "constructor.io",
6
6
  "license": "MIT",
@@ -82,7 +82,7 @@
82
82
  "@babel/preset-env": "^7.28.3",
83
83
  "@babel/preset-react": "^7.27.1",
84
84
  "@babel/preset-typescript": "^7.27.1",
85
- "@constructor-io/constructorio-client-javascript": "2.93.1",
85
+ "@constructor-io/constructorio-client-javascript": "2.94.0",
86
86
  "@constructor-io/constructorio-ui-components": "^1.6.0",
87
87
  "@cspell/eslint-plugin": "^6.18.1",
88
88
  "@storybook/addon-a11y": "^8.6.14",