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

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, reason) {
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, reason: rating === "down" ? reason ?? null : null })
1196
+ }
1197
+ );
1198
+ if (!res.ok) throw new Error(`${res.status}`);
1199
+ options?.onFeedback?.({ conversationId, messageId, rating, reason });
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)
@@ -1776,31 +1823,41 @@ function XbNovaChat({
1776
1823
  /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex-1 space-y-3 overflow-y-auto p-4", children: [
1777
1824
  showDisclosure ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "rounded-lg bg-neutral-50 px-3 py-2 text-[11px] text-neutral-400 dark:bg-neutral-800", children: "\uC9C0\uAE08 \uBCF4\uACE0 \uACC4\uC2E0 \uD654\uBA74\uACFC \uB9E4\uBB3C \uC815\uBCF4\uB97C \uAE30\uC900\uC73C\uB85C \uC548\uB0B4\uD574 \uB4DC\uB824\uC694." }) : null,
1778
1825
  firstBubble ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Bubble, { role: "assistant", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(AssistantMarkdown, { text: firstBubble }) }) : null,
1779
- messages.map((message, i) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { children: message.role === "user" ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Bubble, { role: "user", children: message.parts.map((p) => p.kind === "text" ? p.text : "").join("") }) : /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "space-y-1", children: [
1780
- message.parts.map((part, j) => {
1781
- if (part.kind === "text") {
1782
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Bubble, { role: "assistant", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(AssistantMarkdown, { text: part.text }) }, j);
1783
- }
1784
- if (clientActionHandlers.has(part.toolName)) {
1785
- const result = clientResults[part.toolCallId];
1786
- if (result) {
1787
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1788
- ToolView,
1789
- {
1790
- part: { ...part, state: "output", view: result.view, output: result.output },
1791
- viewRenderers,
1792
- onVerdictAction
1793
- },
1794
- j
1795
- );
1796
- }
1797
- const spec = clientActionHandlers.get(part.toolName);
1798
- return spec?.render ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { children: spec.render(part.input) }, j) : null;
1799
- }
1800
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ToolView, { part, viewRenderers, onVerdictAction }, j);
1801
- }),
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
1803
- ] }) }, i)),
1826
+ messages.map((message, i) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { children: message.role === "user" ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Bubble, { role: "user", children: message.parts.map((p) => p.kind === "text" ? p.text : "").join("") }) : /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
1827
+ AnswerActionsFrame,
1828
+ {
1829
+ enabled: feedbackEnabled && !busy && !!message.id && message.parts.some((p) => p.kind === "text" && p.text.trim()),
1830
+ text: message.parts.filter((p) => p.kind === "text").map((p) => p.kind === "text" ? p.text : "").join("\n\n"),
1831
+ rating: message.id ? feedback[message.id] ?? null : null,
1832
+ isLast: i === messages.length - 1,
1833
+ onRate: (r, reason) => void setFeedback(message.id, r, reason),
1834
+ children: [
1835
+ message.parts.map((part, j) => {
1836
+ if (part.kind === "text") {
1837
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Bubble, { role: "assistant", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(AssistantMarkdown, { text: part.text }) }, j);
1838
+ }
1839
+ if (clientActionHandlers.has(part.toolName)) {
1840
+ const result = clientResults[part.toolCallId];
1841
+ if (result) {
1842
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1843
+ ToolView,
1844
+ {
1845
+ part: { ...part, state: "output", view: result.view, output: result.output },
1846
+ viewRenderers,
1847
+ onVerdictAction
1848
+ },
1849
+ j
1850
+ );
1851
+ }
1852
+ const spec = clientActionHandlers.get(part.toolName);
1853
+ return spec?.render ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { children: spec.render(part.input) }, j) : null;
1854
+ }
1855
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ToolView, { part, viewRenderers, onVerdictAction }, j);
1856
+ }),
1857
+ !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
1858
+ ]
1859
+ }
1860
+ ) }, i)),
1804
1861
  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
1862
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { ref: bottomRef })
1806
1863
  ] }),
