@xbrain-swe/nova-chat-widget 0.1.8 → 0.1.9

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
@@ -1057,6 +1057,9 @@ async function consumeSse(response, handlers) {
1057
1057
  continue;
1058
1058
  }
1059
1059
  switch (event.type) {
1060
+ case "start":
1061
+ if (typeof event.messageId === "string" && event.messageId) handlers.onMessageId?.(event.messageId);
1062
+ break;
1060
1063
  case "text-start": {
1061
1064
  const id = String(event.id);
1062
1065
  textIndex.set(id, parts.length);
@@ -1133,7 +1136,7 @@ async function resumeToolResult(apiBaseUrl, apiKey, body, handlers) {
1133
1136
  // src/use-chat.ts
1134
1137
  var DEFAULT_CLIENT_ACTION_TIMEOUT_MS = 15e3;
1135
1138
  function useXbNovaChat(options) {
1136
- const { auth, trackRegistry, clientActionHandlers } = useXbNovaInternal();
1139
+ const { auth, trackRegistry, clientActionHandlers, visitorId } = useXbNovaInternal();
1137
1140
  const botId = options?.botId ?? auth.botId;
1138
1141
  const nudgeNodeId = options?.nudgeNodeId;
1139
1142
  const { apiKey, apiBaseUrl } = auth;
@@ -1142,6 +1145,9 @@ function useXbNovaChat(options) {
1142
1145
  const [busy, setBusy] = (0, import_react8.useState)(false);
1143
1146
  const [error, setError] = (0, import_react8.useState)(null);
1144
1147
  const [clientResults, setClientResults] = (0, import_react8.useState)({});
1148
+ const [feedback, setFeedbackState] = (0, import_react8.useState)({});
1149
+ const feedbackRef = (0, import_react8.useRef)(feedback);
1150
+ feedbackRef.current = feedback;
1145
1151
  const dispatchedClientActionsRef = (0, import_react8.useRef)(/* @__PURE__ */ new Set());
1146
1152
  const nudgeNodeIdRef = (0, import_react8.useRef)(nudgeNodeId);
1147
1153
  nudgeNodeIdRef.current = nudgeNodeId;
@@ -1150,6 +1156,7 @@ function useXbNovaChat(options) {
1150
1156
  setMessages([]);
1151
1157
  setError(null);
1152
1158
  setClientResults({});
1159
+ setFeedbackState({});
1153
1160
  dispatchedClientActionsRef.current = /* @__PURE__ */ new Set();
1154
1161
  fetch(`${apiBaseUrl}/v1/conversations`, {
1155
1162
  method: "POST",
@@ -1159,9 +1166,42 @@ function useXbNovaChat(options) {
1159
1166
  }, [apiBaseUrl, apiKey, botId]);
1160
1167
  const appendToLastAssistant = (existingParts, newParts) => setMessages((prev) => {
1161
1168
  const next = [...prev];
1162
- next[next.length - 1] = { role: "assistant", parts: [...existingParts, ...newParts] };
1169
+ next[next.length - 1] = { ...next[next.length - 1], role: "assistant", parts: [...existingParts, ...newParts] };
1170
+ return next;
1171
+ });
1172
+ const setLastAssistantId = (id) => setMessages((prev) => {
1173
+ if (prev.length === 0) return prev;
1174
+ const next = [...prev];
1175
+ next[next.length - 1] = { ...next[next.length - 1], id };
1163
1176
  return next;
1164
1177
  });
1178
+ async function setFeedback(messageId, rating) {
1179
+ const conversationId = onboarding?.conversation_id;
1180
+ if (!conversationId || !messageId) return;
1181
+ const prevRating = feedbackRef.current[messageId] ?? null;
1182
+ const apply = (r) => setFeedbackState((prev) => {
1183
+ const next = { ...prev };
1184
+ if (r === null) delete next[messageId];
1185
+ else next[messageId] = r;
1186
+ return next;
1187
+ });
1188
+ apply(rating);
1189
+ try {
1190
+ const res = await fetch(
1191
+ `${apiBaseUrl}/v1/conversations/${encodeURIComponent(conversationId)}/messages/${encodeURIComponent(messageId)}/feedback`,
1192
+ {
1193
+ method: "PUT",
1194
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
1195
+ body: JSON.stringify({ rating, visitor_id: visitorId })
1196
+ }
1197
+ );
1198
+ if (!res.ok) throw new Error(`${res.status}`);
1199
+ options?.onFeedback?.({ conversationId, messageId, rating });
1200
+ } catch (e) {
1201
+ console.warn("[xbnova] \uD53C\uB4DC\uBC31 \uC800\uC7A5 \uC2E4\uD328", e);
1202
+ apply(prevRating);
1203
+ }
1204
+ }
1165
1205
  async function runClientAction(spec, part, existingParts) {
1166
1206
  if (spec.dismissSheet) options?.onDismissSheet?.();
1167
1207
  let result;
@@ -1207,7 +1247,11 @@ function useXbNovaChat(options) {
1207
1247
  tool_call_id: part.toolCallId,
1208
1248
  result_text: resultText
1209
1249
  },
1210
- { onPart: (newParts) => appendToLastAssistant(existingParts, newParts), onError: setError }
1250
+ {
1251
+ onPart: (newParts) => appendToLastAssistant(existingParts, newParts),
1252
+ onMessageId: setLastAssistantId,
1253
+ onError: setError
1254
+ }
1211
1255
  );
1212
1256
  } catch (e) {
1213
1257
  setError(e instanceof Error ? e.message : String(e));
@@ -1240,7 +1284,7 @@ function useXbNovaChat(options) {
1240
1284
  ]);
1241
1285
  const replaceLast = (parts) => setMessages((prev) => {
1242
1286
  const next = [...prev];
1243
- next[next.length - 1] = { role: "assistant", parts };
1287
+ next[next.length - 1] = { ...next[next.length - 1], role: "assistant", parts };
1244
1288
  return next;
1245
1289
  });
1246
1290
  const focused = getFocusedTrackEntry(trackRegistry);
@@ -1256,7 +1300,7 @@ function useXbNovaChat(options) {
1256
1300
  context,
1257
1301
  nudge_node_id: nudgeNodeId ?? null
1258
1302
  },
1259
- { onPart: replaceLast, onError: setError }
1303
+ { onPart: replaceLast, onMessageId: setLastAssistantId, onError: setError }
1260
1304
  );
1261
1305
  } catch (e) {
1262
1306
  setError(e instanceof Error ? e.message : String(e));
@@ -1264,7 +1308,7 @@ function useXbNovaChat(options) {
1264
1308
  setBusy(false);
1265
1309
  }
1266
1310
  }
1267
- return { onboarding, messages, busy, error, send, clientResults };
1311
+ return { onboarding, messages, busy, error, send, clientResults, feedback, setFeedback };
1268
1312
  }
1269
1313
 
1270
1314
  // src/views.tsx
@@ -1524,7 +1568,9 @@ function XbNovaChat({
1524
1568
  defaultOpen = false,
1525
1569
  idleText = "AI\uC5D0\uAC8C \uC774 \uCC28 \uBB3C\uC5B4\uBCF4\uAE30",
1526
1570
  avatarSize = 28,
1527
- onVerdictAction
1571
+ onVerdictAction,
1572
+ feedback: feedbackEnabled = true,
1573
+ onFeedback
1528
1574
  }) {
1529
1575
  const { auth, trackRegistry, factsRegistry, visitorId, clientActionHandlers, theme } = useXbNovaInternal();
1530
1576
  const themeVars = {
@@ -1547,7 +1593,8 @@ function XbNovaChat({
1547
1593
  const [input, setInput] = (0, import_react9.useState)("");
1548
1594
  const bottomRef = (0, import_react9.useRef)(null);
1549
1595
  const typedHintText = useTypewriter(hint?.text ?? "", hint?.triggerNodeId ?? "");
1550
- const { onboarding, messages, busy, error, send, clientResults } = useXbNovaChat({
1596
+ const { onboarding, messages, busy, error, send, clientResults, feedback, setFeedback } = useXbNovaChat({
1597
+ onFeedback,
1551
1598
  botId: activeBotId ?? botId,
1552
1599
  nudgeNodeId: activeNudgeNodeId,
1553
1600
  onDismissSheet: () => setOpen(false)
@@ -1799,7 +1846,14 @@ function XbNovaChat({
1799
1846
  }
1800
1847
  return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ToolView, { part, viewRenderers, onVerdictAction }, j);
1801
1848
  }),
1802
- !hasVisiblePart(message.parts, clientActionHandlers, clientResults) && busy ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Bubble, { role: "assistant", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "text-neutral-400", children: "\uC0DD\uAC01\uD558\uB294 \uC911\u2026" }) }) : null
1849
+ !hasVisiblePart(message.parts, clientActionHandlers, clientResults) && busy ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Bubble, { role: "assistant", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "text-neutral-400", children: "\uC0DD\uAC01\uD558\uB294 \uC911\u2026" }) }) : null,
1850
+ feedbackEnabled && !busy && message.id && message.parts.some((p) => p.kind === "text" && p.text.trim()) ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1851
+ FeedbackButtons,
1852
+ {
1853
+ rating: feedback[message.id] ?? null,
1854
+ onChange: (r) => void setFeedback(message.id, r)
1855
+ }
1856
+ ) : null
1803
1857
  ] }) }, i)),
