@xbrain-swe/nova-chat-widget 0.2.4-dev.15 → 0.3.0-dev.17

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.cjs CHANGED
@@ -117,23 +117,34 @@ function InternalProvider({
117
117
  const sessionId = auth.sessionKey || null;
118
118
  const latestRef = (0, import_react.useRef)({ auth, visitorId, sessionId });
119
119
  latestRef.current = { auth, visitorId, sessionId };
120
- const sendEvent = (0, import_react.useCallback)((kind, fields = {}) => {
120
+ const sendEvent = (0, import_react.useCallback)((event, properties = {}) => {
121
121
  const { auth: a, visitorId: v, sessionId: sid } = latestRef.current;
122
- fetch(`${a.apiBaseUrl}/v1/nudge-events`, {
122
+ if (!v) return;
123
+ const { conversation_id: conversationId, occurred_at: occurredAt, ...rest } = properties;
124
+ fetch(`${a.apiBaseUrl}/v1/events`, {
123
125
  method: "POST",
124
- headers: { "Content-Type": "application/json" },
125
- body: JSON.stringify({ app_id: a.appId, kind, visitor_id: v || null, session_id: sid, ...fields })
126
+ keepalive: true,
127
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${a.apiKey}` },
128
+ body: JSON.stringify({
129
+ app_id: a.appId,
130
+ event,
131
+ visitor_id: v,
132
+ session_id: sid,
133
+ conversation_id: conversationId ?? null,
134
+ occurred_at: occurredAt ?? (/* @__PURE__ */ new Date()).toISOString(),
135
+ properties: { screen: a.screen, ...rest }
136
+ })
126
137
  }).catch(() => {
127
138
  });
128
139
  }, []);
129
140
  const widgetViewSentRef = (0, import_react.useRef)(false);
141
+ const mountedAtRef = (0, import_react.useRef)((/* @__PURE__ */ new Date()).toISOString());
130
142
  (0, import_react.useEffect)(() => {
131
143
  if (widgetViewSentRef.current) return;
132
144
  const send = () => {
133
145
  if (widgetViewSentRef.current) return;
134
146
  widgetViewSentRef.current = true;
135
- const screen = latestRef.current.auth.screen;
136
- sendEvent("widget_view", screen ? { payload: { screen } } : {});
147
+ sendEvent("widget_view", { occurred_at: mountedAtRef.current });
137
148
  };
138
149
  if (sessionId) {
139
150
  send();
@@ -1224,7 +1235,6 @@ function useXbNovaChat(options) {
1224
1235
  const [resumed, setResumed] = (0, import_react8.useState)(false);
1225
1236
  const [sentOpening, setSentOpening] = (0, import_react8.useState)(null);
1226
1237
  const conversationIdRef = (0, import_react8.useRef)(null);
1227
- const pendingOpeningRef = (0, import_react8.useRef)(null);
1228
1238
  const messagesRef = (0, import_react8.useRef)(messages);
1229
1239
  messagesRef.current = messages;
1230
1240
  const [busy, setBusy] = (0, import_react8.useState)(false);
@@ -1247,7 +1257,6 @@ function useXbNovaChat(options) {
1247
1257
  setFeedbackState({});
1248
1258
  dispatchedClientActionsRef.current = /* @__PURE__ */ new Set();
1249
1259
  conversationIdRef.current = null;
1250
- pendingOpeningRef.current = null;
1251
1260
  fetch(`${apiBaseUrl}/v1/conversations`, {
1252
1261
  method: "POST",
1253
1262
  headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
@@ -1280,16 +1289,6 @@ function useXbNovaChat(options) {
1280
1289
  cancelled = true;
1281
1290
  };
1282
1291
  }, [apiBaseUrl, apiKey, botId, userKey, threadKey]);
1283
- const openingMessage = options?.openingMessage;
1284
- (0, import_react8.useEffect)(() => {
1285
- if (!onboarding || !nudgeNodeId || !openingMessage) return;
1286
- if (messagesRef.current.length === 0) return;
1287
- setMessages((prev) => [
1288
- ...prev,
1289
- { role: "assistant", parts: [{ kind: "text", id: `nudge:${nudgeNodeId}:${prev.length}`, text: openingMessage }] }
1290
- ]);
1291
- pendingOpeningRef.current = openingMessage;
1292
- }, [onboarding, nudgeNodeId, openingMessage]);
1293
1292
  const appendToLastAssistant = (existingParts, newParts) => setMessages((prev) => {
1294
1293
  const next = [...prev];
1295
1294
  next[next.length - 1] = { ...next[next.length - 1], role: "assistant", parts: [...existingParts, ...newParts] };
@@ -1322,6 +1321,7 @@ function useXbNovaChat(options) {
1322
1321
  }
1323
1322
  );
1324
1323
  if (!res.ok) throw new Error(`${res.status}`);
1324
+ sendEvent("feedback_submit", { conversation_id: conversationId, message_id: messageId, rating, reason });
1325
1325
  options?.onFeedback?.({ conversationId, messageId, rating, reason });
1326
1326
  } catch (e) {
1327
1327
  console.warn("[xbnova] \uD53C\uB4DC\uBC31 \uC800\uC7A5 \uC2E4\uD328", e);
@@ -1331,7 +1331,7 @@ function useXbNovaChat(options) {
1331
1331
  function postClientActionEvent(kind, action, label) {
1332
1332
  const conversationId = conversationIdRef.current;
1333
1333
  if (!conversationId) return;
1334
- sendEvent(kind, { conversation_id: conversationId, payload: label ? { action, label } : { action } });
1334
+ sendEvent(kind, { conversation_id: conversationId, action, label });
1335
1335
  }
1336
1336
  function trackClientActionClick(action, label) {
1337
1337
  postClientActionEvent("cta_click", action, label);
@@ -1407,13 +1407,20 @@ function useXbNovaChat(options) {
1407
1407
  void runClientAction(spec, part, last.parts);
1408
1408
  }
1409
1409
  }, [messages, clientActionHandlers, onboarding, apiBaseUrl, apiKey, botId]);
1410
- async function send(text) {
1410
+ async function send(text, nudgeOpening) {
1411
1411
  const trimmed = text.trim();
1412
1412
  if (!trimmed || busy) return;
1413
+ sendEvent("message_send", {
1414
+ conversation_id: conversationIdRef.current,
1415
+ is_first: messages.length === 0,
1416
+ length: trimmed.length
1417
+ });
1413
1418
  setError(null);
1414
1419
  setBusy(true);
1420
+ const resumedOpening = messages.length > 0 && nudgeOpening ? nudgeOpening : null;
1415
1421
  setMessages((prev) => [
1416
1422
  ...prev,
1423
+ ...resumedOpening ? [{ role: "assistant", parts: [{ kind: "text", id: `nudge:${prev.length}`, text: resumedOpening }] }] : [],
1417
1424
  { role: "user", parts: [{ kind: "text", id: "u", text: trimmed }] },
1418
1425
  { role: "assistant", parts: [] }
1419
1426
  ]);
@@ -1426,14 +1433,13 @@ function useXbNovaChat(options) {
1426
1433
  const notes = collectContextEntries(contextRegistry);
1427
1434
  const screen = focused ? { region: focused.region, data: resolveFactValue(focused.data) } : {};
1428
1435
  const context = focused || notes.length ? { ...screen, ...notes.length ? { notes } : {} } : void 0;
1429
- let openingMessage2;
1436
+ let openingMessage;
1430
1437
  if (messages.length === 0) {
1431
- openingMessage2 = (options?.openingMessage ?? onboarding?.greeting) || null;
1432
- setSentOpening(openingMessage2);
1438
+ openingMessage = (options?.openingMessage ?? onboarding?.greeting) || null;
1439
+ setSentOpening(openingMessage);
1433
1440
  } else {
1434
- openingMessage2 = pendingOpeningRef.current;
1441
+ openingMessage = resumedOpening;
1435
1442
  }
1436
- pendingOpeningRef.current = null;
1437
1443
  try {
1438
1444
  await streamChat(
1439
1445
  apiBaseUrl,
@@ -1444,7 +1450,7 @@ function useXbNovaChat(options) {
1444
1450
  conversation_id: conversationIdRef.current,
1445
1451
  context,
1446
1452
  nudge_node_id: nudgeNodeId ?? null,
1447
- opening_message: openingMessage2,
1453
+ opening_message: openingMessage,
1448
1454
  visitor_id: userKey || null,
1449
1455
  thread_key: threadKey ?? null,
1450
1456
  session_id: sessionId
@@ -1464,7 +1470,7 @@ function useXbNovaChat(options) {
1464
1470
  setBusy(false);
1465
1471
  }
1466
1472
  }
1467
- const opening = resumed ? null : sentOpening ?? options?.openingMessage ?? onboarding?.greeting ?? null;
1473
+ const opening = resumed ? null : messages.length > 0 ? sentOpening : options?.openingMessage ?? onboarding?.greeting ?? null;
1468
1474
  return {
1469
1475
  onboarding,
1470
1476
  opening,
@@ -1764,6 +1770,7 @@ function XbNovaChat({
1764
1770
  const [activeBotId, setActiveBotId] = (0, import_react9.useState)(void 0);
1765
1771
  const [activeNudgeNodeId, setActiveNudgeNodeId] = (0, import_react9.useState)(void 0);
1766
1772
  const [nudgeQuickReplies, setNudgeQuickReplies] = (0, import_react9.useState)([]);
1773
+ const [nudgePrompt, setNudgePrompt] = (0, import_react9.useState)(null);
1767
1774
  const [openingLine, setOpeningLine] = (0, import_react9.useState)(null);
1768
1775
  const [showDisclosure, setShowDisclosure] = (0, import_react9.useState)(false);
1769
1776
  const [input, setInput] = (0, import_react9.useState)("");
@@ -1833,9 +1840,11 @@ function XbNovaChat({
1833
1840
  setOpeningLine(consumed.text);
1834
1841
  setActiveNudgeNodeId(consumed.messageNodeId);
1835
1842
  setNudgeQuickReplies(consumed.quickReplies);
1843
+ setNudgePrompt(consumed.text);
1836
1844
  sendEvent("nudge_open", { nudge_node_id: consumed.messageNodeId, bot_id: consumed.botId });
1837
1845
  } else {
1838
1846
  setNudgeQuickReplies([]);
1847
+ setNudgePrompt(null);
1839
1848
  setActiveNudgeNodeId(void 0);
1840
1849
  const focused = getFocusedTrackEntry(trackRegistry);
1841
1850
  setOpeningLine(
@@ -1847,12 +1856,17 @@ function XbNovaChat({
1847
1856
  markDisclosureSeen();
1848
1857
  }
1849
1858
  setOpen(true);
1859
+ sendEvent("chat_open", { from_nudge: Boolean(consumed) });
1850
1860
  try {
1851
1861
  onOpen?.({ fromNudge: Boolean(consumed), hintText: consumed?.text });
1852
1862
  } catch (err) {
1853
1863
  console.warn("[XbNova] onOpen \uCF5C\uBC31 \uC624\uB958", err);
1854
1864
  }
1855
1865
  }
1866
+ function closeSheet() {
1867
+ setOpen(false);
1868
+ sendEvent("chat_close");
1869
+ }
1856
1870
  if (!open) {
1857
1871
  if (barHidden) return null;
1858
1872
  const showHint = Boolean(hint && hintsEnabled);
@@ -1957,7 +1971,7 @@ function XbNovaChat({
1957
1971
  transition: "opacity 260ms ease-out",
1958
1972
  ...backdropStyle
1959
1973
  },
1960
- children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "h-full w-full bg-black/45", onClick: () => setOpen(false) })
1974
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "h-full w-full bg-black/45", onClick: closeSheet })
1961
1975
  }
1962
1976
  ) : null,
1963
1977
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
@@ -1993,7 +2007,7 @@ function XbNovaChat({
1993
2007
  "button",
1994
2008
  {
1995
2009
  type: "button",
1996
- onClick: () => setOpen(false),
2010
+ onClick: closeSheet,
1997
2011
  "aria-label": "\uCC44\uD305 \uB2EB\uAE30",
1998
2012
  className: "shrink-0 text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-200",
1999
2013
  children: "\xD7"
@@ -2052,19 +2066,24 @@ function XbNovaChat({
2052
2066
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { ref: bottomRef })
2053
2067
  ] }),
2054
2068
  (() => {
2055
- if (nudgeQuickReplies.length > 0) {
2056
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "flex flex-wrap gap-1.5 border-t border-neutral-200 px-3.5 py-2.5 dark:border-neutral-700", children: nudgeQuickReplies.map((q) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2057
- "button",
2058
- {
2059
- onClick: () => {
2060
- setNudgeQuickReplies([]);
2061
- send(q);
2069
+ const prompt = messages.length > 0 ? nudgePrompt : null;
2070
+ if (prompt || nudgeQuickReplies.length > 0) {
2071
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "space-y-2 border-t border-neutral-200 px-3.5 py-2.5 dark:border-neutral-700", children: [
2072
+ prompt ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "text-sm font-medium", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(AssistantMarkdown, { text: prompt }) }) : null,
2073
+ nudgeQuickReplies.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "flex flex-wrap gap-1.5", children: nudgeQuickReplies.map((q) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2074
+ "button",
2075
+ {
2076
+ onClick: () => {
2077
+ setNudgeQuickReplies([]);
2078
+ setNudgePrompt(null);
2079
+ send(q, prompt);
2080
+ },
2081
+ className: "xbnova-chip rounded-full border px-2.5 py-1 text-[11px] font-semibold transition",
2082
+ children: q
2062
2083
  },
2063
- className: "xbnova-chip rounded-full border px-2.5 py-1 text-[11px] font-semibold transition",
2064
- children: q
2065
- },
2066
- q
2067
- )) });
2084
+ q
2085
+ )) }) : null
2086
+ ] });
2068
2087
  }
2069
2088
  const quickReplies = onboarding?.quick_replies ?? [];
2070
2089
  if (messages.length !== 0 || quickReplies.length === 0) return null;
@@ -2084,7 +2103,8 @@ function XbNovaChat({
2084
2103
  onSubmit: (e) => {
2085
2104
  e.preventDefault();
2086
2105
  setNudgeQuickReplies([]);
2087
- send(input);
2106
+ setNudgePrompt(null);
2107
+ send(input, messages.length > 0 ? nudgePrompt : null);
2088
2108
  setInput("");
2089
2109
  },
2090
2110
  className: "flex items-center gap-2 border-t border-neutral-200 px-3 py-2.5 dark:border-neutral-700",