@xbrain-swe/nova-chat-widget 0.3.0-dev.16 → 0.3.0-dev.18

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
@@ -107,7 +107,7 @@ type XbNovaTheme = {
107
107
  colorScheme: "light" | "dark" | "system";
108
108
  };
109
109
 
110
- declare function XbNovaProvider({ apiKey, botId, appId, screen, apiBaseUrl, clientActions, theme, userKey, threadKey, sessionKey, children, }: {
110
+ declare function XbNovaProvider({ apiKey, botId, appId, screen, apiBaseUrl, clientActions, theme, userKey, threadKey, sessionKey, eventProperties, children, }: {
111
111
  apiKey: string;
112
112
  botId: string;
113
113
  appId: string;
@@ -123,6 +123,8 @@ declare function XbNovaProvider({ apiKey, botId, appId, screen, apiBaseUrl, clie
123
123
  /** 이 방문의 세션 키(예: GA4 session_id). nova가 남기는 이벤트(위젯 노출·넛지·상담 버튼)와 대화에 실려
124
124
  * 콘솔에서 세션 단위로 집계된다. 값이 바뀌면(세션 갱신) 그 뒤 이벤트부터 새 값으로 실린다. 안 주면 세션은 비운다. */
125
125
  sessionKey?: string;
126
+ /** 모든 분석 이벤트에 함께 실을 값(예: `{ vehicle_id: data.id }`). 평평한 원시값만 넣고 개인정보는 넣지 않는다. */
127
+ eventProperties?: Record<string, string | number | boolean | null | undefined>;
126
128
  /** 이 페이지가 AI에게 내어줄 client action tool — 이름별로 설명·스키마·실행 함수를
127
129
  * 등록한다("SDK 초기화 시점에 등록"). 스펙은 App 레벨 레지스트리로 자동 동기화되고
128
130
  * (`XbNovaClientActionSync`), 콘솔의 tool 목록에 반영된다. 핸들러는 여기서만 산다. */
@@ -171,7 +173,11 @@ type UseXbNovaChatResult = {
171
173
  messages: ChatMessage[];
172
174
  busy: boolean;
173
175
  error: string | null;
174
- send: (text: string) => Promise<void>;
176
+ /** `nudgeOpening`은 대화 중 넛지를 눌러 띄운 질문이다. 답을 보낼 때 그 앞에 AI 발화로 남긴다. */
177
+ send: (text: string, options?: {
178
+ nudgeOpening?: string | null;
179
+ inputType?: "typed" | "quick_reply";
180
+ }) => Promise<void>;
175
181
  /** client action 핸들러가 돌려준 정규화 결과(tool_call_id별). 채팅 화면이 view 렌더러로 그린다. */
176
182
  clientResults: ClientResults;
177
183
  /** messageId → 현재 평가. 평가 안 한 메시지는 키가 없다. */
package/dist/index.d.ts CHANGED
@@ -107,7 +107,7 @@ type XbNovaTheme = {
107
107
  colorScheme: "light" | "dark" | "system";
108
108
  };
109
109
 
110
- declare function XbNovaProvider({ apiKey, botId, appId, screen, apiBaseUrl, clientActions, theme, userKey, threadKey, sessionKey, children, }: {
110
+ declare function XbNovaProvider({ apiKey, botId, appId, screen, apiBaseUrl, clientActions, theme, userKey, threadKey, sessionKey, eventProperties, children, }: {
111
111
  apiKey: string;
112
112
  botId: string;
113
113
  appId: string;
@@ -123,6 +123,8 @@ declare function XbNovaProvider({ apiKey, botId, appId, screen, apiBaseUrl, clie
123
123
  /** 이 방문의 세션 키(예: GA4 session_id). nova가 남기는 이벤트(위젯 노출·넛지·상담 버튼)와 대화에 실려
124
124
  * 콘솔에서 세션 단위로 집계된다. 값이 바뀌면(세션 갱신) 그 뒤 이벤트부터 새 값으로 실린다. 안 주면 세션은 비운다. */
125
125
  sessionKey?: string;
126
+ /** 모든 분석 이벤트에 함께 실을 값(예: `{ vehicle_id: data.id }`). 평평한 원시값만 넣고 개인정보는 넣지 않는다. */
127
+ eventProperties?: Record<string, string | number | boolean | null | undefined>;
126
128
  /** 이 페이지가 AI에게 내어줄 client action tool — 이름별로 설명·스키마·실행 함수를
127
129
  * 등록한다("SDK 초기화 시점에 등록"). 스펙은 App 레벨 레지스트리로 자동 동기화되고
128
130
  * (`XbNovaClientActionSync`), 콘솔의 tool 목록에 반영된다. 핸들러는 여기서만 산다. */
@@ -171,7 +173,11 @@ type UseXbNovaChatResult = {
171
173
  messages: ChatMessage[];
172
174
  busy: boolean;
173
175
  error: string | null;
174
- send: (text: string) => Promise<void>;
176
+ /** `nudgeOpening`은 대화 중 넛지를 눌러 띄운 질문이다. 답을 보낼 때 그 앞에 AI 발화로 남긴다. */
177
+ send: (text: string, options?: {
178
+ nudgeOpening?: string | null;
179
+ inputType?: "typed" | "quick_reply";
180
+ }) => Promise<void>;
175
181
  /** client action 핸들러가 돌려준 정규화 결과(tool_call_id별). 채팅 화면이 view 렌더러로 그린다. */
176
182
  clientResults: ClientResults;
177
183
  /** messageId → 현재 평가. 평가 안 한 메시지는 키가 없다. */
package/dist/index.js CHANGED
@@ -25,7 +25,7 @@ function useXbNovaInternal() {
25
25
  return ctx;
26
26
  }
27
27
  var VISITOR_STORAGE_KEY = "xbnova_visitor_id";
28
- var WIDGET_VIEW_WAIT_MS = 1e4;
28
+ var HOST_IDENTITY_WAIT_MS = 1e4;
29
29
  function resolveVisitorId() {
30
30
  if (typeof window === "undefined") return "";
31
31
  try {
@@ -70,10 +70,10 @@ function InternalProvider({
70
70
  const sessionId = auth.sessionKey || null;
71
71
  const latestRef = useRef({ auth, visitorId, sessionId });
72
72
  latestRef.current = { auth, visitorId, sessionId };
73
- const sendEvent = useCallback((event, properties = {}) => {
73
+ const postEvent = useCallback((event, properties) => {
74
74
  const { auth: a, visitorId: v, sessionId: sid } = latestRef.current;
75
75
  if (!v) return;
76
- const { conversation_id: conversationId, ...rest } = properties;
76
+ const { conversation_id: conversationId, occurred_at: occurredAt, ...rest } = properties;
77
77
  fetch(`${a.apiBaseUrl}/v1/events`, {
78
78
  method: "POST",
79
79
  keepalive: true,
@@ -84,26 +84,45 @@ function InternalProvider({
84
84
  visitor_id: v,
85
85
  session_id: sid,
86
86
  conversation_id: conversationId ?? null,
87
- properties: { screen: a.screen, ...rest }
87
+ occurred_at: occurredAt,
88
+ properties: { screen: a.screen, ...a.eventProperties, ...rest }
88
89
  })
89
90
  }).catch(() => {
90
91
  });
91
92
  }, []);
92
- const widgetViewSentRef = useRef(false);
93
+ const eventQueueRef = useRef([]);
94
+ const identitySettledRef = useRef(false);
95
+ const flushEvents = useCallback(() => {
96
+ identitySettledRef.current = true;
97
+ for (const [event, properties] of eventQueueRef.current.splice(0)) postEvent(event, properties);
98
+ }, [postEvent]);
99
+ const sendEvent = useCallback(
100
+ (event, properties = {}) => {
101
+ const stamped = { ...properties, occurred_at: properties.occurred_at ?? (/* @__PURE__ */ new Date()).toISOString() };
102
+ if (identitySettledRef.current) postEvent(event, stamped);
103
+ else eventQueueRef.current.push([event, stamped]);
104
+ },
105
+ [postEvent]
106
+ );
107
+ const hostIdentity = auth.userKey || sessionId;
93
108
  useEffect(() => {
94
- if (widgetViewSentRef.current) return;
95
- const send = () => {
96
- if (widgetViewSentRef.current) return;
97
- widgetViewSentRef.current = true;
98
- sendEvent("widget_view");
99
- };
100
- if (sessionId) {
101
- send();
109
+ if (hostIdentity) {
110
+ flushEvents();
102
111
  return;
103
112
  }
104
- const timer = setTimeout(send, WIDGET_VIEW_WAIT_MS);
105
- return () => clearTimeout(timer);
106
- }, [sessionId, sendEvent]);
113
+ const timer = setTimeout(flushEvents, HOST_IDENTITY_WAIT_MS);
114
+ window.addEventListener("pagehide", flushEvents);
115
+ return () => {
116
+ clearTimeout(timer);
117
+ window.removeEventListener("pagehide", flushEvents);
118
+ };
119
+ }, [hostIdentity, flushEvents]);
120
+ const widgetViewSentRef = useRef(false);
121
+ useEffect(() => {
122
+ if (widgetViewSentRef.current) return;
123
+ widgetViewSentRef.current = true;
124
+ sendEvent("view_vehicle");
125
+ }, [sendEvent]);
107
126
  const value = useMemo(
108
127
  () => ({
109
128
  auth,
@@ -699,7 +718,7 @@ function XbNovaNudgeLoop({ appId }) {
699
718
  autoHideMs: messageNode.auto_hide_ms ?? 2e4,
700
719
  quickReplies: messageNode.quick_replies ?? []
701
720
  });
702
- sendEvent("nudge_impression", { nudge_node_id: messageNode.id, bot_id: botId });
721
+ sendEvent("nudge_AI_view", { nudge_node_id: messageNode.id, bot_id: botId });
703
722
  };
704
723
  const tryFireFrom = (node) => {
705
724
  const nextEdge = (edgesFrom.get(node.id) ?? [])[0];
@@ -905,6 +924,7 @@ function XbNovaProvider({
905
924
  userKey,
906
925
  threadKey,
907
926
  sessionKey,
927
+ eventProperties,
908
928
  children
909
929
  }) {
910
930
  useEffect6(() => {
@@ -920,6 +940,7 @@ function XbNovaProvider({
920
940
  useEffect6(() => {
921
941
  if (screen) bumpVisitCount(screen);
922
942
  }, [screen]);
943
+ const eventPropertiesJson = JSON.stringify(eventProperties ?? null);
923
944
  const auth = useMemo2(
924
945
  () => ({
925
946
  apiKey,
@@ -929,9 +950,10 @@ function XbNovaProvider({
929
950
  screen,
930
951
  userKey: userKey || void 0,
931
952
  threadKey: threadKey || void 0,
932
- sessionKey: sessionKey || void 0
953
+ sessionKey: sessionKey || void 0,
954
+ eventProperties: JSON.parse(eventPropertiesJson) ?? void 0
933
955
  }),
934
- [apiKey, botId, appId, apiBaseUrl, screen, userKey, threadKey, sessionKey]
956
+ [apiKey, botId, appId, apiBaseUrl, screen, userKey, threadKey, sessionKey, eventPropertiesJson]
935
957
  );
936
958
  const resolvedTheme = useMemo2(
937
959
  () => ({ ...DEFAULT_THEME, ...theme }),
@@ -1174,6 +1196,10 @@ async function resumeToolResult(apiBaseUrl, apiKey, body, handlers) {
1174
1196
 
1175
1197
  // src/use-chat.ts
1176
1198
  var DEFAULT_CLIENT_ACTION_TIMEOUT_MS = 15e3;
1199
+ var CTA_CLICK_EVENTS = {
1200
+ suggest_consultation: { event: "apply_AI_click", ctaType: "consult" },
1201
+ suggest_finance_limit: { event: "finance_AI_click", ctaType: "finance" }
1202
+ };
1177
1203
  function useXbNovaChat(options) {
1178
1204
  const { auth, trackRegistry, contextRegistry, clientActionHandlers, visitorId, sessionId, sendEvent } = useXbNovaInternal();
1179
1205
  const botId = options?.botId ?? auth.botId;
@@ -1185,7 +1211,6 @@ function useXbNovaChat(options) {
1185
1211
  const [resumed, setResumed] = useState3(false);
1186
1212
  const [sentOpening, setSentOpening] = useState3(null);
1187
1213
  const conversationIdRef = useRef7(null);
1188
- const pendingOpeningRef = useRef7(null);
1189
1214
  const messagesRef = useRef7(messages);
1190
1215
  messagesRef.current = messages;
1191
1216
  const [busy, setBusy] = useState3(false);
@@ -1208,7 +1233,6 @@ function useXbNovaChat(options) {
1208
1233
  setFeedbackState({});
1209
1234
  dispatchedClientActionsRef.current = /* @__PURE__ */ new Set();
1210
1235
  conversationIdRef.current = null;
1211
- pendingOpeningRef.current = null;
1212
1236
  fetch(`${apiBaseUrl}/v1/conversations`, {
1213
1237
  method: "POST",
1214
1238
  headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
@@ -1241,16 +1265,6 @@ function useXbNovaChat(options) {
1241
1265
  cancelled = true;
1242
1266
  };
1243
1267
  }, [apiBaseUrl, apiKey, botId, userKey, threadKey]);
1244
- const openingMessage = options?.openingMessage;
1245
- useEffect8(() => {
1246
- if (!onboarding || !nudgeNodeId || !openingMessage) return;
1247
- if (messagesRef.current.length === 0) return;
1248
- setMessages((prev) => [
1249
- ...prev,
1250
- { role: "assistant", parts: [{ kind: "text", id: `nudge:${nudgeNodeId}:${prev.length}`, text: openingMessage }] }
1251
- ]);
1252
- pendingOpeningRef.current = openingMessage;
1253
- }, [onboarding, nudgeNodeId, openingMessage]);
1254
1268
  const appendToLastAssistant = (existingParts, newParts) => setMessages((prev) => {
1255
1269
  const next = [...prev];
1256
1270
  next[next.length - 1] = { ...next[next.length - 1], role: "assistant", parts: [...existingParts, ...newParts] };
@@ -1290,13 +1304,14 @@ function useXbNovaChat(options) {
1290
1304
  apply(prevRating);
1291
1305
  }
1292
1306
  }
1293
- function postClientActionEvent(kind, action, label) {
1307
+ function postClientActionEvent(event, action, label, ctaType) {
1294
1308
  const conversationId = conversationIdRef.current;
1295
1309
  if (!conversationId) return;
1296
- sendEvent(kind, { conversation_id: conversationId, action, label });
1310
+ sendEvent(event, { conversation_id: conversationId, action, label, cta_type: ctaType });
1297
1311
  }
1298
1312
  function trackClientActionClick(action, label) {
1299
- postClientActionEvent("cta_click", action, label);
1313
+ const cta = CTA_CLICK_EVENTS[action];
1314
+ postClientActionEvent(cta?.event ?? "cta_click", action, label, cta?.ctaType);
1300
1315
  }
1301
1316
  async function runClientAction(spec, part, existingParts) {
1302
1317
  if (spec.dismissSheet) options?.onDismissSheet?.();
@@ -1369,7 +1384,7 @@ function useXbNovaChat(options) {
1369
1384
  void runClientAction(spec, part, last.parts);
1370
1385
  }
1371
1386
  }, [messages, clientActionHandlers, onboarding, apiBaseUrl, apiKey, botId]);
1372
- async function send(text) {
1387
+ async function send(text, { nudgeOpening, inputType = "typed" } = {}) {
1373
1388
  const trimmed = text.trim();
1374
1389
  if (!trimmed || busy) return;
1375
1390
  sendEvent("message_send", {
@@ -1377,10 +1392,19 @@ function useXbNovaChat(options) {
1377
1392
  is_first: messages.length === 0,
1378
1393
  length: trimmed.length
1379
1394
  });
1395
+ let firstQuestionPending = messages.length === 0;
1396
+ const reportFirstQuestion = (conversationId) => {
1397
+ if (!firstQuestionPending) return;
1398
+ firstQuestionPending = false;
1399
+ sendEvent("question_AI_first", { conversation_id: conversationId, input_type: inputType });
1400
+ };
1401
+ if (conversationIdRef.current) reportFirstQuestion(conversationIdRef.current);
1380
1402
  setError(null);
1381
1403
  setBusy(true);
1404
+ const resumedOpening = messages.length > 0 && nudgeOpening ? nudgeOpening : null;
1382
1405
  setMessages((prev) => [
1383
1406
  ...prev,
1407
+ ...resumedOpening ? [{ role: "assistant", parts: [{ kind: "text", id: `nudge:${prev.length}`, text: resumedOpening }] }] : [],
1384
1408
  { role: "user", parts: [{ kind: "text", id: "u", text: trimmed }] },
1385
1409
  { role: "assistant", parts: [] }
1386
1410
  ]);
@@ -1393,14 +1417,13 @@ function useXbNovaChat(options) {
1393
1417
  const notes = collectContextEntries(contextRegistry);
1394
1418
  const screen = focused ? { region: focused.region, data: resolveFactValue(focused.data) } : {};
1395
1419
  const context = focused || notes.length ? { ...screen, ...notes.length ? { notes } : {} } : void 0;
1396
- let openingMessage2;
1420
+ let openingMessage;
1397
1421
  if (messages.length === 0) {
1398
- openingMessage2 = (options?.openingMessage ?? onboarding?.greeting) || null;
1399
- setSentOpening(openingMessage2);
1422
+ openingMessage = (options?.openingMessage ?? onboarding?.greeting) || null;
1423
+ setSentOpening(openingMessage);
1400
1424
  } else {
1401
- openingMessage2 = pendingOpeningRef.current;
1425
+ openingMessage = resumedOpening;
1402
1426
  }
1403
- pendingOpeningRef.current = null;
1404
1427
  try {
1405
1428
  await streamChat(
1406
1429
  apiBaseUrl,
@@ -1411,7 +1434,7 @@ function useXbNovaChat(options) {
1411
1434
  conversation_id: conversationIdRef.current,
1412
1435
  context,
1413
1436
  nudge_node_id: nudgeNodeId ?? null,
1414
- opening_message: openingMessage2,
1437
+ opening_message: openingMessage,
1415
1438
  visitor_id: userKey || null,
1416
1439
  thread_key: threadKey ?? null,
1417
1440
  session_id: sessionId
@@ -1421,6 +1444,7 @@ function useXbNovaChat(options) {
1421
1444
  onMessageId: setLastAssistantId,
1422
1445
  onConversationId: (id) => {
1423
1446
  conversationIdRef.current = id;
1447
+ reportFirstQuestion(id);
1424
1448
  },
1425
1449
  onError: setError
1426
1450
  }
@@ -1431,7 +1455,7 @@ function useXbNovaChat(options) {
1431
1455
  setBusy(false);
1432
1456
  }
1433
1457
  }
1434
- const opening = resumed ? null : sentOpening ?? options?.openingMessage ?? onboarding?.greeting ?? null;
1458
+ const opening = resumed ? null : messages.length > 0 ? sentOpening : options?.openingMessage ?? onboarding?.greeting ?? null;
1435
1459
  return {
1436
1460
  onboarding,
1437
1461
  opening,
@@ -1731,6 +1755,7 @@ function XbNovaChat({
1731
1755
  const [activeBotId, setActiveBotId] = useState4(void 0);
1732
1756
  const [activeNudgeNodeId, setActiveNudgeNodeId] = useState4(void 0);
1733
1757
  const [nudgeQuickReplies, setNudgeQuickReplies] = useState4([]);
1758
+ const [nudgePrompt, setNudgePrompt] = useState4(null);
1734
1759
  const [openingLine, setOpeningLine] = useState4(null);
1735
1760
  const [showDisclosure, setShowDisclosure] = useState4(false);
1736
1761
  const [input, setInput] = useState4("");
@@ -1800,10 +1825,13 @@ function XbNovaChat({
1800
1825
  setOpeningLine(consumed.text);
1801
1826
  setActiveNudgeNodeId(consumed.messageNodeId);
1802
1827
  setNudgeQuickReplies(consumed.quickReplies);
1803
- sendEvent("nudge_open", { nudge_node_id: consumed.messageNodeId, bot_id: consumed.botId });
1828
+ setNudgePrompt(consumed.text);
1829
+ sendEvent("nudge_AI_click", { nudge_node_id: consumed.messageNodeId, bot_id: consumed.botId });
1804
1830
  } else {
1805
1831
  setNudgeQuickReplies([]);
1832
+ setNudgePrompt(null);
1806
1833
  setActiveNudgeNodeId(void 0);
1834
+ sendEvent("chat_AI_open", { source: "direct" });
1807
1835
  const focused = getFocusedTrackEntry(trackRegistry);
1808
1836
  setOpeningLine(
1809
1837
  focused ? `${focused.label ?? focused.region}\uC744(\uB97C) \uBCF4\uACE0 \uACC4\uC2DC\uB124\uC694. \uAD81\uAE08\uD558\uC2E0 \uC810\uC744 \uD3B8\uD558\uAC8C \uBB3C\uC5B4\uBCF4\uC138\uC694.` : null
@@ -1814,7 +1842,6 @@ function XbNovaChat({
1814
1842
  markDisclosureSeen();
1815
1843
  }
1816
1844
  setOpen(true);
1817
- sendEvent("chat_open", { from_nudge: Boolean(consumed) });
1818
1845
  try {
1819
1846
  onOpen?.({ fromNudge: Boolean(consumed), hintText: consumed?.text });
1820
1847
  } catch (err) {
@@ -2024,26 +2051,31 @@ function XbNovaChat({
2024
2051
  /* @__PURE__ */ jsx7("div", { ref: bottomRef })
2025
2052
  ] }),
2026
2053
  (() => {
2027
- if (nudgeQuickReplies.length > 0) {
2028
- return /* @__PURE__ */ jsx7("div", { className: "flex flex-wrap gap-1.5 border-t border-neutral-200 px-3.5 py-2.5 dark:border-neutral-700", children: nudgeQuickReplies.map((q) => /* @__PURE__ */ jsx7(
2029
- "button",
2030
- {
2031
- onClick: () => {
2032
- setNudgeQuickReplies([]);
2033
- send(q);
2054
+ const prompt = messages.length > 0 ? nudgePrompt : null;
2055
+ if (prompt || nudgeQuickReplies.length > 0) {
2056
+ return /* @__PURE__ */ jsxs3("div", { className: "space-y-2 border-t border-neutral-200 px-3.5 py-2.5 dark:border-neutral-700", children: [
2057
+ prompt ? /* @__PURE__ */ jsx7("div", { className: "text-sm font-medium", children: /* @__PURE__ */ jsx7(AssistantMarkdown, { text: prompt }) }) : null,
2058
+ nudgeQuickReplies.length > 0 ? /* @__PURE__ */ jsx7("div", { className: "flex flex-wrap gap-1.5", children: nudgeQuickReplies.map((q) => /* @__PURE__ */ jsx7(
2059
+ "button",
2060
+ {
2061
+ onClick: () => {
2062
+ setNudgeQuickReplies([]);
2063
+ setNudgePrompt(null);
2064
+ send(q, { nudgeOpening: prompt, inputType: "quick_reply" });
2065
+ },
2066
+ className: "xbnova-chip rounded-full border px-2.5 py-1 text-[11px] font-semibold transition",
2067
+ children: q
2034
2068
  },
2035
- className: "xbnova-chip rounded-full border px-2.5 py-1 text-[11px] font-semibold transition",
2036
- children: q
2037
- },
2038
- q
2039
- )) });
2069
+ q
2070
+ )) }) : null
2071
+ ] });
2040
2072
  }
2041
2073
  const quickReplies = onboarding?.quick_replies ?? [];
2042
2074
  if (messages.length !== 0 || quickReplies.length === 0) return null;
2043
2075
  return /* @__PURE__ */ jsx7("div", { className: "flex flex-wrap gap-1.5 border-t border-neutral-200 px-3.5 py-2.5 dark:border-neutral-700", children: quickReplies.map((q) => /* @__PURE__ */ jsx7(
2044
2076
  "button",
2045
2077
  {
2046
- onClick: () => send(q),
2078
+ onClick: () => send(q, { inputType: "quick_reply" }),
2047
2079
  className: "xbnova-chip rounded-full border px-2.5 py-1 text-[11px] font-semibold transition",
2048
2080
  children: q
2049
2081
  },
@@ -2056,7 +2088,8 @@ function XbNovaChat({
2056
2088
  onSubmit: (e) => {
2057
2089
  e.preventDefault();
2058
2090
  setNudgeQuickReplies([]);
2059
- send(input);
2091
+ setNudgePrompt(null);
2092
+ send(input, { nudgeOpening: messages.length > 0 ? nudgePrompt : null });
2060
2093
  setInput("");
2061
2094
  },
2062
2095
  className: "flex items-center gap-2 border-t border-neutral-200 px-3 py-2.5 dark:border-neutral-700",