lulichat 1.0.2 → 1.0.4
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/README.md +1 -0
- package/dist/components/LuliChat.d.ts +3 -0
- package/dist/index.d.ts +4 -0
- package/dist/lulichat-support.umd.js +10 -10
- package/dist/types/index.d.ts +85 -0
- package/package.json +15 -5
- package/dist/lulichat-support.es.js +0 -1729
- package/dist/style.css +0 -1
- package/src/components/ChatIcon.tsx +0 -27
- package/src/components/ChatInterface.tsx +0 -211
- package/src/components/ChatWidget.tsx +0 -198
- package/src/components/ContactForm.tsx +0 -155
- package/src/components/LuliChat.tsx +0 -48
- package/src/components/ui/Button.tsx +0 -44
- package/src/components/ui/Form.tsx +0 -174
- package/src/components/ui/Icons.tsx +0 -443
- package/src/components/ui/Input.tsx +0 -31
- package/src/constants.ts +0 -32
- package/src/hooks/useSocket.ts +0 -45
- package/src/index.css +0 -383
- package/src/index.ts +0 -5
- package/src/lib/socket.ts +0 -96
- package/src/main.tsx +0 -15
- package/src/types/index.ts +0 -93
- package/src/utils/api.ts +0 -59
- package/src/utils/index.ts +0 -5
- package/src/vite-env.d.ts +0 -1
package/dist/style.css
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
@import"https://fonts.googleapis.com/css2?family=Inclusive+Sans:ital@0;1&family=Outfit:wght@100..900&display=swap";:root{--background: 0 0% 100%;--foreground: 222.2 84% 4.9%;--card: 0 0% 100%;--card-foreground: 222.2 84% 4.9%;--popover: 0 0% 100%;--popover-foreground: 222.2 84% 4.9%;--primary-color: #755ae2;--primary: 251.91deg 70.1% 61.96%;--primary-foreground: 210 40% 98%;--secondary: 210 40% 96.1%;--secondary-foreground: 222.2 47.4% 11.2%;--muted: 210 40% 96.1%;--muted-foreground: 215.4 16.3% 46.9%;--accent: 210 40% 96.1%;--accent-foreground: 222.2 47.4% 11.2%;--destructive: 0 84.2% 60.2%;--destructive-foreground: 210 40% 98%;--border: 214.3 31.8% 91.4%;--input: 214.3 31.8% 91.4%;--ring: 222.2 84% 4.9%;--radius: .5rem}.dark{--background: 222.2 84% 4.9%;--foreground: 210 40% 98%;--card: 222.2 84% 4.9%;--card-foreground: 210 40% 98%;--popover: 222.2 84% 4.9%;--popover-foreground: 210 40% 98%;--primary: 210 40% 98%;--primary-foreground: 222.2 47.4% 11.2%;--secondary: 217.2 32.6% 17.5%;--secondary-foreground: 210 40% 98%;--muted: 217.2 32.6% 17.5%;--muted-foreground: 215 20.2% 65.1%;--accent: 217.2 32.6% 17.5%;--accent-foreground: 210 40% 98%;--destructive: 0 62.8% 30.6%;--destructive-foreground: 210 40% 98%;--border: 217.2 32.6% 17.5%;--input: 217.2 32.6% 17.5%;--ring: 212.7 26.8% 83.9%}.lulichat,.lulichat *{box-sizing:border-box;margin:0;padding:0;border:none;outline:none}.lulichat button{-moz-appearance:none;appearance:none;-webkit-appearance:none;outline:none}.lulichat{position:fixed;z-index:50;display:flex;flex-direction:column;row-gap:10px;font-family:Outfit,sans-serif;font-optical-sizing:auto;font-weight:300;max-width:400px}.lulichat-main{background-color:var(--background);color:var(--foreground);border-radius:12px;border:4px solid hsl(var(--primary));box-shadow:4px 4px 20px 4px #0000001a;display:flex;flex-direction:column}.lulichat-bottom-right{bottom:1rem;right:1rem}.lulichat-bottom-left{bottom:1rem;left:1rem}.lulichat-top-right{top:1rem;right:1rem}.lulichat-container-top-left{top:1rem;left:1rem}.lulichat-btn{display:inline-flex;align-items:center;justify-content:center;gap:.5rem;white-space:nowrap;font-size:1rem;font-weight:500;transition:background-color .2s,color .2s;outline:none;cursor:pointer}.lulichat-btn.lulichat-contact-form-btn{background-color:#fff;color:hsl(var(--primary))}.lulichat-btn-group{display:flex;gap:.4rem}.lulichat-btn:disabled{cursor:not-allowed;opacity:.5}.lulichat-btn-rounded{border-radius:1.5rem}.lulichat-btn-none{border-radius:0rem}.lulichat-btn-circle{border-radius:50%}.lulichat-btn:focus-visible{outline:2px solid var(--ring);outline-offset:2px}.lulichat-btn:disabled{pointer-events:none;opacity:.5}.lulichat-btn-primary{background-color:hsl(var(--primary));color:#fff}.lulichat-btn-primary:hover:not(:disabled){background-color:#000}.lulichat-btn-default{background:hsl(var(--primary));color:hsl(var(--primary-foreground))}.lulichat-btn-default:hover{background:rgba(var(--primary),.9)}.lulichat-btn-destructive{background:hsl(var(--destructive));color:hsl(var(--destructive-foreground))}.lulichat-btn-destructive:hover{background:rgba(var(--destructive),.9)}.lulichat-btn-outline{border:1px solid hsl(var(--border));background:hsl(var(--background));color:inherit}.lulichat-btn-outline:hover{background:hsl(var(--accent));color:hsl(var(--accent-foreground))}.lulichat-btn-secondary{background:hsl(var(--secondary));color:hsl(var(--secondary-foreground))}.lulichat-btn-secondary:hover{background:hsl(var(--accent))}.lulichat-btn-ghost:hover{background:hsl(var(--accent));color:hsl(var(--accent-foreground))}.lulichat-btn-link{color:hsl(var(--primary));text-underline-offset:4px}.lulichat-btn-link:hover{text-decoration:underline}.lulichat-btn-md{height:2.5rem;padding:.5rem 1rem}.lulichat-btn-sm{height:2.25rem;padding:0 .75rem}.lulichat-btn-lg{height:2.75rem;padding:0 2rem}.lulichat-btn-icon{height:2.5rem;width:2.5rem}.lulichat-contact-form{background-color:hsl(var(--primary));padding:16px;color:#fff}.lulichat-form-group{max-height:0;opacity:0;overflow:hidden;transition:max-height .7s cubic-bezier(.22,1,.36,1),opacity .7s cubic-bezier(.22,1,.36,1)}.lulichat-form-group.open{opacity:1;max-height:500px}.lulichat-form-header{margin-bottom:1.5rem}.lulichat-form-item{margin-bottom:1rem;display:block;flex-direction:column;gap:.5rem}.lulichat-form-label{font-size:1rem;font-weight:500;display:block;margin-bottom:.25rem}.lulichat-form-item input,.lulichat-form-item textarea,.lulichat-input{padding:.7rem .75rem;border:1px solid #d1d5db;border-radius:.5rem;font-size:1rem;width:100%;transition:border-color .2s;transition:background-color .5s;background-color:#fff;color:#222}input:-internal-autofill-selected,textarea:-webkit-autofill{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#0003!important;color:fieldtext!important}.lulichat-form-item input.transparent{background-color:#0003!important;color:#fff}.lulichat-form-item input.error{border-color:red}.lulichat-form-item input.transparent:hover,.lulichat-form-item input.transparent:focus{background-color:#0006!important}.lulichat-form-item input.transparent::placeholder{color:#fff}.lulichat-form-item input:focus,.lulichat-form-item textarea:focus{border-color:#ceced2;outline:none}.lulichat-form-item input:focus::placeholder,.lulichat-form-item textarea:focus::placeholder{opacity:.5}.lulichat-form-item input[aria-invalid=true],.lulichat-form-item textarea[aria-invalid=true]{border-color:#ef4444}.lulichat-form-item .error{color:#fd8989;font-size:.875rem;margin-top:.25rem;font-weight:400}.lulichat-tag{padding:5px 8px;font-weight:500;border-radius:24px;font-size:.75rem;text-transform:capitalize!important;background-color:#e7e6e6;cursor:pointer;transition:background-color .4s,color .2s}.lulichat-tag:hover{background-color:var(--primary-color);color:#fff}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
|
|
3
|
-
const ChatIcon: React.FC<{ onClick: () => void }> = ({ onClick }) => {
|
|
4
|
-
return (
|
|
5
|
-
<div
|
|
6
|
-
onClick={onClick}
|
|
7
|
-
style={{
|
|
8
|
-
position: 'fixed',
|
|
9
|
-
bottom: '20px',
|
|
10
|
-
right: '20px',
|
|
11
|
-
cursor: 'pointer',
|
|
12
|
-
backgroundColor: '#007bff',
|
|
13
|
-
borderRadius: '50%',
|
|
14
|
-
padding: '10px',
|
|
15
|
-
boxShadow: '0 2px 10px rgba(0, 0, 0, 0.2)',
|
|
16
|
-
}}
|
|
17
|
-
>
|
|
18
|
-
<img
|
|
19
|
-
src="/path/to/chat-icon.svg"
|
|
20
|
-
alt="Chat"
|
|
21
|
-
style={{ width: '30px', height: '30px' }}
|
|
22
|
-
/>
|
|
23
|
-
</div>
|
|
24
|
-
);
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
export default ChatIcon;
|
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
import React, { useState, useRef, useEffect } from "react";
|
|
2
|
-
|
|
3
|
-
import { ChatMessage, ChatSession, CompanyInfo } from "../types";
|
|
4
|
-
import { ChatSocket } from "../lib/socket";
|
|
5
|
-
import { Headphones, Send, User } from "./ui/Icons";
|
|
6
|
-
import Button from "./ui/Button";
|
|
7
|
-
|
|
8
|
-
interface ChatInterfaceProps {
|
|
9
|
-
session: ChatSession;
|
|
10
|
-
companyInfo: CompanyInfo["data"];
|
|
11
|
-
onClose: () => void;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export const ChatInterface: React.FC<ChatInterfaceProps> = ({
|
|
15
|
-
session,
|
|
16
|
-
companyInfo,
|
|
17
|
-
onClose,
|
|
18
|
-
}) => {
|
|
19
|
-
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
|
20
|
-
const [newMessage, setNewMessage] = useState("");
|
|
21
|
-
const [isTyping, setIsTyping] = useState(false);
|
|
22
|
-
const [connectionStatus, setConnectionStatus] = useState<
|
|
23
|
-
"connected" | "disconnected" | "error"
|
|
24
|
-
>("disconnected");
|
|
25
|
-
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
26
|
-
const socketRef = useRef<ChatSocket | null>(null);
|
|
27
|
-
|
|
28
|
-
useEffect(() => {
|
|
29
|
-
// Initialize socket connection
|
|
30
|
-
socketRef.current = new ChatSocket(session.token, session.id);
|
|
31
|
-
|
|
32
|
-
socketRef.current.onMessage((message) => {
|
|
33
|
-
setMessages((prev) => [...prev, message]);
|
|
34
|
-
setIsTyping(false);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
socketRef.current.onStatus((status) => {
|
|
38
|
-
setConnectionStatus(status);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
// Add welcome message
|
|
42
|
-
const welcomeMessage: ChatMessage = {
|
|
43
|
-
id: "welcome",
|
|
44
|
-
content: "",
|
|
45
|
-
timestamp: new Date(),
|
|
46
|
-
sender: "system",
|
|
47
|
-
};
|
|
48
|
-
setMessages([welcomeMessage]);
|
|
49
|
-
|
|
50
|
-
return () => {
|
|
51
|
-
socketRef.current?.disconnect();
|
|
52
|
-
};
|
|
53
|
-
}, [session, companyInfo]);
|
|
54
|
-
|
|
55
|
-
useEffect(() => {
|
|
56
|
-
scrollToBottom();
|
|
57
|
-
}, [messages]);
|
|
58
|
-
|
|
59
|
-
const scrollToBottom = () => {
|
|
60
|
-
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
const handleSendMessage = () => {
|
|
64
|
-
if (!newMessage.trim() || connectionStatus !== "connected") return;
|
|
65
|
-
|
|
66
|
-
const userMessage: ChatMessage = {
|
|
67
|
-
id: Date.now().toString(),
|
|
68
|
-
content: newMessage,
|
|
69
|
-
timestamp: new Date(),
|
|
70
|
-
sender: "user",
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
setMessages((prev) => [...prev, userMessage]);
|
|
74
|
-
socketRef.current?.sendMessage(newMessage);
|
|
75
|
-
setNewMessage("");
|
|
76
|
-
setIsTyping(true);
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
const handleKeyPress = (e: React.KeyboardEvent) => {
|
|
80
|
-
if (e.key === "Enter" && !e.shiftKey) {
|
|
81
|
-
e.preventDefault();
|
|
82
|
-
handleSendMessage();
|
|
83
|
-
}
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
const formatTime = (date: Date) => {
|
|
87
|
-
return new Intl.DateTimeFormat("en-US", {
|
|
88
|
-
hour: "2-digit",
|
|
89
|
-
minute: "2-digit",
|
|
90
|
-
}).format(date);
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
const getMessageIcon = (sender: ChatMessage["sender"]) => {
|
|
94
|
-
switch (sender) {
|
|
95
|
-
case "agent":
|
|
96
|
-
return <Headphones className="w-4 h-4" />;
|
|
97
|
-
case "user":
|
|
98
|
-
return <User className="w-4 h-4" />;
|
|
99
|
-
default:
|
|
100
|
-
return null;
|
|
101
|
-
}
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
return (
|
|
105
|
-
<div className="flex flex-col h-full">
|
|
106
|
-
{/* Header */}
|
|
107
|
-
<div className="bg-primary text-primary-foreground p-4 rounded-t-lg">
|
|
108
|
-
<div className="flex items-center justify-between">
|
|
109
|
-
<div className="flex items-center space-x-2">
|
|
110
|
-
<div className="w-2 h-2 rounded-full bg-green-400"></div>
|
|
111
|
-
<span className="font-medium">{companyInfo.name}</span>
|
|
112
|
-
</div>
|
|
113
|
-
<Button
|
|
114
|
-
variant="ghost"
|
|
115
|
-
size="sm"
|
|
116
|
-
onClick={onClose}
|
|
117
|
-
className="text-primary-foreground hover:bg-primary-foreground/20"
|
|
118
|
-
>
|
|
119
|
-
×
|
|
120
|
-
</Button>
|
|
121
|
-
</div>
|
|
122
|
-
<div className="text-xs opacity-80 mt-1">
|
|
123
|
-
{connectionStatus === "connected" ? "Online" : "Connecting..."}
|
|
124
|
-
</div>
|
|
125
|
-
</div>
|
|
126
|
-
|
|
127
|
-
{/* Messages */}
|
|
128
|
-
<div className="flex-1 overflow-y-auto p-4 space-y-4 bg-background">
|
|
129
|
-
{messages.map((message) => (
|
|
130
|
-
<div
|
|
131
|
-
key={message.id}
|
|
132
|
-
className={`flex ${
|
|
133
|
-
message.sender === "user" ? "justify-end" : "justify-start"
|
|
134
|
-
}`}
|
|
135
|
-
>
|
|
136
|
-
<div
|
|
137
|
-
className={`max-w-xs lg:max-w-md px-4 py-2 rounded-lg ${
|
|
138
|
-
message.sender === "user"
|
|
139
|
-
? "bg-primary text-primary-foreground"
|
|
140
|
-
: message.sender === "system"
|
|
141
|
-
? "bg-muted text-muted-foreground text-center"
|
|
142
|
-
: "bg-muted text-foreground"
|
|
143
|
-
}`}
|
|
144
|
-
>
|
|
145
|
-
{message.sender !== "system" && (
|
|
146
|
-
<div className="flex items-center space-x-1 mb-1">
|
|
147
|
-
{getMessageIcon(message.sender)}
|
|
148
|
-
<span className="text-xs opacity-70">
|
|
149
|
-
{message.senderName ||
|
|
150
|
-
(message.sender === "user" ? "You" : "Support")}
|
|
151
|
-
</span>
|
|
152
|
-
</div>
|
|
153
|
-
)}
|
|
154
|
-
<p className="text-sm">{message.content}</p>
|
|
155
|
-
<p className="text-xs opacity-60 mt-1">
|
|
156
|
-
{formatTime(message.timestamp)}
|
|
157
|
-
</p>
|
|
158
|
-
</div>
|
|
159
|
-
</div>
|
|
160
|
-
))}
|
|
161
|
-
|
|
162
|
-
{isTyping && (
|
|
163
|
-
<div className="flex justify-start">
|
|
164
|
-
<div className="bg-muted text-foreground px-4 py-2 rounded-lg">
|
|
165
|
-
<div className="flex space-x-1">
|
|
166
|
-
<div className="w-2 h-2 bg-current rounded-full animate-bounce"></div>
|
|
167
|
-
<div
|
|
168
|
-
className="w-2 h-2 bg-current rounded-full animate-bounce"
|
|
169
|
-
style={{ animationDelay: "0.1s" }}
|
|
170
|
-
></div>
|
|
171
|
-
<div
|
|
172
|
-
className="w-2 h-2 bg-current rounded-full animate-bounce"
|
|
173
|
-
style={{ animationDelay: "0.2s" }}
|
|
174
|
-
></div>
|
|
175
|
-
</div>
|
|
176
|
-
</div>
|
|
177
|
-
</div>
|
|
178
|
-
)}
|
|
179
|
-
<div ref={messagesEndRef} />
|
|
180
|
-
</div>
|
|
181
|
-
|
|
182
|
-
{/* Input */}
|
|
183
|
-
<div className="p-4 border-t bg-background">
|
|
184
|
-
<div className="flex space-x-2">
|
|
185
|
-
<input
|
|
186
|
-
value={newMessage}
|
|
187
|
-
onChange={(e) => setNewMessage(e.target.value)}
|
|
188
|
-
onKeyPress={handleKeyPress}
|
|
189
|
-
placeholder="Type your message..."
|
|
190
|
-
disabled={connectionStatus !== "connected"}
|
|
191
|
-
className="flex-1"
|
|
192
|
-
/>
|
|
193
|
-
<Button
|
|
194
|
-
onClick={handleSendMessage}
|
|
195
|
-
disabled={!newMessage.trim() || connectionStatus !== "connected"}
|
|
196
|
-
size="icon"
|
|
197
|
-
>
|
|
198
|
-
<Send className="w-4 h-4" />
|
|
199
|
-
</Button>
|
|
200
|
-
</div>
|
|
201
|
-
{connectionStatus !== "connected" && (
|
|
202
|
-
<p className="text-xs text-muted-foreground mt-2">
|
|
203
|
-
{connectionStatus === "error"
|
|
204
|
-
? "Connection error. Retrying..."
|
|
205
|
-
: "Connecting..."}
|
|
206
|
-
</p>
|
|
207
|
-
)}
|
|
208
|
-
</div>
|
|
209
|
-
</div>
|
|
210
|
-
);
|
|
211
|
-
};
|
|
@@ -1,198 +0,0 @@
|
|
|
1
|
-
import React, { useState, useEffect } from "react";
|
|
2
|
-
import {
|
|
3
|
-
LuliChatConfig,
|
|
4
|
-
ChatState,
|
|
5
|
-
ContactInfo,
|
|
6
|
-
CompanyInfo,
|
|
7
|
-
ChatSession,
|
|
8
|
-
} from "../types";
|
|
9
|
-
import { LuliChatAPI } from "@/utils/api";
|
|
10
|
-
import { ContactForm } from "./ContactForm";
|
|
11
|
-
import { ChatInterface } from "./ChatInterface";
|
|
12
|
-
import { Loader, MessageCircle } from "./ui/Icons";
|
|
13
|
-
import Button from "./ui/Button";
|
|
14
|
-
import Input from "./ui/Input";
|
|
15
|
-
import { Attachment, Send } from "./ui/Icons";
|
|
16
|
-
|
|
17
|
-
interface ChatWidgetProps {
|
|
18
|
-
config: LuliChatConfig;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export const ChatWidget: React.FC<ChatWidgetProps> = ({ config }) => {
|
|
22
|
-
const [chatState, setChatState] = useState<ChatState>("contact-form");
|
|
23
|
-
const [companyInfo, setCompanyInfo] = useState<CompanyInfo["data"] | null>(
|
|
24
|
-
null
|
|
25
|
-
);
|
|
26
|
-
const [chatSession, setChatSession] = useState<ChatSession | null>(null);
|
|
27
|
-
const [isLoading, setIsLoading] = useState(false);
|
|
28
|
-
const [error, setError] = useState<string | null>(null);
|
|
29
|
-
const [open, setOpen] = React.useState(false);
|
|
30
|
-
|
|
31
|
-
const api = new LuliChatAPI(config);
|
|
32
|
-
|
|
33
|
-
useEffect(() => {
|
|
34
|
-
loadCompanyInfo();
|
|
35
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
36
|
-
}, [config]);
|
|
37
|
-
|
|
38
|
-
const loadCompanyInfo = async () => {
|
|
39
|
-
try {
|
|
40
|
-
const info = await api.getCompanyInfo();
|
|
41
|
-
setCompanyInfo(info.data);
|
|
42
|
-
} catch (err) {
|
|
43
|
-
setError("Failed to load chat configuration");
|
|
44
|
-
console.error("Failed to load company info:", err);
|
|
45
|
-
}
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
const handleContactSubmit = async (contactInfo: ContactInfo) => {
|
|
49
|
-
setIsLoading(true);
|
|
50
|
-
try {
|
|
51
|
-
const response = await api.submitContactInfo(contactInfo);
|
|
52
|
-
const session: ChatSession = {
|
|
53
|
-
id: response.sessionId,
|
|
54
|
-
token: response.token,
|
|
55
|
-
isActive: true,
|
|
56
|
-
};
|
|
57
|
-
setChatSession(session);
|
|
58
|
-
setChatState("chat");
|
|
59
|
-
} catch (err) {
|
|
60
|
-
setError("Failed to start chat. Please try again.");
|
|
61
|
-
console.error("Failed to submit contact info:", err);
|
|
62
|
-
} finally {
|
|
63
|
-
setIsLoading(false);
|
|
64
|
-
}
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
const startAnonymousChat = async () => {
|
|
68
|
-
setIsLoading(true);
|
|
69
|
-
try {
|
|
70
|
-
const response = await api.startAnonymousChat();
|
|
71
|
-
const session: ChatSession = {
|
|
72
|
-
id: response.sessionId,
|
|
73
|
-
token: response.token,
|
|
74
|
-
isActive: true,
|
|
75
|
-
};
|
|
76
|
-
setChatSession(session);
|
|
77
|
-
setChatState("chat");
|
|
78
|
-
} catch (err) {
|
|
79
|
-
setError("Failed to start chat. Please try again.");
|
|
80
|
-
console.error("Failed to start anonymous chat:", err);
|
|
81
|
-
} finally {
|
|
82
|
-
setIsLoading(false);
|
|
83
|
-
}
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
const handleCloseChat = () => {
|
|
87
|
-
setChatState("closed");
|
|
88
|
-
setChatSession(null);
|
|
89
|
-
setError(null);
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
console.log({ companyInfo });
|
|
93
|
-
|
|
94
|
-
return (
|
|
95
|
-
<div className={`lulichat lulichat-${config.position}`}>
|
|
96
|
-
{open && (
|
|
97
|
-
<div className="lulichat-main">
|
|
98
|
-
{error && (
|
|
99
|
-
<div className="p-4 bg-destructive/10 border-b">
|
|
100
|
-
<p className="text-sm text-destructive">{error}</p>
|
|
101
|
-
<button onClick={() => setError(null)} className="mt-2">
|
|
102
|
-
Retry
|
|
103
|
-
</button>
|
|
104
|
-
</div>
|
|
105
|
-
)}
|
|
106
|
-
|
|
107
|
-
<ContactForm
|
|
108
|
-
companyName={companyInfo?.name!}
|
|
109
|
-
onSubmit={handleContactSubmit}
|
|
110
|
-
onSkip={config.allowAnonymous ? startAnonymousChat : undefined}
|
|
111
|
-
allowAnonymous={config.allowAnonymous || false}
|
|
112
|
-
isLoading={isLoading}
|
|
113
|
-
/>
|
|
114
|
-
|
|
115
|
-
<div style={{ padding: 16 }}>
|
|
116
|
-
<div
|
|
117
|
-
style={{
|
|
118
|
-
display: "flex",
|
|
119
|
-
gap: 10,
|
|
120
|
-
flexWrap: "wrap",
|
|
121
|
-
}}
|
|
122
|
-
>
|
|
123
|
-
{companyInfo?.queues.map((queue) => {
|
|
124
|
-
return (
|
|
125
|
-
<div className="lulichat-tag" key={queue.id}>
|
|
126
|
-
{queue.name?.toLocaleLowerCase()}
|
|
127
|
-
</div>
|
|
128
|
-
);
|
|
129
|
-
})}
|
|
130
|
-
</div>
|
|
131
|
-
|
|
132
|
-
<div
|
|
133
|
-
style={{
|
|
134
|
-
height: 2,
|
|
135
|
-
backgroundColor: "hsl(var(--border))",
|
|
136
|
-
marginBlock: 16,
|
|
137
|
-
}}
|
|
138
|
-
/>
|
|
139
|
-
|
|
140
|
-
<div
|
|
141
|
-
style={{
|
|
142
|
-
display: "flex",
|
|
143
|
-
alignItems: "center",
|
|
144
|
-
columnGap: 10,
|
|
145
|
-
}}
|
|
146
|
-
className="form-control"
|
|
147
|
-
>
|
|
148
|
-
<Input
|
|
149
|
-
name="message"
|
|
150
|
-
autoComplete="off"
|
|
151
|
-
style={{ border: "none", paddingInline: 0 }}
|
|
152
|
-
placeholder="What can we help you with ?."
|
|
153
|
-
/>
|
|
154
|
-
<div className="lulichat-btn-group">
|
|
155
|
-
<Button style={{ color: "#00000080" }} size="icon">
|
|
156
|
-
<Attachment height={24} />
|
|
157
|
-
</Button>
|
|
158
|
-
<Button style={{ color: "#00000080" }} size="icon">
|
|
159
|
-
<Send />
|
|
160
|
-
</Button>
|
|
161
|
-
</div>
|
|
162
|
-
</div>
|
|
163
|
-
</div>
|
|
164
|
-
|
|
165
|
-
{chatState === "chat" && chatSession && companyInfo && (
|
|
166
|
-
<ChatInterface
|
|
167
|
-
session={chatSession}
|
|
168
|
-
companyInfo={companyInfo}
|
|
169
|
-
onClose={handleCloseChat}
|
|
170
|
-
/>
|
|
171
|
-
)}
|
|
172
|
-
</div>
|
|
173
|
-
)}
|
|
174
|
-
|
|
175
|
-
<Button
|
|
176
|
-
onClick={() => setOpen(!open)}
|
|
177
|
-
className=""
|
|
178
|
-
shape="circle"
|
|
179
|
-
size="icon"
|
|
180
|
-
style={{
|
|
181
|
-
backgroundColor: config.primaryColor,
|
|
182
|
-
height: "4rem",
|
|
183
|
-
width: "4rem",
|
|
184
|
-
padding: 0,
|
|
185
|
-
color: "#fff",
|
|
186
|
-
alignSelf: "end",
|
|
187
|
-
}}
|
|
188
|
-
disabled={!companyInfo && !error}
|
|
189
|
-
>
|
|
190
|
-
{!companyInfo && !error ? (
|
|
191
|
-
<Loader />
|
|
192
|
-
) : (
|
|
193
|
-
<MessageCircle height={40} width={40} />
|
|
194
|
-
)}
|
|
195
|
-
</Button>
|
|
196
|
-
</div>
|
|
197
|
-
);
|
|
198
|
-
};
|
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
import { ContactInfo } from "@/types";
|
|
2
|
-
import React, { useState } from "react";
|
|
3
|
-
import Input from "./ui/Input";
|
|
4
|
-
import Form from "./ui/Form";
|
|
5
|
-
import { DropdownArrow, Hello, Loader } from "./ui/Icons";
|
|
6
|
-
import Button from "./ui/Button";
|
|
7
|
-
|
|
8
|
-
interface ContactFormProps {
|
|
9
|
-
companyName: string;
|
|
10
|
-
onSubmit: (contactInfo: ContactInfo) => void;
|
|
11
|
-
onSkip?: () => void;
|
|
12
|
-
allowAnonymous: boolean;
|
|
13
|
-
isLoading: boolean;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export const ContactForm: React.FC<ContactFormProps> = ({
|
|
17
|
-
companyName,
|
|
18
|
-
onSubmit,
|
|
19
|
-
onSkip,
|
|
20
|
-
allowAnonymous,
|
|
21
|
-
isLoading,
|
|
22
|
-
}) => {
|
|
23
|
-
const [open, setOpen] = React.useState(false);
|
|
24
|
-
const [formData, setFormData] = useState<ContactInfo>({
|
|
25
|
-
name: "",
|
|
26
|
-
email: "",
|
|
27
|
-
phone: "",
|
|
28
|
-
company: "",
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
const [errors, setErrors] = useState<Partial<ContactInfo>>({});
|
|
32
|
-
const [values, setValues] = React.useState<Record<string, string>>({
|
|
33
|
-
email: "",
|
|
34
|
-
name: "",
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
const validateForm = () => {
|
|
38
|
-
const newErrors: Partial<ContactInfo> = {};
|
|
39
|
-
|
|
40
|
-
if (!formData.name.trim()) {
|
|
41
|
-
newErrors.name = "Name is required";
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
if (!formData.email.trim()) {
|
|
45
|
-
newErrors.email = "Email is required";
|
|
46
|
-
} else if (!/\S+@\S+\.\S+/.test(formData.email)) {
|
|
47
|
-
newErrors.email = "Email is invalid";
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
setErrors(newErrors);
|
|
51
|
-
return Object.keys(newErrors).length === 0;
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
const handleSubmit = (e: React.FormEvent) => {
|
|
55
|
-
e.preventDefault();
|
|
56
|
-
if (validateForm()) {
|
|
57
|
-
onSubmit(formData);
|
|
58
|
-
}
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
const handleInputChange = (field: keyof ContactInfo, value: string) => {
|
|
62
|
-
setFormData((prev) => ({ ...prev, [field]: value }));
|
|
63
|
-
if (errors[field]) {
|
|
64
|
-
setErrors((prev) => ({ ...prev, [field]: undefined }));
|
|
65
|
-
}
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
return (
|
|
69
|
-
<Form
|
|
70
|
-
autoComplete="off"
|
|
71
|
-
onSubmit={handleSubmit}
|
|
72
|
-
onValuesChange={setValues}
|
|
73
|
-
className="lulichat-contact-form"
|
|
74
|
-
>
|
|
75
|
-
<div className="lulichat-form-header">
|
|
76
|
-
<div
|
|
77
|
-
style={{
|
|
78
|
-
marginBottom: "20px",
|
|
79
|
-
display: "flex",
|
|
80
|
-
alignItems: "end",
|
|
81
|
-
columnGap: 8,
|
|
82
|
-
}}
|
|
83
|
-
>
|
|
84
|
-
<Hello
|
|
85
|
-
style={{ display: "block" }}
|
|
86
|
-
color="#DEDEDE6A"
|
|
87
|
-
height={40}
|
|
88
|
-
width={50}
|
|
89
|
-
/>
|
|
90
|
-
<h3 style={{ fontWeight: 500 }}>Hello,</h3>
|
|
91
|
-
</div>
|
|
92
|
-
<h3 style={{ marginBottom: 8 }} className="lulichat-title">
|
|
93
|
-
Welcome to {companyName} Live Chat
|
|
94
|
-
</h3>
|
|
95
|
-
<p
|
|
96
|
-
role="button"
|
|
97
|
-
onClickCapture={() => setOpen(!open)}
|
|
98
|
-
style={{
|
|
99
|
-
textDecoration: "underline",
|
|
100
|
-
lineHeight: "1rem",
|
|
101
|
-
cursor: "pointer",
|
|
102
|
-
}}
|
|
103
|
-
>
|
|
104
|
-
Please provide your details for a better support experience{" "}
|
|
105
|
-
<DropdownArrow
|
|
106
|
-
style={{
|
|
107
|
-
display: "inline-block",
|
|
108
|
-
marginBottom: -2,
|
|
109
|
-
}}
|
|
110
|
-
height={20}
|
|
111
|
-
width={20}
|
|
112
|
-
/>
|
|
113
|
-
</p>
|
|
114
|
-
</div>
|
|
115
|
-
<div className={`lulichat-form-group${open ? " open" : ""}`}>
|
|
116
|
-
<Form.Item error={errors.name} label="Name (Optional)" name="name">
|
|
117
|
-
<Input
|
|
118
|
-
id="name"
|
|
119
|
-
placeholder="Enter your name"
|
|
120
|
-
type="text"
|
|
121
|
-
value={formData.name}
|
|
122
|
-
onChange={(e) => handleInputChange("name", e.target.value)}
|
|
123
|
-
className={"transparent" + (errors.name ? " error" : "")}
|
|
124
|
-
/>
|
|
125
|
-
</Form.Item>
|
|
126
|
-
|
|
127
|
-
<Form.Item name="email" error={errors.email} label="Email">
|
|
128
|
-
<Input
|
|
129
|
-
id="email"
|
|
130
|
-
type="email"
|
|
131
|
-
placeholder="Enter your valid email"
|
|
132
|
-
value={formData.email}
|
|
133
|
-
onChange={(e) => handleInputChange("email", e.target.value)}
|
|
134
|
-
className={"transparent" + (errors.name ? " error" : "")}
|
|
135
|
-
/>
|
|
136
|
-
</Form.Item>
|
|
137
|
-
<Button
|
|
138
|
-
type="submit"
|
|
139
|
-
disabled={isLoading || !values.email}
|
|
140
|
-
style={{ width: "100%", marginTop: 10 }}
|
|
141
|
-
className="lulichat-contact-form-btn"
|
|
142
|
-
>
|
|
143
|
-
{isLoading ? (
|
|
144
|
-
<>
|
|
145
|
-
<Loader />
|
|
146
|
-
Submitting...
|
|
147
|
-
</>
|
|
148
|
-
) : (
|
|
149
|
-
"Submit"
|
|
150
|
-
)}
|
|
151
|
-
</Button>
|
|
152
|
-
</div>
|
|
153
|
-
</Form>
|
|
154
|
-
);
|
|
155
|
-
};
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import { ChatWidget } from "./ChatWidget";
|
|
3
|
-
import { LuliChatConfig } from "../types";
|
|
4
|
-
|
|
5
|
-
interface LuliChatProps {
|
|
6
|
-
apiKey: string;
|
|
7
|
-
baseUrl?: string;
|
|
8
|
-
position?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
|
|
9
|
-
primaryColor?: string;
|
|
10
|
-
companyName?: string;
|
|
11
|
-
welcomeMessage?: string;
|
|
12
|
-
requireContactInfo?: boolean;
|
|
13
|
-
allowAnonymous?: boolean;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export const LuliChat: React.FC<LuliChatProps> = ({
|
|
17
|
-
apiKey,
|
|
18
|
-
baseUrl,
|
|
19
|
-
position = "bottom-right",
|
|
20
|
-
primaryColor = "#007bff",
|
|
21
|
-
companyName = "Support",
|
|
22
|
-
welcomeMessage = "Hello! How can we help you today?",
|
|
23
|
-
requireContactInfo = true,
|
|
24
|
-
allowAnonymous = true,
|
|
25
|
-
}) => {
|
|
26
|
-
const config: LuliChatConfig = {
|
|
27
|
-
apiKey,
|
|
28
|
-
baseUrl,
|
|
29
|
-
position,
|
|
30
|
-
primaryColor,
|
|
31
|
-
companyName,
|
|
32
|
-
welcomeMessage,
|
|
33
|
-
requireContactInfo,
|
|
34
|
-
allowAnonymous,
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
if (!apiKey) {
|
|
38
|
-
console.error("LuliChat: API key is required");
|
|
39
|
-
return null;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return <ChatWidget config={config} />;
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
// Export all types and components for external use
|
|
46
|
-
export * from "../types";
|
|
47
|
-
export { LuliChatAPI } from "../utils/api";
|
|
48
|
-
export { ChatSocket } from "../lib/socket";
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import clsx from "clsx";
|
|
3
|
-
|
|
4
|
-
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
5
|
-
variant?:
|
|
6
|
-
| "primary"
|
|
7
|
-
| "destructive"
|
|
8
|
-
| "outline"
|
|
9
|
-
| "secondary"
|
|
10
|
-
| "ghost"
|
|
11
|
-
| "link";
|
|
12
|
-
size?: "md" | "sm" | "lg" | "icon";
|
|
13
|
-
shape?: "circle" | "rounded" | "none";
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
17
|
-
(
|
|
18
|
-
{
|
|
19
|
-
className,
|
|
20
|
-
variant = "outline",
|
|
21
|
-
size = "md",
|
|
22
|
-
shape = "rounded",
|
|
23
|
-
...props
|
|
24
|
-
},
|
|
25
|
-
ref
|
|
26
|
-
) => {
|
|
27
|
-
return (
|
|
28
|
-
<button
|
|
29
|
-
className={clsx(
|
|
30
|
-
"lulichat-btn",
|
|
31
|
-
`lulichat-btn-${variant}`,
|
|
32
|
-
`lulichat-btn-${size}`,
|
|
33
|
-
`lulichat-btn-${shape}`,
|
|
34
|
-
className
|
|
35
|
-
)}
|
|
36
|
-
ref={ref}
|
|
37
|
-
{...props}
|
|
38
|
-
/>
|
|
39
|
-
);
|
|
40
|
-
}
|
|
41
|
-
);
|
|
42
|
-
Button.displayName = "Button";
|
|
43
|
-
|
|
44
|
-
export default Button;
|