@nocobase/portal-template-default 1.0.1 → 1.0.3

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 (186) hide show
  1. package/README.MD +22 -3
  2. package/package.json +4 -2
  3. package/registry/nocobase-acl/components/role-options.ts +7 -0
  4. package/registry/nocobase-acl/components/role-switcher.tsx +9 -4
  5. package/registry/nocobase-acl/demo/boundary-api.tsx +87 -16
  6. package/registry/nocobase-acl/demo/components.tsx +32 -11
  7. package/registry/nocobase-acl/demo/index.tsx +45 -14
  8. package/registry/nocobase-acl/demo/lazy-route.tsx +2 -7
  9. package/registry/nocobase-acl/demo/prompt-generator.tsx +52 -33
  10. package/registry/nocobase-acl/demo/role-switcher-prompt-generator.tsx +28 -9
  11. package/registry/nocobase-acl/extension.tsx +1 -1
  12. package/registry/nocobase-acl/tests/acl-regression.mjs +7 -6
  13. package/registry/nocobase-ai/components/chat/chat-messages.tsx +3 -4
  14. package/registry/nocobase-ai/components/chat/conversation-list.tsx +2 -4
  15. package/registry/nocobase-ai/components/tools/business-report-dialog.tsx +16 -19
  16. package/registry/nocobase-ai/components/tools/chart-renderer.tsx +3 -14
  17. package/registry/nocobase-ai/components/tools/echarts-preview.tsx +2 -5
  18. package/registry/nocobase-ai/demo/configuration-gate.tsx +9 -12
  19. package/registry/nocobase-ai/extension.tsx +2 -7
  20. package/registry/nocobase-i18n/README.md +53 -0
  21. package/registry/nocobase-i18n/components/index.ts +1 -0
  22. package/registry/nocobase-i18n/components/language-switcher.tsx +138 -0
  23. package/registry/nocobase-i18n/demo/index.tsx +209 -0
  24. package/registry/nocobase-i18n/demo/prompt-generator.tsx +155 -0
  25. package/registry/nocobase-i18n/extension.tsx +46 -0
  26. package/registry/nocobase-i18n/i18n-provider.ts +9 -0
  27. package/registry/nocobase-i18n/index.ts +6 -0
  28. package/registry/nocobase-i18n/locale-store.ts +134 -0
  29. package/registry/nocobase-i18n/locales/en-US.ts +89 -0
  30. package/registry/nocobase-i18n/locales/zh-CN.ts +85 -0
  31. package/registry/nocobase-i18n/provider.tsx +32 -0
  32. package/registry/nocobase-i18n/runtime.ts +118 -0
  33. package/registry/nocobase-i18n/server-resources.ts +184 -0
  34. package/registry/nocobase-i18n/tests/i18n-regression.mjs +42 -0
  35. package/registry.config.json +34 -14
  36. package/scripts/registry.mjs +5 -0
  37. package/src/App.tsx +27 -86
  38. package/src/app/extension.ts +3 -1
  39. package/src/app/extensions.tsx +21 -0
  40. package/src/components/access-control/acl-gate.tsx +4 -10
  41. package/src/components/app-shell/breadcrumb.tsx +39 -3
  42. package/src/components/app-shell/document-title-handler.tsx +11 -20
  43. package/src/components/app-shell/header.tsx +18 -3
  44. package/src/components/app-shell/loading-overlay.tsx +2 -4
  45. package/src/components/app-shell/loading-state.tsx +15 -0
  46. package/src/components/app-shell/sidebar.tsx +75 -47
  47. package/src/components/data-table/data-table-pagination.tsx +27 -7
  48. package/src/components/data-table/data-table-styles.ts +57 -0
  49. package/src/components/data-table/data-table.tsx +14 -59
  50. package/src/components/resources/resource-label.ts +101 -0
  51. package/src/components/resources/views/create-view.tsx +17 -14
  52. package/src/components/resources/views/edit-view.tsx +17 -15
  53. package/src/components/resources/views/list-view.tsx +20 -10
  54. package/src/components/resources/views/show-view.tsx +20 -18
  55. package/src/extensions/nocobase-acl/README.md +19 -0
  56. package/src/extensions/nocobase-acl/components/acl-boundary.tsx +106 -0
  57. package/src/extensions/nocobase-acl/components/index.ts +3 -0
  58. package/src/extensions/nocobase-acl/components/role-options.ts +36 -0
  59. package/src/extensions/nocobase-acl/components/role-switcher.tsx +135 -0
  60. package/src/extensions/nocobase-acl/demo/boundary-api.tsx +186 -0
  61. package/src/extensions/nocobase-acl/demo/components.tsx +231 -0
  62. package/src/extensions/nocobase-acl/demo/index.tsx +505 -0
  63. package/src/extensions/nocobase-acl/demo/lazy-route.tsx +10 -0
  64. package/src/extensions/nocobase-acl/demo/prompt-generator.tsx +134 -0
  65. package/src/extensions/nocobase-acl/demo/role-switcher-prompt-generator.tsx +198 -0
  66. package/src/extensions/nocobase-acl/demo/scenario-section.tsx +37 -0
  67. package/src/extensions/nocobase-acl/extension.tsx +71 -0
  68. package/src/extensions/nocobase-acl/index.ts +1 -0
  69. package/src/extensions/nocobase-ai/README.md +70 -0
  70. package/src/extensions/nocobase-ai/adapters/react-hook-form.ts +20 -0
  71. package/src/extensions/nocobase-ai/assets/nocobase-ai-chat.svg +50 -0
  72. package/src/extensions/nocobase-ai/components/ai-root-provider.tsx +32 -0
  73. package/src/extensions/nocobase-ai/components/chat/ai-employee-avatar.tsx +31 -0
  74. package/src/extensions/nocobase-ai/components/chat/chat-attachment.tsx +92 -0
  75. package/src/extensions/nocobase-ai/components/chat/chat-compact.tsx +90 -0
  76. package/src/extensions/nocobase-ai/components/chat/chat-composer.tsx +429 -0
  77. package/src/extensions/nocobase-ai/components/chat/chat-empty-state.tsx +51 -0
  78. package/src/extensions/nocobase-ai/components/chat/chat-header.tsx +88 -0
  79. package/src/extensions/nocobase-ai/components/chat/chat-history-dialog.tsx +77 -0
  80. package/src/extensions/nocobase-ai/components/chat/chat-message.tsx +252 -0
  81. package/src/extensions/nocobase-ai/components/chat/chat-messages.tsx +173 -0
  82. package/src/extensions/nocobase-ai/components/chat/chat-window.tsx +118 -0
  83. package/src/extensions/nocobase-ai/components/chat/conversation-list.tsx +343 -0
  84. package/src/extensions/nocobase-ai/components/chat/markdown-message.tsx +165 -0
  85. package/src/extensions/nocobase-ai/components/chat/model-select-options.tsx +30 -0
  86. package/src/extensions/nocobase-ai/components/chat/reasoning-panel.tsx +29 -0
  87. package/src/extensions/nocobase-ai/components/chat/sub-agent-conversation.tsx +142 -0
  88. package/src/extensions/nocobase-ai/components/chat/tool-call-card.tsx +316 -0
  89. package/src/extensions/nocobase-ai/components/chat/user-prompt-editor.tsx +99 -0
  90. package/src/extensions/nocobase-ai/components/chat/work-context-chip.tsx +114 -0
  91. package/src/extensions/nocobase-ai/components/index.ts +59 -0
  92. package/src/extensions/nocobase-ai/components/page-elements/ai-form.tsx +44 -0
  93. package/src/extensions/nocobase-ai/components/page-elements/page-element-provider.tsx +469 -0
  94. package/src/extensions/nocobase-ai/components/surfaces/chat-dialog.tsx +31 -0
  95. package/src/extensions/nocobase-ai/components/surfaces/chat-inline.tsx +18 -0
  96. package/src/extensions/nocobase-ai/components/surfaces/chat-page.tsx +18 -0
  97. package/src/extensions/nocobase-ai/components/surfaces/chat-side-panel-layout.tsx +54 -0
  98. package/src/extensions/nocobase-ai/components/surfaces/chat-side-panel.tsx +74 -0
  99. package/src/extensions/nocobase-ai/components/surfaces/chat-surface-actions.tsx +54 -0
  100. package/src/extensions/nocobase-ai/components/tools/builtin-tool-renderers.tsx +31 -0
  101. package/src/extensions/nocobase-ai/components/tools/business-report-dialog.tsx +364 -0
  102. package/src/extensions/nocobase-ai/components/tools/business-report-renderer.tsx +122 -0
  103. package/src/extensions/nocobase-ai/components/tools/business-report-utils.tsx +282 -0
  104. package/src/extensions/nocobase-ai/components/tools/chart-renderer.tsx +99 -0
  105. package/src/extensions/nocobase-ai/components/tools/echarts-preview.tsx +59 -0
  106. package/src/extensions/nocobase-ai/components/tools/echarts-runtime-advanced.ts +18 -0
  107. package/src/extensions/nocobase-ai/components/tools/echarts-runtime-common.ts +20 -0
  108. package/src/extensions/nocobase-ai/components/tools/echarts-runtime-components.ts +34 -0
  109. package/src/extensions/nocobase-ai/components/tools/echarts-runtime-hierarchy.ts +24 -0
  110. package/src/extensions/nocobase-ai/components/tools/echarts-runtime.ts +138 -0
  111. package/src/extensions/nocobase-ai/components/tools/sub-agent-renderer.tsx +67 -0
  112. package/src/extensions/nocobase-ai/components/tools/suggestions-renderer.tsx +59 -0
  113. package/src/extensions/nocobase-ai/components/tools/tool-renderer-provider.tsx +55 -0
  114. package/src/extensions/nocobase-ai/components/tools/tool-renderer-utils.ts +18 -0
  115. package/src/extensions/nocobase-ai/components/tools/workflow-renderer.tsx +108 -0
  116. package/src/extensions/nocobase-ai/components/triggers/ai-chat-floating-trigger.tsx +73 -0
  117. package/src/extensions/nocobase-ai/components/triggers/ai-employee-shortcut.tsx +149 -0
  118. package/src/extensions/nocobase-ai/demo/configuration-gate.tsx +49 -0
  119. package/src/extensions/nocobase-ai/demo/container-showcase.tsx +210 -0
  120. package/src/extensions/nocobase-ai/demo/floating.tsx +134 -0
  121. package/src/extensions/nocobase-ai/demo/index.tsx +346 -0
  122. package/src/extensions/nocobase-ai/demo/interaction-showcase.tsx +151 -0
  123. package/src/extensions/nocobase-ai/demo/page-context-prompt-generator.tsx +355 -0
  124. package/src/extensions/nocobase-ai/demo/page-context.tsx +884 -0
  125. package/src/extensions/nocobase-ai/demo/page-element-showcase.tsx +197 -0
  126. package/src/extensions/nocobase-ai/demo/prompt-card.tsx +41 -0
  127. package/src/extensions/nocobase-ai/demo/prompt-generator.tsx +452 -0
  128. package/src/extensions/nocobase-ai/demo/shortcut.tsx +1207 -0
  129. package/src/extensions/nocobase-ai/demo/tool-cards.tsx +530 -0
  130. package/src/extensions/nocobase-ai/extension.tsx +158 -0
  131. package/src/extensions/nocobase-ai/global-ai-chat.tsx +213 -0
  132. package/src/extensions/nocobase-ai/index.ts +3 -0
  133. package/src/extensions/nocobase-ai/providers/ai-provider.tsx +323 -0
  134. package/src/extensions/nocobase-ai/providers/avatars.ts +817 -0
  135. package/src/extensions/nocobase-ai/providers/chat-context.tsx +109 -0
  136. package/src/extensions/nocobase-ai/providers/chat-controller.ts +84 -0
  137. package/src/extensions/nocobase-ai/providers/chat-message-utils.ts +198 -0
  138. package/src/extensions/nocobase-ai/providers/chat-provider.tsx +871 -0
  139. package/src/extensions/nocobase-ai/providers/chat-reducer.ts +147 -0
  140. package/src/extensions/nocobase-ai/providers/chat-task-utils.ts +105 -0
  141. package/src/extensions/nocobase-ai/providers/chat-transport.ts +184 -0
  142. package/src/extensions/nocobase-ai/providers/form-registry.tsx +271 -0
  143. package/src/extensions/nocobase-ai/providers/frontend-tool-registry.tsx +231 -0
  144. package/src/extensions/nocobase-ai/providers/index.ts +61 -0
  145. package/src/extensions/nocobase-ai/providers/model.ts +32 -0
  146. package/src/extensions/nocobase-ai/providers/page-context.tsx +136 -0
  147. package/src/extensions/nocobase-ai/providers/stream-coalescer.ts +60 -0
  148. package/src/extensions/nocobase-ai/providers/stream-event-utils.ts +82 -0
  149. package/src/extensions/nocobase-ai/providers/stream-parser.ts +61 -0
  150. package/src/extensions/nocobase-ai/providers/sub-agent-stream.ts +316 -0
  151. package/src/extensions/nocobase-ai/providers/types.ts +183 -0
  152. package/src/extensions/nocobase-ai/providers/ui-message-stream.ts +463 -0
  153. package/src/extensions/nocobase-ai/providers/use-automatic-tool-approval.ts +94 -0
  154. package/src/extensions/nocobase-ai/providers/use-chat-attachments.ts +206 -0
  155. package/src/extensions/nocobase-ai/providers/use-chat-message-actions.ts +554 -0
  156. package/src/extensions/nocobase-ai/providers/use-chat-runtime.ts +210 -0
  157. package/src/extensions/nocobase-ai/providers/use-chat-work-context.ts +93 -0
  158. package/src/extensions/nocobase-ai/providers/use-conversation-catalog.ts +108 -0
  159. package/src/extensions/nocobase-ai/providers/use-conversation-history.ts +178 -0
  160. package/src/extensions/nocobase-ai/services/index.ts +7 -0
  161. package/src/extensions/nocobase-ai/services/nocobase-ai-service.ts +454 -0
  162. package/src/extensions/nocobase-ai/services/types.ts +82 -0
  163. package/src/index.tsx +2 -3
  164. package/src/lib/i18n.ts +161 -0
  165. package/src/lib/nocobase/client.ts +19 -2
  166. package/src/locales/en-US.ts +82 -0
  167. package/src/locales/index.ts +13 -0
  168. package/src/locales/zh-CN.ts +79 -0
  169. package/src/pages/users/create.tsx +98 -0
  170. package/src/pages/users/edit.tsx +92 -0
  171. package/src/pages/users/form-context.ts +57 -0
  172. package/src/pages/users/form-fields.tsx +211 -0
  173. package/src/pages/users/list.tsx +205 -0
  174. package/src/pages/users/show.tsx +187 -0
  175. package/src/pages/users/types.ts +20 -0
  176. package/src/providers/constants.ts +1 -0
  177. package/src/pages/blog-posts/create.tsx +0 -177
  178. package/src/pages/blog-posts/edit.tsx +0 -197
  179. package/src/pages/blog-posts/list.tsx +0 -131
  180. package/src/pages/blog-posts/show.tsx +0 -81
  181. package/src/pages/categories/create.tsx +0 -73
  182. package/src/pages/categories/edit.tsx +0 -73
  183. package/src/pages/categories/index.ts +0 -4
  184. package/src/pages/categories/list.tsx +0 -86
  185. package/src/pages/categories/show.tsx +0 -37
  186. /package/src/pages/{blog-posts → users}/index.ts +0 -0