@@ -1901,6 +1958,196 @@ function Avatar({
1901
1958
  const text = branding?.avatar_text?.trim() || "AI";
1902
1959
  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
1960
  }
1961
+ 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";
1962
+ 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";
1963
+ var COPY_PATH = "M9 9h11v11H9zM5 15H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1";
1964
+ var CHECK_PATH = "m5 12 5 5L20 7";
1965
+ var FEEDBACK_REASONS = [
1966
+ { code: "inaccurate", label: "\uC815\uD655\uD558\uC9C0 \uC54A\uC544\uC694" },
1967
+ { code: "irrelevant", label: "\uC9C8\uBB38\uACFC \uB2EC\uB77C\uC694" },
1968
+ { code: "too_long", label: "\uB108\uBB34 \uAE38\uC5B4\uC694" },
1969
+ { code: "unclear", label: "\uC774\uD574\uD558\uAE30 \uC5B4\uB824\uC6CC\uC694" }
1970
+ ];
1971
+ var LONG_PRESS_MS = 420;
1972
+ function ActionIcon({ path, filled }) {
1973
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1974
+ "svg",
1975
+ {
1976
+ width: "16",
1977
+ height: "16",
1978
+ viewBox: "0 0 24 24",
1979
+ fill: filled ? "currentColor" : "none",
1980
+ stroke: "currentColor",
1981
+ strokeWidth: "2",
1982
+ strokeLinecap: "round",
1983
+ strokeLinejoin: "round",
1984
+ "aria-hidden": "true",
1985
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("path", { d: path })
1986
+ }
1987
+ );
1988
+ }
1989
+ async function copyText(text) {
1990
+ try {
1991
+ await navigator.clipboard.writeText(text);
1992
+ return true;
1993
+ } catch {
1994
+ try {
1995
+ const ta = document.createElement("textarea");
1996
+ ta.value = text;
1997
+ ta.style.position = "fixed";
1998
+ ta.style.opacity = "0";
1999
+ document.body.appendChild(ta);
2000
+ ta.select();
2001
+ const ok = document.execCommand("copy");
2002
+ ta.remove();
2003
+ return ok;
2004
+ } catch {
2005
+ return false;
2006
+ }
2007
+ }
2008
+ }
2009
+ function AnswerActions({
2010
+ text,
2011
+ rating,
2012
+ isLast,
2013
+ onRate,
2014
+ children
2015
+ }) {
2016
+ const rootRef = (0, import_react9.useRef)(null);
2017
+ const timerRef = (0, import_react9.useRef)(null);
2018
+ const [menuOpen, setMenuOpen] = (0, import_react9.useState)(false);
2019
+ const [reasonOpen, setReasonOpen] = (0, import_react9.useState)(false);
2020
+ const [copied, setCopied] = (0, import_react9.useState)(false);
2021
+ const [entered, setEntered] = (0, import_react9.useState)(false);
2022
+ const clearTimer = () => {
2023
+ if (timerRef.current) clearTimeout(timerRef.current);
2024
+ timerRef.current = null;
2025
+ };
2026
+ (0, import_react9.useEffect)(() => clearTimer, []);
2027
+ (0, import_react9.useEffect)(() => {
2028
+ if (!menuOpen) {
2029
+ setEntered(false);
2030
+ return;
2031
+ }
2032
+ const raf = requestAnimationFrame(() => setEntered(true));
2033
+ const onDown = (e) => {
2034
+ if (!e.composedPath().includes(rootRef.current)) setMenuOpen(false);
2035
+ };
2036
+ document.addEventListener("pointerdown", onDown, true);
2037
+ return () => {
2038
+ cancelAnimationFrame(raf);
2039
+ document.removeEventListener("pointerdown", onDown, true);
2040
+ };
2041
+ }, [menuOpen]);
2042
+ const startPress = (e) => {
2043
+ if (e.pointerType === "mouse" || isLast) return;
2044
+ clearTimer();
2045
+ timerRef.current = setTimeout(() => {
2046
+ setMenuOpen(true);
2047
+ if (typeof navigator.vibrate === "function") navigator.vibrate(10);
2048
+ }, LONG_PRESS_MS);
2049
+ };
2050
+ const handleCopy = async () => {
2051
+ if (await copyText(text)) {
2052
+ setCopied(true);
2053
+ setTimeout(() => setCopied(false), 1400);
2054
+ }
2055
+ };
2056
+ const handleRate = (kind) => {
2057
+ if (rating === kind) {
2058
+ onRate(null);
2059
+ setReasonOpen(false);
2060
+ return;
2061
+ }
2062
+ onRate(kind);
2063
+ setReasonOpen(kind === "down");
2064
+ if (kind === "up") setMenuOpen(false);
2065
+ };
2066
+ const barVisible = isLast || menuOpen;
2067
+ const floating = menuOpen && !isLast;
2068
+ const btn = (label, onClick, selected, color, icon) => /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
2069
+ "button",
2070
+ {
2071
+ type: "button",
2072
+ "aria-label": label,
2073
+ "aria-pressed": selected,
2074
+ onClick,
2075
+ className: `flex items-center justify-center gap-1 rounded-full text-neutral-400 transition-colors hover:text-neutral-600 dark:text-neutral-500 dark:hover:text-neutral-300 ${floating ? "h-9 px-3 text-xs font-semibold" : "h-7 w-7"}`,
2076
+ style: color ? { color } : void 0,
2077
+ children: [
2078
+ icon,
2079
+ floating ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { children: label }) : null
2080
+ ]
2081
+ }
2082
+ );
2083
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
2084
+ "div",
2085
+ {
2086
+ ref: rootRef,
2087
+ className: "group space-y-1",
2088
+ onPointerDown: startPress,
2089
+ onPointerUp: clearTimer,
2090
+ onPointerCancel: clearTimer,
2091
+ onPointerLeave: clearTimer,
2092
+ onPointerMove: (e) => {
2093
+ if (Math.abs(e.movementX) + Math.abs(e.movementY) > 6) clearTimer();
2094
+ },
2095
+ onContextMenu: (e) => {
2096
+ if (!isLast && e.nativeEvent.pointerType !== "mouse") e.preventDefault();
2097
+ },
2098
+ children: [
2099
+ children,
2100
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2101
+ "div",
2102
+ {
2103
+ className: `flex pl-1 ${barVisible ? "" : "hidden md:group-hover:flex"}`,
2104
+ style: { transition: "opacity 140ms, transform 140ms", opacity: floating && !entered ? 0 : 1, transform: floating && !entered ? "scale(0.94)" : "none" },
2105
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
2106
+ "div",
2107
+ {
2108
+ className: `flex items-center ${floating ? "gap-0.5 rounded-full border border-neutral-200 bg-white p-1 shadow-lg dark:border-neutral-700 dark:bg-neutral-800" : "gap-0.5"}`,
2109
+ children: [
2110
+ btn(copied ? "\uBCF5\uC0AC\uB428" : "\uBCF5\uC0AC", handleCopy, false, copied ? "var(--xbnova-accent-1, #4f46e5)" : void 0, /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ActionIcon, { path: copied ? CHECK_PATH : COPY_PATH })),
2111
+ btn(
2112
+ "\uC88B\uC544\uC694",
2113
+ () => handleRate("up"),
2114
+ rating === "up",
2115
+ rating === "up" ? "var(--xbnova-accent-1, #4f46e5)" : void 0,
2116
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ActionIcon, { path: THUMB_UP_PATH, filled: rating === "up" })
2117
+ ),
2118
+ btn(
2119
+ "\uBCC4\uB85C\uC608\uC694",
2120
+ () => handleRate("down"),
2121
+ rating === "down",
2122
+ rating === "down" ? "#dc2626" : void 0,
2123
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ActionIcon, { path: THUMB_DOWN_PATH, filled: rating === "down" })
2124
+ )
2125
+ ]
2126
+ }
2127
+ )
2128
+ }
2129
+ ),
2130
+ rating === "down" && reasonOpen ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex flex-wrap items-center gap-1.5 pl-1 pt-0.5", children: [
2131
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "text-[11px] text-neutral-400", children: "\uC5B4\uB5A4 \uC810\uC774 \uC544\uC26C\uC6E0\uB098\uC694?" }),
2132
+ FEEDBACK_REASONS.map((r) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2133
+ "button",
2134
+ {
2135
+ type: "button",
2136
+ onClick: () => {
2137
+ onRate("down", r.code);
2138
+ setReasonOpen(false);
2139
+ setMenuOpen(false);
2140
+ },
2141
+ className: "xbnova-chip rounded-full border px-2.5 py-1 text-[11px] font-semibold transition",
2142
+ children: r.label
2143
+ },
2144
+ r.code
2145
+ ))
2146
+ ] }) : null
2147
+ ]
2148
+ }
2149
+ );
2150
+ }
1904
2151
  function Bubble({ role, children }) {
1905
2152
  const mine = role === "user";
1906
2153
  return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: mine ? "flex justify-end" : "flex justify-start", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
@@ -1914,6 +2161,14 @@ function Bubble({ role, children }) {
1914
2161
  function AssistantMarkdown({ text }) {
1915
2162
  return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "space-y-1.5 text-sm leading-relaxed [&_ol]:list-decimal [&_ul]:list-disc [&_ol]:pl-5 [&_ul]:pl-5 [&_li]:pl-0.5 [&_strong]:font-semibold [&_a]:underline [&_code]:rounded [&_code]:bg-black/10 [&_code]:px-1 [&_code]:py-0.5 [&_code]:text-[0.85em] [&_p]:mb-1.5 last:[&_p]:mb-0", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_markdown.default, { remarkPlugins: [import_remark_gfm.default, import_remark_breaks.default], children: text }) });
1916
2163
  }
2164
+ function AnswerActionsFrame({
2165
+ enabled,
2166
+ children,
2167
+ ...rest
2168
+ }) {
2169
+ if (!enabled) return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "space-y-1", children });
2170
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(AnswerActions, { ...rest, children });
2171
+ }
1917
2172
 
1918
2173
  // src/facts.ts
1919
2174
  var import_react10 = require("react");