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

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
@@ -38,6 +38,7 @@ __export(index_exports, {
38
38
  XbNovaTrack: () => XbNovaTrack,
39
39
  trackXbNovaEvent: () => trackXbNovaEvent,
40
40
  useXbNovaChat: () => useXbNovaChat,
41
+ useXbNovaContext: () => useXbNovaContext,
41
42
  useXbNovaFact: () => useXbNovaFact,
42
43
  useXbNovaFacts: () => useXbNovaFacts,
43
44
  useXbNovaSettings: () => useXbNovaSettings,
@@ -86,6 +87,17 @@ function resolveVisitorId() {
86
87
  function resolveFactValue(value) {
87
88
  return typeof value === "function" ? value() : value;
88
89
  }
90
+ function collectContextEntries(registry) {
91
+ const out = [];
92
+ for (const source of registry.values()) {
93
+ const entries = typeof source === "function" ? source() : source;
94
+ if (!Array.isArray(entries)) continue;
95
+ for (const e of entries) {
96
+ if (e && typeof e.title === "string" && typeof e.description === "string") out.push(e);
97
+ }
98
+ }
99
+ return out;
100
+ }
89
101
  function InternalProvider({
90
102
  auth,
91
103
  clientActionHandlers,
@@ -94,6 +106,7 @@ function InternalProvider({
94
106
  }) {
95
107
  const trackRegistry = (0, import_react.useRef)(/* @__PURE__ */ new Map()).current;
96
108
  const factsRegistry = (0, import_react.useRef)(/* @__PURE__ */ new Map()).current;
109
+ const contextRegistry = (0, import_react.useRef)(/* @__PURE__ */ new Map()).current;
97
110
  const emptyHandlers = (0, import_react.useRef)(/* @__PURE__ */ new Map()).current;
98
111
  const visitorIdRef = (0, import_react.useRef)(null);
99
112
  if (visitorIdRef.current === null) {
@@ -104,11 +117,12 @@ function InternalProvider({
104
117
  auth,
105
118
  trackRegistry,
106
119
  factsRegistry,
120
+ contextRegistry,
107
121
  clientActionHandlers: clientActionHandlers ?? emptyHandlers,
108
122
  visitorId: visitorIdRef.current,
109
123
  theme
110
124
  }),
111
- [auth, trackRegistry, factsRegistry, clientActionHandlers, emptyHandlers, theme]
125
+ [auth, trackRegistry, factsRegistry, contextRegistry, clientActionHandlers, emptyHandlers, theme]
112
126
  );
113
127
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(InternalContext.Provider, { value, children });
114
128
  }
@@ -1136,7 +1150,7 @@ async function resumeToolResult(apiBaseUrl, apiKey, body, handlers) {
1136
1150
  // src/use-chat.ts
1137
1151
  var DEFAULT_CLIENT_ACTION_TIMEOUT_MS = 15e3;
1138
1152
  function useXbNovaChat(options) {
1139
- const { auth, trackRegistry, clientActionHandlers, visitorId } = useXbNovaInternal();
1153
+ const { auth, trackRegistry, contextRegistry, clientActionHandlers, visitorId } = useXbNovaInternal();
1140
1154
  const botId = options?.botId ?? auth.botId;
1141
1155
  const nudgeNodeId = options?.nudgeNodeId;
1142
1156
  const { apiKey, apiBaseUrl } = auth;
@@ -1175,7 +1189,7 @@ function useXbNovaChat(options) {
1175
1189
  next[next.length - 1] = { ...next[next.length - 1], id };
1176
1190
  return next;
1177
1191
  });
1178
- async function setFeedback(messageId, rating) {
1192
+ async function setFeedback(messageId, rating, reason) {
1179
1193
  const conversationId = onboarding?.conversation_id;
1180
1194
  if (!conversationId || !messageId) return;
1181
1195
  const prevRating = feedbackRef.current[messageId] ?? null;
@@ -1192,11 +1206,11 @@ function useXbNovaChat(options) {
1192
1206
  {
1193
1207
  method: "PUT",
1194
1208
  headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
1195
- body: JSON.stringify({ rating, visitor_id: visitorId })
1209
+ body: JSON.stringify({ rating, visitor_id: visitorId, reason: rating === "down" ? reason ?? null : null })
1196
1210
  }
1197
1211
  );
1198
1212
  if (!res.ok) throw new Error(`${res.status}`);
1199
- options?.onFeedback?.({ conversationId, messageId, rating });
1213
+ options?.onFeedback?.({ conversationId, messageId, rating, reason });
1200
1214
  } catch (e) {
1201
1215
  console.warn("[xbnova] \uD53C\uB4DC\uBC31 \uC800\uC7A5 \uC2E4\uD328", e);
1202
1216
  apply(prevRating);
@@ -1288,7 +1302,9 @@ function useXbNovaChat(options) {
1288
1302
  return next;
1289
1303
  });
1290
1304
  const focused = getFocusedTrackEntry(trackRegistry);
1291
- const context = focused ? { region: focused.region, data: resolveFactValue(focused.data) } : void 0;
1305
+ const notes = collectContextEntries(contextRegistry);
1306
+ const screen = focused ? { region: focused.region, data: resolveFactValue(focused.data) } : {};
1307
+ const context = focused || notes.length ? { ...screen, ...notes.length ? { notes } : {} } : void 0;
1292
1308
  try {
1293
1309
  await streamChat(
1294
1310
  apiBaseUrl,
@@ -1298,7 +1314,8 @@ function useXbNovaChat(options) {
1298
1314
  message: trimmed,
1299
1315
  conversation_id: onboarding?.conversation_id ?? null,
1300
1316
  context,
1301
- nudge_node_id: nudgeNodeId ?? null
1317
+ nudge_node_id: nudgeNodeId ?? null,
1318
+ opening_message: messages.length === 0 ? (options?.openingMessage ?? onboarding?.greeting) || null : null
1302
1319
  },
1303
1320
  { onPart: replaceLast, onMessageId: setLastAssistantId, onError: setError }
1304
1321
  );
@@ -1597,6 +1614,7 @@ function XbNovaChat({
1597
1614
  onFeedback,
1598
1615
  botId: activeBotId ?? botId,
1599
1616
  nudgeNodeId: activeNudgeNodeId,
1617
+ openingMessage: openingLine ?? void 0,
1600
1618
  onDismissSheet: () => setOpen(false)
1601
1619
  });
1602
1620
  const effectiveBotId = activeBotId ?? botId;
@@ -1823,38 +1841,41 @@ function XbNovaChat({
1823
1841
  /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex-1 space-y-3 overflow-y-auto p-4", children: [
1824
1842
  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,
1825
1843
  firstBubble ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Bubble, { role: "assistant", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(AssistantMarkdown, { text: firstBubble }) }) : null,
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)("div", { className: "space-y-1", children: [
1827
- message.parts.map((part, j) => {
1828
- if (part.kind === "text") {
1829
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Bubble, { role: "assistant", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(AssistantMarkdown, { text: part.text }) }, j);
1830
- }
1831
- if (clientActionHandlers.has(part.toolName)) {
1832
- const result = clientResults[part.toolCallId];
1833
- if (result) {
1834
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1835
- ToolView,
1836
- {
1837
- part: { ...part, state: "output", view: result.view, output: result.output },
1838
- viewRenderers,
1839
- onVerdictAction
1840
- },
1841
- j
1842
- );
1843
- }
1844
- const spec = clientActionHandlers.get(part.toolName);
1845
- return spec?.render ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { children: spec.render(part.input) }, j) : null;
1846
- }
1847
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ToolView, { part, viewRenderers, onVerdictAction }, j);
1848
- }),
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
1857
- ] }) }, i)),
1844
+ 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)(
1845
+ AnswerActionsFrame,
1846
+ {
1847
+ enabled: feedbackEnabled && !busy && !!message.id && message.parts.some((p) => p.kind === "text" && p.text.trim()),
1848
+ text: message.parts.filter((p) => p.kind === "text").map((p) => p.kind === "text" ? p.text : "").join("\n\n"),
1849
+ rating: message.id ? feedback[message.id] ?? null : null,
1850
+ isLast: i === messages.length - 1,
1851
+ onRate: (r, reason) => void setFeedback(message.id, r, reason),
1852
+ children: [
1853
+ message.parts.map((part, j) => {
1854
+ if (part.kind === "text") {
1855
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Bubble, { role: "assistant", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(AssistantMarkdown, { text: part.text }) }, j);
1856
+ }
1857
+ if (clientActionHandlers.has(part.toolName)) {
1858
+ const result = clientResults[part.toolCallId];
1859
+ if (result) {
1860
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1861
+ ToolView,
1862
+ {
1863
+ part: { ...part, state: "output", view: result.view, output: result.output },
1864
+ viewRenderers,
1865
+ onVerdictAction
1866
+ },
1867
+ j
1868
+ );
1869
+ }
1870
+ const spec = clientActionHandlers.get(part.toolName);
1871
+ return spec?.render ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { children: spec.render(part.input) }, j) : null;
1872
+ }
1873
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ToolView, { part, viewRenderers, onVerdictAction }, j);
1874
+ }),
1875
+ !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
1876
+ ]
1877
+ }
1878
+ ) }, i)),
1858
1879
  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,
