@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
@@ -16,6 +16,7 @@ const index = require('../localization/index.cjs');
16
16
  const client = require('@btst/stack/plugins/client');
17
17
  const plugins_aiChat_queryKeys = require('../../../../../../../plugins/ai-chat/query-keys.cjs');
18
18
  const chatHooks = require('../hooks/chat-hooks.cjs');
19
+ const plugins_aiChat_client_context_pageAiContext = require('../../../../../../../plugins/ai-chat/client/context/page-ai-context.cjs');
19
20
 
20
21
  function ChatInterface({
21
22
  apiPath = "/api/chat",
@@ -40,6 +41,7 @@ function ChatInterface({
40
41
  );
41
42
  const basePath = context.useBasePath();
42
43
  const isPublicMode = mode === "public";
44
+ const pageAIContext = plugins_aiChat_client_context_pageAiContext.usePageAIContext();
43
45
  const localization = { ...index.AI_CHAT_LOCALIZATION, ...customLocalization };
44
46
  const queryClient = reactQuery.useQueryClient();
45
47
  const conversationsListQueryKey = React.useMemo(() => {
@@ -83,13 +85,25 @@ function ChatInterface({
83
85
  !initialMessages || initialMessages.length === 0
84
86
  )
85
87
  );
88
+ const pageAIContextRef = React.useRef(pageAIContext);
89
+ React.useEffect(() => {
90
+ pageAIContextRef.current = pageAIContext;
91
+ }, [pageAIContext]);
86
92
  const transport = React.useMemo(
87
93
  () => new ai.DefaultChatTransport({
88
94
  api: apiPath,
89
95
  // In public mode, don't send conversationId
90
96
  body: isPublicMode ? void 0 : () => ({ conversationId: conversationIdRef.current }),
91
- // Handle edit operations by using truncated messages from the ref
97
+ // Handle edit operations and inject page context
92
98
  prepareSendMessagesRequest: ({ messages: hookMessages }) => {
99
+ const currentPageContext = pageAIContextRef.current;
100
+ const pageContextBody = currentPageContext?.pageDescription ? {
101
+ pageContext: currentPageContext.pageDescription,
102
+ availableTools: Object.keys(
103
+ currentPageContext.clientTools ?? {}
104
+ ),
105
+ routeName: currentPageContext.routeName
106
+ } : {};
93
107
  if (editMessagesRef.current !== null) {
94
108
  const newUserMessage = hookMessages[hookMessages.length - 1];
95
109
  const messagesToSend = [...editMessagesRef.current];
@@ -100,22 +114,63 @@ function ChatInterface({
100
114
  return {
101
115
  body: {
102
116
  messages: messagesToSend,
103
- conversationId: conversationIdRef.current
117
+ conversationId: conversationIdRef.current,
118
+ ...pageContextBody
104
119
  }
105
120
  };
106
121
  }
107
122
  return {
108
123
  body: {
109
124
  messages: hookMessages,
110
- conversationId: conversationIdRef.current
125
+ conversationId: conversationIdRef.current,
126
+ ...pageContextBody
111
127
  }
112
128
  };
113
129
  }
114
130
  }),
115
131
  [apiPath, isPublicMode]
116
132
  );
117
- const { messages, sendMessage, status, error, setMessages, regenerate } = react.useChat({
133
+ const addToolOutputRef = React.useRef(null);
134
+ const {
135
+ messages,
136
+ sendMessage,
137
+ status,
138
+ error,
139
+ setMessages,
140
+ regenerate,
141
+ addToolOutput
142
+ } = react.useChat({
118
143
  transport,
144
+ // Automatically resubmit after all client-side tool results are provided
145
+ sendAutomaticallyWhen: ai.lastAssistantMessageIsCompleteWithToolCalls,
146
+ onToolCall: async ({ toolCall }) => {
147
+ const toolName = toolCall.toolName;
148
+ const handler = pageAIContextRef.current?.clientTools?.[toolName];
149
+ if (handler) {
150
+ try {
151
+ const result = await handler(toolCall.input);
152
+ addToolOutputRef.current?.({
153
+ tool: toolName,
154
+ toolCallId: toolCall.toolCallId,
155
+ output: result
156
+ });
157
+ } catch (err) {
158
+ addToolOutputRef.current?.({
159
+ tool: toolName,
160
+ toolCallId: toolCall.toolCallId,
161
+ state: "output-error",
162
+ errorText: err instanceof Error ? err.message : "Tool execution failed"
163
+ });
164
+ }
165
+ } else {
166
+ addToolOutputRef.current?.({
167
+ tool: toolName,
168
+ toolCallId: toolCall.toolCallId,
169
+ state: "output-error",
170
+ errorText: `No client-side handler registered for tool "${toolName}". The page context may have changed while the response was streaming.`
171
+ });
172
+ }
173
+ },
119
174
  onError: (err) => {
120
175
  console.error("useChat onError:", err);
121
176
  if (!id && !hasNavigatedRef.current) {
@@ -138,19 +193,24 @@ function ChatInterface({
138
193
  if (newConversation) {
139
194
  setCurrentConversationId(newConversation.id);
140
195
  conversationIdRef.current = newConversation.id;
141
- const newUrl = `${basePath}/chat/${newConversation.id}`;
142
- if (typeof window !== "undefined") {
143
- window.history.replaceState(
144
- { ...window.history.state },
145
- "",
146
- newUrl
147
- );
196
+ if (variant === "full") {
197
+ const newUrl = `${basePath}/chat/${newConversation.id}`;
198
+ if (typeof window !== "undefined") {
199
+ window.history.replaceState(
200
+ { ...window.history.state },
201
+ "",
202
+ newUrl
203
+ );
204
+ }
148
205
  }
149
206
  }
150
207
  }
151
208
  }
152
209
  }
153
210
  });
211
+ React.useEffect(() => {
212
+ addToolOutputRef.current = addToolOutput;
213
+ }, [addToolOutput]);
154
214
  React.useEffect(() => {
155
215
  if (isEditInProgressRef.current) {
156
216
  return;
@@ -313,17 +373,24 @@ function ChatInterface({
313
373
  ),
314
374
  children: [
315
375
  messages.length === 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col h-full min-h-[300px]", children: [
316
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 flex items-center justify-center text-muted-foreground", children: /* @__PURE__ */ jsxRuntime.jsx("p", { children: localization.CHAT_EMPTY_STATE }) }),
317
- chatSuggestions && chatSuggestions.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap justify-center gap-2 pb-4 max-w-md mx-auto", children: chatSuggestions.map((suggestion, index) => /* @__PURE__ */ jsxRuntime.jsx(
318
- "button",
319
- {
320
- type: "button",
321
- onClick: () => setInput(suggestion),
322
- className: "px-3 py-2 text-sm rounded-lg border border-border bg-background hover:bg-accent hover:text-accent-foreground transition-colors text-foreground",
323
- children: suggestion
324
- },
325
- index
326
- )) })
376
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 flex items-center justify-center text-muted-foreground mb-4", children: /* @__PURE__ */ jsxRuntime.jsx("p", { children: localization.CHAT_EMPTY_STATE }) }),
377
+ (() => {
378
+ const pageSuggestions = pageAIContext?.suggestions ?? [];
379
+ const allSuggestions = [
380
+ ...pageSuggestions,
381
+ ...chatSuggestions ?? []
382
+ ];
383
+ return allSuggestions.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap justify-center gap-2 pb-4 max-w-md mx-auto", children: allSuggestions.map((suggestion, index) => /* @__PURE__ */ jsxRuntime.jsx(
384
+ "button",
385
+ {
386
+ type: "button",
387
+ onClick: () => setInput(suggestion),
388
+ className: "px-3 py-2 text-sm rounded-lg border border-border bg-background hover:bg-accent hover:text-accent-foreground transition-colors text-foreground",
389
+ children: suggestion
390
+ },
391
+ index
392
+ )) }) : null;
393
+ })()
327
394
  ] }) : messages.map((m, index) => /* @__PURE__ */ jsxRuntime.jsx(
328
395
  chatMessage.ChatMessage,
329
396
  {
@@ -7,13 +7,14 @@ import { ChatMessage } from './chat-message.mjs';
7
7
  import { ChatInput } from './chat-input.mjs';
8
8
  import { StackAttribution } from '../../../../../../ui/src/components/stack-attribution.mjs';
9
9
  import { ScrollArea } from '../../../../../../ui/src/components/scroll-area.mjs';
10
- import { DefaultChatTransport } from 'ai';
10
+ import { DefaultChatTransport, lastAssistantMessageIsCompleteWithToolCalls } from 'ai';
11
11
  import { cn } from '../../../../../../ui/src/lib/utils.mjs';
12
12
  import { usePluginOverrides, useBasePath } from '@btst/stack/context';
13
13
  import { AI_CHAT_LOCALIZATION } from '../localization/index.mjs';
14
14
  import { createApiClient } from '@btst/stack/plugins/client';
15
15
  import { createAiChatQueryKeys } from '../../../../../../../plugins/ai-chat/query-keys.mjs';
16
16
  import { useConversation, useConversations } from '../hooks/chat-hooks.mjs';
17
+ import { usePageAIContext } from '../../../../../../../plugins/ai-chat/client/context/page-ai-context.mjs';
17
18
 
18
19
  function ChatInterface({
19
20
  apiPath = "/api/chat",
@@ -38,6 +39,7 @@ function ChatInterface({
38
39
  );
39
40
  const basePath = useBasePath();
40
41
  const isPublicMode = mode === "public";
42
+ const pageAIContext = usePageAIContext();
41
43
  const localization = { ...AI_CHAT_LOCALIZATION, ...customLocalization };
42
44
  const queryClient = useQueryClient();
43
45
  const conversationsListQueryKey = useMemo(() => {
@@ -81,13 +83,25 @@ function ChatInterface({
81
83
  !initialMessages || initialMessages.length === 0
82
84
  )
83
85
  );
86
+ const pageAIContextRef = useRef(pageAIContext);
87
+ useEffect(() => {
88
+ pageAIContextRef.current = pageAIContext;
89
+ }, [pageAIContext]);
84
90
  const transport = useMemo(
85
91
  () => new DefaultChatTransport({
86
92
  api: apiPath,
87
93
  // In public mode, don't send conversationId
88
94
  body: isPublicMode ? void 0 : () => ({ conversationId: conversationIdRef.current }),
89
- // Handle edit operations by using truncated messages from the ref
95
+ // Handle edit operations and inject page context
90
96
  prepareSendMessagesRequest: ({ messages: hookMessages }) => {
97
+ const currentPageContext = pageAIContextRef.current;
98
+ const pageContextBody = currentPageContext?.pageDescription ? {
99
+ pageContext: currentPageContext.pageDescription,
100
+ availableTools: Object.keys(
101
+ currentPageContext.clientTools ?? {}
102
+ ),
103
+ routeName: currentPageContext.routeName
104
+ } : {};
91
105
  if (editMessagesRef.current !== null) {
92
106
  const newUserMessage = hookMessages[hookMessages.length - 1];
93
107
  const messagesToSend = [...editMessagesRef.current];
@@ -98,22 +112,63 @@ function ChatInterface({
98
112
  return {
99
113
  body: {
100
114
  messages: messagesToSend,
101
- conversationId: conversationIdRef.current
115
+ conversationId: conversationIdRef.current,
116
+ ...pageContextBody
102
117
  }
103
118
  };
104
119
  }
105
120
  return {
106
121
  body: {
107
122
  messages: hookMessages,
108
- conversationId: conversationIdRef.current
123
+ conversationId: conversationIdRef.current,
124
+ ...pageContextBody
109
125
  }
110
126
  };
111
127
  }
112
128
  }),
113
129
  [apiPath, isPublicMode]
114
130
  );
115
- const { messages, sendMessage, status, error, setMessages, regenerate } = useChat({
131
+ const addToolOutputRef = useRef(null);
132
+ const {
133
+ messages,
134
+ sendMessage,
135
+ status,
136
+ error,
137
+ setMessages,
138
+ regenerate,
139
+ addToolOutput
140
+ } = useChat({
116
141
  transport,
142
+ // Automatically resubmit after all client-side tool results are provided
143
+ sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls,
144
+ onToolCall: async ({ toolCall }) => {
145
+ const toolName = toolCall.toolName;
146
+ const handler = pageAIContextRef.current?.clientTools?.[toolName];
147
+ if (handler) {
148
+ try {
149
+ const result = await handler(toolCall.input);
150
+ addToolOutputRef.current?.({
151
+ tool: toolName,
152
+ toolCallId: toolCall.toolCallId,
153
+ output: result
154
+ });
155
+ } catch (err) {
156
+ addToolOutputRef.current?.({
157
+ tool: toolName,
158
+ toolCallId: toolCall.toolCallId,
159
+ state: "output-error",
160
+ errorText: err instanceof Error ? err.message : "Tool execution failed"
161
+ });
162
+ }
163
+ } else {
164
+ addToolOutputRef.current?.({
165
+ tool: toolName,
166
+ toolCallId: toolCall.toolCallId,
167
+ state: "output-error",
168
+ errorText: `No client-side handler registered for tool "${toolName}". The page context may have changed while the response was streaming.`
169
+ });
170
+ }
171
+ },
117
172
  onError: (err) => {
118
173
  console.error("useChat onError:", err);
119
174
  if (!id && !hasNavigatedRef.current) {
@@ -136,19 +191,24 @@ function ChatInterface({
136
191
  if (newConversation) {
137
192
  setCurrentConversationId(newConversation.id);
138
193
  conversationIdRef.current = newConversation.id;
139
- const newUrl = `${basePath}/chat/${newConversation.id}`;
140
- if (typeof window !== "undefined") {
141
- window.history.replaceState(
142
- { ...window.history.state },
143
- "",
144
- newUrl
145
- );
194
+ if (variant === "full") {
195
+ const newUrl = `${basePath}/chat/${newConversation.id}`;
196
+ if (typeof window !== "undefined") {
197
+ window.history.replaceState(
198
+ { ...window.history.state },
199
+ "",
200
+ newUrl
201
+ );
202
+ }
146
203
  }
147
204
  }
148
205
  }
149
206
  }
150
207
  }
151
208
  });
209
+ useEffect(() => {
210
+ addToolOutputRef.current = addToolOutput;
211
+ }, [addToolOutput]);
152
212
  useEffect(() => {
153
213
  if (isEditInProgressRef.current) {
154
214
  return;
@@ -311,17 +371,24 @@ function ChatInterface({
311
371
  ),
312
372
  children: [
313
373
  messages.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col h-full min-h-[300px]", children: [
314
- /* @__PURE__ */ jsx("div", { className: "flex-1 flex items-center justify-center text-muted-foreground", children: /* @__PURE__ */ jsx("p", { children: localization.CHAT_EMPTY_STATE }) }),
315
- chatSuggestions && chatSuggestions.length > 0 && /* @__PURE__ */ jsx("div", { className: "flex flex-wrap justify-center gap-2 pb-4 max-w-md mx-auto", children: chatSuggestions.map((suggestion, index) => /* @__PURE__ */ jsx(
316
- "button",
317
- {
318
- type: "button",
319
- onClick: () => setInput(suggestion),
320
- className: "px-3 py-2 text-sm rounded-lg border border-border bg-background hover:bg-accent hover:text-accent-foreground transition-colors text-foreground",
321
- children: suggestion
322
- },
323
- index
324
- )) })
374
+ /* @__PURE__ */ jsx("div", { className: "flex-1 flex items-center justify-center text-muted-foreground mb-4", children: /* @__PURE__ */ jsx("p", { children: localization.CHAT_EMPTY_STATE }) }),
375
+ (() => {
376
+ const pageSuggestions = pageAIContext?.suggestions ?? [];
377
+ const allSuggestions = [
378
+ ...pageSuggestions,
379
+ ...chatSuggestions ?? []
380
+ ];
381
+ return allSuggestions.length > 0 ? /* @__PURE__ */ jsx("div", { className: "flex flex-wrap justify-center gap-2 pb-4 max-w-md mx-auto", children: allSuggestions.map((suggestion, index) => /* @__PURE__ */ jsx(
382
+ "button",
383
+ {
384
+ type: "button",
385
+ onClick: () => setInput(suggestion),
386
+ className: "px-3 py-2 text-sm rounded-lg border border-border bg-background hover:bg-accent hover:text-accent-foreground transition-colors text-foreground",
387
+ children: suggestion
388
+ },
389
+ index
390
+ )) }) : null;
391
+ })()
325
392
  ] }) : messages.map((m, index) => /* @__PURE__ */ jsx(
326
393
  ChatMessage,
327
394
  {
@@ -4,26 +4,36 @@
4
4
  const jsxRuntime = require('react/jsx-runtime');
5
5
  const React = require('react');
6
6
  const button = require('../../../../../../ui/src/components/button.cjs');
7
+ const badge = require('../../../../../../ui/src/components/badge.cjs');
7
8
  const sheet = require('../../../../../../ui/src/components/sheet.cjs');
8
9
  const LucideIcons = require('lucide-react');
9
10
  const utils = require('../../../../../../ui/src/lib/utils.cjs');
10
11
  const chatSidebar = require('./chat-sidebar.cjs');
11
12
  const chatInterface = require('./chat-interface.cjs');
13
+ const plugins_aiChat_client_context_pageAiContext = require('../../../../../../../plugins/ai-chat/client/context/page-ai-context.cjs');
12
14
 
13
- function ChatLayout({
14
- apiBaseURL,
15
- apiBasePath,
16
- conversationId,
17
- layout = "full",
18
- className,
19
- showSidebar = true,
20
- widgetHeight = "600px",
21
- initialMessages,
22
- onMessagesChange
23
- }) {
15
+ function ChatLayout(props) {
16
+ const {
17
+ apiBaseURL,
18
+ apiBasePath,
19
+ conversationId,
20
+ layout = "full",
21
+ className,
22
+ showSidebar = true,
23
+ initialMessages,
24
+ onMessagesChange
25
+ } = props;
26
+ const widgetHeight = props.layout === "widget" ? props.widgetHeight ?? "600px" : "600px";
27
+ const widgetWidth = props.layout === "widget" ? props.widgetWidth ?? "380px" : "380px";
28
+ const defaultOpen = props.layout === "widget" ? props.defaultOpen ?? false : false;
29
+ const showTrigger = props.layout === "widget" ? props.showTrigger ?? true : true;
24
30
  const [sidebarOpen, setSidebarOpen] = React.useState(true);
25
31
  const [mobileSidebarOpen, setMobileSidebarOpen] = React.useState(false);
26
32
  const [chatResetKey, setChatResetKey] = React.useState(0);
33
+ const [widgetOpen, setWidgetOpen] = React.useState(defaultOpen);
34
+ const [widgetResetKey, setWidgetResetKey] = React.useState(0);
35
+ const [widgetEverOpened, setWidgetEverOpened] = React.useState(defaultOpen);
36
+ const pageAIContext = plugins_aiChat_client_context_pageAiContext.usePageAIContext();
27
37
  const apiPath = `${apiBaseURL}${apiBasePath}/chat`;
28
38
  const handleNewChat = React.useCallback(() => {
29
39
  if (!conversationId) {
@@ -31,26 +41,81 @@ function ChatLayout({
31
41
  }
32
42
  }, [conversationId]);
33
43
  if (layout === "widget") {
34
- return /* @__PURE__ */ jsxRuntime.jsx(
35
- "div",
36
- {
37
- className: utils.cn(
38
- "flex flex-col w-full border rounded-xl overflow-hidden bg-background shadow-sm",
39
- className
40
- ),
41
- style: { height: widgetHeight },
42
- children: /* @__PURE__ */ jsxRuntime.jsx(
43
- chatInterface.ChatInterface,
44
- {
45
- apiPath,
46
- id: conversationId,
47
- variant: "widget",
48
- initialMessages,
49
- onMessagesChange
50
- }
51
- )
52
- }
53
- );
44
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: utils.cn("flex flex-col items-end gap-3", className), children: [
45
+ /* @__PURE__ */ jsxRuntime.jsxs(
46
+ "div",
47
+ {
48
+ className: utils.cn(
49
+ "flex flex-col border rounded-xl overflow-hidden bg-background shadow-xl",
50
+ widgetOpen ? "flex" : "hidden"
51
+ ),
52
+ style: { height: widgetHeight, width: widgetWidth },
53
+ children: [
54
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5 px-3 py-1.5 border-b bg-muted/40", children: [
55
+ /* @__PURE__ */ jsxRuntime.jsx(LucideIcons.Sparkles, { className: "h-3 w-3 text-muted-foreground" }),
56
+ pageAIContext ? /* @__PURE__ */ jsxRuntime.jsx(
57
+ badge.Badge,
58
+ {
59
+ variant: "secondary",
60
+ className: "text-xs",
61
+ "data-testid": "page-context-badge",
62
+ children: pageAIContext.routeName
63
+ }
64
+ ) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-muted-foreground font-medium", children: "AI Chat" }),
65
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1" }),
66
+ /* @__PURE__ */ jsxRuntime.jsx(
67
+ button.Button,
68
+ {
69
+ variant: "ghost",
70
+ size: "icon",
71
+ className: "h-5 w-5",
72
+ onClick: () => setWidgetResetKey((prev) => prev + 1),
73
+ "aria-label": "Clear chat",
74
+ title: "Clear chat",
75
+ children: /* @__PURE__ */ jsxRuntime.jsx(LucideIcons.Trash2, { className: "h-3.5 w-3.5" })
76
+ }
77
+ ),
78
+ /* @__PURE__ */ jsxRuntime.jsx(
79
+ button.Button,
80
+ {
81
+ variant: "ghost",
82
+ size: "icon",
83
+ className: "h-5 w-5",
84
+ onClick: () => setWidgetOpen(false),
85
+ "aria-label": "Close chat",
86
+ children: /* @__PURE__ */ jsxRuntime.jsx(LucideIcons.X, { className: "h-3.5 w-3.5" })
87
+ }
88
+ )
89
+ ] }),
90
+ widgetEverOpened && /* @__PURE__ */ jsxRuntime.jsx(
91
+ chatInterface.ChatInterface,
92
+ {
93
+ apiPath,
94
+ id: conversationId,
95
+ variant: "widget",
96
+ initialMessages,
97
+ onMessagesChange
98
+ },
99
+ `widget-${conversationId ?? "new"}-${widgetResetKey}`
100
+ )
101
+ ]
102
+ }
103
+ ),
104
+ showTrigger && /* @__PURE__ */ jsxRuntime.jsx(
105
+ button.Button,
106
+ {
107
+ size: "icon",
108
+ className: "h-12 w-12 rounded-full shadow-lg",
109
+ onClick: () => {
110
+ setWidgetOpen((prev) => !prev);
111
+ setWidgetEverOpened(true);
112
+ },
113
+ "aria-label": widgetOpen ? "Close chat" : "Open chat",
114
+ "data-testid": "widget-trigger",
115
+ children: widgetOpen ? /* @__PURE__ */ jsxRuntime.jsx(LucideIcons.X, { className: "h-5 w-5" }) : /* @__PURE__ */ jsxRuntime.jsx(LucideIcons.Sparkles, { className: "h-5 w-5" })
116
+ }
117
+ )
118
+ ] });
54
119
  }
55
120
  return /* @__PURE__ */ jsxRuntime.jsxs(
56
121
  "div",
@@ -79,7 +144,7 @@ function ChatLayout({
79
144
  }
80
145
  ),
81
146
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 flex flex-col min-w-0", children: [
82
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 p-2 border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60", children: [
147
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 p-2 border-b bg-background/95 backdrop-blur supports-backdrop-filter:bg-background/60", children: [
83
148
  showSidebar && /* @__PURE__ */ jsxRuntime.jsxs(sheet.Sheet, { open: mobileSidebarOpen, onOpenChange: setMobileSidebarOpen, children: [
84
149
  /* @__PURE__ */ jsxRuntime.jsx(sheet.SheetTrigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsx(
85
150
  button.Button,
@@ -113,7 +178,19 @@ function ChatLayout({
113
178
  children: sidebarOpen ? /* @__PURE__ */ jsxRuntime.jsx(LucideIcons.PanelLeftClose, { className: "h-5 w-5" }) : /* @__PURE__ */ jsxRuntime.jsx(LucideIcons.PanelLeft, { className: "h-5 w-5" })
114
179
  }
115
180
  ),
116
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1" })
181
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1" }),
182
+ pageAIContext && /* @__PURE__ */ jsxRuntime.jsxs(
183
+ badge.Badge,
184
+ {
185
+ variant: "secondary",
186
+ className: "text-xs gap-1 mr-2",
187
+ "data-testid": "page-context-badge",
188
+ children: [
189
+ /* @__PURE__ */ jsxRuntime.jsx(LucideIcons.Sparkles, { className: "h-3 w-3" }),
190
+ pageAIContext.routeName
191
+ ]
192
+ }
193
+ )
117
194
  ] }),
118
195
  /* @__PURE__ */ jsxRuntime.jsx(
119
196
  chatInterface.ChatInterface,