@xbrain-swe/nova-chat-widget 0.1.4 → 0.1.6
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 +80 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +53 -30
- package/dist/index.d.ts +53 -30
- package/dist/index.js +80 -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;
|
|
@@ -157,12 +172,18 @@ declare function XbNovaChat({ viewRenderers, botId, barStyle, panelStyle, backdr
|
|
|
157
172
|
onVerdictAction?: (action: string) => void;
|
|
158
173
|
}): react.JSX.Element | null;
|
|
159
174
|
|
|
175
|
+
type ClientResults = Record<string, {
|
|
176
|
+
view: ViewKind;
|
|
177
|
+
output: unknown;
|
|
178
|
+
}>;
|
|
160
179
|
type XbNovaOnboarding = {
|
|
161
180
|
conversation_id: string;
|
|
162
181
|
greeting: string;
|
|
163
182
|
quick_replies: string[];
|
|
164
183
|
branding: {
|
|
165
184
|
title?: string;
|
|
185
|
+
logo_url?: string;
|
|
186
|
+
avatar_text?: string;
|
|
166
187
|
};
|
|
167
188
|
};
|
|
168
189
|
type UseXbNovaChatResult = {
|
|
@@ -171,6 +192,8 @@ type UseXbNovaChatResult = {
|
|
|
171
192
|
busy: boolean;
|
|
172
193
|
error: string | null;
|
|
173
194
|
send: (text: string) => Promise<void>;
|
|
195
|
+
/** client action 핸들러가 돌려준 정규화 결과(tool_call_id별). 채팅 화면이 view 렌더러로 그린다. */
|
|
196
|
+
clientResults: ClientResults;
|
|
174
197
|
};
|
|
175
198
|
/** `botId`를 넘기면 Provider의 기본 봇 대신 그 봇으로 새 대화를 연다 — 넛지 handoff가 쓴다.
|
|
176
199
|
* `nudgeNodeId`를 넘기면 이 대화가 그 넛지에서 시작됐다고 서버에 표시한다 — 넛지 전환
|
|
@@ -226,4 +249,4 @@ declare function useXbNovaSettings(): {
|
|
|
226
249
|
showBar: () => void;
|
|
227
250
|
};
|
|
228
251
|
|
|
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 };
|
|
252
|
+
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;
|
|
@@ -157,12 +172,18 @@ declare function XbNovaChat({ viewRenderers, botId, barStyle, panelStyle, backdr
|
|
|
157
172
|
onVerdictAction?: (action: string) => void;
|
|
158
173
|
}): react.JSX.Element | null;
|
|
159
174
|
|
|
175
|
+
type ClientResults = Record<string, {
|
|
176
|
+
view: ViewKind;
|
|
177
|
+
output: unknown;
|
|
178
|
+
}>;
|
|
160
179
|
type XbNovaOnboarding = {
|
|
161
180
|
conversation_id: string;
|
|
162
181
|
greeting: string;
|
|
163
182
|
quick_replies: string[];
|
|
164
183
|
branding: {
|
|
165
184
|
title?: string;
|
|
185
|
+
logo_url?: string;
|
|
186
|
+
avatar_text?: string;
|
|
166
187
|
};
|
|
167
188
|
};
|
|
168
189
|
type UseXbNovaChatResult = {
|
|
@@ -171,6 +192,8 @@ type UseXbNovaChatResult = {
|
|
|
171
192
|
busy: boolean;
|
|
172
193
|
error: string | null;
|
|
173
194
|
send: (text: string) => Promise<void>;
|
|
195
|
+
/** client action 핸들러가 돌려준 정규화 결과(tool_call_id별). 채팅 화면이 view 렌더러로 그린다. */
|
|
196
|
+
clientResults: ClientResults;
|
|
174
197
|
};
|
|
175
198
|
/** `botId`를 넘기면 Provider의 기본 봇 대신 그 봇으로 새 대화를 연다 — 넛지 handoff가 쓴다.
|
|
176
199
|
* `nudgeNodeId`를 넘기면 이 대화가 그 넛지에서 시작됐다고 서버에 표시한다 — 넛지 전환
|
|
@@ -226,4 +249,4 @@ declare function useXbNovaSettings(): {
|
|
|
226
249
|
showBar: () => void;
|
|
227
250
|
};
|
|
228
251
|
|
|
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 };
|
|
252
|
+
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
|
});
|
|
@@ -1472,7 +1498,7 @@ function XbNovaChat({
|
|
|
1472
1498
|
const [input, setInput] = useState4("");
|
|
1473
1499
|
const bottomRef = useRef8(null);
|
|
1474
1500
|
const typedHintText = useTypewriter(hint?.text ?? "", hint?.triggerNodeId ?? "");
|
|
1475
|
-
const { onboarding, messages, busy, error, send } = useXbNovaChat({
|
|
1501
|
+
const { onboarding, messages, busy, error, send, clientResults } = useXbNovaChat({
|
|
1476
1502
|
botId: activeBotId ?? botId,
|
|
1477
1503
|
nudgeNodeId: activeNudgeNodeId,
|
|
1478
1504
|
onDismissSheet: () => setOpen(false)
|
|
@@ -1602,11 +1628,11 @@ function XbNovaChat({
|
|
|
1602
1628
|
className: "flex shrink-0 items-center justify-center",
|
|
1603
1629
|
style: { width: collapsedSize, height: collapsedSize },
|
|
1604
1630
|
children: /* @__PURE__ */ jsx7(
|
|
1605
|
-
|
|
1631
|
+
Avatar,
|
|
1606
1632
|
{
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1633
|
+
branding: onboarding?.branding,
|
|
1634
|
+
className: "h-7 w-7 text-[11px] shadow-md",
|
|
1635
|
+
gradient: avatarGradientStyle
|
|
1610
1636
|
}
|
|
1611
1637
|
)
|
|
1612
1638
|
}
|
|
@@ -1650,11 +1676,11 @@ function XbNovaChat({
|
|
|
1650
1676
|
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
1677
|
/* @__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
1678
|
/* @__PURE__ */ jsx7(
|
|
1653
|
-
|
|
1679
|
+
Avatar,
|
|
1654
1680
|
{
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1681
|
+
branding: onboarding?.branding,
|
|
1682
|
+
className: "h-6 w-6 shrink-0 text-[10px]",
|
|
1683
|
+
gradient: avatarGradientStyle
|
|
1658
1684
|
}
|
|
1659
1685
|
),
|
|
1660
1686
|
/* @__PURE__ */ jsxs3("div", { className: "min-w-0 flex-1", children: [
|
|
@@ -1681,12 +1707,24 @@ function XbNovaChat({
|
|
|
1681
1707
|
return /* @__PURE__ */ jsx7(Bubble, { role: "assistant", children: /* @__PURE__ */ jsx7(AssistantMarkdown, { text: part.text }) }, j);
|
|
1682
1708
|
}
|
|
1683
1709
|
if (clientActionHandlers.has(part.toolName)) {
|
|
1710
|
+
const result = clientResults[part.toolCallId];
|
|
1711
|
+
if (result) {
|
|
1712
|
+
return /* @__PURE__ */ jsx7(
|
|
1713
|
+
ToolView,
|
|
1714
|
+
{
|
|
1715
|
+
part: { ...part, state: "output", view: result.view, output: result.output },
|
|
1716
|
+
viewRenderers,
|
|
1717
|
+
onVerdictAction
|
|
1718
|
+
},
|
|
1719
|
+
j
|
|
1720
|
+
);
|
|
1721
|
+
}
|
|
1684
1722
|
const spec = clientActionHandlers.get(part.toolName);
|
|
1685
1723
|
return spec?.render ? /* @__PURE__ */ jsx7("div", { children: spec.render(part.input) }, j) : null;
|
|
1686
1724
|
}
|
|
1687
1725
|
return /* @__PURE__ */ jsx7(ToolView, { part, viewRenderers, onVerdictAction }, j);
|
|
1688
1726
|
}),
|
|
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
|
|
1727
|
+
!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
1728
|
] }) }, i)),
|
|
1691
1729
|
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
1730
|
/* @__PURE__ */ jsx7("div", { ref: bottomRef })
|
|
@@ -1757,6 +1795,34 @@ function XbNovaChat({
|
|
|
1757
1795
|
)
|
|
1758
1796
|
] });
|
|
1759
1797
|
}
|
|
1798
|
+
function Avatar({
|
|
1799
|
+
branding,
|
|
1800
|
+
className,
|
|
1801
|
+
gradient
|
|
1802
|
+
}) {
|
|
1803
|
+
const logoUrl = branding?.logo_url?.trim() ?? "";
|
|
1804
|
+
const [failedUrl, setFailedUrl] = useState4("");
|
|
1805
|
+
if (logoUrl && failedUrl !== logoUrl) {
|
|
1806
|
+
return /* @__PURE__ */ jsx7(
|
|
1807
|
+
"img",
|
|
1808
|
+
{
|
|
1809
|
+
src: logoUrl,
|
|
1810
|
+
alt: "",
|
|
1811
|
+
onError: () => setFailedUrl(logoUrl),
|
|
1812
|
+
className: `${className} flex-none rounded-full bg-white object-cover`
|
|
1813
|
+
}
|
|
1814
|
+
);
|
|
1815
|
+
}
|
|
1816
|
+
const text = branding?.avatar_text?.trim() || "AI";
|
|
1817
|
+
return /* @__PURE__ */ jsx7(
|
|
1818
|
+
"span",
|
|
1819
|
+
{
|
|
1820
|
+
className: `${className} flex items-center justify-center rounded-full font-bold text-white`,
|
|
1821
|
+
style: gradient,
|
|
1822
|
+
children: text
|
|
1823
|
+
}
|
|
1824
|
+
);
|
|
1825
|
+
}
|
|
1760
1826
|
function Bubble({ role, children }) {
|
|
1761
1827
|
const mine = role === "user";
|
|
1762
1828
|
return /* @__PURE__ */ jsx7("div", { className: mine ? "flex justify-end" : "flex justify-start", children: /* @__PURE__ */ jsx7(
|