shopify-chatbot-widget 1.0.9 → 1.0.10

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 (44) hide show
  1. package/.env.development +2 -0
  2. package/dist/jaweb-chatbot.css +1 -1
  3. package/dist/jaweb-chatbot.iife.js +192 -599
  4. package/package.json +1 -1
  5. package/src/App.tsx +9 -0
  6. package/src/Chatbot/Chatbot.css +193 -113
  7. package/src/Chatbot/Chatbot.tsx +26 -19
  8. package/src/Chatbot/components/Cart/Index.tsx +10 -8
  9. package/src/Chatbot/components/ChatInput.tsx +11 -11
  10. package/src/Chatbot/components/Home/Home.css +456 -0
  11. package/src/Chatbot/components/Home/Home.tsx +293 -0
  12. package/src/Chatbot/components/Messages/Addcart.tsx +1 -1
  13. package/src/Chatbot/components/Messages/AssistantMesage.tsx +78 -37
  14. package/src/Chatbot/components/Messages/CardSwiper.tsx +1 -1
  15. package/src/Chatbot/components/Messages/Carousel/CardSwiper.tsx +87 -92
  16. package/src/Chatbot/components/Messages/Carousel/ProductSwiper.css +361 -0
  17. package/src/Chatbot/components/Messages/Carousel/TryOn.tsx +2 -2
  18. package/src/Chatbot/components/Messages/ChatMessages.css +32 -1
  19. package/src/Chatbot/components/Messages/ChatMessages.tsx +21 -38
  20. package/src/Chatbot/components/Messages/OrderCard.css +177 -0
  21. package/src/Chatbot/components/Messages/OrderCard.tsx +120 -0
  22. package/src/Chatbot/components/Messages/ProductCard.css +1 -1
  23. package/src/Chatbot/components/Messages/Support.tsx +311 -118
  24. package/src/Chatbot/components/Messages/Typing.css +31 -35
  25. package/src/Chatbot/components/Messages/Typing.tsx +19 -18
  26. package/src/Chatbot/components/Messenger/Messenger.tsx +67 -14
  27. package/src/Chatbot/components/Powered.css +26 -28
  28. package/src/Chatbot/components/Powered.tsx +1 -1
  29. package/src/Chatbot/components/Sessions/CreateSession.tsx +12 -10
  30. package/src/Chatbot/components/TryOn/View.tsx +2 -2
  31. package/src/Chatbot/components/VoiceMode.css +247 -0
  32. package/src/Chatbot/components/VoiceMode.tsx +613 -0
  33. package/src/hooks/config.tsx +9 -10
  34. package/src/hooks/useChatbotAPI.tsx +146 -61
  35. package/src/hooks/useChatbotData.tsx +8 -5
  36. package/src/hooks/useSessionMessages.tsx +0 -1
  37. package/src/hooks/useWebsocke.tsx +40 -18
  38. package/src/i18n.tsx +384 -10
  39. package/src/Chatbot/components/Messages/Carousel/CardSwiperSalla.tsx +0 -187
  40. package/src/Chatbot/components/Messages/Carousel/CardSwiperZid.tsx +0 -208
  41. package/src/Chatbot/components/Messages/Carousel/SallaSwiper.css +0 -245
  42. package/src/Chatbot/components/Messages/SallaAssistantMessage.tsx +0 -202
  43. package/src/Chatbot/components/Messages/ZidAssistantMesage.tsx +0 -203
  44. package/src/Chatbot/components/Sessions/RenderList.tsx +0 -519
@@ -65,80 +65,165 @@ export const useChatbotAPI = (params: Params) => {
65
65
 
66
66
  }
67
67
 
