@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 +21 -0
- package/README.md +82 -0
- package/dist/index.d.mts +21 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +268 -0
- package/dist/index.mjs +230 -0
- package/package.json +65 -0
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).
|
package/dist/index.d.mts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
+
}
|