@btst/stack 4.1.1 → 4.3.0
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/README.md +2 -2
- package/dist/packages/stack/src/plugins/ai-chat/api/operations.cjs +15 -9
- package/dist/packages/stack/src/plugins/ai-chat/api/operations.mjs +16 -10
- package/dist/packages/stack/src/plugins/ai-chat/api/page-tools.cjs +28 -0
- package/dist/packages/stack/src/plugins/ai-chat/api/page-tools.mjs +29 -2
- package/dist/packages/stack/src/plugins/ai-chat/api/plugin.cjs +5 -0
- package/dist/packages/stack/src/plugins/ai-chat/api/plugin.mjs +5 -0
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-interface.cjs +91 -8
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-interface.mjs +91 -8
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-layout.cjs +242 -68
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-layout.mjs +246 -72
- package/dist/packages/stack/src/plugins/ai-chat/client/components/tool-call-display.cjs +17 -0
- package/dist/packages/stack/src/plugins/ai-chat/client/components/tool-call-display.mjs +18 -1
- package/dist/packages/stack/src/plugins/ai-chat/client/context/chat-analytics.cjs +42 -0
- package/dist/packages/stack/src/plugins/ai-chat/client/context/chat-analytics.mjs +39 -0
- package/dist/packages/stack/src/plugins/ai-chat/client/plugin.cjs +2 -1
- package/dist/packages/stack/src/plugins/ai-chat/client/plugin.mjs +2 -1
- package/dist/plugins/ai-chat/api/index.d.cts +2 -2
- package/dist/plugins/ai-chat/api/index.d.mts +2 -2
- package/dist/plugins/ai-chat/api/index.d.ts +2 -2
- package/dist/plugins/ai-chat/client/components/index.d.cts +2 -1
- package/dist/plugins/ai-chat/client/components/index.d.mts +2 -1
- package/dist/plugins/ai-chat/client/components/index.d.ts +2 -1
- package/dist/plugins/ai-chat/client/context/page-ai-context.cjs +4 -0
- package/dist/plugins/ai-chat/client/context/page-ai-context.d.cts +6 -2
- package/dist/plugins/ai-chat/client/context/page-ai-context.d.mts +6 -2
- package/dist/plugins/ai-chat/client/context/page-ai-context.d.ts +6 -2
- package/dist/plugins/ai-chat/client/context/page-ai-context.mjs +4 -1
- package/dist/plugins/ai-chat/client/hooks/index.d.cts +2 -2
- package/dist/plugins/ai-chat/client/hooks/index.d.mts +2 -2
- package/dist/plugins/ai-chat/client/hooks/index.d.ts +2 -2
- package/dist/plugins/ai-chat/client/index.d.cts +7 -3
- package/dist/plugins/ai-chat/client/index.d.mts +7 -3
- package/dist/plugins/ai-chat/client/index.d.ts +7 -3
- package/dist/plugins/ai-chat/query-keys.d.cts +1 -1
- package/dist/plugins/ai-chat/query-keys.d.mts +1 -1
- package/dist/plugins/ai-chat/query-keys.d.ts +1 -1
- package/dist/plugins/blog/api/index.d.cts +2 -2
- package/dist/plugins/blog/api/index.d.mts +2 -2
- package/dist/plugins/blog/api/index.d.ts +2 -2
- package/dist/plugins/blog/client/hooks/index.d.cts +2 -2
- package/dist/plugins/blog/client/hooks/index.d.mts +2 -2
- package/dist/plugins/blog/client/hooks/index.d.ts +2 -2
- package/dist/plugins/blog/client/index.d.cts +2 -2
- package/dist/plugins/blog/client/index.d.mts +2 -2
- package/dist/plugins/blog/client/index.d.ts +2 -2
- package/dist/plugins/blog/query-keys.d.cts +2 -2
- package/dist/plugins/blog/query-keys.d.mts +2 -2
- package/dist/plugins/blog/query-keys.d.ts +2 -2
- package/dist/plugins/form-builder/api/index.d.cts +1 -1
- package/dist/plugins/form-builder/api/index.d.mts +1 -1
- package/dist/plugins/form-builder/api/index.d.ts +1 -1
- package/dist/plugins/form-builder/client/hooks/index.d.cts +1 -1
- package/dist/plugins/form-builder/client/hooks/index.d.mts +1 -1
- package/dist/plugins/form-builder/client/hooks/index.d.ts +1 -1
- package/dist/plugins/form-builder/query-keys.d.cts +1 -1
- package/dist/plugins/form-builder/query-keys.d.mts +1 -1
- package/dist/plugins/form-builder/query-keys.d.ts +1 -1
- package/dist/shared/{stack.B5yPgHsp.d.mts → stack.B7Mj74BQ.d.mts} +18 -2
- package/dist/shared/{stack.rVF0f509.d.ts → stack.CHrtGKLz.d.mts} +66 -4
- package/dist/shared/{stack.BggTHKlo.d.mts → stack.CZ1h_T4k.d.mts} +90 -90
- package/dist/shared/{stack.DfrRYOIl.d.ts → stack.CnDMBj4z.d.ts} +5 -5
- package/dist/shared/{stack.94kN3pEK.d.mts → stack.D2y4gl03.d.mts} +5 -5
- package/dist/shared/{stack.3Sr6qRtT.d.ts → stack.DBH1Bjko.d.ts} +18 -2
- package/dist/shared/{stack.lPKwd_v0.d.ts → stack.DQpyA3Cq.d.ts} +90 -90
- package/dist/shared/{stack.C4oVW2qK.d.cts → stack.Dgt3MCtW.d.cts} +18 -2
- package/dist/shared/{stack.CtrnoQgH.d.cts → stack.DiYg7Jw1.d.cts} +90 -90
- package/dist/shared/{stack.rVF0f509.d.cts → stack.DzJgoNqJ.d.cts} +66 -4
- package/dist/shared/{stack.rVF0f509.d.mts → stack.qMp2VzNw.d.ts} +66 -4
- package/dist/shared/{stack.BTStHVTy.d.cts → stack.rmWRtSjs.d.cts} +5 -5
- package/package.json +2 -2
- package/src/plugins/ai-chat/__tests__/authorization.test.ts +106 -1
- package/src/plugins/ai-chat/__tests__/classnames-overrides.test.tsx +26 -3
- package/src/plugins/ai-chat/__tests__/page-content.test.ts +177 -0
- package/src/plugins/ai-chat/__tests__/page-widget.test.tsx +675 -0
- package/src/plugins/ai-chat/api/operations.ts +29 -13
- package/src/plugins/ai-chat/api/page-tools.ts +56 -2
- package/src/plugins/ai-chat/api/plugin.ts +13 -0
- package/src/plugins/ai-chat/client/analytics.ts +35 -0
- package/src/plugins/ai-chat/client/components/chat-interface.tsx +113 -10
- package/src/plugins/ai-chat/client/components/chat-layout.tsx +304 -68
- package/src/plugins/ai-chat/client/components/tool-call-display.tsx +29 -1
- package/src/plugins/ai-chat/client/context/chat-analytics.tsx +46 -0
- package/src/plugins/ai-chat/client/context/page-ai-context.tsx +9 -1
- package/src/plugins/ai-chat/client/index.ts +2 -0
- package/src/plugins/ai-chat/client/localization/index.ts +12 -0
- package/src/plugins/ai-chat/client/overrides.ts +5 -0
- package/src/plugins/ai-chat/client/plugin.tsx +3 -0
- package/dist/shared/{stack.BuxUQqL8.d.ts → stack.CGmh-Lpe.d.ts} +12 -12
- package/dist/shared/{stack.Egr3RWhA.d.mts → stack.Doy2_wgl.d.mts} +12 -12
- package/dist/shared/{stack.CKc_Bais.d.cts → stack.DtZgEX84.d.cts} +6 -6
- package/dist/shared/{stack.CKc_Bais.d.mts → stack.DtZgEX84.d.mts} +6 -6
- package/dist/shared/{stack.CKc_Bais.d.ts → stack.DtZgEX84.d.ts} +6 -6
- package/dist/shared/{stack.CX83YNuS.d.cts → stack.zch70Loj.d.cts} +12 -12
|
@@ -1,6 +1,39 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
import { UIMessage } from 'ai';
|
|
3
|
-
import { ComponentType, FormEvent } from 'react';
|
|
3
|
+
import { ComponentType, CSSProperties, ReactNode, FormEvent } from 'react';
|
|
4
|
+
import { PageAIContextConfig } from '../plugins/ai-chat/client/context/page-ai-context.cjs';
|
|
5
|
+
|
|
6
|
+
/** Content-free usage events. No prompts, answers, page descriptions, or tool outputs. */
|
|
7
|
+
type AiChatAnalyticsEvent = {
|
|
8
|
+
/** Caller-supplied page identity; omit sensitive values. */
|
|
9
|
+
pageKey?: string;
|
|
10
|
+
/** Page label, without its description or document content. */
|
|
11
|
+
routeName?: string;
|
|
12
|
+
/** Registered client mode, not an assertion about the viewer's identity. */
|
|
13
|
+
mode: AiChatMode;
|
|
14
|
+
} & ({
|
|
15
|
+
type: "widget_opened" | "widget_closed" | "intro_tip_shown" | "conversation_cleared";
|
|
16
|
+
} | {
|
|
17
|
+
type: "intro_tip_dismissed";
|
|
18
|
+
reason: "timeout" | "manual" | "open";
|
|
19
|
+
} | {
|
|
20
|
+
type: "suggestion_selected";
|
|
21
|
+
index: number;
|
|
22
|
+
} | {
|
|
23
|
+
type: "message_submitted";
|
|
24
|
+
inputKind: "typed" | "suggestion" | "edit" | "retry";
|
|
25
|
+
inputLength: number;
|
|
26
|
+
messageCount: number;
|
|
27
|
+
attachmentCount: number;
|
|
28
|
+
} | {
|
|
29
|
+
type: "response_completed";
|
|
30
|
+
durationMs: number;
|
|
31
|
+
outputLength: number;
|
|
32
|
+
toolCallCount: number;
|
|
33
|
+
} | {
|
|
34
|
+
type: "response_failed";
|
|
35
|
+
durationMs: number;
|
|
36
|
+
});
|
|
4
37
|
|
|
5
38
|
/**
|
|
6
39
|
* AI Chat plugin localization strings
|
|
@@ -68,7 +101,13 @@ interface AiChatLocalization {
|
|
|
68
101
|
A11Y_CHAT_TITLE: string;
|
|
69
102
|
A11Y_CLEAR_CHAT: string;
|
|
70
103
|
A11Y_CLOSE_CHAT: string;
|
|
104
|
+
A11Y_EXPAND_CHAT: string;
|
|
105
|
+
A11Y_COLLAPSE_CHAT: string;
|
|
71
106
|
A11Y_OPEN_CHAT: string;
|
|
107
|
+
A11Y_DISMISS_CHAT_TIP: string;
|
|
108
|
+
PAGE_CONTENT_READING: string;
|
|
109
|
+
PAGE_CONTENT_READ: string;
|
|
110
|
+
PAGE_CONTENT_ERROR: string;
|
|
72
111
|
A11Y_OPEN_MENU: string;
|
|
73
112
|
A11Y_CLOSE_SIDEBAR: string;
|
|
74
113
|
A11Y_OPEN_SIDEBAR: string;
|
|
@@ -94,6 +133,8 @@ type AiChatMode = "authenticated" | "public";
|
|
|
94
133
|
interface AiChatProviderConfig {
|
|
95
134
|
/** Conversation behavior selected by `aiChatClientPlugin()`. */
|
|
96
135
|
readonly mode: AiChatMode;
|
|
136
|
+
/** Whether built-in page reader permission checks and status UI are enabled. */
|
|
137
|
+
readonly pageContent?: boolean;
|
|
97
138
|
}
|
|
98
139
|
/**
|
|
99
140
|
* State of a tool call execution
|
|
@@ -152,6 +193,8 @@ interface RouteContext {
|
|
|
152
193
|
* plugin-specific components and behavior.
|
|
153
194
|
*/
|
|
154
195
|
interface AiChatPluginOverrides {
|
|
196
|
+
/** Optional content-free usage telemetry. Exceptions and rejections are isolated from chat. */
|
|
197
|
+
onAnalyticsEvent?: (event: AiChatAnalyticsEvent) => void | Promise<void>;
|
|
155
198
|
/**
|
|
156
199
|
* Function used to upload a file and return its URL.
|
|
157
200
|
* Called for images, PDFs, text files, and other supported file types.
|
|
@@ -255,6 +298,15 @@ interface ChatLayoutBaseProps {
|
|
|
255
298
|
conversationId?: string;
|
|
256
299
|
/** Additional class name for the container */
|
|
257
300
|
className?: string;
|
|
301
|
+
/** Additional container styles, including application-specific floating offsets. */
|
|
302
|
+
style?: CSSProperties;
|
|
303
|
+
/** Instance-local context. Omit to inherit page registrations; null disables them. */
|
|
304
|
+
pageContext?: PageAIContextConfig | null;
|
|
305
|
+
/**
|
|
306
|
+
* Page identity. Changing it resets the panel, tip, and conversation, stopping
|
|
307
|
+
* an active stream. Supply initialMessages/conversationId for this page only.
|
|
308
|
+
*/
|
|
309
|
+
pageKey?: string;
|
|
258
310
|
/** Whether to show the sidebar */
|
|
259
311
|
showSidebar?: boolean;
|
|
260
312
|
/** Initial messages to populate the chat (useful for localStorage persistence in public mode) */
|
|
@@ -267,10 +319,18 @@ interface ChatLayoutBaseProps {
|
|
|
267
319
|
interface ChatLayoutWidgetProps extends ChatLayoutBaseProps {
|
|
268
320
|
/** Widget mode: compact embeddable panel with a floating trigger button */
|
|
269
321
|
layout: "widget";
|
|
270
|
-
/** Height of the widget panel. Default: `"
|
|
322
|
+
/** Height of the widget panel. Default: `"min(640px, calc(100dvh - 112px))"` */
|
|
271
323
|
widgetHeight?: string | number;
|
|
272
|
-
/** Width of the widget panel. Default: `"
|
|
324
|
+
/** Width of the widget panel. Default: `"min(440px, calc(100vw - 32px))"` */
|
|
273
325
|
widgetWidth?: string | number;
|
|
326
|
+
/** Show a button to expand the widget to the viewport. Default: false. Escape restores the compact size. */
|
|
327
|
+
expandable?: boolean;
|
|
328
|
+
/** Fix the widget to the bottom right. Default: false (embedded). Override offsets with className or style. */
|
|
329
|
+
floating?: boolean;
|
|
330
|
+
/** Optional dismissible introduction shown while the widget is closed. */
|
|
331
|
+
introTip?: string;
|
|
332
|
+
/** Milliseconds before hiding the introduction. Default: 6000. */
|
|
333
|
+
introTipDuration?: number;
|
|
274
334
|
/**
|
|
275
335
|
* Whether the widget panel starts open. Default: `false`.
|
|
276
336
|
* Set to `true` when embedding inside an already-open container such as a
|
|
@@ -285,6 +345,8 @@ interface ChatLayoutWidgetProps extends ChatLayoutBaseProps {
|
|
|
285
345
|
* so that the built-in button does not appear alongside your own UI.
|
|
286
346
|
*/
|
|
287
347
|
showTrigger?: boolean;
|
|
348
|
+
/** Icon shown in the closed widget's trigger button. Defaults to Sparkles. */
|
|
349
|
+
triggerIcon?: ReactNode;
|
|
288
350
|
}
|
|
289
351
|
interface ChatLayoutFullProps extends ChatLayoutBaseProps {
|
|
290
352
|
/** Full-page mode with sidebar navigation (default) */
|
|
@@ -355,4 +417,4 @@ declare function ChatInput({ input, handleInputChange, handleSubmit, isLoading,
|
|
|
355
417
|
declare function ToolCallDisplay({ toolCallId, toolName, state, input, output, errorText, isLoading, }: ToolCallProps): react_jsx_runtime.JSX.Element;
|
|
356
418
|
|
|
357
419
|
export { ChatInterface as C, DEFAULT_ALLOWED_FILE_TYPES as D, ChatLayout as f, ChatSidebar as h, ChatMessage as i, ChatInput as j, ToolCallDisplay as k };
|
|
358
|
-
export type { AiChatMode as A, ToolCallProps as T, AiChatPluginOverrides as a, AiChatProviderConfig as b, AllowedFileType as c, ToolCallState as d, ToolCallRenderer as e, ChatLayoutProps as g };
|
|
420
|
+
export type { AiChatMode as A, ToolCallProps as T, AiChatPluginOverrides as a, AiChatProviderConfig as b, AllowedFileType as c, ToolCallState as d, ToolCallRenderer as e, ChatLayoutProps as g, AiChatAnalyticsEvent as l };
|
|
@@ -1,6 +1,39 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
import { UIMessage } from 'ai';
|
|
3
|
-
import { ComponentType, FormEvent } from 'react';
|
|
3
|
+
import { ComponentType, CSSProperties, ReactNode, FormEvent } from 'react';
|
|
4
|
+
import { PageAIContextConfig } from '../plugins/ai-chat/client/context/page-ai-context.js';
|
|
5
|
+
|
|
6
|
+
/** Content-free usage events. No prompts, answers, page descriptions, or tool outputs. */
|
|
7
|
+
type AiChatAnalyticsEvent = {
|
|
8
|
+
/** Caller-supplied page identity; omit sensitive values. */
|
|
9
|
+
pageKey?: string;
|
|
10
|
+
/** Page label, without its description or document content. */
|
|
11
|
+
routeName?: string;
|
|
12
|
+
/** Registered client mode, not an assertion about the viewer's identity. */
|
|
13
|
+
mode: AiChatMode;
|
|
14
|
+
} & ({
|
|
15
|
+
type: "widget_opened" | "widget_closed" | "intro_tip_shown" | "conversation_cleared";
|
|
16
|
+
} | {
|
|
17
|
+
type: "intro_tip_dismissed";
|
|
18
|
+
reason: "timeout" | "manual" | "open";
|
|
19
|
+
} | {
|
|
20
|
+
type: "suggestion_selected";
|
|
21
|
+
index: number;
|
|
22
|
+
} | {
|
|
23
|
+
type: "message_submitted";
|
|
24
|
+
inputKind: "typed" | "suggestion" | "edit" | "retry";
|
|
25
|
+
inputLength: number;
|
|
26
|
+
messageCount: number;
|
|
27
|
+
attachmentCount: number;
|
|
28
|
+
} | {
|
|
29
|
+
type: "response_completed";
|
|
30
|
+
durationMs: number;
|
|
31
|
+
outputLength: number;
|
|
32
|
+
toolCallCount: number;
|
|
33
|
+
} | {
|
|
34
|
+
type: "response_failed";
|
|
35
|
+
durationMs: number;
|
|
36
|
+
});
|
|
4
37
|
|
|
5
38
|
/**
|
|
6
39
|
* AI Chat plugin localization strings
|
|
@@ -68,7 +101,13 @@ interface AiChatLocalization {
|
|
|
68
101
|
A11Y_CHAT_TITLE: string;
|
|
69
102
|
A11Y_CLEAR_CHAT: string;
|
|
70
103
|
A11Y_CLOSE_CHAT: string;
|
|
104
|
+
A11Y_EXPAND_CHAT: string;
|
|
105
|
+
A11Y_COLLAPSE_CHAT: string;
|
|
71
106
|
A11Y_OPEN_CHAT: string;
|
|
107
|
+
A11Y_DISMISS_CHAT_TIP: string;
|
|
108
|
+
PAGE_CONTENT_READING: string;
|
|
109
|
+
PAGE_CONTENT_READ: string;
|
|
110
|
+
PAGE_CONTENT_ERROR: string;
|
|
72
111
|
A11Y_OPEN_MENU: string;
|
|
73
112
|
A11Y_CLOSE_SIDEBAR: string;
|
|
74
113
|
A11Y_OPEN_SIDEBAR: string;
|
|
@@ -94,6 +133,8 @@ type AiChatMode = "authenticated" | "public";
|
|
|
94
133
|
interface AiChatProviderConfig {
|
|
95
134
|
/** Conversation behavior selected by `aiChatClientPlugin()`. */
|
|
96
135
|
readonly mode: AiChatMode;
|
|
136
|
+
/** Whether built-in page reader permission checks and status UI are enabled. */
|
|
137
|
+
readonly pageContent?: boolean;
|
|
97
138
|
}
|
|
98
139
|
/**
|
|
99
140
|
* State of a tool call execution
|
|
@@ -152,6 +193,8 @@ interface RouteContext {
|
|
|
152
193
|
* plugin-specific components and behavior.
|
|
153
194
|
*/
|
|
154
195
|
interface AiChatPluginOverrides {
|
|
196
|
+
/** Optional content-free usage telemetry. Exceptions and rejections are isolated from chat. */
|
|
197
|
+
onAnalyticsEvent?: (event: AiChatAnalyticsEvent) => void | Promise<void>;
|
|
155
198
|
/**
|
|
156
199
|
* Function used to upload a file and return its URL.
|
|
157
200
|
* Called for images, PDFs, text files, and other supported file types.
|
|
@@ -255,6 +298,15 @@ interface ChatLayoutBaseProps {
|
|
|
255
298
|
conversationId?: string;
|
|
256
299
|
/** Additional class name for the container */
|
|
257
300
|
className?: string;
|
|
301
|
+
/** Additional container styles, including application-specific floating offsets. */
|
|
302
|
+
style?: CSSProperties;
|
|
303
|
+
/** Instance-local context. Omit to inherit page registrations; null disables them. */
|
|
304
|
+
pageContext?: PageAIContextConfig | null;
|
|
305
|
+
/**
|
|
306
|
+
* Page identity. Changing it resets the panel, tip, and conversation, stopping
|
|
307
|
+
* an active stream. Supply initialMessages/conversationId for this page only.
|
|
308
|
+
*/
|
|
309
|
+
pageKey?: string;
|
|
258
310
|
/** Whether to show the sidebar */
|
|
259
311
|
showSidebar?: boolean;
|
|
260
312
|
/** Initial messages to populate the chat (useful for localStorage persistence in public mode) */
|
|
@@ -267,10 +319,18 @@ interface ChatLayoutBaseProps {
|
|
|
267
319
|
interface ChatLayoutWidgetProps extends ChatLayoutBaseProps {
|
|
268
320
|
/** Widget mode: compact embeddable panel with a floating trigger button */
|
|
269
321
|
layout: "widget";
|
|
270
|
-
/** Height of the widget panel. Default: `"
|
|
322
|
+
/** Height of the widget panel. Default: `"min(640px, calc(100dvh - 112px))"` */
|
|
271
323
|
widgetHeight?: string | number;
|
|
272
|
-
/** Width of the widget panel. Default: `"
|
|
324
|
+
/** Width of the widget panel. Default: `"min(440px, calc(100vw - 32px))"` */
|
|
273
325
|
widgetWidth?: string | number;
|
|
326
|
+
/** Show a button to expand the widget to the viewport. Default: false. Escape restores the compact size. */
|
|
327
|
+
expandable?: boolean;
|
|
328
|
+
/** Fix the widget to the bottom right. Default: false (embedded). Override offsets with className or style. */
|
|
329
|
+
floating?: boolean;
|
|
330
|
+
/** Optional dismissible introduction shown while the widget is closed. */
|
|
331
|
+
introTip?: string;
|
|
332
|
+
/** Milliseconds before hiding the introduction. Default: 6000. */
|
|
333
|
+
introTipDuration?: number;
|
|
274
334
|
/**
|
|
275
335
|
* Whether the widget panel starts open. Default: `false`.
|
|
276
336
|
* Set to `true` when embedding inside an already-open container such as a
|
|
@@ -285,6 +345,8 @@ interface ChatLayoutWidgetProps extends ChatLayoutBaseProps {
|
|
|
285
345
|
* so that the built-in button does not appear alongside your own UI.
|
|
286
346
|
*/
|
|
287
347
|
showTrigger?: boolean;
|
|
348
|
+
/** Icon shown in the closed widget's trigger button. Defaults to Sparkles. */
|
|
349
|
+
triggerIcon?: ReactNode;
|
|
288
350
|
}
|
|
289
351
|
interface ChatLayoutFullProps extends ChatLayoutBaseProps {
|
|
290
352
|
/** Full-page mode with sidebar navigation (default) */
|
|
@@ -355,4 +417,4 @@ declare function ChatInput({ input, handleInputChange, handleSubmit, isLoading,
|
|
|
355
417
|
declare function ToolCallDisplay({ toolCallId, toolName, state, input, output, errorText, isLoading, }: ToolCallProps): react_jsx_runtime.JSX.Element;
|
|
356
418
|
|
|
357
419
|
export { ChatInterface as C, DEFAULT_ALLOWED_FILE_TYPES as D, ChatLayout as f, ChatSidebar as h, ChatMessage as i, ChatInput as j, ToolCallDisplay as k };
|
|
358
|
-
export type { AiChatMode as A, ToolCallProps as T, AiChatPluginOverrides as a, AiChatProviderConfig as b, AllowedFileType as c, ToolCallState as d, ToolCallRenderer as e, ChatLayoutProps as g };
|
|
420
|
+
export type { AiChatMode as A, ToolCallProps as T, AiChatPluginOverrides as a, AiChatProviderConfig as b, AllowedFileType as c, ToolCallState as d, ToolCallRenderer as e, ChatLayoutProps as g, AiChatAnalyticsEvent as l };
|
|
@@ -2,7 +2,7 @@ import * as _tanstack_react_query from '@tanstack/react-query';
|
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { ResourceFormConfig, ResourceFormResult } from '@btst/stack/plugins/client/hooks';
|
|
4
4
|
import { S as SerializedPost, a as SerializedTag } from './stack.Dsatjh0B.cjs';
|
|
5
|
-
import { c as createPostSchema, u as updatePostSchema } from './stack.
|
|
5
|
+
import { c as createPostSchema, u as updatePostSchema } from './stack.DtZgEX84.cjs';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Options for the usePosts hook
|
|
@@ -136,10 +136,8 @@ declare function useSuspenseTags(): {
|
|
|
136
136
|
declare function usePostForm<TValues>(config: ResourceFormConfig<TValues, SerializedPost | null, SerializedPost | null>): ResourceFormResult<TValues, SerializedPost | null, SerializedPost | null>;
|
|
137
137
|
/** Create a new post */
|
|
138
138
|
declare function useCreatePost(): _tanstack_react_query.UseMutationResult<SerializedPost | null, Error, {
|
|
139
|
-
title: string;
|
|
140
|
-
content: string;
|
|
141
|
-
excerpt: string;
|
|
142
139
|
published: boolean;
|
|
140
|
+
title: string;
|
|
143
141
|
tags: ({
|
|
144
142
|
name: string;
|
|
145
143
|
} | {
|
|
@@ -147,10 +145,12 @@ declare function useCreatePost(): _tanstack_react_query.UseMutationResult<Serial
|
|
|
147
145
|
name: string;
|
|
148
146
|
slug: string;
|
|
149
147
|
})[];
|
|
148
|
+
content: string;
|
|
149
|
+
excerpt: string;
|
|
150
150
|
slug?: string | undefined;
|
|
151
|
-
publishedAt?: Date | undefined;
|
|
152
151
|
createdAt?: Date | undefined;
|
|
153
152
|
updatedAt?: Date | undefined;
|
|
153
|
+
publishedAt?: Date | undefined;
|
|
154
154
|
image?: string | undefined;
|
|
155
155
|
}>;
|
|
156
156
|
/** Update an existing post by id */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@btst/stack",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.3.0",
|
|
4
4
|
"description": "A composable, plugin-based library for building full-stack applications.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -1142,7 +1142,7 @@
|
|
|
1142
1142
|
"@better-fetch/fetch": "1.3.2",
|
|
1143
1143
|
"@btst/adapter-memory": "3.0.0",
|
|
1144
1144
|
"@btst/yar": "1.3.2",
|
|
1145
|
-
"@tanstack/react-router": "1.
|
|
1145
|
+
"@tanstack/react-router": "1.170.41",
|
|
1146
1146
|
"@types/react": "^19.0.0",
|
|
1147
1147
|
"@types/react-dom": "^19.0.0",
|
|
1148
1148
|
"@types/slug": "^5.0.9",
|
|
@@ -108,6 +108,7 @@ function backend(options?: {
|
|
|
108
108
|
adapter?: (db: DatabaseDefinition) => DBAdapter;
|
|
109
109
|
hooks?: Parameters<typeof aiChatBackendPlugin>[0]["hooks"];
|
|
110
110
|
enablePageTools?: boolean;
|
|
111
|
+
pageContent?: Parameters<typeof aiChatBackendPlugin>[0]["pageContent"];
|
|
111
112
|
tools?: Parameters<typeof aiChatBackendPlugin>[0]["tools"];
|
|
112
113
|
getIdentity?: (
|
|
113
114
|
request: Request,
|
|
@@ -122,6 +123,7 @@ function backend(options?: {
|
|
|
122
123
|
enablePageTools: options?.enablePageTools,
|
|
123
124
|
hooks: options?.hooks,
|
|
124
125
|
tools: options?.tools,
|
|
126
|
+
pageContent: options?.pageContent,
|
|
125
127
|
}),
|
|
126
128
|
},
|
|
127
129
|
adapter: options?.adapter ?? memory,
|
|
@@ -587,6 +589,7 @@ describe("AI Chat operation authorization", () => {
|
|
|
587
589
|
const activated = vi.fn(() => [] as const);
|
|
588
590
|
const app = backend({
|
|
589
591
|
enablePageTools: true,
|
|
592
|
+
pageContent: { pathSchema: z.string(), resolve: () => "reference" },
|
|
590
593
|
hooks: { onBeforeActivateTools: activated },
|
|
591
594
|
});
|
|
592
595
|
const conversation = await seedConversation(app);
|
|
@@ -622,7 +625,7 @@ describe("AI Chat operation authorization", () => {
|
|
|
622
625
|
}
|
|
623
626
|
|
|
624
627
|
expect(activated).toHaveBeenCalledWith(
|
|
625
|
-
["fillBlogForm"],
|
|
628
|
+
["fillBlogForm", "readPage"],
|
|
626
629
|
"newPost",
|
|
627
630
|
expect.any(Object),
|
|
628
631
|
);
|
|
@@ -2049,3 +2052,105 @@ describe("AI Chat operation authorization", () => {
|
|
|
2049
2052
|
expect(before).toHaveBeenCalledOnce();
|
|
2050
2053
|
});
|
|
2051
2054
|
});
|
|
2055
|
+
|
|
2056
|
+
describe("server page content", () => {
|
|
2057
|
+
beforeEach(() => streamText.mockClear());
|
|
2058
|
+
const pageContent = (
|
|
2059
|
+
resolve = vi.fn().mockResolvedValue({ content: "reference" }),
|
|
2060
|
+
) => ({
|
|
2061
|
+
pathSchema: z.string().regex(/^\/articles\/[^/?#]+$/),
|
|
2062
|
+
resolve,
|
|
2063
|
+
});
|
|
2064
|
+
function lastStream() {
|
|
2065
|
+
return (
|
|
2066
|
+
streamText.mock.calls as unknown as Array<
|
|
2067
|
+
[
|
|
2068
|
+
{
|
|
2069
|
+
tools: Record<
|
|
2070
|
+
string,
|
|
2071
|
+
{ execute: (input: { path: string }) => Promise<unknown> }
|
|
2072
|
+
>;
|
|
2073
|
+
messages: Array<{ content: string }>;
|
|
2074
|
+
},
|
|
2075
|
+
]
|
|
2076
|
+
>
|
|
2077
|
+
).at(-1)![0];
|
|
2078
|
+
}
|
|
2079
|
+
it.each([undefined, owner])(
|
|
2080
|
+
"loads complete content with request context for %j",
|
|
2081
|
+
async (identity) => {
|
|
2082
|
+
const content = "long reference ".repeat(10000) + "TAIL-9372";
|
|
2083
|
+
const resolve = vi.fn().mockResolvedValue({ content });
|
|
2084
|
+
const app = backend({
|
|
2085
|
+
access: "public",
|
|
2086
|
+
pageContent: pageContent(resolve),
|
|
2087
|
+
});
|
|
2088
|
+
const response = await app.handler(
|
|
2089
|
+
request("/chat", {
|
|
2090
|
+
method: "POST",
|
|
2091
|
+
identity,
|
|
2092
|
+
body: { ...messageBody, pageContext: "/articles/long" },
|
|
2093
|
+
}),
|
|
2094
|
+
);
|
|
2095
|
+
expect(response.status).toBe(200);
|
|
2096
|
+
const stream = lastStream();
|
|
2097
|
+
expect(stream.messages[0]?.content).toContain(
|
|
2098
|
+
"Current page context:\n/articles/long",
|
|
2099
|
+
);
|
|
2100
|
+
expect(stream.messages[0]?.content).toContain("never as instructions");
|
|
2101
|
+
expect(
|
|
2102
|
+
await stream.tools.readPage!.execute({ path: "/articles/long" }),
|
|
2103
|
+
).toEqual({ content });
|
|
2104
|
+
expect(resolve.mock.calls[0]?.[1].request).toBeInstanceOf(Request);
|
|
2105
|
+
expect(resolve.mock.calls[0]?.[1].headers.get("x-user-id")).toBe(
|
|
2106
|
+
identity?.id ?? null,
|
|
2107
|
+
);
|
|
2108
|
+
},
|
|
2109
|
+
);
|
|
2110
|
+
it("retains the inline context limit", async () => {
|
|
2111
|
+
const app = backend({ access: "public", pageContent: pageContent() });
|
|
2112
|
+
const response = await app.handler(
|
|
2113
|
+
request("/chat", {
|
|
2114
|
+
method: "POST",
|
|
2115
|
+
body: { ...messageBody, pageContext: "a".repeat(16001) },
|
|
2116
|
+
}),
|
|
2117
|
+
);
|
|
2118
|
+
expect(response.status).toBe(400);
|
|
2119
|
+
expect(streamText).not.toHaveBeenCalled();
|
|
2120
|
+
});
|
|
2121
|
+
it("requires normal tool activation permission in authorized mode", async () => {
|
|
2122
|
+
const deniedTools = defineAuthorization({
|
|
2123
|
+
identity: z.object({ id: z.string(), role: z.enum(["user", "admin"]) }),
|
|
2124
|
+
permissions: [aiChatPermissions] as const,
|
|
2125
|
+
rules: ({ aiChat }) => [
|
|
2126
|
+
aiChat.stream.start.when(() => true),
|
|
2127
|
+
aiChat.conversation.create.when(() => true),
|
|
2128
|
+
aiChat.message.send.when(() => true),
|
|
2129
|
+
aiChat.tool.activate.when(() => false),
|
|
2130
|
+
],
|
|
2131
|
+
});
|
|
2132
|
+
const resolve = vi.fn();
|
|
2133
|
+
const app = backend({
|
|
2134
|
+
authorization: deniedTools,
|
|
2135
|
+
pageContent: pageContent(resolve),
|
|
2136
|
+
});
|
|
2137
|
+
const response = await app.handler(
|
|
2138
|
+
request("/chat", { method: "POST", identity: owner, body: messageBody }),
|
|
2139
|
+
);
|
|
2140
|
+
expect(response.status).toBe(403);
|
|
2141
|
+
expect(resolve).not.toHaveBeenCalled();
|
|
2142
|
+
expect(streamText).not.toHaveBeenCalled();
|
|
2143
|
+
});
|
|
2144
|
+
it.each(["tools", "clientToolSchemas"] as const)(
|
|
2145
|
+
"rejects a readPage name collision in %s",
|
|
2146
|
+
(key) => {
|
|
2147
|
+
expect(() =>
|
|
2148
|
+
aiChatBackendPlugin({
|
|
2149
|
+
model,
|
|
2150
|
+
pageContent: pageContent(),
|
|
2151
|
+
[key]: { readPage: {} },
|
|
2152
|
+
}),
|
|
2153
|
+
).toThrow("reserves readPage");
|
|
2154
|
+
},
|
|
2155
|
+
);
|
|
2156
|
+
});
|
|
@@ -40,7 +40,10 @@ vi.mock("../client/hooks/chat-hooks", () => ({
|
|
|
40
40
|
useRenameConversationForm: mocks.useRenameConversationForm,
|
|
41
41
|
useDeleteConversation: mocks.useDeleteConversation,
|
|
42
42
|
}));
|
|
43
|
-
vi.mock("../client/context/page-ai-context", () => ({
|
|
43
|
+
vi.mock("../client/context/page-ai-context", async (importOriginal) => ({
|
|
44
|
+
...(await importOriginal<
|
|
45
|
+
typeof import("../client/context/page-ai-context")
|
|
46
|
+
>()),
|
|
44
47
|
usePageAIContext: () => undefined,
|
|
45
48
|
}));
|
|
46
49
|
// Markdown rendering is unrelated to class wiring and drags in highlight/katex.
|
|
@@ -216,13 +219,33 @@ describe("ai-chat classNames overrides (issue #36)", () => {
|
|
|
216
219
|
expect(list.className).toContain("flex-col");
|
|
217
220
|
});
|
|
218
221
|
|
|
219
|
-
it("
|
|
220
|
-
await render(
|
|
222
|
+
it("preserves container customizations when the widget expands", async () => {
|
|
223
|
+
await render(
|
|
224
|
+
<ChatLayout
|
|
225
|
+
layout="widget"
|
|
226
|
+
defaultOpen
|
|
227
|
+
expandable
|
|
228
|
+
className="chat-theme right-10 bottom-20 z-40"
|
|
229
|
+
style={{ fontFamily: "monospace", right: 24, zIndex: 40 }}
|
|
230
|
+
/>,
|
|
231
|
+
);
|
|
221
232
|
|
|
222
233
|
const trigger = query('[data-testid="widget-trigger"]');
|
|
223
234
|
const widgetRoot = trigger.parentElement!;
|
|
224
235
|
expect(widgetRoot.className).toContain(CLASS_NAMES.container);
|
|
225
236
|
expect(widgetRoot.className).toContain("items-end");
|
|
237
|
+
await act(async () => query('[aria-label="Expand chat"]').click());
|
|
238
|
+
const expanded = document.querySelector<HTMLElement>(
|
|
239
|
+
'[data-slot="dialog-content"]',
|
|
240
|
+
)!;
|
|
241
|
+
expect(expanded.className).toContain("chat-theme");
|
|
242
|
+
expect(expanded.className).toContain(CLASS_NAMES.container);
|
|
243
|
+
expect(expanded.className).not.toContain("right-10");
|
|
244
|
+
expect(expanded.className).not.toContain("bottom-20");
|
|
245
|
+
expect(expanded.className).not.toContain("z-40");
|
|
246
|
+
expect(expanded.style.fontFamily).toBe("monospace");
|
|
247
|
+
expect(expanded.style.right).toBe("0px");
|
|
248
|
+
expect(expanded.style.zIndex).toBe("50");
|
|
226
249
|
});
|
|
227
250
|
|
|
228
251
|
it("leaves base classes untouched when no classNames override is supplied", async () => {
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { createMemoryAdapter } from "@btst/adapter-memory";
|
|
2
|
+
import { MockLanguageModelV2 } from "ai/test";
|
|
3
|
+
import { simulateReadableStream } from "ai";
|
|
4
|
+
import { describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { createBackendStack } from "../../../api";
|
|
7
|
+
import { aiChatBackendPlugin } from "../api";
|
|
8
|
+
|
|
9
|
+
async function readPage(
|
|
10
|
+
path: string,
|
|
11
|
+
pathSchema: z.ZodType<string, string> = z
|
|
12
|
+
.string()
|
|
13
|
+
.regex(/^\/articles\/[^/?#]+$/),
|
|
14
|
+
content: unknown = { content: "reference" },
|
|
15
|
+
hooks?: Parameters<typeof aiChatBackendPlugin>[0]["hooks"],
|
|
16
|
+
) {
|
|
17
|
+
const resolve = vi.fn().mockResolvedValue(content);
|
|
18
|
+
let step = 0;
|
|
19
|
+
const model = new MockLanguageModelV2({
|
|
20
|
+
doStream: async () => ({
|
|
21
|
+
stream: simulateReadableStream<
|
|
22
|
+
Awaited<
|
|
23
|
+
ReturnType<MockLanguageModelV2["doStream"]>
|
|
24
|
+
>["stream"] extends ReadableStream<infer T>
|
|
25
|
+
? T
|
|
26
|
+
: never
|
|
27
|
+
>({
|
|
28
|
+
chunks:
|
|
29
|
+
step++ === 0
|
|
30
|
+
? [
|
|
31
|
+
{
|
|
32
|
+
type: "tool-call",
|
|
33
|
+
toolCallId: "read-1",
|
|
34
|
+
toolName: "readPage",
|
|
35
|
+
input: JSON.stringify({ path }),
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
type: "finish",
|
|
39
|
+
finishReason: "tool-calls",
|
|
40
|
+
usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
|
|
41
|
+
},
|
|
42
|
+
]
|
|
43
|
+
: [
|
|
44
|
+
{ type: "text-start", id: "answer" },
|
|
45
|
+
{
|
|
46
|
+
type: "text-delta",
|
|
47
|
+
id: "answer",
|
|
48
|
+
delta: "Page read completed.",
|
|
49
|
+
},
|
|
50
|
+
{ type: "text-end", id: "answer" },
|
|
51
|
+
{
|
|
52
|
+
type: "finish",
|
|
53
|
+
finishReason: "stop",
|
|
54
|
+
usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
initialDelayInMs: null,
|
|
58
|
+
chunkDelayInMs: null,
|
|
59
|
+
}),
|
|
60
|
+
}),
|
|
61
|
+
});
|
|
62
|
+
const app = createBackendStack({
|
|
63
|
+
basePath: "/api",
|
|
64
|
+
plugins: {
|
|
65
|
+
aiChat: aiChatBackendPlugin({
|
|
66
|
+
model,
|
|
67
|
+
access: "public",
|
|
68
|
+
pageContent: { pathSchema, resolve },
|
|
69
|
+
hooks,
|
|
70
|
+
}),
|
|
71
|
+
},
|
|
72
|
+
adapter: (db) => createMemoryAdapter(db)({}),
|
|
73
|
+
});
|
|
74
|
+
const response = await app.handler(
|
|
75
|
+
new Request("http://localhost/api/chat", {
|
|
76
|
+
method: "POST",
|
|
77
|
+
headers: { "content-type": "application/json" },
|
|
78
|
+
body: JSON.stringify({
|
|
79
|
+
messages: [
|
|
80
|
+
{
|
|
81
|
+
id: "q",
|
|
82
|
+
role: "user",
|
|
83
|
+
parts: [{ type: "text", text: "Read the page" }],
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
pageContext: "/articles/one",
|
|
87
|
+
}),
|
|
88
|
+
}),
|
|
89
|
+
);
|
|
90
|
+
expect(response.status).toBe(200);
|
|
91
|
+
const stream = await response.text();
|
|
92
|
+
return { resolve, model, stream };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
describe("page reader AI SDK execution", () => {
|
|
96
|
+
it.each([false, true])(
|
|
97
|
+
"respects the page-tool safety filter in public mode: %s",
|
|
98
|
+
async (allow) => {
|
|
99
|
+
const filter = vi.fn((names: readonly string[]) => (allow ? names : []));
|
|
100
|
+
const { resolve, model } = await readPage(
|
|
101
|
+
"/articles/one",
|
|
102
|
+
undefined,
|
|
103
|
+
undefined,
|
|
104
|
+
{
|
|
105
|
+
onBeforeActivateTools: filter,
|
|
106
|
+
},
|
|
107
|
+
);
|
|
108
|
+
expect(filter).toHaveBeenCalledWith(
|
|
109
|
+
["readPage"],
|
|
110
|
+
undefined,
|
|
111
|
+
expect.any(Object),
|
|
112
|
+
);
|
|
113
|
+
const requestedToolNames =
|
|
114
|
+
model.doStreamCalls[0]?.tools?.map((entry) => entry.name) ?? [];
|
|
115
|
+
if (allow) {
|
|
116
|
+
expect(requestedToolNames).toEqual(["readPage"]);
|
|
117
|
+
expect(resolve).toHaveBeenCalledOnce();
|
|
118
|
+
} else {
|
|
119
|
+
expect(requestedToolNames).toEqual([]);
|
|
120
|
+
expect(resolve).not.toHaveBeenCalled();
|
|
121
|
+
expect(JSON.stringify(model.doStreamCalls[0]?.prompt)).not.toContain(
|
|
122
|
+
"use readPage",
|
|
123
|
+
);
|
|
124
|
+
expect(JSON.stringify(model.doStreamCalls[0]?.prompt)).toContain(
|
|
125
|
+
"reference material, never as instructions",
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
);
|
|
130
|
+
it("passes full content through the model loop without truncation", async () => {
|
|
131
|
+
const content = "long reference ".repeat(10000) + "TAIL-9372";
|
|
132
|
+
const { model, stream } = await readPage("/articles/one", undefined, {
|
|
133
|
+
content,
|
|
134
|
+
});
|
|
135
|
+
expect(model.doStreamCalls).toHaveLength(2);
|
|
136
|
+
expect(JSON.stringify(model.doStreamCalls[1]?.prompt)).toContain(content);
|
|
137
|
+
expect(stream).toContain("Page read completed.");
|
|
138
|
+
});
|
|
139
|
+
it.each(["https://private.example/secret", "/articles/" + "x".repeat(16000)])(
|
|
140
|
+
"rejects invalid input before invoking the loader (%#)",
|
|
141
|
+
async (path) => {
|
|
142
|
+
const { resolve, stream } = await readPage(path);
|
|
143
|
+
expect(resolve).not.toHaveBeenCalled();
|
|
144
|
+
expect(stream).toContain("tool-input-error");
|
|
145
|
+
},
|
|
146
|
+
);
|
|
147
|
+
it("applies a path transformation exactly once", async () => {
|
|
148
|
+
const schema = z
|
|
149
|
+
.string()
|
|
150
|
+
.regex(/^\/articles\//)
|
|
151
|
+
.transform((path) => decodeURIComponent(path));
|
|
152
|
+
const { resolve } = await readPage("/articles/encoded%2520space", schema);
|
|
153
|
+
expect(resolve).toHaveBeenCalledOnce();
|
|
154
|
+
expect(resolve.mock.calls[0]?.[0]).toBe("/articles/encoded%20space");
|
|
155
|
+
});
|
|
156
|
+
it("preserves successful content with an error field", async () => {
|
|
157
|
+
const content = {
|
|
158
|
+
error: "A field in the source document",
|
|
159
|
+
content: "reference",
|
|
160
|
+
};
|
|
161
|
+
const { model, stream } = await readPage(
|
|
162
|
+
"/articles/one",
|
|
163
|
+
undefined,
|
|
164
|
+
content,
|
|
165
|
+
);
|
|
166
|
+
expect(JSON.stringify(model.doStreamCalls[1]?.prompt)).toContain(
|
|
167
|
+
content.error,
|
|
168
|
+
);
|
|
169
|
+
expect(stream).toContain('"type":"tool-output-available"');
|
|
170
|
+
expect(stream).not.toContain('"type":"tool-output-error"');
|
|
171
|
+
});
|
|
172
|
+
it("returns an unavailable result without inventing page content", async () => {
|
|
173
|
+
const { stream } = await readPage("/articles/draft", undefined, null);
|
|
174
|
+
expect(stream).toContain('"type":"tool-output-error"');
|
|
175
|
+
expect(stream).toContain('"errorText":"Page not found."');
|
|
176
|
+
});
|
|
177
|
+
});
|