shopify-chatbot-widget 0.7.9 → 0.8.0
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 +108 -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,108 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Button, Spin } from 'antd';
|
|
3
|
+
import { LoadingOutlined, CloudUploadOutlined } from '@ant-design/icons';
|
|
4
|
+
import config from '../../../../hooks/config';
|
|
5
|
+
|
|
6
|
+
interface TryOnProps {
|
|
7
|
+
productImage: string; // product variant image URL
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const TryOnBox: React.FC<TryOnProps> = ({ productImage }) => {
|
|
11
|
+
const [userImage, setUserImage] = useState<string | null>(null);
|
|
12
|
+
const [resultImage, setResultImage] = useState<string | null>(null);
|
|
13
|
+
const [loading, setLoading] = useState(false);
|
|
14
|
+
|
|
15
|
+
// Call your Django Ark API
|
|
16
|
+
const sendToArk = async (file: File) => {
|
|
17
|
+
setLoading(true);
|
|
18
|
+
setResultImage(null);
|
|
19
|
+
|
|
20
|
+
const reader = new FileReader();
|
|
21
|
+
reader.onload = async () => {
|
|
22
|
+
const base64UserImage = reader.result as string;
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const response = await fetch(`${config.apiUrl}ark-tryon/`, {
|
|
26
|
+
method: 'POST',
|
|
27
|
+
headers: { 'Content-Type': 'application/json' },
|
|
28
|
+
body: JSON.stringify({
|
|
29
|
+
userImageUrl: base64UserImage, // can also be a hosted URL
|
|
30
|
+
productImageUrl: productImage, // product image URL
|
|
31
|
+
}),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const data = await response.json();
|
|
35
|
+
|
|
36
|
+
if (data.resultImage) {
|
|
37
|
+
setResultImage(data.resultImage); // base64 string or URL from backend
|
|
38
|
+
} else {
|
|
39
|
+
console.error('Ark API error:', data);
|
|
40
|
+
}
|
|
41
|
+
} catch (err) {
|
|
42
|
+
console.error('Error calling Ark API:', err);
|
|
43
|
+
} finally {
|
|
44
|
+
setLoading(false);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
reader.readAsDataURL(file);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
51
|
+
if (e.target.files && e.target.files[0]) {
|
|
52
|
+
setUserImage(URL.createObjectURL(e.target.files[0]));
|
|
53
|
+
sendToArk(e.target.files[0]);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div className="mt-4 border rounded-lg p-4 bg-gray-50">
|
|
59
|
+
{/* Upload prompt */}
|
|
60
|
+
{!userImage && !resultImage && !loading && (
|
|
61
|
+
<label className="flex flex-col items-center justify-center border-2 border-dashed border-gray-400 rounded-lg w-full h-40 cursor-pointer hover:bg-gray-100 transition">
|
|
62
|
+
<CloudUploadOutlined style={{ fontSize: 28, marginBottom: 8 }} />
|
|
63
|
+
<span className="text-gray-500">Click or Drop your photo here</span>
|
|
64
|
+
<input
|
|
65
|
+
type="file"
|
|
66
|
+
accept="image/*"
|
|
67
|
+
className="hidden"
|
|
68
|
+
onChange={handleFileChange}
|
|
69
|
+
/>
|
|
70
|
+
</label>
|
|
71
|
+
)}
|
|
72
|
+
|
|
73
|
+
{/* Loader */}
|
|
74
|
+
{loading && (
|
|
75
|
+
<div className="flex flex-col items-center justify-center h-40">
|
|
76
|
+
<Spin indicator={<LoadingOutlined style={{ fontSize: 32 }} spin />} />
|
|
77
|
+
<p className="mt-2 text-gray-600">Generating your try-on...</p>
|
|
78
|
+
</div>
|
|
79
|
+
)}
|
|
80
|
+
|
|
81
|
+
{/* Result Preview */}
|
|
82
|
+
{!loading && resultImage && (
|
|
83
|
+
<div className="flex flex-col items-center">
|
|
84
|
+
<p className="mb-2 text-sm font-medium">✨ Try-On Result</p>
|
|
85
|
+
<img
|
|
86
|
+
src={resultImage}
|
|
87
|
+
alt="Result"
|
|
88
|
+
className="max-h-64 object-contain rounded-md border"
|
|
89
|
+
/>
|
|
90
|
+
|
|
91
|
+
<Button
|
|
92
|
+
style={{margin:'4px'}}
|
|
93
|
+
type="default"
|
|
94
|
+
className="mt-3"
|
|
95
|
+
onClick={() => {
|
|
96
|
+
setUserImage(null);
|
|
97
|
+
setResultImage(null);
|
|
98
|
+
}}
|
|
99
|
+
>
|
|
100
|
+
Try Another
|
|
101
|
+
</Button>
|
|
102
|
+
</div>
|
|
103
|
+
)}
|
|
104
|
+
</div>
|
|
105
|
+
);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export default TryOnBox;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { useState, useRef, useEffect } from 'react';
|
|
2
|
+
import type { CartItem } from '../../../hooks/useCart';
|
|
3
|
+
import { ShoppingCartOutlined, CloseOutlined } from '@ant-design/icons';
|
|
4
|
+
import { Button } from 'antd';
|
|
5
|
+
import './CartWidget.css'; // import the encapsulated styles
|
|
6
|
+
import config from '../../../hooks/config';
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
items: CartItem[];
|
|
10
|
+
colorCode: string;
|
|
11
|
+
loading: boolean;
|
|
12
|
+
onRefresh: () => void;
|
|
13
|
+
onRemove: (variantId: string, qty: number) => void;
|
|
14
|
+
sendMessage: (message: string) => Promise<void>;
|
|
15
|
+
sessionId: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default function CartWidget({
|
|
19
|
+
items,
|
|
20
|
+
loading,
|
|
21
|
+
colorCode,
|
|
22
|
+
sessionId,
|
|
23
|
+
onRefresh,
|
|
24
|
+
onRemove,
|
|
25
|
+
}: Props) {
|
|
26
|
+
const [open, setOpen] = useState(false);
|
|
27
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
28
|
+
|
|
29
|
+
const [isLoading,setIsLoading]=useState(false)
|
|
30
|
+
|
|
31
|
+
const checkOut = async () => {
|
|
32
|
+
|
|
33
|
+
setIsLoading(true);
|
|
34
|
+
try {
|
|
35
|
+
const response = await fetch(`${config.apiUrl}checkout/shopify`, {
|
|
36
|
+
method: 'POST',
|
|
37
|
+
headers: {
|
|
38
|
+
'Content-Type': 'application/json',
|
|
39
|
+
},
|
|
40
|
+
body: JSON.stringify({
|
|
41
|
+
sessionId,
|
|
42
|
+
company_username: localStorage.getItem('company_username'),
|
|
43
|
+
}),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (!response.ok) throw new Error('Failed to initiate checkout.');
|
|
47
|
+
|
|
48
|
+
const data = await response.json();
|
|
49
|
+
|
|
50
|
+
if (data?.checkoutUrl) {
|
|
51
|
+
|
|
52
|
+
window.parent?.postMessage(
|
|
53
|
+
{ type: 'JAWEB_CHAT', action: 'navigate', target: '_blank', url: data.checkoutUrl },
|
|
54
|
+
'*'
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
} else {
|
|
60
|
+
alert('No checkout URL returned.');
|
|
61
|
+
}
|
|
62
|
+
} catch (err) {
|
|
63
|
+
|
|
64
|
+
console.error('Checkout error:', err);
|
|
65
|
+
} finally {
|
|
66
|
+
setIsLoading(false);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
const handleClickOutside = (e: MouseEvent) => {
|
|
73
|
+
if (
|
|
74
|
+
containerRef.current &&
|
|
75
|
+
!containerRef.current.contains(e.target as Node)
|
|
76
|
+
) {
|
|
77
|
+
setOpen(false);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
81
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
82
|
+
}, []);
|
|
83
|
+
|
|
84
|
+
const totalCount = items.reduce((sum, i) => sum + i.quantity, 0);
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<div className="jawebchatbotcss-cart-widget" ref={containerRef}>
|
|
88
|
+
<button
|
|
89
|
+
onClick={() => setOpen(o => !o)}
|
|
90
|
+
className="jawebchatbotcss-cart-button"
|
|
91
|
+
>
|
|
92
|
+
<ShoppingCartOutlined className="jawebchatbotcss-cart-icon" />
|
|
93
|
+
<span className="jawebchatbotcss-cart-label">Cart ({totalCount})</span>
|
|
94
|
+
</button>
|
|
95
|
+
|
|
96
|
+
{open && (
|
|
97
|
+
<div className="jawebchatbotcss-cart-dropdown">
|
|
98
|
+
<div className="jawebchatbotcss-cart-header">
|
|
99
|
+
<span className="jawebchatbotcss-cart-title">Your Cart</span>
|
|
100
|
+
<button
|
|
101
|
+
onClick={onRefresh}
|
|
102
|
+
disabled={loading}
|
|
103
|
+
className="jawebchatbotcss-refresh-button"
|
|
104
|
+
>
|
|
105
|
+
{loading ? 'Refreshing…' : 'Refresh'}
|
|
106
|
+
</button>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
{items.length === 0 ? (
|
|
110
|
+
<div className="jawebchatbotcss-cart-empty">
|
|
111
|
+
Your cart is empty.
|
|
112
|
+
</div>
|
|
113
|
+
) : (
|
|
114
|
+
<ul className="jawebchatbotcss-cart-list">
|
|
115
|
+
{items.map(i => (
|
|
116
|
+
<li key={i.variantId} className="jawebchatbotcss-cart-item">
|
|
117
|
+
<span className="jawebchatbotcss-item-name">{i.name}</span>
|
|
118
|
+
<div className="jawebchatbotcss-item-controls">
|
|
119
|
+
<span className="jawebchatbotcss-item-qty">×{i.quantity}</span>
|
|
120
|
+
<CloseOutlined
|
|
121
|
+
onClick={() => onRemove(i.variantId, i.quantity)}
|
|
122
|
+
className="jawebchatbotcss-remove-icon"
|
|
123
|
+
/>
|
|
124
|
+
</div>
|
|
125
|
+
</li>
|
|
126
|
+
))}
|
|
127
|
+
</ul>
|
|
128
|
+
)}
|
|
129
|
+
|
|
130
|
+
{items.length > 0 && (
|
|
131
|
+
<div className="jawebchatbotcss-cart-footer">
|
|
132
|
+
<Button
|
|
133
|
+
type="primary"
|
|
134
|
+
style={{ backgroundColor: colorCode }}
|
|
135
|
+
block
|
|
136
|
+
disabled={totalCount === 0}
|
|
137
|
+
loading={isLoading}
|
|
138
|
+
onClick={() => {
|
|
139
|
+
checkOut()
|
|
140
|
+
// Define `checkOut()` or pass it via props
|
|
141
|
+
}}
|
|
142
|
+
>
|
|
143
|
+
Checkout ({totalCount})
|
|
144
|
+
</Button>
|
|
145
|
+
</div>
|
|
146
|
+
)}
|
|
147
|
+
</div>
|
|
148
|
+
)}
|
|
149
|
+
</div>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
type MessagePart = {
|
|
4
|
+
type: string;
|
|
5
|
+
text?: string;
|
|
6
|
+
image_url?: {
|
|
7
|
+
url?: string;
|
|
8
|
+
};
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
type ChatImageProps = {
|
|
12
|
+
message: MessagePart[] & { isBusiness?: boolean };
|
|
13
|
+
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const ChatImage: React.FC<ChatImageProps> = ({ message }) => {
|
|
17
|
+
const imgPart = message.find(part => part.type === 'image_url')?.image_url?.url;
|
|
18
|
+
const isBusiness = (message as any)?.isBusiness;
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<div>
|
|
23
|
+
<div>
|
|
24
|
+
{imgPart && (
|
|
25
|
+
<div className="mb-2 bg-transparent text-white ">
|
|
26
|
+
<img
|
|
27
|
+
src={imgPart}
|
|
28
|
+
alt="Chat image"
|
|
29
|
+
style={{ width: 200 }}
|
|
30
|
+
className={`rounded-md bg-transparent mt-2 max-w-xs ${
|
|
31
|
+
isBusiness ? 'border border-gray-300 shadow-sm' : ''
|
|
32
|
+
}`}
|
|
33
|
+
/>
|
|
34
|
+
</div>
|
|
35
|
+
)}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export default ChatImage;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
type MessagePart = {
|
|
4
|
+
type: string;
|
|
5
|
+
text?: string;
|
|
6
|
+
image_url?: {
|
|
7
|
+
url?: string;
|
|
8
|
+
};
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
type ChatImageProps = {
|
|
12
|
+
message: MessagePart[] & { isBusiness?: boolean };
|
|
13
|
+
colorCode:string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const ChatImageText: React.FC<ChatImageProps> = ({ message ,colorCode}) => {
|
|
17
|
+
const textPart = message.find(part => part.type === 'text')?.text;
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
const formatTextWithLinks = (text: string): React.ReactNode[] => {
|
|
21
|
+
const urlRegex = /(https?:\/\/[^\s]+)/g;
|
|
22
|
+
return text.split(urlRegex).map((part, i) =>
|
|
23
|
+
urlRegex.test(part) ? (
|
|
24
|
+
<a
|
|
25
|
+
key={i}
|
|
26
|
+
href={part.trim()}
|
|
27
|
+
style={{ color: colorCode }}
|
|
28
|
+
target="_blank"
|
|
29
|
+
rel="noopener noreferrer"
|
|
30
|
+
className="underline break-words"
|
|
31
|
+
>
|
|
32
|
+
{part}
|
|
33
|
+
</a>
|
|
34
|
+
) : (
|
|
35
|
+
<span key={i}>{part}</span>
|
|
36
|
+
)
|
|
37
|
+
);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<>
|
|
42
|
+
{textPart && (
|
|
43
|
+
<div
|
|
44
|
+
style={{
|
|
45
|
+
color:'white',
|
|
46
|
+
border:1,
|
|
47
|
+
backgroundColor:colorCode,
|
|
48
|
+
paddingTop:10,
|
|
49
|
+
paddingBottom:10,
|
|
50
|
+
paddingInline:14,
|
|
51
|
+
maxWidth:'85%',
|
|
52
|
+
lineHeight:1.4,
|
|
53
|
+
borderRadius:'16px'
|
|
54
|
+
,wordBreak:'break-word'
|
|
55
|
+
|
|
56
|
+
}}
|
|
57
|
+
>
|
|
58
|
+
|
|
59
|
+
<p >
|
|
60
|
+
{formatTextWithLinks(textPart)}
|
|
61
|
+
</p>
|
|
62
|
+
|
|
63
|
+
</div>
|
|
64
|
+
)}
|
|
65
|
+
</>
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export default ChatImageText;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
type MessagePart = {
|
|
4
|
+
type: string;
|
|
5
|
+
text?: string;
|
|
6
|
+
image_url?: {
|
|
7
|
+
url?: string;
|
|
8
|
+
};
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
type ChatImageProps = {
|
|
12
|
+
message: MessagePart[] & { isBusiness?: boolean };
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const ChatImageTextAssist: React.FC<ChatImageProps> = ({ message }) => {
|
|
16
|
+
const textPart = message.find(part => part.type === 'text')?.text;
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
const formatTextWithLinks = (text: string): React.ReactNode[] => {
|
|
20
|
+
const urlRegex = /(https?:\/\/[^\s]+)/g;
|
|
21
|
+
return text.split(urlRegex).map((part, i) =>
|
|
22
|
+
urlRegex.test(part) ? (
|
|
23
|
+
<a
|
|
24
|
+
key={i}
|
|
25
|
+
href={part.trim()}
|
|
26
|
+
style={{ color: 'black' }}
|
|
27
|
+
target="_blank"
|
|
28
|
+
rel="noopener noreferrer"
|
|
29
|
+
className="underline break-words"
|
|
30
|
+
>
|
|
31
|
+
{part}
|
|
32
|
+
</a>
|
|
33
|
+
) : (
|
|
34
|
+
<span key={i}>{part}</span>
|
|
35
|
+
)
|
|
36
|
+
);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<>
|
|
41
|
+
{textPart && (
|
|
42
|
+
<div
|
|
43
|
+
style={{
|
|
44
|
+
color:'black',
|
|
45
|
+
border:1,
|
|
46
|
+
paddingTop:10,
|
|
47
|
+
paddingBottom:10,
|
|
48
|
+
paddingInline:14,
|
|
49
|
+
maxWidth:'85%',
|
|
50
|
+
lineHeight:1.4,
|
|
51
|
+
borderRadius:'16px'
|
|
52
|
+
,wordBreak:'break-word',
|
|
53
|
+
backgroundColor:'#f0f0f0'
|
|
54
|
+
}}
|
|
55
|
+
>
|
|
56
|
+
|
|
57
|
+
<p >
|
|
58
|
+
{formatTextWithLinks(textPart)}
|
|
59
|
+
</p>
|
|
60
|
+
|
|
61
|
+
</div>
|
|
62
|
+
)}
|
|
63
|
+
</>
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export default ChatImageTextAssist;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
.chat-image-wrapper {
|
|
2
|
+
padding: 4px 12px;
|
|
3
|
+
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.box-loader {
|
|
7
|
+
width: 6px;
|
|
8
|
+
height: 10px;
|
|
9
|
+
border-radius: 3px;
|
|
10
|
+
animation: pulseBox 0.8s infinite alternate;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.box-loader:nth-child(2) {
|
|
14
|
+
animation-delay: 0.2s;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.box-loader:nth-child(3) {
|
|
18
|
+
animation-delay: 0.4s;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@keyframes pulseBox {
|
|
22
|
+
from {
|
|
23
|
+
transform: scale(1);
|
|
24
|
+
opacity: 0.6;
|
|
25
|
+
}
|
|
26
|
+
to {
|
|
27
|
+
transform: scale(1.3);
|
|
28
|
+
opacity: 1;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|