@btst/stack 2.3.0 → 2.4.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/dist/packages/stack/src/client/components/compose.cjs +1 -2
- package/dist/packages/stack/src/client/components/compose.mjs +1 -2
- package/dist/packages/stack/src/plugins/ai-chat/api/page-tools.cjs +71 -0
- package/dist/packages/stack/src/plugins/ai-chat/api/page-tools.mjs +68 -0
- package/dist/packages/stack/src/plugins/ai-chat/api/plugin.cjs +54 -7
- package/dist/packages/stack/src/plugins/ai-chat/api/plugin.mjs +54 -7
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-input.cjs +2 -2
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-input.mjs +2 -2
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-interface.cjs +89 -22
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-interface.mjs +90 -23
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-layout.cjs +110 -33
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-layout.mjs +112 -35
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-sidebar.cjs +1 -1
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-sidebar.mjs +1 -1
- package/dist/packages/stack/src/plugins/ai-chat/schemas.cjs +17 -1
- package/dist/packages/stack/src/plugins/ai-chat/schemas.mjs +17 -1
- package/dist/packages/stack/src/plugins/blog/client/components/forms/post-forms.cjs +15 -2
- package/dist/packages/stack/src/plugins/blog/client/components/forms/post-forms.mjs +16 -3
- package/dist/packages/stack/src/plugins/blog/client/components/pages/edit-post-page.internal.cjs +24 -1
- package/dist/packages/stack/src/plugins/blog/client/components/pages/edit-post-page.internal.mjs +24 -1
- package/dist/packages/stack/src/plugins/blog/client/components/pages/fill-blog-form-handler.cjs +26 -0
- package/dist/packages/stack/src/plugins/blog/client/components/pages/fill-blog-form-handler.mjs +24 -0
- package/dist/packages/stack/src/plugins/blog/client/components/pages/new-post-page.internal.cjs +30 -1
- package/dist/packages/stack/src/plugins/blog/client/components/pages/new-post-page.internal.mjs +30 -1
- package/dist/packages/stack/src/plugins/blog/client/components/pages/post-page.internal.cjs +18 -0
- package/dist/packages/stack/src/plugins/blog/client/components/pages/post-page.internal.mjs +18 -0
- package/dist/packages/stack/src/plugins/cms/api/mutations.cjs +48 -0
- package/dist/packages/stack/src/plugins/cms/api/mutations.mjs +46 -0
- package/dist/packages/stack/src/plugins/cms/api/plugin.cjs +7 -1
- package/dist/packages/stack/src/plugins/cms/api/plugin.mjs +7 -1
- package/dist/packages/stack/src/plugins/kanban/api/mutations.cjs +91 -0
- package/dist/packages/stack/src/plugins/kanban/api/mutations.mjs +87 -0
- package/dist/packages/stack/src/plugins/kanban/api/plugin.cjs +6 -1
- package/dist/packages/stack/src/plugins/kanban/api/plugin.mjs +6 -1
- package/dist/packages/stack/src/plugins/kanban/client/hooks/kanban-hooks.cjs +7 -3
- package/dist/packages/stack/src/plugins/kanban/client/hooks/kanban-hooks.mjs +7 -3
- package/dist/packages/stack/src/plugins/ui-builder/client/components/pages/page-builder-page.internal.cjs +89 -0
- package/dist/packages/stack/src/plugins/ui-builder/client/components/pages/page-builder-page.internal.mjs +89 -0
- package/dist/plugins/ai-chat/api/index.d.cts +1 -1
- package/dist/plugins/ai-chat/api/index.d.mts +1 -1
- package/dist/plugins/ai-chat/api/index.d.ts +1 -1
- package/dist/plugins/ai-chat/client/components/index.d.cts +1 -1
- package/dist/plugins/ai-chat/client/components/index.d.mts +1 -1
- package/dist/plugins/ai-chat/client/components/index.d.ts +1 -1
- package/dist/plugins/ai-chat/client/context/page-ai-context.cjs +92 -0
- package/dist/plugins/ai-chat/client/context/page-ai-context.d.cts +84 -0
- package/dist/plugins/ai-chat/client/context/page-ai-context.d.mts +84 -0
- package/dist/plugins/ai-chat/client/context/page-ai-context.d.ts +84 -0
- package/dist/plugins/ai-chat/client/context/page-ai-context.mjs +88 -0
- package/dist/plugins/ai-chat/client/hooks/index.d.cts +1 -1
- package/dist/plugins/ai-chat/client/hooks/index.d.mts +1 -1
- package/dist/plugins/ai-chat/client/hooks/index.d.ts +1 -1
- package/dist/plugins/ai-chat/client/index.d.cts +2 -2
- package/dist/plugins/ai-chat/client/index.d.mts +2 -2
- package/dist/plugins/ai-chat/client/index.d.ts +2 -2
- 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 +1 -1
- package/dist/plugins/blog/client/index.d.mts +1 -1
- package/dist/plugins/blog/client/index.d.ts +1 -1
- 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/cms/api/index.cjs +2 -0
- package/dist/plugins/cms/api/index.d.cts +1 -1
- package/dist/plugins/cms/api/index.d.mts +1 -1
- package/dist/plugins/cms/api/index.d.ts +1 -1
- package/dist/plugins/cms/api/index.mjs +1 -0
- package/dist/plugins/cms/query-keys.d.cts +1 -1
- package/dist/plugins/cms/query-keys.d.mts +1 -1
- package/dist/plugins/cms/query-keys.d.ts +1 -1
- 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/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/plugins/kanban/api/index.cjs +4 -0
- package/dist/plugins/kanban/api/index.d.cts +1 -1
- package/dist/plugins/kanban/api/index.d.mts +1 -1
- package/dist/plugins/kanban/api/index.d.ts +1 -1
- package/dist/plugins/kanban/api/index.mjs +1 -0
- package/dist/plugins/kanban/query-keys.d.cts +1 -1
- package/dist/plugins/kanban/query-keys.d.mts +1 -1
- package/dist/plugins/kanban/query-keys.d.ts +1 -1
- package/dist/shared/{stack.BeSm90va.d.ts → stack.BEn34wW6.d.ts} +60 -2
- package/dist/shared/{stack.IdtKDRka.d.cts → stack.BUkC2EsZ.d.cts} +32 -2
- package/dist/shared/{stack.DaOcgmrM.d.ts → stack.BV9hnvu4.d.cts} +31 -7
- package/dist/shared/{stack.DaOcgmrM.d.cts → stack.BV9hnvu4.d.mts} +31 -7
- package/dist/shared/{stack.DaOcgmrM.d.mts → stack.BV9hnvu4.d.ts} +31 -7
- package/dist/shared/{stack.rTy7-wQU.d.mts → stack.BepFXT3w.d.mts} +70 -15
- package/dist/shared/{stack.BKfolAyK.d.ts → stack.CL8ts1Mu.d.ts} +3 -3
- package/dist/shared/{stack.CP68pFEH.d.mts → stack.CczspVn2.d.mts} +32 -2
- package/dist/shared/{stack.TIBF2AOx.d.ts → stack.CgWzG5jH.d.ts} +70 -15
- package/dist/shared/{stack.BpolpQpf.d.cts → stack.D3GB6wKv.d.cts} +70 -15
- package/dist/shared/{stack.B1EeBt1b.d.ts → stack.DASmUVjX.d.ts} +32 -2
- package/dist/shared/{stack.Dg09R0oB.d.mts → stack.DTDxgFj8.d.mts} +60 -2
- package/dist/shared/{stack.CMh_EdxW.d.cts → stack.DWoCZff7.d.cts} +60 -2
- package/dist/shared/{stack.snB1EDP7.d.cts → stack.Dk5r4W1F.d.mts} +3 -3
- package/dist/shared/{stack.BIXEI6v_.d.mts → stack.heOA9gzA.d.cts} +3 -3
- package/package.json +14 -1
- package/src/client/components/compose.tsx +7 -4
- package/src/plugins/ai-chat/api/page-tools.ts +111 -0
- package/src/plugins/ai-chat/api/plugin.ts +180 -9
- package/src/plugins/ai-chat/client/components/chat-input.tsx +2 -2
- package/src/plugins/ai-chat/client/components/chat-interface.tsx +154 -58
- package/src/plugins/ai-chat/client/components/chat-layout.tsx +166 -32
- package/src/plugins/ai-chat/client/components/chat-sidebar.tsx +1 -1
- package/src/plugins/ai-chat/client/context/page-ai-context.tsx +240 -0
- package/src/plugins/ai-chat/schemas.ts +16 -0
- package/src/plugins/blog/client/components/forms/post-forms.tsx +29 -2
- package/src/plugins/blog/client/components/pages/edit-post-page.internal.tsx +28 -0
- package/src/plugins/blog/client/components/pages/fill-blog-form-handler.ts +38 -0
- package/src/plugins/blog/client/components/pages/new-post-page.internal.tsx +33 -1
- package/src/plugins/blog/client/components/pages/post-page.internal.tsx +20 -0
- package/src/plugins/cms/api/index.ts +4 -0
- package/src/plugins/cms/api/mutations.ts +84 -0
- package/src/plugins/cms/api/plugin.ts +9 -0
- package/src/plugins/kanban/api/index.ts +6 -0
- package/src/plugins/kanban/api/mutations.ts +169 -0
- package/src/plugins/kanban/api/plugin.ts +12 -0
- package/src/plugins/kanban/client/hooks/kanban-hooks.tsx +4 -0
- package/src/plugins/ui-builder/client/components/pages/page-builder-page.internal.tsx +132 -0
- package/dist/shared/{stack.C5dtIncc.d.mts → stack.B7ONvlD_.d.mts} +1 -1
- package/dist/shared/{stack.CBON0dWL.d.cts → stack.BQmuNl5p.d.cts} +2 -2
- package/dist/shared/{stack.CBON0dWL.d.mts → stack.BQmuNl5p.d.mts} +2 -2
- package/dist/shared/{stack.CBON0dWL.d.ts → stack.BQmuNl5p.d.ts} +2 -2
- package/dist/shared/{stack.CIP6QS9l.d.ts → stack.Kq2-QzOC.d.ts} +1 -1
- package/dist/shared/{stack.Dw0Ly2TM.d.cts → stack.kcdnD4gA.d.cts} +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as _tanstack_react_query from '@tanstack/react-query';
|
|
2
2
|
import { QueryClient } from '@tanstack/react-query';
|
|
3
3
|
import { createApiClient } from '@btst/stack/plugins/client';
|
|
4
|
-
import { P as Post, T as Tag, c as createPostSchema, u as updatePostSchema, S as SerializedPost, a as SerializedTag } from './stack.
|
|
4
|
+
import { P as Post, T as Tag, c as createPostSchema, u as updatePostSchema, S as SerializedPost, a as SerializedTag } from './stack.BQmuNl5p.cjs';
|
|
5
5
|
import * as _btst_stack_plugins_api from '@btst/stack/plugins/api';
|
|
6
6
|
import * as better_call from 'better-call';
|
|
7
7
|
import { Adapter } from '@btst/db';
|
|
@@ -218,11 +218,11 @@ declare const blogBackendPlugin: (hooks?: BlogBackendHooks) => _btst_stack_plugi
|
|
|
218
218
|
name: z.ZodString;
|
|
219
219
|
slug: z.ZodString;
|
|
220
220
|
}, z.core.$strip>]>>>>;
|
|
221
|
+
title: z.ZodString;
|
|
221
222
|
slug: z.ZodOptional<z.ZodString>;
|
|
222
|
-
publishedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
223
223
|
createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
224
224
|
updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
225
|
-
|
|
225
|
+
publishedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
226
226
|
content: z.ZodString;
|
|
227
227
|
excerpt: z.ZodString;
|
|
228
228
|
image: z.ZodOptional<z.ZodString>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@btst/stack",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "A composable, plugin-based library for building full-stack applications.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -163,6 +163,16 @@
|
|
|
163
163
|
"default": "./dist/plugins/ai-chat/client/hooks/index.cjs"
|
|
164
164
|
}
|
|
165
165
|
},
|
|
166
|
+
"./plugins/ai-chat/client/context": {
|
|
167
|
+
"import": {
|
|
168
|
+
"types": "./dist/plugins/ai-chat/client/context/page-ai-context.d.ts",
|
|
169
|
+
"default": "./dist/plugins/ai-chat/client/context/page-ai-context.mjs"
|
|
170
|
+
},
|
|
171
|
+
"require": {
|
|
172
|
+
"types": "./dist/plugins/ai-chat/client/context/page-ai-context.d.cts",
|
|
173
|
+
"default": "./dist/plugins/ai-chat/client/context/page-ai-context.cjs"
|
|
174
|
+
}
|
|
175
|
+
},
|
|
166
176
|
"./plugins/ai-chat/css": "./dist/plugins/ai-chat/style.css",
|
|
167
177
|
"./plugins/blog/css": "./dist/plugins/blog/style.css",
|
|
168
178
|
"./plugins/cms/api": {
|
|
@@ -392,6 +402,9 @@
|
|
|
392
402
|
"plugins/ai-chat/client/hooks": [
|
|
393
403
|
"./dist/plugins/ai-chat/client/hooks/index.d.ts"
|
|
394
404
|
],
|
|
405
|
+
"plugins/ai-chat/client/context": [
|
|
406
|
+
"./dist/plugins/ai-chat/client/context/page-ai-context.d.ts"
|
|
407
|
+
],
|
|
395
408
|
"plugins/cms/api": [
|
|
396
409
|
"./dist/plugins/cms/api/index.d.ts"
|
|
397
410
|
],
|
|
@@ -89,10 +89,13 @@ export function ComposedRoute({
|
|
|
89
89
|
}) {
|
|
90
90
|
if (PageComponent) {
|
|
91
91
|
const content = <PageComponent {...props} />;
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
92
|
+
// Always provide the same fallback on server and client — using
|
|
93
|
+
// `typeof window !== "undefined"` here would produce a different JSX tree
|
|
94
|
+
// on each side, shifting React's useId() counter and causing hydration
|
|
95
|
+
// mismatches in any descendant that uses Radix (Select, Dialog, etc.).
|
|
96
|
+
// If the Suspense boundary never actually suspends during SSR (data is
|
|
97
|
+
// prefetched), React won't emit the fallback into the HTML anyway.
|
|
98
|
+
const suspenseFallback = LoadingComponent ? <LoadingComponent /> : null;
|
|
96
99
|
|
|
97
100
|
// If an ErrorComponent is provided (which itself may be lazy), ensure we have
|
|
98
101
|
// a Suspense boundary that can handle both the page content and the lazy error UI
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { tool } from "ai";
|
|
2
|
+
import type { Tool } from "ai";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Maps each built-in page tool to the route names that are permitted to request it.
|
|
7
|
+
*
|
|
8
|
+
* The server cross-checks the `routeName` field sent with every chat request
|
|
9
|
+
* against this allowlist before including a built-in tool in the streamText call.
|
|
10
|
+
* A tool is only activated when both conditions are true:
|
|
11
|
+
* 1. The tool name appears in the request's `availableTools` list, AND
|
|
12
|
+
* 2. The request's `routeName` is in this tool's allowlist.
|
|
13
|
+
*
|
|
14
|
+
* Consumer-defined tools (via `clientToolSchemas`) are not validated here —
|
|
15
|
+
* the consumer is responsible for their own access control.
|
|
16
|
+
*/
|
|
17
|
+
export const BUILT_IN_PAGE_TOOL_ROUTE_ALLOWLIST: Record<string, string[]> = {
|
|
18
|
+
/** Blog new-post and edit-post pages */
|
|
19
|
+
fillBlogForm: ["blog-new-post", "blog-edit-post", "newPost", "editPost"],
|
|
20
|
+
/** UI builder edit page */
|
|
21
|
+
updatePageLayers: ["ui-builder-edit-page"],
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Built-in client-side-only tool schemas for route-aware AI context.
|
|
26
|
+
*
|
|
27
|
+
* These tools have no `execute` function — they are handled on the client side
|
|
28
|
+
* via the onToolCall handler in ChatInterface, which dispatches to handlers
|
|
29
|
+
* registered by pages via useRegisterPageAIContext.
|
|
30
|
+
*
|
|
31
|
+
* Consumers can add their own tool schemas via `clientToolSchemas` in AiChatBackendConfig.
|
|
32
|
+
*/
|
|
33
|
+
export const BUILT_IN_PAGE_TOOL_SCHEMAS: Record<string, Tool> = {
|
|
34
|
+
/**
|
|
35
|
+
* Fill in the blog post editor form fields.
|
|
36
|
+
* Registered by blog new/edit page via useRegisterPageAIContext.
|
|
37
|
+
*/
|
|
38
|
+
fillBlogForm: tool({
|
|
39
|
+
description:
|
|
40
|
+
"Fill in the blog post editor form fields. Call this when the user asks to write, draft, or populate a blog post. You can fill any combination of title, content, excerpt, and tags.",
|
|
41
|
+
inputSchema: z.object({
|
|
42
|
+
title: z.string().optional().describe("The post title"),
|
|
43
|
+
content: z
|
|
44
|
+
.string()
|
|
45
|
+
.optional()
|
|
46
|
+
.describe(
|
|
47
|
+
"Full markdown content for the post body. Use proper markdown formatting with headings, lists, etc.",
|
|
48
|
+
),
|
|
49
|
+
excerpt: z
|
|
50
|
+
.string()
|
|
51
|
+
.optional()
|
|
52
|
+
.describe("A short summary/excerpt of the post (1-2 sentences)"),
|
|
53
|
+
tags: z
|
|
54
|
+
.array(z.string())
|
|
55
|
+
.optional()
|
|
56
|
+
.describe("Array of tag names to apply to the post"),
|
|
57
|
+
}),
|
|
58
|
+
}),
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Replace the UI builder page layers with new ones.
|
|
62
|
+
* Registered by the UI builder edit page via useRegisterPageAIContext.
|
|
63
|
+
*/
|
|
64
|
+
updatePageLayers: tool({
|
|
65
|
+
description: `Replace the UI builder page component layers. Call this when the user asks to change, add, redesign, or update the page layout and components.
|
|
66
|
+
|
|
67
|
+
Rules:
|
|
68
|
+
- Provide the COMPLETE layer tree, not a partial diff. The entire tree will replace the current layers.
|
|
69
|
+
- Only use component types that appear in the "Available Component Types" list in the page context.
|
|
70
|
+
- Every layer must have a unique \`id\` string (e.g. "hero-section", "card-title-1").
|
|
71
|
+
- The \`type\` field must exactly match a name from the component registry (e.g. "div", "Button", "Card", "Flexbox").
|
|
72
|
+
- The \`name\` field is the human-readable label shown in the layers panel.
|
|
73
|
+
- \`props\` contains component-specific props (className uses Tailwind classes).
|
|
74
|
+
- \`children\` is either an array of child ComponentLayer objects, or a plain string for text content.
|
|
75
|
+
- Use \`Flexbox\` or \`Grid\` for layout instead of raw div flex/grid when possible.
|
|
76
|
+
- Preserve any layers the user has not asked to change — read the current layers from the page context first.
|
|
77
|
+
- ALWAYS use shadcn/ui semantic color tokens in className (e.g. bg-background, bg-card, bg-primary, text-foreground, text-muted-foreground, text-primary-foreground, border-border) instead of hardcoded Tailwind colors like bg-white, bg-gray-*, text-black, etc. This ensures the UI automatically adapts to light and dark themes.`,
|
|
78
|
+
inputSchema: z.object({
|
|
79
|
+
layers: z
|
|
80
|
+
.array(
|
|
81
|
+
z.object({
|
|
82
|
+
id: z.string().describe("Unique identifier for this layer"),
|
|
83
|
+
type: z
|
|
84
|
+
.string()
|
|
85
|
+
.describe(
|
|
86
|
+
"Component type — must match a key in the component registry (e.g. 'div', 'Button', 'Card', 'Flexbox')",
|
|
87
|
+
),
|
|
88
|
+
name: z
|
|
89
|
+
.string()
|
|
90
|
+
.describe(
|
|
91
|
+
"Human-readable display name shown in the layers panel",
|
|
92
|
+
),
|
|
93
|
+
props: z
|
|
94
|
+
.record(z.string(), z.any())
|
|
95
|
+
.describe(
|
|
96
|
+
"Component props object. Use Tailwind classes for className. See the component registry for valid props per type.",
|
|
97
|
+
),
|
|
98
|
+
children: z
|
|
99
|
+
.any()
|
|
100
|
+
.optional()
|
|
101
|
+
.describe(
|
|
102
|
+
"Child layers (array of ComponentLayer) or plain text string",
|
|
103
|
+
),
|
|
104
|
+
}),
|
|
105
|
+
)
|
|
106
|
+
.describe(
|
|
107
|
+
"Complete replacement layer tree. Must include ALL layers for the page, not just changed ones.",
|
|
108
|
+
),
|
|
109
|
+
}),
|
|
110
|
+
}),
|
|
111
|
+
};
|
|
@@ -17,6 +17,10 @@ import {
|
|
|
17
17
|
} from "../schemas";
|
|
18
18
|
import type { Conversation, ConversationWithMessages, Message } from "../types";
|
|
19
19
|
import { getAllConversations, getConversationById } from "./getters";
|
|
20
|
+
import {
|
|
21
|
+
BUILT_IN_PAGE_TOOL_ROUTE_ALLOWLIST,
|
|
22
|
+
BUILT_IN_PAGE_TOOL_SCHEMAS,
|
|
23
|
+
} from "./page-tools";
|
|
20
24
|
|
|
21
25
|
/**
|
|
22
26
|
* Context passed to AI Chat API hooks
|
|
@@ -98,6 +102,23 @@ export interface AiChatBackendHooks {
|
|
|
98
102
|
context: ChatApiContext,
|
|
99
103
|
) => Promise<boolean> | boolean;
|
|
100
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Called after the structural routeName/allowlist validation, with the list
|
|
107
|
+
* of tool names that passed. Return a filtered subset to further restrict
|
|
108
|
+
* which tools the LLM sees, or return [] to suppress all page tools.
|
|
109
|
+
* Throw an Error to abort the entire chat request with a 403 response.
|
|
110
|
+
* Not called when no tools passed the structural validation step.
|
|
111
|
+
*
|
|
112
|
+
* @param toolNames - Names that passed the routeName allowlist check
|
|
113
|
+
* @param routeName - routeName claimed by the request (may be undefined)
|
|
114
|
+
* @param context - Full request context (headers, body, etc.)
|
|
115
|
+
*/
|
|
116
|
+
onBeforeToolsActivated?: (
|
|
117
|
+
toolNames: string[],
|
|
118
|
+
routeName: string | undefined,
|
|
119
|
+
context: ChatApiContext,
|
|
120
|
+
) => Promise<string[]> | string[];
|
|
121
|
+
|
|
101
122
|
// ============== Lifecycle Hooks ==============
|
|
102
123
|
|
|
103
124
|
/**
|
|
@@ -232,6 +253,32 @@ export type AiChatMode = "authenticated" | "public";
|
|
|
232
253
|
/**
|
|
233
254
|
* Configuration for AI Chat backend plugin
|
|
234
255
|
*/
|
|
256
|
+
/**
|
|
257
|
+
* Extracts only the literal (non-index-signature) keys from a type.
|
|
258
|
+
* For `Record<string, T>` this resolves to `never`, so collision checks are
|
|
259
|
+
* skipped when the tools map is typed with a broad string index.
|
|
260
|
+
*/
|
|
261
|
+
type KnownKeys<T> = {
|
|
262
|
+
[K in keyof T]: string extends K ? never : K;
|
|
263
|
+
}[keyof T];
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Ensures `TClientTools` has no keys that are also literal keys in `TTools`.
|
|
267
|
+
* Colliding keys are mapped to `never`, which produces a compile-time error
|
|
268
|
+
* at the point of the duplicate key. When `TTools` uses a string index
|
|
269
|
+
* signature the check is skipped to avoid false positives.
|
|
270
|
+
*/
|
|
271
|
+
type NoKeyCollision<
|
|
272
|
+
TTools,
|
|
273
|
+
TClientTools extends Record<string, Tool>,
|
|
274
|
+
> = KnownKeys<TTools> & keyof TClientTools extends never
|
|
275
|
+
? TClientTools
|
|
276
|
+
: {
|
|
277
|
+
[K in keyof TClientTools]: K extends KnownKeys<TTools>
|
|
278
|
+
? never // duplicate of a server-side tool — remove from clientToolSchemas
|
|
279
|
+
: TClientTools[K];
|
|
280
|
+
};
|
|
281
|
+
|
|
235
282
|
export interface AiChatBackendConfig {
|
|
236
283
|
/**
|
|
237
284
|
* The language model to use for chat completions.
|
|
@@ -269,6 +316,31 @@ export interface AiChatBackendConfig {
|
|
|
269
316
|
*/
|
|
270
317
|
tools?: Record<string, Tool>;
|
|
271
318
|
|
|
319
|
+
/**
|
|
320
|
+
* Enable route-aware page tools.
|
|
321
|
+
* When true, the server will include tool schemas for client-side page tools
|
|
322
|
+
* (e.g. fillBlogForm, updatePageLayers) based on the availableTools list
|
|
323
|
+
* sent with each request.
|
|
324
|
+
* @default false
|
|
325
|
+
*/
|
|
326
|
+
enablePageTools?: boolean;
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Custom client-side tool schemas for non-BTST pages.
|
|
330
|
+
* Merged with built-in page tool schemas (fillBlogForm, updatePageLayers).
|
|
331
|
+
* Only included when enablePageTools is true and the tool name appears in
|
|
332
|
+
* the availableTools list sent with the request.
|
|
333
|
+
*
|
|
334
|
+
* @example
|
|
335
|
+
* clientToolSchemas: {
|
|
336
|
+
* addToCart: tool({
|
|
337
|
+
* description: "Add current product to cart",
|
|
338
|
+
* parameters: z.object({ quantity: z.number().int().min(1) }),
|
|
339
|
+
* }),
|
|
340
|
+
* }
|
|
341
|
+
*/
|
|
342
|
+
clientToolSchemas?: Record<string, Tool>;
|
|
343
|
+
|
|
272
344
|
/**
|
|
273
345
|
* Optional hooks for customizing plugin behavior
|
|
274
346
|
*/
|
|
@@ -282,7 +354,15 @@ export interface AiChatBackendConfig {
|
|
|
282
354
|
*
|
|
283
355
|
* @param config - Configuration including model, tools, and optional hooks
|
|
284
356
|
*/
|
|
285
|
-
export const aiChatBackendPlugin =
|
|
357
|
+
export const aiChatBackendPlugin = <
|
|
358
|
+
TTools extends Record<string, Tool> = Record<never, Tool>,
|
|
359
|
+
TClientTools extends Record<string, Tool> = Record<never, Tool>,
|
|
360
|
+
>(
|
|
361
|
+
config: Omit<AiChatBackendConfig, "tools" | "clientToolSchemas"> & {
|
|
362
|
+
tools?: TTools;
|
|
363
|
+
clientToolSchemas?: NoKeyCollision<TTools, TClientTools>;
|
|
364
|
+
},
|
|
365
|
+
) =>
|
|
286
366
|
defineBackendPlugin({
|
|
287
367
|
name: "ai-chat",
|
|
288
368
|
// Always include db schema - in public mode we just don't use it
|
|
@@ -350,7 +430,13 @@ export const aiChatBackendPlugin = (config: AiChatBackendConfig) =>
|
|
|
350
430
|
body: chatRequestSchema,
|
|
351
431
|
},
|
|
352
432
|
async (ctx) => {
|
|
353
|
-
const {
|
|
433
|
+
const {
|
|
434
|
+
messages: rawMessages,
|
|
435
|
+
conversationId,
|
|
436
|
+
pageContext,
|
|
437
|
+
availableTools,
|
|
438
|
+
routeName,
|
|
439
|
+
} = ctx.body;
|
|
354
440
|
const uiMessages = rawMessages as UIMessage[];
|
|
355
441
|
|
|
356
442
|
const context: ChatApiContext = {
|
|
@@ -388,22 +474,107 @@ export const aiChatBackendPlugin = (config: AiChatBackendConfig) =>
|
|
|
388
474
|
// Convert UIMessages to CoreMessages for streamText
|
|
389
475
|
const modelMessages = convertToModelMessages(uiMessages);
|
|
390
476
|
|
|
391
|
-
//
|
|
392
|
-
const
|
|
477
|
+
// Build system prompt: base config + optional page context
|
|
478
|
+
const pageContextContent =
|
|
479
|
+
pageContext && pageContext.trim()
|
|
480
|
+
? `\n\nCurrent page context:\n${pageContext}`
|
|
481
|
+
: "";
|
|
482
|
+
const systemContent = config.systemPrompt
|
|
483
|
+
? `${config.systemPrompt}${pageContextContent}`
|
|
484
|
+
: pageContextContent || undefined;
|
|
485
|
+
|
|
486
|
+
const messagesWithSystem = systemContent
|
|
393
487
|
? [
|
|
394
|
-
{ role: "system" as const, content:
|
|
488
|
+
{ role: "system" as const, content: systemContent },
|
|
395
489
|
...modelMessages,
|
|
396
490
|
]
|
|
397
491
|
: modelMessages;
|
|
398
492
|
|
|
493
|
+
// Merge page tool schemas when enablePageTools is on.
|
|
494
|
+
// Built-in schemas are only included when the request's routeName is in
|
|
495
|
+
// the tool's allowlist — this prevents a page from claiming tools that
|
|
496
|
+
// are intended for a different route (e.g. requesting updatePageLayers
|
|
497
|
+
// from a blog page). Consumer clientToolSchemas are trusted as-is.
|
|
498
|
+
const activePageTools: Record<string, Tool> =
|
|
499
|
+
config.enablePageTools &&
|
|
500
|
+
availableTools &&
|
|
501
|
+
availableTools.length > 0
|
|
502
|
+
? (() => {
|
|
503
|
+
const consumerSchemas: Record<string, Tool> =
|
|
504
|
+
(config.clientToolSchemas as Record<string, Tool>) ?? {};
|
|
505
|
+
return Object.fromEntries(
|
|
506
|
+
availableTools
|
|
507
|
+
.filter((name) => {
|
|
508
|
+
// Built-in tool: require routeName to be in its allowlist
|
|
509
|
+
if (name in BUILT_IN_PAGE_TOOL_SCHEMAS) {
|
|
510
|
+
const allowed =
|
|
511
|
+
BUILT_IN_PAGE_TOOL_ROUTE_ALLOWLIST[name];
|
|
512
|
+
return (
|
|
513
|
+
allowed &&
|
|
514
|
+
routeName &&
|
|
515
|
+
allowed.includes(routeName)
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
// Consumer-defined tool: allow if schema is registered
|
|
519
|
+
return name in consumerSchemas;
|
|
520
|
+
})
|
|
521
|
+
.map((name) => {
|
|
522
|
+
const schema =
|
|
523
|
+
BUILT_IN_PAGE_TOOL_SCHEMAS[name] ??
|
|
524
|
+
consumerSchemas[name]!;
|
|
525
|
+
return [name, schema];
|
|
526
|
+
}),
|
|
527
|
+
);
|
|
528
|
+
})()
|
|
529
|
+
: {};
|
|
530
|
+
|
|
531
|
+
// Consumer hook: user-level tool authorization.
|
|
532
|
+
// Runs after the structural routeName allowlist check.
|
|
533
|
+
// A thrown Error is caught and returned as a 403 response,
|
|
534
|
+
// consistent with how onBeforeChat handles return false → 403.
|
|
535
|
+
if (
|
|
536
|
+
config.hooks?.onBeforeToolsActivated &&
|
|
537
|
+
Object.keys(activePageTools).length > 0
|
|
538
|
+
) {
|
|
539
|
+
try {
|
|
540
|
+
const allowed = await config.hooks.onBeforeToolsActivated(
|
|
541
|
+
Object.keys(activePageTools),
|
|
542
|
+
routeName,
|
|
543
|
+
context,
|
|
544
|
+
);
|
|
545
|
+
const allowedSet = new Set(allowed);
|
|
546
|
+
for (const key of Object.keys(activePageTools)) {
|
|
547
|
+
if (!allowedSet.has(key)) {
|
|
548
|
+
delete activePageTools[key];
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
} catch (hookError) {
|
|
552
|
+
throw ctx.error(403, {
|
|
553
|
+
message:
|
|
554
|
+
hookError instanceof Error
|
|
555
|
+
? hookError.message
|
|
556
|
+
: "Unauthorized: Tool activation denied",
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Page tools are layered under server-side tools so that a
|
|
562
|
+
// clientToolSchemas entry with the same name as a tool in
|
|
563
|
+
// config.tools never silently drops its `execute` function.
|
|
564
|
+
// Server-side tools always win on collision.
|
|
565
|
+
const mergedTools =
|
|
566
|
+
Object.keys(activePageTools).length > 0
|
|
567
|
+
? { ...activePageTools, ...config.tools }
|
|
568
|
+
: config.tools;
|
|
569
|
+
|
|
399
570
|
// PUBLIC MODE: Stream without persistence
|
|
400
571
|
if (isPublicMode) {
|
|
401
572
|
const result = streamText({
|
|
402
573
|
model: config.model,
|
|
403
574
|
messages: messagesWithSystem,
|
|
404
|
-
tools:
|
|
575
|
+
tools: mergedTools,
|
|
405
576
|
// Enable multi-step tool calls if tools are configured
|
|
406
|
-
...(
|
|
577
|
+
...(mergedTools ? { stopWhen: stepCountIs(5) } : {}),
|
|
407
578
|
});
|
|
408
579
|
|
|
409
580
|
return result.toUIMessageStreamResponse({
|
|
@@ -557,9 +728,9 @@ export const aiChatBackendPlugin = (config: AiChatBackendConfig) =>
|
|
|
557
728
|
const result = streamText({
|
|
558
729
|
model: config.model,
|
|
559
730
|
messages: messagesWithSystem,
|
|
560
|
-
tools:
|
|
731
|
+
tools: mergedTools,
|
|
561
732
|
// Enable multi-step tool calls if tools are configured
|
|
562
|
-
...(
|
|
733
|
+
...(mergedTools ? { stopWhen: stepCountIs(5) } : {}),
|
|
563
734
|
onFinish: async (completion: { text: string }) => {
|
|
564
735
|
// Wrap in try-catch since this runs after the response is sent
|
|
565
736
|
// and errors would otherwise become unhandled promise rejections
|
|
@@ -260,14 +260,14 @@ export function ChatInput({
|
|
|
260
260
|
)}
|
|
261
261
|
|
|
262
262
|
{/* Text Input */}
|
|
263
|
-
<div className="relative flex-1">
|
|
263
|
+
<div className="relative flex-1 min-w-0">
|
|
264
264
|
<Textarea
|
|
265
265
|
value={input}
|
|
266
266
|
onChange={handleInputChange}
|
|
267
267
|
onKeyDown={handleKeyDown}
|
|
268
268
|
placeholder={placeholder || localization.CHAT_PLACEHOLDER}
|
|
269
269
|
className={cn(
|
|
270
|
-
"resize-none pr-12",
|
|
270
|
+
"resize-none pr-12 max-w-full",
|
|
271
271
|
isCompact
|
|
272
272
|
? "min-h-[40px] max-h-[120px] py-2"
|
|
273
273
|
: "min-h-[50px] max-h-[200px] py-3",
|