@shop-prompter/react 0.1.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,25 +1,26 @@
1
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
- }
2
+ "name": "@shop-prompter/react",
3
+ "version": "1.0.0",
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
+ "devDependencies": {
14
+ "react": "^19.2.7",
15
+ "tsup": "^8.5.1",
16
+ "typescript": "^6.0.3",
17
+ "@shop-prompter/core": "0.0.0"
18
+ },
19
+ "dependencies": {
20
+ "marked": "^18.0.5",
21
+ "uuid": "^14.0.0"
22
+ },
23
+ "scripts": {
24
+ "build": "tsup src/index.ts --format cjs,esm --dts"
25
+ }
25
26
  }
@@ -0,0 +1,9 @@
1
+ // SVG Path Constants for Thumbs-Up and Thumbs-Down Icons
2
+
3
+ export const THUMBS_UP_WRIST = 'M7 10v12';
4
+ export const THUMBS_UP_HAND = 'M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2h0a3.13 3.13 0 0 1 3 3.88Z';
5
+
6
+ export const THUMBS_DOWN_WRIST = 'M17 14V2';
7
+ export const THUMBS_DOWN_HAND = 'M9 18.12 10 14H4.17a2 2 0 0 1-1.92-2.56l2.33-8A2 2 0 0 1 6.5 2H20a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-2.76a2 2 0 0 0-1.79 1.11L12 22h0a3.13 3.13 0 0 1-3-3.88Z';
8
+
9
+ export const THINKING_SPINNER = 'M21 12a9 9 0 1 1-6.219-8.56';
@@ -0,0 +1,120 @@
1
+ import { ShopPrompterApiConfiguration } from './shop-prompter.types'
2
+
3
+ export interface ProductPricing {
4
+ taxPercentage: number
5
+ gross: number
6
+ currency: string | undefined
7
+ }
8
+
9
+ export interface ProductAvailability {
10
+ count: number
11
+ alwaysAvailable: boolean
12
+ }
13
+
14
+ export interface ProductCategory {
15
+ key: string
16
+ name: Record<string, string>
17
+ }
18
+
19
+ export interface ProductLocalization {
20
+ name: string
21
+ description: string
22
+ }
23
+
24
+ export interface ProductApiResponse {
25
+ pricing: ProductPricing
26
+ availability: ProductAvailability
27
+ images: string[]
28
+ categories: ProductCategory[]
29
+ sku: string
30
+ localization: Record<string, ProductLocalization>
31
+ }
32
+
33
+ export interface ProductApi {
34
+ setApiConfig(config: ShopPrompterApiConfiguration): void
35
+ getProducts(skus: string[]): Promise<ProductApiResponse[]>
36
+ isCached(skus: string[]): boolean
37
+ getCached(skus: string[]): ProductApiResponse[]
38
+ }
39
+
40
+ export class DefaultProductApi implements ProductApi {
41
+ private config: ShopPrompterApiConfiguration | null = null
42
+ private cache = new Map<string, ProductApiResponse | null>()
43
+
44
+ setApiConfig(config: ShopPrompterApiConfiguration): void {
45
+ this.config = config
46
+ }
47
+
48
+ isCached(skus: string[]): boolean {
49
+ if (!skus || skus.length === 0) return true
50
+ return skus.every(sku => this.cache.has(sku))
51
+ }
52
+
53
+ getCached(skus: string[]): ProductApiResponse[] {
54
+ if (!skus || skus.length === 0) return []
55
+ return skus.map(sku => this.cache.get(sku)).filter(Boolean) as ProductApiResponse[]
56
+ }
57
+
58
+ async getProducts(skus: string[]): Promise<ProductApiResponse[]> {
59
+ if (!this.config) {
60
+ throw new Error('API configuration not set. Call setApiConfig first.')
61
+ }
62
+
63
+ const cachedProducts: ProductApiResponse[] = []
64
+ const missingSkus: string[] = []
65
+
66
+ for (const sku of skus) {
67
+ if (this.cache.has(sku)) {
68
+ const cachedVal = this.cache.get(sku)
69
+ if (cachedVal) {
70
+ cachedProducts.push(cachedVal)
71
+ }
72
+ } else {
73
+ missingSkus.push(sku)
74
+ }
75
+ }
76
+
77
+ if (missingSkus.length === 0) {
78
+ // Maintain the order of requested SKUs from cached products
79
+ return skus.map(sku => this.cache.get(sku)).filter(Boolean) as ProductApiResponse[]
80
+ }
81
+
82
+ const baseUrl = this.config.productsBaseUrl || this.config.baseUrl
83
+ const url = `${baseUrl}/products?skus=${encodeURIComponent(missingSkus.join(','))}`
84
+
85
+ const headers: Record<string, string> = {
86
+ 'Content-Type': 'application/json',
87
+ }
88
+
89
+ const response = await fetch(url, {
90
+ method: 'GET',
91
+ headers,
92
+ })
93
+
94
+ if (!response.ok) {
95
+ throw new Error(`Product API error: ${response.status}`)
96
+ }
97
+
98
+ const newProducts: ProductApiResponse[] = await response.json()
99
+
100
+ const returnedSkus = new Set<string>()
101
+ for (const p of newProducts) {
102
+ if (p.sku) {
103
+ this.cache.set(p.sku, p)
104
+ returnedSkus.add(p.sku)
105
+ }
106
+ }
107
+
108
+ // Cache missing SKUs as null to prevent repeating API requests
109
+ for (const sku of missingSkus) {
110
+ if (!returnedSkus.has(sku)) {
111
+ this.cache.set(sku, null)
112
+ }
113
+ }
114
+
115
+ // Return all requested products maintaining the initial skus array order
116
+ return skus.map(sku => this.cache.get(sku)).filter(Boolean) as ProductApiResponse[]
117
+ }
118
+ }
119
+
120
+ export const productApi = new DefaultProductApi()
@@ -2,14 +2,54 @@ import { v4 as uuidv4 } from 'uuid';
2
2
 
