nextjs-chatbot-ui 1.0.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/LICENSE +21 -0
- package/README.md +317 -0
- package/components/AdminSetup.tsx +566 -0
- package/components/Chatbot.tsx +465 -0
- package/index.tsx +5 -0
- package/package.json +62 -0
- package/types/admin.ts +29 -0
- package/types/index.ts +51 -0
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useState, useEffect, useRef } from 'react';
|
|
4
|
+
import { ChatbotConfig, Message } from '../types';
|
|
5
|
+
import clsx from 'clsx';
|
|
6
|
+
|
|
7
|
+
interface ChatbotProps {
|
|
8
|
+
config: ChatbotConfig;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const Chatbot: React.FC<ChatbotProps> = ({ config }) => {
|
|
12
|
+
const [isOpen, setIsOpen] = useState(config.autoOpen || false);
|
|
13
|
+
const [messages, setMessages] = useState<Message[]>([]);
|
|
14
|
+
const [inputValue, setInputValue] = useState('');
|
|
15
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
16
|
+
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
17
|
+
const inputRef = useRef<HTMLTextAreaElement>(null);
|
|
18
|
+
|
|
19
|
+
const labels = {
|
|
20
|
+
title: config.labels?.title || 'Chat Support',
|
|
21
|
+
placeholder: config.labels?.placeholder || 'Type your message...',
|
|
22
|
+
sendButton: config.labels?.sendButton || 'Send',
|
|
23
|
+
typingIndicator: config.labels?.typingIndicator || 'Typing...',
|
|
24
|
+
welcomeMessage: config.labels?.welcomeMessage || 'Hello! How can I help you today?',
|
|
25
|
+
errorMessage: config.labels?.errorMessage || 'Sorry, something went wrong. Please try again.',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const position = config.position || 'bottom-right';
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (isOpen && messages.length === 0) {
|
|
32
|
+
// Add welcome message
|
|
33
|
+
const welcomeMsg: Message = {
|
|
34
|
+
id: 'welcome-1',
|
|
35
|
+
text: labels.welcomeMessage,
|
|
36
|
+
sender: 'bot',
|
|
37
|
+
timestamp: new Date(),
|
|
38
|
+
avatar: config.botInfo?.avatar,
|
|
39
|
+
};
|
|
40
|
+
setMessages([welcomeMsg]);
|
|
41
|
+
}
|
|
42
|
+
}, [isOpen]);
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
scrollToBottom();
|
|
46
|
+
}, [messages]);
|
|
47
|
+
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (isOpen && inputRef.current) {
|
|
50
|
+
inputRef.current.focus();
|
|
51
|
+
}
|
|
52
|
+
}, [isOpen]);
|
|
53
|
+
|
|
54
|
+
const scrollToBottom = () => {
|
|
55
|
+
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const sendMessage = async (messageText?: string) => {
|
|
59
|
+
const textToSend = messageText || inputValue;
|
|
60
|
+
if (!textToSend.trim() || isLoading) return;
|
|
61
|
+
|
|
62
|
+
const userMessage: Message = {
|
|
63
|
+
id: `user-${Date.now()}`,
|
|
64
|
+
text: textToSend,
|
|
65
|
+
sender: 'user',
|
|
66
|
+
timestamp: new Date(),
|
|
67
|
+
avatar: config.userInfo?.avatar,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
setMessages((prev) => [...prev, userMessage]);
|
|
71
|
+
if (!messageText) {
|
|
72
|
+
setInputValue('');
|
|
73
|
+
}
|
|
74
|
+
setIsLoading(true);
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const response = await fetch(config.backendUrl, {
|
|
78
|
+
method: 'POST',
|
|
79
|
+
headers: {
|
|
80
|
+
'Content-Type': 'application/json',
|
|
81
|
+
},
|
|
82
|
+
body: JSON.stringify({
|
|
83
|
+
message: textToSend,
|
|
84
|
+
userInfo: config.userInfo,
|
|
85
|
+
}),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
if (!response.ok) {
|
|
89
|
+
throw new Error('Failed to send message');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const data = await response.json();
|
|
93
|
+
|
|
94
|
+
const botMessage: Message = {
|
|
95
|
+
id: `bot-${Date.now()}`,
|
|
96
|
+
text: data.message || data.response || labels.errorMessage,
|
|
97
|
+
sender: 'bot',
|
|
98
|
+
timestamp: new Date(),
|
|
99
|
+
avatar: config.botInfo?.avatar,
|
|
100
|
+
suggestedReplies: data.suggestedReplies || data.suggestions || undefined,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
setMessages((prev) => [...prev, botMessage]);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
const errorMessage: Message = {
|
|
106
|
+
id: `error-${Date.now()}`,
|
|
107
|
+
text: labels.errorMessage,
|
|
108
|
+
sender: 'bot',
|
|
109
|
+
timestamp: new Date(),
|
|
110
|
+
avatar: config.botInfo?.avatar,
|
|
111
|
+
};
|
|
112
|
+
setMessages((prev) => [...prev, errorMessage]);
|
|
113
|
+
} finally {
|
|
114
|
+
setIsLoading(false);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
const formatTime = (date: Date) => {
|
|
120
|
+
return new Intl.DateTimeFormat('en-US', {
|
|
121
|
+
hour: '2-digit',
|
|
122
|
+
minute: '2-digit',
|
|
123
|
+
}).format(date);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const formatDate = (date: Date) => {
|
|
127
|
+
return new Intl.DateTimeFormat('en-US', {
|
|
128
|
+
month: 'long',
|
|
129
|
+
day: 'numeric',
|
|
130
|
+
year: 'numeric',
|
|
131
|
+
}).format(date);
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const shouldShowDateSeparator = (currentIndex: number) => {
|
|
135
|
+
if (!config.showDateSeparator) return false;
|
|
136
|
+
if (currentIndex === 0) return true;
|
|
137
|
+
|
|
138
|
+
const currentMsg = messages[currentIndex];
|
|
139
|
+
const prevMsg = messages[currentIndex - 1];
|
|
140
|
+
|
|
141
|
+
const currentDate = new Date(currentMsg.timestamp).toDateString();
|
|
142
|
+
const prevDate = new Date(prevMsg.timestamp).toDateString();
|
|
143
|
+
|
|
144
|
+
return currentDate !== prevDate;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const handleSuggestedReply = (reply: string) => {
|
|
148
|
+
sendMessage(reply);
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const positionClasses = {
|
|
152
|
+
'bottom-right': 'bottom-4 right-4',
|
|
153
|
+
'bottom-left': 'bottom-4 left-4',
|
|
154
|
+
'top-right': 'top-4 right-4',
|
|
155
|
+
'top-left': 'top-4 left-4',
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const primaryColor = config.primaryColor || '#6B46C1'; // Default purple like Sendbird
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<div className={clsx('fixed z-50', positionClasses[position])}>
|
|
162
|
+
{!isOpen ? (
|
|
163
|
+
<button
|
|
164
|
+
onClick={() => setIsOpen(true)}
|
|
165
|
+
className="rounded-full p-4 shadow-lg transition-all duration-300 hover:scale-110"
|
|
166
|
+
style={{
|
|
167
|
+
backgroundColor: primaryColor,
|
|
168
|
+
}}
|
|
169
|
+
aria-label="Open chat"
|
|
170
|
+
>
|
|
171
|
+
<svg
|
|
172
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
173
|
+
className="h-6 w-6 text-white"
|
|
174
|
+
fill="none"
|
|
175
|
+
viewBox="0 0 24 24"
|
|
176
|
+
stroke="currentColor"
|
|
177
|
+
>
|
|
178
|
+
<path
|
|
179
|
+
strokeLinecap="round"
|
|
180
|
+
strokeLinejoin="round"
|
|
181
|
+
strokeWidth={2}
|
|
182
|
+
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
|
|
183
|
+
/>
|
|
184
|
+
</svg>
|
|
185
|
+
</button>
|
|
186
|
+
) : (
|
|
187
|
+
<div className="bg-white rounded-lg shadow-2xl w-96 h-[600px] flex flex-col border border-gray-200 overflow-hidden">
|
|
188
|
+
{/* Header - Light gray like Sendbird */}
|
|
189
|
+
<div className="bg-gray-50 border-b border-gray-200 px-4 py-3 flex items-center justify-between">
|
|
190
|
+
<div>
|
|
191
|
+
<h3 className="font-semibold text-base text-gray-900">
|
|
192
|
+
{config.botInfo?.name || labels.title}
|
|
193
|
+
</h3>
|
|
194
|
+
<p className="text-xs text-gray-500">Online</p>
|
|
195
|
+
</div>
|
|
196
|
+
<div className="flex items-center gap-2">
|
|
197
|
+
<button
|
|
198
|
+
onClick={() => {
|
|
199
|
+
setMessages([]);
|
|
200
|
+
setIsOpen(false);
|
|
201
|
+
setIsOpen(true);
|
|
202
|
+
}}
|
|
203
|
+
className="text-gray-500 hover:text-gray-700 transition-colors p-1"
|
|
204
|
+
aria-label="Restart chat"
|
|
205
|
+
>
|
|
206
|
+
<svg
|
|
207
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
208
|
+
className="h-5 w-5"
|
|
209
|
+
fill="none"
|
|
210
|
+
viewBox="0 0 24 24"
|
|
211
|
+
stroke="currentColor"
|
|
212
|
+
>
|
|
213
|
+
<path
|
|
214
|
+
strokeLinecap="round"
|
|
215
|
+
strokeLinejoin="round"
|
|
216
|
+
strokeWidth={2}
|
|
217
|
+
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
|
|
218
|
+
/>
|
|
219
|
+
</svg>
|
|
220
|
+
</button>
|
|
221
|
+
<button
|
|
222
|
+
className="text-gray-500 hover:text-gray-700 transition-colors p-1"
|
|
223
|
+
aria-label="Expand chat"
|
|
224
|
+
>
|
|
225
|
+
<svg
|
|
226
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
227
|
+
className="h-5 w-5"
|
|
228
|
+
fill="none"
|
|
229
|
+
viewBox="0 0 24 24"
|
|
230
|
+
stroke="currentColor"
|
|
231
|
+
>
|
|
232
|
+
<path
|
|
233
|
+
strokeLinecap="round"
|
|
234
|
+
strokeLinejoin="round"
|
|
235
|
+
strokeWidth={2}
|
|
236
|
+
d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4"
|
|
237
|
+
/>
|
|
238
|
+
</svg>
|
|
239
|
+
</button>
|
|
240
|
+
<button
|
|
241
|
+
onClick={() => setIsOpen(false)}
|
|
242
|
+
className="text-gray-500 hover:text-gray-700 transition-colors p-1"
|
|
243
|
+
aria-label="Close chat"
|
|
244
|
+
>
|
|
245
|
+
<svg
|
|
246
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
247
|
+
className="h-5 w-5"
|
|
248
|
+
fill="none"
|
|
249
|
+
viewBox="0 0 24 24"
|
|
250
|
+
stroke="currentColor"
|
|
251
|
+
>
|
|
252
|
+
<path
|
|
253
|
+
strokeLinecap="round"
|
|
254
|
+
strokeLinejoin="round"
|
|
255
|
+
strokeWidth={2}
|
|
256
|
+
d="M6 18L18 6M6 6l12 12"
|
|
257
|
+
/>
|
|
258
|
+
</svg>
|
|
259
|
+
</button>
|
|
260
|
+
</div>
|
|
261
|
+
</div>
|
|
262
|
+
|
|
263
|
+
{/* Messages */}
|
|
264
|
+
<div
|
|
265
|
+
className="flex-1 overflow-y-auto px-4 py-4 space-y-4"
|
|
266
|
+
style={{
|
|
267
|
+
backgroundColor: config.backgroundColor || '#ffffff',
|
|
268
|
+
}}
|
|
269
|
+
>
|
|
270
|
+
{messages.map((message, index) => (
|
|
271
|
+
<React.Fragment key={message.id}>
|
|
272
|
+
{shouldShowDateSeparator(index) && (
|
|
273
|
+
<div className="flex items-center justify-center my-4">
|
|
274
|
+
<span className="text-xs text-gray-500 bg-white px-3 py-1 rounded-full border border-gray-200">
|
|
275
|
+
{formatDate(message.timestamp)}
|
|
276
|
+
</span>
|
|
277
|
+
</div>
|
|
278
|
+
)}
|
|
279
|
+
<div
|
|
280
|
+
className={clsx(
|
|
281
|
+
'flex',
|
|
282
|
+
message.sender === 'user' ? 'justify-end' : 'justify-start'
|
|
283
|
+
)}
|
|
284
|
+
>
|
|
285
|
+
<div className="flex flex-col gap-1 max-w-[75%]">
|
|
286
|
+
<div className="relative">
|
|
287
|
+
{/* Bubble tail for bot messages */}
|
|
288
|
+
{message.sender === 'bot' && (
|
|
289
|
+
<div
|
|
290
|
+
className="absolute -left-2 bottom-0 w-0 h-0 z-0"
|
|
291
|
+
style={{
|
|
292
|
+
borderTop: '6px solid transparent',
|
|
293
|
+
borderBottom: '6px solid transparent',
|
|
294
|
+
borderRight: '8px solid #F3F4F6',
|
|
295
|
+
}}
|
|
296
|
+
/>
|
|
297
|
+
)}
|
|
298
|
+
{/* Bubble tail for user messages */}
|
|
299
|
+
{message.sender === 'user' && (
|
|
300
|
+
<div
|
|
301
|
+
className="absolute -right-2 bottom-0 w-0 h-0 z-0"
|
|
302
|
+
style={{
|
|
303
|
+
borderTop: '6px solid transparent',
|
|
304
|
+
borderBottom: '6px solid transparent',
|
|
305
|
+
borderLeft: `8px solid ${primaryColor}`,
|
|
306
|
+
}}
|
|
307
|
+
/>
|
|
308
|
+
)}
|
|
309
|
+
<div
|
|
310
|
+
className={clsx(
|
|
311
|
+
'rounded-2xl px-4 py-2.5 relative',
|
|
312
|
+
message.sender === 'user'
|
|
313
|
+
? 'text-white rounded-br-sm'
|
|
314
|
+
: 'bg-gray-100 text-gray-900 rounded-bl-sm'
|
|
315
|
+
)}
|
|
316
|
+
style={{
|
|
317
|
+
backgroundColor:
|
|
318
|
+
message.sender === 'user'
|
|
319
|
+
? primaryColor
|
|
320
|
+
: undefined,
|
|
321
|
+
}}
|
|
322
|
+
>
|
|
323
|
+
<p className="text-sm whitespace-pre-wrap break-words leading-relaxed">
|
|
324
|
+
{message.text}
|
|
325
|
+
</p>
|
|
326
|
+
{config.showTimestamp && (
|
|
327
|
+
<p
|
|
328
|
+
className={clsx(
|
|
329
|
+
'text-xs mt-1.5',
|
|
330
|
+
message.sender === 'user'
|
|
331
|
+
? 'text-white/70'
|
|
332
|
+
: 'text-gray-500'
|
|
333
|
+
)}
|
|
334
|
+
>
|
|
335
|
+
{formatTime(message.timestamp)}
|
|
336
|
+
</p>
|
|
337
|
+
)}
|
|
338
|
+
</div>
|
|
339
|
+
</div>
|
|
340
|
+
{/* Suggested Replies */}
|
|
341
|
+
{message.sender === 'bot' && message.suggestedReplies && message.suggestedReplies.length > 0 && (
|
|
342
|
+
<div className="flex flex-col gap-2 mt-1">
|
|
343
|
+
{message.suggestedReplies.map((reply, idx) => (
|
|
344
|
+
<button
|
|
345
|
+
key={idx}
|
|
346
|
+
onClick={() => handleSuggestedReply(reply)}
|
|
347
|
+
className="text-left rounded-xl px-4 py-2.5 text-sm font-medium transition-all hover:scale-[1.02] border border-gray-200 hover:border-gray-300 bg-white shadow-sm hover:shadow"
|
|
348
|
+
style={{
|
|
349
|
+
color: primaryColor,
|
|
350
|
+
borderColor: `${primaryColor}40`,
|
|
351
|
+
}}
|
|
352
|
+
>
|
|
353
|
+
{reply}
|
|
354
|
+
</button>
|
|
355
|
+
))}
|
|
356
|
+
</div>
|
|
357
|
+
)}
|
|
358
|
+
</div>
|
|
359
|
+
</div>
|
|
360
|
+
</React.Fragment>
|
|
361
|
+
))}
|
|
362
|
+
{isLoading && (
|
|
363
|
+
<div className="flex justify-start">
|
|
364
|
+
<div className="bg-gray-100 rounded-2xl rounded-bl-sm px-4 py-2.5 relative">
|
|
365
|
+
<div className="absolute -left-2 bottom-0 w-0 h-0 z-0" style={{
|
|
366
|
+
borderTop: '6px solid transparent',
|
|
367
|
+
borderBottom: '6px solid transparent',
|
|
368
|
+
borderRight: '8px solid #F3F4F6',
|
|
369
|
+
}} />
|
|
370
|
+
<div className="flex gap-1 relative z-10">
|
|
371
|
+
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '0ms' }}></div>
|
|
372
|
+
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '150ms' }}></div>
|
|
373
|
+
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '300ms' }}></div>
|
|
374
|
+
</div>
|
|
375
|
+
</div>
|
|
376
|
+
</div>
|
|
377
|
+
)}
|
|
378
|
+
<div ref={messagesEndRef} />
|
|
379
|
+
</div>
|
|
380
|
+
|
|
381
|
+
{/* Footer - Powered by */}
|
|
382
|
+
{config.poweredByText && (
|
|
383
|
+
<div className="border-t border-gray-200 px-4 py-2 bg-gray-50">
|
|
384
|
+
<p className="text-xs text-gray-500 text-center">
|
|
385
|
+
{config.poweredByText}
|
|
386
|
+
</p>
|
|
387
|
+
</div>
|
|
388
|
+
)}
|
|
389
|
+
|
|
390
|
+
{/* Input */}
|
|
391
|
+
<div className="border-t border-gray-200 bg-white">
|
|
392
|
+
<div className="p-3">
|
|
393
|
+
<div className="flex items-end gap-2 bg-gray-50 rounded-2xl px-4 py-2.5 border border-gray-200 focus-within:border-gray-300 focus-within:bg-white transition-colors">
|
|
394
|
+
<textarea
|
|
395
|
+
ref={inputRef}
|
|
396
|
+
value={inputValue}
|
|
397
|
+
onChange={(e) => {
|
|
398
|
+
setInputValue(e.target.value);
|
|
399
|
+
// Auto-resize textarea
|
|
400
|
+
e.target.style.height = 'auto';
|
|
401
|
+
e.target.style.height = `${Math.min(e.target.scrollHeight, 120)}px`;
|
|
402
|
+
}}
|
|
403
|
+
onKeyDown={(e) => {
|
|
404
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
405
|
+
e.preventDefault();
|
|
406
|
+
sendMessage();
|
|
407
|
+
}
|
|
408
|
+
}}
|
|
409
|
+
placeholder={labels.placeholder}
|
|
410
|
+
disabled={isLoading}
|
|
411
|
+
rows={1}
|
|
412
|
+
className="flex-1 resize-none bg-transparent border-0 focus:outline-none disabled:cursor-not-allowed text-sm text-gray-900 placeholder:text-gray-500 max-h-[120px] overflow-y-auto"
|
|
413
|
+
/>
|
|
414
|
+
<button
|
|
415
|
+
onClick={() => sendMessage()}
|
|
416
|
+
disabled={!inputValue.trim() || isLoading}
|
|
417
|
+
className="flex-shrink-0 rounded-full p-2 transition-all disabled:opacity-30 disabled:cursor-not-allowed flex items-center justify-center"
|
|
418
|
+
style={{
|
|
419
|
+
backgroundColor: inputValue.trim() ? primaryColor : 'transparent',
|
|
420
|
+
color: inputValue.trim() ? 'white' : '#9CA3AF',
|
|
421
|
+
}}
|
|
422
|
+
aria-label="Send message"
|
|
423
|
+
>
|
|
424
|
+
{inputValue.trim() ? (
|
|
425
|
+
<svg
|
|
426
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
427
|
+
className="h-5 w-5"
|
|
428
|
+
fill="none"
|
|
429
|
+
viewBox="0 0 24 24"
|
|
430
|
+
stroke="currentColor"
|
|
431
|
+
strokeWidth={2.5}
|
|
432
|
+
>
|
|
433
|
+
<path
|
|
434
|
+
strokeLinecap="round"
|
|
435
|
+
strokeLinejoin="round"
|
|
436
|
+
d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5"
|
|
437
|
+
/>
|
|
438
|
+
</svg>
|
|
439
|
+
) : (
|
|
440
|
+
<svg
|
|
441
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
442
|
+
className="h-5 w-5"
|
|
443
|
+
fill="none"
|
|
444
|
+
viewBox="0 0 24 24"
|
|
445
|
+
stroke="currentColor"
|
|
446
|
+
>
|
|
447
|
+
<path
|
|
448
|
+
strokeLinecap="round"
|
|
449
|
+
strokeLinejoin="round"
|
|
450
|
+
strokeWidth={2}
|
|
451
|
+
d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z"
|
|
452
|
+
/>
|
|
453
|
+
</svg>
|
|
454
|
+
)}
|
|
455
|
+
</button>
|
|
456
|
+
</div>
|
|
457
|
+
</div>
|
|
458
|
+
</div>
|
|
459
|
+
</div>
|
|
460
|
+
)}
|
|
461
|
+
</div>
|
|
462
|
+
);
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
export default Chatbot;
|
package/index.tsx
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { default as Chatbot } from './components/Chatbot';
|
|
2
|
+
export type { ChatbotConfig, Message, ChatbotProps } from './types';
|
|
3
|
+
|
|
4
|
+
export { default as AdminSetup } from './components/AdminSetup';
|
|
5
|
+
export type { DatabaseType, DatabaseConnection, ColumnSelection, DatabaseConfig, AdminSetupProps } from './types/admin';
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nextjs-chatbot-ui",
|
|
3
|
+
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"description": "A configurable chatbot UI component for Next.js with Tailwind CSS",
|
|
6
|
+
"main": "./index.tsx",
|
|
7
|
+
"types": "./types/index.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./index.tsx",
|
|
11
|
+
"require": "./index.tsx",
|
|
12
|
+
"types": "./types/index.ts"
|
|
13
|
+
},
|
|
14
|
+
"./components/Chatbot": {
|
|
15
|
+
"import": "./components/Chatbot.tsx",
|
|
16
|
+
"require": "./components/Chatbot.tsx"
|
|
17
|
+
},
|
|
18
|
+
"./types": {
|
|
19
|
+
"import": "./types/index.ts",
|
|
20
|
+
"require": "./types/index.ts",
|
|
21
|
+
"types": "./types/index.ts"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"components",
|
|
26
|
+
"types",
|
|
27
|
+
"index.tsx",
|
|
28
|
+
"README.md"
|
|
29
|
+
],
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "echo 'Package is ready to use. Source files are included.'",
|
|
32
|
+
"dev": "next dev",
|
|
33
|
+
"prepare": "npm run build"
|
|
34
|
+
},
|
|
35
|
+
"keywords": [
|
|
36
|
+
"chatbot",
|
|
37
|
+
"ui",
|
|
38
|
+
"component",
|
|
39
|
+
"nextjs",
|
|
40
|
+
"tailwind",
|
|
41
|
+
"react"
|
|
42
|
+
],
|
|
43
|
+
"author": "",
|
|
44
|
+
"license": "MIT",
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"react": "^18.0.0",
|
|
47
|
+
"react-dom": "^18.0.0",
|
|
48
|
+
"next": "^14.0.0"
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"clsx": "^2.1.0"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@types/node": "^20.0.0",
|
|
55
|
+
"@types/react": "^18.0.0",
|
|
56
|
+
"@types/react-dom": "^18.0.0",
|
|
57
|
+
"autoprefixer": "^10.4.16",
|
|
58
|
+
"postcss": "^8.4.32",
|
|
59
|
+
"tailwindcss": "^3.4.0",
|
|
60
|
+
"typescript": "^5.3.3"
|
|
61
|
+
}
|
|
62
|
+
}
|
package/types/admin.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export type DatabaseType = 'mongodb' | 'postgres';
|
|
2
|
+
|
|
3
|
+
export interface DatabaseConnection {
|
|
4
|
+
type: DatabaseType;
|
|
5
|
+
host: string;
|
|
6
|
+
port: number;
|
|
7
|
+
database: string;
|
|
8
|
+
username?: string;
|
|
9
|
+
password?: string;
|
|
10
|
+
connectionString?: string; // For MongoDB connection string
|
|
11
|
+
ssl?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ColumnSelection {
|
|
15
|
+
embeddingColumns: string[];
|
|
16
|
+
llmColumns: string[];
|
|
17
|
+
chromaColumns: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface DatabaseConfig {
|
|
21
|
+
connection: DatabaseConnection;
|
|
22
|
+
columnSelection: ColumnSelection;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface AdminSetupProps {
|
|
26
|
+
onSave?: (config: DatabaseConfig) => void;
|
|
27
|
+
onTestConnection?: (connection: DatabaseConnection) => Promise<boolean>;
|
|
28
|
+
onFetchColumns?: (connection: DatabaseConnection) => Promise<string[]>;
|
|
29
|
+
}
|
package/types/index.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export interface ChatbotConfig {
|
|
2
|
+
// Backend configuration
|
|
3
|
+
backendUrl: string;
|
|
4
|
+
|
|
5
|
+
// UI Labels
|
|
6
|
+
labels?: {
|
|
7
|
+
title?: string;
|
|
8
|
+
placeholder?: string;
|
|
9
|
+
sendButton?: string;
|
|
10
|
+
typingIndicator?: string;
|
|
11
|
+
welcomeMessage?: string;
|
|
12
|
+
errorMessage?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// Position configuration
|
|
16
|
+
position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
|
|
17
|
+
|
|
18
|
+
// User/Bot information
|
|
19
|
+
userInfo?: {
|
|
20
|
+
name?: string;
|
|
21
|
+
avatar?: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
botInfo?: {
|
|
25
|
+
name?: string;
|
|
26
|
+
avatar?: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Styling
|
|
30
|
+
primaryColor?: string;
|
|
31
|
+
backgroundColor?: string;
|
|
32
|
+
|
|
33
|
+
// Behavior
|
|
34
|
+
autoOpen?: boolean;
|
|
35
|
+
showTimestamp?: boolean;
|
|
36
|
+
showDateSeparator?: boolean;
|
|
37
|
+
poweredByText?: string; // Footer text like "Powered by sendbird"
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface Message {
|
|
41
|
+
id: string;
|
|
42
|
+
text: string;
|
|
43
|
+
sender: 'user' | 'bot';
|
|
44
|
+
timestamp: Date;
|
|
45
|
+
avatar?: string;
|
|
46
|
+
suggestedReplies?: string[]; // Suggested reply buttons
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface ChatbotProps {
|
|
50
|
+
config: ChatbotConfig;
|
|
51
|
+
}
|