@waysdrop/chat 1.0.3 → 1.0.5
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 +23 -8
- package/dist/index.cjs +102 -80
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +102 -80
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -43,10 +43,12 @@ Mount `<ChatWidget />` once at the root of your app. It renders a floating butto
|
|
|
43
43
|
|
|
44
44
|
```ts
|
|
45
45
|
type ChatConfig = {
|
|
46
|
-
serverUrl: string
|
|
47
|
-
apiUrl: string
|
|
48
|
-
token?: string
|
|
49
|
-
visitorId?: string
|
|
46
|
+
serverUrl: string // Socket.IO server URL
|
|
47
|
+
apiUrl: string // REST base URL (used for file uploads)
|
|
48
|
+
token?: string // JWT for authenticated users — omit for visitor flow
|
|
49
|
+
visitorId?: string // Pass a returning visitor's ID to restore chat history
|
|
50
|
+
theme?: 'light' | 'dark' | 'system' // defaults to 'system'
|
|
51
|
+
primaryColor?: string // any valid CSS color — defaults to Waysdrop blue
|
|
50
52
|
}
|
|
51
53
|
```
|
|
52
54
|
|
|
@@ -60,7 +62,20 @@ saveVisitorId(id) // writes to localStorage
|
|
|
60
62
|
clearVisitorId() // clears — use on logout
|
|
61
63
|
```
|
|
62
64
|
|
|
63
|
-
**Authenticated flow** — pass a `token` (JWT). The socket server resolves the user from it. No `visitorId` needed.
|
|
65
|
+
**Authenticated flow** — pass a `token` (JWT). The socket server resolves the user from it. No `visitorId` needed. If the token changes at runtime (user logs in after mount), the widget automatically destroys the old socket connection and reconnects with the new token.
|
|
66
|
+
|
|
67
|
+
**Theming** — `theme` controls the color scheme. `system` follows the OS `prefers-color-scheme`. `light` and `dark` force it regardless of the OS setting. `primaryColor` accepts any valid CSS color value:
|
|
68
|
+
|
|
69
|
+
```tsx
|
|
70
|
+
<ChatWidget
|
|
71
|
+
config={{
|
|
72
|
+
serverUrl: '...',
|
|
73
|
+
apiUrl: '...',
|
|
74
|
+
theme: 'dark',
|
|
75
|
+
primaryColor: '#7c3aed',
|
|
76
|
+
}}
|
|
77
|
+
/>
|
|
78
|
+
```
|
|
64
79
|
|
|
65
80
|
---
|
|
66
81
|
|
|
@@ -81,11 +96,11 @@ const {
|
|
|
81
96
|
visitorInfo, // VisitorInfo | null
|
|
82
97
|
setVisitorInfo,
|
|
83
98
|
sendMessage, // (content: string, info?: VisitorInfo) => void
|
|
84
|
-
sendFile, // (file: File, info?: VisitorInfo) => Promise<void>
|
|
99
|
+
sendFile, // (file: File, content?: string, info?: VisitorInfo) => Promise<void>
|
|
85
100
|
} = useChat(config)
|
|
86
101
|
```
|
|
87
102
|
|
|
88
|
-
`sendMessage` and `sendFile` accept an optional `VisitorInfo` argument for the first message in a visitor session
|
|
103
|
+
`sendMessage` and `sendFile` accept an optional `VisitorInfo` argument for the first message in a visitor session. `sendFile` also accepts an optional `content` string to send text alongside the file in a single message.
|
|
89
104
|
|
|
90
105
|
---
|
|
91
106
|
|
|
@@ -104,7 +119,7 @@ type ChatMessage = {
|
|
|
104
119
|
}
|
|
105
120
|
|
|
106
121
|
type VisitorInfo = {
|
|
107
|
-
email: string
|
|
122
|
+
email: string
|
|
108
123
|
name?: string
|
|
109
124
|
phone?: string
|
|
110
125
|
}
|
package/dist/index.cjs
CHANGED
|
@@ -63,12 +63,15 @@ var import_css8 = require("@emotion/css");
|
|
|
63
63
|
// src/lib/upload.ts
|
|
64
64
|
var VISITOR_ID_KEY = "waysdrop_visitor_id";
|
|
65
65
|
var saveVisitorId = (id) => {
|
|
66
|
+
if (typeof window === "undefined") return;
|
|
66
67
|
localStorage.setItem(VISITOR_ID_KEY, id);
|
|
67
68
|
};
|
|
68
69
|
var loadVisitorId = () => {
|
|
70
|
+
if (typeof window === "undefined") return null;
|
|
69
71
|
return localStorage.getItem(VISITOR_ID_KEY);
|
|
70
72
|
};
|
|
71
73
|
var clearVisitorId = () => {
|
|
74
|
+
if (typeof window === "undefined") return;
|
|
72
75
|
localStorage.removeItem(VISITOR_ID_KEY);
|
|
73
76
|
};
|
|
74
77
|
var uploadFile = async (file, config) => {
|
|
@@ -102,8 +105,11 @@ var import_react = require("react");
|
|
|
102
105
|
// src/lib/socket.ts
|
|
103
106
|
var import_socket = require("socket.io-client");
|
|
104
107
|
var socket = null;
|
|
105
|
-
var
|
|
106
|
-
if (socket)
|
|
108
|
+
var createSocket = (config) => {
|
|
109
|
+
if (socket) {
|
|
110
|
+
socket.disconnect();
|
|
111
|
+
socket = null;
|
|
112
|
+
}
|
|
107
113
|
const { serverUrl, token, visitorId } = config;
|
|
108
114
|
socket = (0, import_socket.io)(serverUrl, {
|
|
109
115
|
transports: ["websocket", "polling"],
|
|
@@ -112,6 +118,10 @@ var getSocket = (config) => {
|
|
|
112
118
|
});
|
|
113
119
|
return socket;
|
|
114
120
|
};
|
|
121
|
+
var getSocket = (config) => {
|
|
122
|
+
if (socket) return socket;
|
|
123
|
+
return createSocket(config);
|
|
124
|
+
};
|
|
115
125
|
var destroySocket = () => {
|
|
116
126
|
if (socket) {
|
|
117
127
|
socket.disconnect();
|
|
@@ -163,7 +173,7 @@ var useChat = (config) => {
|
|
|
163
173
|
reset
|
|
164
174
|
} = useChatStore();
|
|
165
175
|
(0, import_react.useEffect)(() => {
|
|
166
|
-
const socket2 =
|
|
176
|
+
const socket2 = createSocket(config);
|
|
167
177
|
setStatus("connecting");
|
|
168
178
|
socket2.connect();
|
|
169
179
|
socket2.on("connected", (payload) => {
|
|
@@ -190,7 +200,7 @@ var useChat = (config) => {
|
|
|
190
200
|
destroySocket();
|
|
191
201
|
reset();
|
|
192
202
|
};
|
|
193
|
-
}, []);
|
|
203
|
+
}, [config.token]);
|
|
194
204
|
const sendMessage = (0, import_react.useCallback)(
|
|
195
205
|
(content, info) => {
|
|
196
206
|
var _a, _b;
|
|
@@ -243,7 +253,7 @@ var useChat = (config) => {
|
|
|
243
253
|
var import_react2 = require("react");
|
|
244
254
|
var import_css = require("@emotion/css");
|
|
245
255
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
246
|
-
var BUTTON_SIZE =
|
|
256
|
+
var BUTTON_SIZE = 58;
|
|
247
257
|
var FloatingButton = ({ isOpen, onToggle }) => {
|
|
248
258
|
const [isMobile, setIsMobile] = (0, import_react2.useState)(false);
|
|
249
259
|
(0, import_react2.useEffect)(() => {
|
|
@@ -253,49 +263,17 @@ var FloatingButton = ({ isOpen, onToggle }) => {
|
|
|
253
263
|
return () => window.removeEventListener("resize", check);
|
|
254
264
|
}, []);
|
|
255
265
|
if (isMobile && isOpen) {
|
|
256
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
257
|
-
"
|
|
258
|
-
{
|
|
259
|
-
|
|
260
|
-
onClick: onToggle,
|
|
261
|
-
type: "button",
|
|
262
|
-
children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
263
|
-
"svg",
|
|
264
|
-
{
|
|
265
|
-
width: "22",
|
|
266
|
-
height: "22",
|
|
267
|
-
viewBox: "0 0 24 24",
|
|
268
|
-
fill: "none",
|
|
269
|
-
stroke: "currentColor",
|
|
270
|
-
strokeWidth: "2.5",
|
|
271
|
-
strokeLinecap: "round",
|
|
272
|
-
children: [
|
|
273
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
|
|
274
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
|
|
275
|
-
]
|
|
276
|
-
}
|
|
277
|
-
)
|
|
278
|
-
}
|
|
279
|
-
);
|
|
266
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: styles.mobileCloseBtn, onClick: onToggle, type: "button", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", stroke: "#fff", strokeWidth: "2.5", strokeLinecap: "round", children: [
|
|
267
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
|
|
268
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
|
|
269
|
+
] }) });
|
|
280
270
|
}
|
|
281
271
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", { className: styles.btn, onClick: onToggle, type: "button", children: [
|
|
282
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: styles.icon(!isOpen), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "
|
|
283
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: styles.icon(isOpen), children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
284
|
-
"
|
|
285
|
-
{
|
|
286
|
-
|
|
287
|
-
height: "20",
|
|
288
|
-
viewBox: "0 0 24 24",
|
|
289
|
-
fill: "none",
|
|
290
|
-
stroke: "currentColor",
|
|
291
|
-
strokeWidth: "2.5",
|
|
292
|
-
strokeLinecap: "round",
|
|
293
|
-
children: [
|
|
294
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
|
|
295
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
|
|
296
|
-
]
|
|
297
|
-
}
|
|
298
|
-
) })
|
|
272
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: styles.icon(!isOpen), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "#fff", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M20 2H4a2 2 0 0 0-2 2v18l4-4h14a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2z" }) }) }),
|
|
273
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: styles.icon(isOpen), children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "#fff", strokeWidth: "2.5", strokeLinecap: "round", children: [
|
|
274
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
|
|
275
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
|
|
276
|
+
] }) })
|
|
299
277
|
] });
|
|
300
278
|
};
|
|
301
279
|
var styles = {
|
|
@@ -306,8 +284,7 @@ var styles = {
|
|
|
306
284
|
width: ${BUTTON_SIZE}px;
|
|
307
285
|
height: ${BUTTON_SIZE}px;
|
|
308
286
|
border-radius: 50%;
|
|
309
|
-
background: var(--wds-primary);
|
|
310
|
-
color: #fff;
|
|
287
|
+
background: var(--wds-primary, oklch(0.5811 0.2268 259.15));
|
|
311
288
|
border: none;
|
|
312
289
|
cursor: pointer;
|
|
313
290
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.18);
|
|
@@ -328,8 +305,7 @@ var styles = {
|
|
|
328
305
|
width: ${BUTTON_SIZE}px;
|
|
329
306
|
height: ${BUTTON_SIZE}px;
|
|
330
307
|
border-radius: 50%;
|
|
331
|
-
background: var(--wds-primary);
|
|
332
|
-
color: #fff;
|
|
308
|
+
background: var(--wds-primary, oklch(0.5811 0.2268 259.15));
|
|
333
309
|
border: none;
|
|
334
310
|
cursor: pointer;
|
|
335
311
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25);
|
|
@@ -343,9 +319,7 @@ var styles = {
|
|
|
343
319
|
display: flex;
|
|
344
320
|
align-items: center;
|
|
345
321
|
justify-content: center;
|
|
346
|
-
transition:
|
|
347
|
-
opacity 0.2s,
|
|
348
|
-
transform 0.2s;
|
|
322
|
+
transition: opacity 0.2s, transform 0.2s;
|
|
349
323
|
opacity: ${visible ? 1 : 0};
|
|
350
324
|
transform: ${visible ? "scale(1)" : "scale(0.6) rotate(30deg)"};
|
|
351
325
|
pointer-events: ${visible ? "auto" : "none"};
|
|
@@ -1071,7 +1045,6 @@ var styles5 = {
|
|
|
1071
1045
|
margin: 0;
|
|
1072
1046
|
text-align: center;
|
|
1073
1047
|
opacity: 0.7;
|
|
1074
|
-
text-align: center;
|
|
1075
1048
|
`
|
|
1076
1049
|
};
|
|
1077
1050
|
|
|
@@ -1092,7 +1065,7 @@ var ChatScreen = ({
|
|
|
1092
1065
|
const connected = status === "connected";
|
|
1093
1066
|
const [showScrollBtn, setShowScrollBtn] = (0, import_react4.useState)(false);
|
|
1094
1067
|
const lastMsg = messages[messages.length - 1];
|
|
1095
|
-
const isThinking = connected && (lastMsg == null ? void 0 : lastMsg.direction) === "
|
|
1068
|
+
const isThinking = connected && (lastMsg == null ? void 0 : lastMsg.direction) === "OUTBOUND";
|
|
1096
1069
|
(0, import_react4.useEffect)(() => {
|
|
1097
1070
|
var _a;
|
|
1098
1071
|
(_a = bottomRef.current) == null ? void 0 : _a.scrollIntoView({ behavior: "smooth" });
|
|
@@ -1356,16 +1329,74 @@ var styles6 = {
|
|
|
1356
1329
|
|
|
1357
1330
|
// src/components/ChatWidget.tsx
|
|
1358
1331
|
var import_jsx_runtime8 = require("react/jsx-runtime");
|
|
1332
|
+
var LIGHT_VARS = `
|
|
1333
|
+
--wds-bg: oklch(1 0 0);
|
|
1334
|
+
--wds-fg: oklch(0.145 0 0);
|
|
1335
|
+
--wds-muted: oklch(0.556 0 0);
|
|
1336
|
+
--wds-muted-bg: oklch(0.97 0 0);
|
|
1337
|
+
--wds-border: oklch(0.922 0 0);
|
|
1338
|
+
`;
|
|
1339
|
+
var DARK_VARS = `
|
|
1340
|
+
--wds-bg: oklch(0.145 0 0);
|
|
1341
|
+
--wds-fg: oklch(0.985 0 0);
|
|
1342
|
+
--wds-muted: oklch(0.708 0 0);
|
|
1343
|
+
--wds-muted-bg: oklch(0.205 0 0);
|
|
1344
|
+
--wds-border: oklch(1 0 0 / 10%);
|
|
1345
|
+
`;
|
|
1346
|
+
function buildPrimaryVars(color) {
|
|
1347
|
+
return `
|
|
1348
|
+
--wds-primary: ${color};
|
|
1349
|
+
--wds-primary-soft: color-mix(in oklch, ${color} 12%, transparent);
|
|
1350
|
+
--wds-primary-border: color-mix(in oklch, ${color} 40%, transparent);
|
|
1351
|
+
`;
|
|
1352
|
+
}
|
|
1353
|
+
var DEFAULT_PRIMARY = "oklch(0.5811 0.2268 259.15)";
|
|
1354
|
+
function buildThemeStyle(theme, primaryColor) {
|
|
1355
|
+
const primary = buildPrimaryVars(primaryColor);
|
|
1356
|
+
if (theme === "light") {
|
|
1357
|
+
return `
|
|
1358
|
+
[data-wds-root] {
|
|
1359
|
+
${LIGHT_VARS}
|
|
1360
|
+
${primary}
|
|
1361
|
+
}
|
|
1362
|
+
`;
|
|
1363
|
+
}
|
|
1364
|
+
if (theme === "dark") {
|
|
1365
|
+
return `
|
|
1366
|
+
[data-wds-root] {
|
|
1367
|
+
${DARK_VARS}
|
|
1368
|
+
${primary}
|
|
1369
|
+
}
|
|
1370
|
+
`;
|
|
1371
|
+
}
|
|
1372
|
+
return `
|
|
1373
|
+
[data-wds-root] {
|
|
1374
|
+
${LIGHT_VARS}
|
|
1375
|
+
${primary}
|
|
1376
|
+
}
|
|
1377
|
+
@media (prefers-color-scheme: dark) {
|
|
1378
|
+
[data-wds-root] {
|
|
1379
|
+
${DARK_VARS}
|
|
1380
|
+
--wds-primary-soft: color-mix(in oklch, ${primaryColor} 15%, transparent);
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
`;
|
|
1384
|
+
}
|
|
1359
1385
|
var ChatWidget = ({ config }) => {
|
|
1360
|
-
var _a, _b;
|
|
1361
1386
|
const [isOpen, setIsOpen] = (0, import_react5.useState)(false);
|
|
1362
1387
|
const [isExpanded, setIsExpanded] = (0, import_react5.useState)(false);
|
|
1363
1388
|
const [view, setView] = (0, import_react5.useState)("home");
|
|
1389
|
+
const [visitorId] = (0, import_react5.useState)(
|
|
1390
|
+
() => {
|
|
1391
|
+
var _a, _b;
|
|
1392
|
+
return (_b = (_a = config.visitorId) != null ? _a : loadVisitorId()) != null ? _b : void 0;
|
|
1393
|
+
}
|
|
1394
|
+
);
|
|
1364
1395
|
const reset = useChatStore((s) => s.reset);
|
|
1365
1396
|
const resolvedConfig = __spreadProps(__spreadValues({}, config), {
|
|
1366
|
-
visitorId
|
|
1397
|
+
visitorId
|
|
1367
1398
|
});
|
|
1368
|
-
const hasHistory = !!
|
|
1399
|
+
const hasHistory = !!visitorId;
|
|
1369
1400
|
const { status, messages, error, sendMessage, sendFile } = useChat(resolvedConfig);
|
|
1370
1401
|
const handleNewConversation = () => {
|
|
1371
1402
|
reset();
|
|
@@ -1380,6 +1411,19 @@ var ChatWidget = ({ config }) => {
|
|
|
1380
1411
|
document.body.style.overflow = "";
|
|
1381
1412
|
};
|
|
1382
1413
|
}, [isOpen]);
|
|
1414
|
+
(0, import_react5.useEffect)(() => {
|
|
1415
|
+
var _a, _b;
|
|
1416
|
+
const theme = (_a = config.theme) != null ? _a : "system";
|
|
1417
|
+
const primaryColor = (_b = config.primaryColor) != null ? _b : DEFAULT_PRIMARY;
|
|
1418
|
+
const style = buildThemeStyle(theme, primaryColor);
|
|
1419
|
+
const el = document.createElement("style");
|
|
1420
|
+
el.setAttribute("data-wds-theme", "");
|
|
1421
|
+
el.textContent = style;
|
|
1422
|
+
document.head.appendChild(el);
|
|
1423
|
+
return () => {
|
|
1424
|
+
document.head.removeChild(el);
|
|
1425
|
+
};
|
|
1426
|
+
}, [config.theme, config.primaryColor]);
|
|
1383
1427
|
return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
|
|
1384
1428
|
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
1385
1429
|
FloatingButton,
|
|
@@ -1420,28 +1464,6 @@ var ChatWidget = ({ config }) => {
|
|
|
1420
1464
|
] });
|
|
1421
1465
|
};
|
|
1422
1466
|
import_css8.injectGlobal`
|
|
1423
|
-
:root {
|
|
1424
|
-
--wds-primary: oklch(0.5811 0.2268 259.15);
|
|
1425
|
-
--wds-primary-soft: oklch(0.5811 0.2268 259.15 / 0.12);
|
|
1426
|
-
--wds-primary-border: oklch(0.7695 0.1177 255.22 / 0.4);
|
|
1427
|
-
--wds-bg: oklch(1 0 0);
|
|
1428
|
-
--wds-fg: oklch(0.145 0 0);
|
|
1429
|
-
--wds-muted: oklch(0.556 0 0);
|
|
1430
|
-
--wds-muted-bg: oklch(0.97 0 0);
|
|
1431
|
-
--wds-border: oklch(0.922 0 0);
|
|
1432
|
-
}
|
|
1433
|
-
|
|
1434
|
-
@media (prefers-color-scheme: dark) {
|
|
1435
|
-
:root {
|
|
1436
|
-
--wds-bg: oklch(0.145 0 0);
|
|
1437
|
-
--wds-fg: oklch(0.985 0 0);
|
|
1438
|
-
--wds-muted: oklch(0.708 0 0);
|
|
1439
|
-
--wds-muted-bg: oklch(0.205 0 0);
|
|
1440
|
-
--wds-border: oklch(1 0 0 / 10%);
|
|
1441
|
-
--wds-primary-soft: oklch(0.5811 0.2268 259.15 / 0.15);
|
|
1442
|
-
}
|
|
1443
|
-
}
|
|
1444
|
-
|
|
1445
1467
|
[data-wds-root] * {
|
|
1446
1468
|
box-sizing: border-box;
|
|
1447
1469
|
text-align: left;
|