@xbrain-swe/nova-chat-widget 0.1.7 → 0.1.9
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 +142 -23
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +55 -37
- package/dist/index.d.ts +55 -37
- package/dist/index.js +142 -23
- package/dist/index.js.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -24,6 +24,8 @@ type TextPart = {
|
|
|
24
24
|
};
|
|
25
25
|
type MessagePart = TextPart | ToolPart;
|
|
26
26
|
type ChatMessage = {
|
|
27
|
+
/** assistant 메시지가 서버에 저장될 때의 id(`start` 청크의 `messageId`). 만족도 피드백 대상 식별용. */
|
|
28
|
+
id?: string;
|
|
27
29
|
role: "user" | "assistant";
|
|
28
30
|
parts: MessagePart[];
|
|
29
31
|
};
|
|
@@ -109,6 +111,53 @@ declare function XbNovaProvider({ apiKey, botId, appId, screen, apiBaseUrl, clie
|
|
|
109
111
|
children: React.ReactNode;
|
|
110
112
|
}): react.JSX.Element;
|
|
111
113
|
|
|
114
|
+
type ClientResults = Record<string, {
|
|
115
|
+
view: ViewKind;
|
|
116
|
+
output: unknown;
|
|
117
|
+
}>;
|
|
118
|
+
type XbNovaFeedbackRating = "up" | "down" | null;
|
|
119
|
+
/** 답변 만족도 저장 성공 후 호스트로 전달되는 이벤트. `rating: null`은 평가 취소. */
|
|
120
|
+
type XbNovaFeedbackEvent = {
|
|
121
|
+
conversationId: string;
|
|
122
|
+
messageId: string;
|
|
123
|
+
rating: XbNovaFeedbackRating;
|
|
124
|
+
};
|
|
125
|
+
type XbNovaOnboarding = {
|
|
126
|
+
conversation_id: string;
|
|
127
|
+
greeting: string;
|
|
128
|
+
quick_replies: string[];
|
|
129
|
+
branding: {
|
|
130
|
+
title?: string;
|
|
131
|
+
logo_url?: string;
|
|
132
|
+
avatar_text?: string;
|
|
133
|
+
};
|
|
134
|
+
};
|
|
135
|
+
type UseXbNovaChatResult = {
|
|
136
|
+
onboarding: XbNovaOnboarding | null;
|
|
137
|
+
messages: ChatMessage[];
|
|
138
|
+
busy: boolean;
|
|
139
|
+
error: string | null;
|
|
140
|
+
send: (text: string) => Promise<void>;
|
|
141
|
+
/** client action 핸들러가 돌려준 정규화 결과(tool_call_id별). 채팅 화면이 view 렌더러로 그린다. */
|
|
142
|
+
clientResults: ClientResults;
|
|
143
|
+
/** messageId → 현재 평가. 평가 안 한 메시지는 키가 없다. */
|
|
144
|
+
feedback: Record<string, XbNovaFeedbackRating>;
|
|
145
|
+
/** 낙관적으로 반영하고 서버에 저장한다. 실패하면 되돌린다. */
|
|
146
|
+
setFeedback: (messageId: string, rating: XbNovaFeedbackRating) => Promise<void>;
|
|
147
|
+
};
|
|
148
|
+
/** `botId`를 넘기면 Provider의 기본 봇 대신 그 봇으로 새 대화를 연다 — 넛지 handoff가 쓴다.
|
|
149
|
+
* `nudgeNodeId`를 넘기면 이 대화가 그 넛지에서 시작됐다고 서버에 표시한다 — 넛지 전환
|
|
150
|
+
* 퍼널(노출→클릭→첫질문→전환)의 마지막 단계가 이 값이 채워진 대화 수를 센다.
|
|
151
|
+
* `onDismissSheet`는 `dismissSheet: true`인 client action이 실행될 때 호출된다 —
|
|
152
|
+
* `<XbNovaChat>`이 시트를 닫는 함수를 넘겨준다(모바일에서 시트가 페이지 대부분을 덮어서,
|
|
153
|
+
* 안 닫으면 그 밑에서 일어나는 일이 안 보인다). */
|
|
154
|
+
declare function useXbNovaChat(options?: {
|
|
155
|
+
botId?: string;
|
|
156
|
+
nudgeNodeId?: string;
|
|
157
|
+
onDismissSheet?: () => void;
|
|
158
|
+
onFeedback?: (e: XbNovaFeedbackEvent) => void;
|
|
159
|
+
}): UseXbNovaChatResult;
|
|
160
|
+
|
|
112
161
|
type ProductItem = {
|
|
113
162
|
id: string;
|
|
114
163
|
title: string;
|
|
@@ -148,7 +197,7 @@ type ViewRenderer = (props: {
|
|
|
148
197
|
}) => React.ReactNode;
|
|
149
198
|
type ViewRendererMap = Partial<Record<ViewKind, ViewRenderer>>;
|
|
150
199
|
|
|
151
|
-
declare function XbNovaChat({ viewRenderers, botId, barStyle, panelStyle, backdropStyle, defaultOpen, idleText, avatarSize, onVerdictAction, }: {
|
|
200
|
+
declare function XbNovaChat({ viewRenderers, botId, barStyle, panelStyle, backdropStyle, defaultOpen, idleText, avatarSize, onVerdictAction, feedback: feedbackEnabled, onFeedback, }: {
|
|
152
201
|
/** 특정 view만 자체 컴포넌트로 덮어쓰고 싶을 때 — 지정 안 한 view는 기본 렌더러를 쓴다. */
|
|
153
202
|
viewRenderers?: ViewRendererMap;
|
|
154
203
|
/** Provider의 기본 봇 대신 다른 봇과 고정적으로 대화하고 싶을 때. 힌트를 눌러 열면
|
|
@@ -173,43 +222,12 @@ declare function XbNovaChat({ viewRenderers, botId, barStyle, panelStyle, backdr
|
|
|
173
222
|
/** 판단 카드의 행동 버튼을 눌렀을 때 — 실제 동작(상담신청 폼 열기, 섹션 스크롤 등)은
|
|
174
223
|
* 소비자가 정한다. SDK는 버튼에 적힌 `action` 문자열을 그대로 넘겨줄 뿐 뜻을 모른다. */
|
|
175
224
|
onVerdictAction?: (action: string) => void;
|
|
225
|
+
/** 완료된 답변 아래 👍/👎 "상담 만족도" 버튼을 보일지. 기본 true. */
|
|
226
|
+
feedback?: boolean;
|
|
227
|
+
/** 만족도가 서버에 저장된 뒤 호출된다(분석 이벤트 전송용). `rating: null`은 평가 취소. */
|
|
228
|
+
onFeedback?: (e: XbNovaFeedbackEvent) => void;
|
|
176
229
|
}): react.JSX.Element | null;
|
|
177
230
|
|
|
178
|
-
type ClientResults = Record<string, {
|
|
179
|
-
view: ViewKind;
|
|
180
|
-
output: unknown;
|
|
181
|
-
}>;
|
|
182
|
-
type XbNovaOnboarding = {
|
|
183
|
-
conversation_id: string;
|
|
184
|
-
greeting: string;
|
|
185
|
-
quick_replies: string[];
|
|
186
|
-
branding: {
|
|
187
|
-
title?: string;
|
|
188
|
-
logo_url?: string;
|
|
189
|
-
avatar_text?: string;
|
|
190
|
-
};
|
|
191
|
-
};
|
|
192
|
-
type UseXbNovaChatResult = {
|
|
193
|
-
onboarding: XbNovaOnboarding | null;
|
|
194
|
-
messages: ChatMessage[];
|
|
195
|
-
busy: boolean;
|
|
196
|
-
error: string | null;
|
|
197
|
-
send: (text: string) => Promise<void>;
|
|
198
|
-
/** client action 핸들러가 돌려준 정규화 결과(tool_call_id별). 채팅 화면이 view 렌더러로 그린다. */
|
|
199
|
-
clientResults: ClientResults;
|
|
200
|
-
};
|
|
201
|
-
/** `botId`를 넘기면 Provider의 기본 봇 대신 그 봇으로 새 대화를 연다 — 넛지 handoff가 쓴다.
|
|
202
|
-
* `nudgeNodeId`를 넘기면 이 대화가 그 넛지에서 시작됐다고 서버에 표시한다 — 넛지 전환
|
|
203
|
-
* 퍼널(노출→클릭→첫질문→전환)의 마지막 단계가 이 값이 채워진 대화 수를 센다.
|
|
204
|
-
* `onDismissSheet`는 `dismissSheet: true`인 client action이 실행될 때 호출된다 —
|
|
205
|
-
* `<XbNovaChat>`이 시트를 닫는 함수를 넘겨준다(모바일에서 시트가 페이지 대부분을 덮어서,
|
|
206
|
-
* 안 닫으면 그 밑에서 일어나는 일이 안 보인다). */
|
|
207
|
-
declare function useXbNovaChat(options?: {
|
|
208
|
-
botId?: string;
|
|
209
|
-
nudgeNodeId?: string;
|
|
210
|
-
onDismissSheet?: () => void;
|
|
211
|
-
}): UseXbNovaChatResult;
|
|
212
|
-
|
|
213
231
|
/** ref형 — 감싸는 엘리먼트를 늘리기 싫을 때. `<div ref={useXbNovaTrack("price", data)}>`. */
|
|
214
232
|
declare function useXbNovaTrack<T extends HTMLElement = HTMLElement>(region: string, data?: FactValue, label?: string): (node: T | null) => void;
|
|
215
233
|
/** 선언형 — 감싸기만 하면 됨. `<XbNovaTrack region="price" data={{...}}>...</XbNovaTrack>`. */
|
|
@@ -252,4 +270,4 @@ declare function useXbNovaSettings(): {
|
|
|
252
270
|
showBar: () => void;
|
|
253
271
|
};
|
|
254
272
|
|
|
255
|
-
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 XbNovaOnboarding, XbNovaProvider, type XbNovaTheme, XbNovaTrack, trackXbNovaEvent, useXbNovaChat, useXbNovaFact, useXbNovaFacts, useXbNovaSettings, useXbNovaTrack, useXbNovaVisitor };
|
|
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 };
|
package/dist/index.d.ts
CHANGED
|
@@ -24,6 +24,8 @@ type TextPart = {
|
|
|
24
24
|
};
|
|
25
25
|
type MessagePart = TextPart | ToolPart;
|
|
26
26
|
type ChatMessage = {
|
|
27
|
+
/** assistant 메시지가 서버에 저장될 때의 id(`start` 청크의 `messageId`). 만족도 피드백 대상 식별용. */
|
|
28
|
+
id?: string;
|
|
27
29
|
role: "user" | "assistant";
|
|
28
30
|
parts: MessagePart[];
|
|
29
31
|
};
|
|
@@ -109,6 +111,53 @@ declare function XbNovaProvider({ apiKey, botId, appId, screen, apiBaseUrl, clie
|
|
|
109
111
|
children: React.ReactNode;
|
|
110
112
|
}): react.JSX.Element;
|
|
111
113
|
|
|
114
|
+
type ClientResults = Record<string, {
|
|
115
|
+
view: ViewKind;
|
|
116
|
+
output: unknown;
|
|
117
|
+
}>;
|
|
118
|
+
type XbNovaFeedbackRating = "up" | "down" | null;
|
|
119
|
+
/** 답변 만족도 저장 성공 후 호스트로 전달되는 이벤트. `rating: null`은 평가 취소. */
|
|
120
|
+
type XbNovaFeedbackEvent = {
|
|
121
|
+
conversationId: string;
|
|
122
|
+
messageId: string;
|
|
123
|
+
rating: XbNovaFeedbackRating;
|
|
124
|
+
};
|
|
125
|
+
type XbNovaOnboarding = {
|
|
126
|
+
conversation_id: string;
|
|
127
|
+
greeting: string;
|
|
128
|
+
quick_replies: string[];
|
|
129
|
+
branding: {
|
|
130
|
+
title?: string;
|
|
131
|
+
logo_url?: string;
|
|
132
|
+
avatar_text?: string;
|
|
133
|
+
};
|
|
134
|
+
};
|
|
135
|
+
type UseXbNovaChatResult = {
|
|
136
|
+
onboarding: XbNovaOnboarding | null;
|
|
137
|
+
messages: ChatMessage[];
|
|
138
|
+
busy: boolean;
|
|
139
|
+
error: string | null;
|
|
140
|
+
send: (text: string) => Promise<void>;
|
|
141
|
+
/** client action 핸들러가 돌려준 정규화 결과(tool_call_id별). 채팅 화면이 view 렌더러로 그린다. */
|
|
142
|
+
clientResults: ClientResults;
|
|
143
|
+
/** messageId → 현재 평가. 평가 안 한 메시지는 키가 없다. */
|
|
144
|
+
feedback: Record<string, XbNovaFeedbackRating>;
|
|
145
|
+
/** 낙관적으로 반영하고 서버에 저장한다. 실패하면 되돌린다. */
|
|
146
|
+
setFeedback: (messageId: string, rating: XbNovaFeedbackRating) => Promise<void>;
|
|
147
|
+
};
|
|
148
|
+
/** `botId`를 넘기면 Provider의 기본 봇 대신 그 봇으로 새 대화를 연다 — 넛지 handoff가 쓴다.
|
|
149
|
+
* `nudgeNodeId`를 넘기면 이 대화가 그 넛지에서 시작됐다고 서버에 표시한다 — 넛지 전환
|
|
150
|
+
* 퍼널(노출→클릭→첫질문→전환)의 마지막 단계가 이 값이 채워진 대화 수를 센다.
|
|
151
|
+
* `onDismissSheet`는 `dismissSheet: true`인 client action이 실행될 때 호출된다 —
|
|
152
|
+
* `<XbNovaChat>`이 시트를 닫는 함수를 넘겨준다(모바일에서 시트가 페이지 대부분을 덮어서,
|
|
153
|
+
* 안 닫으면 그 밑에서 일어나는 일이 안 보인다). */
|
|
154
|
+
declare function useXbNovaChat(options?: {
|
|
155
|
+
botId?: string;
|
|
156
|
+
nudgeNodeId?: string;
|
|
157
|
+
onDismissSheet?: () => void;
|
|
158
|
+
onFeedback?: (e: XbNovaFeedbackEvent) => void;
|
|
159
|
+
}): UseXbNovaChatResult;
|
|
160
|
+
|
|
112
161
|
type ProductItem = {
|
|
113
162
|
id: string;
|
|
114
163
|
title: string;
|
|
@@ -148,7 +197,7 @@ type ViewRenderer = (props: {
|
|
|
148
197
|
}) => React.ReactNode;
|
|
149
198
|
type ViewRendererMap = Partial<Record<ViewKind, ViewRenderer>>;
|
|
150
199
|
|
|
151
|
-
declare function XbNovaChat({ viewRenderers, botId, barStyle, panelStyle, backdropStyle, defaultOpen, idleText, avatarSize, onVerdictAction, }: {
|
|
200
|
+
declare function XbNovaChat({ viewRenderers, botId, barStyle, panelStyle, backdropStyle, defaultOpen, idleText, avatarSize, onVerdictAction, feedback: feedbackEnabled, onFeedback, }: {
|
|
152
201
|
/** 특정 view만 자체 컴포넌트로 덮어쓰고 싶을 때 — 지정 안 한 view는 기본 렌더러를 쓴다. */
|
|
153
202
|
viewRenderers?: ViewRendererMap;
|
|
154
203
|
/** Provider의 기본 봇 대신 다른 봇과 고정적으로 대화하고 싶을 때. 힌트를 눌러 열면
|
|
@@ -173,43 +222,12 @@ declare function XbNovaChat({ viewRenderers, botId, barStyle, panelStyle, backdr
|
|
|
173
222
|
/** 판단 카드의 행동 버튼을 눌렀을 때 — 실제 동작(상담신청 폼 열기, 섹션 스크롤 등)은
|
|
174
223
|
* 소비자가 정한다. SDK는 버튼에 적힌 `action` 문자열을 그대로 넘겨줄 뿐 뜻을 모른다. */
|
|
175
224
|
onVerdictAction?: (action: string) => void;
|
|
225
|
+
/** 완료된 답변 아래 👍/👎 "상담 만족도" 버튼을 보일지. 기본 true. */
|
|
226
|
+
feedback?: boolean;
|
|
227
|
+
/** 만족도가 서버에 저장된 뒤 호출된다(분석 이벤트 전송용). `rating: null`은 평가 취소. */
|
|
228
|
+
onFeedback?: (e: XbNovaFeedbackEvent) => void;
|
|
176
229
|
}): react.JSX.Element | null;
|
|
177
230
|
|
|
178
|
-
type ClientResults = Record<string, {
|
|
179
|
-
view: ViewKind;
|
|
180
|
-
output: unknown;
|
|
181
|
-
}>;
|
|
182
|
-
type XbNovaOnboarding = {
|
|
183
|
-
conversation_id: string;
|
|
184
|
-
greeting: string;
|
|
185
|
-
quick_replies: string[];
|
|
186
|
-
branding: {
|
|
187
|
-
title?: string;
|
|
188
|
-
logo_url?: string;
|
|
189
|
-
avatar_text?: string;
|
|
190
|
-
};
|
|
191
|
-
};
|
|
192
|
-
type UseXbNovaChatResult = {
|
|
193
|
-
onboarding: XbNovaOnboarding | null;
|
|
194
|
-
messages: ChatMessage[];
|
|
195
|
-
busy: boolean;
|
|
196
|
-
error: string | null;
|
|
197
|
-
send: (text: string) => Promise<void>;
|
|
198
|
-
/** client action 핸들러가 돌려준 정규화 결과(tool_call_id별). 채팅 화면이 view 렌더러로 그린다. */
|
|
199
|
-
clientResults: ClientResults;
|
|
200
|
-
};
|
|
201
|
-
/** `botId`를 넘기면 Provider의 기본 봇 대신 그 봇으로 새 대화를 연다 — 넛지 handoff가 쓴다.
|
|
202
|
-
* `nudgeNodeId`를 넘기면 이 대화가 그 넛지에서 시작됐다고 서버에 표시한다 — 넛지 전환
|
|
203
|
-
* 퍼널(노출→클릭→첫질문→전환)의 마지막 단계가 이 값이 채워진 대화 수를 센다.
|
|
204
|
-
* `onDismissSheet`는 `dismissSheet: true`인 client action이 실행될 때 호출된다 —
|
|
205
|
-
* `<XbNovaChat>`이 시트를 닫는 함수를 넘겨준다(모바일에서 시트가 페이지 대부분을 덮어서,
|
|
206
|
-
* 안 닫으면 그 밑에서 일어나는 일이 안 보인다). */
|
|
207
|
-
declare function useXbNovaChat(options?: {
|
|
208
|
-
botId?: string;
|
|
209
|
-
nudgeNodeId?: string;
|
|
210
|
-
onDismissSheet?: () => void;
|
|
211
|
-
}): UseXbNovaChatResult;
|
|
212
|
-
|
|
213
231
|
/** ref형 — 감싸는 엘리먼트를 늘리기 싫을 때. `<div ref={useXbNovaTrack("price", data)}>`. */
|
|
214
232
|
declare function useXbNovaTrack<T extends HTMLElement = HTMLElement>(region: string, data?: FactValue, label?: string): (node: T | null) => void;
|
|
215
233
|
/** 선언형 — 감싸기만 하면 됨. `<XbNovaTrack region="price" data={{...}}>...</XbNovaTrack>`. */
|
|
@@ -252,4 +270,4 @@ declare function useXbNovaSettings(): {
|
|
|
252
270
|
showBar: () => void;
|
|
253
271
|
};
|
|
254
272
|
|
|
255
|
-
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 XbNovaOnboarding, XbNovaProvider, type XbNovaTheme, XbNovaTrack, trackXbNovaEvent, useXbNovaChat, useXbNovaFact, useXbNovaFacts, useXbNovaSettings, useXbNovaTrack, useXbNovaVisitor };
|
|
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 };
|
package/dist/index.js
CHANGED
|
@@ -1010,6 +1010,9 @@ async function consumeSse(response, handlers) {
|
|
|
1010
1010
|
continue;
|
|
1011
1011
|
}
|
|
1012
1012
|
switch (event.type) {
|
|
1013
|
+
case "start":
|
|
1014
|
+
if (typeof event.messageId === "string" && event.messageId) handlers.onMessageId?.(event.messageId);
|
|
1015
|
+
break;
|
|
1013
1016
|
case "text-start": {
|
|
1014
1017
|
const id = String(event.id);
|
|
1015
1018
|
textIndex.set(id, parts.length);
|
|
@@ -1086,7 +1089,7 @@ async function resumeToolResult(apiBaseUrl, apiKey, body, handlers) {
|
|
|
1086
1089
|
// src/use-chat.ts
|
|
1087
1090
|
var DEFAULT_CLIENT_ACTION_TIMEOUT_MS = 15e3;
|
|
1088
1091
|
function useXbNovaChat(options) {
|
|
1089
|
-
const { auth, trackRegistry, clientActionHandlers } = useXbNovaInternal();
|
|
1092
|
+
const { auth, trackRegistry, clientActionHandlers, visitorId } = useXbNovaInternal();
|
|
1090
1093
|
const botId = options?.botId ?? auth.botId;
|
|
1091
1094
|
const nudgeNodeId = options?.nudgeNodeId;
|
|
1092
1095
|
const { apiKey, apiBaseUrl } = auth;
|
|
@@ -1095,6 +1098,9 @@ function useXbNovaChat(options) {
|
|
|
1095
1098
|
const [busy, setBusy] = useState3(false);
|
|
1096
1099
|
const [error, setError] = useState3(null);
|
|
1097
1100
|
const [clientResults, setClientResults] = useState3({});
|
|
1101
|
+
const [feedback, setFeedbackState] = useState3({});
|
|
1102
|
+
const feedbackRef = useRef7(feedback);
|
|
1103
|
+
feedbackRef.current = feedback;
|
|
1098
1104
|
const dispatchedClientActionsRef = useRef7(/* @__PURE__ */ new Set());
|
|
1099
1105
|
const nudgeNodeIdRef = useRef7(nudgeNodeId);
|
|
1100
1106
|
nudgeNodeIdRef.current = nudgeNodeId;
|
|
@@ -1103,6 +1109,7 @@ function useXbNovaChat(options) {
|
|
|
1103
1109
|
setMessages([]);
|
|
1104
1110
|
setError(null);
|
|
1105
1111
|
setClientResults({});
|
|
1112
|
+
setFeedbackState({});
|
|
1106
1113
|
dispatchedClientActionsRef.current = /* @__PURE__ */ new Set();
|
|
1107
1114
|
fetch(`${apiBaseUrl}/v1/conversations`, {
|
|
1108
1115
|
method: "POST",
|
|
@@ -1112,9 +1119,42 @@ function useXbNovaChat(options) {
|
|
|
1112
1119
|
}, [apiBaseUrl, apiKey, botId]);
|
|
1113
1120
|
const appendToLastAssistant = (existingParts, newParts) => setMessages((prev) => {
|
|
1114
1121
|
const next = [...prev];
|
|
1115
|
-
next[next.length - 1] = { role: "assistant", parts: [...existingParts, ...newParts] };
|
|
1122
|
+
next[next.length - 1] = { ...next[next.length - 1], role: "assistant", parts: [...existingParts, ...newParts] };
|
|
1123
|
+
return next;
|
|
1124
|
+
});
|
|
1125
|
+
const setLastAssistantId = (id) => setMessages((prev) => {
|
|
1126
|
+
if (prev.length === 0) return prev;
|
|
1127
|
+
const next = [...prev];
|
|
1128
|
+
next[next.length - 1] = { ...next[next.length - 1], id };
|
|
1116
1129
|
return next;
|
|
1117
1130
|
});
|
|
1131
|
+
async function setFeedback(messageId, rating) {
|
|
1132
|
+
const conversationId = onboarding?.conversation_id;
|
|
1133
|
+
if (!conversationId || !messageId) return;
|
|
1134
|
+
const prevRating = feedbackRef.current[messageId] ?? null;
|
|
1135
|
+
const apply = (r) => setFeedbackState((prev) => {
|
|
1136
|
+
const next = { ...prev };
|
|
1137
|
+
if (r === null) delete next[messageId];
|
|
1138
|
+
else next[messageId] = r;
|
|
1139
|
+
return next;
|
|
1140
|
+
});
|
|
1141
|
+
apply(rating);
|
|
1142
|
+
try {
|
|
1143
|
+
const res = await fetch(
|
|
1144
|
+
`${apiBaseUrl}/v1/conversations/${encodeURIComponent(conversationId)}/messages/${encodeURIComponent(messageId)}/feedback`,
|
|
1145
|
+
{
|
|
1146
|
+
method: "PUT",
|
|
1147
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
|
|
1148
|
+
body: JSON.stringify({ rating, visitor_id: visitorId })
|
|
1149
|
+
}
|
|
1150
|
+
);
|
|
1151
|
+
if (!res.ok) throw new Error(`${res.status}`);
|
|
1152
|
+
options?.onFeedback?.({ conversationId, messageId, rating });
|
|
1153
|
+
} catch (e) {
|
|
1154
|
+
console.warn("[xbnova] \uD53C\uB4DC\uBC31 \uC800\uC7A5 \uC2E4\uD328", e);
|
|
1155
|
+
apply(prevRating);
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1118
1158
|
async function runClientAction(spec, part, existingParts) {
|
|
1119
1159
|
if (spec.dismissSheet) options?.onDismissSheet?.();
|
|
1120
1160
|
let result;
|
|
@@ -1160,7 +1200,11 @@ function useXbNovaChat(options) {
|
|
|
1160
1200
|
tool_call_id: part.toolCallId,
|
|
1161
1201
|
result_text: resultText
|
|
1162
1202
|
},
|
|
1163
|
-
{
|
|
1203
|
+
{
|
|
1204
|
+
onPart: (newParts) => appendToLastAssistant(existingParts, newParts),
|
|
1205
|
+
onMessageId: setLastAssistantId,
|
|
1206
|
+
onError: setError
|
|
1207
|
+
}
|
|
1164
1208
|
);
|
|
1165
1209
|
} catch (e) {
|
|
1166
1210
|
setError(e instanceof Error ? e.message : String(e));
|
|
@@ -1193,7 +1237,7 @@ function useXbNovaChat(options) {
|
|
|
1193
1237
|
]);
|
|
1194
1238
|
const replaceLast = (parts) => setMessages((prev) => {
|
|
1195
1239
|
const next = [...prev];
|
|
1196
|
-
next[next.length - 1] = { role: "assistant", parts };
|
|
1240
|
+
next[next.length - 1] = { ...next[next.length - 1], role: "assistant", parts };
|
|
1197
1241
|
return next;
|
|
1198
1242
|
});
|
|
1199
1243
|
const focused = getFocusedTrackEntry(trackRegistry);
|
|
@@ -1209,7 +1253,7 @@ function useXbNovaChat(options) {
|
|
|
1209
1253
|
context,
|
|
1210
1254
|
nudge_node_id: nudgeNodeId ?? null
|
|
1211
1255
|
},
|
|
1212
|
-
{ onPart: replaceLast, onError: setError }
|
|
1256
|
+
{ onPart: replaceLast, onMessageId: setLastAssistantId, onError: setError }
|
|
1213
1257
|
);
|
|
1214
1258
|
} catch (e) {
|
|
1215
1259
|
setError(e instanceof Error ? e.message : String(e));
|
|
@@ -1217,7 +1261,7 @@ function useXbNovaChat(options) {
|
|
|
1217
1261
|
setBusy(false);
|
|
1218
1262
|
}
|
|
1219
1263
|
}
|
|
1220
|
-
return { onboarding, messages, busy, error, send, clientResults };
|
|
1264
|
+
return { onboarding, messages, busy, error, send, clientResults, feedback, setFeedback };
|
|
1221
1265
|
}
|
|
1222
1266
|
|
|
1223
1267
|
// src/views.tsx
|
|
@@ -1477,7 +1521,9 @@ function XbNovaChat({
|
|
|
1477
1521
|
defaultOpen = false,
|
|
1478
1522
|
idleText = "AI\uC5D0\uAC8C \uC774 \uCC28 \uBB3C\uC5B4\uBCF4\uAE30",
|
|
1479
1523
|
avatarSize = 28,
|
|
1480
|
-
onVerdictAction
|
|
1524
|
+
onVerdictAction,
|
|
1525
|
+
feedback: feedbackEnabled = true,
|
|
1526
|
+
onFeedback
|
|
1481
1527
|
}) {
|
|
1482
1528
|
const { auth, trackRegistry, factsRegistry, visitorId, clientActionHandlers, theme } = useXbNovaInternal();
|
|
1483
1529
|
const themeVars = {
|
|
@@ -1500,11 +1546,34 @@ function XbNovaChat({
|
|
|
1500
1546
|
const [input, setInput] = useState4("");
|
|
1501
1547
|
const bottomRef = useRef8(null);
|
|
1502
1548
|
const typedHintText = useTypewriter(hint?.text ?? "", hint?.triggerNodeId ?? "");
|
|
1503
|
-
const { onboarding, messages, busy, error, send, clientResults } = useXbNovaChat({
|
|
1549
|
+
const { onboarding, messages, busy, error, send, clientResults, feedback, setFeedback } = useXbNovaChat({
|
|
1550
|
+
onFeedback,
|
|
1504
1551
|
botId: activeBotId ?? botId,
|
|
1505
1552
|
nudgeNodeId: activeNudgeNodeId,
|
|
1506
1553
|
onDismissSheet: () => setOpen(false)
|
|
1507
1554
|
});
|
|
1555
|
+
const effectiveBotId = activeBotId ?? botId;
|
|
1556
|
+
const [cachedBranding, setCachedBranding] = useState4(null);
|
|
1557
|
+
useEffect8(() => {
|
|
1558
|
+
try {
|
|
1559
|
+
const raw = window.localStorage.getItem(`xbnova_branding:${effectiveBotId}`);
|
|
1560
|
+
setCachedBranding(raw ? JSON.parse(raw) : null);
|
|
1561
|
+
} catch {
|
|
1562
|
+
setCachedBranding(null);
|
|
1563
|
+
}
|
|
1564
|
+
}, [effectiveBotId]);
|
|
1565
|
+
useEffect8(() => {
|
|
1566
|
+
if (!onboarding?.branding) return;
|
|
1567
|
+
try {
|
|
1568
|
+
window.localStorage.setItem(
|
|
1569
|
+
`xbnova_branding:${effectiveBotId}`,
|
|
1570
|
+
JSON.stringify({ logo_url: onboarding.branding.logo_url, avatar_text: onboarding.branding.avatar_text })
|
|
1571
|
+
);
|
|
1572
|
+
} catch {
|
|
1573
|
+
}
|
|
1574
|
+
}, [onboarding?.branding, effectiveBotId]);
|
|
1575
|
+
const branding = onboarding?.branding ?? cachedBranding ?? void 0;
|
|
1576
|
+
const brandingReady = Boolean(onboarding) || cachedBranding !== null || Boolean(error);
|
|
1508
1577
|
useEffect8(() => {
|
|
1509
1578
|
bottomRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
1510
1579
|
}, [messages]);
|
|
@@ -1632,7 +1701,8 @@ function XbNovaChat({
|
|
|
1632
1701
|
children: /* @__PURE__ */ jsx7(
|
|
1633
1702
|
Avatar,
|
|
1634
1703
|
{
|
|
1635
|
-
branding
|
|
1704
|
+
branding,
|
|
1705
|
+
ready: brandingReady,
|
|
1636
1706
|
size: avatarSize,
|
|
1637
1707
|
className: "shadow-md",
|
|
1638
1708
|
gradient: avatarGradientStyle
|
|
@@ -1681,7 +1751,8 @@ function XbNovaChat({
|
|
|
1681
1751
|
/* @__PURE__ */ jsx7(
|
|
1682
1752
|
Avatar,
|
|
1683
1753
|
{
|
|
1684
|
-
branding
|
|
1754
|
+
branding,
|
|
1755
|
+
ready: brandingReady,
|
|
1685
1756
|
size: headerAvatarSize,
|
|
1686
1757
|
className: "shrink-0",
|
|
1687
1758
|
gradient: avatarGradientStyle
|
|
@@ -1728,7 +1799,14 @@ function XbNovaChat({
|
|
|
1728
1799
|
}
|
|
1729
1800
|
return /* @__PURE__ */ jsx7(ToolView, { part, viewRenderers, onVerdictAction }, j);
|
|
1730
1801
|
}),
|
|
1731
|
-
!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
|
|
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
|
|
1732
1810
|
] }) }, i)),
|
|
1733
1811
|
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,
|
|
1734
1812
|
/* @__PURE__ */ jsx7("div", { ref: bottomRef })
|
|
@@ -1801,6 +1879,7 @@ function XbNovaChat({
|
|
|
1801
1879
|
}
|
|
1802
1880
|
function Avatar({
|
|
1803
1881
|
branding,
|
|
1882
|
+
ready,
|
|
1804
1883
|
size,
|
|
1805
1884
|
className = "",
|
|
1806
1885
|
gradient
|
|
@@ -1808,27 +1887,67 @@ function Avatar({
|
|
|
1808
1887
|
const box = { width: size, height: size };
|
|
1809
1888
|
const logoUrl = branding?.logo_url?.trim() ?? "";
|
|
1810
1889
|
const [failedUrl, setFailedUrl] = useState4("");
|
|
1890
|
+
const [loadedUrl, setLoadedUrl] = useState4("");
|
|
1891
|
+
const base = `${className} relative flex flex-none items-center justify-center overflow-hidden rounded-full font-bold text-white`;
|
|
1892
|
+
if (!ready) {
|
|
1893
|
+
return /* @__PURE__ */ jsx7("span", { className: base, style: { ...gradient, ...box }, "aria-hidden": true });
|
|
1894
|
+
}
|
|
1811
1895
|
if (logoUrl && failedUrl !== logoUrl) {
|
|
1812
|
-
return /* @__PURE__ */ jsx7(
|
|
1896
|
+
return /* @__PURE__ */ jsx7("span", { className: base, style: { ...gradient, ...box }, children: /* @__PURE__ */ jsx7(
|
|
1813
1897
|
"img",
|
|
1814
1898
|
{
|
|
1815
1899
|
src: logoUrl,
|
|
1816
1900
|
alt: "",
|
|
1901
|
+
onLoad: () => setLoadedUrl(logoUrl),
|
|
1817
1902
|
onError: () => setFailedUrl(logoUrl),
|
|
1818
|
-
|
|
1819
|
-
|
|
1903
|
+
className: "absolute inset-0 h-full w-full rounded-full bg-white object-cover transition-opacity duration-200",
|
|
1904
|
+
style: { opacity: loadedUrl === logoUrl ? 1 : 0 }
|
|
1820
1905
|
}
|
|
1821
|
-
);
|
|
1906
|
+
) });
|
|
1822
1907
|
}
|
|
1823
1908
|
const text = branding?.avatar_text?.trim() || "AI";
|
|
1824
|
-
return /* @__PURE__ */ jsx7(
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1909
|
+
return /* @__PURE__ */ jsx7("span", { className: base, style: { ...gradient, ...box, fontSize: Math.max(10, Math.round(size * 0.4)) }, children: text });
|
|
1910
|
+
}
|
|
1911
|
+
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
|
+
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({
|
|
1914
|
+
rating,
|
|
1915
|
+
onChange
|
|
1916
|
+
}) {
|
|
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",
|
|
1931
|
+
{
|
|
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 })
|
|
1942
|
+
}
|
|
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
|
+
] });
|
|
1832
1951
|
}
|
|
1833
1952
|
function Bubble({ role, children }) {
|
|
1834
1953
|
const mine = role === "user";
|