shopify-chatbot-widget 0.0.4 → 0.2.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 (46) hide show
  1. package/dist/jaweb-chatbot.css +1 -0
  2. package/dist/jaweb-chatbot.iife.js +685 -0
  3. package/dist/vite.svg +1 -0
  4. package/package.json +1 -1
  5. package/src/App.css +0 -42
  6. package/src/App.tsx +0 -13
  7. package/src/Chatbot/Chatbot.css +0 -154
  8. package/src/Chatbot/Chatbot.tsx +0 -207
  9. package/src/Chatbot/components/ChatInput.css +0 -127
  10. package/src/Chatbot/components/ChatInput.tsx +0 -262
  11. package/src/Chatbot/components/Messages/Addcart.tsx +0 -70
  12. package/src/Chatbot/components/Messages/AgentStatus.tsx +0 -37
  13. package/src/Chatbot/components/Messages/AssistantMesage.tsx +0 -316
  14. package/src/Chatbot/components/Messages/AudioMessage.tsx +0 -123
  15. package/src/Chatbot/components/Messages/CartWidget.tsx +0 -116
  16. package/src/Chatbot/components/Messages/ChatImage.tsx +0 -46
  17. package/src/Chatbot/components/Messages/ChatImageText.tsx +0 -74
  18. package/src/Chatbot/components/Messages/ChatImageTextAssis.tsx +0 -72
  19. package/src/Chatbot/components/Messages/ChatMessages.css +0 -5
  20. package/src/Chatbot/components/Messages/ChatMessages.tsx +0 -284
  21. package/src/Chatbot/components/Messages/Support.tsx +0 -177
  22. package/src/Chatbot/components/Messages/Typing.css +0 -27
  23. package/src/Chatbot/components/Messages/Typing.tsx +0 -14
  24. package/src/Chatbot/components/Messages/ZidAssistantMesage.tsx +0 -142
  25. package/src/Chatbot/components/Messenger/Messenger.tsx +0 -521
  26. package/src/Chatbot/components/Notification.tsx +0 -27
  27. package/src/Chatbot/components/Powered.css +0 -16
  28. package/src/Chatbot/components/Powered.tsx +0 -13
  29. package/src/Chatbot/components/Sessions/CreateSession.tsx +0 -139
  30. package/src/Chatbot/components/Sessions/RenderList.tsx +0 -202
  31. package/src/Chatbot/components/Suggestions.css +0 -34
  32. package/src/Chatbot/components/Suggestions.tsx +0 -28
  33. package/src/assets/react.svg +0 -1
  34. package/src/chatbot-widget/index.tsx +0 -14
  35. package/src/hooks/config.tsx +0 -13
  36. package/src/hooks/useCart.tsx +0 -99
  37. package/src/hooks/useChatbotAPI.tsx +0 -160
  38. package/src/hooks/useChatbotData.tsx +0 -129
  39. package/src/hooks/useSessionMessages.tsx +0 -51
  40. package/src/hooks/useWebsocke.tsx +0 -45
  41. package/src/i18n.tsx +0 -30
  42. package/src/index.css +0 -1
  43. package/src/main.tsx +0 -10
  44. package/src/types/chatbot.ts +0 -38
  45. package/src/utils/cookies.tsx +0 -14
  46. package/src/vite-env.d.ts +0 -1
