@xbrain-swe/nova-chat-widget 0.1.3 → 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 +102 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +57 -30
- package/dist/index.d.ts +57 -30
- package/dist/index.js +102 -15
- 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 제외)을 덮어서, 시트를 안 닫으면
|
|
@@ -43,6 +86,10 @@ type FactValue = unknown | (() => unknown);
|
|
|
43
86
|
type XbNovaTheme = {
|
|
44
87
|
accentFrom: string;
|
|
45
88
|
accentTo: string;
|
|
89
|
+
/** 위젯의 라이트/다크. `"system"`(기본)은 브라우저·OS 설정을 따른다 — 호스트 사이트가 다크
|
|
90
|
+
* 모드를 지원하지 않는데 사용자 OS만 다크이면 사이트는 밝은데 위젯만 어두워진다. 그런 사이트는
|
|
91
|
+
* `"light"`로 고정하고, 사이트 자체 테마를 가진 호스트는 그 값을 넘기면 된다. */
|
|
92
|
+
colorScheme: "light" | "dark" | "system";
|
|
46
93
|
};
|
|
47
94
|
|
|
48
95
|
declare function XbNovaProvider({ apiKey, botId, appId, screen, apiBaseUrl, clientActions, theme, children, }: {
|
|
@@ -62,34 +109,6 @@ declare function XbNovaProvider({ apiKey, botId, appId, screen, apiBaseUrl, clie
|
|
|
62
109
|
children: React.ReactNode;
|
|
63
110
|
}): react.JSX.Element;
|
|
64
111
|
|
|
65
|
-
/**
|
|
66
|
-
* AI SDK UI Message Stream v1 파서. `nova/web/lib/chat-stream.ts`에서 그대로 옮겼다 — 이미
|
|
67
|
-
* 콘솔에서 검증된 로직이라 새로 설계하지 않는다.
|
|
68
|
-
*
|
|
69
|
-
* `ai` 패키지에 의존하지 않는다 — 고객사 번들에 peer 버전 충돌을 만들지 않기 위해서다.
|
|
70
|
-
*/
|
|
71
|
-
type ViewKind = "product_list" | "product_detail" | "metric" | "choices" | "form" | "verdict";
|
|
72
|
-
type ToolPart = {
|
|
73
|
-
kind: "tool";
|
|
74
|
-
toolCallId: string;
|
|
75
|
-
toolName: string;
|
|
76
|
-
state: "input" | "output" | "error";
|
|
77
|
-
input?: unknown;
|
|
78
|
-
output?: unknown;
|
|
79
|
-
/** 위젯은 tool 이름이 아니라 이 값으로 렌더러를 고른다. */
|
|
80
|
-
view?: ViewKind | null;
|
|
81
|
-
};
|
|
82
|
-
type TextPart = {
|
|
83
|
-
kind: "text";
|
|
84
|
-
id: string;
|
|
85
|
-
text: string;
|
|
86
|
-
};
|
|
87
|
-
type MessagePart = TextPart | ToolPart;
|
|
88
|
-
type ChatMessage = {
|
|
89
|
-
role: "user" | "assistant";
|
|
90
|
-
parts: MessagePart[];
|
|
91
|
-
};
|
|
92
|
-
|
|
93
112
|
type ProductItem = {
|
|
94
113
|
id: string;
|
|
95
114
|
title: string;
|
|
@@ -153,12 +172,18 @@ declare function XbNovaChat({ viewRenderers, botId, barStyle, panelStyle, backdr
|
|
|
153
172
|
onVerdictAction?: (action: string) => void;
|
|
154
173
|
}): react.JSX.Element | null;
|
|
155
174
|
|
|
175
|
+
type ClientResults = Record<string, {
|
|
176
|
+
view: ViewKind;
|
|
177
|
+
output: unknown;
|
|
178
|
+
}>;
|
|
156
179
|
type XbNovaOnboarding = {
|
|
157
180
|
conversation_id: string;
|
|
158
181
|
greeting: string;
|
|
159
182
|
quick_replies: string[];
|
|
160
183
|
branding: {
|
|
161
184
|
title?: string;
|
|
185
|
+
logo_url?: string;
|
|
186
|
+
avatar_text?: string;
|
|
162
187
|
};
|
|
163
188
|
};
|
|
164
189
|
type UseXbNovaChatResult = {
|
|
@@ -167,6 +192,8 @@ type UseXbNovaChatResult = {
|
|
|
167
192
|
busy: boolean;
|
|
168
193
|
error: string | null;
|
|
169
194
|
send: (text: string) => Promise<void>;
|
|
195
|
+
/** client action 핸들러가 돌려준 정규화 결과(tool_call_id별). 채팅 화면이 view 렌더러로 그린다. */
|
|
196
|
+
clientResults: ClientResults;
|
|
170
197
|
};
|
|
171
198
|
/** `botId`를 넘기면 Provider의 기본 봇 대신 그 봇으로 새 대화를 연다 — 넛지 handoff가 쓴다.
|
|
172
199
|
* `nudgeNodeId`를 넘기면 이 대화가 그 넛지에서 시작됐다고 서버에 표시한다 — 넛지 전환
|
|
@@ -222,4 +249,4 @@ declare function useXbNovaSettings(): {
|
|
|
222
249
|
showBar: () => void;
|
|
223
250
|
};
|
|
224
251
|
|
|
225
|
-
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 제외)을 덮어서, 시트를 안 닫으면
|
|
@@ -43,6 +86,10 @@ type FactValue = unknown | (() => unknown);
|
|
|
43
86
|
type XbNovaTheme = {
|
|
44
87
|
accentFrom: string;
|
|
45
88
|
accentTo: string;
|
|
89
|
+
/** 위젯의 라이트/다크. `"system"`(기본)은 브라우저·OS 설정을 따른다 — 호스트 사이트가 다크
|
|
90
|
+
* 모드를 지원하지 않는데 사용자 OS만 다크이면 사이트는 밝은데 위젯만 어두워진다. 그런 사이트는
|
|
91
|
+
* `"light"`로 고정하고, 사이트 자체 테마를 가진 호스트는 그 값을 넘기면 된다. */
|
|
92
|
+
colorScheme: "light" | "dark" | "system";
|
|
46
93
|
};
|
|
47
94
|
|
|
48
95
|
declare function XbNovaProvider({ apiKey, botId, appId, screen, apiBaseUrl, clientActions, theme, children, }: {
|
|
@@ -62,34 +109,6 @@ declare function XbNovaProvider({ apiKey, botId, appId, screen, apiBaseUrl, clie
|
|
|
62
109
|
children: React.ReactNode;
|
|
63
110
|
}): react.JSX.Element;
|
|
64
111
|
|
|
65
|
-
/**
|
|
66
|
-
* AI SDK UI Message Stream v1 파서. `nova/web/lib/chat-stream.ts`에서 그대로 옮겼다 — 이미
|
|
67
|
-
* 콘솔에서 검증된 로직이라 새로 설계하지 않는다.
|
|
68
|
-
*
|
|
69
|
-
* `ai` 패키지에 의존하지 않는다 — 고객사 번들에 peer 버전 충돌을 만들지 않기 위해서다.
|
|
70
|
-
*/
|
|
71
|
-
type ViewKind = "product_list" | "product_detail" | "metric" | "choices" | "form" | "verdict";
|
|
72
|
-
type ToolPart = {
|
|
73
|
-
kind: "tool";
|
|
74
|
-
toolCallId: string;
|
|
75
|
-
toolName: string;
|
|
76
|
-
state: "input" | "output" | "error";
|
|
77
|
-
input?: unknown;
|
|
78
|
-
output?: unknown;
|
|
79
|
-
/** 위젯은 tool 이름이 아니라 이 값으로 렌더러를 고른다. */
|
|
80
|
-
view?: ViewKind | null;
|
|
81
|
-
};
|
|
82
|
-
type TextPart = {
|
|
83
|
-
kind: "text";
|
|
84
|
-
id: string;
|
|
85
|
-
text: string;
|
|
86
|
-
};
|
|
87
|
-
type MessagePart = TextPart | ToolPart;
|
|
88
|
-
type ChatMessage = {
|
|
89
|
-
role: "user" | "assistant";
|
|
90
|
-
parts: MessagePart[];
|
|
91
|
-
};
|
|
92
|
-
|
|
93
112
|
type ProductItem = {
|
|
94
113
|
id: string;
|
|
95
114
|
title: string;
|
|
@@ -153,12 +172,18 @@ declare function XbNovaChat({ viewRenderers, botId, barStyle, panelStyle, backdr
|
|
|
153
172
|
onVerdictAction?: (action: string) => void;
|
|
154
173
|
}): react.JSX.Element | null;
|
|
155
174
|
|
|
175
|
+
type ClientResults = Record<string, {
|
|
176
|
+
view: ViewKind;
|
|
177
|
+
output: unknown;
|
|
178
|
+
}>;
|
|
156
179
|
type XbNovaOnboarding = {
|
|
157
180
|
conversation_id: string;
|
|
158
181
|
greeting: string;
|
|
159
182
|
quick_replies: string[];
|
|
160
183
|
branding: {
|
|
161
184
|
title?: string;
|
|
185
|
+
logo_url?: string;
|
|
186
|
+
avatar_text?: string;
|
|
162
187
|
};
|
|
163
188
|
};
|
|
164
189
|
type UseXbNovaChatResult = {
|
|
@@ -167,6 +192,8 @@ type UseXbNovaChatResult = {
|
|
|
167
192
|
busy: boolean;
|
|
168
193
|
error: string | null;
|
|
169
194
|
send: (text: string) => Promise<void>;
|
|
195
|
+
/** client action 핸들러가 돌려준 정규화 결과(tool_call_id별). 채팅 화면이 view 렌더러로 그린다. */
|
|
196
|
+
clientResults: ClientResults;
|
|
170
197
|
};
|
|
171
198
|
/** `botId`를 넘기면 Provider의 기본 봇 대신 그 봇으로 새 대화를 연다 — 넛지 handoff가 쓴다.
|
|
172
199
|
* `nudgeNodeId`를 넘기면 이 대화가 그 넛지에서 시작됐다고 서버에 표시한다 — 넛지 전환
|
|
@@ -222,4 +249,4 @@ declare function useXbNovaSettings(): {
|
|
|
222
249
|
showBar: () => void;
|
|
223
250
|
};
|
|
224
251
|
|
|
225
|
-
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
|
@@ -11,7 +11,8 @@ import { createContext, useContext, useMemo, useRef } from "react";
|
|
|
11
11
|
import { jsx } from "react/jsx-runtime";
|
|
12
12
|
var DEFAULT_THEME = {
|
|
13
13
|
accentFrom: "#4f46e5",
|
|
14
|
-
accentTo: "#7c3aed"
|
|
14
|
+
accentTo: "#7c3aed",
|
|
15
|
+
colorScheme: "system"
|
|
15
16
|
};
|
|
16
17
|
var InternalContext = createContext(null);
|
|
17
18
|
function useXbNovaInternal() {
|
|
@@ -887,6 +888,7 @@ function XbNovaProvider({
|
|
|
887
888
|
{
|
|
888
889
|
handler: spec.handler,
|
|
889
890
|
awaitResult: spec.awaitResult ?? false,
|
|
891
|
+
timeoutMs: spec.timeoutMs,
|
|
890
892
|
dismissSheet: spec.dismissSheet ?? false,
|
|
891
893
|
render: spec.render
|
|
892
894
|
}
|
|
@@ -921,10 +923,25 @@ function loadCss() {
|
|
|
921
923
|
}
|
|
922
924
|
return cssPromise;
|
|
923
925
|
}
|
|
926
|
+
function useResolvedScheme(scheme) {
|
|
927
|
+
const [systemDark, setSystemDark] = useState2(false);
|
|
928
|
+
useEffect6(() => {
|
|
929
|
+
if (scheme !== "system") return;
|
|
930
|
+
const mq = window.matchMedia("(prefers-color-scheme: dark)");
|
|
931
|
+
setSystemDark(mq.matches);
|
|
932
|
+
const onChange = (e) => setSystemDark(e.matches);
|
|
933
|
+
mq.addEventListener("change", onChange);
|
|
934
|
+
return () => mq.removeEventListener("change", onChange);
|
|
935
|
+
}, [scheme]);
|
|
936
|
+
if (scheme === "system") return systemDark ? "dark" : "light";
|
|
937
|
+
return scheme;
|
|
938
|
+
}
|
|
924
939
|
function XbNovaShadowRoot({
|
|
925
940
|
children,
|
|
926
941
|
style
|
|
927
942
|
}) {
|
|
943
|
+
const { theme } = useXbNovaInternal();
|
|
944
|
+
const scheme = useResolvedScheme(theme.colorScheme);
|
|
928
945
|
const hostRef = useRef6(null);
|
|
929
946
|
const [mountNode, setMountNode] = useState2(null);
|
|
930
947
|
const [portalTarget, setPortalTarget] = useState2(null);
|
|
@@ -946,6 +963,11 @@ function XbNovaShadowRoot({
|
|
|
946
963
|
shadow.appendChild(mount);
|
|
947
964
|
setMountNode(mount);
|
|
948
965
|
}, [portalTarget]);
|
|
966
|
+
useEffect6(() => {
|
|
967
|
+
if (!mountNode) return;
|
|
968
|
+
mountNode.dataset.xbnovaScheme = scheme;
|
|
969
|
+
mountNode.style.colorScheme = scheme;
|
|
970
|
+
}, [mountNode, scheme]);
|
|
949
971
|
if (!portalTarget) return null;
|
|
950
972
|
return createPortal(
|
|
951
973
|
/* @__PURE__ */ jsx5("div", { ref: hostRef, style, children: mountNode ? createPortal(children, mountNode) : null }),
|
|
@@ -1062,6 +1084,7 @@ async function resumeToolResult(apiBaseUrl, apiKey, body, handlers) {
|
|
|
1062
1084
|
}
|
|
1063
1085
|
|
|
1064
1086
|
// src/use-chat.ts
|
|
1087
|
+
var DEFAULT_CLIENT_ACTION_TIMEOUT_MS = 15e3;
|
|
1065
1088
|
function useXbNovaChat(options) {
|
|
1066
1089
|
const { auth, trackRegistry, clientActionHandlers } = useXbNovaInternal();
|
|
1067
1090
|
const botId = options?.botId ?? auth.botId;
|
|
@@ -1071,6 +1094,7 @@ function useXbNovaChat(options) {
|
|
|
1071
1094
|
const [messages, setMessages] = useState3([]);
|
|
1072
1095
|
const [busy, setBusy] = useState3(false);
|
|
1073
1096
|
const [error, setError] = useState3(null);
|
|
1097
|
+
const [clientResults, setClientResults] = useState3({});
|
|
1074
1098
|
const dispatchedClientActionsRef = useRef7(/* @__PURE__ */ new Set());
|
|
1075
1099
|
const nudgeNodeIdRef = useRef7(nudgeNodeId);
|
|
1076
1100
|
nudgeNodeIdRef.current = nudgeNodeId;
|
|
@@ -1078,6 +1102,7 @@ function useXbNovaChat(options) {
|
|
|
1078
1102
|
setOnboarding(null);
|
|
1079
1103
|
setMessages([]);
|
|
1080
1104
|
setError(null);
|
|
1105
|
+
setClientResults({});
|
|
1081
1106
|
dispatchedClientActionsRef.current = /* @__PURE__ */ new Set();
|
|
1082
1107
|
fetch(`${apiBaseUrl}/v1/conversations`, {
|
|
1083
1108
|
method: "POST",
|
|
@@ -1094,7 +1119,20 @@ function useXbNovaChat(options) {
|
|
|
1094
1119
|
if (spec.dismissSheet) options?.onDismissSheet?.();
|
|
1095
1120
|
let result;
|
|
1096
1121
|
try {
|
|
1097
|
-
|
|
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
|
+
}
|
|
1098
1136
|
} catch (e) {
|
|
1099
1137
|
if (!spec.awaitResult) return;
|
|
1100
1138
|
result = e instanceof Error ? `\uC624\uB958: ${e.message}` : "\uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4.";
|
|
@@ -1102,7 +1140,15 @@ function useXbNovaChat(options) {
|
|
|
1102
1140
|
if (!spec.awaitResult) return;
|
|
1103
1141
|
const conversationId = onboarding?.conversation_id;
|
|
1104
1142
|
if (!conversationId) return;
|
|
1105
|
-
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.";
|
|
1106
1152
|
setBusy(true);
|
|
1107
1153
|
try {
|
|
1108
1154
|
await resumeToolResult(
|
|
@@ -1171,7 +1217,7 @@ function useXbNovaChat(options) {
|
|
|
1171
1217
|
setBusy(false);
|
|
1172
1218
|
}
|
|
1173
1219
|
}
|
|
1174
|
-
return { onboarding, messages, busy, error, send };
|
|
1220
|
+
return { onboarding, messages, busy, error, send, clientResults };
|
|
1175
1221
|
}
|
|
1176
1222
|
|
|
1177
1223
|
// src/views.tsx
|
|
@@ -1294,9 +1340,10 @@ function ToolView({
|
|
|
1294
1340
|
|
|
1295
1341
|
// src/chat.tsx
|
|
1296
1342
|
import { Fragment, jsx as jsx7, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1297
|
-
function hasVisiblePart(parts, clientActionHandlers) {
|
|
1343
|
+
function hasVisiblePart(parts, clientActionHandlers, clientResults) {
|
|
1298
1344
|
return parts.some((part) => {
|
|
1299
1345
|
if (part.kind === "text") return true;
|
|
1346
|
+
if (clientResults[part.toolCallId]) return true;
|
|
1300
1347
|
const spec = clientActionHandlers.get(part.toolName);
|
|
1301
1348
|
return spec ? Boolean(spec.render) : true;
|
|
1302
1349
|
});
|
|
@@ -1451,7 +1498,7 @@ function XbNovaChat({
|
|
|
1451
1498
|
const [input, setInput] = useState4("");
|
|
1452
1499
|
const bottomRef = useRef8(null);
|
|
1453
1500
|
const typedHintText = useTypewriter(hint?.text ?? "", hint?.triggerNodeId ?? "");
|
|
1454
|
-
const { onboarding, messages, busy, error, send } = useXbNovaChat({
|
|
1501
|
+
const { onboarding, messages, busy, error, send, clientResults } = useXbNovaChat({
|
|
1455
1502
|
botId: activeBotId ?? botId,
|
|
1456
1503
|
nudgeNodeId: activeNudgeNodeId,
|
|
1457
1504
|
onDismissSheet: () => setOpen(false)
|
|
@@ -1581,11 +1628,11 @@ function XbNovaChat({
|
|
|
1581
1628
|
className: "flex shrink-0 items-center justify-center",
|
|
1582
1629
|
style: { width: collapsedSize, height: collapsedSize },
|
|
1583
1630
|
children: /* @__PURE__ */ jsx7(
|
|
1584
|
-
|
|
1631
|
+
Avatar,
|
|
1585
1632
|
{
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1633
|
+
branding: onboarding?.branding,
|
|
1634
|
+
className: "h-7 w-7 text-[11px] shadow-md",
|
|
1635
|
+
gradient: avatarGradientStyle
|
|
1589
1636
|
}
|
|
1590
1637
|
)
|
|
1591
1638
|
}
|
|
@@ -1629,11 +1676,11 @@ function XbNovaChat({
|
|
|
1629
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: [
|
|
1630
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: [
|
|
1631
1678
|
/* @__PURE__ */ jsx7(
|
|
1632
|
-
|
|
1679
|
+
Avatar,
|
|
1633
1680
|
{
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1681
|
+
branding: onboarding?.branding,
|
|
1682
|
+
className: "h-6 w-6 shrink-0 text-[10px]",
|
|
1683
|
+
gradient: avatarGradientStyle
|
|
1637
1684
|
}
|
|
1638
1685
|
),
|
|
1639
1686
|
/* @__PURE__ */ jsxs3("div", { className: "min-w-0 flex-1", children: [
|
|
@@ -1660,12 +1707,24 @@ function XbNovaChat({
|
|
|
1660
1707
|
return /* @__PURE__ */ jsx7(Bubble, { role: "assistant", children: /* @__PURE__ */ jsx7(AssistantMarkdown, { text: part.text }) }, j);
|
|
1661
1708
|
}
|
|
1662
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
|
+
}
|
|
1663
1722
|
const spec = clientActionHandlers.get(part.toolName);
|
|
1664
1723
|
return spec?.render ? /* @__PURE__ */ jsx7("div", { children: spec.render(part.input) }, j) : null;
|
|
1665
1724
|
}
|
|
1666
1725
|
return /* @__PURE__ */ jsx7(ToolView, { part, viewRenderers, onVerdictAction }, j);
|
|
1667
1726
|
}),
|
|
1668
|
-
!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
|
|
1669
1728
|
] }) }, i)),
|
|
1670
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,
|
|
1671
1730
|
/* @__PURE__ */ jsx7("div", { ref: bottomRef })
|
|
@@ -1736,6 +1795,34 @@ function XbNovaChat({
|
|
|
1736
1795
|
)
|
|
1737
1796
|
] });
|
|
1738
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
|
+
}
|
|
1739
1826
|
function Bubble({ role, children }) {
|
|
1740
1827
|
const mine = role === "user";
|
|
1741
1828
|
return /* @__PURE__ */ jsx7("div", { className: mine ? "flex justify-end" : "flex justify-start", children: /* @__PURE__ */ jsx7(
|