@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
|
@@ -40,8 +40,7 @@ function ComposedRoute({
|
|
|
40
40
|
}) {
|
|
41
41
|
if (PageComponent) {
|
|
42
42
|
const content = /* @__PURE__ */ jsxRuntime.jsx(PageComponent, { ...props });
|
|
43
|
-
const
|
|
44
|
-
const suspenseFallback = isBrowser && LoadingComponent ? /* @__PURE__ */ jsxRuntime.jsx(LoadingComponent, {}) : null;
|
|
43
|
+
const suspenseFallback = LoadingComponent ? /* @__PURE__ */ jsxRuntime.jsx(LoadingComponent, {}) : null;
|
|
45
44
|
if (ErrorComponent) {
|
|
46
45
|
return /* @__PURE__ */ jsxRuntime.jsx(React.Suspense, { fallback: suspenseFallback, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
47
46
|
errorBoundary.ErrorBoundary,
|
|
@@ -38,8 +38,7 @@ function ComposedRoute({
|
|
|
38
38
|
}) {
|
|
39
39
|
if (PageComponent) {
|
|
40
40
|
const content = /* @__PURE__ */ jsx(PageComponent, { ...props });
|
|
41
|
-
const
|
|
42
|
-
const suspenseFallback = isBrowser && LoadingComponent ? /* @__PURE__ */ jsx(LoadingComponent, {}) : null;
|
|
41
|
+
const suspenseFallback = LoadingComponent ? /* @__PURE__ */ jsx(LoadingComponent, {}) : null;
|
|
43
42
|
if (ErrorComponent) {
|
|
44
43
|
return /* @__PURE__ */ jsx(Suspense, { fallback: suspenseFallback, children: /* @__PURE__ */ jsx(
|
|
45
44
|
ErrorBoundary,
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const ai = require('ai');
|
|
4
|
+
const z = require('zod');
|
|
5
|
+
|
|
6
|
+
const BUILT_IN_PAGE_TOOL_ROUTE_ALLOWLIST = {
|
|
7
|
+
/** Blog new-post and edit-post pages */
|
|
8
|
+
fillBlogForm: ["blog-new-post", "blog-edit-post", "newPost", "editPost"],
|
|
9
|
+
/** UI builder edit page */
|
|
10
|
+
updatePageLayers: ["ui-builder-edit-page"]
|
|
11
|
+
};
|
|
12
|
+
const BUILT_IN_PAGE_TOOL_SCHEMAS = {
|
|
13
|
+
/**
|
|
14
|
+
* Fill in the blog post editor form fields.
|
|
15
|
+
* Registered by blog new/edit page via useRegisterPageAIContext.
|
|
16
|
+
*/
|
|
17
|
+
fillBlogForm: ai.tool({
|
|
18
|
+
description: "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.",
|
|
19
|
+
inputSchema: z.z.object({
|
|
20
|
+
title: z.z.string().optional().describe("The post title"),
|
|
21
|
+
content: z.z.string().optional().describe(
|
|
22
|
+
"Full markdown content for the post body. Use proper markdown formatting with headings, lists, etc."
|
|
23
|
+
),
|
|
24
|
+
excerpt: z.z.string().optional().describe("A short summary/excerpt of the post (1-2 sentences)"),
|
|
25
|
+
tags: z.z.array(z.z.string()).optional().describe("Array of tag names to apply to the post")
|
|
26
|
+
})
|
|
27
|
+
}),
|
|
28
|
+
/**
|
|
29
|
+
* Replace the UI builder page layers with new ones.
|
|
30
|
+
* Registered by the UI builder edit page via useRegisterPageAIContext.
|
|
31
|
+
*/
|
|
32
|
+
updatePageLayers: ai.tool({
|
|
33
|
+
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.
|
|
34
|
+
|
|
35
|
+
Rules:
|
|
36
|
+
- Provide the COMPLETE layer tree, not a partial diff. The entire tree will replace the current layers.
|
|
37
|
+
- Only use component types that appear in the "Available Component Types" list in the page context.
|
|
38
|
+
- Every layer must have a unique \`id\` string (e.g. "hero-section", "card-title-1").
|
|
39
|
+
- The \`type\` field must exactly match a name from the component registry (e.g. "div", "Button", "Card", "Flexbox").
|
|
40
|
+
- The \`name\` field is the human-readable label shown in the layers panel.
|
|
41
|
+
- \`props\` contains component-specific props (className uses Tailwind classes).
|
|
42
|
+
- \`children\` is either an array of child ComponentLayer objects, or a plain string for text content.
|
|
43
|
+
- Use \`Flexbox\` or \`Grid\` for layout instead of raw div flex/grid when possible.
|
|
44
|
+
- Preserve any layers the user has not asked to change \u2014 read the current layers from the page context first.
|
|
45
|
+
- 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.`,
|
|
46
|
+
inputSchema: z.z.object({
|
|
47
|
+
layers: z.z.array(
|
|
48
|
+
z.z.object({
|
|
49
|
+
id: z.z.string().describe("Unique identifier for this layer"),
|
|
50
|
+
type: z.z.string().describe(
|
|
51
|
+
"Component type \u2014 must match a key in the component registry (e.g. 'div', 'Button', 'Card', 'Flexbox')"
|
|
52
|
+
),
|
|
53
|
+
name: z.z.string().describe(
|
|
54
|
+
"Human-readable display name shown in the layers panel"
|
|
55
|
+
),
|
|
56
|
+
props: z.z.record(z.z.string(), z.z.any()).describe(
|
|
57
|
+
"Component props object. Use Tailwind classes for className. See the component registry for valid props per type."
|
|
58
|
+
),
|
|
59
|
+
children: z.z.any().optional().describe(
|
|
60
|
+
"Child layers (array of ComponentLayer) or plain text string"
|
|
61
|
+
)
|
|
62
|
+
})
|
|
63
|
+
).describe(
|
|
64
|
+
"Complete replacement layer tree. Must include ALL layers for the page, not just changed ones."
|
|
65
|
+
)
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
exports.BUILT_IN_PAGE_TOOL_ROUTE_ALLOWLIST = BUILT_IN_PAGE_TOOL_ROUTE_ALLOWLIST;
|
|
71
|
+
exports.BUILT_IN_PAGE_TOOL_SCHEMAS = BUILT_IN_PAGE_TOOL_SCHEMAS;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { tool } from 'ai';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
const BUILT_IN_PAGE_TOOL_ROUTE_ALLOWLIST = {
|
|
5
|
+
/** Blog new-post and edit-post pages */
|
|
6
|
+
fillBlogForm: ["blog-new-post", "blog-edit-post", "newPost", "editPost"],
|
|
7
|
+
/** UI builder edit page */
|
|
8
|
+
updatePageLayers: ["ui-builder-edit-page"]
|
|
9
|
+
};
|
|
10
|
+
const BUILT_IN_PAGE_TOOL_SCHEMAS = {
|
|
11
|
+
/**
|
|
12
|
+
* Fill in the blog post editor form fields.
|
|
13
|
+
* Registered by blog new/edit page via useRegisterPageAIContext.
|
|
14
|
+
*/
|
|
15
|
+
fillBlogForm: tool({
|
|
16
|
+
description: "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.",
|
|
17
|
+
inputSchema: z.object({
|
|
18
|
+
title: z.string().optional().describe("The post title"),
|
|
19
|
+
content: z.string().optional().describe(
|
|
20
|
+
"Full markdown content for the post body. Use proper markdown formatting with headings, lists, etc."
|
|
21
|
+
),
|
|
22
|
+
excerpt: z.string().optional().describe("A short summary/excerpt of the post (1-2 sentences)"),
|
|
23
|
+
tags: z.array(z.string()).optional().describe("Array of tag names to apply to the post")
|
|
24
|
+
})
|
|
25
|
+
}),
|
|
26
|
+
/**
|
|
27
|
+
* Replace the UI builder page layers with new ones.
|
|
28
|
+
* Registered by the UI builder edit page via useRegisterPageAIContext.
|
|
29
|
+
*/
|
|
30
|
+
updatePageLayers: tool({
|
|
31
|
+
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.
|
|
32
|
+
|
|
33
|
+
Rules:
|
|
34
|
+
- Provide the COMPLETE layer tree, not a partial diff. The entire tree will replace the current layers.
|
|
35
|
+
- Only use component types that appear in the "Available Component Types" list in the page context.
|
|
36
|
+
- Every layer must have a unique \`id\` string (e.g. "hero-section", "card-title-1").
|
|
37
|
+
- The \`type\` field must exactly match a name from the component registry (e.g. "div", "Button", "Card", "Flexbox").
|
|
38
|
+
- The \`name\` field is the human-readable label shown in the layers panel.
|
|
39
|
+
- \`props\` contains component-specific props (className uses Tailwind classes).
|
|
40
|
+
- \`children\` is either an array of child ComponentLayer objects, or a plain string for text content.
|
|
41
|
+
- Use \`Flexbox\` or \`Grid\` for layout instead of raw div flex/grid when possible.
|
|
42
|
+
- Preserve any layers the user has not asked to change \u2014 read the current layers from the page context first.
|
|
43
|
+
- 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.`,
|
|
44
|
+
inputSchema: z.object({
|
|
45
|
+
layers: z.array(
|
|
46
|
+
z.object({
|
|
47
|
+
id: z.string().describe("Unique identifier for this layer"),
|
|
48
|
+
type: z.string().describe(
|
|
49
|
+
"Component type \u2014 must match a key in the component registry (e.g. 'div', 'Button', 'Card', 'Flexbox')"
|
|
50
|
+
),
|
|
51
|
+
name: z.string().describe(
|
|
52
|
+
"Human-readable display name shown in the layers panel"
|
|
53
|
+
),
|
|
54
|
+
props: z.record(z.string(), z.any()).describe(
|
|
55
|
+
"Component props object. Use Tailwind classes for className. See the component registry for valid props per type."
|
|
56
|
+
),
|
|
57
|
+
children: z.any().optional().describe(
|
|
58
|
+
"Child layers (array of ComponentLayer) or plain text string"
|
|
59
|
+
)
|
|
60
|
+
})
|
|
61
|
+
).describe(
|
|
62
|
+
"Complete replacement layer tree. Must include ALL layers for the page, not just changed ones."
|
|
63
|
+
)
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export { BUILT_IN_PAGE_TOOL_ROUTE_ALLOWLIST, BUILT_IN_PAGE_TOOL_SCHEMAS };
|
|
@@ -5,6 +5,7 @@ const ai = require('ai');
|
|
|
5
5
|
const db = require('../db.cjs');
|
|
6
6
|
const schemas = require('../schemas.cjs');
|
|
7
7
|
const getters = require('./getters.cjs');
|
|
8
|
+
const pageTools = require('./page-tools.cjs');
|
|
8
9
|
|
|
9
10
|
const aiChatBackendPlugin = (config) => api.defineBackendPlugin({
|
|
10
11
|
name: "ai-chat",
|
|
@@ -52,7 +53,13 @@ const aiChatBackendPlugin = (config) => api.defineBackendPlugin({
|
|
|
52
53
|
body: schemas.chatRequestSchema
|
|
53
54
|
},
|
|
54
55
|
async (ctx) => {
|
|
55
|
-
const {
|
|
56
|
+
const {
|
|
57
|
+
messages: rawMessages,
|
|
58
|
+
conversationId,
|
|
59
|
+
pageContext,
|
|
60
|
+
availableTools,
|
|
61
|
+
routeName
|
|
62
|
+
} = ctx.body;
|
|
56
63
|
const uiMessages = rawMessages;
|
|
57
64
|
const context = {
|
|
58
65
|
body: ctx.body,
|
|
@@ -83,17 +90,57 @@ const aiChatBackendPlugin = (config) => api.defineBackendPlugin({
|
|
|
83
90
|
}
|
|
84
91
|
const firstMessageContent = getMessageTextContent(firstMessage);
|
|
85
92
|
const modelMessages = ai.convertToModelMessages(uiMessages);
|
|
86
|
-
const
|
|
87
|
-
|
|
93
|
+
const pageContextContent = pageContext && pageContext.trim() ? `
|
|
94
|
+
|
|
95
|
+
Current page context:
|
|
96
|
+
${pageContext}` : "";
|
|
97
|
+
const systemContent = config.systemPrompt ? `${config.systemPrompt}${pageContextContent}` : pageContextContent || void 0;
|
|
98
|
+
const messagesWithSystem = systemContent ? [
|
|
99
|
+
{ role: "system", content: systemContent },
|
|
88
100
|
...modelMessages
|
|
89
101
|
] : modelMessages;
|
|
102
|
+
const activePageTools = config.enablePageTools && availableTools && availableTools.length > 0 ? (() => {
|
|
103
|
+
const consumerSchemas = config.clientToolSchemas ?? {};
|
|
104
|
+
return Object.fromEntries(
|
|
105
|
+
availableTools.filter((name) => {
|
|
106
|
+
if (name in pageTools.BUILT_IN_PAGE_TOOL_SCHEMAS) {
|
|
107
|
+
const allowed = pageTools.BUILT_IN_PAGE_TOOL_ROUTE_ALLOWLIST[name];
|
|
108
|
+
return allowed && routeName && allowed.includes(routeName);
|
|
109
|
+
}
|
|
110
|
+
return name in consumerSchemas;
|
|
111
|
+
}).map((name) => {
|
|
112
|
+
const schema = pageTools.BUILT_IN_PAGE_TOOL_SCHEMAS[name] ?? consumerSchemas[name];
|
|
113
|
+
return [name, schema];
|
|
114
|
+
})
|
|
115
|
+
);
|
|
116
|
+
})() : {};
|
|
117
|
+
if (config.hooks?.onBeforeToolsActivated && Object.keys(activePageTools).length > 0) {
|
|
118
|
+
try {
|
|
119
|
+
const allowed = await config.hooks.onBeforeToolsActivated(
|
|
120
|
+
Object.keys(activePageTools),
|
|
121
|
+
routeName,
|
|
122
|
+
context
|
|
123
|
+
);
|
|
124
|
+
const allowedSet = new Set(allowed);
|
|
125
|
+
for (const key of Object.keys(activePageTools)) {
|
|
126
|
+
if (!allowedSet.has(key)) {
|
|
127
|
+
delete activePageTools[key];
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
} catch (hookError) {
|
|
131
|
+
throw ctx.error(403, {
|
|
132
|
+
message: hookError instanceof Error ? hookError.message : "Unauthorized: Tool activation denied"
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const mergedTools = Object.keys(activePageTools).length > 0 ? { ...activePageTools, ...config.tools } : config.tools;
|
|
90
137
|
if (isPublicMode) {
|
|
91
138
|
const result2 = ai.streamText({
|
|
92
139
|
model: config.model,
|
|
93
140
|
messages: messagesWithSystem,
|
|
94
|
-
tools:
|
|
141
|
+
tools: mergedTools,
|
|
95
142
|
// Enable multi-step tool calls if tools are configured
|
|
96
|
-
...
|
|
143
|
+
...mergedTools ? { stopWhen: ai.stepCountIs(5) } : {}
|
|
97
144
|
});
|
|
98
145
|
return result2.toUIMessageStreamResponse({
|
|
99
146
|
originalMessages: uiMessages
|
|
@@ -201,9 +248,9 @@ const aiChatBackendPlugin = (config) => api.defineBackendPlugin({
|
|
|
201
248
|
const result = ai.streamText({
|
|
202
249
|
model: config.model,
|
|
203
250
|
messages: messagesWithSystem,
|
|
204
|
-
tools:
|
|
251
|
+
tools: mergedTools,
|
|
205
252
|
// Enable multi-step tool calls if tools are configured
|
|
206
|
-
...
|
|
253
|
+
...mergedTools ? { stopWhen: ai.stepCountIs(5) } : {},
|
|
207
254
|
onFinish: async (completion) => {
|
|
208
255
|
try {
|
|
209
256
|
const assistantParts = completion.text ? [{ type: "text", text: completion.text }] : [];
|
|
@@ -3,6 +3,7 @@ import { convertToModelMessages, streamText, stepCountIs } from 'ai';
|
|
|
3
3
|
import { aiChatSchema } from '../db.mjs';
|
|
4
4
|
import { chatRequestSchema, createConversationSchema, updateConversationSchema } from '../schemas.mjs';
|
|
5
5
|
import { getConversationById, getAllConversations } from './getters.mjs';
|
|
6
|
+
import { BUILT_IN_PAGE_TOOL_SCHEMAS, BUILT_IN_PAGE_TOOL_ROUTE_ALLOWLIST } from './page-tools.mjs';
|
|
6
7
|
|
|
7
8
|
const aiChatBackendPlugin = (config) => defineBackendPlugin({
|
|
8
9
|
name: "ai-chat",
|
|
@@ -50,7 +51,13 @@ const aiChatBackendPlugin = (config) => defineBackendPlugin({
|
|
|
50
51
|
body: chatRequestSchema
|
|
51
52
|
},
|
|
52
53
|
async (ctx) => {
|
|
53
|
-
const {
|
|
54
|
+
const {
|
|
55
|
+
messages: rawMessages,
|
|
56
|
+
conversationId,
|
|
57
|
+
pageContext,
|
|
58
|
+
availableTools,
|
|
59
|
+
routeName
|
|
60
|
+
} = ctx.body;
|
|
54
61
|
const uiMessages = rawMessages;
|
|
55
62
|
const context = {
|
|
56
63
|
body: ctx.body,
|
|
@@ -81,17 +88,57 @@ const aiChatBackendPlugin = (config) => defineBackendPlugin({
|
|
|
81
88
|
}
|
|
82
89
|
const firstMessageContent = getMessageTextContent(firstMessage);
|
|
83
90
|
const modelMessages = convertToModelMessages(uiMessages);
|
|
84
|
-
const
|
|
85
|
-
|
|
91
|
+
const pageContextContent = pageContext && pageContext.trim() ? `
|
|
92
|
+
|
|
93
|
+
Current page context:
|
|
94
|
+
${pageContext}` : "";
|
|
95
|
+
const systemContent = config.systemPrompt ? `${config.systemPrompt}${pageContextContent}` : pageContextContent || void 0;
|
|
96
|
+
const messagesWithSystem = systemContent ? [
|
|
97
|
+
{ role: "system", content: systemContent },
|
|
86
98
|
...modelMessages
|
|
87
99
|
] : modelMessages;
|
|
100
|
+
const activePageTools = config.enablePageTools && availableTools && availableTools.length > 0 ? (() => {
|
|
101
|
+
const consumerSchemas = config.clientToolSchemas ?? {};
|
|
102
|
+
return Object.fromEntries(
|
|
103
|
+
availableTools.filter((name) => {
|
|
104
|
+
if (name in BUILT_IN_PAGE_TOOL_SCHEMAS) {
|
|
105
|
+
const allowed = BUILT_IN_PAGE_TOOL_ROUTE_ALLOWLIST[name];
|
|
106
|
+
return allowed && routeName && allowed.includes(routeName);
|
|
107
|
+
}
|
|
108
|
+
return name in consumerSchemas;
|
|
109
|
+
}).map((name) => {
|
|
110
|
+
const schema = BUILT_IN_PAGE_TOOL_SCHEMAS[name] ?? consumerSchemas[name];
|
|
111
|
+
return [name, schema];
|
|
112
|
+
})
|
|
113
|
+
);
|
|
114
|
+
})() : {};
|
|
115
|
+
if (config.hooks?.onBeforeToolsActivated && Object.keys(activePageTools).length > 0) {
|
|
116
|
+
try {
|
|
117
|
+
const allowed = await config.hooks.onBeforeToolsActivated(
|
|
118
|
+
Object.keys(activePageTools),
|
|
119
|
+
routeName,
|
|
120
|
+
context
|
|
121
|
+
);
|
|
122
|
+
const allowedSet = new Set(allowed);
|
|
123
|
+
for (const key of Object.keys(activePageTools)) {
|
|
124
|
+
if (!allowedSet.has(key)) {
|
|
125
|
+
delete activePageTools[key];
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
} catch (hookError) {
|
|
129
|
+
throw ctx.error(403, {
|
|
130
|
+
message: hookError instanceof Error ? hookError.message : "Unauthorized: Tool activation denied"
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const mergedTools = Object.keys(activePageTools).length > 0 ? { ...activePageTools, ...config.tools } : config.tools;
|
|
88
135
|
if (isPublicMode) {
|
|
89
136
|
const result2 = streamText({
|
|
90
137
|
model: config.model,
|
|
91
138
|
messages: messagesWithSystem,
|
|
92
|
-
tools:
|
|
139
|
+
tools: mergedTools,
|
|
93
140
|
// Enable multi-step tool calls if tools are configured
|
|
94
|
-
...
|
|
141
|
+
...mergedTools ? { stopWhen: stepCountIs(5) } : {}
|
|
95
142
|
});
|
|
96
143
|
return result2.toUIMessageStreamResponse({
|
|
97
144
|
originalMessages: uiMessages
|
|
@@ -199,9 +246,9 @@ const aiChatBackendPlugin = (config) => defineBackendPlugin({
|
|
|
199
246
|
const result = streamText({
|
|
200
247
|
model: config.model,
|
|
201
248
|
messages: messagesWithSystem,
|
|
202
|
-
tools:
|
|
249
|
+
tools: mergedTools,
|
|
203
250
|
// Enable multi-step tool calls if tools are configured
|
|
204
|
-
...
|
|
251
|
+
...mergedTools ? { stopWhen: stepCountIs(5) } : {},
|
|
205
252
|
onFinish: async (completion) => {
|
|
206
253
|
try {
|
|
207
254
|
const assistantParts = completion.text ? [{ type: "text", text: completion.text }] : [];
|
|
@@ -184,7 +184,7 @@ function ChatInput({
|
|
|
184
184
|
}
|
|
185
185
|
)
|
|
186
186
|
] }),
|
|
187
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative flex-1", children: [
|
|
187
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative flex-1 min-w-0", children: [
|
|
188
188
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
189
189
|
textarea.Textarea,
|
|
190
190
|
{
|
|
@@ -193,7 +193,7 @@ function ChatInput({
|
|
|
193
193
|
onKeyDown: handleKeyDown,
|
|
194
194
|
placeholder: placeholder || localization.CHAT_PLACEHOLDER,
|
|
195
195
|
className: utils.cn(
|
|
196
|
-
"resize-none pr-12",
|
|
196
|
+
"resize-none pr-12 max-w-full",
|
|
197
197
|
isCompact ? "min-h-[40px] max-h-[120px] py-2" : "min-h-[50px] max-h-[200px] py-3"
|
|
198
198
|
),
|
|
199
199
|
rows: 1
|
|
@@ -182,7 +182,7 @@ function ChatInput({
|
|
|
182
182
|
}
|
|
183
183
|
)
|
|
184
184
|
] }),
|
|
185
|
-
/* @__PURE__ */ jsxs("div", { className: "relative flex-1", children: [
|
|
185
|
+
/* @__PURE__ */ jsxs("div", { className: "relative flex-1 min-w-0", children: [
|
|
186
186
|
/* @__PURE__ */ jsx(
|
|
187
187
|
Textarea,
|
|
188
188
|
{
|
|
@@ -191,7 +191,7 @@ function ChatInput({
|
|
|
191
191
|
onKeyDown: handleKeyDown,
|
|
192
192
|
placeholder: placeholder || localization.CHAT_PLACEHOLDER,
|
|
193
193
|
className: cn(
|
|
194
|
-
"resize-none pr-12",
|
|
194
|
+
"resize-none pr-12 max-w-full",
|
|
195
195
|
isCompact ? "min-h-[40px] max-h-[120px] py-2" : "min-h-[50px] max-h-[200px] py-3"
|
|
196
196
|
),
|
|
197
197
|
rows: 1
|
|
@@ -16,6 +16,7 @@ const index = require('../localization/index.cjs');
|
|
|
16
16
|
const client = require('@btst/stack/plugins/client');
|
|
17
17
|
const plugins_aiChat_queryKeys = require('../../../../../../../plugins/ai-chat/query-keys.cjs');
|
|
18
18
|
const chatHooks = require('../hooks/chat-hooks.cjs');
|
|
19
|
+
const plugins_aiChat_client_context_pageAiContext = require('../../../../../../../plugins/ai-chat/client/context/page-ai-context.cjs');
|
|
19
20
|
|
|
20
21
|
function ChatInterface({
|
|
21
22
|
apiPath = "/api/chat",
|
|
@@ -40,6 +41,7 @@ function ChatInterface({
|
|
|
40
41
|
);
|
|
41
42
|
const basePath = context.useBasePath();
|
|
42
43
|
const isPublicMode = mode === "public";
|
|
44
|
+
const pageAIContext = plugins_aiChat_client_context_pageAiContext.usePageAIContext();
|
|
43
45
|
const localization = { ...index.AI_CHAT_LOCALIZATION, ...customLocalization };
|
|
44
46
|
const queryClient = reactQuery.useQueryClient();
|
|
45
47
|
const conversationsListQueryKey = React.useMemo(() => {
|
|
@@ -83,13 +85,25 @@ function ChatInterface({
|
|
|
83
85
|
!initialMessages || initialMessages.length === 0
|
|
84
86
|
)
|
|
85
87
|
);
|
|
88
|
+
const pageAIContextRef = React.useRef(pageAIContext);
|
|
89
|
+
React.useEffect(() => {
|
|
90
|
+
pageAIContextRef.current = pageAIContext;
|
|
91
|
+
}, [pageAIContext]);
|
|
86
92
|
const transport = React.useMemo(
|
|
87
93
|
() => new ai.DefaultChatTransport({
|
|
88
94
|
api: apiPath,
|
|
89
95
|
// In public mode, don't send conversationId
|
|
90
96
|
body: isPublicMode ? void 0 : () => ({ conversationId: conversationIdRef.current }),
|
|
91
|
-
// Handle edit operations
|
|
97
|
+
// Handle edit operations and inject page context
|
|
92
98
|
prepareSendMessagesRequest: ({ messages: hookMessages }) => {
|
|
99
|
+
const currentPageContext = pageAIContextRef.current;
|
|
100
|
+
const pageContextBody = currentPageContext?.pageDescription ? {
|
|
101
|
+
pageContext: currentPageContext.pageDescription,
|
|
102
|
+
availableTools: Object.keys(
|
|
103
|
+
currentPageContext.clientTools ?? {}
|
|
104
|
+
),
|
|
105
|
+
routeName: currentPageContext.routeName
|
|
106
|
+
} : {};
|
|
93
107
|
if (editMessagesRef.current !== null) {
|
|
94
108
|
const newUserMessage = hookMessages[hookMessages.length - 1];
|
|
95
109
|
const messagesToSend = [...editMessagesRef.current];
|
|
@@ -100,22 +114,63 @@ function ChatInterface({
|
|
|
100
114
|
return {
|
|
101
115
|
body: {
|
|
102
116
|
messages: messagesToSend,
|
|
103
|
-
conversationId: conversationIdRef.current
|
|
117
|
+
conversationId: conversationIdRef.current,
|
|
118
|
+
...pageContextBody
|
|
104
119
|
}
|
|
105
120
|
};
|
|
106
121
|
}
|
|
107
122
|
return {
|
|
108
123
|
body: {
|
|
109
124
|
messages: hookMessages,
|
|
110
|
-
conversationId: conversationIdRef.current
|
|
125
|
+
conversationId: conversationIdRef.current,
|
|
126
|
+
...pageContextBody
|
|
111
127
|
}
|
|
112
128
|
};
|
|
113
129
|
}
|
|
114
130
|
}),
|
|
115
131
|
[apiPath, isPublicMode]
|
|
116
132
|
);
|
|
117
|
-
const
|
|
133
|
+
const addToolOutputRef = React.useRef(null);
|
|
134
|
+
const {
|
|
135
|
+
messages,
|
|
136
|
+
sendMessage,
|
|
137
|
+
status,
|
|
138
|
+
error,
|
|
139
|
+
setMessages,
|
|
140
|
+
regenerate,
|
|
141
|
+
addToolOutput
|
|
142
|
+
} = react.useChat({
|
|
118
143
|
transport,
|
|
144
|
+
// Automatically resubmit after all client-side tool results are provided
|
|
145
|
+
sendAutomaticallyWhen: ai.lastAssistantMessageIsCompleteWithToolCalls,
|
|
146
|
+
onToolCall: async ({ toolCall }) => {
|
|
147
|
+
const toolName = toolCall.toolName;
|
|
148
|
+
const handler = pageAIContextRef.current?.clientTools?.[toolName];
|
|
149
|
+
if (handler) {
|
|
150
|
+
try {
|
|
151
|
+
const result = await handler(toolCall.input);
|
|
152
|
+
addToolOutputRef.current?.({
|
|
153
|
+
tool: toolName,
|
|
154
|
+
toolCallId: toolCall.toolCallId,
|
|
155
|
+
output: result
|
|
156
|
+
});
|
|
157
|
+
} catch (err) {
|
|
158
|
+
addToolOutputRef.current?.({
|
|
159
|
+
tool: toolName,
|
|
160
|
+
toolCallId: toolCall.toolCallId,
|
|
161
|
+
state: "output-error",
|
|
162
|
+
errorText: err instanceof Error ? err.message : "Tool execution failed"
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
} else {
|
|
166
|
+
addToolOutputRef.current?.({
|
|
167
|
+
tool: toolName,
|
|
168
|
+
toolCallId: toolCall.toolCallId,
|
|
169
|
+
state: "output-error",
|
|
170
|
+
errorText: `No client-side handler registered for tool "${toolName}". The page context may have changed while the response was streaming.`
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
},
|
|
119
174
|
onError: (err) => {
|
|
120
175
|
console.error("useChat onError:", err);
|
|
121
176
|
if (!id && !hasNavigatedRef.current) {
|
|
@@ -138,19 +193,24 @@ function ChatInterface({
|
|
|
138
193
|
if (newConversation) {
|
|
139
194
|
setCurrentConversationId(newConversation.id);
|
|
140
195
|
conversationIdRef.current = newConversation.id;
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
window
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
196
|
+
if (variant === "full") {
|
|
197
|
+
const newUrl = `${basePath}/chat/${newConversation.id}`;
|
|
198
|
+
if (typeof window !== "undefined") {
|
|
199
|
+
window.history.replaceState(
|
|
200
|
+
{ ...window.history.state },
|
|
201
|
+
"",
|
|
202
|
+
newUrl
|
|
203
|
+
);
|
|
204
|
+
}
|
|
148
205
|
}
|
|
149
206
|
}
|
|
150
207
|
}
|
|
151
208
|
}
|
|
152
209
|
}
|
|
153
210
|
});
|
|
211
|
+
React.useEffect(() => {
|
|
212
|
+
addToolOutputRef.current = addToolOutput;
|
|
213
|
+
}, [addToolOutput]);
|
|
154
214
|
React.useEffect(() => {
|
|
155
215
|
if (isEditInProgressRef.current) {
|
|
156
216
|
return;
|
|
@@ -313,17 +373,24 @@ function ChatInterface({
|
|
|
313
373
|
),
|
|
314
374
|
children: [
|
|
315
375
|
messages.length === 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col h-full min-h-[300px]", children: [
|
|
316
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 flex items-center justify-center text-muted-foreground", children: /* @__PURE__ */ jsxRuntime.jsx("p", { children: localization.CHAT_EMPTY_STATE }) }),
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
376
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 flex items-center justify-center text-muted-foreground mb-4", children: /* @__PURE__ */ jsxRuntime.jsx("p", { children: localization.CHAT_EMPTY_STATE }) }),
|
|
377
|
+
(() => {
|
|
378
|
+
const pageSuggestions = pageAIContext?.suggestions ?? [];
|
|
379
|
+
const allSuggestions = [
|
|
380
|
+
...pageSuggestions,
|
|
381
|
+
...chatSuggestions ?? []
|
|
382
|
+
];
|
|
383
|
+
return allSuggestions.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap justify-center gap-2 pb-4 max-w-md mx-auto", children: allSuggestions.map((suggestion, index) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
384
|
+
"button",
|
|
385
|
+
{
|
|
386
|
+
type: "button",
|
|
387
|
+
onClick: () => setInput(suggestion),
|
|
388
|
+
className: "px-3 py-2 text-sm rounded-lg border border-border bg-background hover:bg-accent hover:text-accent-foreground transition-colors text-foreground",
|
|
389
|
+
children: suggestion
|
|
390
|
+
},
|
|
391
|
+
index
|
|
392
|
+
)) }) : null;
|
|
393
|
+
})()
|
|
327
394
|
] }) : messages.map((m, index) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
328
395
|
chatMessage.ChatMessage,
|
|
329
396
|
{
|