@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.d.cts CHANGED
@@ -171,7 +171,8 @@ type UseXbNovaChatResult = {
171
171
  messages: ChatMessage[];
172
172
  busy: boolean;
173
173
  error: string | null;
174
- send: (text: string) => Promise<void>;
174
+ /** `nudgeOpening`은 대화 중 넛지를 눌러 띄운 질문이다. 답을 보낼 때 그 앞에 AI 발화로 남긴다. */
175
+ send: (text: string, nudgeOpening?: string | null) => Promise<void>;
175
176
  /** client action 핸들러가 돌려준 정규화 결과(tool_call_id별). 채팅 화면이 view 렌더러로 그린다. */
176
177
  clientResults: ClientResults;
177
178
  /** messageId → 현재 평가. 평가 안 한 메시지는 키가 없다. */
package/dist/index.d.ts CHANGED
@@ -171,7 +171,8 @@ type UseXbNovaChatResult = {
171
171
  messages: ChatMessage[];
172
172
  busy: boolean;
173
173
  error: string | null;
174
- send: (text: string) => Promise<void>;
174
+ /** `nudgeOpening`은 대화 중 넛지를 눌러 띄운 질문이다. 답을 보낼 때 그 앞에 AI 발화로 남긴다. */
175
+ send: (text: string, nudgeOpening?: string | null) => Promise<void>;
175
176
  /** client action 핸들러가 돌려준 정규화 결과(tool_call_id별). 채팅 화면이 view 렌더러로 그린다. */
176
177
  clientResults: ClientResults;
177
178
  /** messageId → 현재 평가. 평가 안 한 메시지는 키가 없다. */
package/dist/index.js CHANGED
@@ -73,7 +73,7 @@ function InternalProvider({
73
73
  const sendEvent = useCallback((event, properties = {}) => {
74
74
  const { auth: a, visitorId: v, sessionId: sid } = latestRef.current;
75
75
  if (!v) return;
76
- const { conversation_id: conversationId, ...rest } = properties;
76
+ const { conversation_id: conversationId, occurred_at: occurredAt, ...rest } = properties;
77
77
  fetch(`${a.apiBaseUrl}/v1/events`, {
78
78
  method: "POST",
79
79
  keepalive: true,
@@ -84,18 +84,20 @@ function InternalProvider({
84
84
  visitor_id: v,
85
85
  session_id: sid,
86
86
  conversation_id: conversationId ?? null,
87
+ occurred_at: occurredAt ?? (/* @__PURE__ */ new Date()).toISOString(),
87
88
  properties: { screen: a.screen, ...rest }
88
89
  })
89
90
  }).catch(() => {
90
91
  });
91
92
  }, []);
92
93
  const widgetViewSentRef = useRef(false);
94
+ const mountedAtRef = useRef((/* @__PURE__ */ new Date()).toISOString());
93
95
  useEffect(() => {
94
96
  if (widgetViewSentRef.current) return;
95
97
  const send = () => {
96
98
  if (widgetViewSentRef.current) return;
97
99
  widgetViewSentRef.current = true;
98
- sendEvent("widget_view");
100
+ sendEvent("widget_view", { occurred_at: mountedAtRef.current });
99
101
  };
100
102
  if (sessionId) {
101
103
  send();
@@ -1185,7 +1187,6 @@ function useXbNovaChat(options) {
1185
1187
  const [resumed, setResumed] = useState3(false);
1186
1188
  const [sentOpening, setSentOpening] = useState3(null);
1187
1189
  const conversationIdRef = useRef7(null);
1188
- const pendingOpeningRef = useRef7(null);
1189
1190
  const messagesRef = useRef7(messages);
1190
1191
  messagesRef.current = messages;
1191
1192
  const [busy, setBusy] = useState3(false);
@@ -1208,7 +1209,6 @@ function useXbNovaChat(options) {
1208
1209
  setFeedbackState({});
1209
1210
  dispatchedClientActionsRef.current = /* @__PURE__ */ new Set();
1210
1211
  conversationIdRef.current = null;
1211
- pendingOpeningRef.current = null;
1212
1212
  fetch(`${apiBaseUrl}/v1/conversations`, {
1213
1213
  method: "POST",
1214
1214
  headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
@@ -1241,16 +1241,6 @@ function useXbNovaChat(options) {
1241
1241
  cancelled = true;
1242
1242
  };
1243
1243
  }, [apiBaseUrl, apiKey, botId, userKey, threadKey]);
1244
- const openingMessage = options?.openingMessage;
1245
- useEffect8(() => {
1246
- if (!onboarding || !nudgeNodeId || !openingMessage) return;
1247
- if (messagesRef.current.length === 0) return;
1248
- setMessages((prev) => [
1249
- ...prev,
1250
- { role: "assistant", parts: [{ kind: "text", id: `nudge:${nudgeNodeId}:${prev.length}`, text: openingMessage }] }
1251
- ]);
1252
- pendingOpeningRef.current = openingMessage;
1253
- }, [onboarding, nudgeNodeId, openingMessage]);
1254
1244
  const appendToLastAssistant = (existingParts, newParts) => setMessages((prev) => {
1255
1245
  const next = [...prev];
1256
1246
  next[next.length - 1] = { ...next[next.length - 1], role: "assistant", parts: [...existingParts, ...newParts] };
@@ -1369,7 +1359,7 @@ function useXbNovaChat(options) {
1369
1359
  void runClientAction(spec, part, last.parts);
1370
1360
  }
1371
1361
  }, [messages, clientActionHandlers, onboarding, apiBaseUrl, apiKey, botId]);
1372
- async function send(text) {
1362
+ async function send(text, nudgeOpening) {
1373
1363
  const trimmed = text.trim();
1374
1364
  if (!trimmed || busy) return;
1375
1365
  sendEvent("message_send", {
@@ -1379,8 +1369,10 @@ function useXbNovaChat(options) {
1379
1369
  });
1380
1370
  setError(null);
1381
1371
  setBusy(true);
1372
+ const resumedOpening = messages.length > 0 && nudgeOpening ? nudgeOpening : null;
1382
1373
  setMessages((prev) => [
1383
1374
  ...prev,
1375
+ ...resumedOpening ? [{ role: "assistant", parts: [{ kind: "text", id: `nudge:${prev.length}`, text: resumedOpening }] }] : [],
1384
1376
  { role: "user", parts: [{ kind: "text", id: "u", text: trimmed }] },
1385
1377
  { role: "assistant", parts: [] }
1386
1378
  ]);
@@ -1393,14 +1385,13 @@ function useXbNovaChat(options) {
1393
1385
  const notes = collectContextEntries(contextRegistry);
1394
1386
  const screen = focused ? { region: focused.region, data: resolveFactValue(focused.data) } : {};
1395
1387
  const context = focused || notes.length ? { ...screen, ...notes.length ? { notes } : {} } : void 0;
1396
- let openingMessage2;
1388
+ let openingMessage;
1397
1389
  if (messages.length === 0) {
1398
- openingMessage2 = (options?.openingMessage ?? onboarding?.greeting) || null;
1399
- setSentOpening(openingMessage2);
1390
+ openingMessage = (options?.openingMessage ?? onboarding?.greeting) || null;
1391
+ setSentOpening(openingMessage);
1400
1392
  } else {
1401
- openingMessage2 = pendingOpeningRef.current;
1393
+ openingMessage = resumedOpening;
1402
1394
  }
1403
- pendingOpeningRef.current = null;
1404
1395
  try {
1405
1396
  await streamChat(
1406
1397
  apiBaseUrl,
@@ -1411,7 +1402,7 @@ function useXbNovaChat(options) {
1411
1402
  conversation_id: conversationIdRef.current,
1412
1403
  context,
1413
1404
  nudge_node_id: nudgeNodeId ?? null,
1414
- opening_message: openingMessage2,
1405
+ opening_message: openingMessage,
1415
1406
  visitor_id: userKey || null,
1416
1407
  thread_key: threadKey ?? null,
1417
1408
  session_id: sessionId
@@ -1431,7 +1422,7 @@ function useXbNovaChat(options) {
1431
1422
  setBusy(false);
1432
1423
  }
1433
1424
  }
1434
- const opening = resumed ? null : sentOpening ?? options?.openingMessage ?? onboarding?.greeting ?? null;
1425
+ const opening = resumed ? null : messages.length > 0 ? sentOpening : options?.openingMessage ?? onboarding?.greeting ?? null;
1435
1426
  return {
1436
1427
  onboarding,
1437
1428
  opening,
@@ -1731,6 +1722,7 @@ function XbNovaChat({
1731
1722
  const [activeBotId, setActiveBotId] = useState4(void 0);
1732
1723
  const [activeNudgeNodeId, setActiveNudgeNodeId] = useState4(void 0);
1733
1724
  const [nudgeQuickReplies, setNudgeQuickReplies] = useState4([]);
1725
+ const [nudgePrompt, setNudgePrompt] = useState4(null);
1734
1726
  const [openingLine, setOpeningLine] = useState4(null);
1735
1727
  const [showDisclosure, setShowDisclosure] = useState4(false);
1736
1728
  const [input, setInput] = useState4("");
@@ -1800,9 +1792,11 @@ function XbNovaChat({
1800
1792
  setOpeningLine(consumed.text);
1801
1793
  setActiveNudgeNodeId(consumed.messageNodeId);
1802
1794
  setNudgeQuickReplies(consumed.quickReplies);
1795
+ setNudgePrompt(consumed.text);
1803
1796
  sendEvent("nudge_open", { nudge_node_id: consumed.messageNodeId, bot_id: consumed.botId });
1804
1797
  } else {
1805
1798
  setNudgeQuickReplies([]);
1799
+ setNudgePrompt(null);
1806
1800
  setActiveNudgeNodeId(void 0);
1807
1801
  const focused = getFocusedTrackEntry(trackRegistry);
1808
1802
  setOpeningLine(
@@ -2024,19 +2018,24 @@ function XbNovaChat({
2024
2018
  /* @__PURE__ */ jsx7("div", { ref: bottomRef })
2025
2019
  ] }),
2026
2020
  (() => {
2027
- if (nudgeQuickReplies.length > 0) {
2028
- return /* @__PURE__ */ jsx7("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__ */ jsx7(
2029
- "button",
2030
- {
2031
- onClick: () => {
2032
- setNudgeQuickReplies([]);
2033
- send(q);
2021
+ const prompt = messages.length > 0 ? nudgePrompt : null;
2022
+ if (prompt || nudgeQuickReplies.length > 0) {
2023
+ return /* @__PURE__ */ jsxs3("div", { className: "space-y-2 border-t border-neutral-200 px-3.5 py-2.5 dark:border-neutral-700", children: [
2024
+ prompt ? /* @__PURE__ */ jsx7("div", { className: "text-sm font-medium", children: /* @__PURE__ */ jsx7(AssistantMarkdown, { text: prompt }) }) : null,
2025
+ nudgeQuickReplies.length > 0 ? /* @__PURE__ */ jsx7("div", { className: "flex flex-wrap gap-1.5", children: nudgeQuickReplies.map((q) => /* @__PURE__ */ jsx7(
2026
+ "button",
2027
+ {
2028
+ onClick: () => {
2029
+ setNudgeQuickReplies([]);
2030
+ setNudgePrompt(null);
2031
+ send(q, prompt);
2032
+ },
2033
+ className: "xbnova-chip rounded-full border px-2.5 py-1 text-[11px] font-semibold transition",
2034
+ children: q
2034
2035
  },
2035
- className: "xbnova-chip rounded-full border px-2.5 py-1 text-[11px] font-semibold transition",
2036
- children: q
2037
- },
2038
- q
2039
- )) });
2036
+ q
2037
+ )) }) : null
2038
+ ] });
2040
2039
  }
2041
2040
  const quickReplies = onboarding?.quick_replies ?? [];
2042
2041
  if (messages.length !== 0 || quickReplies.length === 0) return null;
@@ -2056,7 +2055,8 @@ function XbNovaChat({
2056
2055
  onSubmit: (e) => {
2057
2056
  e.preventDefault();
2058
2057
  setNudgeQuickReplies([]);
2059
- send(input);
2058
+ setNudgePrompt(null);
2059
+ send(input, messages.length > 0 ? nudgePrompt : null);
2060
2060
  setInput("");
2061
2061
  },
2062
2062
  className: "flex items-center gap-2 border-t border-neutral-200 px-3 py-2.5 dark:border-neutral-700",