@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 +264 -73
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +21 -2
- package/dist/index.d.ts +21 -2
- package/dist/index.js +264 -74
- package/dist/index.js.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -80,6 +80,12 @@ type XbNovaClientAction = {
|
|
|
80
80
|
type XbNovaClientActions = Record<string, XbNovaClientAction>;
|
|
81
81
|
|
|
82
82
|
type FactValue = unknown | (() => unknown);
|
|
83
|
+
/** `useXbNovaContext`로 호스트가 AI에게 알려주는 상품·화면 부가 설명 한 건. 내용은 전부
|
|
84
|
+
* 호스트 자율이다 — 가입 됨/안됨 같은 판단도 호스트가 `description`에 문장으로 쓴다. */
|
|
85
|
+
type XbNovaContextEntry = {
|
|
86
|
+
title: string;
|
|
87
|
+
description: string;
|
|
88
|
+
};
|
|
83
89
|
/** 위젯 아바타·pill 배경 그라디언트에 쓰는 두 색상 — 기본값은 SDK 자체 톤(인디고→바이올렛)
|
|
84
90
|
* 이고, 소비자가 `<XbNovaProvider theme={{accentFrom, accentTo}}>`로 자기 브랜드 컬러를
|
|
85
91
|
* 넘기면 그걸 쓴다("카통령 브랜드 컬러를 따르게 해달라" — 파랑/보라가 사이트 톤과 안
|
|
@@ -116,11 +122,15 @@ type ClientResults = Record<string, {
|
|
|
116
122
|
output: unknown;
|
|
117
123
|
}>;
|
|
118
124
|
type XbNovaFeedbackRating = "up" | "down" | null;
|
|
125
|
+
/** 👎 사유 칩의 코드. */
|
|
126
|
+
type XbNovaFeedbackReason = "inaccurate" | "irrelevant" | "too_long" | "unclear";
|
|
119
127
|
/** 답변 만족도 저장 성공 후 호스트로 전달되는 이벤트. `rating: null`은 평가 취소. */
|
|
120
128
|
type XbNovaFeedbackEvent = {
|
|
121
129
|
conversationId: string;
|
|
122
130
|
messageId: string;
|
|
123
131
|
rating: XbNovaFeedbackRating;
|
|
132
|
+
/** 👎일 때 사용자가 고른 사유 코드(없으면 undefined). */
|
|
133
|
+
reason?: XbNovaFeedbackReason;
|
|
124
134
|
};
|
|
125
135
|
type XbNovaOnboarding = {
|
|
126
136
|
conversation_id: string;
|
|
@@ -143,7 +153,7 @@ type UseXbNovaChatResult = {
|
|
|
143
153
|
/** messageId → 현재 평가. 평가 안 한 메시지는 키가 없다. */
|
|
144
154
|
feedback: Record<string, XbNovaFeedbackRating>;
|
|
145
155
|
/** 낙관적으로 반영하고 서버에 저장한다. 실패하면 되돌린다. */
|
|
146
|
-
setFeedback: (messageId: string, rating: XbNovaFeedbackRating) => Promise<void>;
|
|
156
|
+
setFeedback: (messageId: string, rating: XbNovaFeedbackRating, reason?: XbNovaFeedbackReason) => Promise<void>;
|
|
147
157
|
};
|
|
148
158
|
/** `botId`를 넘기면 Provider의 기본 봇 대신 그 봇으로 새 대화를 연다 — 넛지 handoff가 쓴다.
|
|
149
159
|
* `nudgeNodeId`를 넘기면 이 대화가 그 넛지에서 시작됐다고 서버에 표시한다 — 넛지 전환
|
|
@@ -154,6 +164,10 @@ type UseXbNovaChatResult = {
|
|
|
154
164
|
declare function useXbNovaChat(options?: {
|
|
155
165
|
botId?: string;
|
|
156
166
|
nudgeNodeId?: string;
|
|
167
|
+
/** 위젯이 첫 말풍선으로 보여준 문구(넛지 문구). 첫 메시지를 보낼 때 서버에 같이 보내 AI가
|
|
168
|
+
* "방금 이런 말을 건넸다"는 걸 알게 한다 — 없으면 봇 인사말을 쓴다. 서버가 설정과 일치하는지
|
|
169
|
+
* 검증해서 맞는 것만 이력에 저장한다. */
|
|
170
|
+
openingMessage?: string;
|
|
157
171
|
onDismissSheet?: () => void;
|
|
158
172
|
onFeedback?: (e: XbNovaFeedbackEvent) => void;
|
|
159
173
|
}): UseXbNovaChatResult;
|
|
@@ -245,6 +259,11 @@ declare function useXbNovaFact(key: string, value: FactValue): void;
|
|
|
245
259
|
/** 필드가 여러 개인 객체 하나를 한 번에 등록한다 — 매물 데이터처럼. `useXbNovaFact`를
|
|
246
260
|
* 필드마다 반복하는 것과 같다. */
|
|
247
261
|
declare function useXbNovaFacts(values: Record<string, FactValue>): void;
|
|
262
|
+
/** AI에게 알려줄 상품·화면 부가 설명을 등록한다. 넛지 조건용인 facts와 달리 **챗 요청의
|
|
263
|
+
* `context`로 전송**되어 답변의 근거가 된다. 내용(가입 가능 여부 같은 판단 포함)은 호스트가
|
|
264
|
+
* `description`에 문장으로 쓴다 — SDK는 형식 검증과 전달만 한다. 배열 또는 함수(pull) 둘 다
|
|
265
|
+
* 된다. 컴포넌트가 언마운트되면 자동으로 빠진다. */
|
|
266
|
+
declare function useXbNovaContext(entries: XbNovaContextEntry[] | (() => XbNovaContextEntry[])): void;
|
|
248
267
|
/** 방문자 식별자 — 없으면 SDK가 즉시 만들어 localStorage에 고정한 뒤 그 값을 반환한다. */
|
|
249
268
|
declare function useXbNovaVisitor(): string;
|
|
250
269
|
/** 선언형 — 서버 컴포넌트 트리에서 훅을 못 쓸 때. `<XbNovaFact id="xbnova_summary" value="..." />`. */
|
|
@@ -270,4 +289,4 @@ declare function useXbNovaSettings(): {
|
|
|
270
289
|
showBar: () => void;
|
|
271
290
|
};
|
|
272
291
|
|
|
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 };
|
|
292
|
+
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, type XbNovaContextEntry, XbNovaFact, XbNovaFacts, type XbNovaFeedbackEvent, type XbNovaFeedbackRating, type XbNovaFeedbackReason, type XbNovaOnboarding, XbNovaProvider, type XbNovaTheme, XbNovaTrack, trackXbNovaEvent, useXbNovaChat, useXbNovaContext, useXbNovaFact, useXbNovaFacts, useXbNovaSettings, useXbNovaTrack, useXbNovaVisitor };
|
package/dist/index.d.ts
CHANGED
|
@@ -80,6 +80,12 @@ type XbNovaClientAction = {
|
|
|
80
80
|
type XbNovaClientActions = Record<string, XbNovaClientAction>;
|
|
81
81
|
|
|
82
82
|
type FactValue = unknown | (() => unknown);
|
|
83
|
+
/** `useXbNovaContext`로 호스트가 AI에게 알려주는 상품·화면 부가 설명 한 건. 내용은 전부
|
|
84
|
+
* 호스트 자율이다 — 가입 됨/안됨 같은 판단도 호스트가 `description`에 문장으로 쓴다. */
|
|
85
|
+
type XbNovaContextEntry = {
|
|
86
|
+
title: string;
|
|
87
|
+
description: string;
|
|
88
|
+
};
|
|
83
89
|
/** 위젯 아바타·pill 배경 그라디언트에 쓰는 두 색상 — 기본값은 SDK 자체 톤(인디고→바이올렛)
|
|
84
90
|
* 이고, 소비자가 `<XbNovaProvider theme={{accentFrom, accentTo}}>`로 자기 브랜드 컬러를
|
|
85
91
|
* 넘기면 그걸 쓴다("카통령 브랜드 컬러를 따르게 해달라" — 파랑/보라가 사이트 톤과 안
|
|
@@ -116,11 +122,15 @@ type ClientResults = Record<string, {
|
|
|
116
122
|
output: unknown;
|
|
117
123
|
}>;
|
|
118
124
|
type XbNovaFeedbackRating = "up" | "down" | null;
|
|
125
|
+
/** 👎 사유 칩의 코드. */
|
|
126
|
+
type XbNovaFeedbackReason = "inaccurate" | "irrelevant" | "too_long" | "unclear";
|
|
119
127
|
/** 답변 만족도 저장 성공 후 호스트로 전달되는 이벤트. `rating: null`은 평가 취소. */
|
|
120
128
|
type XbNovaFeedbackEvent = {
|
|
121
129
|
conversationId: string;
|
|
122
130
|
messageId: string;
|
|
123
131
|
rating: XbNovaFeedbackRating;
|
|
132
|
+
/** 👎일 때 사용자가 고른 사유 코드(없으면 undefined). */
|
|
133
|
+
reason?: XbNovaFeedbackReason;
|
|
124
134
|
};
|
|
125
135
|
type XbNovaOnboarding = {
|
|
126
136
|
conversation_id: string;
|
|
@@ -143,7 +153,7 @@ type UseXbNovaChatResult = {
|
|
|
143
153
|
/** messageId → 현재 평가. 평가 안 한 메시지는 키가 없다. */
|
|
144
154
|
feedback: Record<string, XbNovaFeedbackRating>;
|
|
145
155
|
/** 낙관적으로 반영하고 서버에 저장한다. 실패하면 되돌린다. */
|
|
146
|
-
setFeedback: (messageId: string, rating: XbNovaFeedbackRating) => Promise<void>;
|
|
156
|
+
setFeedback: (messageId: string, rating: XbNovaFeedbackRating, reason?: XbNovaFeedbackReason) => Promise<void>;
|
|
147
157
|
};
|
|
148
158
|
/** `botId`를 넘기면 Provider의 기본 봇 대신 그 봇으로 새 대화를 연다 — 넛지 handoff가 쓴다.
|
|
149
159
|
* `nudgeNodeId`를 넘기면 이 대화가 그 넛지에서 시작됐다고 서버에 표시한다 — 넛지 전환
|
|
@@ -154,6 +164,10 @@ type UseXbNovaChatResult = {
|
|
|
154
164
|
declare function useXbNovaChat(options?: {
|
|
155
165
|
botId?: string;
|
|
156
166
|
nudgeNodeId?: string;
|
|
167
|
+
/** 위젯이 첫 말풍선으로 보여준 문구(넛지 문구). 첫 메시지를 보낼 때 서버에 같이 보내 AI가
|
|
168
|
+
* "방금 이런 말을 건넸다"는 걸 알게 한다 — 없으면 봇 인사말을 쓴다. 서버가 설정과 일치하는지
|
|
169
|
+
* 검증해서 맞는 것만 이력에 저장한다. */
|
|
170
|
+
openingMessage?: string;
|
|
157
171
|
onDismissSheet?: () => void;
|
|
158
172
|
onFeedback?: (e: XbNovaFeedbackEvent) => void;
|
|
159
173
|
}): UseXbNovaChatResult;
|
|
@@ -245,6 +259,11 @@ declare function useXbNovaFact(key: string, value: FactValue): void;
|
|
|
245
259
|
/** 필드가 여러 개인 객체 하나를 한 번에 등록한다 — 매물 데이터처럼. `useXbNovaFact`를
|
|
246
260
|
* 필드마다 반복하는 것과 같다. */
|
|
247
261
|
declare function useXbNovaFacts(values: Record<string, FactValue>): void;
|
|
262
|
+
/** AI에게 알려줄 상품·화면 부가 설명을 등록한다. 넛지 조건용인 facts와 달리 **챗 요청의
|
|
263
|
+
* `context`로 전송**되어 답변의 근거가 된다. 내용(가입 가능 여부 같은 판단 포함)은 호스트가
|
|
264
|
+
* `description`에 문장으로 쓴다 — SDK는 형식 검증과 전달만 한다. 배열 또는 함수(pull) 둘 다
|
|
265
|
+
* 된다. 컴포넌트가 언마운트되면 자동으로 빠진다. */
|
|
266
|
+
declare function useXbNovaContext(entries: XbNovaContextEntry[] | (() => XbNovaContextEntry[])): void;
|
|
248
267
|
/** 방문자 식별자 — 없으면 SDK가 즉시 만들어 localStorage에 고정한 뒤 그 값을 반환한다. */
|
|
249
268
|
declare function useXbNovaVisitor(): string;
|
|
250
269
|
/** 선언형 — 서버 컴포넌트 트리에서 훅을 못 쓸 때. `<XbNovaFact id="xbnova_summary" value="..." />`. */
|
|
@@ -270,4 +289,4 @@ declare function useXbNovaSettings(): {
|
|
|
270
289
|
showBar: () => void;
|
|
271
290
|
};
|
|
272
291
|
|
|
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 };
|
|
292
|
+
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, type XbNovaContextEntry, XbNovaFact, XbNovaFacts, type XbNovaFeedbackEvent, type XbNovaFeedbackRating, type XbNovaFeedbackReason, type XbNovaOnboarding, XbNovaProvider, type XbNovaTheme, XbNovaTrack, trackXbNovaEvent, useXbNovaChat, useXbNovaContext, useXbNovaFact, useXbNovaFacts, useXbNovaSettings, useXbNovaTrack, useXbNovaVisitor };
|
package/dist/index.js
CHANGED
|
@@ -40,6 +40,17 @@ function resolveVisitorId() {
|
|
|
40
40
|
function resolveFactValue(value) {
|
|
41
41
|
return typeof value === "function" ? value() : value;
|
|
42
42
|
}
|
|
43
|
+
function collectContextEntries(registry) {
|
|
44
|
+
const out = [];
|
|
45
|
+
for (const source of registry.values()) {
|
|
46
|
+
const entries = typeof source === "function" ? source() : source;
|
|
47
|
+
if (!Array.isArray(entries)) continue;
|
|
48
|
+
for (const e of entries) {
|
|
49
|
+
if (e && typeof e.title === "string" && typeof e.description === "string") out.push(e);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
43
54
|
function InternalProvider({
|
|
44
55
|
auth,
|
|
45
56
|
clientActionHandlers,
|
|
@@ -48,6 +59,7 @@ function InternalProvider({
|
|
|
48
59
|
}) {
|
|
49
60
|
const trackRegistry = useRef(/* @__PURE__ */ new Map()).current;
|
|
50
61
|
const factsRegistry = useRef(/* @__PURE__ */ new Map()).current;
|
|
62
|
+
const contextRegistry = useRef(/* @__PURE__ */ new Map()).current;
|
|
51
63
|
const emptyHandlers = useRef(/* @__PURE__ */ new Map()).current;
|
|
52
64
|
const visitorIdRef = useRef(null);
|
|
53
65
|
if (visitorIdRef.current === null) {
|
|
@@ -58,11 +70,12 @@ function InternalProvider({
|
|
|
58
70
|
auth,
|
|
59
71
|
trackRegistry,
|
|
60
72
|
factsRegistry,
|
|
73
|
+
contextRegistry,
|
|
61
74
|
clientActionHandlers: clientActionHandlers ?? emptyHandlers,
|
|
62
75
|
visitorId: visitorIdRef.current,
|
|
63
76
|
theme
|
|
64
77
|
}),
|
|
65
|
-
[auth, trackRegistry, factsRegistry, clientActionHandlers, emptyHandlers, theme]
|
|
78
|
+
[auth, trackRegistry, factsRegistry, contextRegistry, clientActionHandlers, emptyHandlers, theme]
|
|
66
79
|
);
|
|
67
80
|
return /* @__PURE__ */ jsx(InternalContext.Provider, { value, children });
|
|
68
81
|
}
|
|
@@ -1089,7 +1102,7 @@ async function resumeToolResult(apiBaseUrl, apiKey, body, handlers) {
|
|
|
1089
1102
|
// src/use-chat.ts
|
|
1090
1103
|
var DEFAULT_CLIENT_ACTION_TIMEOUT_MS = 15e3;
|
|
1091
1104
|
function useXbNovaChat(options) {
|
|
1092
|
-
const { auth, trackRegistry, clientActionHandlers, visitorId } = useXbNovaInternal();
|
|
1105
|
+
const { auth, trackRegistry, contextRegistry, clientActionHandlers, visitorId } = useXbNovaInternal();
|
|
1093
1106
|
const botId = options?.botId ?? auth.botId;
|
|
1094
1107
|
const nudgeNodeId = options?.nudgeNodeId;
|
|
1095
1108
|
const { apiKey, apiBaseUrl } = auth;
|
|
@@ -1128,7 +1141,7 @@ function useXbNovaChat(options) {
|
|
|
1128
1141
|
next[next.length - 1] = { ...next[next.length - 1], id };
|
|
1129
1142
|
return next;
|
|
1130
1143
|
});
|
|
1131
|
-
async function setFeedback(messageId, rating) {
|
|
1144
|
+
async function setFeedback(messageId, rating, reason) {
|
|
1132
1145
|
const conversationId = onboarding?.conversation_id;
|
|
1133
1146
|
if (!conversationId || !messageId) return;
|
|
1134
1147
|
const prevRating = feedbackRef.current[messageId] ?? null;
|
|
@@ -1145,11 +1158,11 @@ function useXbNovaChat(options) {
|
|
|
1145
1158
|
{
|
|
1146
1159
|
method: "PUT",
|
|
1147
1160
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
|
|
1148
|
-
body: JSON.stringify({ rating, visitor_id: visitorId })
|
|
1161
|
+
body: JSON.stringify({ rating, visitor_id: visitorId, reason: rating === "down" ? reason ?? null : null })
|
|
1149
1162
|
}
|
|
1150
1163
|
);
|
|
1151
1164
|
if (!res.ok) throw new Error(`${res.status}`);
|
|
1152
|
-
options?.onFeedback?.({ conversationId, messageId, rating });
|
|
1165
|
+
options?.onFeedback?.({ conversationId, messageId, rating, reason });
|
|
1153
1166
|
} catch (e) {
|
|
1154
1167
|
console.warn("[xbnova] \uD53C\uB4DC\uBC31 \uC800\uC7A5 \uC2E4\uD328", e);
|
|
1155
1168
|
apply(prevRating);
|
|
@@ -1241,7 +1254,9 @@ function useXbNovaChat(options) {
|
|
|
1241
1254
|
return next;
|
|
1242
1255
|
});
|
|
1243
1256
|
const focused = getFocusedTrackEntry(trackRegistry);
|
|
1244
|
-
const
|
|
1257
|
+
const notes = collectContextEntries(contextRegistry);
|
|
1258
|
+
const screen = focused ? { region: focused.region, data: resolveFactValue(focused.data) } : {};
|
|
1259
|
+
const context = focused || notes.length ? { ...screen, ...notes.length ? { notes } : {} } : void 0;
|
|
1245
1260
|
try {
|
|
1246
1261
|
await streamChat(
|
|
1247
1262
|
apiBaseUrl,
|
|
@@ -1251,7 +1266,8 @@ function useXbNovaChat(options) {
|
|
|
1251
1266
|
message: trimmed,
|
|
1252
1267
|
conversation_id: onboarding?.conversation_id ?? null,
|
|
1253
1268
|
context,
|
|
1254
|
-
nudge_node_id: nudgeNodeId ?? null
|
|
1269
|
+
nudge_node_id: nudgeNodeId ?? null,
|
|
1270
|
+
opening_message: messages.length === 0 ? (options?.openingMessage ?? onboarding?.greeting) || null : null
|
|
1255
1271
|
},
|
|
1256
1272
|
{ onPart: replaceLast, onMessageId: setLastAssistantId, onError: setError }
|
|
1257
1273
|
);
|
|
@@ -1550,6 +1566,7 @@ function XbNovaChat({
|
|
|
1550
1566
|
onFeedback,
|
|
1551
1567
|
botId: activeBotId ?? botId,
|
|
1552
1568
|
nudgeNodeId: activeNudgeNodeId,
|
|
1569
|
+
openingMessage: openingLine ?? void 0,
|
|
1553
1570
|
onDismissSheet: () => setOpen(false)
|
|
1554
1571
|
});
|
|
1555
1572
|
const effectiveBotId = activeBotId ?? botId;
|
|
@@ -1776,38 +1793,41 @@ function XbNovaChat({
|
|
|
1776
1793
|
/* @__PURE__ */ jsxs3("div", { className: "flex-1 space-y-3 overflow-y-auto p-4", children: [
|
|
1777
1794
|
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
1795
|
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(
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1796
|
+
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(
|
|
1797
|
+
AnswerActionsFrame,
|
|
1798
|
+
{
|
|
1799
|
+
enabled: feedbackEnabled && !busy && !!message.id && message.parts.some((p) => p.kind === "text" && p.text.trim()),
|
|
1800
|
+
text: message.parts.filter((p) => p.kind === "text").map((p) => p.kind === "text" ? p.text : "").join("\n\n"),
|
|
1801
|
+
rating: message.id ? feedback[message.id] ?? null : null,
|
|
1802
|
+
isLast: i === messages.length - 1,
|
|
1803
|
+
onRate: (r, reason) => void setFeedback(message.id, r, reason),
|
|
1804
|
+
children: [
|
|
1805
|
+
message.parts.map((part, j) => {
|
|
1806
|
+
if (part.kind === "text") {
|
|
1807
|
+
return /* @__PURE__ */ jsx7(Bubble, { role: "assistant", children: /* @__PURE__ */ jsx7(AssistantMarkdown, { text: part.text }) }, j);
|
|
1808
|
+
}
|
|
1809
|
+
if (clientActionHandlers.has(part.toolName)) {
|
|
1810
|
+
const result = clientResults[part.toolCallId];
|
|
1811
|
+
if (result) {
|
|
1812
|
+
return /* @__PURE__ */ jsx7(
|
|
1813
|
+
ToolView,
|
|
1814
|
+
{
|
|
1815
|
+
part: { ...part, state: "output", view: result.view, output: result.output },
|
|
1816
|
+
viewRenderers,
|
|
1817
|
+
onVerdictAction
|
|
1818
|
+
},
|
|
1819
|
+
j
|
|
1820
|
+
);
|
|
1821
|
+
}
|
|
1822
|
+
const spec = clientActionHandlers.get(part.toolName);
|
|
1823
|
+
return spec?.render ? /* @__PURE__ */ jsx7("div", { children: spec.render(part.input) }, j) : null;
|
|
1824
|
+
}
|
|
1825
|
+
return /* @__PURE__ */ jsx7(ToolView, { part, viewRenderers, onVerdictAction }, j);
|
|
1826
|
+
}),
|
|
1827
|
+
!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
|
|
1828
|
+
]
|
|
1829
|
+
}
|
|
1830
|
+
) }, i)),
|
|
1811
1831
|
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
1832
|
/* @__PURE__ */ jsx7("div", { ref: bottomRef })
|
|
1813
1833
|
] }),
|
|
@@ -1910,44 +1930,193 @@ function Avatar({
|
|
|
1910
1930
|
}
|
|
1911
1931
|
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
1932
|
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
|
-
|
|
1933
|
+
var COPY_PATH = "M9 9h11v11H9zM5 15H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1";
|
|
1934
|
+
var CHECK_PATH = "m5 12 5 5L20 7";
|
|
1935
|
+
var FEEDBACK_REASONS = [
|
|
1936
|
+
{ code: "inaccurate", label: "\uC815\uD655\uD558\uC9C0 \uC54A\uC544\uC694" },
|
|
1937
|
+
{ code: "irrelevant", label: "\uC9C8\uBB38\uACFC \uB2EC\uB77C\uC694" },
|
|
1938
|
+
{ code: "too_long", label: "\uB108\uBB34 \uAE38\uC5B4\uC694" },
|
|
1939
|
+
{ code: "unclear", label: "\uC774\uD574\uD558\uAE30 \uC5B4\uB824\uC6CC\uC694" }
|
|
1940
|
+
];
|
|
1941
|
+
var LONG_PRESS_MS = 420;
|
|
1942
|
+
function ActionIcon({ path, filled }) {
|
|
1943
|
+
return /* @__PURE__ */ jsx7(
|
|
1944
|
+
"svg",
|
|
1945
|
+
{
|
|
1946
|
+
width: "16",
|
|
1947
|
+
height: "16",
|
|
1948
|
+
viewBox: "0 0 24 24",
|
|
1949
|
+
fill: filled ? "currentColor" : "none",
|
|
1950
|
+
stroke: "currentColor",
|
|
1951
|
+
strokeWidth: "2",
|
|
1952
|
+
strokeLinecap: "round",
|
|
1953
|
+
strokeLinejoin: "round",
|
|
1954
|
+
"aria-hidden": "true",
|
|
1955
|
+
children: /* @__PURE__ */ jsx7("path", { d: path })
|
|
1956
|
+
}
|
|
1957
|
+
);
|
|
1958
|
+
}
|
|
1959
|
+
async function copyText(text) {
|
|
1960
|
+
try {
|
|
1961
|
+
await navigator.clipboard.writeText(text);
|
|
1962
|
+
return true;
|
|
1963
|
+
} catch {
|
|
1964
|
+
try {
|
|
1965
|
+
const ta = document.createElement("textarea");
|
|
1966
|
+
ta.value = text;
|
|
1967
|
+
ta.style.position = "fixed";
|
|
1968
|
+
ta.style.opacity = "0";
|
|
1969
|
+
document.body.appendChild(ta);
|
|
1970
|
+
ta.select();
|
|
1971
|
+
const ok = document.execCommand("copy");
|
|
1972
|
+
ta.remove();
|
|
1973
|
+
return ok;
|
|
1974
|
+
} catch {
|
|
1975
|
+
return false;
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
function AnswerActions({
|
|
1980
|
+
text,
|
|
1914
1981
|
rating,
|
|
1915
|
-
|
|
1982
|
+
isLast,
|
|
1983
|
+
onRate,
|
|
1984
|
+
children
|
|
1916
1985
|
}) {
|
|
1917
|
-
const
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1986
|
+
const rootRef = useRef8(null);
|
|
1987
|
+
const timerRef = useRef8(null);
|
|
1988
|
+
const [menuOpen, setMenuOpen] = useState4(false);
|
|
1989
|
+
const [reasonOpen, setReasonOpen] = useState4(false);
|
|
1990
|
+
const [copied, setCopied] = useState4(false);
|
|
1991
|
+
const [entered, setEntered] = useState4(false);
|
|
1992
|
+
const clearTimer = () => {
|
|
1993
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
1994
|
+
timerRef.current = null;
|
|
1995
|
+
};
|
|
1996
|
+
useEffect8(() => clearTimer, []);
|
|
1997
|
+
useEffect8(() => {
|
|
1998
|
+
if (!menuOpen) {
|
|
1999
|
+
setEntered(false);
|
|
2000
|
+
return;
|
|
2001
|
+
}
|
|
2002
|
+
const raf = requestAnimationFrame(() => setEntered(true));
|
|
2003
|
+
const onDown = (e) => {
|
|
2004
|
+
if (!e.composedPath().includes(rootRef.current)) setMenuOpen(false);
|
|
2005
|
+
};
|
|
2006
|
+
document.addEventListener("pointerdown", onDown, true);
|
|
2007
|
+
return () => {
|
|
2008
|
+
cancelAnimationFrame(raf);
|
|
2009
|
+
document.removeEventListener("pointerdown", onDown, true);
|
|
2010
|
+
};
|
|
2011
|
+
}, [menuOpen]);
|
|
2012
|
+
const startPress = (e) => {
|
|
2013
|
+
if (e.pointerType === "mouse" || isLast) return;
|
|
2014
|
+
clearTimer();
|
|
2015
|
+
timerRef.current = setTimeout(() => {
|
|
2016
|
+
setMenuOpen(true);
|
|
2017
|
+
if (typeof navigator.vibrate === "function") navigator.vibrate(10);
|
|
2018
|
+
}, LONG_PRESS_MS);
|
|
2019
|
+
};
|
|
2020
|
+
const handleCopy = async () => {
|
|
2021
|
+
if (await copyText(text)) {
|
|
2022
|
+
setCopied(true);
|
|
2023
|
+
setTimeout(() => setCopied(false), 1400);
|
|
2024
|
+
}
|
|
2025
|
+
};
|
|
2026
|
+
const handleRate = (kind) => {
|
|
2027
|
+
if (rating === kind) {
|
|
2028
|
+
onRate(null);
|
|
2029
|
+
setReasonOpen(false);
|
|
2030
|
+
return;
|
|
2031
|
+
}
|
|
2032
|
+
onRate(kind);
|
|
2033
|
+
setReasonOpen(kind === "down");
|
|
2034
|
+
if (kind === "up") setMenuOpen(false);
|
|
2035
|
+
};
|
|
2036
|
+
const barVisible = isLast || menuOpen;
|
|
2037
|
+
const floating = menuOpen && !isLast;
|
|
2038
|
+
const btn = (label, onClick, selected, color, icon) => /* @__PURE__ */ jsxs3(
|
|
2039
|
+
"button",
|
|
2040
|
+
{
|
|
2041
|
+
type: "button",
|
|
2042
|
+
"aria-label": label,
|
|
2043
|
+
"aria-pressed": selected,
|
|
2044
|
+
onClick,
|
|
2045
|
+
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"}`,
|
|
2046
|
+
style: color ? { color } : void 0,
|
|
2047
|
+
children: [
|
|
2048
|
+
icon,
|
|
2049
|
+
floating ? /* @__PURE__ */ jsx7("span", { children: label }) : null
|
|
2050
|
+
]
|
|
2051
|
+
}
|
|
2052
|
+
);
|
|
2053
|
+
return /* @__PURE__ */ jsxs3(
|
|
2054
|
+
"div",
|
|
2055
|
+
{
|
|
2056
|
+
ref: rootRef,
|
|
2057
|
+
className: "group space-y-1",
|
|
2058
|
+
onPointerDown: startPress,
|
|
2059
|
+
onPointerUp: clearTimer,
|
|
2060
|
+
onPointerCancel: clearTimer,
|
|
2061
|
+
onPointerLeave: clearTimer,
|
|
2062
|
+
onPointerMove: (e) => {
|
|
2063
|
+
if (Math.abs(e.movementX) + Math.abs(e.movementY) > 6) clearTimer();
|
|
2064
|
+
},
|
|
2065
|
+
onContextMenu: (e) => {
|
|
2066
|
+
if (!isLast && e.nativeEvent.pointerType !== "mouse") e.preventDefault();
|
|
2067
|
+
},
|
|
2068
|
+
children: [
|
|
2069
|
+
children,
|
|
2070
|
+
/* @__PURE__ */ jsx7(
|
|
2071
|
+
"div",
|
|
1931
2072
|
{
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
2073
|
+
className: `flex pl-1 ${barVisible ? "" : "hidden md:group-hover:flex"}`,
|
|
2074
|
+
style: { transition: "opacity 140ms, transform 140ms", opacity: floating && !entered ? 0 : 1, transform: floating && !entered ? "scale(0.94)" : "none" },
|
|
2075
|
+
children: /* @__PURE__ */ jsxs3(
|
|
2076
|
+
"div",
|
|
2077
|
+
{
|
|
2078
|
+
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"}`,
|
|
2079
|
+
children: [
|
|
2080
|
+
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 })),
|
|
2081
|
+
btn(
|
|
2082
|
+
"\uC88B\uC544\uC694",
|
|
2083
|
+
() => handleRate("up"),
|
|
2084
|
+
rating === "up",
|
|
2085
|
+
rating === "up" ? "var(--xbnova-accent-1, #4f46e5)" : void 0,
|
|
2086
|
+
/* @__PURE__ */ jsx7(ActionIcon, { path: THUMB_UP_PATH, filled: rating === "up" })
|
|
2087
|
+
),
|
|
2088
|
+
btn(
|
|
2089
|
+
"\uBCC4\uB85C\uC608\uC694",
|
|
2090
|
+
() => handleRate("down"),
|
|
2091
|
+
rating === "down",
|
|
2092
|
+
rating === "down" ? "#dc2626" : void 0,
|
|
2093
|
+
/* @__PURE__ */ jsx7(ActionIcon, { path: THUMB_DOWN_PATH, filled: rating === "down" })
|
|
2094
|
+
)
|
|
2095
|
+
]
|
|
2096
|
+
}
|
|
2097
|
+
)
|
|
1942
2098
|
}
|
|
1943
|
-
)
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
2099
|
+
),
|
|
2100
|
+
rating === "down" && reasonOpen ? /* @__PURE__ */ jsxs3("div", { className: "flex flex-wrap items-center gap-1.5 pl-1 pt-0.5", children: [
|
|
2101
|
+
/* @__PURE__ */ jsx7("span", { className: "text-[11px] text-neutral-400", children: "\uC5B4\uB5A4 \uC810\uC774 \uC544\uC26C\uC6E0\uB098\uC694?" }),
|
|
2102
|
+
FEEDBACK_REASONS.map((r) => /* @__PURE__ */ jsx7(
|
|
2103
|
+
"button",
|
|
2104
|
+
{
|
|
2105
|
+
type: "button",
|
|
2106
|
+
onClick: () => {
|
|
2107
|
+
onRate("down", r.code);
|
|
2108
|
+
setReasonOpen(false);
|
|
2109
|
+
setMenuOpen(false);
|
|
2110
|
+
},
|
|
2111
|
+
className: "xbnova-chip rounded-full border px-2.5 py-1 text-[11px] font-semibold transition",
|
|
2112
|
+
children: r.label
|
|
2113
|
+
},
|
|
2114
|
+
r.code
|
|
2115
|
+
))
|
|
2116
|
+
] }) : null
|
|
2117
|
+
]
|
|
2118
|
+
}
|
|
2119
|
+
);
|
|
1951
2120
|
}
|
|
1952
2121
|
function Bubble({ role, children }) {
|
|
1953
2122
|
const mine = role === "user";
|
|
@@ -1962,9 +2131,17 @@ function Bubble({ role, children }) {
|
|
|
1962
2131
|
function AssistantMarkdown({ text }) {
|
|
1963
2132
|
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
2133
|
}
|
|
2134
|
+
function AnswerActionsFrame({
|
|
2135
|
+
enabled,
|
|
2136
|
+
children,
|
|
2137
|
+
...rest
|
|
2138
|
+
}) {
|
|
2139
|
+
if (!enabled) return /* @__PURE__ */ jsx7("div", { className: "space-y-1", children });
|
|
2140
|
+
return /* @__PURE__ */ jsx7(AnswerActions, { ...rest, children });
|
|
2141
|
+
}
|
|
1965
2142
|
|
|
1966
2143
|
// src/facts.ts
|
|
1967
|
-
import { useEffect as useEffect9, useRef as useRef9 } from "react";
|
|
2144
|
+
import { useEffect as useEffect9, useId, useRef as useRef9 } from "react";
|
|
1968
2145
|
function useXbNovaFact(key, value) {
|
|
1969
2146
|
const { factsRegistry } = useXbNovaInternal();
|
|
1970
2147
|
useEffect9(() => {
|
|
@@ -1993,6 +2170,18 @@ function useXbNovaFacts(values) {
|
|
|
1993
2170
|
};
|
|
1994
2171
|
}, [factsRegistry]);
|
|
1995
2172
|
}
|
|
2173
|
+
function useXbNovaContext(entries) {
|
|
2174
|
+
const { contextRegistry } = useXbNovaInternal();
|
|
2175
|
+
const id = useId();
|
|
2176
|
+
useEffect9(() => {
|
|
2177
|
+
contextRegistry.set(id, entries);
|
|
2178
|
+
});
|
|
2179
|
+
useEffect9(() => {
|
|
2180
|
+
return () => {
|
|
2181
|
+
contextRegistry.delete(id);
|
|
2182
|
+
};
|
|
2183
|
+
}, [id, contextRegistry]);
|
|
2184
|
+
}
|
|
1996
2185
|
function useXbNovaVisitor() {
|
|
1997
2186
|
const { visitorId } = useXbNovaInternal();
|
|
1998
2187
|
return visitorId;
|
|
@@ -2013,6 +2202,7 @@ export {
|
|
|
2013
2202
|
XbNovaTrack,
|
|
2014
2203
|
trackXbNovaEvent,
|
|
2015
2204
|
useXbNovaChat,
|
|
2205
|
+
useXbNovaContext,
|
|
2016
2206
|
useXbNovaFact,
|
|
2017
2207
|
useXbNovaFacts,
|
|
2018
2208
|
useXbNovaSettings,
|