@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
|
@@ -17,6 +17,11 @@ import {
|
|
|
17
17
|
} from "../schemas";
|
|
18
18
|
import type { Conversation, ConversationWithMessages, Message } from "../types";
|
|
19
19
|
import { getAllConversations, getConversationById } from "./getters";
|
|
20
|
+
import {
|
|
21
|
+
BUILT_IN_PAGE_TOOL_ROUTE_ALLOWLIST,
|
|
22
|
+
BUILT_IN_PAGE_TOOL_SCHEMAS,
|
|
23
|
+
} from "./page-tools";
|
|
24
|
+
import { runHookWithShim } from "../../utils";
|
|
20
25
|
|
|
21
26
|
/**
|
|
22
27
|
* Context passed to AI Chat API hooks
|
|
@@ -36,48 +41,46 @@ export interface ChatApiContext<TBody = any, TParams = any, TQuery = any> {
|
|
|
36
41
|
*/
|
|
37
42
|
export interface AiChatBackendHooks {
|
|
38
43
|
// ============== Authorization Hooks ==============
|
|
39
|
-
//
|
|
44
|
+
// Throw an error to deny access
|
|
40
45
|
|
|
41
46
|
/**
|
|
42
|
-
* Called before processing a chat message.
|
|
47
|
+
* Called before processing a chat message. Throw an error to deny access.
|
|
43
48
|
* @param messages - Array of messages being sent
|
|
44
49
|
* @param context - Request context with headers, etc.
|
|
45
50
|
*/
|
|
46
51
|
onBeforeChat?: (
|
|
47
52
|
messages: Array<{ role: string; content: string }>,
|
|
48
53
|
context: ChatApiContext,
|
|
49
|
-
) => Promise<
|
|
54
|
+
) => Promise<void> | void;
|
|
50
55
|
|
|
51
56
|
/**
|
|
52
|
-
* Called before listing conversations.
|
|
57
|
+
* Called before listing conversations. Throw an error to deny access.
|
|
53
58
|
* @param context - Request context with headers, etc.
|
|
54
59
|
*/
|
|
55
|
-
onBeforeListConversations?: (
|
|
56
|
-
context: ChatApiContext,
|
|
57
|
-
) => Promise<boolean> | boolean;
|
|
60
|
+
onBeforeListConversations?: (context: ChatApiContext) => Promise<void> | void;
|
|
58
61
|
|
|
59
62
|
/**
|
|
60
|
-
* Called before getting a single conversation.
|
|
63
|
+
* Called before getting a single conversation. Throw an error to deny access.
|
|
61
64
|
* @param conversationId - ID of the conversation being accessed
|
|
62
65
|
* @param context - Request context with headers, etc.
|
|
63
66
|
*/
|
|
64
67
|
onBeforeGetConversation?: (
|
|
65
68
|
conversationId: string,
|
|
66
69
|
context: ChatApiContext,
|
|
67
|
-
) => Promise<
|
|
70
|
+
) => Promise<void> | void;
|
|
68
71
|
|
|
69
72
|
/**
|
|
70
|
-
* Called before creating a conversation.
|
|
73
|
+
* Called before creating a conversation. Throw an error to deny access.
|
|
71
74
|
* @param data - Conversation data being created
|
|
72
75
|
* @param context - Request context with headers, etc.
|
|
73
76
|
*/
|
|
74
77
|
onBeforeCreateConversation?: (
|
|
75
78
|
data: { id?: string; title?: string },
|
|
76
79
|
context: ChatApiContext,
|
|
77
|
-
) => Promise<
|
|
80
|
+
) => Promise<void> | void;
|
|
78
81
|
|
|
79
82
|
/**
|
|
80
|
-
* Called before updating a conversation.
|
|
83
|
+
* Called before updating a conversation. Throw an error to deny access.
|
|
81
84
|
* @param conversationId - ID of the conversation being updated
|
|
82
85
|
* @param data - Updated conversation data
|
|
83
86
|
* @param context - Request context with headers, etc.
|
|
@@ -86,17 +89,34 @@ export interface AiChatBackendHooks {
|
|
|
86
89
|
conversationId: string,
|
|
87
90
|
data: { title?: string },
|
|
88
91
|
context: ChatApiContext,
|
|
89
|
-
) => Promise<
|
|
92
|
+
) => Promise<void> | void;
|
|
90
93
|
|
|
91
94
|
/**
|
|
92
|
-
* Called before deleting a conversation.
|
|
95
|
+
* Called before deleting a conversation. Throw an error to deny access.
|
|
93
96
|
* @param conversationId - ID of the conversation being deleted
|
|
94
97
|
* @param context - Request context with headers, etc.
|
|
95
98
|
*/
|
|
96
99
|
onBeforeDeleteConversation?: (
|
|
97
100
|
conversationId: string,
|
|
98
101
|
context: ChatApiContext,
|
|
99
|
-
) => Promise<
|
|
102
|
+
) => Promise<void> | void;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Called after the structural routeName/allowlist validation, with the list
|
|
106
|
+
* of tool names that passed. Return a filtered subset to further restrict
|
|
107
|
+
* which tools the LLM sees, or return [] to suppress all page tools.
|
|
108
|
+
* Throw an Error to abort the entire chat request with a 403 response.
|
|
109
|
+
* Not called when no tools passed the structural validation step.
|
|
110
|
+
*
|
|
111
|
+
* @param toolNames - Names that passed the routeName allowlist check
|
|
112
|
+
* @param routeName - routeName claimed by the request (may be undefined)
|
|
113
|
+
* @param context - Full request context (headers, body, etc.)
|
|
114
|
+
*/
|
|
115
|
+
onBeforeToolsActivated?: (
|
|
116
|
+
toolNames: string[],
|
|
117
|
+
routeName: string | undefined,
|
|
118
|
+
context: ChatApiContext,
|
|
119
|
+
) => Promise<string[]> | string[];
|
|
100
120
|
|
|
101
121
|
// ============== Lifecycle Hooks ==============
|
|
102
122
|
|
|
@@ -232,6 +252,32 @@ export type AiChatMode = "authenticated" | "public";
|
|
|
232
252
|
/**
|
|
233
253
|
* Configuration for AI Chat backend plugin
|
|
234
254
|
*/
|
|
255
|
+
/**
|
|
256
|
+
* Extracts only the literal (non-index-signature) keys from a type.
|
|
257
|
+
* For `Record<string, T>` this resolves to `never`, so collision checks are
|
|
258
|
+
* skipped when the tools map is typed with a broad string index.
|
|
259
|
+
*/
|
|
260
|
+
type KnownKeys<T> = {
|
|
261
|
+
[K in keyof T]: string extends K ? never : K;
|
|
262
|
+
}[keyof T];
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Ensures `TClientTools` has no keys that are also literal keys in `TTools`.
|
|
266
|
+
* Colliding keys are mapped to `never`, which produces a compile-time error
|
|
267
|
+
* at the point of the duplicate key. When `TTools` uses a string index
|
|
268
|
+
* signature the check is skipped to avoid false positives.
|
|
269
|
+
*/
|
|
270
|
+
type NoKeyCollision<
|
|
271
|
+
TTools,
|
|
272
|
+
TClientTools extends Record<string, Tool>,
|
|
273
|
+
> = KnownKeys<TTools> & keyof TClientTools extends never
|
|
274
|
+
? TClientTools
|
|
275
|
+
: {
|
|
276
|
+
[K in keyof TClientTools]: K extends KnownKeys<TTools>
|
|
277
|
+
? never // duplicate of a server-side tool — remove from clientToolSchemas
|
|
278
|
+
: TClientTools[K];
|
|
279
|
+
};
|
|
280
|
+
|
|
235
281
|
export interface AiChatBackendConfig {
|
|
236
282
|
/**
|
|
237
283
|
* The language model to use for chat completions.
|
|
@@ -269,6 +315,31 @@ export interface AiChatBackendConfig {
|
|
|
269
315
|
*/
|
|
270
316
|
tools?: Record<string, Tool>;
|
|
271
317
|
|
|
318
|
+
/**
|
|
319
|
+
* Enable route-aware page tools.
|
|
320
|
+
* When true, the server will include tool schemas for client-side page tools
|
|
321
|
+
* (e.g. fillBlogForm, updatePageLayers) based on the availableTools list
|
|
322
|
+
* sent with each request.
|
|
323
|
+
* @default false
|
|
324
|
+
*/
|
|
325
|
+
enablePageTools?: boolean;
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Custom client-side tool schemas for non-BTST pages.
|
|
329
|
+
* Merged with built-in page tool schemas (fillBlogForm, updatePageLayers).
|
|
330
|
+
* Only included when enablePageTools is true and the tool name appears in
|
|
331
|
+
* the availableTools list sent with the request.
|
|
332
|
+
*
|
|
333
|
+
* @example
|
|
334
|
+
* clientToolSchemas: {
|
|
335
|
+
* addToCart: tool({
|
|
336
|
+
* description: "Add current product to cart",
|
|
337
|
+
* parameters: z.object({ quantity: z.number().int().min(1) }),
|
|
338
|
+
* }),
|
|
339
|
+
* }
|
|
340
|
+
*/
|
|
341
|
+
clientToolSchemas?: Record<string, Tool>;
|
|
342
|
+
|
|
272
343
|
/**
|
|
273
344
|
* Optional hooks for customizing plugin behavior
|
|
274
345
|
*/
|
|
@@ -282,7 +353,15 @@ export interface AiChatBackendConfig {
|
|
|
282
353
|
*
|
|
283
354
|
* @param config - Configuration including model, tools, and optional hooks
|
|
284
355
|
*/
|
|
285
|
-
export const aiChatBackendPlugin =
|
|
356
|
+
export const aiChatBackendPlugin = <
|
|
357
|
+
TTools extends Record<string, Tool> = Record<never, Tool>,
|
|
358
|
+
TClientTools extends Record<string, Tool> = Record<never, Tool>,
|
|
359
|
+
>(
|
|
360
|
+
config: Omit<AiChatBackendConfig, "tools" | "clientToolSchemas"> & {
|
|
361
|
+
tools?: TTools;
|
|
362
|
+
clientToolSchemas?: NoKeyCollision<TTools, TClientTools>;
|
|
363
|
+
},
|
|
364
|
+
) =>
|
|
286
365
|
defineBackendPlugin({
|
|
287
366
|
name: "ai-chat",
|
|
288
367
|
// Always include db schema - in public mode we just don't use it
|
|
@@ -350,7 +429,13 @@ export const aiChatBackendPlugin = (config: AiChatBackendConfig) =>
|
|
|
350
429
|
body: chatRequestSchema,
|
|
351
430
|
},
|
|
352
431
|
async (ctx) => {
|
|
353
|
-
const {
|
|
432
|
+
const {
|
|
433
|
+
messages: rawMessages,
|
|
434
|
+
conversationId,
|
|
435
|
+
pageContext,
|
|
436
|
+
availableTools,
|
|
437
|
+
routeName,
|
|
438
|
+
} = ctx.body;
|
|
354
439
|
const uiMessages = rawMessages as UIMessage[];
|
|
355
440
|
|
|
356
441
|
const context: ChatApiContext = {
|
|
@@ -366,15 +451,11 @@ export const aiChatBackendPlugin = (config: AiChatBackendConfig) =>
|
|
|
366
451
|
role: msg.role,
|
|
367
452
|
content: getMessageTextContent(msg),
|
|
368
453
|
}));
|
|
369
|
-
|
|
370
|
-
messagesForHook,
|
|
371
|
-
|
|
454
|
+
await runHookWithShim(
|
|
455
|
+
() => config.hooks!.onBeforeChat!(messagesForHook, context),
|
|
456
|
+
ctx.error,
|
|
457
|
+
"Unauthorized: Cannot start chat",
|
|
372
458
|
);
|
|
373
|
-
if (!canChat) {
|
|
374
|
-
throw ctx.error(403, {
|
|
375
|
-
message: "Unauthorized: Cannot start chat",
|
|
376
|
-
});
|
|
377
|
-
}
|
|
378
459
|
}
|
|
379
460
|
|
|
380
461
|
const firstMessage = uiMessages[0];
|
|
@@ -388,22 +469,107 @@ export const aiChatBackendPlugin = (config: AiChatBackendConfig) =>
|
|
|
388
469
|
// Convert UIMessages to CoreMessages for streamText
|
|
389
470
|
const modelMessages = convertToModelMessages(uiMessages);
|
|
390
471
|
|
|
391
|
-
//
|
|
392
|
-
const
|
|
472
|
+
// Build system prompt: base config + optional page context
|
|
473
|
+
const pageContextContent =
|
|
474
|
+
pageContext && pageContext.trim()
|
|
475
|
+
? `\n\nCurrent page context:\n${pageContext}`
|
|
476
|
+
: "";
|
|
477
|
+
const systemContent = config.systemPrompt
|
|
478
|
+
? `${config.systemPrompt}${pageContextContent}`
|
|
479
|
+
: pageContextContent || undefined;
|
|
480
|
+
|
|
481
|
+
const messagesWithSystem = systemContent
|
|
393
482
|
? [
|
|
394
|
-
{ role: "system" as const, content:
|
|
483
|
+
{ role: "system" as const, content: systemContent },
|
|
395
484
|
...modelMessages,
|
|
396
485
|
]
|
|
397
486
|
: modelMessages;
|
|
398
487
|
|
|
488
|
+
// Merge page tool schemas when enablePageTools is on.
|
|
489
|
+
// Built-in schemas are only included when the request's routeName is in
|
|
490
|
+
// the tool's allowlist — this prevents a page from claiming tools that
|
|
491
|
+
// are intended for a different route (e.g. requesting updatePageLayers
|
|
492
|
+
// from a blog page). Consumer clientToolSchemas are trusted as-is.
|
|
493
|
+
const activePageTools: Record<string, Tool> =
|
|
494
|
+
config.enablePageTools &&
|
|
495
|
+
availableTools &&
|
|
496
|
+
availableTools.length > 0
|
|
497
|
+
? (() => {
|
|
498
|
+
const consumerSchemas: Record<string, Tool> =
|
|
499
|
+
(config.clientToolSchemas as Record<string, Tool>) ?? {};
|
|
500
|
+
return Object.fromEntries(
|
|
501
|
+
availableTools
|
|
502
|
+
.filter((name) => {
|
|
503
|
+
// Built-in tool: require routeName to be in its allowlist
|
|
504
|
+
if (name in BUILT_IN_PAGE_TOOL_SCHEMAS) {
|
|
505
|
+
const allowed =
|
|
506
|
+
BUILT_IN_PAGE_TOOL_ROUTE_ALLOWLIST[name];
|
|
507
|
+
return (
|
|
508
|
+
allowed &&
|
|
509
|
+
routeName &&
|
|
510
|
+
allowed.includes(routeName)
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
// Consumer-defined tool: allow if schema is registered
|
|
514
|
+
return name in consumerSchemas;
|
|
515
|
+
})
|
|
516
|
+
.map((name) => {
|
|
517
|
+
const schema =
|
|
518
|
+
BUILT_IN_PAGE_TOOL_SCHEMAS[name] ??
|
|
519
|
+
consumerSchemas[name]!;
|
|
520
|
+
return [name, schema];
|
|
521
|
+
}),
|
|
522
|
+
);
|
|
523
|
+
})()
|
|
524
|
+
: {};
|
|
525
|
+
|
|
526
|
+
// Consumer hook: user-level tool authorization.
|
|
527
|
+
// Runs after the structural routeName allowlist check.
|
|
528
|
+
// A thrown Error is caught and returned as a 403 response,
|
|
529
|
+
// consistent with how onBeforeChat handles return false → 403.
|
|
530
|
+
if (
|
|
531
|
+
config.hooks?.onBeforeToolsActivated &&
|
|
532
|
+
Object.keys(activePageTools).length > 0
|
|
533
|
+
) {
|
|
534
|
+
try {
|
|
535
|
+
const allowed = await config.hooks.onBeforeToolsActivated(
|
|
536
|
+
Object.keys(activePageTools),
|
|
537
|
+
routeName,
|
|
538
|
+
context,
|
|
539
|
+
);
|
|
540
|
+
const allowedSet = new Set(allowed);
|
|
541
|
+
for (const key of Object.keys(activePageTools)) {
|
|
542
|
+
if (!allowedSet.has(key)) {
|
|
543
|
+
delete activePageTools[key];
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
} catch (hookError) {
|
|
547
|
+
throw ctx.error(403, {
|
|
548
|
+
message:
|
|
549
|
+
hookError instanceof Error
|
|
550
|
+
? hookError.message
|
|
551
|
+
: "Unauthorized: Tool activation denied",
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Page tools are layered under server-side tools so that a
|
|
557
|
+
// clientToolSchemas entry with the same name as a tool in
|
|
558
|
+
// config.tools never silently drops its `execute` function.
|
|
559
|
+
// Server-side tools always win on collision.
|
|
560
|
+
const mergedTools =
|
|
561
|
+
Object.keys(activePageTools).length > 0
|
|
562
|
+
? { ...activePageTools, ...config.tools }
|
|
563
|
+
: config.tools;
|
|
564
|
+
|
|
399
565
|
// PUBLIC MODE: Stream without persistence
|
|
400
566
|
if (isPublicMode) {
|
|
401
567
|
const result = streamText({
|
|
402
568
|
model: config.model,
|
|
403
569
|
messages: messagesWithSystem,
|
|
404
|
-
tools:
|
|
570
|
+
tools: mergedTools,
|
|
405
571
|
// Enable multi-step tool calls if tools are configured
|
|
406
|
-
...(
|
|
572
|
+
...(mergedTools ? { stopWhen: stepCountIs(5) } : {}),
|
|
407
573
|
});
|
|
408
574
|
|
|
409
575
|
return result.toUIMessageStreamResponse({
|
|
@@ -557,9 +723,9 @@ export const aiChatBackendPlugin = (config: AiChatBackendConfig) =>
|
|
|
557
723
|
const result = streamText({
|
|
558
724
|
model: config.model,
|
|
559
725
|
messages: messagesWithSystem,
|
|
560
|
-
tools:
|
|
726
|
+
tools: mergedTools,
|
|
561
727
|
// Enable multi-step tool calls if tools are configured
|
|
562
|
-
...(
|
|
728
|
+
...(mergedTools ? { stopWhen: stepCountIs(5) } : {}),
|
|
563
729
|
onFinish: async (completion: { text: string }) => {
|
|
564
730
|
// Wrap in try-catch since this runs after the response is sent
|
|
565
731
|
// and errors would otherwise become unhandled promise rejections
|
|
@@ -677,15 +843,15 @@ export const aiChatBackendPlugin = (config: AiChatBackendConfig) =>
|
|
|
677
843
|
|
|
678
844
|
// Authorization hook
|
|
679
845
|
if (config.hooks?.onBeforeCreateConversation) {
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
846
|
+
await runHookWithShim(
|
|
847
|
+
() =>
|
|
848
|
+
config.hooks!.onBeforeCreateConversation!(
|
|
849
|
+
{ id, title },
|
|
850
|
+
context,
|
|
851
|
+
),
|
|
852
|
+
ctx.error,
|
|
853
|
+
"Unauthorized: Cannot create conversation",
|
|
683
854
|
);
|
|
684
|
-
if (!canCreate) {
|
|
685
|
-
throw ctx.error(403, {
|
|
686
|
-
message: "Unauthorized: Cannot create conversation",
|
|
687
|
-
});
|
|
688
|
-
}
|
|
689
855
|
}
|
|
690
856
|
|
|
691
857
|
const newConv = await adapter.create<Conversation>({
|
|
@@ -743,13 +909,11 @@ export const aiChatBackendPlugin = (config: AiChatBackendConfig) =>
|
|
|
743
909
|
|
|
744
910
|
// Authorization hook
|
|
745
911
|
if (config.hooks?.onBeforeListConversations) {
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
});
|
|
752
|
-
}
|
|
912
|
+
await runHookWithShim(
|
|
913
|
+
() => config.hooks!.onBeforeListConversations!(context),
|
|
914
|
+
ctx.error,
|
|
915
|
+
"Unauthorized: Cannot list conversations",
|
|
916
|
+
);
|
|
753
917
|
}
|
|
754
918
|
|
|
755
919
|
// Build where conditions - filter by userId if set
|
|
@@ -820,15 +984,11 @@ export const aiChatBackendPlugin = (config: AiChatBackendConfig) =>
|
|
|
820
984
|
|
|
821
985
|
// Authorization hook
|
|
822
986
|
if (config.hooks?.onBeforeGetConversation) {
|
|
823
|
-
|
|
824
|
-
id,
|
|
825
|
-
|
|
987
|
+
await runHookWithShim(
|
|
988
|
+
() => config.hooks!.onBeforeGetConversation!(id, context),
|
|
989
|
+
ctx.error,
|
|
990
|
+
"Unauthorized: Cannot get conversation",
|
|
826
991
|
);
|
|
827
|
-
if (!canGet) {
|
|
828
|
-
throw ctx.error(403, {
|
|
829
|
-
message: "Unauthorized: Cannot get conversation",
|
|
830
|
-
});
|
|
831
|
-
}
|
|
832
992
|
}
|
|
833
993
|
|
|
834
994
|
// Fetch conversation with messages in a single query using join
|
|
@@ -945,16 +1105,16 @@ export const aiChatBackendPlugin = (config: AiChatBackendConfig) =>
|
|
|
945
1105
|
|
|
946
1106
|
// Authorization hook
|
|
947
1107
|
if (config.hooks?.onBeforeUpdateConversation) {
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
1108
|
+
await runHookWithShim(
|
|
1109
|
+
() =>
|
|
1110
|
+
config.hooks!.onBeforeUpdateConversation!(
|
|
1111
|
+
id,
|
|
1112
|
+
{ title },
|
|
1113
|
+
context,
|
|
1114
|
+
),
|
|
1115
|
+
ctx.error,
|
|
1116
|
+
"Unauthorized: Cannot update conversation",
|
|
952
1117
|
);
|
|
953
|
-
if (!canUpdate) {
|
|
954
|
-
throw ctx.error(403, {
|
|
955
|
-
message: "Unauthorized: Cannot update conversation",
|
|
956
|
-
});
|
|
957
|
-
}
|
|
958
1118
|
}
|
|
959
1119
|
|
|
960
1120
|
const updated = await adapter.update<Conversation>({
|
|
@@ -1040,15 +1200,11 @@ export const aiChatBackendPlugin = (config: AiChatBackendConfig) =>
|
|
|
1040
1200
|
|
|
1041
1201
|
// Authorization hook
|
|
1042
1202
|
if (config.hooks?.onBeforeDeleteConversation) {
|
|
1043
|
-
|
|
1044
|
-
id,
|
|
1045
|
-
|
|
1203
|
+
await runHookWithShim(
|
|
1204
|
+
() => config.hooks!.onBeforeDeleteConversation!(id, context),
|
|
1205
|
+
ctx.error,
|
|
1206
|
+
"Unauthorized: Cannot delete conversation",
|
|
1046
1207
|
);
|
|
1047
|
-
if (!canDelete) {
|
|
1048
|
-
throw ctx.error(403, {
|
|
1049
|
-
message: "Unauthorized: Cannot delete conversation",
|
|
1050
|
-
});
|
|
1051
|
-
}
|
|
1052
1208
|
}
|
|
1053
1209
|
|
|
1054
1210
|
// Messages are automatically deleted via cascade (onDelete: "cascade")
|
|
@@ -260,14 +260,14 @@ export function ChatInput({
|
|
|
260
260
|
)}
|
|
261
261
|
|
|
262
262
|
{/* Text Input */}
|
|
263
|
-
<div className="relative flex-1">
|
|
263
|
+
<div className="relative flex-1 min-w-0">
|
|
264
264
|
<Textarea
|
|
265
265
|
value={input}
|
|
266
266
|
onChange={handleInputChange}
|
|
267
267
|
onKeyDown={handleKeyDown}
|
|
268
268
|
placeholder={placeholder || localization.CHAT_PLACEHOLDER}
|
|
269
269
|
className={cn(
|
|
270
|
-
"resize-none pr-12",
|
|
270
|
+
"resize-none pr-12 max-w-full",
|
|
271
271
|
isCompact
|
|
272
272
|
? "min-h-[40px] max-h-[120px] py-2"
|
|
273
273
|
: "min-h-[50px] max-h-[200px] py-3",
|