@nocobase/portal-template-default 1.0.0 → 1.0.2

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 (194) hide show
  1. package/README.MD +90 -1
  2. package/components.json +3 -1
  3. package/package.json +21 -5
  4. package/registry/nocobase-acl/README.md +19 -0
  5. package/registry/nocobase-acl/components/acl-boundary.tsx +106 -0
  6. package/registry/nocobase-acl/components/index.ts +3 -0
  7. package/registry/nocobase-acl/components/role-options.ts +36 -0
  8. package/registry/nocobase-acl/components/role-switcher.tsx +135 -0
  9. package/registry/nocobase-acl/demo/boundary-api.tsx +186 -0
  10. package/registry/nocobase-acl/demo/components.tsx +231 -0
  11. package/registry/nocobase-acl/demo/index.tsx +505 -0
  12. package/registry/nocobase-acl/demo/lazy-route.tsx +10 -0
  13. package/registry/nocobase-acl/demo/prompt-generator.tsx +134 -0
  14. package/registry/nocobase-acl/demo/role-switcher-prompt-generator.tsx +198 -0
  15. package/registry/nocobase-acl/demo/scenario-section.tsx +37 -0
  16. package/registry/nocobase-acl/extension.tsx +71 -0
  17. package/registry/nocobase-acl/index.ts +1 -0
  18. package/registry/nocobase-acl/tests/acl-regression.mjs +314 -0
  19. package/registry/nocobase-ai/README.md +70 -0
  20. package/registry/nocobase-ai/adapters/react-hook-form.ts +20 -0
  21. package/registry/nocobase-ai/assets/nocobase-ai-chat.svg +50 -0
  22. package/registry/nocobase-ai/components/ai-root-provider.tsx +32 -0
  23. package/registry/nocobase-ai/components/chat/ai-employee-avatar.tsx +31 -0
  24. package/registry/nocobase-ai/components/chat/chat-attachment.tsx +92 -0
  25. package/registry/nocobase-ai/components/chat/chat-compact.tsx +90 -0
  26. package/registry/nocobase-ai/components/chat/chat-composer.tsx +429 -0
  27. package/registry/nocobase-ai/components/chat/chat-empty-state.tsx +51 -0
  28. package/registry/nocobase-ai/components/chat/chat-header.tsx +88 -0
  29. package/registry/nocobase-ai/components/chat/chat-history-dialog.tsx +77 -0
  30. package/registry/nocobase-ai/components/chat/chat-message.tsx +252 -0
  31. package/registry/nocobase-ai/components/chat/chat-messages.tsx +173 -0
  32. package/registry/nocobase-ai/components/chat/chat-window.tsx +118 -0
  33. package/registry/nocobase-ai/components/chat/conversation-list.tsx +343 -0
  34. package/registry/nocobase-ai/components/chat/markdown-message.tsx +165 -0
  35. package/registry/nocobase-ai/components/chat/model-select-options.tsx +30 -0
  36. package/registry/nocobase-ai/components/chat/reasoning-panel.tsx +29 -0
  37. package/registry/nocobase-ai/components/chat/sub-agent-conversation.tsx +142 -0
  38. package/registry/nocobase-ai/components/chat/tool-call-card.tsx +316 -0
  39. package/registry/nocobase-ai/components/chat/user-prompt-editor.tsx +99 -0
  40. package/registry/nocobase-ai/components/chat/work-context-chip.tsx +114 -0
  41. package/registry/nocobase-ai/components/index.ts +59 -0
  42. package/registry/nocobase-ai/components/page-elements/ai-form.tsx +44 -0
  43. package/registry/nocobase-ai/components/page-elements/page-element-provider.tsx +469 -0
  44. package/registry/nocobase-ai/components/surfaces/chat-dialog.tsx +31 -0
  45. package/registry/nocobase-ai/components/surfaces/chat-inline.tsx +18 -0
  46. package/registry/nocobase-ai/components/surfaces/chat-page.tsx +18 -0
  47. package/registry/nocobase-ai/components/surfaces/chat-side-panel-layout.tsx +54 -0
  48. package/registry/nocobase-ai/components/surfaces/chat-side-panel.tsx +74 -0
  49. package/registry/nocobase-ai/components/surfaces/chat-surface-actions.tsx +54 -0
  50. package/registry/nocobase-ai/components/tools/builtin-tool-renderers.tsx +31 -0
  51. package/registry/nocobase-ai/components/tools/business-report-dialog.tsx +364 -0
  52. package/registry/nocobase-ai/components/tools/business-report-renderer.tsx +122 -0
  53. package/registry/nocobase-ai/components/tools/business-report-utils.tsx +282 -0
  54. package/registry/nocobase-ai/components/tools/chart-renderer.tsx +99 -0
  55. package/registry/nocobase-ai/components/tools/echarts-preview.tsx +59 -0
  56. package/registry/nocobase-ai/components/tools/echarts-runtime-advanced.ts +18 -0
  57. package/registry/nocobase-ai/components/tools/echarts-runtime-common.ts +20 -0
  58. package/registry/nocobase-ai/components/tools/echarts-runtime-components.ts +34 -0
  59. package/registry/nocobase-ai/components/tools/echarts-runtime-hierarchy.ts +24 -0
  60. package/registry/nocobase-ai/components/tools/echarts-runtime.ts +138 -0
  61. package/registry/nocobase-ai/components/tools/sub-agent-renderer.tsx +67 -0
  62. package/registry/nocobase-ai/components/tools/suggestions-renderer.tsx +59 -0
  63. package/registry/nocobase-ai/components/tools/tool-renderer-provider.tsx +55 -0
  64. package/registry/nocobase-ai/components/tools/tool-renderer-utils.ts +18 -0
  65. package/registry/nocobase-ai/components/tools/workflow-renderer.tsx +108 -0
  66. package/registry/nocobase-ai/components/triggers/ai-chat-floating-trigger.tsx +73 -0
  67. package/registry/nocobase-ai/components/triggers/ai-employee-shortcut.tsx +149 -0
  68. package/registry/nocobase-ai/demo/configuration-gate.tsx +49 -0
  69. package/registry/nocobase-ai/demo/container-showcase.tsx +210 -0
  70. package/registry/nocobase-ai/demo/floating.tsx +134 -0
  71. package/registry/nocobase-ai/demo/index.tsx +346 -0
  72. package/registry/nocobase-ai/demo/interaction-showcase.tsx +151 -0
  73. package/registry/nocobase-ai/demo/page-context-prompt-generator.tsx +355 -0
  74. package/registry/nocobase-ai/demo/page-context.tsx +884 -0
  75. package/registry/nocobase-ai/demo/page-element-showcase.tsx +197 -0
  76. package/registry/nocobase-ai/demo/prompt-card.tsx +41 -0
  77. package/registry/nocobase-ai/demo/prompt-generator.tsx +452 -0
  78. package/registry/nocobase-ai/demo/shortcut.tsx +1207 -0
  79. package/registry/nocobase-ai/demo/tool-cards.tsx +530 -0
  80. package/registry/nocobase-ai/extension.tsx +158 -0
  81. package/registry/nocobase-ai/global-ai-chat.tsx +213 -0
  82. package/registry/nocobase-ai/index.ts +3 -0
  83. package/registry/nocobase-ai/providers/ai-provider.tsx +323 -0
  84. package/registry/nocobase-ai/providers/avatars.ts +817 -0
  85. package/registry/nocobase-ai/providers/chat-context.tsx +109 -0
  86. package/registry/nocobase-ai/providers/chat-controller.ts +84 -0
  87. package/registry/nocobase-ai/providers/chat-message-utils.ts +198 -0
  88. package/registry/nocobase-ai/providers/chat-provider.tsx +871 -0
  89. package/registry/nocobase-ai/providers/chat-reducer.ts +147 -0
  90. package/registry/nocobase-ai/providers/chat-task-utils.ts +105 -0
  91. package/registry/nocobase-ai/providers/chat-transport.ts +184 -0
  92. package/registry/nocobase-ai/providers/form-registry.tsx +271 -0
  93. package/registry/nocobase-ai/providers/frontend-tool-registry.tsx +231 -0
  94. package/registry/nocobase-ai/providers/index.ts +61 -0
  95. package/registry/nocobase-ai/providers/model.ts +32 -0
  96. package/registry/nocobase-ai/providers/page-context.tsx +136 -0
  97. package/registry/nocobase-ai/providers/stream-coalescer.ts +60 -0
  98. package/registry/nocobase-ai/providers/stream-event-utils.ts +82 -0
  99. package/registry/nocobase-ai/providers/stream-parser.ts +61 -0
  100. package/registry/nocobase-ai/providers/sub-agent-stream.ts +316 -0
  101. package/registry/nocobase-ai/providers/types.ts +183 -0
  102. package/registry/nocobase-ai/providers/ui-message-stream.ts +463 -0
  103. package/registry/nocobase-ai/providers/use-automatic-tool-approval.ts +94 -0
  104. package/registry/nocobase-ai/providers/use-chat-attachments.ts +206 -0
  105. package/registry/nocobase-ai/providers/use-chat-message-actions.ts +554 -0
  106. package/registry/nocobase-ai/providers/use-chat-runtime.ts +210 -0
  107. package/registry/nocobase-ai/providers/use-chat-work-context.ts +93 -0
  108. package/registry/nocobase-ai/providers/use-conversation-catalog.ts +108 -0
  109. package/registry/nocobase-ai/providers/use-conversation-history.ts +178 -0
  110. package/registry/nocobase-ai/services/index.ts +7 -0
  111. package/registry/nocobase-ai/services/nocobase-ai-service.ts +454 -0
  112. package/registry/nocobase-ai/services/types.ts +82 -0
  113. package/registry/nocobase-ai/tests/business-report-regression.mjs +70 -0
  114. package/registry/nocobase-ai/tests/form-filler-regression.mjs +129 -0
  115. package/registry/nocobase-ai/tests/frontend-tool-regression.mjs +128 -0
  116. package/registry/nocobase-ai/tests/page-context-regression.mjs +48 -0
  117. package/registry/nocobase-ai/tests/stream-regression.mjs +207 -0
  118. package/registry/nocobase-i18n/README.md +53 -0
  119. package/registry/nocobase-i18n/components/index.ts +1 -0
  120. package/registry/nocobase-i18n/components/language-switcher.tsx +138 -0
  121. package/registry/nocobase-i18n/demo/index.tsx +209 -0
  122. package/registry/nocobase-i18n/demo/prompt-generator.tsx +155 -0
  123. package/registry/nocobase-i18n/extension.tsx +46 -0
  124. package/registry/nocobase-i18n/i18n-provider.ts +9 -0
  125. package/registry/nocobase-i18n/index.ts +6 -0
  126. package/registry/nocobase-i18n/locale-store.ts +134 -0
  127. package/registry/nocobase-i18n/locales/en-US.ts +89 -0
  128. package/registry/nocobase-i18n/locales/zh-CN.ts +85 -0
  129. package/registry/nocobase-i18n/provider.tsx +32 -0
  130. package/registry/nocobase-i18n/runtime.ts +118 -0
  131. package/registry/nocobase-i18n/server-resources.ts +184 -0
  132. package/registry/nocobase-i18n/tests/i18n-regression.mjs +42 -0
  133. package/registry.config.json +163 -0
  134. package/scripts/registry.mjs +164 -0
  135. package/src/App.css +95 -0
  136. package/src/App.tsx +87 -69
  137. package/src/app/extension.ts +11 -0
  138. package/src/app/extensions.tsx +49 -0
  139. package/src/components/access-control/access-denied.tsx +30 -0
  140. package/src/components/access-control/acl-bootstrap.tsx +9 -0
  141. package/src/components/access-control/acl-gate.tsx +36 -0
  142. package/src/components/access-control/navigate-to-accessible-resource.tsx +17 -0
  143. package/src/components/access-control/resource-access-guard.tsx +21 -0
  144. package/src/components/access-control/use-acl-runtime.ts +28 -0
  145. package/src/components/app-shell/breadcrumb.tsx +39 -3
  146. package/src/components/app-shell/document-title-handler.tsx +11 -20
  147. package/src/components/app-shell/header.tsx +22 -4
  148. package/src/components/app-shell/loading-overlay.tsx +2 -4
  149. package/src/components/app-shell/loading-state.tsx +15 -0
  150. package/src/components/app-shell/sidebar.tsx +103 -45
  151. package/src/components/data-table/data-table-pagination.tsx +27 -7
  152. package/src/components/data-table/data-table-styles.ts +57 -0
  153. package/src/components/data-table/data-table.tsx +14 -59
  154. package/src/components/resources/resource-label.ts +101 -0
  155. package/src/components/resources/views/create-view.tsx +17 -14
  156. package/src/components/resources/views/edit-view.tsx +17 -15
  157. package/src/components/resources/views/list-view.tsx +20 -10
  158. package/src/components/resources/views/show-view.tsx +20 -18
  159. package/src/index.tsx +2 -3
  160. package/src/lib/i18n.ts +161 -0
  161. package/src/lib/nocobase/acl/action.ts +147 -0
  162. package/src/lib/nocobase/acl/data-source.ts +24 -0
  163. package/src/lib/nocobase/acl/index.ts +6 -0
  164. package/src/lib/nocobase/acl/menu.ts +41 -0
  165. package/src/lib/nocobase/acl/record-permissions.ts +111 -0
  166. package/src/lib/nocobase/acl/store.ts +182 -0
  167. package/src/lib/nocobase/acl/types.ts +96 -0
  168. package/src/lib/nocobase/client.ts +279 -0
  169. package/src/lib/nocobase/error.ts +32 -0
  170. package/src/locales/en-US.ts +82 -0
  171. package/src/locales/index.ts +13 -0
  172. package/src/locales/zh-CN.ts +79 -0
  173. package/src/pages/users/create.tsx +98 -0
  174. package/src/pages/users/edit.tsx +92 -0
  175. package/src/pages/users/form-context.ts +57 -0
  176. package/src/pages/users/form-fields.tsx +211 -0
  177. package/src/pages/users/list.tsx +205 -0
  178. package/src/pages/users/show.tsx +187 -0
  179. package/src/pages/users/types.ts +20 -0
  180. package/src/providers/access-control.ts +34 -0
  181. package/src/providers/auth.ts +74 -60
  182. package/src/providers/constants.ts +3 -37
  183. package/src/providers/data.ts +242 -85
  184. package/vite.config.ts +17 -0
  185. package/src/pages/blog-posts/create.tsx +0 -177
  186. package/src/pages/blog-posts/edit.tsx +0 -197
  187. package/src/pages/blog-posts/list.tsx +0 -131
  188. package/src/pages/blog-posts/show.tsx +0 -81
  189. package/src/pages/categories/create.tsx +0 -73
  190. package/src/pages/categories/edit.tsx +0 -73
  191. package/src/pages/categories/index.ts +0 -4
  192. package/src/pages/categories/list.tsx +0 -86
  193. package/src/pages/categories/show.tsx +0 -37
  194. /package/src/pages/{blog-posts → users}/index.ts +0 -0