68
+ const requestBody = JSON.stringify({
69
+ message: userMessage,
70
+ isBusiness: true,
71
+ session_id: sessionId,
72
+ organization: localStorage.getItem('company_username'),
73
+ isImage,
74
+ image_url: finalImageUrl,
75
+ });
76
+
77
+ const finalizeMessages = (data: any, streamId?: number) => {
78
+ if (data.message_history) {
79
+ const assistantMessage: any = {
80
+ role: 'assistant',
81
+ content: data.message,
82
+ sender: 'Ai Agent',
83
+ isBusiness: false,
84
+ isHtml: data.isHtml,
85
+ };
86
+ if (data.isConnect) assistantMessage.isConnect = true;
87
+ if (data.isBookings) assistantMessage.isBookings = true;
88
+
89
+ // Broadcast the reply so the merchant dashboard sees it live
90
+ if (socket?.readyState === WebSocket.OPEN) {
91
+ socket.send(
92
+ JSON.stringify({
93
+ message: data.message,
94
+ additionalData: {
95
+ isBusiness: false,
96
+ SessionId: sessionId,
97
+ sender: 'AI',
98
+ response: 'from_ai_web',
99
+ },
100
+ })
101
+ );
102
+ }
103
+
104
+ setMessages((prev) => {
105
+ const filtered = prev.filter(
106
+ (msg) => !msg.typing && (streamId === undefined || msg.streamId !== streamId)
107
+ );
108
+ return [...filtered, assistantMessage];
109
+ });
110
+ } else if (data.status === 'success') {
111
+ setMessages((prev) =>
112
+ prev.filter((msg) => !msg.typing && (streamId === undefined || msg.streamId !== streamId))
113
+ );
114
+ }
115
+ };
116
+
117
+ // ---- Streaming path: tokens render as the model writes ----
68
118
  try {
69
- const res = await fetch(`${config.apiUrl}message-create/`, {
119
+ const streamRes = await fetch(`${config.apiUrl}message-create-stream/`, {
70
120
  method: 'POST',
71
121
  headers: { 'Content-Type': 'application/json' },
72
- body: JSON.stringify({
73
- message: userMessage,
74
- isBusiness: true,
75
- session_id: sessionId,
76
- organization: localStorage.getItem('company_username'),
77
- isImage,
78
- image_url: finalImageUrl,
79
- }),
122
+ body: requestBody,
80
123
  });
81
124
 
82
- if (res.ok) {
83
- const data = await res.json();
84
-
85
-
86
- if (data.message_history) {
87
-
88
- const assistantMessage = data.isConnect
89
- ? {
90
- role: 'assistant',
91
- content: data.message,
92
- isConnect: true,
93
- sender: 'Ai Agent',
94
- isBusiness: false,
95
- isHtml: data.isHtml,
96
- }
97
- : data.isBookings
98
- ? {
99
- role: 'assistant',
100
- content: data.message,
101
- isBookings: true,
102
- sender: 'Ai Agent',
103
- isBusiness: false,
104
- isHtml: data.isHtml,
105
- }
106
- : {
125
+ if (
126
+ streamRes.ok &&
127
+ streamRes.body &&
128
+ (streamRes.headers.get('content-type') || '').includes('text/event-stream')
129
+ ) {
130
+ const reader = streamRes.body.getReader();
131
+ const decoder = new TextDecoder();
132
+ const streamId = Date.now();
133
+ let buffer = '';
134
+ let target = ''; // full text received so far
135
+ let shown = 0; // characters currently revealed
136
+ let streamEnded = false;
137
+ let finalData: any = null;
138
+
139
+ const renderLive = (content: string) => {
140
+ setMessages((prev) => {
141
+ const withoutTyping = prev.filter((msg) => !msg.typing);
142
+ const idx = withoutTyping.findIndex((msg) => msg.streamId === streamId);
143
+ const live = {
107
144
  role: 'assistant',
108
- content: data.message,
145
+ content,
109
146
  sender: 'Ai Agent',
110
147
  isBusiness: false,
111
- isHtml: data.isHtml,
148
+ streamId,
112
149
  };
113
-
114
- // Send message through WebSocket if available
115
- if (socket?.readyState === WebSocket.OPEN) {
116
- socket.send(
117
- JSON.stringify({
118
- message: data.message,
119
- additionalData: {
120
- isBusiness: false,
121
- SessionId: sessionId,
122
- sender: 'AI',
123
- response: 'from_ai_web',
124
- },
125
- })
126
- );
150
+ if (idx === -1) return [...withoutTyping, live];
151
+ const copy = [...withoutTyping];
152
+ copy[idx] = live;
153
+ return copy;
154
+ });
155
+ };
156
+
157
+ // Pump: read SSE events, grow the target text
158
+ const pump = (async () => {
159
+ try {
160
+ while (true) {
161
+ const { done, value } = await reader.read();
162
+ if (done) break;
163
+ buffer += decoder.decode(value, { stream: true });
164
+ const events = buffer.split('\n\n');
165
+ buffer = events.pop() || '';
166
+ for (const evt of events) {
167
+ const line = evt.split('\n').find((l) => l.startsWith('data: '));
168
+ if (!line) continue;
169
+ const payload = JSON.parse(line.slice(6));
170
+ if (payload.delta) {
171
+ target += payload.delta;
172
+ } else if (payload.done) {
173
+ finalData = payload.response;
174
+ } else if (payload.error) {
175
+ throw new Error(payload.error);
176
+ }
177
+ }
178
+ }
179
+ } finally {
180
+ streamEnded = true; // always let the typewriter finish
127
181
  }
182
+ })();
183
+
184
+ // Typewriter: reveal the target smoothly (~120 chars/s, speeding up
185
+ // if the model runs far ahead so long answers never lag behind)
186
+ const typewriter = new Promise<void>((resolve) => {
187
+ const tick = () => {
188
+ if (shown < target.length) {
189
+ const backlog = target.length - shown;
190
+ const step = Math.max(2, Math.ceil(backlog / 30));
191
+ shown = Math.min(shown + step, target.length);
192
+ renderLive(target.slice(0, shown));
193
+ } else if (streamEnded) {
194
+ resolve();
195
+ return;
196
+ }
197
+ setTimeout(tick, 24);
198
+ };
199
+ tick();
200
+ });
128
201
 
129
-
130
- // Update messages safely
131
- setMessages((prev) => {
132
- const filtered = prev.filter((msg) => !msg.typing);
133
- const updated = [...filtered, assistantMessage];
134
-
135
-
202
+ await pump;
203
+ await typewriter;
136
204
 
137
- return updated;
138
- });
139
- } else if (data.status === 'success') {
140
- setMessages((prev) => prev.filter((msg) => !msg.typing));
205
+ if (finalData) {
206
+ finalizeMessages(finalData, streamId);
207
+ return;
141
208
  }
209
+ // Stream closed without a final event — drop the live bubble and fall back
210
+ setMessages((prev) => prev.filter((msg) => msg.streamId !== streamId));
211
+ }
212
+ } catch (streamError) {
213
+ console.warn('Streaming failed, falling back to blocking endpoint', streamError);
214
+ }
215
+
216
+ // ---- Fallback: original blocking endpoint ----
217
+ try {
218
+ const res = await fetch(`${config.apiUrl}message-create/`, {
219
+ method: 'POST',
220
+ headers: { 'Content-Type': 'application/json' },
221
+ body: requestBody,
222
+ });
223
+
224
+ if (res.ok) {
225
+ const data = await res.json();
226
+ finalizeMessages(data);
142
227
  } else {
143
228
  console.error('message-create failed');
144
229
  }
@@ -51,11 +51,14 @@ export const useChatbotData = () => {
51
51
 
52
52
  const data = chatbotDetailsResponse.data.data;
53
53
 
54
- if (data.language === 'Arabic') {
55
- i18n.changeLanguage('ar');
56
- } else {
57
- i18n.changeLanguage('en');
58
- }
54
+ // Merchant's language is only the DEFAULT — a visitor's own
55
+ // choice (jaweb_widget_lang) always wins.
56
+ try {
57
+ if (!localStorage.getItem('jaweb_widget_lang')) {
58
+ if (data.language === 'Arabic') i18n.changeLanguage('ar');
59
+ else if (data.language === 'French') i18n.changeLanguage('fr');
60
+ }
61
+ } catch { /* ignore */ }
59
62
 
60
63
 
61
64
  // console.log(data)
@@ -19,7 +19,6 @@ export const useChatbotMessage = (sessionId: string | null) => {
19
19
  const res = await axios.get<{ messages: ChatMessage[] }>(
20
20
  `${config.apiUrl}chat-message-history/`,
21
21
  {
22
- withCredentials: true,
23
22
  params: {
24
23
  session_id: sid,
25
24
  username: localStorage.getItem('company_username'),
@@ -1,5 +1,5 @@
1
1
  // hooks/useChatSocket.ts
2
- import { useEffect, useRef } from 'react';
2
+ import { useEffect, useRef, useState } from 'react';
3
3
  import config from './config';
4
4
 
5
5
  export default function useChatSocket(
@@ -9,6 +9,9 @@ export default function useChatSocket(
9
9
  const socketRef = useRef<WebSocket | null>(null);
10
10
  // keep a stable ref to the latest onMessage
11
11
  const onMessageRef = useRef(onMessage);
12
+ // bumping this re-renders consumers so they pick up the fresh socket
13
+ const [, setConnectionEpoch] = useState(0);
14
+
12
15
  useEffect(() => {
13
16
  onMessageRef.current = onMessage;
14
17
  }, [onMessage]);
@@ -16,27 +19,46 @@ export default function useChatSocket(
16
19
  useEffect(() => {
17
20
  if (!session_id) return;
18
21
 
19
- const ws = new WebSocket(`${config.websocketUrl}/chat/${session_id}/`);
20
- socketRef.current = ws;
21
-
22
- // precise add/remove so we never leak handlers
23
- const handle = (event: MessageEvent) => {
24
- try {
25
- const data = JSON.parse(event.data);
26
- onMessageRef.current(data);
27
- } catch (e) {
28
- console.error('Invalid JSON', e);
29
- }
22
+ let attempts = 0;
23
+ let closedByCleanup = false;
24
+ let reconnectTimer: number | undefined;
25
+ let ws: WebSocket;
26
+
27
+ const connect = () => {
28
+ // role=visitor marks presence so agent replies while away trigger
29
+ // the "come back" email instead of vanishing
30
+ ws = new WebSocket(`${config.websocketUrl}/chat/${session_id}/?role=visitor`);
31
+ socketRef.current = ws;
32
+
33
+ ws.addEventListener('open', () => {
34
+ attempts = 0;
35
+ setConnectionEpoch((n) => n + 1);
36
+ });
37
+
38
+ ws.addEventListener('message', (event: MessageEvent) => {
39
+ try {
40
+ const data = JSON.parse(event.data);
41
+ onMessageRef.current(data);
42
+ } catch (e) {
43
+ console.error('Invalid JSON', e);
44
+ }
45
+ });
46
+
47
+ // Reconnect with exponential backoff (1s, 2s, 4s… capped at 15s).
48
+ // Covers server restarts, redis hiccups, sleeping phones, flaky wifi.
49
+ ws.addEventListener('close', () => {
50
+ if (closedByCleanup) return;
51
+ const delay = Math.min(1000 * 2 ** attempts, 15000);
52
+ attempts += 1;
53
+ reconnectTimer = window.setTimeout(connect, delay);
54
+ });
30
55
  };
31
- ws.addEventListener('message', handle);
32
56
 
33
- // you can still do onopen/onclose, but tear them down too if you attach them
34
- const handleClose = () => { /* reconnect logic… */ };
35
- ws.addEventListener('close', handleClose);
57
+ connect();
36
58
 
37
59
  return () => {
38
- ws.removeEventListener('message', handle);
39
- ws.removeEventListener('close', handleClose);
60
+ closedByCleanup = true;
61
+ if (reconnectTimer) window.clearTimeout(reconnectTimer);
40
62
  ws.close();
41
63
  };
42
64
  }, [session_id]);