@btst/stack 2.3.0 → 2.5.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.
Files changed (208) hide show
  1. package/dist/packages/stack/src/client/components/compose.cjs +1 -2
  2. package/dist/packages/stack/src/client/components/compose.mjs +1 -2
  3. package/dist/packages/stack/src/plugins/ai-chat/api/page-tools.cjs +71 -0
  4. package/dist/packages/stack/src/plugins/ai-chat/api/page-tools.mjs +68 -0
  5. package/dist/packages/stack/src/plugins/ai-chat/api/plugin.cjs +87 -54
  6. package/dist/packages/stack/src/plugins/ai-chat/api/plugin.mjs +87 -54
  7. package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-input.cjs +2 -2
  8. package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-input.mjs +2 -2
  9. package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-interface.cjs +89 -22
  10. package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-interface.mjs +90 -23
  11. package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-layout.cjs +110 -33
  12. package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-layout.mjs +112 -35
  13. package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-sidebar.cjs +1 -1
  14. package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-sidebar.mjs +1 -1
  15. package/dist/packages/stack/src/plugins/ai-chat/client/plugin.cjs +14 -21
  16. package/dist/packages/stack/src/plugins/ai-chat/client/plugin.mjs +15 -22
  17. package/dist/packages/stack/src/plugins/ai-chat/schemas.cjs +17 -1
  18. package/dist/packages/stack/src/plugins/ai-chat/schemas.mjs +17 -1
  19. package/dist/packages/stack/src/plugins/blog/api/plugin.cjs +28 -45
  20. package/dist/packages/stack/src/plugins/blog/api/plugin.mjs +22 -39
  21. package/dist/packages/stack/src/plugins/blog/client/components/forms/post-forms.cjs +15 -2
  22. package/dist/packages/stack/src/plugins/blog/client/components/forms/post-forms.mjs +16 -3
  23. package/dist/packages/stack/src/plugins/blog/client/components/pages/edit-post-page.internal.cjs +24 -1
  24. package/dist/packages/stack/src/plugins/blog/client/components/pages/edit-post-page.internal.mjs +24 -1
  25. package/dist/packages/stack/src/plugins/blog/client/components/pages/fill-blog-form-handler.cjs +26 -0
  26. package/dist/packages/stack/src/plugins/blog/client/components/pages/fill-blog-form-handler.mjs +24 -0
  27. package/dist/packages/stack/src/plugins/blog/client/components/pages/new-post-page.internal.cjs +30 -1
  28. package/dist/packages/stack/src/plugins/blog/client/components/pages/new-post-page.internal.mjs +30 -1
  29. package/dist/packages/stack/src/plugins/blog/client/components/pages/post-page.internal.cjs +18 -0
  30. package/dist/packages/stack/src/plugins/blog/client/components/pages/post-page.internal.mjs +18 -0
  31. package/dist/packages/stack/src/plugins/blog/client/plugin.cjs +23 -27
  32. package/dist/packages/stack/src/plugins/blog/client/plugin.mjs +24 -28
  33. package/dist/packages/stack/src/plugins/cms/api/mutations.cjs +48 -0
  34. package/dist/packages/stack/src/plugins/cms/api/mutations.mjs +46 -0
  35. package/dist/packages/stack/src/plugins/cms/api/plugin.cjs +21 -18
  36. package/dist/packages/stack/src/plugins/cms/api/plugin.mjs +21 -18
  37. package/dist/packages/stack/src/plugins/cms/client/plugin.cjs +11 -15
  38. package/dist/packages/stack/src/plugins/cms/client/plugin.mjs +12 -16
  39. package/dist/packages/stack/src/plugins/form-builder/api/plugin.cjs +58 -62
  40. package/dist/packages/stack/src/plugins/form-builder/api/plugin.mjs +58 -62
  41. package/dist/packages/stack/src/plugins/form-builder/client/plugin.cjs +12 -12
  42. package/dist/packages/stack/src/plugins/form-builder/client/plugin.mjs +13 -13
  43. package/dist/packages/stack/src/plugins/kanban/api/mutations.cjs +91 -0
  44. package/dist/packages/stack/src/plugins/kanban/api/mutations.mjs +87 -0
  45. package/dist/packages/stack/src/plugins/kanban/api/plugin.cjs +92 -118
  46. package/dist/packages/stack/src/plugins/kanban/api/plugin.mjs +89 -115
  47. package/dist/packages/stack/src/plugins/kanban/client/hooks/kanban-hooks.cjs +7 -3
  48. package/dist/packages/stack/src/plugins/kanban/client/hooks/kanban-hooks.mjs +7 -3
  49. package/dist/packages/stack/src/plugins/kanban/client/plugin.cjs +22 -29
  50. package/dist/packages/stack/src/plugins/kanban/client/plugin.mjs +23 -30
  51. package/dist/packages/stack/src/plugins/ui-builder/client/components/pages/page-builder-page.internal.cjs +89 -0
  52. package/dist/packages/stack/src/plugins/ui-builder/client/components/pages/page-builder-page.internal.mjs +89 -0
  53. package/dist/packages/stack/src/plugins/ui-builder/client/plugin.cjs +8 -8
  54. package/dist/packages/stack/src/plugins/ui-builder/client/plugin.mjs +9 -9
  55. package/dist/packages/stack/src/plugins/utils.cjs +42 -0
  56. package/dist/packages/stack/src/plugins/utils.mjs +41 -1
  57. package/dist/plugins/ai-chat/api/index.d.cts +1 -1
  58. package/dist/plugins/ai-chat/api/index.d.mts +1 -1
  59. package/dist/plugins/ai-chat/api/index.d.ts +1 -1
  60. package/dist/plugins/ai-chat/client/components/index.d.cts +1 -1
  61. package/dist/plugins/ai-chat/client/components/index.d.mts +1 -1
  62. package/dist/plugins/ai-chat/client/components/index.d.ts +1 -1
  63. package/dist/plugins/ai-chat/client/context/page-ai-context.cjs +92 -0
  64. package/dist/plugins/ai-chat/client/context/page-ai-context.d.cts +84 -0
  65. package/dist/plugins/ai-chat/client/context/page-ai-context.d.mts +84 -0
  66. package/dist/plugins/ai-chat/client/context/page-ai-context.d.ts +84 -0
  67. package/dist/plugins/ai-chat/client/context/page-ai-context.mjs +88 -0
  68. package/dist/plugins/ai-chat/client/hooks/index.d.cts +1 -1
  69. package/dist/plugins/ai-chat/client/hooks/index.d.mts +1 -1
  70. package/dist/plugins/ai-chat/client/hooks/index.d.ts +1 -1
  71. package/dist/plugins/ai-chat/client/index.d.cts +10 -10
  72. package/dist/plugins/ai-chat/client/index.d.mts +10 -10
  73. package/dist/plugins/ai-chat/client/index.d.ts +10 -10
  74. package/dist/plugins/ai-chat/query-keys.d.cts +1 -1
  75. package/dist/plugins/ai-chat/query-keys.d.mts +1 -1
  76. package/dist/plugins/ai-chat/query-keys.d.ts +1 -1
  77. package/dist/plugins/blog/api/index.d.cts +2 -2
  78. package/dist/plugins/blog/api/index.d.mts +2 -2
  79. package/dist/plugins/blog/api/index.d.ts +2 -2
  80. package/dist/plugins/blog/client/hooks/index.d.cts +2 -2
  81. package/dist/plugins/blog/client/hooks/index.d.mts +2 -2
  82. package/dist/plugins/blog/client/hooks/index.d.ts +2 -2
  83. package/dist/plugins/blog/client/index.d.cts +13 -13
  84. package/dist/plugins/blog/client/index.d.mts +13 -13
  85. package/dist/plugins/blog/client/index.d.ts +13 -13
  86. package/dist/plugins/blog/query-keys.d.cts +2 -2
  87. package/dist/plugins/blog/query-keys.d.mts +2 -2
  88. package/dist/plugins/blog/query-keys.d.ts +2 -2
  89. package/dist/plugins/client/index.cjs +1 -0
  90. package/dist/plugins/client/index.d.cts +8 -1
  91. package/dist/plugins/client/index.d.mts +8 -1
  92. package/dist/plugins/client/index.d.ts +8 -1
  93. package/dist/plugins/client/index.mjs +1 -1
  94. package/dist/plugins/cms/api/index.cjs +2 -0
  95. package/dist/plugins/cms/api/index.d.cts +2 -2
  96. package/dist/plugins/cms/api/index.d.mts +2 -2
  97. package/dist/plugins/cms/api/index.d.ts +2 -2
  98. package/dist/plugins/cms/api/index.mjs +1 -0
  99. package/dist/plugins/cms/client/hooks/index.d.cts +1 -1
  100. package/dist/plugins/cms/client/hooks/index.d.mts +1 -1
  101. package/dist/plugins/cms/client/hooks/index.d.ts +1 -1
  102. package/dist/plugins/cms/client/index.d.cts +6 -6
  103. package/dist/plugins/cms/client/index.d.mts +6 -6
  104. package/dist/plugins/cms/client/index.d.ts +6 -6
  105. package/dist/plugins/cms/query-keys.d.cts +2 -2
  106. package/dist/plugins/cms/query-keys.d.mts +2 -2
  107. package/dist/plugins/cms/query-keys.d.ts +2 -2
  108. package/dist/plugins/form-builder/api/index.d.cts +2 -2
  109. package/dist/plugins/form-builder/api/index.d.mts +2 -2
  110. package/dist/plugins/form-builder/api/index.d.ts +2 -2
  111. package/dist/plugins/form-builder/client/components/index.d.cts +1 -1
  112. package/dist/plugins/form-builder/client/components/index.d.mts +1 -1
  113. package/dist/plugins/form-builder/client/components/index.d.ts +1 -1
  114. package/dist/plugins/form-builder/client/hooks/index.d.cts +1 -1
  115. package/dist/plugins/form-builder/client/hooks/index.d.mts +1 -1
  116. package/dist/plugins/form-builder/client/hooks/index.d.ts +1 -1
  117. package/dist/plugins/form-builder/client/index.d.cts +6 -6
  118. package/dist/plugins/form-builder/client/index.d.mts +6 -6
  119. package/dist/plugins/form-builder/client/index.d.ts +6 -6
  120. package/dist/plugins/form-builder/query-keys.d.cts +2 -2
  121. package/dist/plugins/form-builder/query-keys.d.mts +2 -2
  122. package/dist/plugins/form-builder/query-keys.d.ts +2 -2
  123. package/dist/plugins/kanban/api/index.cjs +4 -0
  124. package/dist/plugins/kanban/api/index.d.cts +1 -1
  125. package/dist/plugins/kanban/api/index.d.mts +1 -1
  126. package/dist/plugins/kanban/api/index.d.ts +1 -1
  127. package/dist/plugins/kanban/api/index.mjs +1 -0
  128. package/dist/plugins/kanban/client/index.d.cts +12 -12
  129. package/dist/plugins/kanban/client/index.d.mts +12 -12
  130. package/dist/plugins/kanban/client/index.d.ts +12 -12
  131. package/dist/plugins/kanban/query-keys.d.cts +1 -1
  132. package/dist/plugins/kanban/query-keys.d.mts +1 -1
  133. package/dist/plugins/kanban/query-keys.d.ts +1 -1
  134. package/dist/plugins/ui-builder/client/hooks/index.d.cts +1 -1
  135. package/dist/plugins/ui-builder/client/hooks/index.d.mts +1 -1
  136. package/dist/plugins/ui-builder/client/hooks/index.d.ts +1 -1
  137. package/dist/plugins/ui-builder/client/index.d.cts +3 -3
  138. package/dist/plugins/ui-builder/client/index.d.mts +3 -3
  139. package/dist/plugins/ui-builder/client/index.d.ts +3 -3
  140. package/dist/plugins/ui-builder/index.d.cts +2 -2
  141. package/dist/plugins/ui-builder/index.d.mts +2 -2
  142. package/dist/plugins/ui-builder/index.d.ts +2 -2
  143. package/dist/shared/{stack.C-WUPMT6.d.cts → stack.B2xZTSiO.d.cts} +4 -4
  144. package/dist/shared/{stack.B1EeBt1b.d.ts → stack.B58oHdqm.d.mts} +33 -3
  145. package/dist/shared/{stack.CVDTkMoO.d.mts → stack.B8QD11QU.d.cts} +7 -7
  146. package/dist/shared/{stack.CVDTkMoO.d.cts → stack.B8QD11QU.d.mts} +7 -7
  147. package/dist/shared/{stack.CVDTkMoO.d.ts → stack.B8QD11QU.d.ts} +7 -7
  148. package/dist/shared/{stack.CIP6QS9l.d.ts → stack.BDVEpue1.d.ts} +1 -1
  149. package/dist/shared/{stack.C5dtIncc.d.mts → stack.BTvbxZvw.d.cts} +1 -1
  150. package/dist/shared/{stack.DaOcgmrM.d.ts → stack.BV9hnvu4.d.cts} +31 -7
  151. package/dist/shared/{stack.DaOcgmrM.d.cts → stack.BV9hnvu4.d.mts} +31 -7
  152. package/dist/shared/{stack.DaOcgmrM.d.mts → stack.BV9hnvu4.d.ts} +31 -7
  153. package/dist/shared/{stack.DdI5W6MB.d.mts → stack.BozPgbrZ.d.cts} +19 -19
  154. package/dist/shared/{stack.DdI5W6MB.d.ts → stack.BozPgbrZ.d.mts} +19 -19
  155. package/dist/shared/{stack.DdI5W6MB.d.cts → stack.BozPgbrZ.d.ts} +19 -19
  156. package/dist/shared/{stack.CP68pFEH.d.mts → stack.C9Mg2Q46.d.cts} +33 -3
  157. package/dist/shared/{stack.BeSm90va.d.ts → stack.CTDVxbrA.d.ts} +72 -14
  158. package/dist/shared/{stack.C-Ptrz8s.d.ts → stack.Cj_zKww4.d.ts} +4 -4
  159. package/dist/shared/{stack.TIBF2AOx.d.ts → stack.CxaFNQCV.d.mts} +89 -34
  160. package/dist/shared/{stack.CMh_EdxW.d.cts → stack.D-b5zbPm.d.cts} +72 -14
  161. package/dist/shared/{stack.Dw0Ly2TM.d.cts → stack.DTtmJPQO.d.mts} +1 -1
  162. package/dist/shared/{stack.BKfolAyK.d.ts → stack.DXnclTG7.d.ts} +11 -11
  163. package/dist/shared/{stack.snB1EDP7.d.cts → stack.DaZM10cp.d.cts} +11 -11
  164. package/dist/shared/{stack.Dg09R0oB.d.mts → stack.FVWf2JhZ.d.mts} +72 -14
  165. package/dist/shared/{stack.BIXEI6v_.d.mts → stack.cfCkioTe.d.mts} +11 -11
  166. package/dist/shared/{stack.6fUOjLs9.d.mts → stack.dH7u-TJH.d.mts} +4 -4
  167. package/dist/shared/{stack.BpolpQpf.d.cts → stack.j75TpKh2.d.ts} +89 -34
  168. package/dist/shared/{stack.rTy7-wQU.d.mts → stack.n1_i1p2B.d.cts} +89 -34
  169. package/dist/shared/{stack.IdtKDRka.d.cts → stack.sO33ZDhK.d.ts} +33 -3
  170. package/package.json +14 -1
  171. package/src/client/components/compose.tsx +7 -4
  172. package/src/plugins/ai-chat/api/page-tools.ts +111 -0
  173. package/src/plugins/ai-chat/api/plugin.ts +228 -72
  174. package/src/plugins/ai-chat/client/components/chat-input.tsx +2 -2
  175. package/src/plugins/ai-chat/client/components/chat-interface.tsx +154 -58
  176. package/src/plugins/ai-chat/client/components/chat-layout.tsx +166 -32
  177. package/src/plugins/ai-chat/client/components/chat-sidebar.tsx +1 -1
  178. package/src/plugins/ai-chat/client/context/page-ai-context.tsx +240 -0
  179. package/src/plugins/ai-chat/client/plugin.tsx +23 -31
  180. package/src/plugins/ai-chat/schemas.ts +16 -0
  181. package/src/plugins/blog/api/plugin.ts +31 -47
  182. package/src/plugins/blog/client/components/forms/post-forms.tsx +29 -2
  183. package/src/plugins/blog/client/components/pages/edit-post-page.internal.tsx +28 -0
  184. package/src/plugins/blog/client/components/pages/fill-blog-form-handler.ts +38 -0
  185. package/src/plugins/blog/client/components/pages/new-post-page.internal.tsx +33 -1
  186. package/src/plugins/blog/client/components/pages/post-page.internal.tsx +20 -0
  187. package/src/plugins/blog/client/plugin.tsx +36 -39
  188. package/src/plugins/client/index.ts +5 -1
  189. package/src/plugins/cms/api/index.ts +4 -0
  190. package/src/plugins/cms/api/mutations.ts +84 -0
  191. package/src/plugins/cms/api/plugin.ts +23 -17
  192. package/src/plugins/cms/client/plugin.tsx +18 -21
  193. package/src/plugins/cms/types.ts +7 -7
  194. package/src/plugins/form-builder/api/plugin.ts +64 -64
  195. package/src/plugins/form-builder/client/plugin.tsx +19 -18
  196. package/src/plugins/form-builder/types.ts +19 -24
  197. package/src/plugins/kanban/api/index.ts +6 -0
  198. package/src/plugins/kanban/api/mutations.ts +169 -0
  199. package/src/plugins/kanban/api/plugin.ts +123 -136
  200. package/src/plugins/kanban/client/hooks/kanban-hooks.tsx +4 -0
  201. package/src/plugins/kanban/client/plugin.tsx +35 -41
  202. package/src/plugins/ui-builder/client/components/pages/page-builder-page.internal.tsx +132 -0
  203. package/src/plugins/ui-builder/client/plugin.tsx +11 -10
  204. package/src/plugins/ui-builder/types.ts +4 -4
  205. package/src/plugins/utils.ts +92 -1
  206. package/dist/shared/{stack.CBON0dWL.d.mts → stack.BQmuNl5p.d.cts} +2 -2
  207. package/dist/shared/{stack.CBON0dWL.d.ts → stack.BQmuNl5p.d.mts} +2 -2
  208. package/dist/shared/{stack.CBON0dWL.d.cts → stack.BQmuNl5p.d.ts} +2 -2