@@ -0,0 +1,871 @@
1
+ import { Chat, useChat } from "@ai-sdk/react";
2
+ import {
3
+ useCallback,
4
+ useEffect,
5
+ useMemo,
6
+ useReducer,
7
+ useRef,
8
+ useState,
9
+ type PropsWithChildren,
10
+ } from "react";
11
+ import { aiChatReducer, createAIChatState } from "./chat-reducer";
12
+ import {
13
+ AIChatContext,
14
+ AIChatMessagesContext,
15
+ AIChatStatusContext,
16
+ type AIChatBaseContextValue,
17
+ type AIChatMessagesContextValue,
18
+ type AIChatStatusContextValue,
19
+ } from "./chat-context";
20
+ import {
21
+ createAIChatTaskRuntime,
22
+ findAIChatTaskModel,
23
+ findTriggeredAIEmployee,
24
+ getConfiguredAIChatTaskSet,
25
+ getTriggeredAIEmployeeTask,
26
+ getTriggeredAIWorkContext,
27
+ type AIChatTaskSet,
28
+ } from "./chat-task-utils";
29
+ import {
30
+ useAIChatControllerState,
31
+ type AIChatController,
32
+ } from "./chat-controller";
33
+ import { useAI } from "./ai-provider";
34
+ import { findAIModel, getAIModelKey } from "./model";
35
+ import { useChatAttachments } from "./use-chat-attachments";
36
+ import {
37
+ useChatMessageActions,
38
+ type AIMessageEditingSnapshot,
39
+ } from "./use-chat-message-actions";
40
+ import { useChatWorkContext } from "./use-chat-work-context";
41
+ import { useConversationCatalog } from "./use-conversation-catalog";
42
+ import { useConversationHistory } from "./use-conversation-history";
43
+ import { useChatRuntime } from "./use-chat-runtime";
44
+ import {
45
+ getAIWorkContextRequiredTools,
46
+ mergeAIRequiredTools,
47
+ useAIPageContextResolver,
48
+ useAIPageContextScope,
49
+ } from "./page-context";
50
+ import {
51
+ AI_DRAFT_CONVERSATION_ID,
52
+ type AIChatMessage,
53
+ type AIChatTaskRuntime,
54
+ type AIConversation,
55
+ type AIEmployeeTask,
56
+ type AIEmployeeTasks,
57
+ type AIEmployeeTaskTrigger,
58
+ type AIWorkContextItem,
59
+ } from "./types";
60
+
61
+ const EMPTY_TASKS: AIEmployeeTask[] = [];
62
+ const EMPTY_EMPLOYEE_TASKS: AIEmployeeTasks = {};
63
+
64
+ export type AIChatProviderProps = PropsWithChildren<{
65
+ id: string;
66
+ controller?: AIChatController;
67
+ defaultEmployee?: string;
68
+ defaultTasks?: AIEmployeeTask[];
69
+ employeeTasks?: AIEmployeeTasks;
70
+ webSearch?: boolean;
71
+ }>;
72
+
73
+ export function AIChatProvider({
74
+ id,
75
+ controller,
76
+ defaultEmployee,
77
+ defaultTasks = EMPTY_TASKS,
78
+ employeeTasks = EMPTY_EMPLOYEE_TASKS,
79
+ webSearch = false,
80
+ children,
81
+ }: AIChatProviderProps) {
82
+ const ai = useAI();
83
+ const resolvePageContext = useAIPageContextResolver();
84
+ const inheritedPageContext = useAIPageContextScope();
85
+ const { open: chatSurfaceOpen } = useAIChatControllerState(controller);
86
+ const chatSurfaceOpenRef = useRef(chatSurfaceOpen);
87
+ chatSurfaceOpenRef.current = chatSurfaceOpen;
88
+ const { configurationStatus, listConversations } = ai;
89
+ const defaultEmployeeUsername =
90
+ ai.employees.find((employee) => employee.username === defaultEmployee)
91
+ ?.username ??
92
+ ai.employees[0]?.username ??
93
+ "assistant";
94
+ const [state, dispatch] = useReducer(
95
+ aiChatReducer,
96
+ createAIChatState({
97
+ conversations: [],
98
+ employeeUsername: defaultEmployeeUsername,
99
+ model: ai.models[0] ? getAIModelKey(ai.models[0]) : "default",
100
+ })
101
+ );
102
+ const conversationFinishedHandlerRef =
103
+ useRef<
104
+ (conversationId: string, chat: Chat<AIChatMessage>) => Promise<void>
105
+ >(undefined);
106
+ const [historyError, setHistoryError] = useState<Error>();
107
+ const [interactionError, setInteractionError] = useState<Error>();
108
+ const interactionVersionRef = useRef(0);
109
+ const invalidatePendingInteraction = useCallback(() => {
110
+ interactionVersionRef.current += 1;
111
+ }, []);
112
+ const setConversationList = useCallback(
113
+ (conversations: AIConversation[]) =>
114
+ dispatch({ type: "set-conversations", conversations }),
115
+ []
116
+ );
117
+ const {
118
+ loading: conversationsLoading,
119
+ search: conversationSearch,
120
+ refresh: refreshConversationCatalog,
121
+ searchConversations,
122
+ updateCatalog: updateConversationCatalog,
123
+ } = useConversationCatalog({
124
+ configurationStatus,
125
+ listConversations,
126
+ onChange: setConversationList,
127
+ onError: setHistoryError,
128
+ });
129
+ const {
130
+ attachments,
131
+ uploadingAttachments,
132
+ uploadFiles,
133
+ removeAttachment,
134
+ setConversationAttachments,
135
+ moveAttachments,
136
+ removeConversationAttachments,
137
+ getConversationAttachments,
138
+ } = useChatAttachments(state.activeConversationId);
139
+ const {
140
+ workContext,
141
+ addWorkContext,
142
+ removeWorkContext,
143
+ setConversationWorkContext,
144
+ moveWorkContext,
145
+ removeConversationWorkContext,
146
+ getConversationWorkContext,
147
+ } = useChatWorkContext(state.activeConversationId);
148
+ const [editingMessageId, setEditingMessageId] = useState<string>();
149
+ const editingSnapshotRef = useRef<AIMessageEditingSnapshot | undefined>(
150
+ undefined
151
+ );
152
+ const webSearchRef = useRef(webSearch);
153
+ const taskRuntimeRef = useRef<AIChatTaskRuntime | undefined>(undefined);
154
+ const [pendingTask, setPendingTask] = useState<{
155
+ key: string;
156
+ employeeUsername: string;
157
+ task: AIEmployeeTask;
158
+ auto: boolean;
159
+ }>();
160
+ const getConfiguredTaskSet = useCallback(
161
+ (employeeUsername: string) =>
162
+ getConfiguredAIChatTaskSet({
163
+ employeeUsername,
164
+ defaultEmployeeUsername,
165
+ defaultTasks,
166
+ employeeTasks,
167
+ inheritedContext: inheritedPageContext,
168
+ }),
169
+ [defaultEmployeeUsername, defaultTasks, employeeTasks, inheritedPageContext]
170
+ );
171
+ const [activeTaskSet, setActiveTaskSet] = useState<AIChatTaskSet | undefined>(
172
+ () => getConfiguredTaskSet(defaultEmployeeUsername)
173
+ );
174
+ const [composerFocusRequest, requestComposerFocus] = useReducer(
175
+ (request: number) => request + 1,
176
+ 0
177
+ );
178
+ const stateRef = useRef(state);
179
+ stateRef.current = state;
180
+ webSearchRef.current = webSearch;
181
+ const {
182
+ transportsRef,
183
+ runtimeContextsRef,
184
+ getRuntimeContext,
185
+ getChat,
186
+ getTransport,
187
+ remove: removeChatRuntime,
188
+ } = useChatRuntime({
189
+ id,
190
+ ai,
191
+ stateRef,
192
+ taskRuntimeRef,
193
+ webSearchRef,
194
+ conversationFinishedHandlerRef,
195
+ moveAttachments,
196
+ moveWorkContext,
197
+ dispatch,
198
+ });
199
+
200
+ const currentEmployee =
201
+ ai.employees.find(
202
+ (employee) => employee.username === state.selectedEmployeeUsername
203
+ ) ?? ai.employees[0];
204
+ const currentModel =
205
+ findAIModel(ai.models, state.selectedModel) ?? ai.models[0];
206
+
207
+ if (!currentEmployee || !currentModel) {
208
+ throw new Error("AIProvider requires at least one employee and model");
209
+ }
210
+
211
+ const getActiveConversationId = useCallback(
212
+ () => stateRef.current.activeConversationId,
213
+ []
214
+ );
215
+ const markConversationRead = useCallback(
216
+ (conversationId: string) =>
217
+ dispatch({ type: "mark-conversation-read", conversationId }),
218
+ []
219
+ );
220
+ const {
221
+ invalidate: invalidateConversationHistory,
222
+ load: loadConversationMessages,
223
+ loadingId: messageLoadingId,
224
+ refresh: refreshConversationMessages,
225
+ } = useConversationHistory({
226
+ chatSurfaceOpen,
227
+ activeConversationId: state.activeConversationId,
228
+ getActiveConversationId,
229
+ getChat,
230
+ getTransport,
231
+ getConversationMessages: ai.getConversationMessages,
232
+ getConversationActiveState: ai.getConversationActiveState,
233
+ onMarkRead: markConversationRead,
234
+ onError: setHistoryError,
235
+ });
236
+
237
+ const activeChat = getChat(state.activeConversationId);
238
+ const chat = useChat<AIChatMessage>({
239
+ chat: activeChat,
240
+ experimental_throttle: 32,
241
+ });
242
+ const draft = state.drafts[state.activeConversationId] ?? "";
243
+ const activeConversation = state.conversations.find(
244
+ (conversation) => conversation.id === state.activeConversationId
245
+ );
246
+
247
+ const setDraft = useCallback(
248
+ (value: string) => {
249
+ dispatch({
250
+ type: "set-draft",
251
+ conversationId: state.activeConversationId,
252
+ value,
253
+ });
254
+ },
255
+ [state.activeConversationId]
256
+ );
257
+
258
+ const sendText = useCallback(
259
+ async (rawValue: string) => {
260
+ const value = rawValue.trim();
261
+ const operationVersion = interactionVersionRef.current + 1;
262
+ interactionVersionRef.current = operationVersion;
263
+ const currentState = stateRef.current;
264
+ const currentId = currentState.activeConversationId;
265
+ const employee =
266
+ ai.employees.find(
267
+ (item) => item.username === currentState.selectedEmployeeUsername
268
+ ) ?? ai.employees[0];
269
+ const model =
270
+ findAIModel(ai.models, currentState.selectedModel) ?? ai.models[0];
271
+ const conversation = currentState.conversations.find(
272
+ (item) => item.id === currentId
273
+ );
274
+ if (!employee || !model) return;
275
+ const currentAttachments = getConversationAttachments(currentId);
276
+ const unresolvedWorkContext = getConversationWorkContext(currentId);
277
+ if (
278
+ currentAttachments.some(
279
+ (attachment) => attachment.status === "uploading"
280
+ ) ||
281
+ (!value &&
282
+ !currentAttachments.some(
283
+ (attachment) => attachment.status === "done"
284
+ ) &&
285
+ !unresolvedWorkContext.length) ||
286
+ activeChat.status === "streaming" ||
287
+ activeChat.status === "submitted"
288
+ )
289
+ return;
290
+
291
+ setInteractionError(undefined);
292
+ let currentWorkContext = unresolvedWorkContext;
293
+ try {
294
+ currentWorkContext = resolvePageContext
295
+ ? await resolvePageContext(unresolvedWorkContext)
296
+ : unresolvedWorkContext;
297
+ } catch (error) {
298
+ if (interactionVersionRef.current !== operationVersion) return;
299
+ setInteractionError(
300
+ error instanceof Error
301
+ ? error
302
+ : new Error("Unable to read the selected page context")
303
+ );
304
+ return;
305
+ }
306
+ const latestState = stateRef.current;
307
+ if (
308
+ interactionVersionRef.current !== operationVersion ||
309
+ latestState.activeConversationId !== currentId ||
310
+ latestState.selectedEmployeeUsername !== employee.username ||
311
+ latestState.selectedModel !== getAIModelKey(model) ||
312
+ getConversationAttachments(currentId) !== currentAttachments ||
313
+ getConversationWorkContext(currentId) !== unresolvedWorkContext
314
+ ) {
315
+ return;
316
+ }
317
+
318
+ const completedAttachments = currentAttachments.filter(
319
+ (attachment) => attachment.status === "done"
320
+ );
321
+ if (!value && !completedAttachments.length && !currentWorkContext.length) {
322
+ return;
323
+ }
324
+ const requiredTools = getAIWorkContextRequiredTools(currentWorkContext);
325
+ const currentTask = taskRuntimeRef.current;
326
+ const runtimeTask =
327
+ currentTask || requiredTools.length
328
+ ? {
329
+ ...(currentTask ?? { workContext: [] }),
330
+ skillSettings: mergeAIRequiredTools(
331
+ currentTask?.skillSettings,
332
+ requiredTools
333
+ ),
334
+ }
335
+ : undefined;
336
+ runtimeContextsRef.current.set(currentId, {
337
+ employeeUsername: employee.username,
338
+ model: getAIModelKey(model),
339
+ task: runtimeTask,
340
+ });
341
+ const title =
342
+ value ||
343
+ completedAttachments[0]?.filename ||
344
+ currentWorkContext[0]?.title ||
345
+ "New conversation";
346
+ if (!conversation) {
347
+ dispatch({
348
+ type: "add-conversation",
349
+ conversation: {
350
+ id: currentId,
351
+ title: title.slice(0, 42),
352
+ employeeUsername: employee.username,
353
+ updatedAt: new Date().toISOString(),
354
+ },
355
+ });
356
+ }
357
+
358
+ dispatch({ type: "set-draft", conversationId: currentId, value: "" });
359
+ setConversationAttachments(currentId, []);
360
+ setConversationWorkContext(currentId, []);
361
+ const activeEditingMessageId = editingMessageId;
362
+ setEditingMessageId(undefined);
363
+ editingSnapshotRef.current = undefined;
364
+ await activeChat.sendMessage({
365
+ parts: [
366
+ ...(value ? [{ type: "text" as const, text: value }] : []),
367
+ ...completedAttachments
368
+ .filter((attachment) => attachment.url || attachment.preview)
369
+ .map((attachment) => ({
370
+ type: "file" as const,
371
+ mediaType: attachment.mimetype ?? "application/octet-stream",
372
+ filename: attachment.filename,
373
+ url: attachment.url ?? attachment.preview ?? "",
374
+ })),
375
+ ],
376
+ metadata: {
377
+ createdAt: new Date().toISOString(),
378
+ employeeUsername: employee.username,
379
+ editingMessageId: activeEditingMessageId,
380
+ attachments: completedAttachments,
381
+ workContext: currentWorkContext,
382
+ },
383
+ });
384
+ },
385
+ [
386
+ ai.employees,
387
+ ai.models,
388
+ activeChat,
389
+ editingMessageId,
390
+ getConversationAttachments,
391
+ getConversationWorkContext,
392
+ resolvePageContext,
393
+ runtimeContextsRef,
394
+ setConversationAttachments,
395
+ setConversationWorkContext,
396
+ ]
397
+ );
398
+
399
+ const send = useCallback(async () => {
400
+ const value =
401
+ stateRef.current.drafts[stateRef.current.activeConversationId] ?? "";
402
+ await sendText(value);
403
+ }, [sendText]);
404
+
405
+ const {
406
+ retryMessage,
407
+ decideToolCall,
408
+ startEditingMessage,
409
+ cancelEditingMessage,
410
+ clearAutomaticToolApproval,
411
+ processAutomaticToolApprovals,
412
+ } = useChatMessageActions({
413
+ ai,
414
+ activeChat,
415
+ stateRef,
416
+ chatSurfaceOpenRef,
417
+ transportsRef,
418
+ getRuntimeContext,
419
+ refreshConversationMessages,
420
+ setHistoryError,
421
+ editingSnapshotRef,
422
+ setEditingMessageId,
423
+ getConversationAttachments,
424
+ getConversationWorkContext,
425
+ setConversationAttachments,
426
+ setConversationWorkContext,
427
+ dispatch,
428
+ requestComposerFocus,
429
+ });
430
+
431
+ const handleConversationFinished = useCallback(
432
+ async (conversationId: string, targetChat: Chat<AIChatMessage>) => {
433
+ try {
434
+ const updateRead =
435
+ stateRef.current.activeConversationId === conversationId &&
436
+ chatSurfaceOpenRef.current;
437
+ await refreshConversationMessages(conversationId, targetChat, {
438
+ updateRead,
439
+ });
440
+ await refreshConversationCatalog();
441
+ await processAutomaticToolApprovals(conversationId, targetChat);
442
+ } catch (error) {
443
+ setHistoryError(
444
+ error instanceof Error
445
+ ? error
446
+ : new Error("Unable to refresh the conversation")
447
+ );
448
+ }
449
+ },
450
+ [
451
+ processAutomaticToolApprovals,
452
+ refreshConversationCatalog,
453
+ refreshConversationMessages,
454
+ ]
455
+ );
456
+ conversationFinishedHandlerRef.current = handleConversationFinished;
457
+
458
+ const startNewConversation = useCallback(() => {
459
+ invalidatePendingInteraction();
460
+ const snapshot = editingSnapshotRef.current;
461
+ if (
462
+ snapshot &&
463
+ snapshot.conversationId === stateRef.current.activeConversationId
464
+ ) {
465
+ activeChat.messages = snapshot.messages;
466
+ setConversationAttachments(snapshot.conversationId, snapshot.attachments);
467
+ setConversationWorkContext(snapshot.conversationId, snapshot.workContext);
468
+ }
469
+ removeChatRuntime(AI_DRAFT_CONVERSATION_ID);
470
+ invalidateConversationHistory();
471
+ taskRuntimeRef.current = undefined;
472
+ setInteractionError(undefined);
473
+ setPendingTask(undefined);
474
+ setEditingMessageId(undefined);
475
+ editingSnapshotRef.current = undefined;
476
+ setConversationAttachments(AI_DRAFT_CONVERSATION_ID, []);
477
+ setConversationWorkContext(AI_DRAFT_CONVERSATION_ID, []);
478
+ setActiveTaskSet(
479
+ getConfiguredTaskSet(stateRef.current.selectedEmployeeUsername)
480
+ );
481
+ dispatch({ type: "start-new-conversation" });
482
+ requestComposerFocus();
483
+ }, [
484
+ activeChat,
485
+ getConfiguredTaskSet,
486
+ invalidateConversationHistory,
487
+ invalidatePendingInteraction,
488
+ removeChatRuntime,
489
+ setConversationAttachments,
490
+ setConversationWorkContext,
491
+ ]);
492
+
493
+ const triggerTask = useCallback(
494
+ async (options: AIEmployeeTaskTrigger) => {
495
+ const operationVersion = interactionVersionRef.current + 1;
496
+ interactionVersionRef.current = operationVersion;
497
+ cancelEditingMessage();
498
+ const employee = findTriggeredAIEmployee(
499
+ ai.employees,
500
+ options.aiEmployee
501
+ );
502
+
503
+ if (!employee) {
504
+ console.warn(
505
+ `AI employee "${String(options.aiEmployee)}" was not found.`
506
+ );
507
+ return;
508
+ }
509
+
510
+ if (options.open !== false) controller?.open();
511
+
512
+ const task = getTriggeredAIEmployeeTask(options);
513
+ const contextItems = getTriggeredAIWorkContext(
514
+ options,
515
+ task,
516
+ inheritedPageContext
517
+ );
518
+ let workContext: AIWorkContextItem[];
519
+ try {
520
+ setInteractionError(undefined);
521
+ workContext = resolvePageContext
522
+ ? await resolvePageContext(contextItems)
523
+ : contextItems;
524
+ } catch (error) {
525
+ if (interactionVersionRef.current !== operationVersion) return;
526
+ setInteractionError(
527
+ error instanceof Error
528
+ ? error
529
+ : new Error("Unable to read the selected page context")
530
+ );
531
+ return;
532
+ }
533
+ if (interactionVersionRef.current !== operationVersion) return;
534
+ taskRuntimeRef.current = createAIChatTaskRuntime(task, workContext);
535
+
536
+ removeChatRuntime(AI_DRAFT_CONVERSATION_ID);
537
+ invalidateConversationHistory();
538
+ setConversationAttachments(AI_DRAFT_CONVERSATION_ID, []);
539
+ setConversationWorkContext(AI_DRAFT_CONVERSATION_ID, workContext);
540
+ dispatch({ type: "select-employee", username: employee.username });
541
+ dispatch({ type: "start-new-conversation" });
542
+
543
+ const taskModel = findAIChatTaskModel(ai.models, task);
544
+ const resolvedModel = taskModel ?? ai.models[0];
545
+ if (resolvedModel) {
546
+ dispatch({
547
+ type: "select-model",
548
+ model: getAIModelKey(resolvedModel),
549
+ });
550
+ }
551
+
552
+ if (task) {
553
+ setActiveTaskSet(undefined);
554
+ setPendingTask({
555
+ key: crypto.randomUUID(),
556
+ employeeUsername: employee.username,
557
+ task,
558
+ auto: options.auto !== false,
559
+ });
560
+ } else if (options.tasks?.length) {
561
+ setPendingTask(undefined);
562
+ setActiveTaskSet({
563
+ employeeUsername: employee.username,
564
+ tasks: options.tasks,
565
+ context: options.context,
566
+ });
567
+ } else {
568
+ setPendingTask(undefined);
569
+ setActiveTaskSet(getConfiguredTaskSet(employee.username));
570
+ }
571
+ requestComposerFocus();
572
+ },
573
+ [
574
+ ai.employees,
575
+ ai.models,
576
+ cancelEditingMessage,
577
+ controller,
578
+ getConfiguredTaskSet,
579
+ inheritedPageContext,
580
+ invalidateConversationHistory,
581
+ resolvePageContext,
582
+ removeChatRuntime,
583
+ setConversationAttachments,
584
+ setConversationWorkContext,
585
+ ]
586
+ );
587
+
588
+ useEffect(() => {
589
+ if (
590
+ !pendingTask ||
591
+ state.activeConversationId !== AI_DRAFT_CONVERSATION_ID ||
592
+ currentEmployee.username !== pendingTask.employeeUsername
593
+ ) {
594
+ return;
595
+ }
596
+
597
+ const userMessage =
598
+ pendingTask.task.message?.user ?? pendingTask.task.title ?? "";
599
+ setPendingTask(undefined);
600
+ if (pendingTask.auto && pendingTask.task.autoSend && userMessage.trim()) {
601
+ void sendText(userMessage);
602
+ return;
603
+ }
604
+ dispatch({
605
+ type: "set-draft",
606
+ conversationId: AI_DRAFT_CONVERSATION_ID,
607
+ value: userMessage,
608
+ });
609
+ }, [
610
+ currentEmployee.username,
611
+ pendingTask,
612
+ sendText,
613
+ state.activeConversationId,
614
+ ]);
615
+
616
+ useEffect(() => {
617
+ if (!controller) return;
618
+ return controller.bindTaskHandler(triggerTask);
619
+ }, [controller, triggerTask]);
620
+
621
+ const runTask = useCallback(
622
+ (task: AIEmployeeTask) => {
623
+ if (!activeTaskSet) return;
624
+ void triggerTask({
625
+ aiEmployee: activeTaskSet.employeeUsername,
626
+ task,
627
+ context: activeTaskSet.context,
628
+ auto: true,
629
+ open: false,
630
+ });
631
+ },
632
+ [activeTaskSet, triggerTask]
633
+ );
634
+
635
+ const removeConversation = useCallback(
636
+ async (conversationId: string) => {
637
+ if (stateRef.current.activeConversationId === conversationId) {
638
+ invalidatePendingInteraction();
639
+ }
640
+ try {
641
+ await ai.destroyConversation(conversationId);
642
+ } catch (error) {
643
+ const resolvedError =
644
+ error instanceof Error
645
+ ? error
646
+ : new Error("Unable to delete conversation");
647
+ setHistoryError(resolvedError);
648
+ throw resolvedError;
649
+ }
650
+ removeChatRuntime(conversationId);
651
+ clearAutomaticToolApproval(conversationId);
652
+ removeConversationAttachments(conversationId);
653
+ removeConversationWorkContext(conversationId);
654
+ dispatch({ type: "remove-conversation", conversationId });
655
+ updateConversationCatalog((items) =>
656
+ items.filter((conversation) => conversation.id !== conversationId)
657
+ );
658
+ if (stateRef.current.activeConversationId === conversationId) {
659
+ removeChatRuntime(AI_DRAFT_CONVERSATION_ID);
660
+ invalidateConversationHistory();
661
+ taskRuntimeRef.current = undefined;
662
+ setPendingTask(undefined);
663
+ setEditingMessageId(undefined);
664
+ editingSnapshotRef.current = undefined;
665
+ setConversationAttachments(AI_DRAFT_CONVERSATION_ID, []);
666
+ setConversationWorkContext(AI_DRAFT_CONVERSATION_ID, []);
667
+ setActiveTaskSet(
668
+ getConfiguredTaskSet(stateRef.current.selectedEmployeeUsername)
669
+ );
670
+ dispatch({ type: "start-new-conversation" });
671
+ requestComposerFocus();
672
+ }
673
+ },
674
+ [
675
+ ai,
676
+ clearAutomaticToolApproval,
677
+ getConfiguredTaskSet,
678
+ invalidateConversationHistory,
679
+ invalidatePendingInteraction,
680
+ removeConversationAttachments,
681
+ removeConversationWorkContext,
682
+ removeChatRuntime,
683
+ setConversationAttachments,
684
+ setConversationWorkContext,
685
+ updateConversationCatalog,
686
+ ]
687
+ );
688
+
689
+ const renameConversation = useCallback(
690
+ async (conversationId: string, rawTitle: string) => {
691
+ const title = rawTitle.trim();
692
+ if (!title) return;
693
+ const conversation = stateRef.current.conversations.find(
694
+ (item) => item.id === conversationId
695
+ );
696
+ if (!conversation || conversation.title === title) return;
697
+ await ai.updateConversationTitle(conversationId, title);
698
+ dispatch({ type: "rename-conversation", conversationId, title });
699
+ updateConversationCatalog((items) =>
700
+ items.map((item) =>
701
+ item.id === conversationId ? { ...item, title } : item
702
+ )
703
+ );
704
+ },
705
+ [ai, updateConversationCatalog]
706
+ );
707
+
708
+ const value = useMemo<AIChatBaseContextValue>(
709
+ () => ({
710
+ id,
711
+ employees: ai.employees,
712
+ models: ai.models,
713
+ currentEmployee,
714
+ currentModel,
715
+ activeConversation,
716
+ activeConversationId: state.activeConversationId,
717
+ conversations: state.conversations,
718
+ conversationsLoading,
719
+ conversationSearch,
720
+ messagesLoading: messageLoadingId === state.activeConversationId,
721
+ historyError,
722
+ interactionError,
723
+ conversationListOpen: state.conversationListOpen,
724
+ availableTasks: activeTaskSet?.tasks ?? [],
725
+ composerFocusRequest,
726
+ draft,
727
+ attachments,
728
+ uploadingAttachments,
729
+ workContext,
730
+ editingMessageId,
731
+ setDraft,
732
+ uploadFiles,
733
+ removeAttachment,
734
+ addWorkContext,
735
+ removeWorkContext,
736
+ send,
737
+ stop: () => activeChat.stop(),
738
+ regenerate: () => activeChat.regenerate(),
739
+ retryMessage,
740
+ decideToolCall,
741
+ startNewConversation,
742
+ selectConversation: (conversationId) => {
743
+ invalidatePendingInteraction();
744
+ cancelEditingMessage();
745
+ setInteractionError(undefined);
746
+ taskRuntimeRef.current = undefined;
747
+ setPendingTask(undefined);
748
+ setActiveTaskSet(undefined);
749
+ const conversation = stateRef.current.conversations.find(
750
+ (item) => item.id === conversationId
751
+ );
752
+ if (conversation?.employeeUsername) {
753
+ dispatch({
754
+ type: "select-employee",
755
+ username: conversation.employeeUsername,
756
+ });
757
+ }
758
+ if (conversation?.model) {
759
+ const model = ai.models.find(
760
+ (item) =>
761
+ item.value === conversation.model?.model &&
762
+ (!conversation.model.llmService ||
763
+ item.llmService === conversation.model.llmService)
764
+ );
765
+ if (model) {
766
+ dispatch({ type: "select-model", model: getAIModelKey(model) });
767
+ }
768
+ }
769
+ dispatch({ type: "set-active-conversation", conversationId });
770
+ void loadConversationMessages(conversationId);
771
+ requestComposerFocus();
772
+ },
773
+ renameConversation,
774
+ removeConversation,
775
+ searchConversations,
776
+ setConversationListOpen: (open) =>
777
+ dispatch({ type: "set-conversation-list-open", open }),
778
+ selectEmployee: (username) => {
779
+ invalidatePendingInteraction();
780
+ cancelEditingMessage();
781
+ setInteractionError(undefined);
782
+ removeChatRuntime(AI_DRAFT_CONVERSATION_ID);
783
+ invalidateConversationHistory();
784
+ taskRuntimeRef.current = undefined;
785
+ setPendingTask(undefined);
786
+ setActiveTaskSet(getConfiguredTaskSet(username));
787
+ setConversationAttachments(AI_DRAFT_CONVERSATION_ID, []);
788
+ setConversationWorkContext(AI_DRAFT_CONVERSATION_ID, []);
789
+ dispatch({ type: "select-employee", username });
790
+ dispatch({ type: "start-new-conversation" });
791
+ requestComposerFocus();
792
+ },
793
+ selectModel: (model) => {
794
+ invalidatePendingInteraction();
795
+ dispatch({ type: "select-model", model });
796
+ },
797
+ startEditingMessage,
798
+ cancelEditingMessage,
799
+ saveUserPrompt: (prompt) =>
800
+ ai.updateEmployeeUserPrompt(currentEmployee.username, prompt),
801
+ triggerTask,
802
+ runTask,
803
+ focusComposer: requestComposerFocus,
804
+ }),
805
+ [
806
+ activeConversation,
807
+ ai,
808
+ activeChat,
809
+ composerFocusRequest,
810
+ conversationsLoading,
811
+ conversationSearch,
812
+ messageLoadingId,
813
+ historyError,
814
+ interactionError,
815
+ attachments,
816
+ uploadingAttachments,
817
+ workContext,
818
+ editingMessageId,
819
+ currentEmployee,
820
+ currentModel,
821
+ getConfiguredTaskSet,
822
+ invalidateConversationHistory,
823
+ invalidatePendingInteraction,
824
+ draft,
825
+ id,
826
+ removeConversation,
827
+ removeChatRuntime,
828
+ searchConversations,
829
+ renameConversation,
830
+ removeAttachment,
831
+ addWorkContext,
832
+ removeWorkContext,
833
+ setConversationAttachments,
834
+ setConversationWorkContext,
835
+ runTask,
836
+ retryMessage,
837
+ decideToolCall,
838
+ send,
839
+ setDraft,
840
+ uploadFiles,
841
+ startEditingMessage,
842
+ cancelEditingMessage,
843
+ loadConversationMessages,
844
+ startNewConversation,
845
+ triggerTask,
846
+ state.activeConversationId,
847
+ state.conversationListOpen,
848
+ state.conversations,
849
+ activeTaskSet,
850
+ ]
851
+ );
852
+
853
+ const messagesValue = useMemo<AIChatMessagesContextValue>(
854
+ () => ({ messages: chat.messages }),
855
+ [chat.messages]
856
+ );
857
+ const statusValue = useMemo<AIChatStatusContextValue>(
858
+ () => ({ status: chat.status, error: chat.error }),
859
+ [chat.error, chat.status]
860
+ );
861
+
862
+ return (
863
+ <AIChatContext.Provider value={value}>
864
+ <AIChatStatusContext.Provider value={statusValue}>
865
+ <AIChatMessagesContext.Provider value={messagesValue}>
866
+ {children}
867
+ </AIChatMessagesContext.Provider>
868
+ </AIChatStatusContext.Provider>
869
+ </AIChatContext.Provider>
870
+ );
871
+ }