@@ -1,262 +0,0 @@
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
- import "./ChatInput.css"; // <-- Import your raw CSS
16
-
17
- interface ChatInputProps {
18
- userMessage: string;
19
- setUserMessage: React.Dispatch<React.SetStateAction<string>>;
20
- handleSendMessage: () => void;
21
- handleSendImage: (file: File, message: string) => void;
22
- handleSendVoice: (audio: Blob) => void;
23
- selectedImage: File | null;
24
- setSelectedImage: (file: File | null) => void;
25
- audioBlob: Blob | null;
26
- setAudioBlob: (blob: Blob | null) => void;
27
- isSending: boolean;
28
-
29
- }
30
-
31
- const ChatInput: React.FC<ChatInputProps> = ({
32
- userMessage,
33
- setUserMessage,
34
- handleSendMessage,
35
- handleSendImage,
36
- handleSendVoice,
37
- selectedImage,
38
- setSelectedImage,
39
- audioBlob,
40
- setAudioBlob,
41
- isSending
42
- }) => {
43
- const { t } = useTranslation();
44
- const [isRecording, setIsRecording] = useState(false);
45
- const [isPlaying, setIsPlaying] = useState(false);
46
-
47
- const mediaRecorderRef = useRef<MediaRecorder | null>(null);
48
- const fileInputRef = useRef<HTMLInputElement | null>(null);
49
- const audioChunks = useRef<Blob[]>([]);
50
- const waveformRef = useRef<HTMLDivElement | null>(null);
51
- const wavesurfer = useRef<WaveSurfer | null>(null);
52
-
53
- const handleImageUpload = (event: ChangeEvent<HTMLInputElement>) => {
54
- const file = event.target.files?.[0];
55
- if (file) setSelectedImage(file);
56
- };
57
-
58
- const removeSelectedImage = () => {
59
- setSelectedImage(null);
60
- };
61
-
62
- const handleVoiceRecord = async () => {
63
- if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
64
- alert(t("chatInput.voice.notSupported"));
65
- return;
66
- }
67
-
68
- if (!isRecording) {
69
- try {
70
- const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
71
- const mimeType = MediaRecorder.isTypeSupported("audio/webm")
72
- ? "audio/webm"
73
- : "audio/mp4";
74
-
75
- const recorder = new MediaRecorder(stream, { mimeType });
76
- mediaRecorderRef.current = recorder;
77
- audioChunks.current = [];
78
-
79
- recorder.ondataavailable = (event) => {
80
- if (event.data.size > 0) audioChunks.current.push(event.data);
81
- };
82
-
83
- recorder.onstop = () => {
84
- const blob = new Blob(audioChunks.current, { type: mimeType });
85
- setAudioBlob(blob);
86
- setIsRecording(false);
87
- stream.getTracks().forEach((track) => track.stop());
88
- };
89
-
90
- recorder.start();
91
- setIsRecording(true);
92
- } catch (err) {
93
- console.error(err);
94
- alert(t("chatInput.voice.permissionError"));
95
- }
96
- } else {
97
- mediaRecorderRef.current?.stop();
98
- }
99
- };
100
-
101
- useEffect(() => {
102
- if (audioBlob && waveformRef.current) {
103
- wavesurfer.current?.destroy();
104
- const url = URL.createObjectURL(audioBlob);
105
-
106
- wavesurfer.current = WaveSurfer.create({
107
- container: waveformRef.current,
108
- waveColor: "#ccc",
109
- progressColor: "#7F28F8",
110
- cursorWidth: 0,
111
- barWidth: 3,
112
- barHeight: 1.5,
113
- barGap: 2,
114
- height: 40,
115
- interact: true,
116
- });
117
-
118
- wavesurfer.current.load(url);
119
- wavesurfer.current.on("finish", () => setIsPlaying(false));
120
- }
121
- }, [audioBlob]);
122
-
123
- const togglePlay = () => {
124
- if (wavesurfer.current) {
125
- wavesurfer.current.playPause();
126
- setIsPlaying((prev) => !prev);
127
- }
128
- };
129
-
130
- const handleCancelRecording = () => {
131
- setIsRecording(false);
132
- setAudioBlob(null);
133
- wavesurfer.current?.destroy();
134
- };
135
-
136
- return (
137
- <div className="chat-input-container">
138
- <button
139
- className="chat-icon-button"
140
- onClick={() => fileInputRef.current?.click()}
141
- >
142
- {selectedImage ? (
143
- <div className="image-preview-wrapper">
144
- <img
145
- src={URL.createObjectURL(selectedImage)}
146
- alt={t("chatInput.image.selectedAlt")}
147
- className="image-preview"
148
- />
149
- <CloseCircleOutlined
150
- className="remove-image-icon"
151
- onClick={(e) => {
152
- e.stopPropagation();
153
- removeSelectedImage();
154
- }}
155
- />
156
- </div>
157
- ) : (
158
- <PictureOutlined style={{ fontSize: "20px" }} />
159
- )}
160
- </button>
161
-
162
- <input
163
- disabled={isSending}
164
- type="file"
165
- accept="image/*"
166
- ref={fileInputRef}
167
- onChange={handleImageUpload}
168
- className="hidden-input"
169
- />
170
-
171
- <Tooltip title={isRecording ? "Stop Recording" : "Send Audio"}>
172
- <button
173
- className={`chat-icon-button ${isRecording ? "recording" : ""}`}
174
- onClick={handleVoiceRecord}
175
- >
176
- {isRecording ? (
177
- <StopOutlined style={{ fontSize: "20px" }} />
178
- ) : (
179
- <AudioOutlined style={{ fontSize: "20px" }} />
180
- )}
181
- </button>
182
- </Tooltip>
183
-
184
- <div className="chat-input-wrapper">
185
- {audioBlob ? (
186
- <div className="audio-preview">
187
- <button className="audio-play-button" onClick={togglePlay}>
188
- {isPlaying ? (
189
- <PauseCircleOutlined style={{ fontSize: "24px" }} />
190
- ) : (
191
- <PlayCircleOutlined style={{ fontSize: "24px" }} />
192
- )}
193
- </button>
194
- <div ref={waveformRef} className="waveform" />
195
- <CloseCircleOutlined
196
- className="remove-audio-icon"
197
- onClick={handleCancelRecording}
198
- />
199
- </div>
200
- ) : isRecording ? (
201
- <div className="recording-indicator">
202
- <span>{'Recording'}</span>
203
- </div>
204
- ) : (
205
- <Input
206
- type="text"
207
- className="text-input"
208
- disabled={isSending}
209
- value={userMessage}
210
- onChange={(e) => setUserMessage(e.target.value)}
211
- placeholder={'Message...'}
212
- onKeyDown={(e) => {
213
- if (e.key === "Enter") {
214
- e.preventDefault();
215
-
216
- if (selectedImage) {
217
- handleSendImage(selectedImage, userMessage);
218
- removeSelectedImage();
219
- setUserMessage("");
220
- } else if (userMessage.trim()) {
221
- handleSendMessage();
222
- }
223
- }
224
- }}
225
-
226
-
227
-
228
- />
229
- )}
230
- </div>
231
-
232
- <button
233
- className="chat-icon-button"
234
- disabled={isSending}
235
-
236
- onClick={() => {
237
- const hasImage = !!selectedImage;
238
- const hasText = !!userMessage.trim();
239
- const hasAudio = !!audioBlob;
240
-
241
- if (hasImage) {
242
- handleSendImage(selectedImage, userMessage); // ✅ send image + text together
243
- removeSelectedImage();
244
- setUserMessage(""); // ✅ clear text to avoid double-send
245
- } else if (hasAudio) {
246
- handleSendVoice(audioBlob);
247
- handleCancelRecording();
248
- } else if (hasText) {
249
- handleSendMessage(); // ✅ only send if no image/audio
250
- }
251
- }}
252
-
253
-
254
- aria-label={t("chatInput.send")}
255
- >
256
- <SendOutlined style={{ fontSize: "20px" }} />
257
- </button>
258
- </div>
259
- );
260
- };
261
-
262
- export default ChatInput;
@@ -1,70 +0,0 @@
1
- import React from 'react';
2
-
3
- interface AddToCartConfirmationProps {
4
- message: string;
5
- colorCode:string;
6
- }
7
-
8
- const AddToCartConfirmation: React.FC<AddToCartConfirmationProps> = ({ message,colorCode }) => {
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: colorCode }}
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>
64
- {formatTextWithLinks(message)}
65
- </p>
66
- );
67
- };
68
-
69
- export default AddToCartConfirmation;
70
-
@@ -1,37 +0,0 @@
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;
@@ -1,316 +0,0 @@
1
- import React, { useState, useMemo } from 'react';
2
-
3
- interface ProductVariant {
4
- name: string;
5
- price: string;
6
- link: string;
7
- image: string;
8
- variant_id: string;
9
- variant_tag_name: string;
10
- color?: string;
11
- size?: string;
12
- }
13
-
14
- interface AssistantMessageProps {
15
- message: string;
16
- colorCode: string;
17
- isBusiness?: boolean;
18
- handleAddToCart: (variantId: string, name: string, qty?: number) => Promise<void>;
19
- handleRemoveFromCart: (variantId: string, qty?: number) => Promise<void>;
20
- }
21
-
22
- const AssistantMessage: React.FC<AssistantMessageProps> = ({
23
- message,
24
- colorCode,
25
- handleAddToCart,
26
- handleRemoveFromCart
27
- }) => {
28
-
29
-
30
-
31
- const [counts, setCounts] = useState<Record<string, number>>({});
32
- const [selectedVariants, setSelectedVariants] = useState<Record<string, string>>({});
33
-
34
- const formatTextWithLinks = (text: string): React.ReactNode[] => {
35
- const urlRegex = /(https?:\/\/[^\s]+)/g;
36
- return text?.split(urlRegex).map((part, i) =>
37
- urlRegex.test(part) ? (
38
- <a
39
- key={i}
40
- href={part.trim()}
41
- style={{ color: colorCode }}
42
- target="_blank"
43
- rel="noopener noreferrer"
44
- className="underline break-words"
45
- >
46
- {part}
47
- </a>
48
- ) : (
49
- <span key={i}>{part}</span>
50
- )
51
- );
52
- };
53
- const extractProductDetails = (text: string): ProductVariant | null => {
54
- const normalized = text.trim();
55
-
56
- // 🛠️ Instead of requiring it to START with 'Name:', just check if it CONTAINS it
57
- if (!normalized.includes('Name:') || !normalized.includes('VariantId')) return null;
58
-
59
- const nameMatch = normalized.match(/Name\s*[:\-]\s*(.+)/i);
60
- const priceMatch = normalized.match(/Price\s*[:\-]\s*(.+)/i);
61
- const linkMatch = normalized.match(/Link\s*[:\-]\s*(https?:\/\/[^\s]+)/i);
62
- const variantMatch = normalized.match(/VariantId\s*[:\-]\s*(.+)/i);
63
- const tagNameMatch = normalized.match(/variant_tag_name\s*[:\-]\s*(.+)/i);
64
- const colorMatch = normalized.match(/^color\s*[:\-]\s*([^\r\n]*)$/im);
65
- const sizeMatch = normalized.match(/^size\s*[:\-]\s*([^\r\n]*)$/im);
66
-
67
-
68
-
69
-
70
-
71
- if (!nameMatch || !priceMatch || !linkMatch || !variantMatch || !tagNameMatch) return null;
72
-
73
-
74
-
75
-
76
-
77
-
78
- return {
79
- name: nameMatch[1].trim(),
80
- price: priceMatch[1].trim(),
81
- link: linkMatch[1].trim(),
82
- image: '',
83
- variant_id: variantMatch[1].trim(),
84
- variant_tag_name: tagNameMatch[1].trim(),
85
- color: colorMatch?.[1]?.trim(),
86
- size: sizeMatch?.[1]?.trim()
87
- };
88
- };
89
-
90
-
91
- const parsedSegments = useMemo(() => {
92
- const parts = message.split('|||').map(p => p.trim()).filter(Boolean);
93
- const segments: { type: 'text' | 'product'; data: any }[] = [];
94
- let lastProduct: ProductVariant | null = null;
95
-
96
- for (const part of parts) {
97
- if (part.startsWith('img - ')) {
98
- const url = part.replace(/^img\s*[-:]\s*/, '').trim();
99
-
100
- // Attach to last known product
101
- if (lastProduct) {
102
- lastProduct.image = url;
103
- } else {
104
- // Fallback: attach to last product in segments if available
105
- const lastSegment = [...segments].reverse().find(s => s.type === 'product');
106
- if (lastSegment) {
107
- (lastSegment.data as ProductVariant).image = url;
108
- }
109
- }
110
- }
111
-
112
- // 🛠 Loosen this check to allow any block with Name: and VariantId to be considered a product
113
- else if (part.includes('Name:') && part.includes('VariantId')) {
114
- const product = extractProductDetails(part);
115
-
116
- if (product) {
117
- segments.push({ type: 'product', data: product });
118
- lastProduct = product;
119
- }
120
- }
121
-
122
- else {
123
- segments.push({ type: 'text', data: part });
124
- lastProduct = null;
125
- }
126
- }
127
-
128
-
129
- return segments;
130
- }, [message]);
131
-
132
-
133
- const groupedProducts = useMemo(() => {
134
- const groups: Record<string, ProductVariant[]> = {};
135
- for (const seg of parsedSegments) {
136
- if (seg.type === 'product') {
137
- const p = seg.data as ProductVariant;
138
- if (!groups[p.variant_tag_name]) groups[p.variant_tag_name] = [];
139
- groups[p.variant_tag_name].push(p);
140
- }
141
- }
142
- return groups;
143
- }, [parsedSegments]);
144
-
145
- const shortenName = (name: string, maxLength = 22) =>
146
- name.length > maxLength ? name.slice(0, maxLength) + '...' : name;
147
-
148
- return (
149
- <div className="space-y-6 mt-4">
150
- {(() => {
151
- const renderedTags = new Set<string>();
152
-
153
- return parsedSegments.map((seg, idx) => {
154
- if (seg.type === 'text') {
155
- return (
156
- <p key={idx} className="whitespace-pre-wrap break-words">
157
-
158
- {formatTextWithLinks(seg.data)}
159
- </p>
160
- );
161
- }
162
-
163
- if (seg.type === 'product') {
164
- const product = seg.data as ProductVariant;
165
- const tag = product.variant_tag_name;
166
- if (renderedTags.has(tag)) return null;
167
- renderedTags.add(tag);
168
-
169
- const group = groupedProducts[tag];
170
-
171
- const currentVariantId = selectedVariants[tag] || group[0].variant_id;
172
- const currentVariant = group.find(v => v.variant_id === currentVariantId) || group[0];
173
- const count = counts[currentVariant.variant_id] || 0;
174
-
175
- const colors = Array.from(
176
- new Set(group.map(v => v.color?.trim()).filter(c => c && c.toLowerCase() !== 'none'))
177
- );
178
-
179
- const sizes = Array.from(
180
- new Set(group.map(v => v.size?.trim()).filter(s => s && s.toLowerCase() !== 'none'))
181
- );
182
-
183
-
184
-
185
-
186
-
187
- const isComboValid = (color?: string, size?: string) =>
188
- group.some(v => v.color === color && v.size === size);
189
-
190
- const setActiveVariant = (color?: string, size?: string) => {
191
- const match = group.find(v => {
192
- return (!color || v.color === color) && (!size || v.size === size);
193
- });
194
- if (match) {
195
- setSelectedVariants(prev => ({
196
- ...prev,
197
- [tag]: match.variant_id
198
- }));
199
- }
200
- };
201
-
202
- const increment = async () => {
203
- const next = count + 1;
204
- setCounts(c => ({ ...c, [currentVariant.variant_id]: next }));
205
- await handleAddToCart(currentVariant.variant_id, currentVariant.name, 1);
206
- };
207
-
208
- const decrement = async () => {
209
- if (count === 0) return;
210
- const next = count - 1;
211
- setCounts(c => ({ ...c, [currentVariant.variant_id]: next }));
212
- await handleRemoveFromCart(currentVariant.variant_id, 1);
213
- };
214
-
215
- return (
216
- <div
217
- key={idx}
218
- className="bg-white shadow-md rounded-2xl p-4 flex flex-col sm:flex-row items-center sm:items-start space-y-4 sm:space-y-0 sm:space-x-6"
219
- >
220
- <div className="w-32 h-32 sm:w-40 sm:h-40">
221
- <img
222
- src={currentVariant.image}
223
- alt={currentVariant.name}
224
- className="w-full h-full object-cover rounded-xl cursor-pointer"
225
- onClick={() => window.open(currentVariant.link, '_blank')}
226
- />
227
- </div>
228
-
229
- <div className="flex-1 space-y-3">
230
- <a
231
- href={currentVariant.link}
232
- target="_blank"
233
- rel="noopener noreferrer"
234
- className="text-lg font-semibold text-gray-900"
235
- >
236
- {shortenName(currentVariant.name)}
237
- </a>
238
- <p className="text-md font-medium text-gray-700">{currentVariant.price}</p>
239
-
240
- {colors.length > 0 && (
241
- <div>
242
- <p className="text-sm text-gray-500 font-medium">Color:</p>
243
- <div className="flex flex-wrap gap-2 mt-1">
244
- {colors.map(color => {
245
- const disabled = !isComboValid(color, currentVariant.size);
246
- const isActive = color === currentVariant.color;
247
- return (
248
- <button
249
- key={color}
250
- disabled={disabled}
251
- onClick={() => setActiveVariant(color, currentVariant.size)}
252
- className={`px-3 py-1 rounded-full border text-sm ${
253
- isActive ? 'text-white' : 'text-gray-800'
254
- } ${disabled ? 'opacity-40 cursor-not-allowed' : 'bg-gray-100'}`}
255
- style={isActive ? { backgroundColor: colorCode,color:'white' } : {}}
256
- >
257
- {color=="None"?"One Color" :color}
258
- </button>
259
- );
260
- })}
261
- </div>
262
- </div>
263
- )}
264
-
265
- {sizes.length > 0 && (
266
- <div>
267
- <p className="text-sm text-gray-500 font-medium">Size:</p>
268
- <div className="flex flex-wrap gap-2 mt-1">
269
- {sizes.map(size => {
270
- const disabled = !isComboValid(currentVariant.color, size);
271
- const isActive = size === currentVariant.size;
272
- return (
273
- <button
274
- key={size}
275
- disabled={disabled}
276
- onClick={() => setActiveVariant(currentVariant.color, size)}
277
- className={`px-3 py-1 rounded-full border text-sm ${
278
- isActive ? 'text-white' : 'text-gray-800'
279
- } ${disabled ? 'opacity-40 cursor-not-allowed' : 'bg-gray-100'}`}
280
- style={isActive ? { backgroundColor: colorCode,color:'white' } : {}}
281
- >
282
- {size==="None"? "One size":size}
283
- </button>
284
- );
285
- })}
286
- </div>
287
- </div>
288
- )}
289
-
290
- {/* Cart Controls */}
291
- <div className="inline-flex items-center space-x-3 mt-2">
292
- <button onClick={decrement} className="px-3 py-1 rounded-lg border">
293
-
294
- </button>
295
- <span className="min-w-[1.25rem] text-center font-medium">{count}</span>
296
- <button
297
- onClick={increment}
298
- className="px-3 py-1 rounded-lg border"
299
- style={{ backgroundColor: colorCode, color: '#fff' }}
300
- >
301
- +
302
- </button>
303
- </div>
304
- </div>
305
- </div>
306
- );
307
- }
308
-
309
- return null;
310
- });
311
- })()}
312
- </div>
313
- );
314
- };
315
-
316
- export default AssistantMessage;