@nocobase/portal-template-default 1.0.3 → 1.0.5

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 (170) hide show
  1. package/README.MD +10 -5
  2. package/package.json +17 -17
  3. package/registry/nocobase-ai/components/chat/chat-message.tsx +8 -6
  4. package/registry/nocobase-ai/components/chat/chat-messages.tsx +8 -3
  5. package/registry/nocobase-ai/components/chat/markdown-message.tsx +26 -0
  6. package/registry/nocobase-ai/components/chat/sub-agent-conversation.tsx +3 -3
  7. package/registry/nocobase-ai/demo/configuration-gate.tsx +83 -35
  8. package/registry/nocobase-i18n/README.md +23 -32
  9. package/registry/nocobase-i18n/components/language-switcher.tsx +1 -1
  10. package/registry/nocobase-i18n/demo/index.tsx +2 -2
  11. package/registry/nocobase-i18n/demo/prompt-generator.tsx +3 -2
  12. package/registry/nocobase-i18n/extension.tsx +3 -4
  13. package/registry/nocobase-i18n/index.ts +4 -3
  14. package/registry/nocobase-i18n/locales/en-US.ts +2 -52
  15. package/registry/nocobase-i18n/locales/index.ts +8 -0
  16. package/registry/nocobase-i18n/locales/zh-CN.ts +2 -52
  17. package/registry/nocobase-i18n/provider.tsx +18 -6
  18. package/registry/nocobase-i18n/server-resources.ts +13 -31
  19. package/registry/nocobase-i18n/tests/i18n-regression.mjs +14 -5
  20. package/registry/nocobase-route-surfaces/README.md +21 -0
  21. package/registry/nocobase-route-surfaces/demo/index.tsx +266 -0
  22. package/{src/extensions/nocobase-acl → registry/nocobase-route-surfaces}/demo/lazy-route.tsx +2 -1
  23. package/registry/nocobase-route-surfaces/demo/prompt-generator.tsx +90 -0
  24. package/registry/nocobase-route-surfaces/demo/scenarios.ts +95 -0
  25. package/registry/nocobase-route-surfaces/extension.tsx +119 -0
  26. package/registry/nocobase-route-surfaces/index.ts +7 -0
  27. package/registry/nocobase-route-surfaces/route-dialog.tsx +145 -0
  28. package/registry/nocobase-route-surfaces/route-drawer.tsx +186 -0
  29. package/registry/nocobase-route-surfaces/route-page.tsx +29 -0
  30. package/registry/nocobase-route-surfaces/route-surface-context.ts +13 -0
  31. package/registry/nocobase-route-surfaces/use-refine-unsaved-changes.tsx +106 -0
  32. package/registry/nocobase-route-surfaces/use-route-surface-close.ts +15 -0
  33. package/registry/nocobase-route-surfaces/use-route-surface-state.ts +65 -0
  34. package/registry.config.json +30 -3
  35. package/scripts/registry.mjs +0 -2
  36. package/src/App.tsx +65 -35
  37. package/src/app/extension.ts +1 -2
  38. package/src/app/extensions.tsx +0 -14
  39. package/src/components/app-shell/breadcrumb.tsx +67 -2
  40. package/src/components/app-shell/sidebar.tsx +2 -2
  41. package/src/components/resources/views/list-view.tsx +3 -2
  42. package/src/components/ui/sheet.tsx +22 -21
  43. package/src/lib/nocobase/client.ts +6 -1
  44. package/src/locales/en-US.ts +81 -0
  45. package/src/locales/zh-CN.ts +77 -0
  46. package/src/pages/users/create.tsx +76 -53
  47. package/src/pages/users/edit.tsx +87 -56
  48. package/src/pages/users/index.ts +3 -0
  49. package/src/pages/users/layout.tsx +15 -0
  50. package/src/pages/users/list.tsx +19 -21
  51. package/src/pages/users/role-badges.tsx +43 -0
  52. package/src/pages/users/role-detail.tsx +272 -0
  53. package/src/pages/users/role-utils.ts +6 -0
  54. package/src/pages/users/routes.ts +24 -0
  55. package/src/pages/users/show.tsx +195 -104
  56. package/src/pages/users/types.ts +13 -0
  57. package/src/providers/i18n/index.ts +3 -0
  58. package/{registry/nocobase-i18n → src/providers/i18n}/runtime.ts +63 -16
  59. package/src/providers/system-settings/context.ts +26 -0
  60. package/src/providers/system-settings/index.ts +2 -0
  61. package/src/providers/system-settings/provider.tsx +94 -0
  62. package/src/extensions/nocobase-acl/README.md +0 -19
  63. package/src/extensions/nocobase-acl/components/acl-boundary.tsx +0 -106
  64. package/src/extensions/nocobase-acl/components/index.ts +0 -3
  65. package/src/extensions/nocobase-acl/components/role-options.ts +0 -36
  66. package/src/extensions/nocobase-acl/components/role-switcher.tsx +0 -135
  67. package/src/extensions/nocobase-acl/demo/boundary-api.tsx +0 -186
  68. package/src/extensions/nocobase-acl/demo/components.tsx +0 -231
  69. package/src/extensions/nocobase-acl/demo/index.tsx +0 -505
  70. package/src/extensions/nocobase-acl/demo/prompt-generator.tsx +0 -134
  71. package/src/extensions/nocobase-acl/demo/role-switcher-prompt-generator.tsx +0 -198
  72. package/src/extensions/nocobase-acl/demo/scenario-section.tsx +0 -37
  73. package/src/extensions/nocobase-acl/extension.tsx +0 -71
  74. package/src/extensions/nocobase-acl/index.ts +0 -1
  75. package/src/extensions/nocobase-ai/README.md +0 -70
  76. package/src/extensions/nocobase-ai/adapters/react-hook-form.ts +0 -20
  77. package/src/extensions/nocobase-ai/assets/nocobase-ai-chat.svg +0 -50
  78. package/src/extensions/nocobase-ai/components/ai-root-provider.tsx +0 -32
  79. package/src/extensions/nocobase-ai/components/chat/ai-employee-avatar.tsx +0 -31
  80. package/src/extensions/nocobase-ai/components/chat/chat-attachment.tsx +0 -92
  81. package/src/extensions/nocobase-ai/components/chat/chat-compact.tsx +0 -90
  82. package/src/extensions/nocobase-ai/components/chat/chat-composer.tsx +0 -429
  83. package/src/extensions/nocobase-ai/components/chat/chat-empty-state.tsx +0 -51
  84. package/src/extensions/nocobase-ai/components/chat/chat-header.tsx +0 -88
  85. package/src/extensions/nocobase-ai/components/chat/chat-history-dialog.tsx +0 -77
  86. package/src/extensions/nocobase-ai/components/chat/chat-message.tsx +0 -252
  87. package/src/extensions/nocobase-ai/components/chat/chat-messages.tsx +0 -173
  88. package/src/extensions/nocobase-ai/components/chat/chat-window.tsx +0 -118
  89. package/src/extensions/nocobase-ai/components/chat/conversation-list.tsx +0 -343
  90. package/src/extensions/nocobase-ai/components/chat/markdown-message.tsx +0 -165
  91. package/src/extensions/nocobase-ai/components/chat/model-select-options.tsx +0 -30
  92. package/src/extensions/nocobase-ai/components/chat/reasoning-panel.tsx +0 -29
  93. package/src/extensions/nocobase-ai/components/chat/sub-agent-conversation.tsx +0 -142
  94. package/src/extensions/nocobase-ai/components/chat/tool-call-card.tsx +0 -316
  95. package/src/extensions/nocobase-ai/components/chat/user-prompt-editor.tsx +0 -99
  96. package/src/extensions/nocobase-ai/components/chat/work-context-chip.tsx +0 -114
  97. package/src/extensions/nocobase-ai/components/index.ts +0 -59
  98. package/src/extensions/nocobase-ai/components/page-elements/ai-form.tsx +0 -44
  99. package/src/extensions/nocobase-ai/components/page-elements/page-element-provider.tsx +0 -469
  100. package/src/extensions/nocobase-ai/components/surfaces/chat-dialog.tsx +0 -31
  101. package/src/extensions/nocobase-ai/components/surfaces/chat-inline.tsx +0 -18
  102. package/src/extensions/nocobase-ai/components/surfaces/chat-page.tsx +0 -18
  103. package/src/extensions/nocobase-ai/components/surfaces/chat-side-panel-layout.tsx +0 -54
  104. package/src/extensions/nocobase-ai/components/surfaces/chat-side-panel.tsx +0 -74
  105. package/src/extensions/nocobase-ai/components/surfaces/chat-surface-actions.tsx +0 -54
  106. package/src/extensions/nocobase-ai/components/tools/builtin-tool-renderers.tsx +0 -31
  107. package/src/extensions/nocobase-ai/components/tools/business-report-dialog.tsx +0 -364
  108. package/src/extensions/nocobase-ai/components/tools/business-report-renderer.tsx +0 -122
  109. package/src/extensions/nocobase-ai/components/tools/business-report-utils.tsx +0 -282
  110. package/src/extensions/nocobase-ai/components/tools/chart-renderer.tsx +0 -99
  111. package/src/extensions/nocobase-ai/components/tools/echarts-preview.tsx +0 -59
  112. package/src/extensions/nocobase-ai/components/tools/echarts-runtime-advanced.ts +0 -18
  113. package/src/extensions/nocobase-ai/components/tools/echarts-runtime-common.ts +0 -20
  114. package/src/extensions/nocobase-ai/components/tools/echarts-runtime-components.ts +0 -34
  115. package/src/extensions/nocobase-ai/components/tools/echarts-runtime-hierarchy.ts +0 -24
  116. package/src/extensions/nocobase-ai/components/tools/echarts-runtime.ts +0 -138
  117. package/src/extensions/nocobase-ai/components/tools/sub-agent-renderer.tsx +0 -67
  118. package/src/extensions/nocobase-ai/components/tools/suggestions-renderer.tsx +0 -59
  119. package/src/extensions/nocobase-ai/components/tools/tool-renderer-provider.tsx +0 -55
  120. package/src/extensions/nocobase-ai/components/tools/tool-renderer-utils.ts +0 -18
  121. package/src/extensions/nocobase-ai/components/tools/workflow-renderer.tsx +0 -108
  122. package/src/extensions/nocobase-ai/components/triggers/ai-chat-floating-trigger.tsx +0 -73
  123. package/src/extensions/nocobase-ai/components/triggers/ai-employee-shortcut.tsx +0 -149
  124. package/src/extensions/nocobase-ai/demo/configuration-gate.tsx +0 -49
  125. package/src/extensions/nocobase-ai/demo/container-showcase.tsx +0 -210
  126. package/src/extensions/nocobase-ai/demo/floating.tsx +0 -134
  127. package/src/extensions/nocobase-ai/demo/index.tsx +0 -346
  128. package/src/extensions/nocobase-ai/demo/interaction-showcase.tsx +0 -151
  129. package/src/extensions/nocobase-ai/demo/page-context-prompt-generator.tsx +0 -355
  130. package/src/extensions/nocobase-ai/demo/page-context.tsx +0 -884
  131. package/src/extensions/nocobase-ai/demo/page-element-showcase.tsx +0 -197
  132. package/src/extensions/nocobase-ai/demo/prompt-card.tsx +0 -41
  133. package/src/extensions/nocobase-ai/demo/prompt-generator.tsx +0 -452
  134. package/src/extensions/nocobase-ai/demo/shortcut.tsx +0 -1207
  135. package/src/extensions/nocobase-ai/demo/tool-cards.tsx +0 -530
  136. package/src/extensions/nocobase-ai/extension.tsx +0 -158
  137. package/src/extensions/nocobase-ai/global-ai-chat.tsx +0 -213
  138. package/src/extensions/nocobase-ai/index.ts +0 -3
  139. package/src/extensions/nocobase-ai/providers/ai-provider.tsx +0 -323
  140. package/src/extensions/nocobase-ai/providers/avatars.ts +0 -817
  141. package/src/extensions/nocobase-ai/providers/chat-context.tsx +0 -109
  142. package/src/extensions/nocobase-ai/providers/chat-controller.ts +0 -84
  143. package/src/extensions/nocobase-ai/providers/chat-message-utils.ts +0 -198
  144. package/src/extensions/nocobase-ai/providers/chat-provider.tsx +0 -871
  145. package/src/extensions/nocobase-ai/providers/chat-reducer.ts +0 -147
  146. package/src/extensions/nocobase-ai/providers/chat-task-utils.ts +0 -105
  147. package/src/extensions/nocobase-ai/providers/chat-transport.ts +0 -184
  148. package/src/extensions/nocobase-ai/providers/form-registry.tsx +0 -271
  149. package/src/extensions/nocobase-ai/providers/frontend-tool-registry.tsx +0 -231
  150. package/src/extensions/nocobase-ai/providers/index.ts +0 -61
  151. package/src/extensions/nocobase-ai/providers/model.ts +0 -32
  152. package/src/extensions/nocobase-ai/providers/page-context.tsx +0 -136
  153. package/src/extensions/nocobase-ai/providers/stream-coalescer.ts +0 -60
  154. package/src/extensions/nocobase-ai/providers/stream-event-utils.ts +0 -82
  155. package/src/extensions/nocobase-ai/providers/stream-parser.ts +0 -61
  156. package/src/extensions/nocobase-ai/providers/sub-agent-stream.ts +0 -316
  157. package/src/extensions/nocobase-ai/providers/types.ts +0 -183
  158. package/src/extensions/nocobase-ai/providers/ui-message-stream.ts +0 -463
  159. package/src/extensions/nocobase-ai/providers/use-automatic-tool-approval.ts +0 -94
  160. package/src/extensions/nocobase-ai/providers/use-chat-attachments.ts +0 -206
  161. package/src/extensions/nocobase-ai/providers/use-chat-message-actions.ts +0 -554
  162. package/src/extensions/nocobase-ai/providers/use-chat-runtime.ts +0 -210
  163. package/src/extensions/nocobase-ai/providers/use-chat-work-context.ts +0 -93
  164. package/src/extensions/nocobase-ai/providers/use-conversation-catalog.ts +0 -108
  165. package/src/extensions/nocobase-ai/providers/use-conversation-history.ts +0 -178
  166. package/src/extensions/nocobase-ai/services/index.ts +0 -7
  167. package/src/extensions/nocobase-ai/services/nocobase-ai-service.ts +0 -454
  168. package/src/extensions/nocobase-ai/services/types.ts +0 -82
  169. /package/{registry/nocobase-i18n → src/providers/i18n}/i18n-provider.ts +0 -0
  170. /package/{registry/nocobase-i18n → src/providers/i18n}/locale-store.ts +0 -0
