@xbrain-swe/nova-chat-widget 0.1.4 → 0.1.7
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 +87 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +57 -31
- package/dist/index.d.ts +57 -31
- package/dist/index.js +87 -14
- package/dist/index.js.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,44 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* AI SDK UI Message Stream v1 파서. `nova/web/lib/chat-stream.ts`에서 그대로 옮겼다 — 이미
|
|
5
|
+
* 콘솔에서 검증된 로직이라 새로 설계하지 않는다.
|
|
6
|
+
*
|
|
7
|
+
* `ai` 패키지에 의존하지 않는다 — 고객사 번들에 peer 버전 충돌을 만들지 않기 위해서다.
|
|
8
|
+
*/
|
|
9
|
+
type ViewKind = "product_list" | "product_detail" | "metric" | "choices" | "form" | "verdict";
|
|
10
|
+
type ToolPart = {
|
|
11
|
+
kind: "tool";
|
|
12
|
+
toolCallId: string;
|
|
13
|
+
toolName: string;
|
|
14
|
+
state: "input" | "output" | "error";
|
|
15
|
+
input?: unknown;
|
|
16
|
+
output?: unknown;
|
|
17
|
+
/** 위젯은 tool 이름이 아니라 이 값으로 렌더러를 고른다. */
|
|
18
|
+
view?: ViewKind | null;
|
|
19
|
+
};
|
|
20
|
+
type TextPart = {
|
|
21
|
+
kind: "text";
|
|
22
|
+
id: string;
|
|
23
|
+
text: string;
|
|
24
|
+
};
|
|
25
|
+
type MessagePart = TextPart | ToolPart;
|
|
26
|
+
type ChatMessage = {
|
|
27
|
+
role: "user" | "assistant";
|
|
28
|
+
parts: MessagePart[];
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** `awaitResult` 핸들러가 구조화된 결과를 돌려줄 때의 모양. `text`는 모델이 읽는 결과(서버로
|
|
32
|
+
* 전송)고, `view`+`output`은 채팅 말풍선에 직접 그릴 정규화된 데이터다 — 서버에는 가지 않는다.
|
|
33
|
+
* 렌더러는 tool 이름이 아니라 `view`로 고르므로(`views.tsx`) 고객사가 UI를 만들 필요가 없다.
|
|
34
|
+
* 예: 상품 검색 결과는 `view:"product_list"`, `output:{ items: ProductItem[], total }`.
|
|
35
|
+
* MCP 서버를 쓸 수 없는 환경에서 MCP tool 결과의 `_meta` view와 같은 효과를 내는 대체 경로다.
|
|
36
|
+
* 대화를 새로고침하면 이 그림은 사라진다(서버에는 `text`만 남는다). */
|
|
37
|
+
type XbNovaClientActionResult = {
|
|
38
|
+
text: string;
|
|
39
|
+
view?: ViewKind;
|
|
40
|
+
output?: unknown;
|
|
41
|
+
};
|
|
3
42
|
type XbNovaClientAction = {
|
|
4
43
|
/** 모델이 "언제 이 tool을 부를지" 판단하는 근거 — MCP tool description과 같은 역할. */
|
|
5
44
|
description: string;
|
|
@@ -15,11 +54,15 @@ type XbNovaClientAction = {
|
|
|
15
54
|
* interrupt/resume과 같은 패턴). false(기본)면 `handler`는 그냥 실행되고 끝 —
|
|
16
55
|
* 반환값은 안 쓴다. */
|
|
17
56
|
awaitResult?: boolean;
|
|
57
|
+
/** `awaitResult:true`일 때 핸들러 응답을 기다리는 최대 시간(ms, 기본 15000). 넘으면 SDK가
|
|
58
|
+
* "오류: 응답 시간이 초과되었습니다."를 tool 결과로 서버에 보내 모델이 대화를 이어가게
|
|
59
|
+
* 한다 — 이후 핸들러가 뒤늦게 resolve돼도 그 값은 무시된다. fire-and-forget에는 적용 안 된다. */
|
|
60
|
+
timeoutMs?: number;
|
|
18
61
|
/** 실제로 실행할 동작 — 채팅에는 아무것도 안 그리고 이 함수만 호출된다. `payload`는 이
|
|
19
62
|
* tool 호출의 input을 그대로 되돌려준다. `awaitResult:true`면 반환값(또는 resolve된
|
|
20
63
|
* Promise)이 진짜 tool 결과 텍스트가 된다 — 비워두면 "핸들러가 결과를 반환하지
|
|
21
64
|
* 않았습니다" 에러로 처리된다. */
|
|
22
|
-
handler: (payload: unknown) => void | string | Promise<void | string>;
|
|
65
|
+
handler: (payload: unknown) => void | string | XbNovaClientActionResult | Promise<void | string | XbNovaClientActionResult>;
|
|
23
66
|
/** true면 이 tool이 실행되는 시점에 대화 시트를 닫는다(대화는 안 사라진다, 나중에
|
|
24
67
|
* 다시 열면 이어짐). `highlight_region`처럼 "페이지의 특정 부분을 보여주는" 액션에
|
|
25
68
|
* 필요하다 — 모바일에서는 시트가 화면 대부분(위 7rem 제외)을 덮어서, 시트를 안 닫으면
|
|
@@ -66,34 +109,6 @@ declare function XbNovaProvider({ apiKey, botId, appId, screen, apiBaseUrl, clie
|
|
|
66
109
|
children: React.ReactNode;
|
|
67
110
|
}): react.JSX.Element;
|
|
68
111
|
|
|
69
|
-
/**
|
|
70
|
-
* AI SDK UI Message Stream v1 파서. `nova/web/lib/chat-stream.ts`에서 그대로 옮겼다 — 이미
|
|
71
|
-
* 콘솔에서 검증된 로직이라 새로 설계하지 않는다.
|
|
72
|
-
*
|
|
73
|
-
* `ai` 패키지에 의존하지 않는다 — 고객사 번들에 peer 버전 충돌을 만들지 않기 위해서다.
|
|
74
|
-
*/
|
|
75
|
-
type ViewKind = "product_list" | "product_detail" | "metric" | "choices" | "form" | "verdict";
|
|
76
|
-
type ToolPart = {
|
|
77
|
-
kind: "tool";
|
|
78
|
-
toolCallId: string;
|
|
79
|
-
toolName: string;
|
|
80
|
-
state: "input" | "output" | "error";
|
|
81
|
-
input?: unknown;
|
|
82
|
-
output?: unknown;
|
|
83
|
-
/** 위젯은 tool 이름이 아니라 이 값으로 렌더러를 고른다. */
|
|
84
|
-
view?: ViewKind | null;
|
|
85
|
-
};
|
|
86
|
-
type TextPart = {
|
|
87
|
-
kind: "text";
|
|
88
|
-
id: string;
|
|
89
|
-
text: string;
|
|
90
|
-
};
|
|
91
|
-
type MessagePart = TextPart | ToolPart;
|
|
92
|
-
type ChatMessage = {
|
|
93
|
-
role: "user" | "assistant";
|
|
94
|
-
parts: MessagePart[];
|
|
95
|
-
};
|
|
96
|
-
|
|
97
112
|
type ProductItem = {
|
|
98
113
|
id: string;
|
|
99
114
|
title: string;
|
|
@@ -133,7 +148,7 @@ type ViewRenderer = (props: {
|
|
|
133
148
|
}) => React.ReactNode;
|
|
134
149
|
type ViewRendererMap = Partial<Record<ViewKind, ViewRenderer>>;
|
|
135
150
|
|
|
136
|
-
declare function XbNovaChat({ viewRenderers, botId, barStyle, panelStyle, backdropStyle, defaultOpen, idleText, onVerdictAction, }: {
|
|
151
|
+
declare function XbNovaChat({ viewRenderers, botId, barStyle, panelStyle, backdropStyle, defaultOpen, idleText, avatarSize, onVerdictAction, }: {
|
|
137
152
|
/** 특정 view만 자체 컴포넌트로 덮어쓰고 싶을 때 — 지정 안 한 view는 기본 렌더러를 쓴다. */
|
|
138
153
|
viewRenderers?: ViewRendererMap;
|
|
139
154
|
/** Provider의 기본 봇 대신 다른 봇과 고정적으로 대화하고 싶을 때. 힌트를 눌러 열면
|
|
@@ -152,17 +167,26 @@ declare function XbNovaChat({ viewRenderers, botId, barStyle, panelStyle, backdr
|
|
|
152
167
|
defaultOpen?: boolean;
|
|
153
168
|
/** 기본(idle) 상태 바 문구. */
|
|
154
169
|
idleText?: string;
|
|
170
|
+
/** 바(AI 버튼)의 동그란 아바타 지름(px), 기본 28. 바의 접힌 원은 52px이라 44~48이면 원을 거의 채운다.
|
|
171
|
+
* 채팅 헤더의 아바타는 이 값에 맞춰 24~36px로 따라 커진다. */
|
|
172
|
+
avatarSize?: number;
|
|
155
173
|
/** 판단 카드의 행동 버튼을 눌렀을 때 — 실제 동작(상담신청 폼 열기, 섹션 스크롤 등)은
|
|
156
174
|
* 소비자가 정한다. SDK는 버튼에 적힌 `action` 문자열을 그대로 넘겨줄 뿐 뜻을 모른다. */
|
|
157
175
|
onVerdictAction?: (action: string) => void;
|
|
158
176
|
}): react.JSX.Element | null;
|
|
159
177
|
|
|
178
|
+
type ClientResults = Record<string, {
|
|
179
|
+
view: ViewKind;
|
|
180
|
+
output: unknown;
|
|
181
|
+
}>;
|
|
160
182
|
type XbNovaOnboarding = {
|
|
161
183
|
conversation_id: string;
|
|
162
184
|
greeting: string;
|
|
163
185
|
quick_replies: string[];
|
|
164
186
|
branding: {
|
|
165
187
|
title?: string;
|
|
188
|
+
logo_url?: string;
|
|
189
|
+
avatar_text?: string;
|
|
166
190
|
};
|
|
167
191
|
};
|
|
168
192
|
type UseXbNovaChatResult = {
|
|
@@ -171,6 +195,8 @@ type UseXbNovaChatResult = {
|
|
|
171
195
|
busy: boolean;
|
|
172
196
|
error: string | null;
|
|
173
197
|
send: (text: string) => Promise<void>;
|
|
198
|
+
/** client action 핸들러가 돌려준 정규화 결과(tool_call_id별). 채팅 화면이 view 렌더러로 그린다. */
|
|
199
|
+
clientResults: ClientResults;
|
|
174
200
|
};
|
|
175
201
|
/** `botId`를 넘기면 Provider의 기본 봇 대신 그 봇으로 새 대화를 연다 — 넛지 handoff가 쓴다.
|
|
176
202
|
* `nudgeNodeId`를 넘기면 이 대화가 그 넛지에서 시작됐다고 서버에 표시한다 — 넛지 전환
|
|
@@ -226,4 +252,4 @@ declare function useXbNovaSettings(): {
|
|
|
226
252
|
showBar: () => void;
|
|
227
253
|
};
|
|
228
254
|
|
|
229
|
-
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 XbNovaClientActions, XbNovaFact, XbNovaFacts, type XbNovaOnboarding, XbNovaProvider, type XbNovaTheme, XbNovaTrack, trackXbNovaEvent, useXbNovaChat, useXbNovaFact, useXbNovaFacts, useXbNovaSettings, useXbNovaTrack, useXbNovaVisitor };
|
|
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 };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,44 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* AI SDK UI Message Stream v1 파서. `nova/web/lib/chat-stream.ts`에서 그대로 옮겼다 — 이미
|
|
5
|
+
* 콘솔에서 검증된 로직이라 새로 설계하지 않는다.
|
|
6
|
+
*
|
|
7
|
+
* `ai` 패키지에 의존하지 않는다 — 고객사 번들에 peer 버전 충돌을 만들지 않기 위해서다.
|
|
8
|
+
*/
|
|
9
|
+
type ViewKind = "product_list" | "product_detail" | "metric" | "choices" | "form" | "verdict";
|
|
10
|
+
type ToolPart = {
|
|
11
|
+
kind: "tool";
|
|
12
|
+
toolCallId: string;
|
|
13
|
+
toolName: string;
|
|
14
|
+
state: "input" | "output" | "error";
|
|
15
|
+
input?: unknown;
|
|
16
|
+
output?: unknown;
|
|
17
|
+
/** 위젯은 tool 이름이 아니라 이 값으로 렌더러를 고른다. */
|
|
18
|
+
view?: ViewKind | null;
|
|
19
|
+
};
|
|
20
|
+
type TextPart = {
|
|
21
|
+
kind: "text";
|
|
22
|
+
id: string;
|
|
23
|
+
text: string;
|
|
24
|
+
};
|
|
25
|
+
type MessagePart = TextPart | ToolPart;
|
|
26
|
+
type ChatMessage = {
|
|
27
|
+
role: "user" | "assistant";
|
|
28
|
+
parts: MessagePart[];
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** `awaitResult` 핸들러가 구조화된 결과를 돌려줄 때의 모양. `text`는 모델이 읽는 결과(서버로
|
|
32
|
+
* 전송)고, `view`+`output`은 채팅 말풍선에 직접 그릴 정규화된 데이터다 — 서버에는 가지 않는다.
|
|
33
|
+
* 렌더러는 tool 이름이 아니라 `view`로 고르므로(`views.tsx`) 고객사가 UI를 만들 필요가 없다.
|
|
34
|
+
* 예: 상품 검색 결과는 `view:"product_list"`, `output:{ items: ProductItem[], total }`.
|
|
35
|
+
* MCP 서버를 쓸 수 없는 환경에서 MCP tool 결과의 `_meta` view와 같은 효과를 내는 대체 경로다.
|
|
36
|
+
* 대화를 새로고침하면 이 그림은 사라진다(서버에는 `text`만 남는다). */
|
|
37
|
+
type XbNovaClientActionResult = {
|
|
38
|
+
text: string;
|
|
39
|
+
view?: ViewKind;
|
|
40
|
+
output?: unknown;
|
|
41
|
+
};
|
|
3
42
|
type XbNovaClientAction = {
|
|
4
43
|
/** 모델이 "언제 이 tool을 부를지" 판단하는 근거 — MCP tool description과 같은 역할. */
|
|
5
44
|
description: string;
|
|
@@ -15,11 +54,15 @@ type XbNovaClientAction = {
|
|
|
15
54
|
* interrupt/resume과 같은 패턴). false(기본)면 `handler`는 그냥 실행되고 끝 —
|
|
16
55
|
* 반환값은 안 쓴다. */
|
|
17
56
|
awaitResult?: boolean;
|
|
57
|
+
/** `awaitResult:true`일 때 핸들러 응답을 기다리는 최대 시간(ms, 기본 15000). 넘으면 SDK가
|
|
58
|
+
* "오류: 응답 시간이 초과되었습니다."를 tool 결과로 서버에 보내 모델이 대화를 이어가게
|
|
59
|
+
* 한다 — 이후 핸들러가 뒤늦게 resolve돼도 그 값은 무시된다. fire-and-forget에는 적용 안 된다. */
|
|
60
|
+
timeoutMs?: number;
|
|
18
61
|
/** 실제로 실행할 동작 — 채팅에는 아무것도 안 그리고 이 함수만 호출된다. `payload`는 이
|
|
19
62
|
* tool 호출의 input을 그대로 되돌려준다. `awaitResult:true`면 반환값(또는 resolve된
|
|
20
63
|
* Promise)이 진짜 tool 결과 텍스트가 된다 — 비워두면 "핸들러가 결과를 반환하지
|
|
21
64
|
* 않았습니다" 에러로 처리된다. */
|
|
22
|
-
handler: (payload: unknown) => void | string | Promise<void | string>;
|
|
65
|
+
handler: (payload: unknown) => void | string | XbNovaClientActionResult | Promise<void | string | XbNovaClientActionResult>;
|
|
23
66
|
/** true면 이 tool이 실행되는 시점에 대화 시트를 닫는다(대화는 안 사라진다, 나중에
|
|
24
67
|
* 다시 열면 이어짐). `highlight_region`처럼 "페이지의 특정 부분을 보여주는" 액션에
|
|
25
68
|
* 필요하다 — 모바일에서는 시트가 화면 대부분(위 7rem 제외)을 덮어서, 시트를 안 닫으면
|
|
@@ -66,34 +109,6 @@ declare function XbNovaProvider({ apiKey, botId, appId, screen, apiBaseUrl, clie
|
|
|
66
109
|
children: React.ReactNode;
|
|
67
110
|
}): react.JSX.Element;
|
|
68
111
|
|
|
69
|
-
/**
|
|
70
|
-
* AI SDK UI Message Stream v1 파서. `nova/web/lib/chat-stream.ts`에서 그대로 옮겼다 — 이미
|
|
71
|
-
* 콘솔에서 검증된 로직이라 새로 설계하지 않는다.
|
|
72
|
-
*
|
|
73
|
-
* `ai` 패키지에 의존하지 않는다 — 고객사 번들에 peer 버전 충돌을 만들지 않기 위해서다.
|
|
74
|
-
*/
|
|
75
|
-
type ViewKind = "product_list" | "product_detail" | "metric" | "choices" | "form" | "verdict";
|
|
76
|
-
type ToolPart = {
|
|
77
|
-
kind: "tool";
|
|
78
|
-
toolCallId: string;
|
|
79
|
-
toolName: string;
|
|
80
|
-
state: "input" | "output" | "error";
|
|
81
|
-
input?: unknown;
|
|
82
|
-
output?: unknown;
|
|
83
|
-
/** 위젯은 tool 이름이 아니라 이 값으로 렌더러를 고른다. */
|
|
84
|
-
view?: ViewKind | null;
|
|
85
|
-
};
|
|
86
|
-
type TextPart = {
|
|
87
|
-
kind: "text";
|
|
88
|
-
id: string;
|
|
89
|
-
text: string;
|
|
90
|
-
};
|
|
91
|
-
type MessagePart = TextPart | ToolPart;
|
|
92
|
-
type ChatMessage = {
|
|
93
|
-
role: "user" | "assistant";
|
|
94
|
-
parts: MessagePart[];
|
|
95
|
-
};
|
|
96
|
-
|
|
97
112
|
type ProductItem = {
|
|
98
113
|
id: string;
|
|
99
114
|
title: string;
|
|
@@ -133,7 +148,7 @@ type ViewRenderer = (props: {
|
|
|
133
148
|
}) => React.ReactNode;
|
|
134
149
|
type ViewRendererMap = Partial<Record<ViewKind, ViewRenderer>>;
|
|
135
150
|
|
|
136
|
-
declare function XbNovaChat({ viewRenderers, botId, barStyle, panelStyle, backdropStyle, defaultOpen, idleText, onVerdictAction, }: {
|
|
151
|
+
declare function XbNovaChat({ viewRenderers, botId, barStyle, panelStyle, backdropStyle, defaultOpen, idleText, avatarSize, onVerdictAction, }: {
|
|
137
152
|
/** 특정 view만 자체 컴포넌트로 덮어쓰고 싶을 때 — 지정 안 한 view는 기본 렌더러를 쓴다. */
|
|
138
153
|
viewRenderers?: ViewRendererMap;
|
|
139
154
|
/** Provider의 기본 봇 대신 다른 봇과 고정적으로 대화하고 싶을 때. 힌트를 눌러 열면
|
|
@@ -152,17 +167,26 @@ declare function XbNovaChat({ viewRenderers, botId, barStyle, panelStyle, backdr
|
|
|
152
167
|
defaultOpen?: boolean;
|
|
153
168
|
/** 기본(idle) 상태 바 문구. */
|
|
154
169
|
idleText?: string;
|
|
170
|
+
/** 바(AI 버튼)의 동그란 아바타 지름(px), 기본 28. 바의 접힌 원은 52px이라 44~48이면 원을 거의 채운다.
|
|
171
|
+
* 채팅 헤더의 아바타는 이 값에 맞춰 24~36px로 따라 커진다. */
|
|
172
|
+
avatarSize?: number;
|
|
155
173
|
/** 판단 카드의 행동 버튼을 눌렀을 때 — 실제 동작(상담신청 폼 열기, 섹션 스크롤 등)은
|
|
156
174
|
* 소비자가 정한다. SDK는 버튼에 적힌 `action` 문자열을 그대로 넘겨줄 뿐 뜻을 모른다. */
|
|
157
175
|
onVerdictAction?: (action: string) => void;
|
|
158
176
|
}): react.JSX.Element | null;
|
|
159
177
|
|
|
178
|
+
type ClientResults = Record<string, {
|
|
179
|
+
view: ViewKind;
|
|
180
|
+
output: unknown;
|
|
181
|
+
}>;
|
|
160
182
|
type XbNovaOnboarding = {
|
|
161
183
|
conversation_id: string;
|
|
162
184
|
greeting: string;
|
|
163
185
|
quick_replies: string[];
|
|
164
186
|
branding: {
|
|
165
187
|
title?: string;
|
|
188
|
+
logo_url?: string;
|
|
189
|
+
avatar_text?: string;
|
|
166
190
|
};
|
|
167
191
|
};
|
|
168
192
|
type UseXbNovaChatResult = {
|
|
@@ -171,6 +195,8 @@ type UseXbNovaChatResult = {
|
|
|
171
195
|
busy: boolean;
|
|
172
196
|
error: string | null;
|
|
173
197
|
send: (text: string) => Promise<void>;
|
|
198
|
+
/** client action 핸들러가 돌려준 정규화 결과(tool_call_id별). 채팅 화면이 view 렌더러로 그린다. */
|
|
199
|
+
clientResults: ClientResults;
|
|
174
200
|
};
|
|
175
201
|
/** `botId`를 넘기면 Provider의 기본 봇 대신 그 봇으로 새 대화를 연다 — 넛지 handoff가 쓴다.
|
|
176
202
|
* `nudgeNodeId`를 넘기면 이 대화가 그 넛지에서 시작됐다고 서버에 표시한다 — 넛지 전환
|
|
@@ -226,4 +252,4 @@ declare function useXbNovaSettings(): {
|
|
|
226
252
|
showBar: () => void;
|
|
227
253
|
};
|
|
228
254
|
|
|
229
|
-
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 XbNovaClientActions, XbNovaFact, XbNovaFacts, type XbNovaOnboarding, XbNovaProvider, type XbNovaTheme, XbNovaTrack, trackXbNovaEvent, useXbNovaChat, useXbNovaFact, useXbNovaFacts, useXbNovaSettings, useXbNovaTrack, useXbNovaVisitor };
|
|
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 };
|
package/dist/index.js
CHANGED
|
@@ -888,6 +888,7 @@ function XbNovaProvider({
|
|
|
888
888
|
{
|
|
889
889
|
handler: spec.handler,
|
|
890
890
|
awaitResult: spec.awaitResult ?? false,
|
|
891
|
+
timeoutMs: spec.timeoutMs,
|
|
891
892
|
dismissSheet: spec.dismissSheet ?? false,
|
|
892
893
|
render: spec.render
|
|
893
894
|
}
|
|
@@ -1083,6 +1084,7 @@ async function resumeToolResult(apiBaseUrl, apiKey, body, handlers) {
|
|
|
1083
1084
|
}
|
|
1084
1085
|
|
|
1085
1086
|
// src/use-chat.ts
|
|
1087
|
+
var DEFAULT_CLIENT_ACTION_TIMEOUT_MS = 15e3;
|
|
1086
1088
|
function useXbNovaChat(options) {
|
|
1087
1089
|
const { auth, trackRegistry, clientActionHandlers } = useXbNovaInternal();
|
|
1088
1090
|
const botId = options?.botId ?? auth.botId;
|
|
@@ -1092,6 +1094,7 @@ function useXbNovaChat(options) {
|
|
|
1092
1094
|
const [messages, setMessages] = useState3([]);
|
|
1093
1095
|
const [busy, setBusy] = useState3(false);
|
|
1094
1096
|
const [error, setError] = useState3(null);
|
|
1097
|
+
const [clientResults, setClientResults] = useState3({});
|
|
1095
1098
|
const dispatchedClientActionsRef = useRef7(/* @__PURE__ */ new Set());
|
|
1096
1099
|
const nudgeNodeIdRef = useRef7(nudgeNodeId);
|
|
1097
1100
|
nudgeNodeIdRef.current = nudgeNodeId;
|
|
@@ -1099,6 +1102,7 @@ function useXbNovaChat(options) {
|
|
|
1099
1102
|
setOnboarding(null);
|
|
1100
1103
|
setMessages([]);
|
|
1101
1104
|
setError(null);
|
|
1105
|
+
setClientResults({});
|
|
1102
1106
|
dispatchedClientActionsRef.current = /* @__PURE__ */ new Set();
|
|
1103
1107
|
fetch(`${apiBaseUrl}/v1/conversations`, {
|
|
1104
1108
|
method: "POST",
|
|
@@ -1115,7 +1119,20 @@ function useXbNovaChat(options) {
|
|
|
1115
1119
|
if (spec.dismissSheet) options?.onDismissSheet?.();
|
|
1116
1120
|
let result;
|
|
1117
1121
|
try {
|
|
1118
|
-
|
|
1122
|
+
if (!spec.awaitResult) {
|
|
1123
|
+
await spec.handler(part.input);
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
const timeoutMs = spec.timeoutMs ?? DEFAULT_CLIENT_ACTION_TIMEOUT_MS;
|
|
1127
|
+
let timer;
|
|
1128
|
+
const timeout = new Promise((resolve) => {
|
|
1129
|
+
timer = setTimeout(() => resolve("\uC624\uB958: \uC751\uB2F5 \uC2DC\uAC04\uC774 \uCD08\uACFC\uB418\uC5C8\uC2B5\uB2C8\uB2E4."), timeoutMs);
|
|
1130
|
+
});
|
|
1131
|
+
try {
|
|
1132
|
+
result = await Promise.race([Promise.resolve(spec.handler(part.input)), timeout]);
|
|
1133
|
+
} finally {
|
|
1134
|
+
clearTimeout(timer);
|
|
1135
|
+
}
|
|
1119
1136
|
} catch (e) {
|
|
1120
1137
|
if (!spec.awaitResult) return;
|
|
1121
1138
|
result = e instanceof Error ? `\uC624\uB958: ${e.message}` : "\uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4.";
|
|
@@ -1123,7 +1140,15 @@ function useXbNovaChat(options) {
|
|
|
1123
1140
|
if (!spec.awaitResult) return;
|
|
1124
1141
|
const conversationId = onboarding?.conversation_id;
|
|
1125
1142
|
if (!conversationId) return;
|
|
1126
|
-
const
|
|
1143
|
+
const structured = result && typeof result === "object" ? result : null;
|
|
1144
|
+
if (structured?.view) {
|
|
1145
|
+
setClientResults((prev) => ({
|
|
1146
|
+
...prev,
|
|
1147
|
+
[part.toolCallId]: { view: structured.view, output: structured.output }
|
|
1148
|
+
}));
|
|
1149
|
+
}
|
|
1150
|
+
const rawText = structured ? structured.text : result;
|
|
1151
|
+
const resultText = typeof rawText === "string" && rawText.trim() ? rawText : "\uD578\uB4E4\uB7EC\uAC00 \uACB0\uACFC\uB97C \uBC18\uD658\uD558\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.";
|
|
1127
1152
|
setBusy(true);
|
|
1128
1153
|
try {
|
|
1129
1154
|
await resumeToolResult(
|
|
@@ -1192,7 +1217,7 @@ function useXbNovaChat(options) {
|
|
|
1192
1217
|
setBusy(false);
|
|
1193
1218
|
}
|
|
1194
1219
|
}
|
|
1195
|
-
return { onboarding, messages, busy, error, send };
|
|
1220
|
+
return { onboarding, messages, busy, error, send, clientResults };
|
|
1196
1221
|
}
|
|
1197
1222
|
|
|
1198
1223
|
// src/views.tsx
|
|
@@ -1315,9 +1340,10 @@ function ToolView({
|
|
|
1315
1340
|
|
|
1316
1341
|
// src/chat.tsx
|
|
1317
1342
|
import { Fragment, jsx as jsx7, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1318
|
-
function hasVisiblePart(parts, clientActionHandlers) {
|
|
1343
|
+
function hasVisiblePart(parts, clientActionHandlers, clientResults) {
|
|
1319
1344
|
return parts.some((part) => {
|
|
1320
1345
|
if (part.kind === "text") return true;
|
|
1346
|
+
if (clientResults[part.toolCallId]) return true;
|
|
1321
1347
|
const spec = clientActionHandlers.get(part.toolName);
|
|
1322
1348
|
return spec ? Boolean(spec.render) : true;
|
|
1323
1349
|
});
|
|
@@ -1450,6 +1476,7 @@ function XbNovaChat({
|
|
|
1450
1476
|
backdropStyle,
|
|
1451
1477
|
defaultOpen = false,
|
|
1452
1478
|
idleText = "AI\uC5D0\uAC8C \uC774 \uCC28 \uBB3C\uC5B4\uBCF4\uAE30",
|
|
1479
|
+
avatarSize = 28,
|
|
1453
1480
|
onVerdictAction
|
|
1454
1481
|
}) {
|
|
1455
1482
|
const { auth, trackRegistry, factsRegistry, visitorId, clientActionHandlers, theme } = useXbNovaInternal();
|
|
@@ -1457,6 +1484,7 @@ function XbNovaChat({
|
|
|
1457
1484
|
"--xbnova-accent-1": theme.accentFrom,
|
|
1458
1485
|
"--xbnova-accent-2": theme.accentTo
|
|
1459
1486
|
};
|
|
1487
|
+
const headerAvatarSize = Math.min(36, Math.max(24, Math.round(avatarSize * 0.8)));
|
|
1460
1488
|
const avatarGradientStyle = {
|
|
1461
1489
|
background: "linear-gradient(135deg, var(--xbnova-accent-1), var(--xbnova-accent-2))"
|
|
1462
1490
|
};
|
|
@@ -1472,7 +1500,7 @@ function XbNovaChat({
|
|
|
1472
1500
|
const [input, setInput] = useState4("");
|
|
1473
1501
|
const bottomRef = useRef8(null);
|
|
1474
1502
|
const typedHintText = useTypewriter(hint?.text ?? "", hint?.triggerNodeId ?? "");
|
|
1475
|
-
const { onboarding, messages, busy, error, send } = useXbNovaChat({
|
|
1503
|
+
const { onboarding, messages, busy, error, send, clientResults } = useXbNovaChat({
|
|
1476
1504
|
botId: activeBotId ?? botId,
|
|
1477
1505
|
nudgeNodeId: activeNudgeNodeId,
|
|
1478
1506
|
onDismissSheet: () => setOpen(false)
|
|
@@ -1602,11 +1630,12 @@ function XbNovaChat({
|
|
|
1602
1630
|
className: "flex shrink-0 items-center justify-center",
|
|
1603
1631
|
style: { width: collapsedSize, height: collapsedSize },
|
|
1604
1632
|
children: /* @__PURE__ */ jsx7(
|
|
1605
|
-
|
|
1633
|
+
Avatar,
|
|
1606
1634
|
{
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1635
|
+
branding: onboarding?.branding,
|
|
1636
|
+
size: avatarSize,
|
|
1637
|
+
className: "shadow-md",
|
|
1638
|
+
gradient: avatarGradientStyle
|
|
1610
1639
|
}
|
|
1611
1640
|
)
|
|
1612
1641
|
}
|
|
@@ -1650,11 +1679,12 @@ function XbNovaChat({
|
|
|
1650
1679
|
children: /* @__PURE__ */ jsxs3("div", { className: "flex h-full w-full flex-col rounded-t-[20px] bg-white shadow-2xl sm:rounded-2xl sm:border sm:border-neutral-200 dark:bg-neutral-900 sm:dark:border-neutral-700", children: [
|
|
1651
1680
|
/* @__PURE__ */ jsxs3("header", { className: "flex items-center gap-2 border-b border-neutral-200 px-3.5 py-2.5 dark:border-neutral-700", children: [
|
|
1652
1681
|
/* @__PURE__ */ jsx7(
|
|
1653
|
-
|
|
1682
|
+
Avatar,
|
|
1654
1683
|
{
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1684
|
+
branding: onboarding?.branding,
|
|
1685
|
+
size: headerAvatarSize,
|
|
1686
|
+
className: "shrink-0",
|
|
1687
|
+
gradient: avatarGradientStyle
|
|
1658
1688
|
}
|
|
1659
1689
|
),
|
|
1660
1690
|
/* @__PURE__ */ jsxs3("div", { className: "min-w-0 flex-1", children: [
|
|
@@ -1681,12 +1711,24 @@ function XbNovaChat({
|
|
|
1681
1711
|
return /* @__PURE__ */ jsx7(Bubble, { role: "assistant", children: /* @__PURE__ */ jsx7(AssistantMarkdown, { text: part.text }) }, j);
|
|
1682
1712
|
}
|
|
1683
1713
|
if (clientActionHandlers.has(part.toolName)) {
|
|
1714
|
+
const result = clientResults[part.toolCallId];
|
|
1715
|
+
if (result) {
|
|
1716
|
+
return /* @__PURE__ */ jsx7(
|
|
1717
|
+
ToolView,
|
|
1718
|
+
{
|
|
1719
|
+
part: { ...part, state: "output", view: result.view, output: result.output },
|
|
1720
|
+
viewRenderers,
|
|
1721
|
+
onVerdictAction
|
|
1722
|
+
},
|
|
1723
|
+
j
|
|
1724
|
+
);
|
|
1725
|
+
}
|
|
1684
1726
|
const spec = clientActionHandlers.get(part.toolName);
|
|
1685
1727
|
return spec?.render ? /* @__PURE__ */ jsx7("div", { children: spec.render(part.input) }, j) : null;
|
|
1686
1728
|
}
|
|
1687
1729
|
return /* @__PURE__ */ jsx7(ToolView, { part, viewRenderers, onVerdictAction }, j);
|
|
1688
1730
|
}),
|
|
1689
|
-
!hasVisiblePart(message.parts, clientActionHandlers) && busy ? /* @__PURE__ */ jsx7(Bubble, { role: "assistant", children: /* @__PURE__ */ jsx7("span", { className: "text-neutral-400", children: "\uC0DD\uAC01\uD558\uB294 \uC911\u2026" }) }) : null
|
|
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
|
|
1690
1732
|
] }) }, i)),
|
|
1691
1733
|
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,
|
|
1692
1734
|
/* @__PURE__ */ jsx7("div", { ref: bottomRef })
|
|
@@ -1757,6 +1799,37 @@ function XbNovaChat({
|
|
|
1757
1799
|
)
|
|
1758
1800
|
] });
|
|
1759
1801
|
}
|
|
1802
|
+
function Avatar({
|
|
1803
|
+
branding,
|
|
1804
|
+
size,
|
|
1805
|
+
className = "",
|
|
1806
|
+
gradient
|
|
1807
|
+
}) {
|
|
1808
|
+
const box = { width: size, height: size };
|
|
1809
|
+
const logoUrl = branding?.logo_url?.trim() ?? "";
|
|
1810
|
+
const [failedUrl, setFailedUrl] = useState4("");
|
|
1811
|
+
if (logoUrl && failedUrl !== logoUrl) {
|
|
1812
|
+
return /* @__PURE__ */ jsx7(
|
|
1813
|
+
"img",
|
|
1814
|
+
{
|
|
1815
|
+
src: logoUrl,
|
|
1816
|
+
alt: "",
|
|
1817
|
+
onError: () => setFailedUrl(logoUrl),
|
|
1818
|
+
style: box,
|
|
1819
|
+
className: `${className} flex-none rounded-full bg-white object-cover`
|
|
1820
|
+
}
|
|
1821
|
+
);
|
|
1822
|
+
}
|
|
1823
|
+
const text = branding?.avatar_text?.trim() || "AI";
|
|
1824
|
+
return /* @__PURE__ */ jsx7(
|
|
1825
|
+
"span",
|
|
1826
|
+
{
|
|
1827
|
+
className: `${className} flex flex-none items-center justify-center rounded-full font-bold text-white`,
|
|
1828
|
+
style: { ...gradient, ...box, fontSize: Math.max(10, Math.round(size * 0.4)) },
|
|
1829
|
+
children: text
|
|
1830
|
+
}
|
|
1831
|
+
);
|
|
1832
|
+
}
|
|
1760
1833
|
function Bubble({ role, children }) {
|
|
1761
1834
|
const mine = role === "user";
|
|
1762
1835
|
return /* @__PURE__ */ jsx7("div", { className: mine ? "flex justify-end" : "flex justify-start", children: /* @__PURE__ */ jsx7(
|