@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,213 @@
1
+ import {
2
+ useEffect,
3
+ useMemo,
4
+ useState,
5
+ type CSSProperties,
6
+ type Dispatch,
7
+ type PropsWithChildren,
8
+ type SetStateAction,
9
+ } from "react";
10
+ import {
11
+ AIChatFloatingTrigger,
12
+ NocoBaseAIRootProvider,
13
+ useAIPageElementPicker,
14
+ type AIChatComposerAction,
15
+ } from "./components";
16
+ import { AIChatWindow } from "./components/chat/chat-window";
17
+ import { ChatDialog } from "./components/surfaces/chat-dialog";
18
+ import { ChatSidePanel } from "./components/surfaces/chat-side-panel";
19
+ import { ChatSurfaceActions } from "./components/surfaces/chat-surface-actions";
20
+ import {
21
+ AIChatProvider,
22
+ type AIToolInvokerMap,
23
+ useAI,
24
+ useAIChatBase,
25
+ useAIChatControllerState,
26
+ useGlobalAIChatController,
27
+ } from "./providers";
28
+ import { nocobaseAIService } from "./services";
29
+ import { Globe2, MousePointer2 } from "lucide-react";
30
+
31
+ const DEFAULT_SIDE_PANEL_WIDTH = 450;
32
+ export function NocoBaseAIExtensionProvider({
33
+ children,
34
+ toolInvokers,
35
+ }: PropsWithChildren<{ toolInvokers?: AIToolInvokerMap }>) {
36
+ return (
37
+ <NocoBaseAIRootProvider
38
+ service={nocobaseAIService}
39
+ toolInvokers={toolInvokers}
40
+ >
41
+ <NocoBaseAIGlobalEntry>{children}</NocoBaseAIGlobalEntry>
42
+ </NocoBaseAIRootProvider>
43
+ );
44
+ }
45
+
46
+ function NocoBaseAIGlobalEntry({ children }: PropsWithChildren) {
47
+ const ai = useAI();
48
+ const controller = useGlobalAIChatController();
49
+ const { open } = useAIChatControllerState(controller);
50
+ const [webSearch, setWebSearch] = useState(false);
51
+ const [expanded, setExpanded] = useState(false);
52
+ const ready =
53
+ ai.configurationStatus === "ready" &&
54
+ ai.employees.length > 0 &&
55
+ ai.hasEnabledModels;
56
+
57
+ return (
58
+ <div
59
+ data-open={ready && open && !expanded}
60
+ data-side="right"
61
+ className="chat-side-panel-layout @container min-h-svh min-w-0"
62
+ style={
63
+ {
64
+ "--chat-side-panel-width": `${DEFAULT_SIDE_PANEL_WIDTH}px`,
65
+ } as CSSProperties
66
+ }
67
+ >
68
+ <div className="min-w-0">{children}</div>
69
+ {ready ? (
70
+ <AIChatProvider
71
+ id="starter-global-ai"
72
+ controller={controller}
73
+ webSearch={webSearch}
74
+ >
75
+ <StarterGlobalAIChat
76
+ open={open}
77
+ onOpenChange={controller.setOpen}
78
+ webSearch={webSearch}
79
+ setWebSearch={setWebSearch}
80
+ expanded={expanded}
81
+ setExpanded={setExpanded}
82
+ />
83
+ </AIChatProvider>
84
+ ) : null}
85
+ </div>
86
+ );
87
+ }
88
+
89
+ function StarterGlobalAIChat({
90
+ open,
91
+ onOpenChange,
92
+ webSearch,
93
+ setWebSearch,
94
+ expanded,
95
+ setExpanded,
96
+ }: {
97
+ open: boolean;
98
+ onOpenChange: (open: boolean) => void;
99
+ webSearch: boolean;
100
+ setWebSearch: Dispatch<SetStateAction<boolean>>;
101
+ expanded: boolean;
102
+ setExpanded: Dispatch<SetStateAction<boolean>>;
103
+ }) {
104
+ const {
105
+ id: chatId,
106
+ conversations,
107
+ currentModel,
108
+ addWorkContext,
109
+ focusComposer,
110
+ } = useAIChatBase();
111
+ const { registeredCount, startPicking } = useAIPageElementPicker();
112
+ const supportsWebSearch = currentModel.supportWebSearch === true;
113
+ const unreadCount = conversations.filter(
114
+ (conversation) => conversation.unread
115
+ ).length;
116
+
117
+ useEffect(() => {
118
+ if (!supportsWebSearch && webSearch) setWebSearch(false);
119
+ }, [setWebSearch, supportsWebSearch, webSearch]);
120
+
121
+ const composerActions = useMemo<AIChatComposerAction[]>(
122
+ () => [
123
+ {
124
+ key: "pick-page-element",
125
+ label: "Pick page element",
126
+ icon: <MousePointer2 />,
127
+ disabled: registeredCount === 0,
128
+ onClick: () => {
129
+ if (expanded) setExpanded(false);
130
+ startPicking({
131
+ chatId,
132
+ onSelect: (item) => {
133
+ addWorkContext(item);
134
+ focusComposer();
135
+ },
136
+ });
137
+ },
138
+ },
139
+ {
140
+ key: "web-search",
141
+ label: supportsWebSearch
142
+ ? webSearch
143
+ ? "Disable web search"
144
+ : "Enable web search"
145
+ : "Web search is not supported by this model",
146
+ icon: <Globe2 />,
147
+ active: webSearch,
148
+ disabled: !supportsWebSearch,
149
+ onClick: () => setWebSearch((active) => !active),
150
+ },
151
+ ],
152
+ [
153
+ addWorkContext,
154
+ chatId,
155
+ expanded,
156
+ focusComposer,
157
+ registeredCount,
158
+ setExpanded,
159
+ setWebSearch,
160
+ startPicking,
161
+ supportsWebSearch,
162
+ webSearch,
163
+ ]
164
+ );
165
+ const closeChat = () => {
166
+ setExpanded(false);
167
+ onOpenChange(false);
168
+ };
169
+
170
+ return (
171
+ <>
172
+ <ChatSidePanel
173
+ open={open && !expanded}
174
+ onOpenChange={onOpenChange}
175
+ width={DEFAULT_SIDE_PANEL_WIDTH}
176
+ showCloseHandle={false}
177
+ >
178
+ <AIChatWindow
179
+ headerActions={
180
+ <ChatSurfaceActions
181
+ expanded={false}
182
+ onExpandedChange={setExpanded}
183
+ onClose={closeChat}
184
+ />
185
+ }
186
+ composerActions={composerActions}
187
+ enableAttachments
188
+ attachmentActionIndex={1}
189
+ />
190
+ </ChatSidePanel>
191
+ <ChatDialog
192
+ open={open && expanded}
193
+ onOpenChange={(nextOpen) => {
194
+ if (!nextOpen) closeChat();
195
+ }}
196
+ >
197
+ <AIChatWindow
198
+ headerActions={
199
+ <ChatSurfaceActions
200
+ expanded
201
+ onExpandedChange={setExpanded}
202
+ onClose={closeChat}
203
+ />
204
+ }
205
+ composerActions={composerActions}
206
+ enableAttachments
207
+ attachmentActionIndex={1}
208
+ />
209
+ </ChatDialog>
210
+ <AIChatFloatingTrigger unreadCount={unreadCount} />
211
+ </>
212
+ );
213
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./components";
2
+ export * from "./providers";
3
+ export * from "./services";
@@ -0,0 +1,323 @@
1
+ import {
2
+ createContext,
3
+ useCallback,
4
+ useContext,
5
+ useEffect,
6
+ useMemo,
7
+ useState,
8
+ type PropsWithChildren,
9
+ } from "react";
10
+ import { nocobaseAIService, type AIService } from "../services";
11
+ import { NocoBaseChatTransport } from "./chat-transport";
12
+ import { useAIChatController, type AIChatController } from "./chat-controller";
13
+ import {
14
+ AIFormRegistryProvider,
15
+ createFormFillerInvoker,
16
+ useAIFormRegistry,
17
+ } from "./form-registry";
18
+ import {
19
+ AIFrontendToolRegistryProvider,
20
+ createFrontendToolInvokers,
21
+ useAIFrontendToolRegistry,
22
+ } from "./frontend-tool-registry";
23
+ import type {
24
+ AIConfigurationStatus,
25
+ AIConversation,
26
+ AIEmployee,
27
+ AIModel,
28
+ AIToolCallInvocationContext,
29
+ AIToolInvokerMap,
30
+ AITransportFactory,
31
+ } from "./types";
32
+
33
+ const UNCONFIGURED_MODEL: AIModel = {
34
+ value: "__unconfigured__",
35
+ label: "No enabled model",
36
+ configured: false,
37
+ };
38
+
39
+ const RESERVED_TOOL_INVOKER_NAMES = [
40
+ "formFiller",
41
+ "loadFrontendTool",
42
+ "executeFrontendTool",
43
+ ] as const;
44
+
45
+ type AIProviderValue = {
46
+ configurationStatus: AIConfigurationStatus;
47
+ configurationError?: Error;
48
+ modelConfigurationError?: Error;
49
+ hasEnabledModels: boolean;
50
+ employees: AIEmployee[];
51
+ models: AIModel[];
52
+ globalController: AIChatController;
53
+ createTransport: AITransportFactory;
54
+ uploadFile: AIService["uploadFile"];
55
+ updateEmployeeUserPrompt: (username: string, prompt: string) => Promise<void>;
56
+ listConversations: (keyword?: string) => Promise<AIConversation[]>;
57
+ getConversationMessages: AIService["getConversationMessages"];
58
+ getConversationActiveState: AIService["getConversationActiveState"];
59
+ updateConversationTitle: (sessionId: string, title: string) => Promise<void>;
60
+ destroyConversation: (sessionId: string) => Promise<void>;
61
+ updateToolCallDecision: AIService["updateToolCallDecision"];
62
+ invokeToolCall: (
63
+ toolName: string,
64
+ input: unknown,
65
+ context: AIToolCallInvocationContext
66
+ ) => Promise<{ handled: boolean; result?: unknown }>;
67
+ canAutoApproveToolCall: (
68
+ toolName: string,
69
+ input: unknown,
70
+ context: AIToolCallInvocationContext
71
+ ) => boolean;
72
+ };
73
+
74
+ const AIContext = createContext<AIProviderValue | null>(null);
75
+
76
+ export type AIProviderProps = PropsWithChildren<{
77
+ employees?: AIEmployee[];
78
+ models?: AIModel[];
79
+ service?: AIService;
80
+ toolInvokers?: AIToolInvokerMap;
81
+ globalController?: AIChatController;
82
+ }>;
83
+
84
+ export function AIProvider(props: AIProviderProps) {
85
+ return (
86
+ <AIFormRegistryProvider>
87
+ <AIFrontendToolRegistryProvider>
88
+ <AIProviderRuntime {...props} />
89
+ </AIFrontendToolRegistryProvider>
90
+ </AIFormRegistryProvider>
91
+ );
92
+ }
93
+
94
+ function AIProviderRuntime({
95
+ children,
96
+ employees: providedEmployees,
97
+ models: providedModels,
98
+ service = nocobaseAIService,
99
+ toolInvokers,
100
+ globalController: providedGlobalController,
101
+ }: AIProviderProps) {
102
+ const formRegistry = useAIFormRegistry();
103
+ const frontendToolRegistry = useAIFrontendToolRegistry();
104
+ const resolvedToolInvokers = useMemo<AIToolInvokerMap>(
105
+ () => {
106
+ const reservedToolCollision = RESERVED_TOOL_INVOKER_NAMES.find(
107
+ (name) => toolInvokers?.[name]
108
+ );
109
+ if (reservedToolCollision) {
110
+ throw new Error(
111
+ `Tool invoker "${reservedToolCollision}" is built into AIProvider and cannot be overridden`
112
+ );
113
+ }
114
+ return {
115
+ ...toolInvokers,
116
+ ...createFrontendToolInvokers(frontendToolRegistry),
117
+ formFiller: createFormFillerInvoker(formRegistry),
118
+ };
119
+ },
120
+ [formRegistry, frontendToolRegistry, toolInvokers]
121
+ );
122
+ const [liveConfiguration, setLiveConfiguration] = useState<{
123
+ employees: AIEmployee[];
124
+ models: AIModel[];
125
+ status: AIConfigurationStatus;
126
+ error?: Error;
127
+ modelError?: Error;
128
+ }>({ employees: [], models: [], status: "loading" });
129
+ const internalGlobalController = useAIChatController();
130
+ const globalController = providedGlobalController ?? internalGlobalController;
131
+
132
+ useEffect(() => {
133
+ if (providedEmployees && providedModels) {
134
+ setLiveConfiguration({
135
+ employees: providedEmployees,
136
+ models: providedModels.length ? providedModels : [UNCONFIGURED_MODEL],
137
+ status: "ready",
138
+ modelError: providedModels.length
139
+ ? undefined
140
+ : new Error("No enabled AI models were provided."),
141
+ });
142
+ return;
143
+ }
144
+
145
+ let active = true;
146
+ setLiveConfiguration((current) => ({
147
+ ...current,
148
+ status: "loading",
149
+ error: undefined,
150
+ }));
151
+
152
+ void Promise.allSettled([
153
+ providedEmployees ?? service.listEmployees(),
154
+ providedModels ?? service.listModels(),
155
+ ]).then(([employeesResult, modelsResult]) => {
156
+ if (!active) return;
157
+ if (employeesResult.status === "rejected") {
158
+ setLiveConfiguration({
159
+ employees: [],
160
+ models: [],
161
+ status: "error",
162
+ error:
163
+ employeesResult.reason instanceof Error
164
+ ? employeesResult.reason
165
+ : new Error("Unable to load NocoBase AI configuration."),
166
+ });
167
+ return;
168
+ }
169
+
170
+ const employees = employeesResult.value;
171
+ if (!employees.length) {
172
+ setLiveConfiguration({
173
+ employees: [],
174
+ models: [],
175
+ status: "error",
176
+ error: new Error(
177
+ "No AI employees are available for the current NocoBase user."
178
+ ),
179
+ });
180
+ return;
181
+ }
182
+
183
+ const models =
184
+ modelsResult.status === "fulfilled" ? modelsResult.value : [];
185
+ const modelError =
186
+ modelsResult.status === "rejected"
187
+ ? modelsResult.reason instanceof Error
188
+ ? modelsResult.reason
189
+ : new Error("Unable to load enabled AI models from NocoBase.")
190
+ : models.length
191
+ ? undefined
192
+ : new Error("No enabled AI models were returned by NocoBase.");
193
+ setLiveConfiguration({
194
+ employees,
195
+ models: models.length ? models : [UNCONFIGURED_MODEL],
196
+ status: "ready",
197
+ modelError,
198
+ });
199
+ });
200
+
201
+ return () => {
202
+ active = false;
203
+ };
204
+ }, [providedEmployees, providedModels, service]);
205
+
206
+ const employees = liveConfiguration.employees;
207
+ const models = liveConfiguration.models;
208
+ const configurationStatus = liveConfiguration.status;
209
+ const configurationError = liveConfiguration.error;
210
+ const modelConfigurationError = liveConfiguration.modelError;
211
+ const hasEnabledModels = models.some((model) => model.configured !== false);
212
+
213
+ const updateEmployeeUserPrompt = useCallback(
214
+ async (username: string, prompt: string) => {
215
+ await service.updateEmployeeUserPrompt(username, prompt);
216
+ setLiveConfiguration((current) => ({
217
+ ...current,
218
+ employees: current.employees.map((employee) =>
219
+ employee.username === username
220
+ ? {
221
+ ...employee,
222
+ userConfig: { ...employee.userConfig, prompt },
223
+ }
224
+ : employee
225
+ ),
226
+ }));
227
+ },
228
+ [service]
229
+ );
230
+
231
+ const invokeToolCall = useCallback(
232
+ async (
233
+ toolName: string,
234
+ input: unknown,
235
+ context: AIToolCallInvocationContext
236
+ ) => {
237
+ const invoke = resolvedToolInvokers[toolName];
238
+ if (!invoke) return { handled: false };
239
+ return { handled: true, result: await invoke(input, context) };
240
+ },
241
+ [resolvedToolInvokers]
242
+ );
243
+
244
+ const canAutoApproveToolCall = useCallback(
245
+ (
246
+ toolName: string,
247
+ input: unknown,
248
+ context: AIToolCallInvocationContext
249
+ ) => {
250
+ if (toolName === "formFiller") return false;
251
+ if (
252
+ toolName !== "loadFrontendTool" &&
253
+ toolName !== "executeFrontendTool"
254
+ ) {
255
+ return true;
256
+ }
257
+ const toolId =
258
+ input && typeof input === "object" && !Array.isArray(input)
259
+ ? (input as { toolId?: unknown }).toolId
260
+ : undefined;
261
+ if (
262
+ typeof toolId !== "string" ||
263
+ context.allowedFrontendToolIds?.includes(toolId) !== true
264
+ ) {
265
+ return false;
266
+ }
267
+ const manifest = frontendToolRegistry.getManifest(toolId);
268
+ if (!manifest) return false;
269
+ return toolName === "loadFrontendTool" || manifest.permission === "ALLOW";
270
+ },
271
+ [frontendToolRegistry]
272
+ );
273
+
274
+ const value = useMemo<AIProviderValue>(
275
+ () => ({
276
+ configurationStatus,
277
+ configurationError,
278
+ modelConfigurationError,
279
+ hasEnabledModels,
280
+ employees,
281
+ models,
282
+ globalController,
283
+ uploadFile: service.uploadFile.bind(service),
284
+ updateEmployeeUserPrompt,
285
+ listConversations: service.listConversations.bind(service),
286
+ getConversationMessages: service.getConversationMessages.bind(service),
287
+ getConversationActiveState:
288
+ service.getConversationActiveState.bind(service),
289
+ updateConversationTitle: service.updateConversationTitle.bind(service),
290
+ destroyConversation: service.destroyConversation.bind(service),
291
+ updateToolCallDecision: service.updateToolCallDecision.bind(service),
292
+ invokeToolCall,
293
+ canAutoApproveToolCall,
294
+ createTransport: (options) =>
295
+ new NocoBaseChatTransport({ service, ...options }),
296
+ }),
297
+ [
298
+ configurationError,
299
+ canAutoApproveToolCall,
300
+ configurationStatus,
301
+ employees,
302
+ globalController,
303
+ hasEnabledModels,
304
+ invokeToolCall,
305
+ modelConfigurationError,
306
+ models,
307
+ service,
308
+ updateEmployeeUserPrompt,
309
+ ]
310
+ );
311
+
312
+ return <AIContext.Provider value={value}>{children}</AIContext.Provider>;
313
+ }
314
+
315
+ export function useAI() {
316
+ const value = useContext(AIContext);
317
+ if (!value) throw new Error("useAI must be used inside AIProvider");
318
+ return value;
319
+ }
320
+
321
+ export function useGlobalAIChatController() {
322
+ return useAI().globalController;
323
+ }