@xbrain-swe/nova-chat-widget 0.1.9 β†’ 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.d.cts CHANGED
@@ -116,11 +116,15 @@ type ClientResults = Record<string, {
116
116
  output: unknown;
117
117
  }>;
118
118
  type XbNovaFeedbackRating = "up" | "down" | null;
119
+ /** πŸ‘Ž μ‚¬μœ  칩의 μ½”λ“œ. */
120
+ type XbNovaFeedbackReason = "inaccurate" | "irrelevant" | "too_long" | "unclear";
119
121
  /** λ‹΅λ³€ λ§Œμ‘±λ„ μ €μž₯ 성곡 ν›„ 호슀트둜 μ „λ‹¬λ˜λŠ” 이벀트. `rating: null`은 평가 μ·¨μ†Œ. */
120
122
  type XbNovaFeedbackEvent = {
121
123
  conversationId: string;
122
124
  messageId: string;
123
125
  rating: XbNovaFeedbackRating;
126
+ /** πŸ‘ŽμΌ λ•Œ μ‚¬μš©μžκ°€ κ³ λ₯Έ μ‚¬μœ  μ½”λ“œ(μ—†μœΌλ©΄ undefined). */
127
+ reason?: XbNovaFeedbackReason;
124
128
  };
125
129
  type XbNovaOnboarding = {
126
130
  conversation_id: string;
@@ -143,7 +147,7 @@ type UseXbNovaChatResult = {
143
147
  /** messageId β†’ ν˜„μž¬ 평가. 평가 μ•ˆ ν•œ λ©”μ‹œμ§€λŠ” ν‚€κ°€ μ—†λ‹€. */
144
148
  feedback: Record<string, XbNovaFeedbackRating>;
145
149
  /** λ‚™κ΄€μ μœΌλ‘œ λ°˜μ˜ν•˜κ³  μ„œλ²„μ— μ €μž₯ν•œλ‹€. μ‹€νŒ¨ν•˜λ©΄ λ˜λŒλ¦°λ‹€. */
146
- setFeedback: (messageId: string, rating: XbNovaFeedbackRating) => Promise<void>;
150
+ setFeedback: (messageId: string, rating: XbNovaFeedbackRating, reason?: XbNovaFeedbackReason) => Promise<void>;
147
151
  };
148
152
  /** `botId`λ₯Ό λ„˜κΈ°λ©΄ Provider의 κΈ°λ³Έ 봇 λŒ€μ‹  κ·Έ λ΄‡μœΌλ‘œ μƒˆ λŒ€ν™”λ₯Ό μ—°λ‹€ β€” λ„›μ§€ handoffκ°€ μ“΄λ‹€.
149
153
  * `nudgeNodeId`λ₯Ό λ„˜κΈ°λ©΄ 이 λŒ€ν™”κ°€ κ·Έ λ„›μ§€μ—μ„œ μ‹œμž‘λλ‹€κ³  μ„œλ²„μ— ν‘œμ‹œν•œλ‹€ β€” λ„›μ§€ μ „ν™˜
@@ -270,4 +274,4 @@ declare function useXbNovaSettings(): {
270
274
  showBar: () => void;
271
275
  };
272
276
 
273
- export { type ChatMessage, type FactValue, type MessagePart, type ProductItem, type TextPart, type ToolPart, type UseXbNovaChatResult, type VerdictAction, type VerdictOutput, type ViewKind, type ViewRenderer, type ViewRendererMap, XbNovaChat, type XbNovaClientAction, type XbNovaClientActionResult, type XbNovaClientActions, XbNovaFact, XbNovaFacts, type XbNovaFeedbackEvent, type XbNovaFeedbackRating, type XbNovaOnboarding, XbNovaProvider, type XbNovaTheme, XbNovaTrack, trackXbNovaEvent, useXbNovaChat, useXbNovaFact, useXbNovaFacts, useXbNovaSettings, useXbNovaTrack, useXbNovaVisitor };
277
+ export { type ChatMessage, type FactValue, type MessagePart, type ProductItem, type TextPart, type ToolPart, type UseXbNovaChatResult, type VerdictAction, type VerdictOutput, type ViewKind, type ViewRenderer, type ViewRendererMap, XbNovaChat, type XbNovaClientAction, type XbNovaClientActionResult, type XbNovaClientActions, XbNovaFact, XbNovaFacts, type XbNovaFeedbackEvent, type XbNovaFeedbackRating, type XbNovaFeedbackReason, type XbNovaOnboarding, XbNovaProvider, type XbNovaTheme, XbNovaTrack, trackXbNovaEvent, useXbNovaChat, useXbNovaFact, useXbNovaFacts, useXbNovaSettings, useXbNovaTrack, useXbNovaVisitor };
package/dist/index.d.ts CHANGED
@@ -116,11 +116,15 @@ type ClientResults = Record<string, {
116
116
  output: unknown;
117
117
  }>;
118
118
  type XbNovaFeedbackRating = "up" | "down" | null;
119
+ /** πŸ‘Ž μ‚¬μœ  칩의 μ½”λ“œ. */
120
+ type XbNovaFeedbackReason = "inaccurate" | "irrelevant" | "too_long" | "unclear";
119
121
  /** λ‹΅λ³€ λ§Œμ‘±λ„ μ €μž₯ 성곡 ν›„ 호슀트둜 μ „λ‹¬λ˜λŠ” 이벀트. `rating: null`은 평가 μ·¨μ†Œ. */
120
122
  type XbNovaFeedbackEvent = {
121
123
  conversationId: string;
122
124
  messageId: string;
123
125
  rating: XbNovaFeedbackRating;
126
+ /** πŸ‘ŽμΌ λ•Œ μ‚¬μš©μžκ°€ κ³ λ₯Έ μ‚¬μœ  μ½”λ“œ(μ—†μœΌλ©΄ undefined). */
127
+ reason?: XbNovaFeedbackReason;
124
128
  };
125
129
  type XbNovaOnboarding = {
126
130
  conversation_id: string;
@@ -143,7 +147,7 @@ type UseXbNovaChatResult = {
143
147
  /** messageId β†’ ν˜„μž¬ 평가. 평가 μ•ˆ ν•œ λ©”μ‹œμ§€λŠ” ν‚€κ°€ μ—†λ‹€. */
144
148
  feedback: Record<string, XbNovaFeedbackRating>;
145
149
  /** λ‚™κ΄€μ μœΌλ‘œ λ°˜μ˜ν•˜κ³  μ„œλ²„μ— μ €μž₯ν•œλ‹€. μ‹€νŒ¨ν•˜λ©΄ λ˜λŒλ¦°λ‹€. */
146
- setFeedback: (messageId: string, rating: XbNovaFeedbackRating) => Promise<void>;
150
+ setFeedback: (messageId: string, rating: XbNovaFeedbackRating, reason?: XbNovaFeedbackReason) => Promise<void>;
147
151
  };
148
152
  /** `botId`λ₯Ό λ„˜κΈ°λ©΄ Provider의 κΈ°λ³Έ 봇 λŒ€μ‹  κ·Έ λ΄‡μœΌλ‘œ μƒˆ λŒ€ν™”λ₯Ό μ—°λ‹€ β€” λ„›μ§€ handoffκ°€ μ“΄λ‹€.
149
153
  * `nudgeNodeId`λ₯Ό λ„˜κΈ°λ©΄ 이 λŒ€ν™”κ°€ κ·Έ λ„›μ§€μ—μ„œ μ‹œμž‘λλ‹€κ³  μ„œλ²„μ— ν‘œμ‹œν•œλ‹€ β€” λ„›μ§€ μ „ν™˜
@@ -270,4 +274,4 @@ declare function useXbNovaSettings(): {
270
274
  showBar: () => void;
271
275
  };
272
276
 
273
- export { type ChatMessage, type FactValue, type MessagePart, type ProductItem, type TextPart, type ToolPart, type UseXbNovaChatResult, type VerdictAction, type VerdictOutput, type ViewKind, type ViewRenderer, type ViewRendererMap, XbNovaChat, type XbNovaClientAction, type XbNovaClientActionResult, type XbNovaClientActions, XbNovaFact, XbNovaFacts, type XbNovaFeedbackEvent, type XbNovaFeedbackRating, type XbNovaOnboarding, XbNovaProvider, type XbNovaTheme, XbNovaTrack, trackXbNovaEvent, useXbNovaChat, useXbNovaFact, useXbNovaFacts, useXbNovaSettings, useXbNovaTrack, useXbNovaVisitor };
277
+ export { type ChatMessage, type FactValue, type MessagePart, type ProductItem, type TextPart, type ToolPart, type UseXbNovaChatResult, type VerdictAction, type VerdictOutput, type ViewKind, type ViewRenderer, type ViewRendererMap, XbNovaChat, type XbNovaClientAction, type XbNovaClientActionResult, type XbNovaClientActions, XbNovaFact, XbNovaFacts, type XbNovaFeedbackEvent, type XbNovaFeedbackRating, type XbNovaFeedbackReason, type XbNovaOnboarding, XbNovaProvider, type XbNovaTheme, XbNovaTrack, trackXbNovaEvent, useXbNovaChat, useXbNovaFact, useXbNovaFacts, useXbNovaSettings, useXbNovaTrack, useXbNovaVisitor };
package/dist/index.js CHANGED
@@ -1128,7 +1128,7 @@ function useXbNovaChat(options) {
1128
1128
  next[next.length - 1] = { ...next[next.length - 1], id };
1129
1129
  return next;
1130
1130
  });
1131
- async function setFeedback(messageId, rating) {
1131
+ async function setFeedback(messageId, rating, reason) {
1132
1132
  const conversationId = onboarding?.conversation_id;
1133
1133
  if (!conversationId || !messageId) return;
1134
1134
  const prevRating = feedbackRef.current[messageId] ?? null;
@@ -1145,11 +1145,11 @@ function useXbNovaChat(options) {
1145
1145
  {
1146
1146
  method: "PUT",
1147
1147
  headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
1148
- body: JSON.stringify({ rating, visitor_id: visitorId })
1148
+ body: JSON.stringify({ rating, visitor_id: visitorId, reason: rating === "down" ? reason ?? null : null })
1149
1149
  }
1150
1150
  );
1151
1151
  if (!res.ok) throw new Error(`${res.status}`);
1152
- options?.onFeedback?.({ conversationId, messageId, rating });
1152
+ options?.onFeedback?.({ conversationId, messageId, rating, reason });
1153
1153
  } catch (e) {
1154
1154
  console.warn("[xbnova] \uD53C\uB4DC\uBC31 \uC800\uC7A5 \uC2E4\uD328", e);
1155
1155
  apply(prevRating);
@@ -1776,38 +1776,41 @@ function XbNovaChat({
1776
1776
  /* @__PURE__ */ jsxs3("div", { className: "flex-1 space-y-3 overflow-y-auto p-4", children: [
1777
1777
  showDisclosure ? /* @__PURE__ */ jsx7("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
1778
  firstBubble ? /* @__PURE__ */ jsx7(Bubble, { role: "assistant", children: /* @__PURE__ */ jsx7(AssistantMarkdown, { text: firstBubble }) }) : null,
1779
- messages.map((message, i) => /* @__PURE__ */ jsx7("div", { children: message.role === "user" ? /* @__PURE__ */ jsx7(Bubble, { role: "user", children: message.parts.map((p) => p.kind === "text" ? p.text : "").join("") }) : /* @__PURE__ */ jsxs3("div", { className: "space-y-1", children: [
1780
- message.parts.map((part, j) => {
1781
- if (part.kind === "text") {
1782
- return /* @__PURE__ */ jsx7(Bubble, { role: "assistant", children: /* @__PURE__ */ jsx7(AssistantMarkdown, { text: part.text }) }, j);
1783
- }
1784
- if (clientActionHandlers.has(part.toolName)) {
1785
- const result = clientResults[part.toolCallId];
1786
- if (result) {
1787
- return /* @__PURE__ */ jsx7(
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__ */ jsx7("div", { children: spec.render(part.input) }, j) : null;
1799
- }
1800
- return /* @__PURE__ */ jsx7(ToolView, { part, viewRenderers, onVerdictAction }, j);
1801
- }),
1802
- !hasVisiblePart(message.parts, clientActionHandlers, clientResults) && busy ? /* @__PURE__ */ jsx7(Bubble, { role: "assistant", children: /* @__PURE__ */ jsx7("span", { className: "text-neutral-400", children: "\uC0DD\uAC01\uD558\uB294 \uC911\u2026" }) }) : null,
1803
- feedbackEnabled && !busy && message.id && message.parts.some((p) => p.kind === "text" && p.text.trim()) ? /* @__PURE__ */ jsx7(
1804
- FeedbackButtons,
1805
- {
1806
- rating: feedback[message.id] ?? null,
1807
- onChange: (r) => void setFeedback(message.id, r)
1808
- }
1809
- ) : null
1810
- ] }) }, i)),
1779
+ messages.map((message, i) => /* @__PURE__ */ jsx7("div", { children: message.role === "user" ? /* @__PURE__ */ jsx7(Bubble, { role: "user", children: message.parts.map((p) => p.kind === "text" ? p.text : "").join("") }) : /* @__PURE__ */ jsxs3(
1780
+ AnswerActionsFrame,
1781
+ {
1782
+ enabled: feedbackEnabled && !busy && !!message.id && message.parts.some((p) => p.kind === "text" && p.text.trim()),
1783
+ text: message.parts.filter((p) => p.kind === "text").map((p) => p.kind === "text" ? p.text : "").join("\n\n"),
1784
+ rating: message.id ? feedback[message.id] ?? null : null,
1785
+ isLast: i === messages.length - 1,
1786
+ onRate: (r, reason) => void setFeedback(message.id, r, reason),
1787
+ children: [
1788
+ message.parts.map((part, j) => {
1789
+ if (part.kind === "text") {
1790
+ return /* @__PURE__ */ jsx7(Bubble, { role: "assistant", children: /* @__PURE__ */ jsx7(AssistantMarkdown, { text: part.text }) }, j);
1791
+ }
1792
+ if (clientActionHandlers.has(part.toolName)) {
1793
+ const result = clientResults[part.toolCallId];
1794
+ if (result) {
1795
+ return /* @__PURE__ */ jsx7(
1796
+ ToolView,
1797
+ {
1798
+ part: { ...part, state: "output", view: result.view, output: result.output },
1799
+ viewRenderers,
1800
+ onVerdictAction
1801
+ },
1802
+ j
1803
+ );
1804
+ }
1805
+ const spec = clientActionHandlers.get(part.toolName);
1806
+ return spec?.render ? /* @__PURE__ */ jsx7("div", { children: spec.render(part.input) }, j) : null;
1807
+ }
1808
+ return /* @__PURE__ */ jsx7(ToolView, { part, viewRenderers, onVerdictAction }, j);
1809
+ }),
1810
+ !hasVisiblePart(message.parts, clientActionHandlers, clientResults) && busy ? /* @__PURE__ */ jsx7(Bubble, { role: "assistant", children: /* @__PURE__ */ jsx7("span", { className: "text-neutral-400", children: "\uC0DD\uAC01\uD558\uB294 \uC911\u2026" }) }) : null
1811
+ ]
1812
+ }
1813
+ ) }, i)),
1811
1814
  error ? /* @__PURE__ */ jsx7("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,
1812
1815
  /* @__PURE__ */ jsx7("div", { ref: bottomRef })
1813
1816
  ] }),
@@ -1910,44 +1913,193 @@ function Avatar({
1910
1913
  }
1911
1914
  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";
1912
1915
  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";
1913
- function FeedbackButtons({
1916
+ var COPY_PATH = "M9 9h11v11H9zM5 15H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1";
1917
+ var CHECK_PATH = "m5 12 5 5L20 7";
1918
+ var FEEDBACK_REASONS = [
1919
+ { code: "inaccurate", label: "\uC815\uD655\uD558\uC9C0 \uC54A\uC544\uC694" },
1920
+ { code: "irrelevant", label: "\uC9C8\uBB38\uACFC \uB2EC\uB77C\uC694" },
1921
+ { code: "too_long", label: "\uB108\uBB34 \uAE38\uC5B4\uC694" },
1922
+ { code: "unclear", label: "\uC774\uD574\uD558\uAE30 \uC5B4\uB824\uC6CC\uC694" }
1923
+ ];
1924
+ var LONG_PRESS_MS = 420;
1925
+ function ActionIcon({ path, filled }) {
1926
+ return /* @__PURE__ */ jsx7(
1927
+ "svg",
1928
+ {
1929
+ width: "16",
1930
+ height: "16",
1931
+ viewBox: "0 0 24 24",
1932
+ fill: filled ? "currentColor" : "none",
1933
+ stroke: "currentColor",
1934
+ strokeWidth: "2",
1935
+ strokeLinecap: "round",
1936
+ strokeLinejoin: "round",
1937
+ "aria-hidden": "true",
1938
+ children: /* @__PURE__ */ jsx7("path", { d: path })
1939
+ }
1940
+ );
1941
+ }
1942
+ async function copyText(text) {
1943
+ try {
1944
+ await navigator.clipboard.writeText(text);
1945
+ return true;
1946
+ } catch {
1947
+ try {
1948
+ const ta = document.createElement("textarea");
1949
+ ta.value = text;
1950
+ ta.style.position = "fixed";
1951
+ ta.style.opacity = "0";
1952
+ document.body.appendChild(ta);
1953
+ ta.select();
1954
+ const ok = document.execCommand("copy");
1955
+ ta.remove();
1956
+ return ok;
1957
+ } catch {
1958
+ return false;
1959
+ }
1960
+ }
1961
+ }
1962
+ function AnswerActions({
1963
+ text,
1914
1964
  rating,
1915
- onChange
1965
+ isLast,
1966
+ onRate,
1967
+ children
1916
1968
  }) {
1917
- const item = (kind, label, path) => {
1918
- const selected = rating === kind;
1919
- const color = selected ? kind === "up" ? "var(--xbnova-accent-1, #4f46e5)" : "#dc2626" : void 0;
1920
- return /* @__PURE__ */ jsx7(
1921
- "button",
1922
- {
1923
- type: "button",
1924
- "aria-label": label,
1925
- "aria-pressed": selected,
1926
- onClick: () => onChange(selected ? null : kind),
1927
- 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",
1928
- style: color ? { color } : void 0,
1929
- children: /* @__PURE__ */ jsx7(
1930
- "svg",
1969
+ const rootRef = useRef8(null);
1970
+ const timerRef = useRef8(null);
1971
+ const [menuOpen, setMenuOpen] = useState4(false);
1972
+ const [reasonOpen, setReasonOpen] = useState4(false);
1973
+ const [copied, setCopied] = useState4(false);
1974
+ const [entered, setEntered] = useState4(false);
1975
+ const clearTimer = () => {
1976
+ if (timerRef.current) clearTimeout(timerRef.current);
1977
+ timerRef.current = null;
1978
+ };
1979
+ useEffect8(() => clearTimer, []);
1980
+ useEffect8(() => {
1981
+ if (!menuOpen) {
1982
+ setEntered(false);
1983
+ return;
1984
+ }
1985
+ const raf = requestAnimationFrame(() => setEntered(true));
1986
+ const onDown = (e) => {
1987
+ if (!e.composedPath().includes(rootRef.current)) setMenuOpen(false);
1988
+ };
1989
+ document.addEventListener("pointerdown", onDown, true);
1990
+ return () => {
1991
+ cancelAnimationFrame(raf);
1992
+ document.removeEventListener("pointerdown", onDown, true);
1993
+ };
1994
+ }, [menuOpen]);
1995
+ const startPress = (e) => {
1996
+ if (e.pointerType === "mouse" || isLast) return;
1997
+ clearTimer();
1998
+ timerRef.current = setTimeout(() => {
1999
+ setMenuOpen(true);
2000
+ if (typeof navigator.vibrate === "function") navigator.vibrate(10);
2001
+ }, LONG_PRESS_MS);
2002
+ };
2003
+ const handleCopy = async () => {
2004
+ if (await copyText(text)) {
2005
+ setCopied(true);
2006
+ setTimeout(() => setCopied(false), 1400);
2007
+ }
2008
+ };
2009
+ const handleRate = (kind) => {
2010
+ if (rating === kind) {
2011
+ onRate(null);
2012
+ setReasonOpen(false);
2013
+ return;
2014
+ }
2015
+ onRate(kind);
2016
+ setReasonOpen(kind === "down");
2017
+ if (kind === "up") setMenuOpen(false);
2018
+ };
2019
+ const barVisible = isLast || menuOpen;
2020
+ const floating = menuOpen && !isLast;
2021
+ const btn = (label, onClick, selected, color, icon) => /* @__PURE__ */ jsxs3(
2022
+ "button",
2023
+ {
2024
+ type: "button",
2025
+ "aria-label": label,
2026
+ "aria-pressed": selected,
2027
+ onClick,
2028
+ 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"}`,
2029
+ style: color ? { color } : void 0,
2030
+ children: [
2031
+ icon,
2032
+ floating ? /* @__PURE__ */ jsx7("span", { children: label }) : null
2033
+ ]
2034
+ }
2035
+ );
2036
+ return /* @__PURE__ */ jsxs3(
2037
+ "div",
2038
+ {
2039
+ ref: rootRef,
2040
+ className: "group space-y-1",
2041
+ onPointerDown: startPress,
2042
+ onPointerUp: clearTimer,
2043
+ onPointerCancel: clearTimer,
2044
+ onPointerLeave: clearTimer,
2045
+ onPointerMove: (e) => {
2046
+ if (Math.abs(e.movementX) + Math.abs(e.movementY) > 6) clearTimer();
2047
+ },
2048
+ onContextMenu: (e) => {
2049
+ if (!isLast && e.nativeEvent.pointerType !== "mouse") e.preventDefault();
2050
+ },
2051
+ children: [
2052
+ children,
2053
+ /* @__PURE__ */ jsx7(
2054
+ "div",
1931
2055
  {
1932
- width: "16",
1933
- height: "16",
1934
- viewBox: "0 0 24 24",
1935
- fill: selected ? "currentColor" : "none",
1936
- stroke: "currentColor",
1937
- strokeWidth: "2",
1938
- strokeLinecap: "round",
1939
- strokeLinejoin: "round",
1940
- "aria-hidden": "true",
1941
- children: /* @__PURE__ */ jsx7("path", { d: path })
2056
+ className: `flex pl-1 ${barVisible ? "" : "hidden md:group-hover:flex"}`,
2057
+ style: { transition: "opacity 140ms, transform 140ms", opacity: floating && !entered ? 0 : 1, transform: floating && !entered ? "scale(0.94)" : "none" },
2058
+ children: /* @__PURE__ */ jsxs3(
2059
+ "div",
2060
+ {
2061
+ 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"}`,
2062
+ children: [
2063
+ btn(copied ? "\uBCF5\uC0AC\uB428" : "\uBCF5\uC0AC", handleCopy, false, copied ? "var(--xbnova-accent-1, #4f46e5)" : void 0, /* @__PURE__ */ jsx7(ActionIcon, { path: copied ? CHECK_PATH : COPY_PATH })),
2064
+ btn(
2065
+ "\uC88B\uC544\uC694",
2066
+ () => handleRate("up"),
2067
+ rating === "up",
2068
+ rating === "up" ? "var(--xbnova-accent-1, #4f46e5)" : void 0,
2069
+ /* @__PURE__ */ jsx7(ActionIcon, { path: THUMB_UP_PATH, filled: rating === "up" })
2070
+ ),
2071
+ btn(
2072
+ "\uBCC4\uB85C\uC608\uC694",
2073
+ () => handleRate("down"),
2074
+ rating === "down",
2075
+ rating === "down" ? "#dc2626" : void 0,
2076
+ /* @__PURE__ */ jsx7(ActionIcon, { path: THUMB_DOWN_PATH, filled: rating === "down" })
2077
+ )
2078
+ ]
2079
+ }
2080
+ )
1942
2081
  }
1943
- )
1944
- }
1945
- );
1946
- };
1947
- return /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-0.5 pl-1", children: [
1948
- item("up", "\uB3C4\uC6C0\uC774 \uB410\uC5B4\uC694", THUMB_UP_PATH),
1949
- item("down", "\uB3C4\uC6C0\uC774 \uC548 \uB410\uC5B4\uC694", THUMB_DOWN_PATH)
1950
- ] });
2082
+ ),
2083
+ rating === "down" && reasonOpen ? /* @__PURE__ */ jsxs3("div", { className: "flex flex-wrap items-center gap-1.5 pl-1 pt-0.5", children: [
2084
+ /* @__PURE__ */ jsx7("span", { className: "text-[11px] text-neutral-400", children: "\uC5B4\uB5A4 \uC810\uC774 \uC544\uC26C\uC6E0\uB098\uC694?" }),
2085
+ FEEDBACK_REASONS.map((r) => /* @__PURE__ */ jsx7(
2086
+ "button",
2087
+ {
2088
+ type: "button",
2089
+ onClick: () => {
2090
+ onRate("down", r.code);
2091
+ setReasonOpen(false);
2092
+ setMenuOpen(false);
2093
+ },
2094
+ className: "xbnova-chip rounded-full border px-2.5 py-1 text-[11px] font-semibold transition",
2095
+ children: r.label
2096
+ },
2097
+ r.code
2098
+ ))
2099
+ ] }) : null
2100
+ ]
2101
+ }
2102
+ );
1951
2103
  }
1952
2104
  function Bubble({ role, children }) {
1953
2105
  const mine = role === "user";
@@ -1962,6 +2114,14 @@ function Bubble({ role, children }) {
1962
2114
  function AssistantMarkdown({ text }) {
1963
2115
  return /* @__PURE__ */ jsx7("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__ */ jsx7(ReactMarkdown, { remarkPlugins: [remarkGfm, remarkBreaks], children: text }) });
1964
2116
  }
2117
+ function AnswerActionsFrame({
2118
+ enabled,
2119
+ children,
2120
+ ...rest
2121
+ }) {
2122
+ if (!enabled) return /* @__PURE__ */ jsx7("div", { className: "space-y-1", children });
2123
+ return /* @__PURE__ */ jsx7(AnswerActions, { ...rest, children });
2124
+ }
1965
2125
 
1966
2126
  // src/facts.ts
1967
2127
  import { useEffect as useEffect9, useRef as useRef9 } from "react";