@xbrain-swe/nova-chat-widget 0.1.7 → 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,11 +1593,34 @@ 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)
1554
1601
  });
1602
+ const effectiveBotId = activeBotId ?? botId;
1603
+ const [cachedBranding, setCachedBranding] = (0, import_react9.useState)(null);
1604
+ (0, import_react9.useEffect)(() => {
1605
+ try {
1606
+ const raw = window.localStorage.getItem(`xbnova_branding:${effectiveBotId}`);
1607
+ setCachedBranding(raw ? JSON.parse(raw) : null);
1608
+ } catch {
1609
+ setCachedBranding(null);
1610
+ }
1611
+ }, [effectiveBotId]);
1612
+ (0, import_react9.useEffect)(() => {
1613
+ if (!onboarding?.branding) return;
1614
+ try {
1615
+ window.localStorage.setItem(
1616
+ `xbnova_branding:${effectiveBotId}`,
1617
+ JSON.stringify({ logo_url: onboarding.branding.logo_url, avatar_text: onboarding.branding.avatar_text })
1618
+ );
1619
+ } catch {
1620
+ }
1621
+ }, [onboarding?.branding, effectiveBotId]);
1622
+ const branding = onboarding?.branding ?? cachedBranding ?? void 0;
1623
+ const brandingReady = Boolean(onboarding) || cachedBranding !== null || Boolean(error);
1555
1624
  (0, import_react9.useEffect)(() => {
1556
1625
  bottomRef.current?.scrollIntoView({ behavior: "smooth" });
1557
1626
  }, [messages]);
@@ -1679,7 +1748,8 @@ function XbNovaChat({
1679
1748
  children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1680
1749
  Avatar,
1681
1750
  {
1682
- branding: onboarding?.branding,
1751
+ branding,
1752
+ ready: brandingReady,
1683
1753
  size: avatarSize,
1684
1754
  className: "shadow-md",
1685
1755
  gradient: avatarGradientStyle
@@ -1728,7 +1798,8 @@ function XbNovaChat({
1728
1798
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1729
1799
  Avatar,
1730
1800
  {
1731
- branding: onboarding?.branding,
1801
+ branding,
1802
+ ready: brandingReady,
1732
1803
  size: headerAvatarSize,
1733
1804
  className: "shrink-0",
1734
1805
  gradient: avatarGradientStyle
@@ -1775,7 +1846,14 @@ function XbNovaChat({
1775
1846
  }
1776
1847
  return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ToolView, { part, viewRenderers, onVerdictAction }, j);
1777
1848
  }),
1778
- !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
1779
1857
  ] }) }, i)),
1780
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,
1781
1859
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { ref: bottomRef })
@@ -1848,6 +1926,7 @@ function XbNovaChat({
1848
1926
  }
1849
1927
  function Avatar({
1850
1928
  branding,
1929
+ ready,
1851
1930
  size,
1852
1931
  className = "",
1853
1932
  gradient
@@ -1855,27 +1934,67 @@ function Avatar({
1855
1934
  const box = { width: size, height: size };
1856
1935
  const logoUrl = branding?.logo_url?.trim() ?? "";
1857
1936
  const [failedUrl, setFailedUrl] = (0, import_react9.useState)("");
1937
+ const [loadedUrl, setLoadedUrl] = (0, import_react9.useState)("");
1938
+ const base = `${className} relative flex flex-none items-center justify-center overflow-hidden rounded-full font-bold text-white`;
1939
+ if (!ready) {
1940
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: base, style: { ...gradient, ...box }, "aria-hidden": true });
1941
+ }
1858
1942
  if (logoUrl && failedUrl !== logoUrl) {
1859
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1943
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: base, style: { ...gradient, ...box }, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1860
1944
  "img",
1861
1945
  {
1862
1946
  src: logoUrl,
1863
1947
  alt: "",
1948
+ onLoad: () => setLoadedUrl(logoUrl),
1864
1949
  onError: () => setFailedUrl(logoUrl),
1865
- style: box,
1866
- className: `${className} flex-none rounded-full bg-white object-cover`
1950
+ className: "absolute inset-0 h-full w-full rounded-full bg-white object-cover transition-opacity duration-200",
1951
+ style: { opacity: loadedUrl === logoUrl ? 1 : 0 }
1867
1952
  }
1868
- );
1953
+ ) });
1869
1954
  }
1870
1955
  const text = branding?.avatar_text?.trim() || "AI";
1871
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1872
- "span",
1873
- {
1874
- className: `${className} flex flex-none items-center justify-center rounded-full font-bold text-white`,
1875
- style: { ...gradient, ...box, fontSize: Math.max(10, Math.round(size * 0.4)) },
1876
- children: text
1877
- }
1878
- );
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 });
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
+ ] });
1879
1998
  }
1880
1999
  function Bubble({ role, children }) {
1881
2000
  const mine = role === "user";