@xbrain-swe/nova-chat-widget 0.1.3 → 0.1.6

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
@@ -57,7 +57,8 @@ var import_react = require("react");
57
57
  var import_jsx_runtime = require("react/jsx-runtime");
58
58
  var DEFAULT_THEME = {
59
59
  accentFrom: "#4f46e5",
60
- accentTo: "#7c3aed"
60
+ accentTo: "#7c3aed",
61
+ colorScheme: "system"
61
62
  };
62
63
  var InternalContext = (0, import_react.createContext)(null);
63
64
  function useXbNovaInternal() {
@@ -933,6 +934,7 @@ function XbNovaProvider({
933
934
  {
934
935
  handler: spec.handler,
935
936
  awaitResult: spec.awaitResult ?? false,
937
+ timeoutMs: spec.timeoutMs,
936
938
  dismissSheet: spec.dismissSheet ?? false,
937
939
  render: spec.render
938
940
  }
@@ -968,10 +970,25 @@ function loadCss() {
968
970
  }
969
971
  return cssPromise;
970
972
  }
973
+ function useResolvedScheme(scheme) {
974
+ const [systemDark, setSystemDark] = (0, import_react7.useState)(false);
975
+ (0, import_react7.useEffect)(() => {
976
+ if (scheme !== "system") return;
977
+ const mq = window.matchMedia("(prefers-color-scheme: dark)");
978
+ setSystemDark(mq.matches);
979
+ const onChange = (e) => setSystemDark(e.matches);
980
+ mq.addEventListener("change", onChange);
981
+ return () => mq.removeEventListener("change", onChange);
982
+ }, [scheme]);
983
+ if (scheme === "system") return systemDark ? "dark" : "light";
984
+ return scheme;
985
+ }
971
986
  function XbNovaShadowRoot({
972
987
  children,
973
988
  style
974
989
  }) {
990
+ const { theme } = useXbNovaInternal();
991
+ const scheme = useResolvedScheme(theme.colorScheme);
975
992
  const hostRef = (0, import_react7.useRef)(null);
976
993
  const [mountNode, setMountNode] = (0, import_react7.useState)(null);
977
994
  const [portalTarget, setPortalTarget] = (0, import_react7.useState)(null);
@@ -993,6 +1010,11 @@ function XbNovaShadowRoot({
993
1010
  shadow.appendChild(mount);
994
1011
  setMountNode(mount);
995
1012
  }, [portalTarget]);
1013
+ (0, import_react7.useEffect)(() => {
1014
+ if (!mountNode) return;
1015
+ mountNode.dataset.xbnovaScheme = scheme;
1016
+ mountNode.style.colorScheme = scheme;
1017
+ }, [mountNode, scheme]);
996
1018
  if (!portalTarget) return null;
997
1019
  return (0, import_react_dom.createPortal)(
998
1020
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { ref: hostRef, style, children: mountNode ? (0, import_react_dom.createPortal)(children, mountNode) : null }),
@@ -1109,6 +1131,7 @@ async function resumeToolResult(apiBaseUrl, apiKey, body, handlers) {
1109
1131
  }
1110
1132
 
1111
1133
  // src/use-chat.ts
1134
+ var DEFAULT_CLIENT_ACTION_TIMEOUT_MS = 15e3;
1112
1135
  function useXbNovaChat(options) {
1113
1136
  const { auth, trackRegistry, clientActionHandlers } = useXbNovaInternal();
1114
1137
  const botId = options?.botId ?? auth.botId;
@@ -1118,6 +1141,7 @@ function useXbNovaChat(options) {
1118
1141
  const [messages, setMessages] = (0, import_react8.useState)([]);
1119
1142
  const [busy, setBusy] = (0, import_react8.useState)(false);
1120
1143
  const [error, setError] = (0, import_react8.useState)(null);
1144
+ const [clientResults, setClientResults] = (0, import_react8.useState)({});
1121
1145
  const dispatchedClientActionsRef = (0, import_react8.useRef)(/* @__PURE__ */ new Set());
1122
1146
  const nudgeNodeIdRef = (0, import_react8.useRef)(nudgeNodeId);
1123
1147
  nudgeNodeIdRef.current = nudgeNodeId;
@@ -1125,6 +1149,7 @@ function useXbNovaChat(options) {
1125
1149
  setOnboarding(null);
1126
1150
  setMessages([]);
1127
1151
  setError(null);
1152
+ setClientResults({});
1128
1153
  dispatchedClientActionsRef.current = /* @__PURE__ */ new Set();
1129
1154
  fetch(`${apiBaseUrl}/v1/conversations`, {
1130
1155
  method: "POST",
@@ -1141,7 +1166,20 @@ function useXbNovaChat(options) {
1141
1166
  if (spec.dismissSheet) options?.onDismissSheet?.();
1142
1167
  let result;
1143
1168
  try {
1144
- result = await spec.handler(part.input);
1169
+ if (!spec.awaitResult) {
1170
+ await spec.handler(part.input);
1171
+ return;
1172
+ }
1173
+ const timeoutMs = spec.timeoutMs ?? DEFAULT_CLIENT_ACTION_TIMEOUT_MS;
1174
+ let timer;
1175
+ const timeout = new Promise((resolve) => {
1176
+ timer = setTimeout(() => resolve("\uC624\uB958: \uC751\uB2F5 \uC2DC\uAC04\uC774 \uCD08\uACFC\uB418\uC5C8\uC2B5\uB2C8\uB2E4."), timeoutMs);
1177
+ });
1178
+ try {
1179
+ result = await Promise.race([Promise.resolve(spec.handler(part.input)), timeout]);
1180
+ } finally {
1181
+ clearTimeout(timer);
1182
+ }
1145
1183
  } catch (e) {
1146
1184
  if (!spec.awaitResult) return;
1147
1185
  result = e instanceof Error ? `\uC624\uB958: ${e.message}` : "\uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4.";
@@ -1149,7 +1187,15 @@ function useXbNovaChat(options) {
1149
1187
  if (!spec.awaitResult) return;
1150
1188
  const conversationId = onboarding?.conversation_id;
1151
1189
  if (!conversationId) return;
1152
- const resultText = typeof result === "string" && result.trim() ? result : "\uD578\uB4E4\uB7EC\uAC00 \uACB0\uACFC\uB97C \uBC18\uD658\uD558\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.";
1190
+ const structured = result && typeof result === "object" ? result : null;
1191
+ if (structured?.view) {
1192
+ setClientResults((prev) => ({
1193
+ ...prev,
1194
+ [part.toolCallId]: { view: structured.view, output: structured.output }
1195
+ }));
1196
+ }
1197
+ const rawText = structured ? structured.text : result;
1198
+ const resultText = typeof rawText === "string" && rawText.trim() ? rawText : "\uD578\uB4E4\uB7EC\uAC00 \uACB0\uACFC\uB97C \uBC18\uD658\uD558\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.";
1153
1199
  setBusy(true);
1154
1200
  try {
1155
1201
  await resumeToolResult(
@@ -1218,7 +1264,7 @@ function useXbNovaChat(options) {
1218
1264
  setBusy(false);
1219
1265
  }
1220
1266
  }
1221
- return { onboarding, messages, busy, error, send };
1267
+ return { onboarding, messages, busy, error, send, clientResults };
1222
1268
  }
1223
1269
 
1224
1270
  // src/views.tsx
@@ -1341,9 +1387,10 @@ function ToolView({
1341
1387
 
1342
1388
  // src/chat.tsx
1343
1389
  var import_jsx_runtime7 = require("react/jsx-runtime");
1344
- function hasVisiblePart(parts, clientActionHandlers) {
1390
+ function hasVisiblePart(parts, clientActionHandlers, clientResults) {
1345
1391
  return parts.some((part) => {
1346
1392
  if (part.kind === "text") return true;
1393
+ if (clientResults[part.toolCallId]) return true;
1347
1394
  const spec = clientActionHandlers.get(part.toolName);
1348
1395
  return spec ? Boolean(spec.render) : true;
1349
1396
  });
@@ -1498,7 +1545,7 @@ function XbNovaChat({
1498
1545
  const [input, setInput] = (0, import_react9.useState)("");
1499
1546
  const bottomRef = (0, import_react9.useRef)(null);
1500
1547
  const typedHintText = useTypewriter(hint?.text ?? "", hint?.triggerNodeId ?? "");
1501
- const { onboarding, messages, busy, error, send } = useXbNovaChat({
1548
+ const { onboarding, messages, busy, error, send, clientResults } = useXbNovaChat({
1502
1549
  botId: activeBotId ?? botId,
1503
1550
  nudgeNodeId: activeNudgeNodeId,
1504
1551
  onDismissSheet: () => setOpen(false)
@@ -1628,11 +1675,11 @@ function XbNovaChat({
1628
1675
  className: "flex shrink-0 items-center justify-center",
1629
1676
  style: { width: collapsedSize, height: collapsedSize },
1630
1677
  children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1631
- "span",
1678
+ Avatar,
1632
1679
  {
1633
- className: "flex h-7 w-7 items-center justify-center rounded-full text-[11px] font-bold text-white shadow-md",
1634
- style: avatarGradientStyle,
1635
- children: "AI"
1680
+ branding: onboarding?.branding,
1681
+ className: "h-7 w-7 text-[11px] shadow-md",
1682
+ gradient: avatarGradientStyle
1636
1683
  }
1637
1684
  )
1638
1685
  }
@@ -1676,11 +1723,11 @@ function XbNovaChat({
1676
1723
  children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex h-full w-full flex-col rounded-t-[20px] bg-white shadow-2xl sm:rounded-2xl sm:border sm:border-neutral-200 dark:bg-neutral-900 sm:dark:border-neutral-700", children: [
1677
1724
  /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("header", { className: "flex items-center gap-2 border-b border-neutral-200 px-3.5 py-2.5 dark:border-neutral-700", children: [
1678
1725
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1679
- "span",
1726
+ Avatar,
1680
1727
  {
1681
- className: "flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-[10px] font-bold text-white",
1682
- style: avatarGradientStyle,
1683
- children: "AI"
1728
+ branding: onboarding?.branding,
1729
+ className: "h-6 w-6 shrink-0 text-[10px]",
1730
+ gradient: avatarGradientStyle
1684
1731
  }
1685
1732
  ),
1686
1733
  /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "min-w-0 flex-1", children: [
@@ -1707,12 +1754,24 @@ function XbNovaChat({
1707
1754
  return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Bubble, { role: "assistant", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(AssistantMarkdown, { text: part.text }) }, j);
1708
1755
  }
1709
1756
  if (clientActionHandlers.has(part.toolName)) {
1757
+ const result = clientResults[part.toolCallId];
1758
+ if (result) {
1759
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1760
+ ToolView,
1761
+ {
1762
+ part: { ...part, state: "output", view: result.view, output: result.output },
1763
+ viewRenderers,
1764
+ onVerdictAction
1765
+ },
1766
+ j
1767
+ );
1768
+ }
1710
1769
  const spec = clientActionHandlers.get(part.toolName);
1711
1770
  return spec?.render ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { children: spec.render(part.input) }, j) : null;
1712
1771
  }
1713
1772
  return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ToolView, { part, viewRenderers, onVerdictAction }, j);
1714
1773
  }),
1715
- !hasVisiblePart(message.parts, clientActionHandlers) && 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
1774
+ !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
1716
1775
  ] }) }, i)),
1717
1776
  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,
1718
1777
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { ref: bottomRef })
@@ -1783,6 +1842,34 @@ function XbNovaChat({
1783
1842
  )
1784
1843
  ] });
1785
1844
  }
1845
+ function Avatar({
1846
+ branding,
1847
+ className,
1848
+ gradient
1849
+ }) {
1850
+ const logoUrl = branding?.logo_url?.trim() ?? "";
1851
+ const [failedUrl, setFailedUrl] = (0, import_react9.useState)("");
1852
+ if (logoUrl && failedUrl !== logoUrl) {
1853
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1854
+ "img",
1855
+ {
1856
+ src: logoUrl,
1857
+ alt: "",
1858
+ onError: () => setFailedUrl(logoUrl),
1859
+ className: `${className} flex-none rounded-full bg-white object-cover`
1860
+ }
1861
+ );
1862
+ }
1863
+ const text = branding?.avatar_text?.trim() || "AI";
1864
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1865
+ "span",
1866
+ {
1867
+ className: `${className} flex items-center justify-center rounded-full font-bold text-white`,
1868
+ style: gradient,
1869
+ children: text
1870
+ }
1871
+ );
1872
+ }
1786
1873
  function Bubble({ role, children }) {
1787
1874
  const mine = role === "user";
1788
1875
  return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: mine ? "flex justify-end" : "flex justify-start", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(