1804
1858
  error ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "rounded-lg bg-red-50 px-3 py-2 text-xs text-red-700 dark:bg-red-950 dark:text-red-300", children: error }) : null,
1805
1859
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { ref: bottomRef })
@@ -1901,6 +1955,47 @@ function Avatar({
1901
1955
  const text = branding?.avatar_text?.trim() || "AI";
1902
1956
  return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: base, style: { ...gradient, ...box, fontSize: Math.max(10, Math.round(size * 0.4)) }, children: text });
1903
1957
  }
1958
+ var THUMB_UP_PATH = "M7 10v12M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2a3.13 3.13 0 0 1 3 3.88Z";
1959
+ var THUMB_DOWN_PATH = "M17 14V2M9 18.12 10 14H4.17a2 2 0 0 1-1.92-2.56l2.33-8A2 2 0 0 1 6.5 2H20a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-2.76a2 2 0 0 0-1.79 1.11L12 22a3.13 3.13 0 0 1-3-3.88Z";
1960
+ function FeedbackButtons({
1961
+ rating,
1962
+ onChange
1963
+ }) {
1964
+ const item = (kind, label, path) => {
1965
+ const selected = rating === kind;
1966
+ const color = selected ? kind === "up" ? "var(--xbnova-accent-1, #4f46e5)" : "#dc2626" : void 0;
1967
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1968
+ "button",
1969
+ {
1970
+ type: "button",
1971
+ "aria-label": label,
1972
+ "aria-pressed": selected,
1973
+ onClick: () => onChange(selected ? null : kind),
1974
+ className: "flex h-6 w-6 items-center justify-center rounded-md text-neutral-400 transition-colors hover:text-neutral-600 dark:text-neutral-500 dark:hover:text-neutral-300",
1975
+ style: color ? { color } : void 0,
1976
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1977
+ "svg",
1978
+ {
1979
+ width: "16",
1980
+ height: "16",
1981
+ viewBox: "0 0 24 24",
1982
+ fill: selected ? "currentColor" : "none",
1983
+ stroke: "currentColor",
1984
+ strokeWidth: "2",
1985
+ strokeLinecap: "round",
1986
+ strokeLinejoin: "round",
1987
+ "aria-hidden": "true",
1988
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("path", { d: path })
1989
+ }
1990
+ )
1991
+ }
1992
+ );
1993
+ };
1994
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex items-center gap-0.5 pl-1", children: [
1995
+ item("up", "\uB3C4\uC6C0\uC774 \uB410\uC5B4\uC694", THUMB_UP_PATH),
1996
+ item("down", "\uB3C4\uC6C0\uC774 \uC548 \uB410\uC5B4\uC694", THUMB_DOWN_PATH)
1997
+ ] });
1998
+ }
1904
1999
  function Bubble({ role, children }) {
1905
2000
  const mine = role === "user";
1906
2001
  return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: mine ? "flex justify-end" : "flex justify-start", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(