@shop-prompter/react 0.1.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.
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@shop-prompter/react",
3
+ "version": "0.1.1",
4
+ "main": "dist/index.js",
5
+ "module": "dist/index.mjs",
6
+ "types": "dist/index.d.ts",
7
+ "publishConfig": {
8
+ "access": "public"
9
+ },
10
+ "peerDependencies": {
11
+ "react": "^18.0.0"
12
+ },
13
+ "scripts": {
14
+ "build": "tsup src/index.ts --format cjs,esm --dts"
15
+ },
16
+ "devDependencies": {
17
+ "react": "^19.2.7",
18
+ "@shop-prompter/core": "workspace:*",
19
+ "tsup": "^8.5.1",
20
+ "typescript": "^6.0.3"
21
+ },
22
+ "dependencies": {
23
+ "uuid": "^14.0.0"
24
+ }
25
+ }
@@ -0,0 +1,80 @@
1
+ import { v4 as uuidv4 } from 'uuid';
2
+
3
+ const SESSION_KEY = 'shopprompter_sessionId';
4
+ const USER_KEY = 'shopprompter_userId';
5
+
6
+ export interface SessionHandler {
7
+ initialize(userIdentifier?: string): void;
8
+ regenerate(): void;
9
+ getSessionId(): string;
10
+ getUserId(): string;
11
+ }
12
+
13
+ export class DefaultSessionHandler implements SessionHandler {
14
+ private sessionId: string = '';
15
+ private userId: string = '';
16
+
17
+ initialize(userIdentifier?: string): void {
18
+ // Get or create user identifier
19
+ this.userId = userIdentifier || this.getPersistedUserId() || uuidv4();
20
+ this.persistUserId(this.userId);
21
+
22
+ // Get or create session ID
23
+ this.sessionId = this.getPersistedSessionId() || this.userId;
24
+ this.persistSessionId(this.sessionId);
25
+ }
26
+
27
+ regenerate(): void {
28
+ this.sessionId = uuidv4();
29
+ this.persistSessionId(this.sessionId);
30
+ }
31
+
32
+ getSessionId(): string {
33
+ if (!this.sessionId) {
34
+ this.initialize();
35
+ }
36
+ return this.sessionId;
37
+ }
38
+
39
+ getUserId(): string {
40
+ if (!this.userId) {
41
+ this.initialize();
42
+ }
43
+ return this.userId;
44
+ }
45
+
46
+ private getPersistedSessionId(): string | null {
47
+ try {
48
+ return localStorage.getItem(SESSION_KEY);
49
+ } catch {
50
+ return null;
51
+ }
52
+ }
53
+
54
+ private persistSessionId(sessionId: string): void {
55
+ try {
56
+ localStorage.setItem(SESSION_KEY, sessionId);
57
+ } catch {
58
+ // localStorage not available
59
+ }
60
+ }
61
+
62
+ private getPersistedUserId(): string | null {
63
+ try {
64
+ return localStorage.getItem(USER_KEY);
65
+ } catch {
66
+ return null;
67
+ }
68
+ }
69
+
70
+ private persistUserId(userId: string): void {
71
+ try {
72
+ localStorage.setItem(USER_KEY, userId);
73
+ } catch {
74
+ // localStorage not available
75
+ }
76
+ }
77
+ }
78
+
79
+ // Singleton instance
80
+ export const sessionHandler = new DefaultSessionHandler();
@@ -0,0 +1,389 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+
3
+ import {
4
+ ShopPrompterApiConfiguration,
5
+ ShopPrompterResponse,
6
+ ShopPrompterStreamEvent,
7
+ ShopPrompterStreamTextEvent,
8
+ ShopPrompterStreamProductSkusEvent,
9
+ ShopPrompterStreamNavigationEvent,
10
+ ShopPrompterStreamCartEvent,
11
+ ShopPrompterNavigationOperation,
12
+ ShopPrompterCartOperation,
13
+ AgentExperience,
14
+ } from './shop-prompter.types'
15
+
16
+ export interface ShopPrompterApi {
17
+ setApiConfig(config: ShopPrompterApiConfiguration): void
18
+ execute(
19
+ prompt: string,
20
+ userId: string,
21
+ sessionId: string,
22
+ ): Promise<ShopPrompterResponse>
23
+ executeStream(
24
+ prompt: string,
25
+ userId: string,
26
+ sessionId: string,
27
+ onEvent: (event: ShopPrompterStreamEvent) => void,
28
+ ): Promise<void>
29
+ getAgentExperience(): Promise<AgentExperience | null>
30
+ }
31
+
32
+ export class DefaultShopPrompterApi implements ShopPrompterApi {
33
+ private config: ShopPrompterApiConfiguration | null = null
34
+
35
+ setApiConfig(config: ShopPrompterApiConfiguration): void {
36
+ this.config = config
37
+ }
38
+
39
+ async execute(
40
+ prompt: string,
41
+ userId: string,
42
+ sessionId: string,
43
+ ): Promise<ShopPrompterResponse> {
44
+ if (!this.config) {
45
+ throw new Error('API configuration not set. Call setApiConfig first.')
46
+ }
47
+
48
+ const response = await fetch(`${this.config.baseUrl}/interaction`, {
49
+ method: 'POST',
50
+ headers: {
51
+ 'Content-Type': 'application/json',
52
+ 'X-Api-Key': this.config.apiKey,
53
+ },
54
+ body: JSON.stringify({
55
+ prompt,
56
+ userId,
57
+ sessionId,
58
+ }),
59
+ })
60
+
61
+ if (!response.ok) {
62
+ throw new Error(`API error: ${response.status}`)
63
+ }
64
+
65
+ return response.json()
66
+ }
67
+
68
+ async executeStream(
69
+ prompt: string,
70
+ userId: string,
71
+ sessionId: string,
72
+ onEvent: (event: ShopPrompterStreamEvent) => void,
73
+ cartContext?: { sku: string; quantity: number }[],
74
+ ): Promise<void> {
75
+ if (!this.config) {
76
+ throw new Error('API configuration not set. Call setApiConfig first.')
77
+ }
78
+
79
+ const url = `${this.config.baseUrl}/interaction/stream`
80
+ console.log('ShopPrompter Request:', {
81
+ url,
82
+ prompt,
83
+ userId,
84
+ sessionId,
85
+ cartContext,
86
+ })
87
+
88
+ let response: Response
89
+ try {
90
+ response = await fetch(url, {
91
+ method: 'POST',
92
+ headers: {
93
+ 'Content-Type': 'application/json',
94
+ 'X-Api-Key': this.config.apiKey,
95
+ Accept: 'text/event-stream',
96
+ 'Cache-Control': 'no-cache',
97
+ },
98
+ body: JSON.stringify({
99
+ prompt,
100
+ userId,
101
+ sessionId,
102
+ cart: cartContext,
103
+ }),
104
+ })
105
+ } catch (fetchError) {
106
+ console.error('ShopPrompter Fetch Error:', fetchError)
107
+ onEvent({
108
+ type: 'error',
109
+ reason: `Failed to fetch: ${fetchError instanceof Error ? fetchError.message : 'Network error'}`,
110
+ })
111
+ return
112
+ }
113
+
114
+ console.log('ShopPrompter Response:', response.status)
115
+
116
+ if (!response.ok) {
117
+ const errorText = await response.text().catch(() => '')
118
+ console.error('ShopPrompter API Error:', response.status, errorText)
119
+ onEvent({
120
+ type: 'error',
121
+ reason: `API error: ${response.status} - ${errorText}`,
122
+ })
123
+ return
124
+ }
125
+
126
+ const reader = response.body?.getReader()
127
+ if (!reader) {
128
+ onEvent({ type: 'error', reason: 'No response body' })
129
+ return
130
+ }
131
+
132
+ const decoder = new TextDecoder()
133
+ let buffer = ''
134
+
135
+ try {
136
+ while (true) {
137
+ const { done, value } = await reader.read()
138
+
139
+ if (done) {
140
+ console.log('ShopPrompter Stream done')
141
+ onEvent({ type: 'close' })
142
+ break
143
+ }
144
+
145
+ const chunk = decoder.decode(value, { stream: true })
146
+ console.log('ShopPrompter Raw chunk:', chunk)
147
+ buffer += chunk
148
+ const lines = buffer.split('\n')
149
+ buffer = lines.pop() || ''
150
+
151
+ let currentEventType = ''
152
+ for (const line of lines) {
153
+ console.log('ShopPrompter Line:', line)
154
+ if (line.startsWith('event:')) {
155
+ // Capture the event type for the next data line
156
+ currentEventType = line.slice(6).trim()
157
+ console.log('ShopPrompter Event type:', currentEventType)
158
+
159
+ // If it's an error event, we need to handle it
160
+ if (currentEventType === 'error') {
161
+ onEvent({
162
+ type: 'error',
163
+ reason:
164
+ 'Server returned an error event. Check backend configuration.',
165
+ })
166
+ }
167
+ } else if (line.startsWith('data:')) {
168
+ const data = line.slice(5).trim()
169
+ console.log('ShopPrompter Data:', data)
170
+ if (data) {
171
+ // If we got an error event type, try to parse error details
172
+ if (currentEventType === 'error') {
173
+ try {
174
+ const errorData = JSON.parse(data)
175
+ onEvent({
176
+ type: 'error',
177
+ reason:
178
+ errorData.message ||
179
+ errorData.error ||
180
+ 'Unknown server error',
181
+ })
182
+ } catch {
183
+ onEvent({
184
+ type: 'error',
185
+ reason: data || 'Unknown server error',
186
+ })
187
+ }
188
+ } else {
189
+ const event = this.parseEvent(data)
190
+ console.log('ShopPrompter Parsed event:', event)
191
+ if (event) {
192
+ onEvent(event)
193
+ }
194
+ }
195
+ }
196
+ currentEventType = '' // Reset after processing data
197
+ }
198
+ }
199
+ }
200
+ } catch (error) {
201
+ console.error('ShopPrompter Stream error:', error)
202
+ onEvent({
203
+ type: 'error',
204
+ reason: error instanceof Error ? error.message : 'Unknown error',
205
+ })
206
+ }
207
+ }
208
+
209
+ async getAgentExperience(): Promise<AgentExperience | null> {
210
+ // todo: Return cached version if available
211
+
212
+ if (!this.config) {
213
+ console.warn('API configuration not set. Call setApiConfig first.')
214
+ return null
215
+ }
216
+
217
+ try {
218
+ const response = await fetch(`${this.config.baseUrl}/agent-experience`, {
219
+ method: 'GET',
220
+ headers: {
221
+ 'Content-Type': 'application/json',
222
+ 'X-Api-Key': this.config.apiKey,
223
+ },
224
+ })
225
+
226
+ if (!response.ok) {
227
+ console.error(`Failed to fetch agent experience: ${response.status}`)
228
+ return null
229
+ }
230
+
231
+ const data: AgentExperience = await response.json()
232
+
233
+ console.log(
234
+ '## ~ DefaultShopPrompterApi ~ getAgentExperience ~ data:',
235
+ data,
236
+ )
237
+
238
+ return data
239
+ } catch (error) {
240
+ console.error('Error fetching agent experience:', error)
241
+ return null
242
+ }
243
+ }
244
+
245
+ private parseEvent(data: string): ShopPrompterStreamEvent | null {
246
+ try {
247
+ const json = JSON.parse(data)
248
+
249
+ // Parse based on event type
250
+ // Handle both "token" and "text" fields for text events
251
+ if (json.token !== undefined) {
252
+ // Skip "close" token - it's a signal to end the stream
253
+ if (json.token === 'close') {
254
+ return { type: 'close' }
255
+ }
256
+ return {
257
+ type: 'text',
258
+ text: json.token,
259
+ } as ShopPrompterStreamTextEvent
260
+ }
261
+
262
+ if (json.text !== undefined) {
263
+ return {
264
+ type: 'text',
265
+ text: json.text,
266
+ } as ShopPrompterStreamTextEvent
267
+ }
268
+
269
+ if (json.skus !== undefined) {
270
+ return {
271
+ type: 'productSkus',
272
+ skus: json.skus,
273
+ } as ShopPrompterStreamProductSkusEvent
274
+ }
275
+
276
+ // Also check for products array with SKUs
277
+ if (json.products !== undefined && Array.isArray(json.products)) {
278
+ const skus = json.products.map((p: any) => p.sku).filter(Boolean)
279
+ if (skus.length > 0) {
280
+ return {
281
+ type: 'productSkus',
282
+ skus: skus,
283
+ } as ShopPrompterStreamProductSkusEvent
284
+ }
285
+ }
286
+
287
+ // Check for productSkus field directly
288
+ if (json.productSkus !== undefined) {
289
+ return {
290
+ type: 'productSkus',
291
+ skus: json.productSkus,
292
+ } as ShopPrompterStreamProductSkusEvent
293
+ }
294
+
295
+ // Handle output array format: {"output": [{"type": "product", "data": [...]}]}
296
+ if (json.output !== undefined && Array.isArray(json.output)) {
297
+ for (const item of json.output) {
298
+ if (
299
+ item.type === 'product' &&
300
+ item.data &&
301
+ Array.isArray(item.data)
302
+ ) {
303
+ // Extract SKUs from product data - handle both sku field and direct string SKUs
304
+ const skus = item.data
305
+ .filter((p: any) => p !== null)
306
+ .map((p: any) => (typeof p === 'string' ? p : p?.sku))
307
+ .filter(Boolean)
308
+ if (skus.length > 0) {
309
+ return {
310
+ type: 'productSkus',
311
+ skus: skus,
312
+ } as ShopPrompterStreamProductSkusEvent
313
+ }
314
+ }
315
+ // Also check for skus directly in output item
316
+ if (
317
+ item.type === 'product' &&
318
+ item.skus &&
319
+ Array.isArray(item.skus)
320
+ ) {
321
+ return {
322
+ type: 'productSkus',
323
+ skus: item.skus,
324
+ } as ShopPrompterStreamProductSkusEvent
325
+ }
326
+ }
327
+ }
328
+
329
+ if (json.navigation !== undefined) {
330
+ return {
331
+ type: 'navigation',
332
+ navigationOperations: json.navigation.map((nav: any) => ({
333
+ name: nav.name,
334
+ route: nav.path || nav.route,
335
+ description: nav.description,
336
+ })) as ShopPrompterNavigationOperation[],
337
+ } as ShopPrompterStreamNavigationEvent
338
+ }
339
+
340
+ if (json.cart !== undefined) {
341
+ return {
342
+ type: 'cart',
343
+ operations: json.cart.map((op: any) => ({
344
+ sku: op.sku,
345
+ action: op.action,
346
+ quantity: op.quantity || 1,
347
+ })) as ShopPrompterCartOperation[],
348
+ } as ShopPrompterStreamCartEvent
349
+ }
350
+
351
+ // Also check for cart in output array format
352
+ if (json.output !== undefined && Array.isArray(json.output)) {
353
+ for (const item of json.output) {
354
+ if (item.type === 'cart' && item.data) {
355
+ return {
356
+ type: 'cart',
357
+ operations: (Array.isArray(item.data)
358
+ ? item.data
359
+ : [item.data]
360
+ ).map((op: any) => ({
361
+ sku: op.sku,
362
+ action: op.action || 'add',
363
+ quantity: op.quantity || 1,
364
+ })) as ShopPrompterCartOperation[],
365
+ } as ShopPrompterStreamCartEvent
366
+ }
367
+ }
368
+ }
369
+
370
+ if (json.close !== undefined || json.type === 'close') {
371
+ return { type: 'close' }
372
+ }
373
+
374
+ return null
375
+ } catch {
376
+ // If it's plain text, treat it as a text event
377
+ if (data && typeof data === 'string') {
378
+ return {
379
+ type: 'text',
380
+ text: data,
381
+ } as ShopPrompterStreamTextEvent
382
+ }
383
+ return null
384
+ }
385
+ }
386
+ }
387
+
388
+ // Singleton instance
389
+ export const shopPrompterApi = new DefaultShopPrompterApi()
@@ -0,0 +1,87 @@
1
+ // ShopPrompter API Types
2
+
3
+ export interface ShopPrompterApiConfiguration {
4
+ baseUrl: string
5
+ apiKey: string
6
+ }
7
+
8
+ export interface ShopPrompterResponse {
9
+ text?: string
10
+ products?: ShopPrompterProduct[]
11
+ skus?: string[]
12
+ navigation?: ShopPrompterNavigationOperation[]
13
+ cart?: ShopPrompterCartOperation[]
14
+ }
15
+
16
+ export interface ShopPrompterProduct {
17
+ sku: string
18
+ name: string
19
+ price: number
20
+ currency: string
21
+ images: string[]
22
+ }
23
+
24
+ // Stream Events
25
+ export type ShopPrompterStreamEvent =
26
+ | ShopPrompterStreamTextEvent
27
+ | ShopPrompterStreamProductSkusEvent
28
+ | ShopPrompterStreamNavigationEvent
29
+ | ShopPrompterStreamCartEvent
30
+ | ShopPrompterStreamCloseEvent
31
+ | ShopPrompterErrorEvent
32
+
33
+ export interface ShopPrompterStreamTextEvent {
34
+ type: 'text'
35
+ text: string
36
+ }
37
+
38
+ export interface ShopPrompterStreamProductSkusEvent {
39
+ type: 'productSkus'
40
+ skus: string[]
41
+ }
42
+
43
+ export interface ShopPrompterStreamNavigationEvent {
44
+ type: 'navigation'
45
+ navigationOperations: ShopPrompterNavigationOperation[]
46
+ }
47
+
48
+ export interface ShopPrompterNavigationOperation {
49
+ name: string
50
+ route: string
51
+ description: string
52
+ }
53
+
54
+ export interface ShopPrompterStreamCartEvent {
55
+ type: 'cart'
56
+ operations: ShopPrompterCartOperation[]
57
+ }
58
+
59
+ export interface ShopPrompterCartOperation {
60
+ sku: string
61
+ action: 'add' | 'remove'
62
+ quantity: number
63
+ }
64
+
65
+ export interface ShopPrompterStreamCloseEvent {
66
+ type: 'close'
67
+ }
68
+
69
+ export interface ShopPrompterErrorEvent {
70
+ type: 'error'
71
+ reason: string
72
+ }
73
+
74
+ // Agent Experience Types
75
+ export interface AgentExperience {
76
+ agentName: string
77
+ agentLogoUrl: string
78
+ loadingIndicatorUrl: string
79
+ greeting: {
80
+ title: string
81
+ message: string
82
+ }
83
+ hintText: string
84
+ predefinedPrompts: Array<{
85
+ prompt: string
86
+ }>
87
+ }
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export { default as ShopPrompter } from './shop-prompter.component';
@@ -0,0 +1,32 @@
1
+ export interface ProductCategory {
2
+ id: number;
3
+ name: string;
4
+ }
5
+
6
+ export interface Product {
7
+ name: string;
8
+ sku: string;
9
+ images: string[];
10
+ category: ProductCategory | null;
11
+ price: number;
12
+ currency: string;
13
+ description?: string;
14
+ extras: Record<string, unknown>;
15
+ }
16
+
17
+ export interface Pagination {
18
+ last: number;
19
+ current: number;
20
+ numItemsPerPage: number;
21
+ pageCount: number;
22
+ totalCount: number;
23
+ }
24
+
25
+ export interface ProductSearchResponse {
26
+ products: Product[];
27
+ pagination: Pagination;
28
+ }
29
+
30
+ export interface ProductSkuResponse {
31
+ products: Product[];
32
+ }