1859
1880
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { ref: bottomRef })
1860
1881
  ] }),
@@ -1957,44 +1978,193 @@ function Avatar({
1957
1978
  }
1958
1979
  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
1980
  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({
1981
+ var COPY_PATH = "M9 9h11v11H9zM5 15H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1";
1982
+ var CHECK_PATH = "m5 12 5 5L20 7";
1983
+ var FEEDBACK_REASONS = [
1984
+ { code: "inaccurate", label: "\uC815\uD655\uD558\uC9C0 \uC54A\uC544\uC694" },
1985
+ { code: "irrelevant", label: "\uC9C8\uBB38\uACFC \uB2EC\uB77C\uC694" },
1986
+ { code: "too_long", label: "\uB108\uBB34 \uAE38\uC5B4\uC694" },
1987
+ { code: "unclear", label: "\uC774\uD574\uD558\uAE30 \uC5B4\uB824\uC6CC\uC694" }
1988
+ ];
1989
+ var LONG_PRESS_MS = 420;
1990
+ function ActionIcon({ path, filled }) {
1991
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1992
+ "svg",
1993
+ {
1994
+ width: "16",
1995
+ height: "16",
1996
+ viewBox: "0 0 24 24",
1997
+ fill: filled ? "currentColor" : "none",
1998
+ stroke: "currentColor",
1999
+ strokeWidth: "2",
2000
+ strokeLinecap: "round",
2001
+ strokeLinejoin: "round",
2002
+ "aria-hidden": "true",
2003
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("path", { d: path })
2004
+ }
2005
+ );
2006
+ }
2007
+ async function copyText(text) {
2008
+ try {
2009
+ await navigator.clipboard.writeText(text);
2010
+ return true;
2011
+ } catch {
2012
+ try {
2013
+ const ta = document.createElement("textarea");
2014
+ ta.value = text;
2015
+ ta.style.position = "fixed";
2016
+ ta.style.opacity = "0";
2017
+ document.body.appendChild(ta);
2018
+ ta.select();
2019
+ const ok = document.execCommand("copy");
2020
+ ta.remove();
2021
+ return ok;
2022
+ } catch {
2023
+ return false;
2024
+ }
2025
+ }
2026
+ }
2027
+ function AnswerActions({
2028
+ text,
1961
2029
  rating,
1962
- onChange
2030
+ isLast,
2031
+ onRate,
2032
+ children
1963
2033
  }) {
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",
2034
+ const rootRef = (0, import_react9.useRef)(null);
2035
+ const timerRef = (0, import_react9.useRef)(null);
2036
+ const [menuOpen, setMenuOpen] = (0, import_react9.useState)(false);
2037
+ const [reasonOpen, setReasonOpen] = (0, import_react9.useState)(false);
2038
+ const [copied, setCopied] = (0, import_react9.useState)(false);
2039
+ const [entered, setEntered] = (0, import_react9.useState)(false);
2040
+ const clearTimer = () => {
2041
+ if (timerRef.current) clearTimeout(timerRef.current);
2042
+ timerRef.current = null;
2043
+ };
2044
+ (0, import_react9.useEffect)(() => clearTimer, []);
2045
+ (0, import_react9.useEffect)(() => {
2046
+ if (!menuOpen) {
2047
+ setEntered(false);
2048
+ return;
2049
+ }
2050
+ const raf = requestAnimationFrame(() => setEntered(true));
2051
+ const onDown = (e) => {
2052
+ if (!e.composedPath().includes(rootRef.current)) setMenuOpen(false);
2053
+ };
2054
+ document.addEventListener("pointerdown", onDown, true);
2055
+ return () => {
2056
+ cancelAnimationFrame(raf);
2057
+ document.removeEventListener("pointerdown", onDown, true);
2058
+ };
2059
+ }, [menuOpen]);
2060
+ const startPress = (e) => {
2061
+ if (e.pointerType === "mouse" || isLast) return;
2062
+ clearTimer();
2063
+ timerRef.current = setTimeout(() => {
2064
+ setMenuOpen(true);
2065
+ if (typeof navigator.vibrate === "function") navigator.vibrate(10);
2066
+ }, LONG_PRESS_MS);
2067
+ };
2068
+ const handleCopy = async () => {
2069
+ if (await copyText(text)) {
2070
+ setCopied(true);
2071
+ setTimeout(() => setCopied(false), 1400);
2072
+ }
2073
+ };
2074
+ const handleRate = (kind) => {
2075
+ if (rating === kind) {
2076
+ onRate(null);
2077
+ setReasonOpen(false);
2078
+ return;
2079
+ }
2080
+ onRate(kind);
2081
+ setReasonOpen(kind === "down");
2082
+ if (kind === "up") setMenuOpen(false);
2083
+ };
2084
+ const barVisible = isLast || menuOpen;
2085
+ const floating = menuOpen && !isLast;
2086
+ const btn = (label, onClick, selected, color, icon) => /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
2087
+ "button",
2088
+ {
2089
+ type: "button",
2090
+ "aria-label": label,
2091
+ "aria-pressed": selected,
2092
+ onClick,
2093
+ 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"}`,
2094
+ style: color ? { color } : void 0,
2095
+ children: [
2096
+ icon,
2097
+ floating ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { children: label }) : null
2098
+ ]
2099
+ }
2100
+ );
2101
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
2102
+ "div",
2103
+ {
2104
+ ref: rootRef,
2105
+ className: "group space-y-1",
2106
+ onPointerDown: startPress,
2107
+ onPointerUp: clearTimer,
2108
+ onPointerCancel: clearTimer,
2109
+ onPointerLeave: clearTimer,
2110
+ onPointerMove: (e) => {
2111
+ if (Math.abs(e.movementX) + Math.abs(e.movementY) > 6) clearTimer();
2112
+ },
2113
+ onContextMenu: (e) => {
2114
+ if (!isLast && e.nativeEvent.pointerType !== "mouse") e.preventDefault();
2115
+ },
2116
+ children: [
2117
+ children,
2118
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2119
+ "div",
1978
2120
  {
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 })
2121
+ className: `flex pl-1 ${barVisible ? "" : "hidden md:group-hover:flex"}`,
2122
+ style: { transition: "opacity 140ms, transform 140ms", opacity: floating && !entered ? 0 : 1, transform: floating && !entered ? "scale(0.94)" : "none" },
2123
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
2124
+ "div",
2125
+ {
2126
+ 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"}`,
2127
+ children: [
2128
+ 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 })),
2129
+ btn(
2130
+ "\uC88B\uC544\uC694",
2131
+ () => handleRate("up"),
2132
+ rating === "up",
2133
+ rating === "up" ? "var(--xbnova-accent-1, #4f46e5)" : void 0,
2134
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ActionIcon, { path: THUMB_UP_PATH, filled: rating === "up" })
2135
+ ),
2136
+ btn(
2137
+ "\uBCC4\uB85C\uC608\uC694",
2138
+ () => handleRate("down"),
2139
+ rating === "down",
2140
+ rating === "down" ? "#dc2626" : void 0,
2141
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ActionIcon, { path: THUMB_DOWN_PATH, filled: rating === "down" })
2142
+ )
2143
+ ]
2144
+ }
2145
+ )
1989
2146
  }
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
- ] });
2147
+ ),
2148
+ 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: [
2149
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "text-[11px] text-neutral-400", children: "\uC5B4\uB5A4 \uC810\uC774 \uC544\uC26C\uC6E0\uB098\uC694?" }),
2150
+ FEEDBACK_REASONS.map((r) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2151
+ "button",
2152
+ {
2153
+ type: "button",
2154
+ onClick: () => {
2155
+ onRate("down", r.code);
2156
+ setReasonOpen(false);
2157
+ setMenuOpen(false);
2158
+ },
2159
+ className: "xbnova-chip rounded-full border px-2.5 py-1 text-[11px] font-semibold transition",
2160
+ children: r.label
2161
+ },
2162
+ r.code
2163
+ ))
2164
+ ] }) : null
2165
+ ]
2166
+ }
2167
+ );
1998
2168
  }
