@shashankrai/echo-chat-react-widget 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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Shashank Rai
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,82 @@
1
+ # @shashankrai/echo-chat-react-widget
2
+
3
+ An embeddable React chat widget for the ECHO Platform. Easily add an AI-powered chat assistant to your website using your unique API Key.
4
+
5
+ ## Features
6
+
7
+ - 🚀 Drop-in React component - minimal setup required
8
+ - 💬 AI-powered chat interface
9
+ - 🎨 Customizable and responsive design
10
+ - 🔒 Secure API key authentication
11
+ - âš¡ Lightweight and performant
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ npm install @shashankrai/echo-chat-react-widget
17
+ ```
18
+
19
+ Or using yarn:
20
+ ```bash
21
+ yarn add @shashankrai/echo-chat-react-widget
22
+ ```
23
+
24
+ Or using pnpm:
25
+ ```bash
26
+ pnpm add @shashankrai/echo-chat-react-widget
27
+ ```
28
+
29
+ ## Usage
30
+
31
+ Wrap your application (or a specific page) in the `<EchoProvider>`, and place the `<ChatWidget>` component anywhere in the React tree. The widget automatically handles rendering the floating button and the chat UI.
32
+
33
+ ```tsx
34
+ import { EchoProvider, ChatWidget } from '@shashankrai/echo-chat-react-widget';
35
+
36
+ export default function RootLayout({ children }) {
37
+ return (
38
+ <html lang="en">
39
+ <body>
40
+ <EchoProvider apiKey="YOUR_LIVE_OR_TEST_API_KEY">
41
+ {children}
42
+ {/* Renders the floating chat bubble */}
43
+ <ChatWidget />
44
+ </EchoProvider>
45
+ </body>
46
+ </html>
47
+ );
48
+ }
49
+ ```
50
+
51
+ ### API Key Configuration
52
+
53
+ The `apiKey` passed to the provider securely identifies your company and dynamically scopes all AI queries strictly to your company's knowledge base.
54
+
55
+ - Use your **Live API Key** for production environments
56
+ - Use your **Test API Key** for development and testing
57
+
58
+ ## Props
59
+
60
+ ### EchoProvider
61
+
62
+ | Prop | Type | Required | Description |
63
+ |------|------|----------|-------------|
64
+ | `apiKey` | `string` | Yes | Your Echo API key (Live or Test) |
65
+ | `children` | `ReactNode` | Yes | Your application content |
66
+
67
+ ### ChatWidget
68
+
69
+ The `ChatWidget` component doesn't require any props. Simply place it inside your `EchoProvider`.
70
+
71
+ ## Requirements
72
+
73
+ - React 18.0.0 or higher
74
+ - React DOM 18.0.0 or higher
75
+
76
+ ## License
77
+
78
+ MIT
79
+
80
+ ## Support
81
+
82
+ For issues, questions, or contributions, please visit our [GitHub repository](https://github.com/shashankrai/echo).
@@ -0,0 +1,21 @@
1
+ import React, { ReactNode } from 'react';
2
+
3
+ interface EchoContextType {
4
+ apiKey: string;
5
+ }
6
+ interface EchoProviderProps {
7
+ apiKey: string;
8
+ children: ReactNode;
9
+ }
10
+ declare const EchoProvider: React.FC<EchoProviderProps>;
11
+ declare const useEcho: () => EchoContextType;
12
+
13
+ interface ChatWidgetProps {
14
+ apiUrl?: string;
15
+ initialMessage?: string;
16
+ title?: string;
17
+ primaryColor?: string;
18
+ }
19
+ declare const ChatWidget: React.FC<ChatWidgetProps>;
20
+
21
+ export { ChatWidget, EchoProvider, useEcho };
@@ -0,0 +1,21 @@
1
+ import React, { ReactNode } from 'react';
2
+
3
+ interface EchoContextType {
4
+ apiKey: string;
5
+ }
6
+ interface EchoProviderProps {
7
+ apiKey: string;
8
+ children: ReactNode;
9
+ }
10
+ declare const EchoProvider: React.FC<EchoProviderProps>;
11
+ declare const useEcho: () => EchoContextType;
12
+
13
+ interface ChatWidgetProps {
14
+ apiUrl?: string;
15
+ initialMessage?: string;
16
+ title?: string;
17
+ primaryColor?: string;
18
+ }
19
+ declare const ChatWidget: React.FC<ChatWidgetProps>;
20
+
21
+ export { ChatWidget, EchoProvider, useEcho };
package/dist/index.js ADDED
@@ -0,0 +1,268 @@
1
+ "use client";
2
+ "use strict";
3
+ "use client";
4
+ var __create = Object.create;
5
+ var __defProp = Object.defineProperty;
6
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
7
+ var __getOwnPropNames = Object.getOwnPropertyNames;
8
+ var __getProtoOf = Object.getPrototypeOf;
9
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
10
+ var __export = (target, all) => {
11
+ for (var name in all)
12
+ __defProp(target, name, { get: all[name], enumerable: true });
13
+ };
14
+ var __copyProps = (to, from, except, desc) => {
15
+ if (from && typeof from === "object" || typeof from === "function") {
16
+ for (let key of __getOwnPropNames(from))
17
+ if (!__hasOwnProp.call(to, key) && key !== except)
18
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
19
+ }
20
+ return to;
21
+ };
22
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
23
+ // If the importer is in node compatibility mode or this is not an ESM
24
+ // file that has been converted to a CommonJS file using a Babel-
25
+ // compatible transform (i.e. "__esModule" has not been set), then set
26
+ // "default" to the CommonJS "module.exports" for node compatibility.
27
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
28
+ mod
29
+ ));
30
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
31
+
32
+ // src/index.tsx
33
+ var index_exports = {};
34
+ __export(index_exports, {
35
+ ChatWidget: () => ChatWidget,
36
+ EchoProvider: () => EchoProvider,
37
+ useEcho: () => useEcho
38
+ });
39
+ module.exports = __toCommonJS(index_exports);
40
+
41
+ // src/EchoProvider.tsx
42
+ var import_react = require("react");
43
+ var import_jsx_runtime = require("react/jsx-runtime");
44
+ var EchoContext = (0, import_react.createContext)(void 0);
45
+ var EchoProvider = ({ apiKey, children }) => {
46
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(EchoContext.Provider, { value: { apiKey }, children });
47
+ };
48
+ var useEcho = () => {
49
+ const context = (0, import_react.useContext)(EchoContext);
50
+ if (!context) {
51
+ throw new Error("useEcho must be used within an EchoProvider");
52
+ }
53
+ return context;
54
+ };
55
+
56
+ // src/ChatWidget.tsx
57
+ var import_react2 = require("react");
58
+ var import_socket = require("socket.io-client");
59
+ var import_react_markdown = __toESM(require("react-markdown"));
60
+ var import_remark_gfm = __toESM(require("remark-gfm"));
61
+ var import_jsx_runtime2 = require("react/jsx-runtime");
62
+ var css = `
63
+ .echo-markdown p { margin-bottom: 0.5rem; }
64
+ .echo-markdown p:last-child { margin-bottom: 0; }
65
+ .echo-markdown ul { list-style-type: disc; padding-left: 1.25rem; margin-bottom: 0.5rem; }
66
+ .echo-markdown ol { list-style-type: decimal; padding-left: 1.25rem; margin-bottom: 0.5rem; }
67
+ .echo-markdown li { margin-bottom: 0.25rem; }
68
+ .echo-markdown strong { font-weight: 600; }
69
+ .echo-markdown code { background: rgba(0,0,0,0.05); padding: 0.125rem 0.25rem; border-radius: 0.25rem; font-family: monospace; font-size: 0.875em; }
70
+ .echo-markdown pre { background: rgba(0,0,0,0.05); padding: 0.5rem; border-radius: 0.25rem; overflow-x: auto; font-family: monospace; font-size: 0.875em; margin-bottom: 0.5rem; }
71
+ .echo-markdown a { color: inherit; text-decoration: underline; }
72
+ `;
73
+ var ChatWidget = ({
74
+ apiUrl = "http://localhost:3002",
75
+ initialMessage = "Hello! How can I help you today?",
76
+ title = "Chat Support",
77
+ primaryColor = "#2563eb"
78
+ }) => {
79
+ const { apiKey } = useEcho();
80
+ const [isOpen, setIsOpen] = (0, import_react2.useState)(false);
81
+ const [messages, setMessages] = (0, import_react2.useState)([
82
+ { role: "assistant", content: initialMessage }
83
+ ]);
84
+ const [input, setInput] = (0, import_react2.useState)("");
85
+ const [sessionId, setSessionId] = (0, import_react2.useState)(() => Math.random().toString(36).substring(7));
86
+ const [isLoading, setIsLoading] = (0, import_react2.useState)(false);
87
+ const messagesEndRef = (0, import_react2.useRef)(null);
88
+ const [ticketId, setTicketId] = (0, import_react2.useState)(null);
89
+ const [isLiveChat, setIsLiveChat] = (0, import_react2.useState)(false);
90
+ const socketRef = (0, import_react2.useRef)(null);
91
+ const API_URL = apiUrl;
92
+ const CONVEX_URL = `${API_URL}/chat`;
93
+ (0, import_react2.useEffect)(() => {
94
+ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
95
+ }, [messages]);
96
+ (0, import_react2.useEffect)(() => {
97
+ return () => {
98
+ if (socketRef.current) {
99
+ socketRef.current.disconnect();
100
+ }
101
+ };
102
+ }, []);
103
+ const connectSocket = (newTicketId, currentSessionId) => {
104
+ if (socketRef.current) return;
105
+ const socket = (0, import_socket.io)(API_URL, {
106
+ auth: {
107
+ apiKey
108
+ }
109
+ });
110
+ socket.on("connect", () => {
111
+ console.log("Widget connected for live chat");
112
+ socket.emit("join-ticket", { ticketId: newTicketId, sessionId: currentSessionId });
113
+ setIsLiveChat(true);
114
+ });
115
+ socket.on("new-message", (data) => {
116
+ if (data.role === "agent") {
117
+ setMessages((prev) => [...prev, { role: "agent", content: data.content }]);
118
+ }
119
+ });
120
+ socket.on("user-joined", (data) => {
121
+ if (data.role === "admin" || data.role === "agent") {
122
+ setMessages((prev) => [...prev, { role: "assistant", content: `\u{1F7E2} An agent (${data.email}) has joined the chat.` }]);
123
+ }
124
+ });
125
+ socket.on("ticket-resolved", () => {
126
+ setIsLiveChat(false);
127
+ setMessages((prev) => [...prev, { role: "assistant", content: "\u2705 This ticket has been resolved. The live chat session has ended." }]);
128
+ socket.disconnect();
129
+ socketRef.current = null;
130
+ });
131
+ socketRef.current = socket;
132
+ };
133
+ const handleSend = async () => {
134
+ if (!input.trim() || isLoading) return;
135
+ const userMsg = input.trim();
136
+ setMessages((prev) => [...prev, { role: "user", content: userMsg }]);
137
+ setInput("");
138
+ if (isLiveChat && ticketId && socketRef.current?.connected) {
139
+ socketRef.current.emit("user-message", {
140
+ ticketId,
141
+ message: userMsg
142
+ });
143
+ return;
144
+ }
145
+ setIsLoading(true);
146
+ try {
147
+ const response = await fetch(CONVEX_URL, {
148
+ method: "POST",
149
+ headers: {
150
+ "Content-Type": "application/json",
151
+ "x-api-key": apiKey,
152
+ "x-session-id": sessionId
153
+ },
154
+ body: JSON.stringify({
155
+ message: userMsg
156
+ })
157
+ });
158
+ if (!response.ok) {
159
+ throw new Error("Failed to send message.");
160
+ }
161
+ const data = await response.json();
162
+ if (data.sessionId) setSessionId(data.sessionId);
163
+ setMessages((prev) => [...prev, {
164
+ role: "assistant",
165
+ content: data.response || "No response."
166
+ }]);
167
+ if (data.ticketCreated && data.ticketId) {
168
+ setTicketId(data.ticketId);
169
+ setMessages((prev) => [...prev, {
170
+ role: "assistant",
171
+ content: "\u26A0\uFE0F A support ticket has been created for this query. An admin will be notified and may join this chat."
172
+ }]);
173
+ connectSocket(data.ticketId, data.sessionId || sessionId);
174
+ }
175
+ } catch (err) {
176
+ setMessages((prev) => [...prev, {
177
+ role: "assistant",
178
+ content: `Error: ${err.message}`
179
+ }]);
180
+ } finally {
181
+ setIsLoading(false);
182
+ }
183
+ };
184
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { position: "fixed", bottom: "20px", right: "20px", zIndex: 9999, fontFamily: "sans-serif" }, children: [
185
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("style", { children: css }),
186
+ isOpen && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: {
187
+ width: "380px",
188
+ height: "600px",
189
+ backgroundColor: "white",
190
+ borderRadius: "12px",
191
+ boxShadow: "0 8px 24px rgba(0,0,0,0.15)",
192
+ display: "flex",
193
+ flexDirection: "column",
194
+ overflow: "hidden",
195
+ marginBottom: "16px",
196
+ border: "1px solid #e5e7eb"
197
+ }, children: [
198
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { backgroundColor: primaryColor, color: "white", padding: "16px", fontWeight: "bold", fontSize: "16px" }, children: title }),
199
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { flex: 1, overflowY: "auto", padding: "16px", display: "flex", flexDirection: "column", gap: "12px", backgroundColor: "#f8fafc" }, children: [
200
+ messages.map((msg, i) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "echo-markdown", style: {
201
+ alignSelf: msg.role === "user" ? "flex-end" : "flex-start",
202
+ backgroundColor: msg.role === "user" ? primaryColor : "#ffffff",
203
+ color: msg.role === "user" ? "white" : "#1e293b",
204
+ padding: "12px 16px",
205
+ borderRadius: "16px",
206
+ borderBottomRightRadius: msg.role === "user" ? "4px" : "16px",
207
+ borderTopLeftRadius: msg.role !== "user" ? "4px" : "16px",
208
+ maxWidth: "85%",
209
+ fontSize: "14.5px",
210
+ lineHeight: "1.5",
211
+ boxShadow: "0 1px 2px rgba(0,0,0,0.05)",
212
+ border: msg.role !== "user" ? "1px solid #e2e8f0" : "none"
213
+ }, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_markdown.default, { remarkPlugins: [import_remark_gfm.default], children: msg.content }) }, i)),
214
+ isLoading && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { alignSelf: "flex-start", backgroundColor: "#ffffff", color: "#64748b", padding: "12px 16px", borderRadius: "16px", fontSize: "14.5px", border: "1px solid #e2e8f0" }, children: "Typing..." }),
215
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { ref: messagesEndRef })
216
+ ] }),
217
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { padding: "16px", borderTop: "1px solid #e5e7eb", display: "flex", gap: "8px", backgroundColor: "white" }, children: [
218
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
219
+ "input",
220
+ {
221
+ value: input,
222
+ onChange: (e) => setInput(e.target.value),
223
+ onKeyDown: (e) => e.key === "Enter" && handleSend(),
224
+ placeholder: "Type a message...",
225
+ style: { flex: 1, padding: "10px 12px", borderRadius: "8px", border: "1px solid #cbd5e1", outline: "none", fontSize: "14px" }
226
+ }
227
+ ),
228
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
229
+ "button",
230
+ {
231
+ onClick: handleSend,
232
+ disabled: isLoading,
233
+ style: { backgroundColor: primaryColor, color: "white", border: "none", padding: "10px 16px", borderRadius: "8px", cursor: "pointer", fontWeight: "500", opacity: isLoading ? 0.7 : 1 },
234
+ children: "Send"
235
+ }
236
+ )
237
+ ] })
238
+ ] }),
239
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
240
+ "button",
241
+ {
242
+ onClick: () => setIsOpen(!isOpen),
243
+ style: {
244
+ width: "56px",
245
+ height: "56px",
246
+ borderRadius: "28px",
247
+ backgroundColor: primaryColor,
248
+ color: "white",
249
+ border: "none",
250
+ boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
251
+ cursor: "pointer",
252
+ display: "flex",
253
+ alignItems: "center",
254
+ justifyContent: "center",
255
+ fontSize: "24px",
256
+ float: "right"
257
+ },
258
+ children: isOpen ? "\u2715" : "\u{1F4AC}"
259
+ }
260
+ )
261
+ ] });
262
+ };
263
+ // Annotate the CommonJS export names for ESM import in node:
264
+ 0 && (module.exports = {
265
+ ChatWidget,
266
+ EchoProvider,
267
+ useEcho
268
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,230 @@
1
+ "use client";
2
+ "use client";
3
+
4
+ // src/EchoProvider.tsx
5
+ import { createContext, useContext } from "react";
6
+ import { jsx } from "react/jsx-runtime";
7
+ var EchoContext = createContext(void 0);
8
+ var EchoProvider = ({ apiKey, children }) => {
9
+ return /* @__PURE__ */ jsx(EchoContext.Provider, { value: { apiKey }, children });
10
+ };
11
+ var useEcho = () => {
12
+ const context = useContext(EchoContext);
13
+ if (!context) {
14
+ throw new Error("useEcho must be used within an EchoProvider");
15
+ }
16
+ return context;
17
+ };
18
+
19
+ // src/ChatWidget.tsx
20
+ import { useState, useEffect, useRef } from "react";
21
+ import { io } from "socket.io-client";
22
+ import ReactMarkdown from "react-markdown";
23
+ import remarkGfm from "remark-gfm";
24
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
25
+ var css = `
26
+ .echo-markdown p { margin-bottom: 0.5rem; }
27
+ .echo-markdown p:last-child { margin-bottom: 0; }
28
+ .echo-markdown ul { list-style-type: disc; padding-left: 1.25rem; margin-bottom: 0.5rem; }
29
+ .echo-markdown ol { list-style-type: decimal; padding-left: 1.25rem; margin-bottom: 0.5rem; }
30
+ .echo-markdown li { margin-bottom: 0.25rem; }
31
+ .echo-markdown strong { font-weight: 600; }
32
+ .echo-markdown code { background: rgba(0,0,0,0.05); padding: 0.125rem 0.25rem; border-radius: 0.25rem; font-family: monospace; font-size: 0.875em; }
33
+ .echo-markdown pre { background: rgba(0,0,0,0.05); padding: 0.5rem; border-radius: 0.25rem; overflow-x: auto; font-family: monospace; font-size: 0.875em; margin-bottom: 0.5rem; }
34
+ .echo-markdown a { color: inherit; text-decoration: underline; }
35
+ `;
36
+ var ChatWidget = ({
37
+ apiUrl = "http://localhost:3002",
38
+ initialMessage = "Hello! How can I help you today?",
39
+ title = "Chat Support",
40
+ primaryColor = "#2563eb"
41
+ }) => {
42
+ const { apiKey } = useEcho();
43
+ const [isOpen, setIsOpen] = useState(false);
44
+ const [messages, setMessages] = useState([
45
+ { role: "assistant", content: initialMessage }
46
+ ]);
47
+ const [input, setInput] = useState("");
48
+ const [sessionId, setSessionId] = useState(() => Math.random().toString(36).substring(7));
49
+ const [isLoading, setIsLoading] = useState(false);
50
+ const messagesEndRef = useRef(null);
51
+ const [ticketId, setTicketId] = useState(null);
52
+ const [isLiveChat, setIsLiveChat] = useState(false);
53
+ const socketRef = useRef(null);
54
+ const API_URL = apiUrl;
55
+ const CONVEX_URL = `${API_URL}/chat`;
56
+ useEffect(() => {
57
+ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
58
+ }, [messages]);
59
+ useEffect(() => {
60
+ return () => {
61
+ if (socketRef.current) {
62
+ socketRef.current.disconnect();
63
+ }
64
+ };
65
+ }, []);
66
+ const connectSocket = (newTicketId, currentSessionId) => {
67
+ if (socketRef.current) return;
68
+ const socket = io(API_URL, {
69
+ auth: {
70
+ apiKey
71
+ }
72
+ });
73
+ socket.on("connect", () => {
74
+ console.log("Widget connected for live chat");
75
+ socket.emit("join-ticket", { ticketId: newTicketId, sessionId: currentSessionId });
76
+ setIsLiveChat(true);
77
+ });
78
+ socket.on("new-message", (data) => {
79
+ if (data.role === "agent") {
80
+ setMessages((prev) => [...prev, { role: "agent", content: data.content }]);
81
+ }
82
+ });
83
+ socket.on("user-joined", (data) => {
84
+ if (data.role === "admin" || data.role === "agent") {
85
+ setMessages((prev) => [...prev, { role: "assistant", content: `\u{1F7E2} An agent (${data.email}) has joined the chat.` }]);
86
+ }
87
+ });
88
+ socket.on("ticket-resolved", () => {
89
+ setIsLiveChat(false);
90
+ setMessages((prev) => [...prev, { role: "assistant", content: "\u2705 This ticket has been resolved. The live chat session has ended." }]);
91
+ socket.disconnect();
92
+ socketRef.current = null;
93
+ });
94
+ socketRef.current = socket;
95
+ };
96
+ const handleSend = async () => {
97
+ if (!input.trim() || isLoading) return;
98
+ const userMsg = input.trim();
99
+ setMessages((prev) => [...prev, { role: "user", content: userMsg }]);
100
+ setInput("");
101
+ if (isLiveChat && ticketId && socketRef.current?.connected) {
102
+ socketRef.current.emit("user-message", {
103
+ ticketId,
104
+ message: userMsg
105
+ });
106
+ return;
107
+ }
108
+ setIsLoading(true);
109
+ try {
110
+ const response = await fetch(CONVEX_URL, {
111
+ method: "POST",
112
+ headers: {
113
+ "Content-Type": "application/json",
114
+ "x-api-key": apiKey,
115
+ "x-session-id": sessionId
116
+ },
117
+ body: JSON.stringify({
118
+ message: userMsg
119
+ })
120
+ });
121
+ if (!response.ok) {
122
+ throw new Error("Failed to send message.");
123
+ }
124
+ const data = await response.json();
125
+ if (data.sessionId) setSessionId(data.sessionId);
126
+ setMessages((prev) => [...prev, {
127
+ role: "assistant",
128
+ content: data.response || "No response."
129
+ }]);
130
+ if (data.ticketCreated && data.ticketId) {
131
+ setTicketId(data.ticketId);
132
+ setMessages((prev) => [...prev, {
133
+ role: "assistant",
134
+ content: "\u26A0\uFE0F A support ticket has been created for this query. An admin will be notified and may join this chat."
135
+ }]);
136
+ connectSocket(data.ticketId, data.sessionId || sessionId);
137
+ }
138
+ } catch (err) {
139
+ setMessages((prev) => [...prev, {
140
+ role: "assistant",
141
+ content: `Error: ${err.message}`
142
+ }]);
143
+ } finally {
144
+ setIsLoading(false);
145
+ }
146
+ };
147
+ return /* @__PURE__ */ jsxs("div", { style: { position: "fixed", bottom: "20px", right: "20px", zIndex: 9999, fontFamily: "sans-serif" }, children: [
148
+ /* @__PURE__ */ jsx2("style", { children: css }),
149
+ isOpen && /* @__PURE__ */ jsxs("div", { style: {
150
+ width: "380px",
151
+ height: "600px",
152
+ backgroundColor: "white",
153
+ borderRadius: "12px",
154
+ boxShadow: "0 8px 24px rgba(0,0,0,0.15)",
155
+ display: "flex",
156
+ flexDirection: "column",
157
+ overflow: "hidden",
158
+ marginBottom: "16px",
159
+ border: "1px solid #e5e7eb"
160
+ }, children: [
161
+ /* @__PURE__ */ jsx2("div", { style: { backgroundColor: primaryColor, color: "white", padding: "16px", fontWeight: "bold", fontSize: "16px" }, children: title }),
162
+ /* @__PURE__ */ jsxs("div", { style: { flex: 1, overflowY: "auto", padding: "16px", display: "flex", flexDirection: "column", gap: "12px", backgroundColor: "#f8fafc" }, children: [
163
+ messages.map((msg, i) => /* @__PURE__ */ jsx2("div", { className: "echo-markdown", style: {
164
+ alignSelf: msg.role === "user" ? "flex-end" : "flex-start",
165
+ backgroundColor: msg.role === "user" ? primaryColor : "#ffffff",
166
+ color: msg.role === "user" ? "white" : "#1e293b",
167
+ padding: "12px 16px",
168
+ borderRadius: "16px",
169
+ borderBottomRightRadius: msg.role === "user" ? "4px" : "16px",
170
+ borderTopLeftRadius: msg.role !== "user" ? "4px" : "16px",
171
+ maxWidth: "85%",
172
+ fontSize: "14.5px",
173
+ lineHeight: "1.5",
174
+ boxShadow: "0 1px 2px rgba(0,0,0,0.05)",
175
+ border: msg.role !== "user" ? "1px solid #e2e8f0" : "none"
176
+ }, children: /* @__PURE__ */ jsx2(ReactMarkdown, { remarkPlugins: [remarkGfm], children: msg.content }) }, i)),
177
+ isLoading && /* @__PURE__ */ jsx2("div", { style: { alignSelf: "flex-start", backgroundColor: "#ffffff", color: "#64748b", padding: "12px 16px", borderRadius: "16px", fontSize: "14.5px", border: "1px solid #e2e8f0" }, children: "Typing..." }),
178
+ /* @__PURE__ */ jsx2("div", { ref: messagesEndRef })
179
+ ] }),
180
+ /* @__PURE__ */ jsxs("div", { style: { padding: "16px", borderTop: "1px solid #e5e7eb", display: "flex", gap: "8px", backgroundColor: "white" }, children: [
181
+ /* @__PURE__ */ jsx2(
182
+ "input",
183
+ {
184
+ value: input,
185
+ onChange: (e) => setInput(e.target.value),
186
+ onKeyDown: (e) => e.key === "Enter" && handleSend(),
187
+ placeholder: "Type a message...",
188
+ style: { flex: 1, padding: "10px 12px", borderRadius: "8px", border: "1px solid #cbd5e1", outline: "none", fontSize: "14px" }
189
+ }
190
+ ),
191
+ /* @__PURE__ */ jsx2(
192
+ "button",
193
+ {
194
+ onClick: handleSend,
195
+ disabled: isLoading,
196
+ style: { backgroundColor: primaryColor, color: "white", border: "none", padding: "10px 16px", borderRadius: "8px", cursor: "pointer", fontWeight: "500", opacity: isLoading ? 0.7 : 1 },
197
+ children: "Send"
198
+ }
199
+ )
200
+ ] })
201
+ ] }),
202
+ /* @__PURE__ */ jsx2(
203
+ "button",
204
+ {
205
+ onClick: () => setIsOpen(!isOpen),
206
+ style: {
207
+ width: "56px",
208
+ height: "56px",
209
+ borderRadius: "28px",
210
+ backgroundColor: primaryColor,
211
+ color: "white",
212
+ border: "none",
213
+ boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
214
+ cursor: "pointer",
215
+ display: "flex",
216
+ alignItems: "center",
217
+ justifyContent: "center",
218
+ fontSize: "24px",
219
+ float: "right"
220
+ },
221
+ children: isOpen ? "\u2715" : "\u{1F4AC}"
222
+ }
223
+ )
224
+ ] });
225
+ };
226
+ export {
227
+ ChatWidget,
228
+ EchoProvider,
229
+ useEcho
230
+ };
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@shashankrai/echo-chat-react-widget",
3
+ "version": "1.0.0",
4
+ "description": "A drop-in React chat widget for Echo - an embeddable AI chat assistant for your website",
5
+ "author": "Shashank Rai",
6
+ "license": "MIT",
7
+ "keywords": [
8
+ "react",
9
+ "chat",
10
+ "widget",
11
+ "echo",
12
+ "ai",
13
+ "chatbot",
14
+ "customer-support",
15
+ "embeddable"
16
+ ],
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/shashankrai/echo.git",
20
+ "directory": "packages/widget-react"
21
+ },
22
+ "bugs": {
23
+ "url": "https://github.com/shashankrai/echo/issues"
24
+ },
25
+ "homepage": "https://github.com/shashankrai/echo#readme",
26
+ "main": "dist/index.js",
27
+ "module": "dist/index.mjs",
28
+ "types": "dist/index.d.ts",
29
+ "exports": {
30
+ ".": {
31
+ "types": "./dist/index.d.ts",
32
+ "import": "./dist/index.mjs",
33
+ "require": "./dist/index.js"
34
+ }
35
+ },
36
+ "files": [
37
+ "dist",
38
+ "README.md"
39
+ ],
40
+ "publishConfig": {
41
+ "access": "public",
42
+ "registry": "https://registry.npmjs.org/"
43
+ },
44
+ "scripts": {
45
+ "build": "tsup",
46
+ "dev": "tsup --watch",
47
+ "clean": "rm -rf dist",
48
+ "prepublishOnly": "npm run build"
49
+ },
50
+ "peerDependencies": {
51
+ "react": "^18.0.0 || ^19.0.0",
52
+ "react-dom": "^18.0.0 || ^19.0.0"
53
+ },
54
+ "devDependencies": {
55
+ "@types/react": "^18.2.0",
56
+ "@types/react-dom": "^18.2.0",
57
+ "tsup": "^8.0.2",
58
+ "typescript": "^5.3.3"
59
+ },
60
+ "dependencies": {
61
+ "react-markdown": "^10.1.0",
62
+ "remark-gfm": "^4.0.1",
63
+ "socket.io-client": "^4.8.3"
64
+ }
65
+ }