@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.
- 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 +87 -54
- package/dist/packages/stack/src/plugins/ai-chat/api/plugin.mjs +87 -54
- 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/client/plugin.cjs +14 -21
- package/dist/packages/stack/src/plugins/ai-chat/client/plugin.mjs +15 -22
- 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/api/plugin.cjs +28 -45
- package/dist/packages/stack/src/plugins/blog/api/plugin.mjs +22 -39
- 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/blog/client/plugin.cjs +23 -27
- package/dist/packages/stack/src/plugins/blog/client/plugin.mjs +24 -28
- 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 +21 -18
- package/dist/packages/stack/src/plugins/cms/api/plugin.mjs +21 -18
- package/dist/packages/stack/src/plugins/cms/client/plugin.cjs +11 -15
- package/dist/packages/stack/src/plugins/cms/client/plugin.mjs +12 -16
- package/dist/packages/stack/src/plugins/form-builder/api/plugin.cjs +58 -62
- package/dist/packages/stack/src/plugins/form-builder/api/plugin.mjs +58 -62
- package/dist/packages/stack/src/plugins/form-builder/client/plugin.cjs +12 -12
- package/dist/packages/stack/src/plugins/form-builder/client/plugin.mjs +13 -13
- 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 +92 -118
- package/dist/packages/stack/src/plugins/kanban/api/plugin.mjs +89 -115
- 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/kanban/client/plugin.cjs +22 -29
- package/dist/packages/stack/src/plugins/kanban/client/plugin.mjs +23 -30
- 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/packages/stack/src/plugins/ui-builder/client/plugin.cjs +8 -8
- package/dist/packages/stack/src/plugins/ui-builder/client/plugin.mjs +9 -9
- package/dist/packages/stack/src/plugins/utils.cjs +42 -0
- package/dist/packages/stack/src/plugins/utils.mjs +41 -1
- 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 +10 -10
- package/dist/plugins/ai-chat/client/index.d.mts +10 -10
- package/dist/plugins/ai-chat/client/index.d.ts +10 -10
- 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 +13 -13
- package/dist/plugins/blog/client/index.d.mts +13 -13
- package/dist/plugins/blog/client/index.d.ts +13 -13
- 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/client/index.cjs +1 -0
- package/dist/plugins/client/index.d.cts +8 -1
- package/dist/plugins/client/index.d.mts +8 -1
- package/dist/plugins/client/index.d.ts +8 -1
- package/dist/plugins/client/index.mjs +1 -1
- package/dist/plugins/cms/api/index.cjs +2 -0
- package/dist/plugins/cms/api/index.d.cts +2 -2
- package/dist/plugins/cms/api/index.d.mts +2 -2
- package/dist/plugins/cms/api/index.d.ts +2 -2
- package/dist/plugins/cms/api/index.mjs +1 -0
- package/dist/plugins/cms/client/hooks/index.d.cts +1 -1
- package/dist/plugins/cms/client/hooks/index.d.mts +1 -1
- package/dist/plugins/cms/client/hooks/index.d.ts +1 -1
- package/dist/plugins/cms/client/index.d.cts +6 -6
- package/dist/plugins/cms/client/index.d.mts +6 -6
- package/dist/plugins/cms/client/index.d.ts +6 -6
- package/dist/plugins/cms/query-keys.d.cts +2 -2
- package/dist/plugins/cms/query-keys.d.mts +2 -2
- package/dist/plugins/cms/query-keys.d.ts +2 -2
- package/dist/plugins/form-builder/api/index.d.cts +2 -2
- package/dist/plugins/form-builder/api/index.d.mts +2 -2
- package/dist/plugins/form-builder/api/index.d.ts +2 -2
- package/dist/plugins/form-builder/client/components/index.d.cts +1 -1
- package/dist/plugins/form-builder/client/components/index.d.mts +1 -1
- package/dist/plugins/form-builder/client/components/index.d.ts +1 -1
- package/dist/plugins/form-builder/client/hooks/index.d.cts +1 -1
- package/dist/plugins/form-builder/client/hooks/index.d.mts +1 -1
- package/dist/plugins/form-builder/client/hooks/index.d.ts +1 -1
- package/dist/plugins/form-builder/client/index.d.cts +6 -6
- package/dist/plugins/form-builder/client/index.d.mts +6 -6
- package/dist/plugins/form-builder/client/index.d.ts +6 -6
- package/dist/plugins/form-builder/query-keys.d.cts +2 -2
- package/dist/plugins/form-builder/query-keys.d.mts +2 -2
- package/dist/plugins/form-builder/query-keys.d.ts +2 -2
- 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/client/index.d.cts +12 -12
- package/dist/plugins/kanban/client/index.d.mts +12 -12
- package/dist/plugins/kanban/client/index.d.ts +12 -12
- 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/plugins/ui-builder/client/hooks/index.d.cts +1 -1
- package/dist/plugins/ui-builder/client/hooks/index.d.mts +1 -1
- package/dist/plugins/ui-builder/client/hooks/index.d.ts +1 -1
- package/dist/plugins/ui-builder/client/index.d.cts +3 -3
- package/dist/plugins/ui-builder/client/index.d.mts +3 -3
- package/dist/plugins/ui-builder/client/index.d.ts +3 -3
- package/dist/plugins/ui-builder/index.d.cts +2 -2
- package/dist/plugins/ui-builder/index.d.mts +2 -2
- package/dist/plugins/ui-builder/index.d.ts +2 -2
- package/dist/shared/{stack.C-WUPMT6.d.cts → stack.B2xZTSiO.d.cts} +4 -4
- package/dist/shared/{stack.B1EeBt1b.d.ts → stack.B58oHdqm.d.mts} +33 -3
- package/dist/shared/{stack.CVDTkMoO.d.mts → stack.B8QD11QU.d.cts} +7 -7
- package/dist/shared/{stack.CVDTkMoO.d.cts → stack.B8QD11QU.d.mts} +7 -7
- package/dist/shared/{stack.CVDTkMoO.d.ts → stack.B8QD11QU.d.ts} +7 -7
- package/dist/shared/{stack.CIP6QS9l.d.ts → stack.BDVEpue1.d.ts} +1 -1
- package/dist/shared/{stack.C5dtIncc.d.mts → stack.BTvbxZvw.d.cts} +1 -1
- 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.DdI5W6MB.d.mts → stack.BozPgbrZ.d.cts} +19 -19
- package/dist/shared/{stack.DdI5W6MB.d.ts → stack.BozPgbrZ.d.mts} +19 -19
- package/dist/shared/{stack.DdI5W6MB.d.cts → stack.BozPgbrZ.d.ts} +19 -19
- package/dist/shared/{stack.CP68pFEH.d.mts → stack.C9Mg2Q46.d.cts} +33 -3
- package/dist/shared/{stack.BeSm90va.d.ts → stack.CTDVxbrA.d.ts} +72 -14
- package/dist/shared/{stack.C-Ptrz8s.d.ts → stack.Cj_zKww4.d.ts} +4 -4
- package/dist/shared/{stack.TIBF2AOx.d.ts → stack.CxaFNQCV.d.mts} +89 -34
- package/dist/shared/{stack.CMh_EdxW.d.cts → stack.D-b5zbPm.d.cts} +72 -14
- package/dist/shared/{stack.Dw0Ly2TM.d.cts → stack.DTtmJPQO.d.mts} +1 -1
- package/dist/shared/{stack.BKfolAyK.d.ts → stack.DXnclTG7.d.ts} +11 -11
- package/dist/shared/{stack.snB1EDP7.d.cts → stack.DaZM10cp.d.cts} +11 -11
- package/dist/shared/{stack.Dg09R0oB.d.mts → stack.FVWf2JhZ.d.mts} +72 -14
- package/dist/shared/{stack.BIXEI6v_.d.mts → stack.cfCkioTe.d.mts} +11 -11
- package/dist/shared/{stack.6fUOjLs9.d.mts → stack.dH7u-TJH.d.mts} +4 -4
- package/dist/shared/{stack.BpolpQpf.d.cts → stack.j75TpKh2.d.ts} +89 -34
- package/dist/shared/{stack.rTy7-wQU.d.mts → stack.n1_i1p2B.d.cts} +89 -34
- package/dist/shared/{stack.IdtKDRka.d.cts → stack.sO33ZDhK.d.ts} +33 -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 +228 -72
- 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/client/plugin.tsx +23 -31
- package/src/plugins/ai-chat/schemas.ts +16 -0
- package/src/plugins/blog/api/plugin.ts +31 -47
- 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/blog/client/plugin.tsx +36 -39
- package/src/plugins/client/index.ts +5 -1
- 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 +23 -17
- package/src/plugins/cms/client/plugin.tsx +18 -21
- package/src/plugins/cms/types.ts +7 -7
- package/src/plugins/form-builder/api/plugin.ts +64 -64
- package/src/plugins/form-builder/client/plugin.tsx +19 -18
- package/src/plugins/form-builder/types.ts +19 -24
- 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 +123 -136
- package/src/plugins/kanban/client/hooks/kanban-hooks.tsx +4 -0
- package/src/plugins/kanban/client/plugin.tsx +35 -41
- package/src/plugins/ui-builder/client/components/pages/page-builder-page.internal.tsx +132 -0
- package/src/plugins/ui-builder/client/plugin.tsx +11 -10
- package/src/plugins/ui-builder/types.ts +4 -4
- package/src/plugins/utils.ts +92 -1
- package/dist/shared/{stack.CBON0dWL.d.mts → stack.BQmuNl5p.d.cts} +2 -2
- package/dist/shared/{stack.CBON0dWL.d.ts → stack.BQmuNl5p.d.mts} +2 -2
- package/dist/shared/{stack.CBON0dWL.d.cts → stack.BQmuNl5p.d.ts} +2 -2
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createContext,
|
|
5
|
+
useCallback,
|
|
6
|
+
useContext,
|
|
7
|
+
useId,
|
|
8
|
+
useEffect,
|
|
9
|
+
useMemo,
|
|
10
|
+
useRef,
|
|
11
|
+
useState,
|
|
12
|
+
} from "react";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A client-side tool handler — receives the AI's tool call args and returns a result.
|
|
16
|
+
* The result is sent back to the model so it can continue the conversation.
|
|
17
|
+
*/
|
|
18
|
+
export type PageAIClientTool = (
|
|
19
|
+
args: any,
|
|
20
|
+
) => Promise<{ success: boolean; message?: string }>;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Configuration registered by a page to provide AI context and capabilities.
|
|
24
|
+
* Any component in the tree can call useRegisterPageAIContext with this config.
|
|
25
|
+
*/
|
|
26
|
+
export interface PageAIContextConfig {
|
|
27
|
+
/**
|
|
28
|
+
* Identifier for the current route/page (e.g. "blog-post", "ui-builder-edit-page").
|
|
29
|
+
* Shown as a badge in the chat header.
|
|
30
|
+
*/
|
|
31
|
+
routeName: string;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Human-readable description of the current page and its content.
|
|
35
|
+
* Injected into the AI system prompt so it understands what the user is looking at.
|
|
36
|
+
* Capped at 8,000 characters server-side.
|
|
37
|
+
*/
|
|
38
|
+
pageDescription: string;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Optional suggested prompts shown as quick-action chips in the chat empty state.
|
|
42
|
+
* These augment (not replace) any static suggestions configured in plugin overrides.
|
|
43
|
+
*/
|
|
44
|
+
suggestions?: string[];
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Client-side tool handlers keyed by tool name.
|
|
48
|
+
* When the AI calls a tool by this name, the handler is invoked with the tool args.
|
|
49
|
+
* The result is sent back to the model via addToolResult.
|
|
50
|
+
*
|
|
51
|
+
* Tool schemas must be registered server-side via enablePageTools + clientToolSchemas
|
|
52
|
+
* in aiChatBackendPlugin (built-in tools like fillBlogForm are pre-registered).
|
|
53
|
+
*/
|
|
54
|
+
clientTools?: Record<string, PageAIClientTool>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface PageAIAPIContextValue {
|
|
58
|
+
register: (id: string, config: PageAIContextConfig) => void;
|
|
59
|
+
unregister: (id: string) => void;
|
|
60
|
+
getActive: () => PageAIContextConfig | null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Stable API context — holds register/unregister/getActive.
|
|
65
|
+
* Never changes reference, so useRegisterPageAIContext effects don't re-run
|
|
66
|
+
* simply because the provider re-rendered after a bumpVersion call.
|
|
67
|
+
*/
|
|
68
|
+
const PageAIAPIContext = createContext<PageAIAPIContextValue | null>(null);
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Reactive version context — incremented on every register/unregister.
|
|
72
|
+
* Consumers of usePageAIContext subscribe here so they re-render when
|
|
73
|
+
* registrations change and re-call getActive() to pick up the latest config.
|
|
74
|
+
*/
|
|
75
|
+
const PageAIVersionContext = createContext<number>(0);
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Provider that enables route-aware AI context across the app.
|
|
79
|
+
*
|
|
80
|
+
* Place this at the root layout — above all StackProviders — so it spans
|
|
81
|
+
* both your main app tree and any chat modals rendered as parallel/intercept routes.
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* // app/layout.tsx
|
|
85
|
+
* import { PageAIContextProvider } from "@btst/stack/plugins/ai-chat/client/context"
|
|
86
|
+
*
|
|
87
|
+
* export default function RootLayout({ children }) {
|
|
88
|
+
* return <PageAIContextProvider>{children}</PageAIContextProvider>
|
|
89
|
+
* }
|
|
90
|
+
*/
|
|
91
|
+
export function PageAIContextProvider({
|
|
92
|
+
children,
|
|
93
|
+
}: {
|
|
94
|
+
children: React.ReactNode;
|
|
95
|
+
}) {
|
|
96
|
+
// Map from stable registration id → config
|
|
97
|
+
// Using useRef so mutations don't trigger re-renders of the provider itself
|
|
98
|
+
const registrationsRef = useRef<Map<string, PageAIContextConfig>>(new Map());
|
|
99
|
+
// Track insertion order so the last-registered (most specific) wins
|
|
100
|
+
const insertionOrderRef = useRef<string[]>([]);
|
|
101
|
+
|
|
102
|
+
// Version counter — bumped on every register/unregister so consumers re-read
|
|
103
|
+
const [version, setVersion] = useState(0);
|
|
104
|
+
const bumpVersion = useCallback(() => setVersion((v) => v + 1), []);
|
|
105
|
+
|
|
106
|
+
const register = useCallback(
|
|
107
|
+
(id: string, config: PageAIContextConfig) => {
|
|
108
|
+
registrationsRef.current.set(id, config);
|
|
109
|
+
// Move to end to mark as most recent
|
|
110
|
+
insertionOrderRef.current = insertionOrderRef.current.filter(
|
|
111
|
+
(k) => k !== id,
|
|
112
|
+
);
|
|
113
|
+
insertionOrderRef.current.push(id);
|
|
114
|
+
bumpVersion();
|
|
115
|
+
},
|
|
116
|
+
[bumpVersion],
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const unregister = useCallback(
|
|
120
|
+
(id: string) => {
|
|
121
|
+
registrationsRef.current.delete(id);
|
|
122
|
+
insertionOrderRef.current = insertionOrderRef.current.filter(
|
|
123
|
+
(k) => k !== id,
|
|
124
|
+
);
|
|
125
|
+
bumpVersion();
|
|
126
|
+
},
|
|
127
|
+
[bumpVersion],
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const getActive = useCallback((): PageAIContextConfig | null => {
|
|
131
|
+
const order = insertionOrderRef.current;
|
|
132
|
+
if (order.length === 0) return null;
|
|
133
|
+
// Last registered wins (most deeply nested / most recently mounted)
|
|
134
|
+
const lastId = order[order.length - 1];
|
|
135
|
+
if (!lastId) return null;
|
|
136
|
+
return registrationsRef.current.get(lastId) ?? null;
|
|
137
|
+
}, []);
|
|
138
|
+
|
|
139
|
+
// Memoize the API object so its reference never changes — this is what
|
|
140
|
+
// breaks the infinite loop: useRegisterPageAIContext has `ctx` (the API)
|
|
141
|
+
// in its effect deps, and a stable reference means the effect won't re-run
|
|
142
|
+
// just because the provider re-rendered after bumpVersion().
|
|
143
|
+
const api = useMemo(
|
|
144
|
+
() => ({ register, unregister, getActive }),
|
|
145
|
+
[register, unregister, getActive],
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<PageAIAPIContext.Provider value={api}>
|
|
150
|
+
<PageAIVersionContext.Provider value={version}>
|
|
151
|
+
{children}
|
|
152
|
+
</PageAIVersionContext.Provider>
|
|
153
|
+
</PageAIAPIContext.Provider>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Register page AI context from any component.
|
|
159
|
+
* The registration is cleaned up automatically when the component unmounts.
|
|
160
|
+
*
|
|
161
|
+
* Pass `null` to conditionally disable context (e.g. while data is loading).
|
|
162
|
+
*
|
|
163
|
+
* @example
|
|
164
|
+
* // Blog post page
|
|
165
|
+
* useRegisterPageAIContext(post ? {
|
|
166
|
+
* routeName: "blog-post",
|
|
167
|
+
* pageDescription: `Blog post: "${post.title}"\n\n${post.content?.slice(0, 16000)}`,
|
|
168
|
+
* suggestions: ["Summarize this post", "What are the key takeaways?"],
|
|
169
|
+
* } : null)
|
|
170
|
+
*/
|
|
171
|
+
export function useRegisterPageAIContext(
|
|
172
|
+
config: PageAIContextConfig | null,
|
|
173
|
+
): void {
|
|
174
|
+
// Use the stable API context — its reference never changes, so adding it
|
|
175
|
+
// to the dependency array below does NOT cause the effect to re-run after
|
|
176
|
+
// bumpVersion() fires. This breaks the register → bumpVersion → re-render
|
|
177
|
+
// → effect re-run → register loop that caused "Maximum update depth exceeded".
|
|
178
|
+
const ctx = useContext(PageAIAPIContext);
|
|
179
|
+
const id = useId();
|
|
180
|
+
|
|
181
|
+
// Always keep the ref current so clientTools handlers are never stale.
|
|
182
|
+
// Updating a ref during render is safe — the value is visible to any effect
|
|
183
|
+
// that runs in the same commit.
|
|
184
|
+
const configRef = useRef<PageAIContextConfig | null>(config);
|
|
185
|
+
configRef.current = config;
|
|
186
|
+
|
|
187
|
+
useEffect(() => {
|
|
188
|
+
if (!ctx || !configRef.current) return;
|
|
189
|
+
// Register a live proxy that always reads from configRef. This ensures
|
|
190
|
+
// clientTools handlers are fresh even when the effect doesn't re-run —
|
|
191
|
+
// for example when a handler's closure captures new state but the
|
|
192
|
+
// serializable fields (routeName, pageDescription, suggestions) are unchanged.
|
|
193
|
+
// JSON.stringify silently strips function values, so clientTools would be
|
|
194
|
+
// invisible to a plain JSON.stringify(config) dependency check.
|
|
195
|
+
ctx.register(id, {
|
|
196
|
+
get routeName() {
|
|
197
|
+
return configRef.current?.routeName ?? "";
|
|
198
|
+
},
|
|
199
|
+
get pageDescription() {
|
|
200
|
+
return configRef.current?.pageDescription ?? "";
|
|
201
|
+
},
|
|
202
|
+
get suggestions() {
|
|
203
|
+
return configRef.current?.suggestions;
|
|
204
|
+
},
|
|
205
|
+
get clientTools() {
|
|
206
|
+
return configRef.current?.clientTools;
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
return () => {
|
|
210
|
+
ctx.unregister(id);
|
|
211
|
+
};
|
|
212
|
+
// Track serializable fields individually. JSON.stringify on the whole config
|
|
213
|
+
// would silently strip clientTools (functions), making handler changes invisible.
|
|
214
|
+
// Handler freshness is provided by the ref-proxy above instead.
|
|
215
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
216
|
+
}, [
|
|
217
|
+
ctx,
|
|
218
|
+
id,
|
|
219
|
+
config === null,
|
|
220
|
+
config?.routeName,
|
|
221
|
+
config?.pageDescription,
|
|
222
|
+
JSON.stringify(config?.suggestions),
|
|
223
|
+
]);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Read the currently active page AI context.
|
|
228
|
+
* Returns null when no page has registered context, or when PageAIContextProvider
|
|
229
|
+
* is not in the tree.
|
|
230
|
+
*
|
|
231
|
+
* Used internally by ChatInterface to inject context into requests.
|
|
232
|
+
*/
|
|
233
|
+
export function usePageAIContext(): PageAIContextConfig | null {
|
|
234
|
+
// Subscribe to the version counter so this hook re-runs whenever a page
|
|
235
|
+
// registers or unregisters context, then read the latest active config.
|
|
236
|
+
useContext(PageAIVersionContext);
|
|
237
|
+
const ctx = useContext(PageAIAPIContext);
|
|
238
|
+
if (!ctx) return null;
|
|
239
|
+
return ctx.getActive();
|
|
240
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
defineClientPlugin,
|
|
3
3
|
createApiClient,
|
|
4
|
+
runClientHookWithShim,
|
|
4
5
|
} from "@btst/stack/plugins/client";
|
|
5
6
|
import { createRoute } from "@btst/yar";
|
|
6
7
|
import type { QueryClient } from "@tanstack/react-query";
|
|
@@ -92,35 +93,33 @@ export interface AiChatClientConfig {
|
|
|
92
93
|
*/
|
|
93
94
|
export interface AiChatClientHooks {
|
|
94
95
|
/**
|
|
95
|
-
* Called before loading conversations list.
|
|
96
|
+
* Called before loading conversations list. Throw an error to cancel loading.
|
|
96
97
|
* @param context - Loader context with path, params, etc.
|
|
97
98
|
*/
|
|
98
|
-
beforeLoadConversations?: (
|
|
99
|
-
context: LoaderContext,
|
|
100
|
-
) => Promise<boolean> | boolean;
|
|
99
|
+
beforeLoadConversations?: (context: LoaderContext) => Promise<void> | void;
|
|
101
100
|
|
|
102
101
|
/**
|
|
103
|
-
* Called after conversations are loaded.
|
|
102
|
+
* Called after conversations are loaded. Throw an error to cancel further processing.
|
|
104
103
|
* @param conversations - Array of loaded conversations or null
|
|
105
104
|
* @param context - Loader context
|
|
106
105
|
*/
|
|
107
106
|
afterLoadConversations?: (
|
|
108
107
|
conversations: SerializedConversation[] | null,
|
|
109
108
|
context: LoaderContext,
|
|
110
|
-
) => Promise<
|
|
109
|
+
) => Promise<void> | void;
|
|
111
110
|
|
|
112
111
|
/**
|
|
113
|
-
* Called before loading a single conversation.
|
|
112
|
+
* Called before loading a single conversation. Throw an error to cancel loading.
|
|
114
113
|
* @param id - Conversation ID being loaded
|
|
115
114
|
* @param context - Loader context
|
|
116
115
|
*/
|
|
117
116
|
beforeLoadConversation?: (
|
|
118
117
|
id: string,
|
|
119
118
|
context: LoaderContext,
|
|
120
|
-
) => Promise<
|
|
119
|
+
) => Promise<void> | void;
|
|
121
120
|
|
|
122
121
|
/**
|
|
123
|
-
* Called after a conversation is loaded.
|
|
122
|
+
* Called after a conversation is loaded. Throw an error to cancel further processing.
|
|
124
123
|
* @param conversation - Loaded conversation or null if not found
|
|
125
124
|
* @param id - Conversation ID that was requested
|
|
126
125
|
* @param context - Loader context
|
|
@@ -131,7 +130,7 @@ export interface AiChatClientHooks {
|
|
|
131
130
|
| null,
|
|
132
131
|
id: string,
|
|
133
132
|
context: LoaderContext,
|
|
134
|
-
) => Promise<
|
|
133
|
+
) => Promise<void> | void;
|
|
135
134
|
|
|
136
135
|
/**
|
|
137
136
|
* Called when a loading error occurs
|
|
@@ -163,10 +162,10 @@ function createConversationsLoader(config: AiChatClientConfig) {
|
|
|
163
162
|
try {
|
|
164
163
|
// Before hook
|
|
165
164
|
if (hooks?.beforeLoadConversations) {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
165
|
+
await runClientHookWithShim(
|
|
166
|
+
() => hooks.beforeLoadConversations!(context),
|
|
167
|
+
"Load prevented by beforeLoadConversations hook",
|
|
168
|
+
);
|
|
170
169
|
}
|
|
171
170
|
|
|
172
171
|
const client = createApiClient<AiChatApiRouter>({
|
|
@@ -185,13 +184,10 @@ function createConversationsLoader(config: AiChatClientConfig) {
|
|
|
185
184
|
queryClient.getQueryData<SerializedConversation[]>(
|
|
186
185
|
listQuery.queryKey,
|
|
187
186
|
) || null;
|
|
188
|
-
|
|
189
|
-
conversations,
|
|
190
|
-
|
|
187
|
+
await runClientHookWithShim(
|
|
188
|
+
() => hooks.afterLoadConversations!(conversations, context),
|
|
189
|
+
"Load prevented by afterLoadConversations hook",
|
|
191
190
|
);
|
|
192
|
-
if (canContinue === false) {
|
|
193
|
-
throw new Error("Load prevented by afterLoadConversations hook");
|
|
194
|
-
}
|
|
195
191
|
}
|
|
196
192
|
|
|
197
193
|
// Check for errors
|
|
@@ -230,10 +226,10 @@ function createConversationLoader(id: string, config: AiChatClientConfig) {
|
|
|
230
226
|
try {
|
|
231
227
|
// Before hook
|
|
232
228
|
if (hooks?.beforeLoadConversation) {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
229
|
+
await runClientHookWithShim(
|
|
230
|
+
() => hooks.beforeLoadConversation!(id, context),
|
|
231
|
+
"Load prevented by beforeLoadConversation hook",
|
|
232
|
+
);
|
|
237
233
|
}
|
|
238
234
|
|
|
239
235
|
const client = createApiClient<AiChatApiRouter>({
|
|
@@ -258,14 +254,10 @@ function createConversationLoader(id: string, config: AiChatClientConfig) {
|
|
|
258
254
|
queryClient.getQueryData<
|
|
259
255
|
SerializedConversation & { messages: SerializedMessage[] }
|
|
260
256
|
>(conversationQuery.queryKey) || null;
|
|
261
|
-
|
|
262
|
-
conversation,
|
|
263
|
-
|
|
264
|
-
context,
|
|
257
|
+
await runClientHookWithShim(
|
|
258
|
+
() => hooks.afterLoadConversation!(conversation, id, context),
|
|
259
|
+
"Load prevented by afterLoadConversation hook",
|
|
265
260
|
);
|
|
266
|
-
if (canContinue === false) {
|
|
267
|
-
throw new Error("Load prevented by afterLoadConversation hook");
|
|
268
|
-
}
|
|
269
261
|
}
|
|
270
262
|
|
|
271
263
|
// Check for errors
|
|
@@ -37,4 +37,20 @@ export const chatRequestSchema = z.object({
|
|
|
37
37
|
),
|
|
38
38
|
conversationId: z.string().optional(),
|
|
39
39
|
model: z.string().optional(),
|
|
40
|
+
/**
|
|
41
|
+
* Description of the current page context, injected into the AI system prompt.
|
|
42
|
+
* Sent by ChatInterface when a page has registered context via useRegisterPageAIContext.
|
|
43
|
+
*/
|
|
44
|
+
pageContext: z.string().max(16000).optional(),
|
|
45
|
+
/**
|
|
46
|
+
* Names of client-side tools currently available on the page.
|
|
47
|
+
* The server includes matching tool schemas in the streamText call.
|
|
48
|
+
*/
|
|
49
|
+
availableTools: z.array(z.string()).optional(),
|
|
50
|
+
/**
|
|
51
|
+
* The routeName registered by the page via useRegisterPageAIContext.
|
|
52
|
+
* Cross-validated server-side against each built-in tool's route allowlist
|
|
53
|
+
* to prevent a page from claiming tools intended for a different route.
|
|
54
|
+
*/
|
|
55
|
+
routeName: z.string().optional(),
|
|
40
56
|
});
|
|
@@ -10,6 +10,7 @@ import { getAllPosts, getPostBySlug, getAllTags } from "./getters";
|
|
|
10
10
|
import { BLOG_QUERY_KEYS } from "./query-key-defs";
|
|
11
11
|
import { serializePost, serializeTag } from "./serializers";
|
|
12
12
|
import type { QueryClient } from "@tanstack/react-query";
|
|
13
|
+
import { runHookWithShim } from "../../utils";
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Route keys for the blog plugin — matches the keys returned by
|
|
@@ -131,25 +132,25 @@ export interface BlogApiContext<TBody = any, TParams = any, TQuery = any> {
|
|
|
131
132
|
*/
|
|
132
133
|
export interface BlogBackendHooks {
|
|
133
134
|
/**
|
|
134
|
-
* Called before listing posts.
|
|
135
|
+
* Called before listing posts. Throw an error to deny access.
|
|
135
136
|
* @param filter - Query parameters for filtering posts
|
|
136
137
|
* @param context - Request context with headers, etc.
|
|
137
138
|
*/
|
|
138
139
|
onBeforeListPosts?: (
|
|
139
140
|
filter: z.infer<typeof PostListQuerySchema>,
|
|
140
141
|
context: BlogApiContext,
|
|
141
|
-
) => Promise<
|
|
142
|
+
) => Promise<void> | void;
|
|
142
143
|
/**
|
|
143
|
-
* Called before creating a post.
|
|
144
|
+
* Called before creating a post. Throw an error to deny access.
|
|
144
145
|
* @param data - Post data being created
|
|
145
146
|
* @param context - Request context with headers, etc.
|
|
146
147
|
*/
|
|
147
148
|
onBeforeCreatePost?: (
|
|
148
149
|
data: z.infer<typeof createPostSchema>,
|
|
149
150
|
context: BlogApiContext,
|
|
150
|
-
) => Promise<
|
|
151
|
+
) => Promise<void> | void;
|
|
151
152
|
/**
|
|
152
|
-
* Called before updating a post.
|
|
153
|
+
* Called before updating a post. Throw an error to deny access.
|
|
153
154
|
* @param postId - ID of the post being updated
|
|
154
155
|
* @param data - Updated post data
|
|
155
156
|
* @param context - Request context with headers, etc.
|
|
@@ -158,16 +159,16 @@ export interface BlogBackendHooks {
|
|
|
158
159
|
postId: string,
|
|
159
160
|
data: z.infer<typeof updatePostSchema>,
|
|
160
161
|
context: BlogApiContext,
|
|
161
|
-
) => Promise<
|
|
162
|
+
) => Promise<void> | void;
|
|
162
163
|
/**
|
|
163
|
-
* Called before deleting a post.
|
|
164
|
+
* Called before deleting a post. Throw an error to deny access.
|
|
164
165
|
* @param postId - ID of the post being deleted
|
|
165
166
|
* @param context - Request context with headers, etc.
|
|
166
167
|
*/
|
|
167
168
|
onBeforeDeletePost?: (
|
|
168
169
|
postId: string,
|
|
169
170
|
context: BlogApiContext,
|
|
170
|
-
) => Promise<
|
|
171
|
+
) => Promise<void> | void;
|
|
171
172
|
|
|
172
173
|
/**
|
|
173
174
|
* Called after posts are read successfully
|
|
@@ -350,12 +351,11 @@ export const blogBackendPlugin = (hooks?: BlogBackendHooks) =>
|
|
|
350
351
|
|
|
351
352
|
try {
|
|
352
353
|
if (hooks?.onBeforeListPosts) {
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
}
|
|
354
|
+
await runHookWithShim(
|
|
355
|
+
() => hooks.onBeforeListPosts!(query, context),
|
|
356
|
+
ctx.error,
|
|
357
|
+
"Unauthorized: Cannot list posts",
|
|
358
|
+
);
|
|
359
359
|
}
|
|
360
360
|
|
|
361
361
|
const result = await getAllPosts(adapter, query);
|
|
@@ -387,15 +387,11 @@ export const blogBackendPlugin = (hooks?: BlogBackendHooks) =>
|
|
|
387
387
|
|
|
388
388
|
try {
|
|
389
389
|
if (hooks?.onBeforeCreatePost) {
|
|
390
|
-
|
|
391
|
-
ctx.body,
|
|
392
|
-
|
|
390
|
+
await runHookWithShim(
|
|
391
|
+
() => hooks.onBeforeCreatePost!(ctx.body, context),
|
|
392
|
+
ctx.error,
|
|
393
|
+
"Unauthorized: Cannot create post",
|
|
393
394
|
);
|
|
394
|
-
if (!canCreate) {
|
|
395
|
-
throw ctx.error(403, {
|
|
396
|
-
message: "Unauthorized: Cannot create post",
|
|
397
|
-
});
|
|
398
|
-
}
|
|
399
395
|
}
|
|
400
396
|
|
|
401
397
|
const { tags, ...postData } = ctx.body;
|
|
@@ -471,16 +467,12 @@ export const blogBackendPlugin = (hooks?: BlogBackendHooks) =>
|
|
|
471
467
|
|
|
472
468
|
try {
|
|
473
469
|
if (hooks?.onBeforeUpdatePost) {
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
470
|
+
await runHookWithShim(
|
|
471
|
+
() =>
|
|
472
|
+
hooks.onBeforeUpdatePost!(ctx.params.id, ctx.body, context),
|
|
473
|
+
ctx.error,
|
|
474
|
+
"Unauthorized: Cannot update post",
|
|
478
475
|
);
|
|
479
|
-
if (!canUpdate) {
|
|
480
|
-
throw ctx.error(403, {
|
|
481
|
-
message: "Unauthorized: Cannot update post",
|
|
482
|
-
});
|
|
483
|
-
}
|
|
484
476
|
}
|
|
485
477
|
|
|
486
478
|
const { tags, slug: rawSlug, ...restPostData } = ctx.body;
|
|
@@ -598,15 +590,11 @@ export const blogBackendPlugin = (hooks?: BlogBackendHooks) =>
|
|
|
598
590
|
try {
|
|
599
591
|
// Authorization hook
|
|
600
592
|
if (hooks?.onBeforeDeletePost) {
|
|
601
|
-
|
|
602
|
-
ctx.params.id,
|
|
603
|
-
|
|
593
|
+
await runHookWithShim(
|
|
594
|
+
() => hooks.onBeforeDeletePost!(ctx.params.id, context),
|
|
595
|
+
ctx.error,
|
|
596
|
+
"Unauthorized: Cannot delete post",
|
|
604
597
|
);
|
|
605
|
-
if (!canDelete) {
|
|
606
|
-
throw ctx.error(403, {
|
|
607
|
-
message: "Unauthorized: Cannot delete post",
|
|
608
|
-
});
|
|
609
|
-
}
|
|
610
598
|
}
|
|
611
599
|
|
|
612
600
|
await adapter.delete<Post>({
|
|
@@ -642,15 +630,11 @@ export const blogBackendPlugin = (hooks?: BlogBackendHooks) =>
|
|
|
642
630
|
|
|
643
631
|
try {
|
|
644
632
|
if (hooks?.onBeforeListPosts) {
|
|
645
|
-
|
|
646
|
-
{ published: true },
|
|
647
|
-
|
|
633
|
+
await runHookWithShim(
|
|
634
|
+
() => hooks.onBeforeListPosts!({ published: true }, context),
|
|
635
|
+
ctx.error,
|
|
636
|
+
"Unauthorized: Cannot list posts",
|
|
648
637
|
);
|
|
649
|
-
if (!canList) {
|
|
650
|
-
throw ctx.error(403, {
|
|
651
|
-
message: "Unauthorized: Cannot list posts",
|
|
652
|
-
});
|
|
653
|
-
}
|
|
654
638
|
}
|
|
655
639
|
|
|
656
640
|
const date = query.date;
|
|
@@ -40,7 +40,7 @@ import {
|
|
|
40
40
|
|
|
41
41
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
42
42
|
import { Loader2 } from "lucide-react";
|
|
43
|
-
import { lazy, memo, Suspense, useMemo, useState } from "react";
|
|
43
|
+
import { lazy, memo, Suspense, useEffect, useMemo, useState } from "react";
|
|
44
44
|
import {
|
|
45
45
|
type FieldPath,
|
|
46
46
|
type SubmitHandler,
|
|
@@ -325,6 +325,10 @@ const CustomPostUpdateSchema = PostUpdateSchema.omit({
|
|
|
325
325
|
type AddPostFormProps = {
|
|
326
326
|
onClose: () => void;
|
|
327
327
|
onSuccess: (post: { published: boolean }) => void;
|
|
328
|
+
/** Called once with the form instance so parent components can access form state */
|
|
329
|
+
onFormReady?: (
|
|
330
|
+
form: UseFormReturn<z.input<typeof CustomPostCreateSchema>>,
|
|
331
|
+
) => void;
|
|
328
332
|
};
|
|
329
333
|
|
|
330
334
|
const addPostFormPropsAreEqual = (
|
|
@@ -333,10 +337,15 @@ const addPostFormPropsAreEqual = (
|
|
|
333
337
|
): boolean => {
|
|
334
338
|
if (prevProps.onClose !== nextProps.onClose) return false;
|
|
335
339
|
if (prevProps.onSuccess !== nextProps.onSuccess) return false;
|
|
340
|
+
if (prevProps.onFormReady !== nextProps.onFormReady) return false;
|
|
336
341
|
return true;
|
|
337
342
|
};
|
|
338
343
|
|
|
339
|
-
const AddPostFormComponent = ({
|
|
344
|
+
const AddPostFormComponent = ({
|
|
345
|
+
onClose,
|
|
346
|
+
onSuccess,
|
|
347
|
+
onFormReady,
|
|
348
|
+
}: AddPostFormProps) => {
|
|
340
349
|
const [featuredImageUploading, setFeaturedImageUploading] = useState(false);
|
|
341
350
|
const { localization } = usePluginOverrides<
|
|
342
351
|
BlogPluginOverrides,
|
|
@@ -393,6 +402,12 @@ const AddPostFormComponent = ({ onClose, onSuccess }: AddPostFormProps) => {
|
|
|
393
402
|
},
|
|
394
403
|
});
|
|
395
404
|
|
|
405
|
+
// Expose form instance to parent for AI context integration
|
|
406
|
+
useEffect(() => {
|
|
407
|
+
onFormReady?.(form);
|
|
408
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
409
|
+
}, []);
|
|
410
|
+
|
|
396
411
|
return (
|
|
397
412
|
<PostFormBody
|
|
398
413
|
form={form}
|
|
@@ -417,6 +432,10 @@ type EditPostFormProps = {
|
|
|
417
432
|
onClose: () => void;
|
|
418
433
|
onSuccess: (post: { slug: string; published: boolean }) => void;
|
|
419
434
|
onDelete?: () => void;
|
|
435
|
+
/** Called once with the form instance so parent components can access form state */
|
|
436
|
+
onFormReady?: (
|
|
437
|
+
form: UseFormReturn<z.input<typeof CustomPostUpdateSchema>>,
|
|
438
|
+
) => void;
|
|
420
439
|
};
|
|
421
440
|
|
|
422
441
|
const editPostFormPropsAreEqual = (
|
|
@@ -427,6 +446,7 @@ const editPostFormPropsAreEqual = (
|
|
|
427
446
|
if (prevProps.onClose !== nextProps.onClose) return false;
|
|
428
447
|
if (prevProps.onSuccess !== nextProps.onSuccess) return false;
|
|
429
448
|
if (prevProps.onDelete !== nextProps.onDelete) return false;
|
|
449
|
+
if (prevProps.onFormReady !== nextProps.onFormReady) return false;
|
|
430
450
|
return true;
|
|
431
451
|
};
|
|
432
452
|
|
|
@@ -435,6 +455,7 @@ const EditPostFormComponent = ({
|
|
|
435
455
|
onClose,
|
|
436
456
|
onSuccess,
|
|
437
457
|
onDelete,
|
|
458
|
+
onFormReady,
|
|
438
459
|
}: EditPostFormProps) => {
|
|
439
460
|
const [featuredImageUploading, setFeaturedImageUploading] = useState(false);
|
|
440
461
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
|
@@ -537,6 +558,12 @@ const EditPostFormComponent = ({
|
|
|
537
558
|
values: initialData as z.input<typeof schema>,
|
|
538
559
|
});
|
|
539
560
|
|
|
561
|
+
// Expose form instance to parent for AI context integration
|
|
562
|
+
useEffect(() => {
|
|
563
|
+
onFormReady?.(form);
|
|
564
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
565
|
+
}, []);
|
|
566
|
+
|
|
540
567
|
if (!post) {
|
|
541
568
|
return <EmptyList message={localization.BLOG_PAGE_NOT_FOUND_DESCRIPTION} />;
|
|
542
569
|
}
|
|
@@ -7,6 +7,10 @@ import { PageWrapper } from "../shared/page-wrapper";
|
|
|
7
7
|
import { BLOG_LOCALIZATION } from "../../localization";
|
|
8
8
|
import type { BlogPluginOverrides } from "../../overrides";
|
|
9
9
|
import { useRouteLifecycle } from "@workspace/ui/hooks/use-route-lifecycle";
|
|
10
|
+
import { useRegisterPageAIContext } from "@btst/stack/plugins/ai-chat/client/context";
|
|
11
|
+
import { useRef, useCallback } from "react";
|
|
12
|
+
import type { UseFormReturn } from "react-hook-form";
|
|
13
|
+
import { createFillBlogFormHandler } from "./fill-blog-form-handler";
|
|
10
14
|
|
|
11
15
|
// Internal component with actual page content
|
|
12
16
|
export function EditPostPage({ slug }: { slug: string }) {
|
|
@@ -36,6 +40,29 @@ export function EditPostPage({ slug }: { slug: string }) {
|
|
|
36
40
|
},
|
|
37
41
|
});
|
|
38
42
|
|
|
43
|
+
// Ref to capture the form instance from EditPostForm via onFormReady callback
|
|
44
|
+
const formRef = useRef<UseFormReturn<any> | null>(null);
|
|
45
|
+
const handleFormReady = useCallback((form: UseFormReturn<any>) => {
|
|
46
|
+
formRef.current = form;
|
|
47
|
+
}, []);
|
|
48
|
+
|
|
49
|
+
// Register AI context so the chat can fill in the edit form
|
|
50
|
+
useRegisterPageAIContext({
|
|
51
|
+
routeName: "blog-edit-post",
|
|
52
|
+
pageDescription: `User is editing a blog post (slug: "${slug}") in the admin editor.`,
|
|
53
|
+
suggestions: [
|
|
54
|
+
"Improve this post's title",
|
|
55
|
+
"Rewrite the intro paragraph",
|
|
56
|
+
"Suggest better tags",
|
|
57
|
+
],
|
|
58
|
+
clientTools: {
|
|
59
|
+
fillBlogForm: createFillBlogFormHandler(
|
|
60
|
+
formRef,
|
|
61
|
+
"Form updated successfully",
|
|
62
|
+
),
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
|
|
39
66
|
const handleClose = () => {
|
|
40
67
|
navigate(`${basePath}/blog`);
|
|
41
68
|
};
|
|
@@ -61,6 +88,7 @@ export function EditPostPage({ slug }: { slug: string }) {
|
|
|
61
88
|
onClose={handleClose}
|
|
62
89
|
onSuccess={handleSuccess}
|
|
63
90
|
onDelete={handleDelete}
|
|
91
|
+
onFormReady={handleFormReady}
|
|
64
92
|
/>
|
|
65
93
|
</PageWrapper>
|
|
66
94
|
);
|