@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,871 +0,0 @@
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
- }