@@ -40,8 +40,7 @@ function ComposedRoute({
40
40
  }) {
41
41
  if (PageComponent) {
42
42
  const content = /* @__PURE__ */ jsxRuntime.jsx(PageComponent, { ...props });
43
- const isBrowser = typeof window !== "undefined";
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 isBrowser = typeof window !== "undefined";
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,8 @@ 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');
9
+ const utils = require('../../utils.cjs');
8
10
 
9
11
  const aiChatBackendPlugin = (config) => api.defineBackendPlugin({
10
12
  name: "ai-chat",
@@ -52,7 +54,13 @@ const aiChatBackendPlugin = (config) => api.defineBackendPlugin({
52
54
  body: schemas.chatRequestSchema
53
55
  },
54
56
  async (ctx) => {
55
- const { messages: rawMessages, conversationId } = ctx.body;
57
+ const {
58
+ messages: rawMessages,
59
+ conversationId,
60
+ pageContext,
61
+ availableTools,
62
+ routeName
63
+ } = ctx.body;
56
64
  const uiMessages = rawMessages;
57
65
  const context = {
58
66
  body: ctx.body,
@@ -65,15 +73,11 @@ const aiChatBackendPlugin = (config) => api.defineBackendPlugin({
65
73
  role: msg.role,
66
74
  content: getMessageTextContent(msg)
67
75
  }));
68
- const canChat = await config.hooks.onBeforeChat(
69
- messagesForHook,
70
- context
76
+ await utils.runHookWithShim(
77
+ () => config.hooks.onBeforeChat(messagesForHook, context),
78
+ ctx.error,
79
+ "Unauthorized: Cannot start chat"
71
80
  );
72
- if (!canChat) {
73
- throw ctx.error(403, {
74
- message: "Unauthorized: Cannot start chat"
75
- });
76
- }
77
81
  }
78
82
  const firstMessage = uiMessages[0];
79
83
  if (!firstMessage) {
@@ -83,17 +87,57 @@ const aiChatBackendPlugin = (config) => api.defineBackendPlugin({
83
87
  }
84
88
  const firstMessageContent = getMessageTextContent(firstMessage);
85
89
  const modelMessages = ai.convertToModelMessages(uiMessages);
86
- const messagesWithSystem = config.systemPrompt ? [
87
- { role: "system", content: config.systemPrompt },
90
+ const pageContextContent = pageContext && pageContext.trim() ? `
91
+
92
+ Current page context:
93
+ ${pageContext}` : "";
94
+ const systemContent = config.systemPrompt ? `${config.systemPrompt}${pageContextContent}` : pageContextContent || void 0;
95
+ const messagesWithSystem = systemContent ? [
96
+ { role: "system", content: systemContent },
88
97
  ...modelMessages
89
98
  ] : modelMessages;
99
+ const activePageTools = config.enablePageTools && availableTools && availableTools.length > 0 ? (() => {
100
+ const consumerSchemas = config.clientToolSchemas ?? {};
101
+ return Object.fromEntries(
102
+ availableTools.filter((name) => {
103
+ if (name in pageTools.BUILT_IN_PAGE_TOOL_SCHEMAS) {
104
+ const allowed = pageTools.BUILT_IN_PAGE_TOOL_ROUTE_ALLOWLIST[name];
105
+ return allowed && routeName && allowed.includes(routeName);
106
+ }
107
+ return name in consumerSchemas;
108
+ }).map((name) => {
109
+ const schema = pageTools.BUILT_IN_PAGE_TOOL_SCHEMAS[name] ?? consumerSchemas[name];
110
+ return [name, schema];
111
+ })
112
+ );
113
+ })() : {};
114
+ if (config.hooks?.onBeforeToolsActivated && Object.keys(activePageTools).length > 0) {
115
+ try {
116
+ const allowed = await config.hooks.onBeforeToolsActivated(
117
+ Object.keys(activePageTools),
118
+ routeName,
119
+ context
120
+ );
121
+ const allowedSet = new Set(allowed);
122
+ for (const key of Object.keys(activePageTools)) {
123
+ if (!allowedSet.has(key)) {
124
+ delete activePageTools[key];
125
+ }
126
+ }
127
+ } catch (hookError) {
128
+ throw ctx.error(403, {
129
+ message: hookError instanceof Error ? hookError.message : "Unauthorized: Tool activation denied"
130
+ });
131
+ }
132
+ }
133
+ const mergedTools = Object.keys(activePageTools).length > 0 ? { ...activePageTools, ...config.tools } : config.tools;
90
134
  if (isPublicMode) {
91
135
  const result2 = ai.streamText({
92
136
  model: config.model,
93
137
  messages: messagesWithSystem,
94
- tools: config.tools,
138
+ tools: mergedTools,
95
139
  // Enable multi-step tool calls if tools are configured
96
- ...config.tools ? { stopWhen: ai.stepCountIs(5) } : {}
140
+ ...mergedTools ? { stopWhen: ai.stepCountIs(5) } : {}
97
141
  });
98
142
  return result2.toUIMessageStreamResponse({
99
143
  originalMessages: uiMessages
@@ -201,9 +245,9 @@ const aiChatBackendPlugin = (config) => api.defineBackendPlugin({
201
245
  const result = ai.streamText({
202
246
  model: config.model,
203
247
  messages: messagesWithSystem,
204
- tools: config.tools,
248
+ tools: mergedTools,
205
249
  // Enable multi-step tool calls if tools are configured
206
- ...config.tools ? { stopWhen: ai.stepCountIs(5) } : {},
250
+ ...mergedTools ? { stopWhen: ai.stepCountIs(5) } : {},
207
251
  onFinish: async (completion) => {
208
252
  try {
209
253
  const assistantParts = completion.text ? [{ type: "text", text: completion.text }] : [];
@@ -296,15 +340,14 @@ const aiChatBackendPlugin = (config) => api.defineBackendPlugin({
296
340
  });
297
341
  });
298
342
  if (config.hooks?.onBeforeCreateConversation) {
299
- const canCreate = await config.hooks.onBeforeCreateConversation(
300
- { id, title },
301
- context
343
+ await utils.runHookWithShim(
344
+ () => config.hooks.onBeforeCreateConversation(
345
+ { id, title },
346
+ context
347
+ ),
348
+ ctx.error,
349
+ "Unauthorized: Cannot create conversation"
302
350
  );
303
- if (!canCreate) {
304
- throw ctx.error(403, {
305
- message: "Unauthorized: Cannot create conversation"
306
- });
307
- }
308
351
  }
309
352
  const newConv = await adapter.create({
310
353
  model: "conversation",
@@ -350,12 +393,11 @@ const aiChatBackendPlugin = (config) => api.defineBackendPlugin({
350
393
  });
351
394
  });
352
395
  if (config.hooks?.onBeforeListConversations) {
353
- const canList = await config.hooks.onBeforeListConversations(context);
354
- if (!canList) {
355
- throw ctx.error(403, {
356
- message: "Unauthorized: Cannot list conversations"
357
- });
358
- }
396
+ await utils.runHookWithShim(
397
+ () => config.hooks.onBeforeListConversations(context),
398
+ ctx.error,
399
+ "Unauthorized: Cannot list conversations"
400
+ );
359
401
  }
360
402
  const whereConditions = [];
361
403
  if (userId) {
@@ -408,15 +450,11 @@ const aiChatBackendPlugin = (config) => api.defineBackendPlugin({
408
450
  });
409
451
  });
410
452
  if (config.hooks?.onBeforeGetConversation) {
411
- const canGet = await config.hooks.onBeforeGetConversation(
412
- id,
413
- context
453
+ await utils.runHookWithShim(
454
+ () => config.hooks.onBeforeGetConversation(id, context),
455
+ ctx.error,
456
+ "Unauthorized: Cannot get conversation"
414
457
  );
415
- if (!canGet) {
416
- throw ctx.error(403, {
417
- message: "Unauthorized: Cannot get conversation"
418
- });
419
- }
420
458
  }
421
459
  const conversations = await adapter.findMany({
422
460
  model: "conversation",
@@ -498,16 +536,15 @@ const aiChatBackendPlugin = (config) => api.defineBackendPlugin({
498
536
  });
499
537
  }
500
538
  if (config.hooks?.onBeforeUpdateConversation) {
501
- const canUpdate = await config.hooks.onBeforeUpdateConversation(
502
- id,
503
- { title },
504
- context
539
+ await utils.runHookWithShim(
540
+ () => config.hooks.onBeforeUpdateConversation(
541
+ id,
542
+ { title },
543
+ context
544
+ ),
545
+ ctx.error,
546
+ "Unauthorized: Cannot update conversation"
505
547
  );
506
- if (!canUpdate) {
507
- throw ctx.error(403, {
508
- message: "Unauthorized: Cannot update conversation"
509
- });
510
- }
511
548
  }
512
549
  const updated = await adapter.update({
513
550
  model: "conversation",
@@ -572,15 +609,11 @@ const aiChatBackendPlugin = (config) => api.defineBackendPlugin({
572
609
  });
573
610
  }
574
611
  if (config.hooks?.onBeforeDeleteConversation) {
575
- const canDelete = await config.hooks.onBeforeDeleteConversation(
576
- id,
577
- context
612
+ await utils.runHookWithShim(
613
+ () => config.hooks.onBeforeDeleteConversation(id, context),
614
+ ctx.error,
615
+ "Unauthorized: Cannot delete conversation"
578
616
  );
579
- if (!canDelete) {
580
- throw ctx.error(403, {
581
- message: "Unauthorized: Cannot delete conversation"
582
- });
583
- }
584
617
  }
585
618
  await adapter.delete({
586
619
  model: "conversation",
@@ -3,6 +3,8 @@ 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';
7
+ import { runHookWithShim } from '../../utils.mjs';
6
8
 
7
9
  const aiChatBackendPlugin = (config) => defineBackendPlugin({
8
10
  name: "ai-chat",
@@ -50,7 +52,13 @@ const aiChatBackendPlugin = (config) => defineBackendPlugin({
50
52
  body: chatRequestSchema
51
53
  },
52
54
  async (ctx) => {
53
- const { messages: rawMessages, conversationId } = ctx.body;
55
+ const {
56
+ messages: rawMessages,
57
+ conversationId,
58
+ pageContext,
59
+ availableTools,
60
+ routeName
61
+ } = ctx.body;
54
62
  const uiMessages = rawMessages;
55
63
  const context = {
56
64
  body: ctx.body,
@@ -63,15 +71,11 @@ const aiChatBackendPlugin = (config) => defineBackendPlugin({
63
71
  role: msg.role,
64
72
  content: getMessageTextContent(msg)
65
73
  }));
66
- const canChat = await config.hooks.onBeforeChat(
67
- messagesForHook,
68
- context
74
+ await runHookWithShim(
75
+ () => config.hooks.onBeforeChat(messagesForHook, context),
76
+ ctx.error,
77
+ "Unauthorized: Cannot start chat"
69
78
  );
70
- if (!canChat) {
71
- throw ctx.error(403, {
72
- message: "Unauthorized: Cannot start chat"
73
- });
74
- }
75
79
  }
76
80
  const firstMessage = uiMessages[0];
77
81
  if (!firstMessage) {
@@ -81,17 +85,57 @@ const aiChatBackendPlugin = (config) => defineBackendPlugin({
81
85
  }
82
86
  const firstMessageContent = getMessageTextContent(firstMessage);
83
87
  const modelMessages = convertToModelMessages(uiMessages);
84
- const messagesWithSystem = config.systemPrompt ? [
85
- { role: "system", content: config.systemPrompt },
88
+ const pageContextContent = pageContext && pageContext.trim() ? `
89
+
90
+ Current page context:
91
+ ${pageContext}` : "";
92
+ const systemContent = config.systemPrompt ? `${config.systemPrompt}${pageContextContent}` : pageContextContent || void 0;
93
+ const messagesWithSystem = systemContent ? [
94
+ { role: "system", content: systemContent },
86
95
  ...modelMessages
87
96
  ] : modelMessages;
97
+ const activePageTools = config.enablePageTools && availableTools && availableTools.length > 0 ? (() => {
98
+ const consumerSchemas = config.clientToolSchemas ?? {};
99
+ return Object.fromEntries(
100
+ availableTools.filter((name) => {
101
+ if (name in BUILT_IN_PAGE_TOOL_SCHEMAS) {
102
+ const allowed = BUILT_IN_PAGE_TOOL_ROUTE_ALLOWLIST[name];
103
+ return allowed && routeName && allowed.includes(routeName);
104
+ }
105
+ return name in consumerSchemas;
106
+ }).map((name) => {
107
+ const schema = BUILT_IN_PAGE_TOOL_SCHEMAS[name] ?? consumerSchemas[name];
108
+ return [name, schema];
109
+ })
110
+ );
111
+ })() : {};
112
+ if (config.hooks?.onBeforeToolsActivated && Object.keys(activePageTools).length > 0) {
113
+ try {
114
+ const allowed = await config.hooks.onBeforeToolsActivated(
115
+ Object.keys(activePageTools),
116
+ routeName,
117
+ context
118
+ );
119
+ const allowedSet = new Set(allowed);
120
+ for (const key of Object.keys(activePageTools)) {
121
+ if (!allowedSet.has(key)) {
122
+ delete activePageTools[key];
123
+ }
124
+ }
125
+ } catch (hookError) {
126
+ throw ctx.error(403, {
127
+ message: hookError instanceof Error ? hookError.message : "Unauthorized: Tool activation denied"
128
+ });
129
+ }
130
+ }
131
+ const mergedTools = Object.keys(activePageTools).length > 0 ? { ...activePageTools, ...config.tools } : config.tools;
88
132
  if (isPublicMode) {
89
133
  const result2 = streamText({
90
134
  model: config.model,
91
135
  messages: messagesWithSystem,
92
- tools: config.tools,
136
+ tools: mergedTools,
93
137
  // Enable multi-step tool calls if tools are configured
94
- ...config.tools ? { stopWhen: stepCountIs(5) } : {}
138
+ ...mergedTools ? { stopWhen: stepCountIs(5) } : {}
95
139
  });
96
140
  return result2.toUIMessageStreamResponse({
97
141
  originalMessages: uiMessages
@@ -199,9 +243,9 @@ const aiChatBackendPlugin = (config) => defineBackendPlugin({
199
243
  const result = streamText({
200
244
  model: config.model,
201
245
  messages: messagesWithSystem,
202
- tools: config.tools,
246
+ tools: mergedTools,
203
247
  // Enable multi-step tool calls if tools are configured
204
- ...config.tools ? { stopWhen: stepCountIs(5) } : {},
248
+ ...mergedTools ? { stopWhen: stepCountIs(5) } : {},
205
249
  onFinish: async (completion) => {
206
250
  try {
207
251
  const assistantParts = completion.text ? [{ type: "text", text: completion.text }] : [];
@@ -294,15 +338,14 @@ const aiChatBackendPlugin = (config) => defineBackendPlugin({
294
338
  });
295
339
  });
296
340
  if (config.hooks?.onBeforeCreateConversation) {
297
- const canCreate = await config.hooks.onBeforeCreateConversation(
298
- { id, title },
299
- context
341
+ await runHookWithShim(
342
+ () => config.hooks.onBeforeCreateConversation(
343
+ { id, title },
344
+ context
345
+ ),
346
+ ctx.error,
347
+ "Unauthorized: Cannot create conversation"
300
348
  );
301
- if (!canCreate) {
302
- throw ctx.error(403, {
303
- message: "Unauthorized: Cannot create conversation"
304
- });
305
- }
306
349
  }
307
350
  const newConv = await adapter.create({
308
351
  model: "conversation",
@@ -348,12 +391,11 @@ const aiChatBackendPlugin = (config) => defineBackendPlugin({
348
391
  });
349
392
  });
350
393
  if (config.hooks?.onBeforeListConversations) {
351
- const canList = await config.hooks.onBeforeListConversations(context);
352
- if (!canList) {
353
- throw ctx.error(403, {
354
- message: "Unauthorized: Cannot list conversations"
355
- });
356
- }
394
+ await runHookWithShim(
395
+ () => config.hooks.onBeforeListConversations(context),
396
+ ctx.error,
397
+ "Unauthorized: Cannot list conversations"
398
+ );
357
399
  }
358
400
  const whereConditions = [];
359
401
  if (userId) {
@@ -406,15 +448,11 @@ const aiChatBackendPlugin = (config) => defineBackendPlugin({
406
448
  });
407
449
  });
408
450
  if (config.hooks?.onBeforeGetConversation) {
409
- const canGet = await config.hooks.onBeforeGetConversation(
410
- id,
411
- context
451
+ await runHookWithShim(
452
+ () => config.hooks.onBeforeGetConversation(id, context),
453
+ ctx.error,
454
+ "Unauthorized: Cannot get conversation"
412
455
  );
413
- if (!canGet) {
414
- throw ctx.error(403, {
415
- message: "Unauthorized: Cannot get conversation"
416
- });
417
- }
418
456
  }
419
457
  const conversations = await adapter.findMany({
420
458
  model: "conversation",
@@ -496,16 +534,15 @@ const aiChatBackendPlugin = (config) => defineBackendPlugin({
496
534
  });
497
535
  }
498
536
  if (config.hooks?.onBeforeUpdateConversation) {
499
- const canUpdate = await config.hooks.onBeforeUpdateConversation(
500
- id,
501
- { title },
502
- context
537
+ await runHookWithShim(
538
+ () => config.hooks.onBeforeUpdateConversation(
539
+ id,
540
+ { title },
541
+ context
542
+ ),
543
+ ctx.error,
544
+ "Unauthorized: Cannot update conversation"
503
545
  );
504
- if (!canUpdate) {
505
- throw ctx.error(403, {
506
- message: "Unauthorized: Cannot update conversation"
507
- });
508
- }
509
546
  }
510
547
  const updated = await adapter.update({
511
548
  model: "conversation",
@@ -570,15 +607,11 @@ const aiChatBackendPlugin = (config) => defineBackendPlugin({
570
607
  });
571
608
  }
572
609
  if (config.hooks?.onBeforeDeleteConversation) {
573
- const canDelete = await config.hooks.onBeforeDeleteConversation(
574
- id,
575
- context
610
+ await runHookWithShim(
611
+ () => config.hooks.onBeforeDeleteConversation(id, context),
612
+ ctx.error,
613
+ "Unauthorized: Cannot delete conversation"
576
614
  );
577
- if (!canDelete) {
578
- throw ctx.error(403, {
579
- message: "Unauthorized: Cannot delete conversation"
580
- });
581
- }
582
615
  }
583
616
  await adapter.delete({
584
617
  model: "conversation",
@@ -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