@@ -1,147 +0,0 @@
1
- import { AI_DRAFT_CONVERSATION_ID, type AIConversation } from "./types";
2
-
3
- export type AIChatState = {
4
- activeConversationId: string;
5
- conversations: AIConversation[];
6
- conversationListOpen: boolean;
7
- drafts: Record<string, string>;
8
- selectedEmployeeUsername: string;
9
- selectedModel: string;
10
- };
11
-
12
- export type AIChatAction =
13
- | { type: "reset"; state: AIChatState }
14
- | { type: "set-active-conversation"; conversationId: string }
15
- | { type: "set-conversation-list-open"; open: boolean }
16
- | { type: "set-draft"; conversationId: string; value: string }
17
- | { type: "select-employee"; username: string }
18
- | { type: "select-model"; model: string }
19
- | { type: "start-new-conversation" }
20
- | { type: "add-conversation"; conversation: AIConversation }
21
- | { type: "set-conversations"; conversations: AIConversation[] }
22
- | { type: "remove-conversation"; conversationId: string }
23
- | { type: "mark-conversation-read"; conversationId: string }
24
- | { type: "rename-conversation"; conversationId: string; title: string }
25
- | { type: "replace-conversation-id"; from: string; to: string };
26
-
27
- export function createAIChatState({
28
- conversations,
29
- employeeUsername,
30
- model,
31
- }: {
32
- conversations: AIConversation[];
33
- employeeUsername: string;
34
- model: string;
35
- }): AIChatState {
36
- return {
37
- activeConversationId: conversations[0]?.id ?? AI_DRAFT_CONVERSATION_ID,
38
- conversations,
39
- conversationListOpen: false,
40
- drafts: {},
41
- selectedEmployeeUsername: employeeUsername,
42
- selectedModel: model,
43
- };
44
- }
45
-
46
- export function aiChatReducer(
47
- state: AIChatState,
48
- action: AIChatAction
49
- ): AIChatState {
50
- switch (action.type) {
51
- case "reset":
52
- return action.state;
53
- case "set-active-conversation":
54
- return {
55
- ...state,
56
- activeConversationId: action.conversationId,
57
- conversationListOpen: false,
58
- conversations: state.conversations.map((conversation) =>
59
- conversation.id === action.conversationId
60
- ? { ...conversation, unread: false }
61
- : conversation
62
- ),
63
- };
64
- case "set-conversation-list-open":
65
- return { ...state, conversationListOpen: action.open };
66
- case "set-draft":
67
- return {
68
- ...state,
69
- drafts: { ...state.drafts, [action.conversationId]: action.value },
70
- };
71
- case "select-employee":
72
- return { ...state, selectedEmployeeUsername: action.username };
73
- case "select-model":
74
- return { ...state, selectedModel: action.model };
75
- case "start-new-conversation":
76
- return {
77
- ...state,
78
- activeConversationId: AI_DRAFT_CONVERSATION_ID,
79
- conversationListOpen: false,
80
- drafts: { ...state.drafts, [AI_DRAFT_CONVERSATION_ID]: "" },
81
- };
82
- case "add-conversation":
83
- return {
84
- ...state,
85
- activeConversationId: action.conversation.id,
86
- conversations: [
87
- action.conversation,
88
- ...state.conversations.filter(
89
- (conversation) => conversation.id !== action.conversation.id
90
- ),
91
- ],
92
- drafts: {
93
- ...state.drafts,
94
- [action.conversation.id]:
95
- state.drafts[AI_DRAFT_CONVERSATION_ID] ?? "",
96
- },
97
- };
98
- case "set-conversations":
99
- return {
100
- ...state,
101
- conversations: action.conversations,
102
- };
103
- case "mark-conversation-read":
104
- return {
105
- ...state,
106
- conversations: state.conversations.map((conversation) =>
107
- conversation.id === action.conversationId
108
- ? { ...conversation, unread: false }
109
- : conversation
110
- ),
111
- };
112
- case "remove-conversation": {
113
- const conversations = state.conversations.filter(
114
- (conversation) => conversation.id !== action.conversationId
115
- );
116
- return { ...state, conversations };
117
- }
118
- case "rename-conversation":
119
- return {
120
- ...state,
121
- conversations: state.conversations.map((conversation) =>
122
- conversation.id === action.conversationId
123
- ? { ...conversation, title: action.title }
124
- : conversation
125
- ),
126
- };
127
- case "replace-conversation-id":
128
- return {
129
- ...state,
130
- activeConversationId:
131
- state.activeConversationId === action.from
132
- ? action.to
133
- : state.activeConversationId,
134
- conversations: state.conversations.map((conversation) =>
135
- conversation.id === action.from
136
- ? { ...conversation, id: action.to }
137
- : conversation
138
- ),
139
- drafts: {
140
- ...state.drafts,
141
- [action.to]: state.drafts[action.from] ?? "",
142
- },
143
- };
144
- default:
145
- return state;
146
- }
147
- }
@@ -1,105 +0,0 @@
1
- import { getAIWorkContextRequiredTools, mergeAIRequiredTools } from "./page-context";
2
- import type {
3
- AIChatTaskRuntime,
4
- AIEmployee,
5
- AIEmployeeTask,
6
- AIEmployeeTasks,
7
- AIEmployeeTaskTrigger,
8
- AIModel,
9
- AIWorkContextItem,
10
- } from "./types";
11
-
12
- const EMPTY_TASKS: AIEmployeeTask[] = [];
13
-
14
- export type AIChatTaskSet = {
15
- employeeUsername: string;
16
- tasks: AIEmployeeTask[];
17
- context?: AIWorkContextItem[];
18
- };
19
-
20
- export function getConfiguredAIChatTaskSet({
21
- employeeUsername,
22
- defaultEmployeeUsername,
23
- defaultTasks,
24
- employeeTasks,
25
- inheritedContext,
26
- }: {
27
- employeeUsername: string;
28
- defaultEmployeeUsername: string;
29
- defaultTasks: AIEmployeeTask[];
30
- employeeTasks: AIEmployeeTasks;
31
- inheritedContext: AIWorkContextItem[];
32
- }): AIChatTaskSet | undefined {
33
- const tasks =
34
- employeeTasks[employeeUsername] ??
35
- (employeeUsername === defaultEmployeeUsername
36
- ? defaultTasks
37
- : EMPTY_TASKS);
38
- return tasks.length
39
- ? { employeeUsername, tasks, context: inheritedContext }
40
- : undefined;
41
- }
42
-
43
- export function findTriggeredAIEmployee(
44
- employees: AIEmployee[],
45
- requested: AIEmployeeTaskTrigger["aiEmployee"]
46
- ) {
47
- return typeof requested === "string"
48
- ? employees.find((item) => item.username === requested)
49
- : employees.find((item) => item.username === requested.username) ??
50
- requested;
51
- }
52
-
53
- export function getTriggeredAIEmployeeTask(options: AIEmployeeTaskTrigger) {
54
- return (
55
- options.task ??
56
- (options.tasks?.length === 1 && options.auto !== false
57
- ? options.tasks[0]
58
- : undefined)
59
- );
60
- }
61
-
62
- export function getTriggeredAIWorkContext(
63
- options: AIEmployeeTaskTrigger,
64
- task: AIEmployeeTask | undefined,
65
- inheritedContext: AIWorkContextItem[]
66
- ) {
67
- const taskContext = task?.message?.workContext ?? [];
68
- return taskContext.length
69
- ? taskContext
70
- : options.context?.length
71
- ? options.context
72
- : inheritedContext;
73
- }
74
-
75
- export function createAIChatTaskRuntime(
76
- task: AIEmployeeTask | undefined,
77
- workContext: AIWorkContextItem[]
78
- ): AIChatTaskRuntime {
79
- const requiredTools = getAIWorkContextRequiredTools(workContext);
80
- return task
81
- ? {
82
- systemMessage: task.message?.system,
83
- workContext: [],
84
- skillSettings: mergeAIRequiredTools(task.skillSettings, requiredTools),
85
- webSearch: task.webSearch,
86
- }
87
- : {
88
- workContext: [],
89
- skillSettings: mergeAIRequiredTools(undefined, requiredTools),
90
- };
91
- }
92
-
93
- export function findAIChatTaskModel(
94
- models: AIModel[],
95
- task: AIEmployeeTask | undefined
96
- ) {
97
- return task?.model
98
- ? models.find(
99
- (item) =>
100
- item.value === task.model?.model &&
101
- (!task.model.llmService ||
102
- item.llmService === task.model.llmService)
103
- )
104
- : undefined;
105
- }
@@ -1,184 +0,0 @@
1
- import type { ChatTransport } from "ai";
2
- import type { AIService } from "../services";
3
- import type { AIChatMessage, AIChatRequestContext } from "./types";
4
- import { createNocoBaseUIMessageStream } from "./ui-message-stream";
5
-
6
- const messageText = (message?: AIChatMessage) =>
7
- message?.parts
8
- .filter((part) => part.type === "text")
9
- .map((part) => part.text)
10
- .join("\n") ?? "";
11
-
12
- export class NocoBaseChatTransport implements ChatTransport<AIChatMessage> {
13
- private pendingResend?: { messageId: string };
14
- private pendingConversationResume?: { message?: AIChatMessage };
15
- private pendingToolResume?: {
16
- messageId: string;
17
- responseMessageId: string;
18
- toolCallIds: string[];
19
- toolCallResults: Array<{ id: string; result: unknown }>;
20
- };
21
-
22
- constructor(
23
- private readonly options: {
24
- service: AIService;
25
- getContext: () => AIChatRequestContext;
26
- onSessionCreated?: (sessionId: string) => void;
27
- }
28
- ) {}
29
-
30
- prepareResend(messageId: string) {
31
- this.pendingResend = { messageId };
32
- }
33
-
34
- cancelResend(messageId: string) {
35
- if (this.pendingResend?.messageId === messageId) {
36
- this.pendingResend = undefined;
37
- }
38
- }
39
-
40
- prepareConversationResume(messages: AIChatMessage[]) {
41
- this.pendingConversationResume = {
42
- message: [...messages]
43
- .reverse()
44
- .find((message) => message.role === "assistant"),
45
- };
46
- }
47
-
48
- prepareToolResume(
49
- messageId: string,
50
- responseMessageId: string,
51
- toolCallIds: string[],
52
- toolCallResults: Array<{ id: string; result: unknown }>
53
- ) {
54
- this.pendingToolResume = {
55
- messageId,
56
- responseMessageId,
57
- toolCallIds,
58
- toolCallResults,
59
- };
60
- }
61
-
62
- cancelToolResume(messageId: string) {
63
- if (this.pendingToolResume?.messageId === messageId) {
64
- this.pendingToolResume = undefined;
65
- }
66
- }
67
-
68
- async sendMessages({
69
- messages,
70
- abortSignal,
71
- }: Parameters<ChatTransport<AIChatMessage>["sendMessages"]>[0]) {
72
- const context = this.options.getContext();
73
- if (context.model.configured === false) {
74
- throw new Error(
75
- "No enabled LLM model is configured in NocoBase. Configure and enable an LLM service before starting a conversation."
76
- );
77
- }
78
- let sessionId = context.sessionId;
79
-
80
- const pendingResend = this.pendingResend;
81
- this.pendingResend = undefined;
82
- if (pendingResend) {
83
- if (!sessionId) {
84
- throw new Error("A conversation is required to retry a message.");
85
- }
86
- const stream = await this.options.service.resendMessagesStream(
87
- {
88
- sessionId,
89
- messageId: pendingResend.messageId,
90
- model: {
91
- llmService: context.model.llmService,
92
- model: context.model.value,
93
- },
94
- webSearch: context.task?.webSearch,
95
- },
96
- abortSignal
97
- );
98
- return createNocoBaseUIMessageStream(stream);
99
- }
100
-
101
- if (!sessionId) {
102
- sessionId = await this.options.service.createConversation({
103
- employee: context.employee,
104
- model: context.model,
105
- systemMessage: context.task?.systemMessage,
106
- skillSettings: context.task?.skillSettings,
107
- });
108
- this.options.onSessionCreated?.(sessionId);
109
- }
110
-
111
- const lastMessage = messages.at(-1);
112
- const attachments = lastMessage?.metadata?.attachments?.filter(
113
- (attachment) => attachment.status === "done"
114
- );
115
- const workContext = [
116
- ...(context.task?.workContext ?? []),
117
- ...(lastMessage?.metadata?.workContext ?? []),
118
- ];
119
- const stream = await this.options.service.sendMessagesStream(
120
- {
121
- sessionId,
122
- aiEmployee: context.employee.username,
123
- model: {
124
- llmService: context.model.llmService,
125
- model: context.model.value,
126
- },
127
- systemMessage: context.task?.systemMessage,
128
- skillSettings: context.task?.skillSettings,
129
- webSearch: context.task?.webSearch,
130
- editingMessageId: lastMessage?.metadata?.editingMessageId,
131
- messages: [
132
- {
133
- key: lastMessage?.id ?? crypto.randomUUID(),
134
- role: "user",
135
- content: { type: "text", content: messageText(lastMessage) },
136
- attachments: attachments?.length ? attachments : undefined,
137
- workContext: workContext.length ? workContext : undefined,
138
- },
139
- ],
140
- },
141
- abortSignal
142
- );
143
-
144
- return createNocoBaseUIMessageStream(stream);
145
- }
146
-
147
- async reconnectToStream() {
148
- const pendingToolResume = this.pendingToolResume;
149
- this.pendingToolResume = undefined;
150
- const pendingConversationResume = this.pendingConversationResume;
151
- this.pendingConversationResume = undefined;
152
- const context = this.options.getContext();
153
- if (!context.sessionId) {
154
- throw new Error("A conversation is required to resume a stream.");
155
- }
156
-
157
- if (!pendingToolResume) {
158
- const stream = await this.options.service.resumeConversationStream(
159
- context.sessionId
160
- );
161
- return createNocoBaseUIMessageStream(stream, null, {
162
- seedMessage: pendingConversationResume?.message,
163
- });
164
- }
165
- const stream = await this.options.service.resumeToolCallStream({
166
- sessionId: context.sessionId,
167
- messageId: pendingToolResume.messageId,
168
- toolCallIds: pendingToolResume.toolCallIds,
169
- toolCallResults: pendingToolResume.toolCallResults,
170
- model: {
171
- llmService: context.model.llmService,
172
- model: context.model.value,
173
- },
174
- webSearch: context.task?.webSearch,
175
- });
176
- return createNocoBaseUIMessageStream(
177
- stream,
178
- pendingToolResume.responseMessageId,
179
- {
180
- waitForNewMessage: true,
181
- }
182
- );
183
- }
184
- }
@@ -1,271 +0,0 @@
1
- import {
2
- createContext,
3
- useContext,
4
- useMemo,
5
- type PropsWithChildren,
6
- } from "react";
7
- import type { AIToolInvoker } from "./types";
8
-
9
- export type AIFormField = {
10
- name: string;
11
- title?: string;
12
- type?: string;
13
- description?: string;
14
- readonly?: boolean;
15
- required?: boolean;
16
- enum?: unknown;
17
- [key: string]: unknown;
18
- };
19
-
20
- export type AIFormTarget = {
21
- id: string;
22
- title: string;
23
- fields: AIFormField[];
24
- getValues: () => unknown | Promise<unknown>;
25
- setValues: (values: Record<string, unknown>) => void | Promise<void>;
26
- };
27
-
28
- export type AIFormFillSkippedField = {
29
- name: string;
30
- reason: "undeclared" | "readonly" | "invalid";
31
- message: string;
32
- };
33
-
34
- export type AIFormFillResult = {
35
- status: "success" | "error";
36
- content: string;
37
- appliedFields: string[];
38
- skippedFields: AIFormFillSkippedField[];
39
- };
40
-
41
- export class AIFormRegistry {
42
- private readonly targets = new Map<
43
- string,
44
- { token: symbol; target: AIFormTarget }
45
- >();
46
-
47
- register(target: AIFormTarget) {
48
- if (!target.id.trim()) throw new Error("AI Form id is required");
49
- if (!target.title.trim()) throw new Error("AI Form title is required");
50
- if (!Array.isArray(target.fields)) {
51
- throw new Error("AI Form fields must be an array");
52
- }
53
- if (typeof target.getValues !== "function") {
54
- throw new Error("AI Form getValues must be a function");
55
- }
56
- if (typeof target.setValues !== "function") {
57
- throw new Error("AI Form setValues must be a function");
58
- }
59
- const fieldNames = new Set<string>();
60
- for (const field of target.fields) {
61
- if (!field.name?.trim()) throw new Error("AI Form field name is required");
62
- if (fieldNames.has(field.name)) {
63
- throw new Error(
64
- `AI Form field "${field.name}" is declared more than once`
65
- );
66
- }
67
- fieldNames.add(field.name);
68
- }
69
- if (this.targets.has(target.id)) {
70
- throw new Error(`AI Form "${target.id}" is already registered`);
71
- }
72
- const token = Symbol(target.id);
73
- this.targets.set(target.id, { token, target });
74
- return () => {
75
- if (this.targets.get(target.id)?.token === token) {
76
- this.targets.delete(target.id);
77
- }
78
- };
79
- }
80
-
81
- get(formId: string) {
82
- return this.targets.get(formId)?.target;
83
- }
84
- }
85
-
86
- const AIFormRegistryContext = createContext<AIFormRegistry | null>(null);
87
-
88
- export function AIFormRegistryProvider({ children }: PropsWithChildren) {
89
- const registry = useMemo(() => new AIFormRegistry(), []);
90
- return (
91
- <AIFormRegistryContext.Provider value={registry}>
92
- {children}
93
- </AIFormRegistryContext.Provider>
94
- );
95
- }
96
-
97
- export function useAIFormRegistry() {
98
- const registry = useContext(AIFormRegistryContext);
99
- if (!registry) {
100
- throw new Error(
101
- "useAIFormRegistry must be used inside AIFormRegistryProvider"
102
- );
103
- }
104
- return registry;
105
- }
106
-
107
- const getErrorMessage = (error: unknown) =>
108
- error instanceof Error ? error.message : String(error);
109
-
110
- const getEnumValues = (definition: unknown) => {
111
- if (!Array.isArray(definition)) return undefined;
112
- return definition.map((item) => {
113
- if (item && typeof item === "object" && !Array.isArray(item)) {
114
- return (item as { value?: unknown }).value;
115
- }
116
- return item;
117
- });
118
- };
119
-
120
- const validateFieldValue = (field: AIFormField, value: unknown) => {
121
- const enumValues = getEnumValues(field.enum);
122
- if (enumValues?.length && !enumValues.some((item) => Object.is(item, value))) {
123
- return "The value is not one of the declared options.";
124
- }
125
-
126
- switch (field.type?.toLowerCase()) {
127
- case "string":
128
- case "text":
129
- case "textarea":
130
- case "email":
131
- case "url":
132
- case "date":
133
- case "datetime":
134
- return typeof value === "string" ? undefined : "Expected a string.";
135
- case "number":
136
- case "percent":
137
- return typeof value === "number" && Number.isFinite(value)
138
- ? undefined
139
- : "Expected a finite number.";
140
- case "integer":
141
- return typeof value === "number" && Number.isInteger(value)
142
- ? undefined
143
- : "Expected an integer.";
144
- case "boolean":
145
- case "checkbox":
146
- return typeof value === "boolean" ? undefined : "Expected a boolean.";
147
- case "array":
148
- return Array.isArray(value) ? undefined : "Expected an array.";
149
- case "object":
150
- return value && typeof value === "object" && !Array.isArray(value)
151
- ? undefined
152
- : "Expected an object.";
153
- default:
154
- return undefined;
155
- }
156
- };
157
-
158
- export function createFormFillerInvoker(
159
- registry: AIFormRegistry
160
- ): AIToolInvoker {
161
- return async (input, context) => {
162
- if (!input || typeof input !== "object" || Array.isArray(input)) {
163
- return {
164
- status: "error",
165
- content: "Form filler requires a form identifier and field data.",
166
- appliedFields: [],
167
- skippedFields: [],
168
- };
169
- }
170
-
171
- const { form, data } = input as { form?: unknown; data?: unknown };
172
- if (typeof form !== "string" || !form) {
173
- return {
174
- status: "error",
175
- content: "The target form identifier is missing.",
176
- appliedFields: [],
177
- skippedFields: [],
178
- };
179
- }
180
- if (context.allowedFormIds?.includes(form) !== true) {
181
- return {
182
- status: "error",
183
- content: `The target form "${form}" is not available in this conversation context.`,
184
- appliedFields: [],
185
- skippedFields: [],
186
- };
187
- }
188
- if (!data || typeof data !== "object" || Array.isArray(data)) {
189
- return {
190
- status: "error",
191
- content: "Form filler data must be an object.",
192
- appliedFields: [],
193
- skippedFields: [],
194
- };
195
- }
196
-
197
- const target = registry.get(form);
198
- if (!target) {
199
- return {
200
- status: "error",
201
- content: `The target form "${form}" is not available on this page.`,
202
- appliedFields: [],
203
- skippedFields: [],
204
- };
205
- }
206
-
207
- try {
208
- const fields = new Map(target.fields.map((field) => [field.name, field]));
209
- const accepted: Record<string, unknown> = {};
210
- const skippedFields: AIFormFillSkippedField[] = [];
211
- for (const [name, value] of Object.entries(
212
- data as Record<string, unknown>
213
- )) {
214
- const field = fields.get(name);
215
- if (!field) {
216
- skippedFields.push({
217
- name,
218
- reason: "undeclared",
219
- message: "This field is not declared by the target form.",
220
- });
221
- continue;
222
- }
223
- if (field.readonly) {
224
- skippedFields.push({
225
- name,
226
- reason: "readonly",
227
- message: "This field is read-only.",
228
- });
229
- continue;
230
- }
231
- const validationError = validateFieldValue(field, value);
232
- if (validationError) {
233
- skippedFields.push({
234
- name,
235
- reason: "invalid",
236
- message: validationError,
237
- });
238
- continue;
239
- }
240
- accepted[name] = value;
241
- }
242
-
243
- const appliedFields = Object.keys(accepted);
244
- if (!appliedFields.length) {
245
- return {
246
- status: "error",
247
- content: `No valid editable fields were provided for "${target.title}".`,
248
- appliedFields,
249
- skippedFields,
250
- } satisfies AIFormFillResult;
251
- }
252
-
253
- await target.setValues(accepted);
254
- return {
255
- status: "success",
256
- content: skippedFields.length
257
- ? `Filled ${appliedFields.length} field(s) in "${target.title}" and skipped ${skippedFields.length}. Please review the values and submit the form manually.`
258
- : `Filled "${target.title}". Please review the values and submit the form manually.`,
259
- appliedFields,
260
- skippedFields,
261
- } satisfies AIFormFillResult;
262
- } catch (error) {
263
- return {
264
- status: "error",
265
- content: `Unable to fill "${target.title}": ${getErrorMessage(error)}`,
266
- appliedFields: [],
267
- skippedFields: [],
268
- } satisfies AIFormFillResult;
269
- }
270
- };
271
- }