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.
- package/.env.development +2 -0
- package/dist/jaweb-chatbot.css +1 -1
- package/dist/jaweb-chatbot.iife.js +192 -599
- package/package.json +1 -1
- package/src/App.tsx +9 -0
- package/src/Chatbot/Chatbot.css +193 -113
- package/src/Chatbot/Chatbot.tsx +26 -19
- package/src/Chatbot/components/Cart/Index.tsx +10 -8
- package/src/Chatbot/components/ChatInput.tsx +11 -11
- package/src/Chatbot/components/Home/Home.css +456 -0
- package/src/Chatbot/components/Home/Home.tsx +293 -0
- package/src/Chatbot/components/Messages/Addcart.tsx +1 -1
- package/src/Chatbot/components/Messages/AssistantMesage.tsx +78 -37
- package/src/Chatbot/components/Messages/CardSwiper.tsx +1 -1
- package/src/Chatbot/components/Messages/Carousel/CardSwiper.tsx +87 -92
- package/src/Chatbot/components/Messages/Carousel/ProductSwiper.css +361 -0
- package/src/Chatbot/components/Messages/Carousel/TryOn.tsx +2 -2
- package/src/Chatbot/components/Messages/ChatMessages.css +32 -1
- package/src/Chatbot/components/Messages/ChatMessages.tsx +21 -38
- package/src/Chatbot/components/Messages/OrderCard.css +177 -0
- package/src/Chatbot/components/Messages/OrderCard.tsx +120 -0
- package/src/Chatbot/components/Messages/ProductCard.css +1 -1
- package/src/Chatbot/components/Messages/Support.tsx +311 -118
- package/src/Chatbot/components/Messages/Typing.css +31 -35
- package/src/Chatbot/components/Messages/Typing.tsx +19 -18
- package/src/Chatbot/components/Messenger/Messenger.tsx +67 -14
- package/src/Chatbot/components/Powered.css +26 -28
- package/src/Chatbot/components/Powered.tsx +1 -1
- package/src/Chatbot/components/Sessions/CreateSession.tsx +12 -10
- package/src/Chatbot/components/TryOn/View.tsx +2 -2
- package/src/Chatbot/components/VoiceMode.css +247 -0
- package/src/Chatbot/components/VoiceMode.tsx +613 -0
- package/src/hooks/config.tsx +9 -10
- package/src/hooks/useChatbotAPI.tsx +146 -61
- package/src/hooks/useChatbotData.tsx +8 -5
- package/src/hooks/useSessionMessages.tsx +0 -1
- package/src/hooks/useWebsocke.tsx +40 -18
- package/src/i18n.tsx +384 -10
- package/src/Chatbot/components/Messages/Carousel/CardSwiperSalla.tsx +0 -187
- package/src/Chatbot/components/Messages/Carousel/CardSwiperZid.tsx +0 -208
- package/src/Chatbot/components/Messages/Carousel/SallaSwiper.css +0 -245
- package/src/Chatbot/components/Messages/SallaAssistantMessage.tsx +0 -202
- package/src/Chatbot/components/Messages/ZidAssistantMesage.tsx +0 -203
- 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
|
|
119
|
+
const streamRes = await fetch(`${config.apiUrl}message-create-stream/`, {
|
|
70
120
|
method: 'POST',
|
|
71
121
|
headers: { 'Content-Type': 'application/json' },
|
|
72
|
-
body:
|
|
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 (
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
|
145
|
+
content,
|
|
109
146
|
sender: 'Ai Agent',
|
|
110
147
|
isBusiness: false,
|
|
111
|
-
|
|
148
|
+
streamId,
|
|
112
149
|
};
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
34
|
-
const handleClose = () => { /* reconnect logic… */ };
|
|
35
|
-
ws.addEventListener('close', handleClose);
|
|
57
|
+
connect();
|
|
36
58
|
|
|
37
59
|
return () => {
|
|
38
|
-
|
|
39
|
-
|
|
60
|
+
closedByCleanup = true;
|
|
61
|
+
if (reconnectTimer) window.clearTimeout(reconnectTimer);
|
|
40
62
|
ws.close();
|
|
41
63
|
};
|
|
42
64
|
}, [session_id]);
|