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.
Files changed (58) hide show
  1. package/dist/jaweb-chatbot.css +1 -1
  2. package/dist/jaweb-chatbot.iife.js +3 -3
  3. package/package.json +1 -1
  4. package/src/App.css +42 -0
  5. package/src/App.tsx +13 -0
  6. package/src/Chatbot/Chatbot.css +270 -0
  7. package/src/Chatbot/Chatbot.tsx +214 -0
  8. package/src/Chatbot/ChatbotTransition.css +19 -0
  9. package/src/Chatbot/OLDCHAT.tsx +259 -0
  10. package/src/Chatbot/components/ChatInput.css +127 -0
  11. package/src/Chatbot/components/ChatInput.tsx +354 -0
  12. package/src/Chatbot/components/Messages/Addcart.tsx +70 -0
  13. package/src/Chatbot/components/Messages/AgentStatus.tsx +37 -0
  14. package/src/Chatbot/components/Messages/AssistantMesage.tsx +206 -0
  15. package/src/Chatbot/components/Messages/AudioMessage.tsx +123 -0
  16. package/src/Chatbot/components/Messages/CalendlyEmbeds.tsx +66 -0
  17. package/src/Chatbot/components/Messages/CardSwiper.tsx +194 -0
  18. package/src/Chatbot/components/Messages/Carousel/CardSwiper.tsx +264 -0
  19. package/src/Chatbot/components/Messages/Carousel/CardSwiperSalla.tsx +208 -0
  20. package/src/Chatbot/components/Messages/Carousel/CardSwiperZid.tsx +208 -0
  21. package/src/Chatbot/components/Messages/Carousel/SallaSwiper.css +245 -0
  22. package/src/Chatbot/components/Messages/Carousel/TryOn.tsx +111 -0
  23. package/src/Chatbot/components/Messages/CartWidget.tsx +151 -0
  24. package/src/Chatbot/components/Messages/ChatImage.tsx +46 -0
  25. package/src/Chatbot/components/Messages/ChatImageText.tsx +74 -0
  26. package/src/Chatbot/components/Messages/ChatImageTextAssis.tsx +72 -0
  27. package/src/Chatbot/components/Messages/ChatMessages.css +31 -0
  28. package/src/Chatbot/components/Messages/ChatMessages.tsx +334 -0
  29. package/src/Chatbot/components/Messages/ProductCard.css +149 -0
  30. package/src/Chatbot/components/Messages/SallaAssistantMessage.tsx +196 -0
  31. package/src/Chatbot/components/Messages/Support.tsx +177 -0
  32. package/src/Chatbot/components/Messages/Typing.css +31 -0
  33. package/src/Chatbot/components/Messages/Typing.tsx +19 -0
  34. package/src/Chatbot/components/Messages/ZidAssistantMesage.tsx +196 -0
  35. package/src/Chatbot/components/Messages/cartwidget.css +123 -0
  36. package/src/Chatbot/components/Messenger/Messenger.tsx +531 -0
  37. package/src/Chatbot/components/Notification.tsx +28 -0
  38. package/src/Chatbot/components/Powered.css +29 -0
  39. package/src/Chatbot/components/Powered.tsx +12 -0
  40. package/src/Chatbot/components/Sessions/CreateSession.tsx +201 -0
  41. package/src/Chatbot/components/Sessions/RenderList.tsx +212 -0
  42. package/src/Chatbot/components/Suggestions.css +35 -0
  43. package/src/Chatbot/components/Suggestions.tsx +57 -0
  44. package/src/Chatbot/notification.css +17 -0
  45. package/src/assets/react.svg +1 -0
  46. package/src/chatbot-widget/index.tsx +14 -0
  47. package/src/hooks/config.tsx +13 -0
  48. package/src/hooks/useCart.tsx +107 -0
  49. package/src/hooks/useChatbotAPI.tsx +167 -0
  50. package/src/hooks/useChatbotData.tsx +132 -0
  51. package/src/hooks/useSessionMessages.tsx +50 -0
  52. package/src/hooks/useWebsocke.tsx +45 -0
  53. package/src/i18n.tsx +30 -0
  54. package/src/index.css +2 -0
  55. package/src/main.tsx +10 -0
  56. package/src/types/chatbot.ts +40 -0
  57. package/src/utils/cookies.tsx +14 -0
  58. 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;