shopify-chatbot-widget 0.7.9 → 0.8.2
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/dist/jaweb-chatbot.css +1 -1
- package/dist/jaweb-chatbot.iife.js +3 -3
- package/package.json +1 -1
- package/src/App.css +42 -0
- package/src/App.tsx +13 -0
- package/src/Chatbot/Chatbot.css +270 -0
- package/src/Chatbot/Chatbot.tsx +214 -0
- package/src/Chatbot/ChatbotTransition.css +19 -0
- package/src/Chatbot/OLDCHAT.tsx +259 -0
- package/src/Chatbot/components/ChatInput.css +127 -0
- package/src/Chatbot/components/ChatInput.tsx +354 -0
- package/src/Chatbot/components/Messages/Addcart.tsx +70 -0
- package/src/Chatbot/components/Messages/AgentStatus.tsx +37 -0
- package/src/Chatbot/components/Messages/AssistantMesage.tsx +206 -0
- package/src/Chatbot/components/Messages/AudioMessage.tsx +123 -0
- package/src/Chatbot/components/Messages/CalendlyEmbeds.tsx +66 -0
- package/src/Chatbot/components/Messages/CardSwiper.tsx +194 -0
- package/src/Chatbot/components/Messages/Carousel/CardSwiper.tsx +264 -0
- package/src/Chatbot/components/Messages/Carousel/CardSwiperSalla.tsx +208 -0
- package/src/Chatbot/components/Messages/Carousel/CardSwiperZid.tsx +208 -0
- package/src/Chatbot/components/Messages/Carousel/SallaSwiper.css +245 -0
- package/src/Chatbot/components/Messages/Carousel/TryOn.tsx +111 -0
- package/src/Chatbot/components/Messages/CartWidget.tsx +151 -0
- package/src/Chatbot/components/Messages/ChatImage.tsx +46 -0
- package/src/Chatbot/components/Messages/ChatImageText.tsx +74 -0
- package/src/Chatbot/components/Messages/ChatImageTextAssis.tsx +72 -0
- package/src/Chatbot/components/Messages/ChatMessages.css +31 -0
- package/src/Chatbot/components/Messages/ChatMessages.tsx +334 -0
- package/src/Chatbot/components/Messages/ProductCard.css +149 -0
- package/src/Chatbot/components/Messages/SallaAssistantMessage.tsx +196 -0
- package/src/Chatbot/components/Messages/Support.tsx +177 -0
- package/src/Chatbot/components/Messages/Typing.css +31 -0
- package/src/Chatbot/components/Messages/Typing.tsx +19 -0
- package/src/Chatbot/components/Messages/ZidAssistantMesage.tsx +196 -0
- package/src/Chatbot/components/Messages/cartwidget.css +123 -0
- package/src/Chatbot/components/Messenger/Messenger.tsx +531 -0
- package/src/Chatbot/components/Notification.tsx +28 -0
- package/src/Chatbot/components/Powered.css +29 -0
- package/src/Chatbot/components/Powered.tsx +12 -0
- package/src/Chatbot/components/Sessions/CreateSession.tsx +201 -0
- package/src/Chatbot/components/Sessions/RenderList.tsx +212 -0
- package/src/Chatbot/components/Suggestions.css +35 -0
- package/src/Chatbot/components/Suggestions.tsx +57 -0
- package/src/Chatbot/notification.css +17 -0
- package/src/assets/react.svg +1 -0
- package/src/chatbot-widget/index.tsx +14 -0
- package/src/hooks/config.tsx +13 -0
- package/src/hooks/useCart.tsx +107 -0
- package/src/hooks/useChatbotAPI.tsx +167 -0
- package/src/hooks/useChatbotData.tsx +132 -0
- package/src/hooks/useSessionMessages.tsx +50 -0
- package/src/hooks/useWebsocke.tsx +45 -0
- package/src/i18n.tsx +30 -0
- package/src/index.css +2 -0
- package/src/main.tsx +10 -0
- package/src/types/chatbot.ts +40 -0
- package/src/utils/cookies.tsx +14 -0
- package/src/vite-env.d.ts +1 -0
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import { useState, useRef, useEffect } from "react";
|
|
2
|
+
import type { ChangeEvent } from "react";
|
|
3
|
+
import { useTranslation } from "react-i18next";
|
|
4
|
+
import { Input, Tooltip } from "antd";
|
|
5
|
+
import {
|
|
6
|
+
PictureOutlined,
|
|
7
|
+
SendOutlined,
|
|
8
|
+
CloseCircleOutlined,
|
|
9
|
+
AudioOutlined,
|
|
10
|
+
StopOutlined,
|
|
11
|
+
PlayCircleOutlined,
|
|
12
|
+
PauseCircleOutlined,
|
|
13
|
+
} from "@ant-design/icons";
|
|
14
|
+
import WaveSurfer from "wavesurfer.js";
|
|
15
|
+
|
|
16
|
+
interface ChatInputProps {
|
|
17
|
+
userMessage: string;
|
|
18
|
+
setUserMessage: React.Dispatch<React.SetStateAction<string>>;
|
|
19
|
+
handleSendMessage: () => void;
|
|
20
|
+
handleSendImage: (file: File, message: string) => void;
|
|
21
|
+
handleSendVoice: (audio: Blob) => void;
|
|
22
|
+
selectedImage: File | null;
|
|
23
|
+
setSelectedImage: (file: File | null) => void;
|
|
24
|
+
audioBlob: Blob | null;
|
|
25
|
+
setAudioBlob: (blob: Blob | null) => void;
|
|
26
|
+
isSending: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const ChatInput: React.FC<ChatInputProps> = ({
|
|
30
|
+
userMessage,
|
|
31
|
+
setUserMessage,
|
|
32
|
+
handleSendMessage,
|
|
33
|
+
handleSendImage,
|
|
34
|
+
handleSendVoice,
|
|
35
|
+
selectedImage,
|
|
36
|
+
setSelectedImage,
|
|
37
|
+
audioBlob,
|
|
38
|
+
setAudioBlob,
|
|
39
|
+
isSending,
|
|
40
|
+
}) => {
|
|
41
|
+
const { t } = useTranslation();
|
|
42
|
+
const [isRecording, setIsRecording] = useState(false);
|
|
43
|
+
const [isPlaying, setIsPlaying] = useState(false);
|
|
44
|
+
|
|
45
|
+
const mediaRecorderRef = useRef<MediaRecorder | null>(null);
|
|
46
|
+
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
|
47
|
+
const audioChunks = useRef<Blob[]>([]);
|
|
48
|
+
const waveformRef = useRef<HTMLDivElement | null>(null);
|
|
49
|
+
const wavesurfer = useRef<WaveSurfer | null>(null);
|
|
50
|
+
|
|
51
|
+
const handleImageUpload = (event: ChangeEvent<HTMLInputElement>) => {
|
|
52
|
+
const file = event.target.files?.[0];
|
|
53
|
+
if (file) setSelectedImage(file);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const removeSelectedImage = () => {
|
|
57
|
+
setSelectedImage(null);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const handleVoiceRecord = async () => {
|
|
61
|
+
if (!navigator.mediaDevices?.getUserMedia) {
|
|
62
|
+
alert(t("chatInput.voice.notSupported"));
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!isRecording) {
|
|
67
|
+
try {
|
|
68
|
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
69
|
+
const mimeType = MediaRecorder.isTypeSupported("audio/webm")
|
|
70
|
+
? "audio/webm"
|
|
71
|
+
: "audio/mp4";
|
|
72
|
+
|
|
73
|
+
const recorder = new MediaRecorder(stream, { mimeType });
|
|
74
|
+
mediaRecorderRef.current = recorder;
|
|
75
|
+
audioChunks.current = [];
|
|
76
|
+
|
|
77
|
+
recorder.ondataavailable = (event) => {
|
|
78
|
+
if (event.data.size > 0) audioChunks.current.push(event.data);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
recorder.onstop = () => {
|
|
82
|
+
const blob = new Blob(audioChunks.current, { type: mimeType });
|
|
83
|
+
setAudioBlob(blob);
|
|
84
|
+
setIsRecording(false);
|
|
85
|
+
stream.getTracks().forEach((track) => track.stop());
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
recorder.start();
|
|
89
|
+
setIsRecording(true);
|
|
90
|
+
} catch (err) {
|
|
91
|
+
console.error(err);
|
|
92
|
+
alert(t("chatInput.voice.permissionError"));
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
mediaRecorderRef.current?.stop();
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
if (audioBlob && waveformRef.current) {
|
|
101
|
+
wavesurfer.current?.destroy();
|
|
102
|
+
const url = URL.createObjectURL(audioBlob);
|
|
103
|
+
|
|
104
|
+
wavesurfer.current = WaveSurfer.create({
|
|
105
|
+
container: waveformRef.current,
|
|
106
|
+
waveColor: "#ccc",
|
|
107
|
+
progressColor: "#7F28F8",
|
|
108
|
+
cursorWidth: 0,
|
|
109
|
+
barWidth: 3,
|
|
110
|
+
barHeight: 1.5,
|
|
111
|
+
barGap: 2,
|
|
112
|
+
height: 40,
|
|
113
|
+
interact: true,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
wavesurfer.current.load(url);
|
|
117
|
+
wavesurfer.current.on("finish", () => setIsPlaying(false));
|
|
118
|
+
}
|
|
119
|
+
}, [audioBlob]);
|
|
120
|
+
|
|
121
|
+
const togglePlay = () => {
|
|
122
|
+
if (wavesurfer.current) {
|
|
123
|
+
wavesurfer.current.playPause();
|
|
124
|
+
setIsPlaying((prev) => !prev);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const handleCancelRecording = () => {
|
|
129
|
+
setIsRecording(false);
|
|
130
|
+
setAudioBlob(null);
|
|
131
|
+
wavesurfer.current?.destroy();
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<div
|
|
136
|
+
style={{
|
|
137
|
+
display: "flex",
|
|
138
|
+
alignItems: "center",
|
|
139
|
+
backgroundColor: "white",
|
|
140
|
+
border: "1px solid #d1d5db",
|
|
141
|
+
borderRadius: "9999px",
|
|
142
|
+
padding: "0.5rem 0.75rem",
|
|
143
|
+
boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
|
|
144
|
+
width: "95%",
|
|
145
|
+
height: "56px",
|
|
146
|
+
}}
|
|
147
|
+
>
|
|
148
|
+
<button
|
|
149
|
+
style={{
|
|
150
|
+
color: "#6b7280",
|
|
151
|
+
background: "none",
|
|
152
|
+
border: "none",
|
|
153
|
+
margin: "0 0.5rem",
|
|
154
|
+
cursor: "pointer",
|
|
155
|
+
padding: "0"
|
|
156
|
+
}}
|
|
157
|
+
onClick={() => fileInputRef.current?.click()}
|
|
158
|
+
>
|
|
159
|
+
{selectedImage ? (
|
|
160
|
+
<div style={{ position: "relative" }}>
|
|
161
|
+
<img
|
|
162
|
+
src={URL.createObjectURL(selectedImage)}
|
|
163
|
+
alt={t("chatInput.image.selectedAlt")}
|
|
164
|
+
style={{
|
|
165
|
+
width: "2rem",
|
|
166
|
+
height: "2rem",
|
|
167
|
+
borderRadius: "9999px",
|
|
168
|
+
objectFit: "cover",
|
|
169
|
+
padding: "0"
|
|
170
|
+
}}
|
|
171
|
+
/>
|
|
172
|
+
<CloseCircleOutlined
|
|
173
|
+
style={{
|
|
174
|
+
position: "absolute",
|
|
175
|
+
top: "-0.5rem",
|
|
176
|
+
right: "-0.5rem",
|
|
177
|
+
fontSize: "14px",
|
|
178
|
+
color: "#ef4444",
|
|
179
|
+
background: "white",
|
|
180
|
+
borderRadius: "9999px",
|
|
181
|
+
cursor: "pointer",
|
|
182
|
+
}}
|
|
183
|
+
onClick={(e) => {
|
|
184
|
+
e.stopPropagation();
|
|
185
|
+
removeSelectedImage();
|
|
186
|
+
}}
|
|
187
|
+
/>
|
|
188
|
+
</div>
|
|
189
|
+
) : (
|
|
190
|
+
<PictureOutlined style={{ fontSize: "20px" }} />
|
|
191
|
+
)}
|
|
192
|
+
</button>
|
|
193
|
+
|
|
194
|
+
<input
|
|
195
|
+
disabled={isSending}
|
|
196
|
+
type="file"
|
|
197
|
+
accept="image/*"
|
|
198
|
+
ref={fileInputRef}
|
|
199
|
+
onChange={handleImageUpload}
|
|
200
|
+
style={{ display: "none",padding: "0" }}
|
|
201
|
+
/>
|
|
202
|
+
|
|
203
|
+
<Tooltip title={isRecording ? "Stop Recording" : "Send Audio"}>
|
|
204
|
+
<button
|
|
205
|
+
onClick={handleVoiceRecord}
|
|
206
|
+
style={{
|
|
207
|
+
color: isRecording ? "#ef4444" : "#6b7280",
|
|
208
|
+
background: "none",
|
|
209
|
+
border: "none",
|
|
210
|
+
margin: "0 0.5rem",
|
|
211
|
+
cursor: "pointer",
|
|
212
|
+
padding: "0"
|
|
213
|
+
}}
|
|
214
|
+
>
|
|
215
|
+
{isRecording ? (
|
|
216
|
+
<StopOutlined style={{ fontSize: "20px" }} />
|
|
217
|
+
) : (
|
|
218
|
+
<AudioOutlined style={{ fontSize: "20px" }} />
|
|
219
|
+
)}
|
|
220
|
+
</button>
|
|
221
|
+
</Tooltip>
|
|
222
|
+
|
|
223
|
+
<div
|
|
224
|
+
style={{
|
|
225
|
+
flex: 1,
|
|
226
|
+
margin: "0 0.5rem",
|
|
227
|
+
minWidth: 0,
|
|
228
|
+
maxWidth: "100%",
|
|
229
|
+
display: "flex",
|
|
230
|
+
alignItems: "center",
|
|
231
|
+
}}
|
|
232
|
+
>
|
|
233
|
+
{audioBlob ? (
|
|
234
|
+
<div
|
|
235
|
+
style={{
|
|
236
|
+
display: "flex",
|
|
237
|
+
alignItems: "center",
|
|
238
|
+
width: "100%",
|
|
239
|
+
backgroundColor: "#f3f4f6",
|
|
240
|
+
padding: "0.5rem 1rem",
|
|
241
|
+
borderRadius: "0.5rem",
|
|
242
|
+
}}
|
|
243
|
+
>
|
|
244
|
+
<button
|
|
245
|
+
style={{
|
|
246
|
+
background: "none",
|
|
247
|
+
border: "none",
|
|
248
|
+
marginRight: "0.75rem",
|
|
249
|
+
color: "#7f28f8",
|
|
250
|
+
cursor: "pointer",
|
|
251
|
+
padding: "0"
|
|
252
|
+
}}
|
|
253
|
+
onClick={togglePlay}
|
|
254
|
+
>
|
|
255
|
+
{isPlaying ? (
|
|
256
|
+
<PauseCircleOutlined style={{ fontSize: "24px" }} />
|
|
257
|
+
) : (
|
|
258
|
+
<PlayCircleOutlined style={{ fontSize: "24px" }} />
|
|
259
|
+
)}
|
|
260
|
+
</button>
|
|
261
|
+
<div ref={waveformRef} style={{ width: "100%", height: "2rem" }} />
|
|
262
|
+
<CloseCircleOutlined
|
|
263
|
+
style={{
|
|
264
|
+
fontSize: "18px",
|
|
265
|
+
color: "#ef4444",
|
|
266
|
+
marginLeft: "0.75rem",
|
|
267
|
+
cursor: "pointer",
|
|
268
|
+
}}
|
|
269
|
+
onClick={handleCancelRecording}
|
|
270
|
+
/>
|
|
271
|
+
</div>
|
|
272
|
+
) : isRecording ? (
|
|
273
|
+
<div
|
|
274
|
+
style={{
|
|
275
|
+
display: "flex",
|
|
276
|
+
alignItems: "center",
|
|
277
|
+
justifyContent: "center",
|
|
278
|
+
backgroundColor: "#f3f4f6",
|
|
279
|
+
padding: "0.5rem 1rem",
|
|
280
|
+
borderRadius: "0.5rem",
|
|
281
|
+
animation: "pulse 2s infinite",
|
|
282
|
+
|
|
283
|
+
}}
|
|
284
|
+
>
|
|
285
|
+
<span>Recording</span>
|
|
286
|
+
</div>
|
|
287
|
+
) : (
|
|
288
|
+
<Input
|
|
289
|
+
type="text"
|
|
290
|
+
value={userMessage}
|
|
291
|
+
onChange={(e) => setUserMessage(e.target.value)}
|
|
292
|
+
placeholder={"Message..."}
|
|
293
|
+
style={{
|
|
294
|
+
width: "100%",
|
|
295
|
+
border: "none",
|
|
296
|
+
fontSize: "16px",
|
|
297
|
+
margin:0,
|
|
298
|
+
lineHeight: 1.5,
|
|
299
|
+
color: "#374151",
|
|
300
|
+
padding: "0"
|
|
301
|
+
}}
|
|
302
|
+
onKeyDown={(e) => {
|
|
303
|
+
if (e.key === "Enter") {
|
|
304
|
+
e.preventDefault();
|
|
305
|
+
if (selectedImage) {
|
|
306
|
+
handleSendImage(selectedImage, userMessage);
|
|
307
|
+
removeSelectedImage();
|
|
308
|
+
setUserMessage("");
|
|
309
|
+
} else if (userMessage.trim()) {
|
|
310
|
+
handleSendMessage();
|
|
311
|
+
setUserMessage("");
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}}
|
|
315
|
+
/>
|
|
316
|
+
)}
|
|
317
|
+
</div>
|
|
318
|
+
|
|
319
|
+
<button
|
|
320
|
+
disabled={isSending}
|
|
321
|
+
onClick={() => {
|
|
322
|
+
const hasImage = !!selectedImage;
|
|
323
|
+
const hasText = !!userMessage.trim();
|
|
324
|
+
const hasAudio = !!audioBlob;
|
|
325
|
+
|
|
326
|
+
if (hasImage) {
|
|
327
|
+
handleSendImage(selectedImage, userMessage);
|
|
328
|
+
removeSelectedImage();
|
|
329
|
+
setUserMessage("");
|
|
330
|
+
} else if (hasAudio) {
|
|
331
|
+
handleSendVoice(audioBlob);
|
|
332
|
+
handleCancelRecording();
|
|
333
|
+
} else if (hasText) {
|
|
334
|
+
handleSendMessage();
|
|
335
|
+
setUserMessage("");
|
|
336
|
+
}
|
|
337
|
+
}}
|
|
338
|
+
aria-label={t("chatInput.send")}
|
|
339
|
+
style={{
|
|
340
|
+
color: "#6b7280",
|
|
341
|
+
background: "none",
|
|
342
|
+
border: "none",
|
|
343
|
+
margin: "0 0.5rem",
|
|
344
|
+
cursor: "pointer",
|
|
345
|
+
padding: "0"
|
|
346
|
+
}}
|
|
347
|
+
>
|
|
348
|
+
<SendOutlined style={{ fontSize: "20px" }} />
|
|
349
|
+
</button>
|
|
350
|
+
</div>
|
|
351
|
+
);
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
export default ChatInput;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
interface AddToCartConfirmationProps {
|
|
4
|
+
message: string;
|
|
5
|
+
colorCode:string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const AddToCartConfirmation: React.FC<AddToCartConfirmationProps> = ({ message }) => {
|
|
9
|
+
|
|
10
|
+
const formatTextWithLinks = (text: string): React.ReactNode[] => {
|
|
11
|
+
const urlRegex = /(https?:\/\/[^\s]+)/g;
|
|
12
|
+
return text?.split(urlRegex).map((part, i) =>
|
|
13
|
+
urlRegex.test(part) ? (
|
|
14
|
+
<a
|
|
15
|
+
key={i}
|
|
16
|
+
href={part.trim()}
|
|
17
|
+
style={{ color: "#00b3bc" }}
|
|
18
|
+
target="_blank"
|
|
19
|
+
rel="noopener noreferrer"
|
|
20
|
+
className="underline break-words"
|
|
21
|
+
>
|
|
22
|
+
{part}
|
|
23
|
+
</a>
|
|
24
|
+
) : (
|
|
25
|
+
<span key={i}>{part}</span>
|
|
26
|
+
)
|
|
27
|
+
);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
const triggerPhrase = 'Add this to cart for me';
|
|
32
|
+
|
|
33
|
+
// If it matches the add-to-cart pattern, show confirmation
|
|
34
|
+
if (
|
|
35
|
+
message?.startsWith(triggerPhrase) &&
|
|
36
|
+
message?.includes('name:')
|
|
37
|
+
) {
|
|
38
|
+
const nameMatch = message?.match(/name:([^,]+)/i);
|
|
39
|
+
const productName = nameMatch?.[1]?.trim() || 'Product';
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div
|
|
43
|
+
style={{
|
|
44
|
+
backgroundColor: '#E8F5E9',
|
|
45
|
+
border: '1px solid #C8E6C9',
|
|
46
|
+
borderRadius: '12px',
|
|
47
|
+
padding: '12px 16px',
|
|
48
|
+
color: '#2E7D32',
|
|
49
|
+
fontWeight: 500,
|
|
50
|
+
fontSize: '15px',
|
|
51
|
+
maxWidth: '360px',
|
|
52
|
+
margin: '0 auto',
|
|
53
|
+
textAlign: 'center',
|
|
54
|
+
}}
|
|
55
|
+
>
|
|
56
|
+
✅ <strong>{productName}</strong> has been added to your cart.
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Otherwise, render the default message with link formatting
|
|
62
|
+
return (
|
|
63
|
+
<p style={{color:'white'}}>
|
|
64
|
+
{formatTextWithLinks(message)}
|
|
65
|
+
</p>
|
|
66
|
+
);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export default AddToCartConfirmation;
|
|
70
|
+
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// components/Messages/AgentStatusBanner.tsx
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
interface AgentStatusBannerProps {
|
|
5
|
+
status?: string;
|
|
6
|
+
user?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const AgentStatusBanner: React.FC<AgentStatusBannerProps> = ({ status, user }) => {
|
|
10
|
+
if (!status || !user) return null;
|
|
11
|
+
|
|
12
|
+
let displayText = `${user} is ${status}`;
|
|
13
|
+
|
|
14
|
+
// Override text for specific status (optional)
|
|
15
|
+
if (status === 'typing') {
|
|
16
|
+
displayText = `${user} is typing...`;
|
|
17
|
+
}
|
|
18
|
+
else{
|
|
19
|
+
displayText= ''
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<>
|
|
24
|
+
{displayText&&(
|
|
25
|
+
<div className="flex justify-center mb-2">
|
|
26
|
+
<span className="px-3 py-1 bg-gray-100 text-gray-500 text-sm rounded-full">
|
|
27
|
+
{displayText}
|
|
28
|
+
</span>
|
|
29
|
+
</div>
|
|
30
|
+
)}
|
|
31
|
+
|
|
32
|
+
</>
|
|
33
|
+
|
|
34
|
+
);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export default AgentStatusBanner;
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import React, { useState, useMemo, type FC } from 'react';
|
|
2
|
+
import './ProductCard.css';
|
|
3
|
+
import ProductCarousel from './Carousel/CardSwiper';
|
|
4
|
+
import { ArrowLeftOutlined, ArrowRightOutlined } from '@ant-design/icons';
|
|
5
|
+
import { Button } from 'antd';
|
|
6
|
+
import { useKeenSlider } from 'keen-slider/react';
|
|
7
|
+
import 'keen-slider/keen-slider.min.css';
|
|
8
|
+
|
|
9
|
+
interface ProductVariant {
|
|
10
|
+
name: string;
|
|
11
|
+
price: string;
|
|
12
|
+
link: string;
|
|
13
|
+
image: string;
|
|
14
|
+
variant_id: string;
|
|
15
|
+
variant_tag_name: string;
|
|
16
|
+
option?: string;
|
|
17
|
+
tag?:string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface AssistantMessageProps {
|
|
21
|
+
message: string;
|
|
22
|
+
colorCode: string;
|
|
23
|
+
isBusiness?: boolean;
|
|
24
|
+
disableCheckout?:boolean;
|
|
25
|
+
handleAddToCart: (variantId: string, name: string, qty?: number) => Promise<void>;
|
|
26
|
+
handleRemoveFromCart: (variantId: string, qty?: number) => Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const AssistantMessage: FC<AssistantMessageProps> = ({
|
|
30
|
+
message,
|
|
31
|
+
colorCode,
|
|
32
|
+
disableCheckout,
|
|
33
|
+
handleAddToCart,
|
|
34
|
+
handleRemoveFromCart
|
|
35
|
+
|
|
36
|
+
}) => {
|
|
37
|
+
const [counts, setCounts] = useState<Record<string, number>>({});
|
|
38
|
+
const [selectedVariants, setSelectedVariants] = useState<Record<string, string>>({});
|
|
39
|
+
|
|
40
|
+
const formatTextWithLinks = (text: string): React.ReactNode[] => {
|
|
41
|
+
const urlRegex = /(https?:\/\/[^\s]+)/g;
|
|
42
|
+
return text.split(urlRegex).map((part, i) =>
|
|
43
|
+
urlRegex.test(part) ? (
|
|
44
|
+
<a
|
|
45
|
+
key={i}
|
|
46
|
+
href={part.trim()}
|
|
47
|
+
style={{ color: '#00b3bc' }}
|
|
48
|
+
target="_blank"
|
|
49
|
+
rel="noopener noreferrer"
|
|
50
|
+
className="underline break-words"
|
|
51
|
+
>
|
|
52
|
+
{part}
|
|
53
|
+
</a>
|
|
54
|
+
) : (
|
|
55
|
+
<span key={i}>{part}</span>
|
|
56
|
+
)
|
|
57
|
+
);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const extractProductDetails = (text: string): ProductVariant | null => {
|
|
61
|
+
const normalized = text.trim();
|
|
62
|
+
if (!normalized.includes('Name:') || !normalized.includes('VariantId')) return null;
|
|
63
|
+
const nameMatch = normalized.match(/Name\s*[:\-]\s*(.+)/i);
|
|
64
|
+
const priceMatch = normalized.match(/Price\s*[:\-]\s*(.+)/i);
|
|
65
|
+
const linkMatch = normalized.match(/Link\s*[:\-]\s*(https?:\/\/[^\s]+)/i);
|
|
66
|
+
const variantMatch = normalized.match(/VariantId\s*[:\-]\s*(.+)/i);
|
|
67
|
+
const tagNameMatch = normalized.match(/variant_tag_name\s*[:\-]\s*(.+)/i);
|
|
68
|
+
const OptionMatch = normalized.match(/options\s*[:\-]\s*([^|\r\n]+)/i);
|
|
69
|
+
const tagMatch = normalized.match(/tag\s*[:\-]\s*(.+)/i);
|
|
70
|
+
|
|
71
|
+
if (!nameMatch || !priceMatch || !linkMatch || !variantMatch || !tagNameMatch) return null;
|
|
72
|
+
return {
|
|
73
|
+
name: nameMatch[1].trim(),
|
|
74
|
+
price: priceMatch[1].trim(),
|
|
75
|
+
link: linkMatch[1].trim(),
|
|
76
|
+
image: '',
|
|
77
|
+
variant_id: variantMatch[1].trim(),
|
|
78
|
+
variant_tag_name: tagNameMatch[1].trim(),
|
|
79
|
+
option: OptionMatch?.[1]?.trim(),
|
|
80
|
+
tag: tagMatch?.[1]?.trim(), // 🔹 include the tag
|
|
81
|
+
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const parsedSegments = useMemo(() => {
|
|
86
|
+
const parts = message.split('|||').map(p => p.trim()).filter(Boolean);
|
|
87
|
+
const segments: { type: 'text' | 'product'; data: any }[] = [];
|
|
88
|
+
let lastProduct: ProductVariant | null = null;
|
|
89
|
+
|
|
90
|
+
parts.forEach(part => {
|
|
91
|
+
if (part.startsWith('img - ')) {
|
|
92
|
+
const url = part.replace(/^img\s*[-:]\s*/, '').trim();
|
|
93
|
+
if (lastProduct) lastProduct.image = url;
|
|
94
|
+
else {
|
|
95
|
+
const lastSeg = [...segments].reverse().find(s => s.type === 'product');
|
|
96
|
+
if (lastSeg) (lastSeg.data as ProductVariant).image = url;
|
|
97
|
+
}
|
|
98
|
+
} else if (part.includes('Name:') && part.includes('VariantId')) {
|
|
99
|
+
const product = extractProductDetails(part);
|
|
100
|
+
if (product) {
|
|
101
|
+
segments.push({ type: 'product', data: product });
|
|
102
|
+
lastProduct = product;
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
segments.push({ type: 'text', data: part });
|
|
106
|
+
lastProduct = null;
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
return segments;
|
|
111
|
+
}, [message]);
|
|
112
|
+
|
|
113
|
+
const groupedProducts = useMemo(() => {
|
|
114
|
+
const groups: Record<string, ProductVariant[]> = {};
|
|
115
|
+
parsedSegments.forEach(seg => {
|
|
116
|
+
if (seg.type === 'product') {
|
|
117
|
+
const p = seg.data as ProductVariant;
|
|
118
|
+
groups[p.variant_tag_name] = groups[p.variant_tag_name] || [];
|
|
119
|
+
groups[p.variant_tag_name].push(p);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
return groups;
|
|
123
|
+
}, [parsedSegments]);
|
|
124
|
+
|
|
125
|
+
const shortenName = (name: string, maxLength = 22) =>
|
|
126
|
+
name.length > maxLength ? name.slice(0, maxLength) + '...' : name;
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
const [currentSlide, setCurrentSlide] = useState(0);
|
|
131
|
+
const totalSlides =Object.keys(groupedProducts).length ;
|
|
132
|
+
const [sliderRef, slider] = useKeenSlider<HTMLDivElement>({
|
|
133
|
+
loop: false,
|
|
134
|
+
mode: 'free',
|
|
135
|
+
slides: { perView: 1, spacing: 16 },
|
|
136
|
+
slideChanged(s) {
|
|
137
|
+
setCurrentSlide(s.track.details.rel);
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<div className="space-y-6 mt-4 h-fit">
|
|
144
|
+
{parsedSegments
|
|
145
|
+
.filter(seg => seg.type === 'text')
|
|
146
|
+
.map((seg, i) => (
|
|
147
|
+
<p key={i} className="whitespace-pre-wrap break-words">
|
|
148
|
+
{formatTextWithLinks(seg.data)}
|
|
149
|
+
</p>
|
|
150
|
+
))}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
{Object.keys(groupedProducts).length > 0 && (
|
|
155
|
+
<>
|
|
156
|
+
{Object.keys(groupedProducts).length > 1 && (
|
|
157
|
+
<div
|
|
158
|
+
style={{
|
|
159
|
+
display: 'flex',
|
|
160
|
+
margin: 0,
|
|
161
|
+
marginBottom: '6px',
|
|
162
|
+
justifyContent: 'space-between',
|
|
163
|
+
paddingInline: '4px',
|
|
164
|
+
}}
|
|
165
|
+
>
|
|
166
|
+
<Button
|
|
167
|
+
shape="circle"
|
|
168
|
+
icon={<ArrowLeftOutlined />}
|
|
169
|
+
onClick={() => slider.current?.prev()}
|
|
170
|
+
disabled={currentSlide === 0}
|
|
171
|
+
/>
|
|
172
|
+
<Button
|
|
173
|
+
shape="circle"
|
|
174
|
+
icon={<ArrowRightOutlined />}
|
|
175
|
+
onClick={() => slider.current?.next()}
|
|
176
|
+
disabled={currentSlide === totalSlides - 1}
|
|
177
|
+
/>
|
|
178
|
+
</div>
|
|
179
|
+
)}
|
|
180
|
+
<div ref={sliderRef} className="keen-slider">
|
|
181
|
+
<ProductCarousel
|
|
182
|
+
groupedProducts={groupedProducts}
|
|
183
|
+
selectedVariants={selectedVariants}
|
|
184
|
+
setSelectedVariants={setSelectedVariants}
|
|
185
|
+
counts={counts}
|
|
186
|
+
setCounts={setCounts}
|
|
187
|
+
handleAddToCart={handleAddToCart}
|
|
188
|
+
handleRemoveFromCart={handleRemoveFromCart}
|
|
189
|
+
colorCode={colorCode}
|
|
190
|
+
shortenName={shortenName}
|
|
191
|
+
sliderInstance={slider} // pass it down if needed
|
|
192
|
+
currentSlide={currentSlide}
|
|
193
|
+
totalSlides={totalSlides}
|
|
194
|
+
sliderRef={sliderRef}
|
|
195
|
+
disableCheckout={disableCheckout} // hides the – + controls
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
/>
|
|
199
|
+
</div>
|
|
200
|
+
</>
|
|
201
|
+
)}
|
|
202
|
+
</div>
|
|
203
|
+
);
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
export default AssistantMessage;
|