@@ -0,0 +1,554 @@
1
+ import type { Chat } from "@ai-sdk/react";
2
+ import {
3
+ useCallback,
4
+ type Dispatch,
5
+ type MutableRefObject,
6
+ type SetStateAction,
7
+ } from "react";
8
+ import type { AIChatAction, AIChatState } from "./chat-reducer";
9
+ import { findChatMessage, isAIToolPart } from "./chat-message-utils";
10
+ import { getAIWorkContextToolScope } from "./page-context";
11
+ import type { NocoBaseChatTransport } from "./chat-transport";
12
+ import type { useAI } from "./ai-provider";
13
+ import type { AIConversationRuntimeContext } from "./use-chat-runtime";
14
+ import { useAutomaticToolApproval } from "./use-automatic-tool-approval";
15
+ import type {
16
+ AIChatAttachment,
17
+ AIChatMessage,
18
+ AIToolCallDecision,
19
+ AIToolCallInvocationContext,
20
+ AIWorkContextItem,
21
+ } from "./types";
22
+
23
+ type AIContextValue = ReturnType<typeof useAI>;
24
+
25
+ export type AIMessageEditingSnapshot = {
26
+ conversationId: string;
27
+ messages: AIChatMessage[];
28
+ attachments: AIChatAttachment[];
29
+ workContext: AIWorkContextItem[];
30
+ };
31
+
32
+ type RefreshConversationMessages = (
33
+ conversationId: string,
34
+ targetChat: Chat<AIChatMessage>,
35
+ options?: { updateRead?: boolean }
36
+ ) => Promise<AIChatMessage[]>;
37
+
38
+ const isChatRunning = (chat: Chat<AIChatMessage>) =>
39
+ chat.status === "streaming" || chat.status === "submitted";
40
+
41
+ const getToolInvocationContext = ({
42
+ conversationId,
43
+ messages,
44
+ rootIndex,
45
+ runtimeContext,
46
+ messageId,
47
+ toolCallId,
48
+ toolName,
49
+ automatic,
50
+ }: {
51
+ conversationId: string;
52
+ messages: AIChatMessage[];
53
+ rootIndex: number;
54
+ runtimeContext: AIConversationRuntimeContext;
55
+ messageId: string;
56
+ toolCallId: string;
57
+ toolName: string;
58
+ automatic?: boolean;
59
+ }): AIToolCallInvocationContext => {
60
+ const userContext = messages
61
+ .slice(0, rootIndex + 1)
62
+ .reverse()
63
+ .find((message) => message.role === "user")?.metadata?.workContext;
64
+ return {
65
+ sessionId: conversationId,
66
+ messageId,
67
+ toolCallId,
68
+ toolName,
69
+ automatic,
70
+ ...getAIWorkContextToolScope([
71
+ ...(runtimeContext.task?.workContext ?? []),
72
+ ...(userContext ?? []),
73
+ ]),
74
+ };
75
+ };
76
+
77
+ export function useChatMessageActions({
78
+ ai,
79
+ activeChat,
80
+ stateRef,
81
+ chatSurfaceOpenRef,
82
+ transportsRef,
83
+ getRuntimeContext,
84
+ refreshConversationMessages,
85
+ setHistoryError,
86
+ editingSnapshotRef,
87
+ setEditingMessageId,
88
+ getConversationAttachments,
89
+ getConversationWorkContext,
90
+ setConversationAttachments,
91
+ setConversationWorkContext,
92
+ dispatch,
93
+ requestComposerFocus,
94
+ }: {
95
+ ai: AIContextValue;
96
+ activeChat: Chat<AIChatMessage>;
97
+ stateRef: MutableRefObject<AIChatState>;
98
+ chatSurfaceOpenRef: MutableRefObject<boolean>;
99
+ transportsRef: MutableRefObject<Map<string, NocoBaseChatTransport>>;
100
+ getRuntimeContext: (
101
+ conversationId: string
102
+ ) => AIConversationRuntimeContext;
103
+ refreshConversationMessages: RefreshConversationMessages;
104
+ setHistoryError: Dispatch<SetStateAction<Error | undefined>>;
105
+ editingSnapshotRef: MutableRefObject<AIMessageEditingSnapshot | undefined>;
106
+ setEditingMessageId: Dispatch<SetStateAction<string | undefined>>;
107
+ getConversationAttachments: (conversationId: string) => AIChatAttachment[];
108
+ getConversationWorkContext: (conversationId: string) => AIWorkContextItem[];
109
+ setConversationAttachments: (
110
+ conversationId: string,
111
+ attachments: AIChatAttachment[]
112
+ ) => void;
113
+ setConversationWorkContext: (
114
+ conversationId: string,
115
+ context: AIWorkContextItem[]
116
+ ) => void;
117
+ dispatch: Dispatch<AIChatAction>;
118
+ requestComposerFocus: () => void;
119
+ }) {
120
+ const resolveServerMessage = useCallback(
121
+ async (
122
+ conversationId: string,
123
+ targetChat: Chat<AIChatMessage>,
124
+ message: AIChatMessage
125
+ ) => {
126
+ let messages = targetChat.messages;
127
+ const resolvedMessage = findChatMessage(messages, message.id);
128
+ if (!resolvedMessage) {
129
+ throw new Error(
130
+ "The message is no longer available in this conversation."
131
+ );
132
+ }
133
+ let { targetMessage, rootIndex } = resolvedMessage;
134
+
135
+ if (!targetMessage.metadata?.serverMessageId) {
136
+ messages = await refreshConversationMessages(
137
+ conversationId,
138
+ targetChat,
139
+ {
140
+ updateRead:
141
+ stateRef.current.activeConversationId === conversationId &&
142
+ chatSurfaceOpenRef.current,
143
+ }
144
+ );
145
+ const refreshedMatch = findChatMessage(messages, message.id);
146
+ const refreshedTarget = refreshedMatch?.targetMessage;
147
+ if (!refreshedTarget) {
148
+ throw new Error(
149
+ "Unable to resolve the server message for this action."
150
+ );
151
+ }
152
+ targetMessage = refreshedTarget;
153
+ rootIndex = refreshedMatch.rootIndex;
154
+ }
155
+
156
+ return {
157
+ conversationId,
158
+ messages,
159
+ targetMessage,
160
+ rootIndex,
161
+ serverMessageId: targetMessage.metadata?.serverMessageId,
162
+ };
163
+ },
164
+ [
165
+ chatSurfaceOpenRef,
166
+ refreshConversationMessages,
167
+ stateRef,
168
+ ]
169
+ );
170
+
171
+ const retryMessage = useCallback(
172
+ async (message: AIChatMessage) => {
173
+ if (
174
+ message.role !== "assistant" ||
175
+ activeChat.status === "streaming" ||
176
+ activeChat.status === "submitted"
177
+ ) {
178
+ return;
179
+ }
180
+
181
+ setHistoryError(undefined);
182
+ try {
183
+ const resolved = await resolveServerMessage(
184
+ stateRef.current.activeConversationId,
185
+ activeChat,
186
+ message
187
+ );
188
+ if (!resolved.serverMessageId) {
189
+ throw new Error("The server message id is unavailable for retry.");
190
+ }
191
+ const transport = transportsRef.current.get(resolved.conversationId);
192
+ if (!transport) {
193
+ throw new Error("The NocoBase chat transport is unavailable.");
194
+ }
195
+ transport.prepareResend(resolved.serverMessageId);
196
+ try {
197
+ await activeChat.regenerate({ messageId: resolved.targetMessage.id });
198
+ } catch (error) {
199
+ transport.cancelResend(resolved.serverMessageId);
200
+ throw error;
201
+ }
202
+ } catch (error) {
203
+ setHistoryError(
204
+ error instanceof Error ? error : new Error("Unable to retry message")
205
+ );
206
+ }
207
+ },
208
+ [
209
+ activeChat,
210
+ resolveServerMessage,
211
+ setHistoryError,
212
+ stateRef,
213
+ transportsRef,
214
+ ]
215
+ );
216
+
217
+ const decideConversationToolCall = useCallback(
218
+ async (
219
+ conversationId: string,
220
+ targetChat: Chat<AIChatMessage>,
221
+ decision: AIToolCallDecision,
222
+ options: { automatic?: boolean } = {}
223
+ ) => {
224
+ if (isChatRunning(targetChat)) return;
225
+
226
+ setHistoryError(undefined);
227
+ try {
228
+ const message = findChatMessage(
229
+ targetChat.messages,
230
+ decision.messageId
231
+ )?.targetMessage;
232
+ if (!message) {
233
+ throw new Error("The tool-call message is no longer available.");
234
+ }
235
+ const resolved = await resolveServerMessage(
236
+ conversationId,
237
+ targetChat,
238
+ message
239
+ );
240
+ if (!resolved.serverMessageId) {
241
+ throw new Error(
242
+ "The server message id is unavailable for this tool decision."
243
+ );
244
+ }
245
+ const toolPart = resolved.targetMessage.parts
246
+ .filter(isAIToolPart)
247
+ .find((part) => part.toolCallId === decision.toolCallId);
248
+ const toolName =
249
+ toolPart?.type === "dynamic-tool"
250
+ ? toolPart.toolName
251
+ : toolPart?.type.startsWith("tool-")
252
+ ? toolPart.type.slice(5)
253
+ : decision.toolName;
254
+ const runtimeContext = getRuntimeContext(resolved.conversationId);
255
+ const invocationContext = getToolInvocationContext({
256
+ conversationId: resolved.conversationId,
257
+ messages: resolved.messages,
258
+ rootIndex: resolved.rootIndex,
259
+ runtimeContext,
260
+ messageId: resolved.serverMessageId,
261
+ toolCallId: decision.toolCallId,
262
+ toolName,
263
+ automatic: options.automatic,
264
+ });
265
+ if (
266
+ options.automatic &&
267
+ !ai.canAutoApproveToolCall(
268
+ toolName,
269
+ toolPart?.input ?? decision.input,
270
+ invocationContext
271
+ )
272
+ ) {
273
+ if (toolPart && "callProviderMetadata" in toolPart) {
274
+ const providerMetadata = toolPart.callProviderMetadata ?? {};
275
+ const nocobase =
276
+ providerMetadata.nocobase &&
277
+ typeof providerMetadata.nocobase === "object"
278
+ ? providerMetadata.nocobase
279
+ : {};
280
+ toolPart.callProviderMetadata = {
281
+ ...providerMetadata,
282
+ nocobase: {
283
+ ...nocobase,
284
+ autoApprove: false,
285
+ requiresApproval: true,
286
+ },
287
+ };
288
+ targetChat.messages = [...targetChat.messages];
289
+ }
290
+ return;
291
+ }
292
+ const userDecision =
293
+ decision.decision === "approve"
294
+ ? ({ type: "approve" } as const)
295
+ : decision.decision === "reject"
296
+ ? ({
297
+ type: "reject",
298
+ ...(typeof decision.input === "string"
299
+ ? { message: decision.input }
300
+ : {}),
301
+ } as const)
302
+ : ({
303
+ type: "edit",
304
+ editedAction: { name: toolName, args: decision.input },
305
+ } as const);
306
+ const result = await ai.updateToolCallDecision({
307
+ sessionId: resolved.conversationId,
308
+ messageId: resolved.serverMessageId,
309
+ toolCallId: decision.toolCallId,
310
+ userDecision,
311
+ });
312
+ if (!result.updated) {
313
+ await refreshConversationMessages(
314
+ resolved.conversationId,
315
+ targetChat,
316
+ {
317
+ updateRead:
318
+ stateRef.current.activeConversationId ===
319
+ resolved.conversationId && chatSurfaceOpenRef.current,
320
+ }
321
+ );
322
+ return;
323
+ }
324
+
325
+ const interruptingToolCalls = result.toolCalls.filter(
326
+ (toolCall) =>
327
+ toolCall.willInterrupt === true ||
328
+ toolCall.execution === "frontend" ||
329
+ toolCall.auto === false
330
+ );
331
+ const allWaiting =
332
+ interruptingToolCalls.length > 0 &&
333
+ interruptingToolCalls.every(
334
+ (toolCall) =>
335
+ String(toolCall.invokeStatus).toLowerCase() === "waiting"
336
+ );
337
+ if (!allWaiting) return;
338
+
339
+ const toolCallIds = result.toolCalls.map((toolCall) => toolCall.id);
340
+ const toolCallResults: Array<{ id: string; result: unknown }> = [];
341
+ for (const toolCall of result.toolCalls) {
342
+ const invocation = await ai.invokeToolCall(
343
+ toolCall.name,
344
+ toolCall.args,
345
+ {
346
+ ...invocationContext,
347
+ toolCallId: toolCall.id,
348
+ toolName: toolCall.name,
349
+ }
350
+ );
351
+ if (invocation.handled) {
352
+ toolCallResults.push({
353
+ id: toolCall.id,
354
+ result: invocation.result,
355
+ });
356
+ }
357
+ }
358
+
359
+ const transport = transportsRef.current.get(resolved.conversationId);
360
+ if (!transport) {
361
+ throw new Error("The NocoBase chat transport is unavailable.");
362
+ }
363
+ if (resolved.rootIndex !== targetChat.messages.length - 1) {
364
+ targetChat.messages = resolved.messages.slice(
365
+ 0,
366
+ resolved.rootIndex + 1
367
+ );
368
+ }
369
+ const responseMessageId = `assistant-${crypto.randomUUID()}`;
370
+ targetChat.messages = [
371
+ ...targetChat.messages,
372
+ {
373
+ id: responseMessageId,
374
+ role: "assistant",
375
+ metadata: {
376
+ createdAt: new Date().toISOString(),
377
+ employeeUsername: runtimeContext.employeeUsername,
378
+ },
379
+ parts: [],
380
+ },
381
+ ];
382
+ transport.prepareToolResume(
383
+ resolved.serverMessageId,
384
+ responseMessageId,
385
+ toolCallIds,
386
+ toolCallResults
387
+ );
388
+ try {
389
+ await targetChat.resumeStream();
390
+ } catch (error) {
391
+ transport.cancelToolResume(resolved.serverMessageId);
392
+ targetChat.messages = targetChat.messages.filter(
393
+ (item) => item.id !== responseMessageId
394
+ );
395
+ throw error;
396
+ }
397
+ } catch (error) {
398
+ setHistoryError(
399
+ error instanceof Error
400
+ ? error
401
+ : new Error("Unable to process the tool decision")
402
+ );
403
+ throw error;
404
+ }
405
+ },
406
+ [
407
+ ai,
408
+ chatSurfaceOpenRef,
409
+ getRuntimeContext,
410
+ refreshConversationMessages,
411
+ resolveServerMessage,
412
+ setHistoryError,
413
+ stateRef,
414
+ transportsRef,
415
+ ]
416
+ );
417
+
418
+ const decideToolCall = useCallback(
419
+ (decision: AIToolCallDecision) =>
420
+ decideConversationToolCall(
421
+ stateRef.current.activeConversationId,
422
+ activeChat,
423
+ decision
424
+ ),
425
+ [activeChat, decideConversationToolCall, stateRef]
426
+ );
427
+
428
+ const {
429
+ clearConversation: clearAutomaticToolApproval,
430
+ process: processAutomaticToolApprovals,
431
+ } = useAutomaticToolApproval({
432
+ enabled: true,
433
+ decide: decideConversationToolCall,
434
+ });
435
+
436
+ const startEditingMessage = useCallback(
437
+ async (message: AIChatMessage) => {
438
+ if (
439
+ message.role !== "user" ||
440
+ activeChat.status === "streaming" ||
441
+ activeChat.status === "submitted"
442
+ ) {
443
+ return;
444
+ }
445
+ let messages = activeChat.messages;
446
+ let targetMessage = message;
447
+ let index = messages.findIndex((item) => item.id === message.id);
448
+ if (index < 0) return;
449
+ const conversationId = stateRef.current.activeConversationId;
450
+ if (!message.metadata?.serverMessageId) {
451
+ const userMessageIndex =
452
+ messages.slice(0, index + 1).filter((item) => item.role === "user")
453
+ .length - 1;
454
+ try {
455
+ messages = await refreshConversationMessages(
456
+ conversationId,
457
+ activeChat,
458
+ { updateRead: true }
459
+ );
460
+ } catch (error) {
461
+ setHistoryError(
462
+ error instanceof Error
463
+ ? error
464
+ : new Error("Unable to refresh conversation messages")
465
+ );
466
+ return;
467
+ }
468
+ if (stateRef.current.activeConversationId !== conversationId) return;
469
+ targetMessage = messages.filter((item) => item.role === "user")[
470
+ userMessageIndex
471
+ ];
472
+ if (!targetMessage?.metadata?.serverMessageId) return;
473
+ index = messages.findIndex((item) => item.id === targetMessage.id);
474
+ if (index < 0) return;
475
+ }
476
+ const serverMessageId = targetMessage.metadata?.serverMessageId;
477
+ editingSnapshotRef.current = {
478
+ conversationId,
479
+ messages: [...messages],
480
+ attachments: getConversationAttachments(conversationId),
481
+ workContext: getConversationWorkContext(conversationId),
482
+ };
483
+ setEditingMessageId(serverMessageId ?? targetMessage.id);
484
+ activeChat.messages = messages.slice(0, index);
485
+ setConversationAttachments(
486
+ conversationId,
487
+ targetMessage.metadata?.attachments ?? []
488
+ );
489
+ setConversationWorkContext(
490
+ conversationId,
491
+ targetMessage.metadata?.workContext ?? []
492
+ );
493
+ dispatch({
494
+ type: "set-draft",
495
+ conversationId,
496
+ value: targetMessage.parts
497
+ .filter((part) => part.type === "text")
498
+ .map((part) => part.text)
499
+ .join("\n"),
500
+ });
501
+ requestComposerFocus();
502
+ },
503
+ [
504
+ activeChat,
505
+ dispatch,
506
+ editingSnapshotRef,
507
+ getConversationAttachments,
508
+ getConversationWorkContext,
509
+ refreshConversationMessages,
510
+ requestComposerFocus,
511
+ setConversationAttachments,
512
+ setConversationWorkContext,
513
+ setEditingMessageId,
514
+ setHistoryError,
515
+ stateRef,
516
+ ]
517
+ );
518
+
519
+ const cancelEditingMessage = useCallback(() => {
520
+ const snapshot = editingSnapshotRef.current;
521
+ if (
522
+ snapshot &&
523
+ snapshot.conversationId === stateRef.current.activeConversationId
524
+ ) {
525
+ activeChat.messages = snapshot.messages;
526
+ setConversationAttachments(snapshot.conversationId, snapshot.attachments);
527
+ setConversationWorkContext(snapshot.conversationId, snapshot.workContext);
528
+ dispatch({
529
+ type: "set-draft",
530
+ conversationId: snapshot.conversationId,
531
+ value: "",
532
+ });
533
+ }
534
+ setEditingMessageId(undefined);
535
+ editingSnapshotRef.current = undefined;
536
+ }, [
537
+ activeChat,
538
+ dispatch,
539
+ editingSnapshotRef,
540
+ setConversationAttachments,
541
+ setConversationWorkContext,
542
+ setEditingMessageId,
543
+ stateRef,
544
+ ]);
545
+
546
+ return {
547
+ retryMessage,
548
+ decideToolCall,
549
+ startEditingMessage,
550
+ cancelEditingMessage,
551
+ clearAutomaticToolApproval,
552
+ processAutomaticToolApprovals,
553
+ };
554
+ }