@xbrain-swe/nova-chat-widget 0.3.0-dev.16 → 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
@@ -120,7 +120,7 @@ function InternalProvider({
120
120
  const sendEvent = (0, import_react.useCallback)((event, properties = {}) => {
121
121
  const { auth: a, visitorId: v, sessionId: sid } = latestRef.current;
122
122
  if (!v) return;
123
- const { conversation_id: conversationId, ...rest } = properties;
123
+ const { conversation_id: conversationId, occurred_at: occurredAt, ...rest } = properties;
124
124
  fetch(`${a.apiBaseUrl}/v1/events`, {
125
125
  method: "POST",
126
126
  keepalive: true,
@@ -131,18 +131,20 @@ function InternalProvider({
131
131
  visitor_id: v,
132
132
  session_id: sid,
133
133
  conversation_id: conversationId ?? null,
134
+ occurred_at: occurredAt ?? (/* @__PURE__ */ new Date()).toISOString(),
134
135
  properties: { screen: a.screen, ...rest }
135
136
  })
136
137
  }).catch(() => {
137
138
  });
138
139
  }, []);
139
140
  const widgetViewSentRef = (0, import_react.useRef)(false);
141
+ const mountedAtRef = (0, import_react.useRef)((/* @__PURE__ */ new Date()).toISOString());
140
142
  (0, import_react.useEffect)(() => {
141
143
  if (widgetViewSentRef.current) return;
142
144
  const send = () => {
143
145
  if (widgetViewSentRef.current) return;
144
146
  widgetViewSentRef.current = true;
145
- sendEvent("widget_view");
147
+ sendEvent("widget_view", { occurred_at: mountedAtRef.current });
146
148
  };
147
149
  if (sessionId) {
148
150
  send();
@@ -1233,7 +1235,6 @@ function useXbNovaChat(options) {
1233
1235
  const [resumed, setResumed] = (0, import_react8.useState)(false);
1234
1236
  const [sentOpening, setSentOpening] = (0, import_react8.useState)(null);
1235
1237
  const conversationIdRef = (0, import_react8.useRef)(null);
1236
- const pendingOpeningRef = (0, import_react8.useRef)(null);
1237
1238
  const messagesRef = (0, import_react8.useRef)(messages);
1238
1239
  messagesRef.current = messages;
1239
1240
  const [busy, setBusy] = (0, import_react8.useState)(false);
@@ -1256,7 +1257,6 @@ function useXbNovaChat(options) {
1256
1257
  setFeedbackState({});
1257
1258
  dispatchedClientActionsRef.current = /* @__PURE__ */ new Set();
1258
1259
  conversationIdRef.current = null;
1259
- pendingOpeningRef.current = null;
1260
1260
  fetch(`${apiBaseUrl}/v1/conversations`, {
1261
1261
  method: "POST",
1262
1262
  headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
@@ -1289,16 +1289,6 @@ function useXbNovaChat(options) {
1289
1289
  cancelled = true;
1290
1290
  };
1291
1291
  }, [apiBaseUrl, apiKey, botId, userKey, threadKey]);
1292
- const openingMessage = options?.openingMessage;
1293
- (0, import_react8.useEffect)(() => {
1294
- if (!onboarding || !nudgeNodeId || !openingMessage) return;
1295
- if (messagesRef.current.length === 0) return;
1296
- setMessages((prev) => [
1297
- ...prev,
1298
- { role: "assistant", parts: [{ kind: "text", id: `nudge:${nudgeNodeId}:${prev.length}`, text: openingMessage }] }
1299
- ]);
1300
- pendingOpeningRef.current = openingMessage;
1301
- }, [onboarding, nudgeNodeId, openingMessage]);
1302
1292
  const appendToLastAssistant = (existingParts, newParts) => setMessages((prev) => {
1303
1293
  const next = [...prev];
1304
1294
  next[next.length - 1] = { ...next[next.length - 1], role: "assistant", parts: [...existingParts, ...newParts] };
@@ -1417,7 +1407,7 @@ function useXbNovaChat(options) {
1417
1407
  void runClientAction(spec, part, last.parts);
1418
1408
  }
1419
1409
  }, [messages, clientActionHandlers, onboarding, apiBaseUrl, apiKey, botId]);
1420
- async function send(text) {
1410
+ async function send(text, nudgeOpening) {
1421
1411
  const trimmed = text.trim();
1422
1412
  if (!trimmed || busy) return;
1423
1413
  sendEvent("message_send", {
@@ -1427,8 +1417,10 @@ function useXbNovaChat(options) {
1427
1417
  });
1428
1418
  setError(null);
1429
1419
  setBusy(true);
1420
+ const resumedOpening = messages.length > 0 && nudgeOpening ? nudgeOpening : null;
1430
1421
  setMessages((prev) => [
1431
1422
  ...prev,
1423
+ ...resumedOpening ? [{ role: "assistant", parts: [{ kind: "text", id: `nudge:${prev.length}`, text: resumedOpening }] }] : [],
1432
1424
  { role: "user", parts: [{ kind: "text", id: "u", text: trimmed }] },
1433
1425
  { role: "assistant", parts: [] }
1434
1426
  ]);
@@ -1441,14 +1433,13 @@ function useXbNovaChat(options) {
1441
1433
  const notes = collectContextEntries(contextRegistry);
1442
1434
  const screen = focused ? { region: focused.region, data: resolveFactValue(focused.data) } : {};
1443
1435
  const context = focused || notes.length ? { ...screen, ...notes.length ? { notes } : {} } : void 0;
1444
- let openingMessage2;
1436
+ let openingMessage;
1445
1437
  if (messages.length === 0) {
1446
- openingMessage2 = (options?.openingMessage ?? onboarding?.greeting) || null;
1447
- setSentOpening(openingMessage2);
1438
+ openingMessage = (options?.openingMessage ?? onboarding?.greeting) || null;
1439
+ setSentOpening(openingMessage);
1448
1440
  } else {
1449
- openingMessage2 = pendingOpeningRef.current;
1441
+ openingMessage = resumedOpening;
1450
1442
  }
1451
- pendingOpeningRef.current = null;
1452
1443
  try {
1453
1444
  await streamChat(
1454
1445
  apiBaseUrl,
@@ -1459,7 +1450,7 @@ function useXbNovaChat(options) {
1459
1450
  conversation_id: conversationIdRef.current,
1460
1451
  context,
1461
1452
  nudge_node_id: nudgeNodeId ?? null,
1462
- opening_message: openingMessage2,
1453
+ opening_message: openingMessage,
1463
1454
  visitor_id: userKey || null,
1464
1455
  thread_key: threadKey ?? null,
1465
1456
  session_id: sessionId
@@ -1479,7 +1470,7 @@ function useXbNovaChat(options) {
1479
1470
  setBusy(false);
1480
1471
  }
1481
1472
  }
1482
- const opening = resumed ? null : sentOpening ?? options?.openingMessage ?? onboarding?.greeting ?? null;
1473
+ const opening = resumed ? null : messages.length > 0 ? sentOpening : options?.openingMessage ?? onboarding?.greeting ?? null;
1483
1474
  return {
1484
1475
  onboarding,
1485
1476
  opening,
@@ -1779,6 +1770,7 @@ function XbNovaChat({
1779
1770
  const [activeBotId, setActiveBotId] = (0, import_react9.useState)(void 0);
1780
1771
  const [activeNudgeNodeId, setActiveNudgeNodeId] = (0, import_react9.useState)(void 0);
1781
1772
  const [nudgeQuickReplies, setNudgeQuickReplies] = (0, import_react9.useState)([]);
1773
+ const [nudgePrompt, setNudgePrompt] = (0, import_react9.useState)(null);
1782
1774
  const [openingLine, setOpeningLine] = (0, import_react9.useState)(null);
1783
1775
  const [showDisclosure, setShowDisclosure] = (0, import_react9.useState)(false);
1784
1776
  const [input, setInput] = (0, import_react9.useState)("");
@@ -1848,9 +1840,11 @@ function XbNovaChat({
1848
1840
  setOpeningLine(consumed.text);
1849
1841
  setActiveNudgeNodeId(consumed.messageNodeId);
1850
1842
  setNudgeQuickReplies(consumed.quickReplies);
1843
+ setNudgePrompt(consumed.text);
1851
1844
  sendEvent("nudge_open", { nudge_node_id: consumed.messageNodeId, bot_id: consumed.botId });
1852
1845
  } else {
1853
1846
  setNudgeQuickReplies([]);
1847
+ setNudgePrompt(null);
1854
1848
  setActiveNudgeNodeId(void 0);
1855
1849
  const focused = getFocusedTrackEntry(trackRegistry);
1856
1850
  setOpeningLine(
@@ -2072,19 +2066,24 @@ function XbNovaChat({
2072
2066
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { ref: bottomRef })
2073
2067
  ] }),
2074
2068
  (() => {
2075
- if (nudgeQuickReplies.length > 0) {
2076
- 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)(
2077
- "button",
2078
- {
2079
- onClick: () => {
2080
- setNudgeQuickReplies([]);
2081
- 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
2082
2083
  },
2083
- className: "xbnova-chip rounded-full border px-2.5 py-1 text-[11px] font-semibold transition",
2084
- children: q
2085
- },
2086
- q
2087
- )) });
2084
+ q
2085
+ )) }) : null
2086
+ ] });
2088
2087
  }
2089
2088
  const quickReplies = onboarding?.quick_replies ?? [];
2090
2089
  if (messages.length !== 0 || quickReplies.length === 0) return null;
@@ -2104,7 +2103,8 @@ function XbNovaChat({
2104
2103
  onSubmit: (e) => {
2105
2104
  e.preventDefault();
2106
2105
  setNudgeQuickReplies([]);
2107
- send(input);
2106
+ setNudgePrompt(null);
2107
+ send(input, messages.length > 0 ? nudgePrompt : null);
2108
2108
  setInput("");
2109
2109
  },
2110
2110
  className: "flex items-center gap-2 border-t border-neutral-200 px-3 py-2.5 dark:border-neutral-700",