3
3
  const SESSION_KEY = 'shopprompter_sessionId';
4
4
  const USER_KEY = 'shopprompter_userId';
5
+ const SESSIONS_KEY = 'shopprompter_sessions';
6
+ const MAX_SESSIONS = 3;
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // Persisted message shape stored in localStorage
10
+ // ---------------------------------------------------------------------------
11
+
12
+ export interface PersistedMessage {
13
+ id: string;
14
+ role: 'user' | 'assistant';
15
+ content: string;
16
+ productSkus?: string[];
17
+ /** User feedback on an assistant message. Only relevant when role === 'assistant'. */
18
+ feedback?: 'upvote' | 'downvote' | null;
19
+ }
20
+
21
+ interface SessionData {
22
+ createdAt: number;
23
+ messages: PersistedMessage[];
24
+ }
25
+
26
+ interface SessionStore {
27
+ [sessionId: string]: SessionData;
28
+ }
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Interface
32
+ // ---------------------------------------------------------------------------
5
33
 
6
34
  export interface SessionHandler {
7
35
  initialize(userIdentifier?: string): void;
8
36
  regenerate(): void;
9
37
  getSessionId(): string;
10
38
  getUserId(): string;
39
+ saveUserMessage(sessionId: string, message: PersistedMessage): void;
40
+ saveAssistantMessage(sessionId: string, message: PersistedMessage): void;
41
+ loadMessages(sessionId: string): PersistedMessage[];
42
+ updateMessageFeedback(
43
+ sessionId: string,
44
+ messageId: string,
45
+ feedback: 'upvote' | 'downvote' | null,
46
+ ): void;
11
47
  }
12
48
 
