@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.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
@@ -70,23 +70,34 @@ function InternalProvider({
70
70
  const sessionId = auth.sessionKey || null;
71
71
  const latestRef = useRef({ auth, visitorId, sessionId });
72
72
  latestRef.current = { auth, visitorId, sessionId };
73
- const sendEvent = useCallback((kind, fields = {}) => {
73
+ const sendEvent = useCallback((event, properties = {}) => {
74
74
  const { auth: a, visitorId: v, sessionId: sid } = latestRef.current;
75
- fetch(`${a.apiBaseUrl}/v1/nudge-events`, {
75
+ if (!v) return;
76
+ const { conversation_id: conversationId, occurred_at: occurredAt, ...rest } = properties;
77
+ fetch(`${a.apiBaseUrl}/v1/events`, {
76
78
  method: "POST",
77
- headers: { "Content-Type": "application/json" },
78
- body: JSON.stringify({ app_id: a.appId, kind, visitor_id: v || null, session_id: sid, ...fields })
79
+ keepalive: true,
80
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${a.apiKey}` },
81
+ body: JSON.stringify({
82
+ app_id: a.appId,
83
+ event,
84
+ visitor_id: v,
85
+ session_id: sid,
86
+ conversation_id: conversationId ?? null,
87
+ occurred_at: occurredAt ?? (/* @__PURE__ */ new Date()).toISOString(),
88
+ properties: { screen: a.screen, ...rest }
89
+ })
79
90
  }).catch(() => {
80
91
  });
81
92
  }, []);
82
93
  const widgetViewSentRef = useRef(false);
94
+ const mountedAtRef = useRef((/* @__PURE__ */ new Date()).toISOString());
83
95
  useEffect(() => {
84
96
  if (widgetViewSentRef.current) return;
85
97
  const send = () => {
86
98
  if (widgetViewSentRef.current) return;
87
99
  widgetViewSentRef.current = true;
88
- const screen = latestRef.current.auth.screen;
89
- sendEvent("widget_view", screen ? { payload: { screen } } : {});
100
+ sendEvent("widget_view", { occurred_at: mountedAtRef.current });
90
101
  };
91
102
  if (sessionId) {
92
103
  send();
@@ -1176,7 +1187,6 @@ function useXbNovaChat(options) {
1176
1187
  const [resumed, setResumed] = useState3(false);
1177
1188
  const [sentOpening, setSentOpening] = useState3(null);
1178
1189
  const conversationIdRef = useRef7(null);
1179
- const pendingOpeningRef = useRef7(null);
1180
1190
  const messagesRef = useRef7(messages);
1181
1191
  messagesRef.current = messages;
1182
1192
  const [busy, setBusy] = useState3(false);
@@ -1199,7 +1209,6 @@ function useXbNovaChat(options) {
1199
1209
  setFeedbackState({});
1200
1210
  dispatchedClientActionsRef.current = /* @__PURE__ */ new Set();
1201
1211
  conversationIdRef.current = null;
1202
- pendingOpeningRef.current = null;
1203
1212
  fetch(`${apiBaseUrl}/v1/conversations`, {
1204
1213
  method: "POST",
1205
1214
  headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
@@ -1232,16 +1241,6 @@ function useXbNovaChat(options) {
1232
1241
  cancelled = true;
1233
1242
  };
1234
1243
  }, [apiBaseUrl, apiKey, botId, userKey, threadKey]);
1235
- const openingMessage = options?.openingMessage;
1236
- useEffect8(() => {
1237
- if (!onboarding || !nudgeNodeId || !openingMessage) return;
1238
- if (messagesRef.current.length === 0) return;
1239
- setMessages((prev) => [
1240
- ...prev,
1241
- { role: "assistant", parts: [{ kind: "text", id: `nudge:${nudgeNodeId}:${prev.length}`, text: openingMessage }] }
1242
- ]);
1243
- pendingOpeningRef.current = openingMessage;
1244
- }, [onboarding, nudgeNodeId, openingMessage]);
1245
1244
  const appendToLastAssistant = (existingParts, newParts) => setMessages((prev) => {
1246
1245
  const next = [...prev];
1247
1246
  next[next.length - 1] = { ...next[next.length - 1], role: "assistant", parts: [...existingParts, ...newParts] };
@@ -1274,6 +1273,7 @@ function useXbNovaChat(options) {
1274
1273
  }
1275
1274
  );
1276
1275
  if (!res.ok) throw new Error(`${res.status}`);
1276
+ sendEvent("feedback_submit", { conversation_id: conversationId, message_id: messageId, rating, reason });
1277
1277
  options?.onFeedback?.({ conversationId, messageId, rating, reason });
1278
1278
  } catch (e) {
1279
1279
  console.warn("[xbnova] \uD53C\uB4DC\uBC31 \uC800\uC7A5 \uC2E4\uD328", e);
@@ -1283,7 +1283,7 @@ function useXbNovaChat(options) {
1283
1283
  function postClientActionEvent(kind, action, label) {
1284
1284
  const conversationId = conversationIdRef.current;
1285
1285
  if (!conversationId) return;
1286
- sendEvent(kind, { conversation_id: conversationId, payload: label ? { action, label } : { action } });
1286
+ sendEvent(kind, { conversation_id: conversationId, action, label });
1287
1287
  }
1288
1288
  function trackClientActionClick(action, label) {
1289
1289
  postClientActionEvent("cta_click", action, label);
@@ -1359,13 +1359,20 @@ function useXbNovaChat(options) {
1359
1359
  void runClientAction(spec, part, last.parts);
1360
1360
  }
1361
1361
  }, [messages, clientActionHandlers, onboarding, apiBaseUrl, apiKey, botId]);
1362
- async function send(text) {
1362
+ async function send(text, nudgeOpening) {
1363
1363
  const trimmed = text.trim();
1364
1364
  if (!trimmed || busy) return;
1365
+ sendEvent("message_send", {
1366
+ conversation_id: conversationIdRef.current,
1367
+ is_first: messages.length === 0,
1368
+ length: trimmed.length
1369
+ });
1365
1370
  setError(null);
1366
1371
  setBusy(true);
1372
+ const resumedOpening = messages.length > 0 && nudgeOpening ? nudgeOpening : null;
1367
1373
  setMessages((prev) => [
1368
1374
  ...prev,
1375
+ ...resumedOpening ? [{ role: "assistant", parts: [{ kind: "text", id: `nudge:${prev.length}`, text: resumedOpening }] }] : [],
1369
1376
  { role: "user", parts: [{ kind: "text", id: "u", text: trimmed }] },
1370
1377
  { role: "assistant", parts: [] }
1371
1378
  ]);
@@ -1378,14 +1385,13 @@ function useXbNovaChat(options) {
1378
1385
  const notes = collectContextEntries(contextRegistry);
1379
1386
  const screen = focused ? { region: focused.region, data: resolveFactValue(focused.data) } : {};
1380
1387
  const context = focused || notes.length ? { ...screen, ...notes.length ? { notes } : {} } : void 0;
1381
- let openingMessage2;
1388
+ let openingMessage;
1382
1389
  if (messages.length === 0) {
1383
- openingMessage2 = (options?.openingMessage ?? onboarding?.greeting) || null;
1384
- setSentOpening(openingMessage2);
1390
+ openingMessage = (options?.openingMessage ?? onboarding?.greeting) || null;
1391
+ setSentOpening(openingMessage);
1385
1392
  } else {
1386
- openingMessage2 = pendingOpeningRef.current;
1393
+ openingMessage = resumedOpening;
1387
1394
  }
1388
- pendingOpeningRef.current = null;
1389
1395
  try {
1390
1396
  await streamChat(
1391
1397
  apiBaseUrl,
@@ -1396,7 +1402,7 @@ function useXbNovaChat(options) {
1396
1402
  conversation_id: conversationIdRef.current,
1397
1403
  context,
1398
1404
  nudge_node_id: nudgeNodeId ?? null,
1399
- opening_message: openingMessage2,
1405
+ opening_message: openingMessage,
1400
1406
  visitor_id: userKey || null,
1401
1407
  thread_key: threadKey ?? null,
1402
1408
  session_id: sessionId
@@ -1416,7 +1422,7 @@ function useXbNovaChat(options) {
1416
1422
  setBusy(false);
1417
1423
  }
1418
1424
  }
1419
- const opening = resumed ? null : sentOpening ?? options?.openingMessage ?? onboarding?.greeting ?? null;
1425
+ const opening = resumed ? null : messages.length > 0 ? sentOpening : options?.openingMessage ?? onboarding?.greeting ?? null;
1420
1426
  return {
1421
1427
  onboarding,
1422
1428
  opening,
@@ -1716,6 +1722,7 @@ function XbNovaChat({
1716
1722
  const [activeBotId, setActiveBotId] = useState4(void 0);
1717
1723
  const [activeNudgeNodeId, setActiveNudgeNodeId] = useState4(void 0);
1718
1724
  const [nudgeQuickReplies, setNudgeQuickReplies] = useState4([]);
1725
+ const [nudgePrompt, setNudgePrompt] = useState4(null);
1719
1726
  const [openingLine, setOpeningLine] = useState4(null);
1720
1727
  const [showDisclosure, setShowDisclosure] = useState4(false);
1721
1728
  const [input, setInput] = useState4("");
@@ -1785,9 +1792,11 @@ function XbNovaChat({
1785
1792
  setOpeningLine(consumed.text);
1786
1793
  setActiveNudgeNodeId(consumed.messageNodeId);
1787
1794
  setNudgeQuickReplies(consumed.quickReplies);
1795
+ setNudgePrompt(consumed.text);
1788
1796
  sendEvent("nudge_open", { nudge_node_id: consumed.messageNodeId, bot_id: consumed.botId });
1789
1797
  } else {
1790
1798
  setNudgeQuickReplies([]);
1799
+ setNudgePrompt(null);
1791
1800
  setActiveNudgeNodeId(void 0);
1792
1801
  const focused = getFocusedTrackEntry(trackRegistry);
1793
1802
  setOpeningLine(
@@ -1799,12 +1808,17 @@ function XbNovaChat({
1799
1808
  markDisclosureSeen();
1800
1809
  }
1801
1810
  setOpen(true);
1811
+ sendEvent("chat_open", { from_nudge: Boolean(consumed) });
1802
1812
  try {
1803
1813
  onOpen?.({ fromNudge: Boolean(consumed), hintText: consumed?.text });
1804
1814
  } catch (err) {
1805
1815
  console.warn("[XbNova] onOpen \uCF5C\uBC31 \uC624\uB958", err);
1806
1816
  }
1807
1817
  }
1818
+ function closeSheet() {
1819
+ setOpen(false);
1820
+ sendEvent("chat_close");
1821
+ }
1808
1822
  if (!open) {
1809
1823
  if (barHidden) return null;
1810
1824
  const showHint = Boolean(hint && hintsEnabled);
@@ -1909,7 +1923,7 @@ function XbNovaChat({
1909
1923
  transition: "opacity 260ms ease-out",
1910
1924
  ...backdropStyle
1911
1925
  },
1912
- children: /* @__PURE__ */ jsx7("div", { className: "h-full w-full bg-black/45", onClick: () => setOpen(false) })
1926
+ children: /* @__PURE__ */ jsx7("div", { className: "h-full w-full bg-black/45", onClick: closeSheet })
1913
1927
  }
1914
1928
  ) : null,
1915
1929
  /* @__PURE__ */ jsx7(
@@ -1945,7 +1959,7 @@ function XbNovaChat({
1945
1959
  "button",
1946
1960
  {
1947
1961
  type: "button",
1948
- onClick: () => setOpen(false),
1962
+ onClick: closeSheet,
1949
1963
  "aria-label": "\uCC44\uD305 \uB2EB\uAE30",
1950
1964
  className: "shrink-0 text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-200",
1951
1965
  children: "\xD7"
@@ -2004,19 +2018,24 @@ function XbNovaChat({
2004
2018
  /* @__PURE__ */ jsx7("div", { ref: bottomRef })
2005
2019
  ] }),
2006
2020
  (() => {
2007
- if (nudgeQuickReplies.length > 0) {
2008
- 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(
2009
- "button",
2010
- {
2011
- onClick: () => {
2012
- setNudgeQuickReplies([]);
2013
- 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
2014
2035
  },
2015
- className: "xbnova-chip rounded-full border px-2.5 py-1 text-[11px] font-semibold transition",
2016
- children: q
2017
- },
2018
- q
2019
- )) });
2036
+ q
2037
+ )) }) : null
2038
+ ] });
2020
2039
  }
2021
2040
  const quickReplies = onboarding?.quick_replies ?? [];
2022
2041
  if (messages.length !== 0 || quickReplies.length === 0) return null;
@@ -2036,7 +2055,8 @@ function XbNovaChat({
2036
2055
  onSubmit: (e) => {
2037
2056
  e.preventDefault();
2038
2057
  setNudgeQuickReplies([]);
2039
- send(input);
2058
+ setNudgePrompt(null);
2059
+ send(input, messages.length > 0 ? nudgePrompt : null);
2040
2060
  setInput("");
2041
2061
  },
2042
2062
  className: "flex items-center gap-2 border-t border-neutral-200 px-3 py-2.5 dark:border-neutral-700",