1999
2169
  function Bubble({ role, children }) {
2000
2170
  const mine = role === "user";
@@ -2009,6 +2179,14 @@ function Bubble({ role, children }) {
2009
2179
  function AssistantMarkdown({ text }) {
2010
2180
  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 }) });
2011
2181
  }
2182
+ function AnswerActionsFrame({
2183
+ enabled,
2184
+ children,
2185
+ ...rest
2186
+ }) {
2187
+ if (!enabled) return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "space-y-1", children });
2188
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(AnswerActions, { ...rest, children });
2189
+ }
2012
2190
 
2013
2191
  // src/facts.ts
2014
2192
  var import_react10 = require("react");
@@ -2040,6 +2218,18 @@ function useXbNovaFacts(values) {
2040
2218
  };
2041
2219
  }, [factsRegistry]);
2042
2220
  }
2221
+ function useXbNovaContext(entries) {
2222
+ const { contextRegistry } = useXbNovaInternal();
2223
+ const id = (0, import_react10.useId)();
2224
+ (0, import_react10.useEffect)(() => {
2225
+ contextRegistry.set(id, entries);
2226
+ });
2227
+ (0, import_react10.useEffect)(() => {
2228
+ return () => {
2229
+ contextRegistry.delete(id);
2230
+ };
2231
+ }, [id, contextRegistry]);
2232
+ }
2043
2233
  function useXbNovaVisitor() {
2044
2234
  const { visitorId } = useXbNovaInternal();
2045
2235
  return visitorId;
@@ -2061,6 +2251,7 @@ function XbNovaFacts({ values }) {
2061
2251
  XbNovaTrack,
2062
2252
  trackXbNovaEvent,
2063
2253
  useXbNovaChat,
2254
+ useXbNovaContext,
2064
2255
  useXbNovaFact,
2065
2256
  useXbNovaFacts,
2066
2257
  useXbNovaSettings,