49
+ // ---------------------------------------------------------------------------
50
+ // Implementation
51
+ // ---------------------------------------------------------------------------
52
+
13
53
  export class DefaultSessionHandler implements SessionHandler {
14
54
  private sessionId: string = '';
15
55
  private userId: string = '';
@@ -27,6 +67,8 @@ export class DefaultSessionHandler implements SessionHandler {
27
67
  regenerate(): void {
28
68
  this.sessionId = uuidv4();
29
69
  this.persistSessionId(this.sessionId);
70
+ // The new session's store entry (and eviction) is deferred until the first
71
+ // message is saved, keeping regenerate() lightweight.
30
72
  }
31
73
 
32
74
  getSessionId(): string {
@@ -43,6 +85,117 @@ export class DefaultSessionHandler implements SessionHandler {
43
85
  return this.userId;
44
86
  }
45
87
 
88
+ // -------------------------------------------------------------------------
89
+ // Message persistence
90
+ // -------------------------------------------------------------------------
91
+
92
+ /** Persist a user message immediately when it is sent. */
93
+ saveUserMessage(sessionId: string, message: PersistedMessage): void {
94
+ try {
95
+ const store = this.loadSessionStore();
96
+ if (!store[sessionId]) {
97
+ // New session entry — evict the oldest existing session first if needed.
98
+ this.evictIfNeeded(store);
99
+ store[sessionId] = { createdAt: Date.now(), messages: [] };
100
+ }
101
+ store[sessionId].messages.push(message);
102
+ this.saveSessionStore(store);
103
+ } catch {
104
+ // localStorage not available or quota exceeded — silently ignore.
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Persist the fully-streamed assistant message.
110
+ * If the message ID already exists in the store (shouldn't normally happen)
111
+ * it is replaced; otherwise it is appended.
112
+ */
113
+ saveAssistantMessage(sessionId: string, message: PersistedMessage): void {
114
+ try {
115
+ const store = this.loadSessionStore();
116
+ if (!store[sessionId]) {
117
+ store[sessionId] = { createdAt: Date.now(), messages: [] };
118
+ }
119
+ const msgs = store[sessionId].messages;
120
+ const existingIdx = msgs.findIndex((m) => m.id === message.id);
121
+ if (existingIdx !== -1) {
122
+ msgs[existingIdx] = message;
123
+ } else {
124
+ msgs.push(message);
125
+ }
126
+ this.saveSessionStore(store);
127
+ } catch {
128
+ // localStorage not available or quota exceeded — silently ignore.
129
+ }
130
+ }
131
+
132
+ /** Return the persisted messages for the given session (newest first order preserved). */
133
+ loadMessages(sessionId: string): PersistedMessage[] {
134
+ try {
135
+ const store = this.loadSessionStore();
136
+ return store[sessionId]?.messages ?? [];
137
+ } catch {
138
+ return [];
139
+ }
140
+ }
141
+
142
+ /** Update the feedback field of a specific persisted message. */
143
+ updateMessageFeedback(
144
+ sessionId: string,
145
+ messageId: string,
146
+ feedback: 'upvote' | 'downvote' | null,
147
+ ): void {
148
+ try {
149
+ const store = this.loadSessionStore();
150
+ const session = store[sessionId];
151
+ if (!session) return;
152
+
153
+ const msg = session.messages.find((m) => m.id === messageId);
154
+ if (!msg) return;
155
+
156
+ msg.feedback = feedback;
157
+ this.saveSessionStore(store);
158
+ } catch {
159
+ // localStorage not available — silently ignore.
160
+ }
161
+ }
162
+
163
+ // -------------------------------------------------------------------------
164
+ // Private helpers
165
+ // -------------------------------------------------------------------------
166
+
167
+ /**
168
+ * Remove the single oldest session from the store when it already holds
169
+ * MAX_SESSIONS entries. Mutates the store object in place.
170
+ */
171
+ private evictIfNeeded(store: SessionStore): void {
172
+ const sessionIds = Object.keys(store);
173
+ if (sessionIds.length < MAX_SESSIONS) return;
174
+
175
+ let oldestId = sessionIds[0];
176
+ let oldestTime = store[oldestId].createdAt;
177
+ for (const id of sessionIds) {
178
+ if (store[id].createdAt < oldestTime) {
179
+ oldestId = id;
180
+ oldestTime = store[id].createdAt;
181
+ }
182
+ }
183
+ delete store[oldestId];
184
+ }
185
+
186
+ private loadSessionStore(): SessionStore {
187
+ try {
188
+ const raw = localStorage.getItem(SESSIONS_KEY);
189
+ return raw ? (JSON.parse(raw) as SessionStore) : {};
190
+ } catch {
191
+ return {};
192
+ }
193
+ }
194
+
195
+ private saveSessionStore(store: SessionStore): void {
196
+ localStorage.setItem(SESSIONS_KEY, JSON.stringify(store));
197
+ }
198
+
46
199
  private getPersistedSessionId(): string | null {
47
200
  try {
48
201
  return localStorage.getItem(SESSION_KEY);
@@ -27,6 +27,11 @@ export interface ShopPrompterApi {
27
27
  onEvent: (event: ShopPrompterStreamEvent) => void,
28
28
  ): Promise<void>
29
29
  getAgentExperience(): Promise<AgentExperience | null>
30
+ sendFeedback(
31
+ threadId: string,
32
+ message: string,
33
+ feedback: 'upvote' | 'downvote' | 'clear',
34
+ ): Promise<void>
30
35
  }
31
36
 
32
37
  export class DefaultShopPrompterApi implements ShopPrompterApi {
@@ -36,6 +41,21 @@ export class DefaultShopPrompterApi implements ShopPrompterApi {
36
41
  this.config = config
37
42
  }
38
43
 
44
+ private getHeaders(): Record<string, string> {
45
+ if (!this.config) {
46
+ return { 'Content-Type': 'application/json' }
47
+ }
48
+ const headers: Record<string, string> = {
49
+ 'Content-Type': 'application/json',
50
+ }
51
+ if (this.config.shopPrompterToken) {
52
+ headers['Authorization'] = `Bearer ${this.config.shopPrompterToken}`
53
+ } else if (this.config.apiKey) {
54
+ headers['X-Api-Key'] = this.config.apiKey
55
+ }
56
+ return headers
57
+ }
58
+
39
59
  async execute(
40
60
  prompt: string,
41
61
  userId: string,
@@ -47,10 +67,7 @@ export class DefaultShopPrompterApi implements ShopPrompterApi {
47
67
 
48
68
  const response = await fetch(`${this.config.baseUrl}/interaction`, {
49
69
  method: 'POST',
50
- headers: {
51
- 'Content-Type': 'application/json',
52
- 'X-Api-Key': this.config.apiKey,
53
- },
70
+ headers: this.getHeaders(),
54
71
  body: JSON.stringify({
55
72
  prompt,
56
73
  userId,
@@ -77,21 +94,13 @@ export class DefaultShopPrompterApi implements ShopPrompterApi {
77
94
  }
78
95
 
79
96
  const url = `${this.config.baseUrl}/interaction/stream`
80
- console.log('ShopPrompter Request:', {
81
- url,
82
- prompt,
83
- userId,
84
- sessionId,
85
- cartContext,
86
- })
87
97
 
88
98
  let response: Response
89
99
  try {
90
100
  response = await fetch(url, {
91
101
  method: 'POST',
92
102
  headers: {
93
- 'Content-Type': 'application/json',
94
- 'X-Api-Key': this.config.apiKey,
103
+ ...this.getHeaders(),
95
104
  Accept: 'text/event-stream',
96
105
  'Cache-Control': 'no-cache',
97
106
  },
@@ -111,8 +120,6 @@ export class DefaultShopPrompterApi implements ShopPrompterApi {
111
120
  return
112
121
  }
113
122
 
114
- console.log('ShopPrompter Response:', response.status)
115
-
116
123
  if (!response.ok) {
117
124
  const errorText = await response.text().catch(() => '')
118
125
  console.error('ShopPrompter API Error:', response.status, errorText)
@@ -137,24 +144,20 @@ export class DefaultShopPrompterApi implements ShopPrompterApi {
137
144
  const { done, value } = await reader.read()
138
145
 
139
146
  if (done) {
140
- console.log('ShopPrompter Stream done')
141
147
  onEvent({ type: 'close' })
142
148
  break
143
149
  }
144
150
 
145
151
  const chunk = decoder.decode(value, { stream: true })
146
- console.log('ShopPrompter Raw chunk:', chunk)
147
152
  buffer += chunk
148
153
  const lines = buffer.split('\n')
149
154
  buffer = lines.pop() || ''
150
155
 
151
156
  let currentEventType = ''
152
157
  for (const line of lines) {
153
- console.log('ShopPrompter Line:', line)
154
158
  if (line.startsWith('event:')) {
155
159
  // Capture the event type for the next data line
156
160
  currentEventType = line.slice(6).trim()
157
- console.log('ShopPrompter Event type:', currentEventType)
158
161
 
159
162
  // If it's an error event, we need to handle it
160
163
  if (currentEventType === 'error') {
@@ -166,7 +169,6 @@ export class DefaultShopPrompterApi implements ShopPrompterApi {
166
169
  }
167
170
  } else if (line.startsWith('data:')) {
168
171
  const data = line.slice(5).trim()
169
- console.log('ShopPrompter Data:', data)
170
172
  if (data) {
171
173
  // If we got an error event type, try to parse error details
172
174
  if (currentEventType === 'error') {
@@ -187,7 +189,6 @@ export class DefaultShopPrompterApi implements ShopPrompterApi {
187
189
  }
188
190
  } else {
189
191
  const event = this.parseEvent(data)
190
- console.log('ShopPrompter Parsed event:', event)
191
192
  if (event) {
192
193
  onEvent(event)
193
194
  }
@@ -217,10 +218,7 @@ export class DefaultShopPrompterApi implements ShopPrompterApi {
217
218
  try {
218
219
  const response = await fetch(`${this.config.baseUrl}/agent-experience`, {
219
220
  method: 'GET',
220
- headers: {
221
- 'Content-Type': 'application/json',
222
- 'X-Api-Key': this.config.apiKey,
223
- },
221
+ headers: this.getHeaders(),
224
222
  })
225
223
 
226
224
  if (!response.ok) {
@@ -230,11 +228,6 @@ export class DefaultShopPrompterApi implements ShopPrompterApi {
230
228
 
231
229
  const data: AgentExperience = await response.json()
232
230
 
233
- console.log(
234
- '## ~ DefaultShopPrompterApi ~ getAgentExperience ~ data:',
235
- data,
236
- )
237
-
238
231
  return data
239
232
  } catch (error) {
240
233
  console.error('Error fetching agent experience:', error)
@@ -242,6 +235,41 @@ export class DefaultShopPrompterApi implements ShopPrompterApi {
242
235
  }
243
236
  }
244
237
 
238
+ async sendFeedback(
239
+ threadId: string,
240
+ message: string,
241
+ feedback: 'upvote' | 'downvote' | 'clear',
242
+ ): Promise<void> {
243
+ if (!this.config) {
244
+ throw new Error('API configuration not set. Call setApiConfig first.')
245
+ }
246
+
247
+ try {
248
+ const response = await fetch(`${this.config.baseUrl}/feedback`, {
249
+ method: 'POST',
250
+ headers: this.getHeaders(),
251
+ body: JSON.stringify({
252
+ threadId,
253
+ message,
254
+ feedback,
255
+ }),
256
+ })
257
+
258
+ if (!response.ok) {
259
+ throw new Error(
260
+ 'We are sorry, but we could not process your feedback. Please try again later.',
261
+ )
262
+ }
263
+ } catch (error) {
264
+ if (error instanceof Error && error.message.startsWith('We are sorry')) {
265
+ throw error
266
+ }
267
+ throw new Error(
268
+ 'We are sorry, but we could not process your feedback. Please try again later.',
269
+ )
270
+ }
271
+ }
272
+
245
273
  private parseEvent(data: string): ShopPrompterStreamEvent | null {
246
274
  try {
247
275
  const json = JSON.parse(data)
@@ -2,7 +2,9 @@
2
2
 
3
3
  export interface ShopPrompterApiConfiguration {
4
4
  baseUrl: string
5
- apiKey: string
5
+ apiKey?: string | null
6
+ productsBaseUrl?: string
7
+ shopPrompterToken?: string | null
6
8
  }
7
9
 
8
10
  export interface ShopPrompterResponse {