@waysdrop/chat 1.0.1 → 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/dist/index.js CHANGED
@@ -25,12 +25,15 @@ import { injectGlobal } from "@emotion/css";
25
25
  // src/lib/upload.ts
26
26
  var VISITOR_ID_KEY = "waysdrop_visitor_id";
27
27
  var saveVisitorId = (id) => {
28
+ if (typeof window === "undefined") return;
28
29
  localStorage.setItem(VISITOR_ID_KEY, id);
29
30
  };
30
31
  var loadVisitorId = () => {
32
+ if (typeof window === "undefined") return null;
31
33
  return localStorage.getItem(VISITOR_ID_KEY);
32
34
  };
33
35
  var clearVisitorId = () => {
36
+ if (typeof window === "undefined") return;
34
37
  localStorage.removeItem(VISITOR_ID_KEY);
35
38
  };
36
39
  var uploadFile = async (file, config) => {
@@ -64,8 +67,11 @@ import { useEffect, useCallback } from "react";
64
67
  // src/lib/socket.ts
65
68
  import { io } from "socket.io-client";
66
69
  var socket = null;
67
- var getSocket = (config) => {
68
- if (socket) return socket;
70
+ var createSocket = (config) => {
71
+ if (socket) {
72
+ socket.disconnect();
73
+ socket = null;
74
+ }
69
75
  const { serverUrl, token, visitorId } = config;
70
76
  socket = io(serverUrl, {
71
77
  transports: ["websocket", "polling"],
@@ -74,6 +80,10 @@ var getSocket = (config) => {
74
80
  });
75
81
  return socket;
76
82
  };
83
+ var getSocket = (config) => {
84
+ if (socket) return socket;
85
+ return createSocket(config);
86
+ };
77
87
  var destroySocket = () => {
78
88
  if (socket) {
79
89
  socket.disconnect();
@@ -125,7 +135,7 @@ var useChat = (config) => {
125
135
  reset
126
136
  } = useChatStore();
127
137
  useEffect(() => {
128
- const socket2 = getSocket(config);
138
+ const socket2 = createSocket(config);
129
139
  setStatus("connecting");
130
140
  socket2.connect();
131
141
  socket2.on("connected", (payload) => {
@@ -152,7 +162,7 @@ var useChat = (config) => {
152
162
  destroySocket();
153
163
  reset();
154
164
  };
155
- }, []);
165
+ }, [config.token]);
156
166
  const sendMessage = useCallback(
157
167
  (content, info) => {
158
168
  var _a, _b;
@@ -170,14 +180,15 @@ var useChat = (config) => {
170
180
  [config, visitorInfo]
171
181
  );
172
182
  const sendFile = useCallback(
173
- async (file, info) => {
183
+ async (file, content, info) => {
174
184
  var _a, _b;
175
185
  const url = await uploadFile(file, config);
176
186
  const socket2 = getSocket(config);
177
- const dto = __spreadValues({
178
- file: url,
187
+ const dto = __spreadValues(__spreadProps(__spreadValues({
188
+ file: url
189
+ }, content ? { content } : {}), {
179
190
  externalId: `file-${Date.now()}`
180
- }, (info != null ? info : visitorInfo) ? {
191
+ }), (info != null ? info : visitorInfo) ? {
181
192
  email: (info != null ? info : visitorInfo).email,
182
193
  name: (_a = info != null ? info : visitorInfo) == null ? void 0 : _a.name,
183
194
  phone: (_b = info != null ? info : visitorInfo) == null ? void 0 : _b.phone
@@ -696,7 +707,7 @@ var MessageBubble = ({ message }) => {
696
707
  return /* @__PURE__ */ jsx5("div", { className: styles4.row(isCustomer), children: /* @__PURE__ */ jsxs3("div", { className: styles4.group(isCustomer), children: [
697
708
  isBot && /* @__PURE__ */ jsx5("span", { className: styles4.botLabel, children: "Ways AI" }),
698
709
  /* @__PURE__ */ jsxs3("div", { className: styles4.bubble(isCustomer), children: [
699
- message.file && /* @__PURE__ */ jsx5("a", { href: message.file, target: "_blank", rel: "noreferrer", className: styles4.fileLink, children: "\u{1F4CE} View attachment" }),
710
+ message.file && /* @__PURE__ */ jsx5("a", { href: message.file, target: "_blank", rel: "noreferrer", className: styles4.fileLink(!!message.content), children: "\u{1F4CE} View attachment" }),
700
711
  message.content && (isBot ? /* @__PURE__ */ jsx5(MarkdownRenderer, { content: message.content }) : /* @__PURE__ */ jsx5("p", { className: styles4.text, children: message.content }))
701
712
  ] }),
702
713
  /* @__PURE__ */ jsx5("span", { className: styles4.time, children: formatTime(message.createdAt) })
@@ -739,12 +750,13 @@ var styles4 = {
739
750
  white-space: pre-wrap;
740
751
  text-align: left;
741
752
  `,
742
- fileLink: css5`
753
+ fileLink: (hasContent) => css5`
743
754
  font-size: 0.875rem;
744
755
  color: inherit;
745
756
  text-decoration: underline;
746
757
  display: block;
747
758
  text-align: left;
759
+ ${hasContent ? "margin-bottom: 6px;" : ""}
748
760
  `,
749
761
  time: css5`
750
762
  font-size: 10px;
@@ -777,13 +789,13 @@ var ChatInput = ({ onSendText, onSendFile, disabled, isThinking }) => {
777
789
  if (!trimmed && !pendingFile || disabled || uploading) return;
778
790
  if (pendingFile) {
779
791
  setUploading(true);
780
- onSendFile(pendingFile).finally(() => {
792
+ onSendFile(pendingFile, trimmed || void 0).finally(() => {
781
793
  setUploading(false);
782
794
  setPendingFile(null);
783
795
  setPendingPreview(null);
796
+ setValue("");
784
797
  });
785
- }
786
- if (trimmed) {
798
+ } else if (trimmed) {
787
799
  onSendText(trimmed);
788
800
  setValue("");
789
801
  }
@@ -1064,35 +1076,102 @@ var ChatScreen = ({
1064
1076
  };
1065
1077
  return /* @__PURE__ */ jsxs5("div", { className: styles6.wrapper, children: [
1066
1078
  /* @__PURE__ */ jsxs5("div", { className: styles6.header, children: [
1067
- /* @__PURE__ */ jsx7("button", { className: styles6.iconBtn, onClick: onBack, children: /* @__PURE__ */ jsx7("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx7("polyline", { points: "15 18 9 12 15 6" }) }) }),
1079
+ /* @__PURE__ */ jsx7("button", { className: styles6.iconBtn, onClick: onBack, children: /* @__PURE__ */ jsx7(
1080
+ "svg",
1081
+ {
1082
+ width: "18",
1083
+ height: "18",
1084
+ viewBox: "0 0 24 24",
1085
+ fill: "none",
1086
+ stroke: "currentColor",
1087
+ strokeWidth: "2",
1088
+ strokeLinecap: "round",
1089
+ strokeLinejoin: "round",
1090
+ children: /* @__PURE__ */ jsx7("polyline", { points: "15 18 9 12 15 6" })
1091
+ }
1092
+ ) }),
1068
1093
  /* @__PURE__ */ jsxs5("div", { className: styles6.headerCenter, children: [
1069
1094
  /* @__PURE__ */ jsx7("span", { className: styles6.headerTitle, children: "Support" }),
1070
1095
  /* @__PURE__ */ jsx7("span", { className: styles6.statusDot(connected) }),
1071
1096
  /* @__PURE__ */ jsx7("span", { className: styles6.statusLabel, children: connected ? "Online" : "Connecting..." })
1072
1097
  ] }),
1073
- /* @__PURE__ */ jsx7("button", { className: styles6.expandBtn, onClick: onExpand, children: isExpanded ? /* @__PURE__ */ jsxs5("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
1074
- /* @__PURE__ */ jsx7("polyline", { points: "4 14 10 14 10 20" }),
1075
- /* @__PURE__ */ jsx7("polyline", { points: "20 10 14 10 14 4" }),
1076
- /* @__PURE__ */ jsx7("line", { x1: "10", y1: "14", x2: "3", y2: "21" }),
1077
- /* @__PURE__ */ jsx7("line", { x1: "21", y1: "3", x2: "14", y2: "10" })
1078
- ] }) : /* @__PURE__ */ jsxs5("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
1079
- /* @__PURE__ */ jsx7("polyline", { points: "15 3 21 3 21 9" }),
1080
- /* @__PURE__ */ jsx7("polyline", { points: "9 21 3 21 3 15" }),
1081
- /* @__PURE__ */ jsx7("line", { x1: "21", y1: "3", x2: "14", y2: "10" }),
1082
- /* @__PURE__ */ jsx7("line", { x1: "3", y1: "21", x2: "10", y2: "14" })
1083
- ] }) })
1098
+ /* @__PURE__ */ jsx7("button", { className: styles6.expandBtn, onClick: onExpand, children: isExpanded ? /* @__PURE__ */ jsxs5(
1099
+ "svg",
1100
+ {
1101
+ width: "16",
1102
+ height: "16",
1103
+ viewBox: "0 0 24 24",
1104
+ fill: "none",
1105
+ stroke: "currentColor",
1106
+ strokeWidth: "2",
1107
+ strokeLinecap: "round",
1108
+ strokeLinejoin: "round",
1109
+ children: [
1110
+ /* @__PURE__ */ jsx7("polyline", { points: "4 14 10 14 10 20" }),
1111
+ /* @__PURE__ */ jsx7("polyline", { points: "20 10 14 10 14 4" }),
1112
+ /* @__PURE__ */ jsx7("line", { x1: "10", y1: "14", x2: "3", y2: "21" }),
1113
+ /* @__PURE__ */ jsx7("line", { x1: "21", y1: "3", x2: "14", y2: "10" })
1114
+ ]
1115
+ }
1116
+ ) : /* @__PURE__ */ jsxs5(
1117
+ "svg",
1118
+ {
1119
+ width: "16",
1120
+ height: "16",
1121
+ viewBox: "0 0 24 24",
1122
+ fill: "none",
1123
+ stroke: "currentColor",
1124
+ strokeWidth: "2",
1125
+ strokeLinecap: "round",
1126
+ strokeLinejoin: "round",
1127
+ children: [
1128
+ /* @__PURE__ */ jsx7("polyline", { points: "15 3 21 3 21 9" }),
1129
+ /* @__PURE__ */ jsx7("polyline", { points: "9 21 3 21 3 15" }),
1130
+ /* @__PURE__ */ jsx7("line", { x1: "21", y1: "3", x2: "14", y2: "10" }),
1131
+ /* @__PURE__ */ jsx7("line", { x1: "3", y1: "21", x2: "10", y2: "14" })
1132
+ ]
1133
+ }
1134
+ ) })
1084
1135
  ] }),
1085
1136
  /* @__PURE__ */ jsxs5("div", { className: styles6.messagesWrap, children: [
1086
- /* @__PURE__ */ jsxs5("div", { className: styles6.messages, ref: scrollRef, onScroll: handleScroll, children: [
1087
- error && /* @__PURE__ */ jsx7("div", { className: styles6.errorBanner, children: error.message }),
1088
- messages.length === 0 && connected && /* @__PURE__ */ jsx7("div", { className: styles6.emptyState, children: /* @__PURE__ */ jsx7("p", { children: "Send a message to start the conversation." }) }),
1089
- messages.map((msg) => /* @__PURE__ */ jsx7(MessageBubble, { message: msg }, msg.id)),
1090
- /* @__PURE__ */ jsx7("div", { ref: bottomRef })
1091
- ] }),
1092
- showScrollBtn && /* @__PURE__ */ jsx7("button", { className: styles6.scrollBtn, onClick: () => {
1093
- var _a;
1094
- return (_a = bottomRef.current) == null ? void 0 : _a.scrollIntoView({ behavior: "smooth" });
1095
- }, children: /* @__PURE__ */ jsx7("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx7("polyline", { points: "6 9 12 15 18 9" }) }) })
1137
+ /* @__PURE__ */ jsxs5(
1138
+ "div",
1139
+ {
1140
+ className: styles6.messages,
1141
+ ref: scrollRef,
1142
+ onScroll: handleScroll,
1143
+ children: [
1144
+ error && /* @__PURE__ */ jsx7("div", { className: styles6.errorBanner, children: error.message }),
1145
+ messages.length === 0 && connected && /* @__PURE__ */ jsx7("div", { className: styles6.emptyState, children: /* @__PURE__ */ jsx7("p", { children: "Send a message to start the conversation." }) }),
1146
+ messages.map((msg) => /* @__PURE__ */ jsx7(MessageBubble, { message: msg }, msg.id)),
1147
+ /* @__PURE__ */ jsx7("div", { ref: bottomRef })
1148
+ ]
1149
+ }
1150
+ ),
1151
+ showScrollBtn && /* @__PURE__ */ jsx7(
1152
+ "button",
1153
+ {
1154
+ className: styles6.scrollBtn,
1155
+ onClick: () => {
1156
+ var _a;
1157
+ return (_a = bottomRef.current) == null ? void 0 : _a.scrollIntoView({ behavior: "smooth" });
1158
+ },
1159
+ children: /* @__PURE__ */ jsx7(
1160
+ "svg",
1161
+ {
1162
+ width: "14",
1163
+ height: "14",
1164
+ viewBox: "0 0 24 24",
1165
+ fill: "none",
1166
+ stroke: "currentColor",
1167
+ strokeWidth: "2.5",
1168
+ strokeLinecap: "round",
1169
+ strokeLinejoin: "round",
1170
+ children: /* @__PURE__ */ jsx7("polyline", { points: "6 9 12 15 18 9" })
1171
+ }
1172
+ )
1173
+ }
1174
+ )
1096
1175
  ] }),
1097
1176
  /* @__PURE__ */ jsx7(
1098
1177
  ChatInput,
@@ -1158,7 +1237,9 @@ var styles6 = {
1158
1237
  align-items: center;
1159
1238
  justify-content: center;
1160
1239
  flex-shrink: 0;
1161
- transition: color 0.15s, background 0.15s;
1240
+ transition:
1241
+ color 0.15s,
1242
+ background 0.15s;
1162
1243
  &:hover {
1163
1244
  color: var(--wds-fg);
1164
1245
  background: var(--wds-muted-bg);
@@ -1175,7 +1256,9 @@ var styles6 = {
1175
1256
  align-items: center;
1176
1257
  justify-content: center;
1177
1258
  flex-shrink: 0;
1178
- transition: color 0.15s, background 0.15s;
1259
+ transition:
1260
+ color 0.15s,
1261
+ background 0.15s;
1179
1262
  &:hover {
1180
1263
  color: var(--wds-fg);
1181
1264
  background: var(--wds-muted-bg);
@@ -1232,8 +1315,10 @@ var styles6 = {
1232
1315
  align-items: center;
1233
1316
  justify-content: center;
1234
1317
  cursor: pointer;
1235
- box-shadow: 0 2px 8px rgba(0,0,0,0.12);
1236
- transition: color 0.15s, border-color 0.15s;
1318
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
1319
+ transition:
1320
+ color 0.15s,
1321
+ border-color 0.15s;
1237
1322
  &:hover {
1238
1323
  color: var(--wds-primary);
1239
1324
  border-color: var(--wds-primary);
@@ -1243,16 +1328,74 @@ var styles6 = {
1243
1328
 
1244
1329
  // src/components/ChatWidget.tsx
1245
1330
  import { Fragment, jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
1331
+ var LIGHT_VARS = `
1332
+ --wds-bg: oklch(1 0 0);
1333
+ --wds-fg: oklch(0.145 0 0);
1334
+ --wds-muted: oklch(0.556 0 0);
1335
+ --wds-muted-bg: oklch(0.97 0 0);
1336
+ --wds-border: oklch(0.922 0 0);
1337
+ `;
1338
+ var DARK_VARS = `
1339
+ --wds-bg: oklch(0.145 0 0);
1340
+ --wds-fg: oklch(0.985 0 0);
1341
+ --wds-muted: oklch(0.708 0 0);
1342
+ --wds-muted-bg: oklch(0.205 0 0);
1343
+ --wds-border: oklch(1 0 0 / 10%);
1344
+ `;
1345
+ function buildPrimaryVars(color) {
1346
+ return `
1347
+ --wds-primary: ${color};
1348
+ --wds-primary-soft: color-mix(in oklch, ${color} 12%, transparent);
1349
+ --wds-primary-border: color-mix(in oklch, ${color} 40%, transparent);
1350
+ `;
1351
+ }
1352
+ var DEFAULT_PRIMARY = "oklch(0.5811 0.2268 259.15)";
1353
+ function buildThemeStyle(theme, primaryColor) {
1354
+ const primary = buildPrimaryVars(primaryColor);
1355
+ if (theme === "light") {
1356
+ return `
1357
+ [data-wds-root] {
1358
+ ${LIGHT_VARS}
1359
+ ${primary}
1360
+ }
1361
+ `;
1362
+ }
1363
+ if (theme === "dark") {
1364
+ return `
1365
+ [data-wds-root] {
1366
+ ${DARK_VARS}
1367
+ ${primary}
1368
+ }
1369
+ `;
1370
+ }
1371
+ return `
1372
+ [data-wds-root] {
1373
+ ${LIGHT_VARS}
1374
+ ${primary}
1375
+ }
1376
+ @media (prefers-color-scheme: dark) {
1377
+ [data-wds-root] {
1378
+ ${DARK_VARS}
1379
+ --wds-primary-soft: color-mix(in oklch, ${primaryColor} 15%, transparent);
1380
+ }
1381
+ }
1382
+ `;
1383
+ }
1246
1384
  var ChatWidget = ({ config }) => {
1247
- var _a, _b;
1248
1385
  const [isOpen, setIsOpen] = useState4(false);
1249
1386
  const [isExpanded, setIsExpanded] = useState4(false);
1250
1387
  const [view, setView] = useState4("home");
1388
+ const [visitorId] = useState4(
1389
+ () => {
1390
+ var _a, _b;
1391
+ return (_b = (_a = config.visitorId) != null ? _a : loadVisitorId()) != null ? _b : void 0;
1392
+ }
1393
+ );
1251
1394
  const reset = useChatStore((s) => s.reset);
1252
1395
  const resolvedConfig = __spreadProps(__spreadValues({}, config), {
1253
- visitorId: (_b = (_a = config.visitorId) != null ? _a : loadVisitorId()) != null ? _b : void 0
1396
+ visitorId
1254
1397
  });
1255
- const hasHistory = !!resolvedConfig.visitorId;
1398
+ const hasHistory = !!visitorId;
1256
1399
  const { status, messages, error, sendMessage, sendFile } = useChat(resolvedConfig);
1257
1400
  const handleNewConversation = () => {
1258
1401
  reset();
@@ -1267,6 +1410,19 @@ var ChatWidget = ({ config }) => {
1267
1410
  document.body.style.overflow = "";
1268
1411
  };
1269
1412
  }, [isOpen]);
1413
+ useEffect4(() => {
1414
+ var _a, _b;
1415
+ const theme = (_a = config.theme) != null ? _a : "system";
1416
+ const primaryColor = (_b = config.primaryColor) != null ? _b : DEFAULT_PRIMARY;
1417
+ const style = buildThemeStyle(theme, primaryColor);
1418
+ const el = document.createElement("style");
1419
+ el.setAttribute("data-wds-theme", "");
1420
+ el.textContent = style;
1421
+ document.head.appendChild(el);
1422
+ return () => {
1423
+ document.head.removeChild(el);
1424
+ };
1425
+ }, [config.theme, config.primaryColor]);
1270
1426
  return /* @__PURE__ */ jsxs6(Fragment, { children: [
1271
1427
  /* @__PURE__ */ jsx8(
1272
1428
  FloatingButton,
@@ -1307,28 +1463,6 @@ var ChatWidget = ({ config }) => {
1307
1463
  ] });
1308
1464
  };
1309
1465
  injectGlobal`
1310
- :root {
1311
- --wds-primary: oklch(0.5811 0.2268 259.15);
1312
- --wds-primary-soft: oklch(0.5811 0.2268 259.15 / 0.12);
1313
- --wds-primary-border: oklch(0.7695 0.1177 255.22 / 0.4);
1314
- --wds-bg: oklch(1 0 0);
1315
- --wds-fg: oklch(0.145 0 0);
1316
- --wds-muted: oklch(0.556 0 0);
1317
- --wds-muted-bg: oklch(0.97 0 0);
1318
- --wds-border: oklch(0.922 0 0);
1319
- }
1320
-
1321
- @media (prefers-color-scheme: dark) {
1322
- :root {
1323
- --wds-bg: oklch(0.145 0 0);
1324
- --wds-fg: oklch(0.985 0 0);
1325
- --wds-muted: oklch(0.708 0 0);
1326
- --wds-muted-bg: oklch(0.205 0 0);
1327
- --wds-border: oklch(1 0 0 / 10%);
1328
- --wds-primary-soft: oklch(0.5811 0.2268 259.15 / 0.15);
1329
- }
1330
- }
1331
-
1332
1466
  [data-wds-root] * {
1333
1467
  box-sizing: border-box;
1334
1468
  text-align: left;