@nocobase/portal-template-default 1.0.1 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (186) hide show
  1. package/README.MD +22 -3
  2. package/package.json +4 -2
  3. package/registry/nocobase-acl/components/role-options.ts +7 -0
  4. package/registry/nocobase-acl/components/role-switcher.tsx +9 -4
  5. package/registry/nocobase-acl/demo/boundary-api.tsx +87 -16
  6. package/registry/nocobase-acl/demo/components.tsx +32 -11
  7. package/registry/nocobase-acl/demo/index.tsx +45 -14
  8. package/registry/nocobase-acl/demo/lazy-route.tsx +2 -7
  9. package/registry/nocobase-acl/demo/prompt-generator.tsx +52 -33
  10. package/registry/nocobase-acl/demo/role-switcher-prompt-generator.tsx +28 -9
  11. package/registry/nocobase-acl/extension.tsx +1 -1
  12. package/registry/nocobase-acl/tests/acl-regression.mjs +7 -6
  13. package/registry/nocobase-ai/components/chat/chat-messages.tsx +3 -4
  14. package/registry/nocobase-ai/components/chat/conversation-list.tsx +2 -4
  15. package/registry/nocobase-ai/components/tools/business-report-dialog.tsx +16 -19
  16. package/registry/nocobase-ai/components/tools/chart-renderer.tsx +3 -14
  17. package/registry/nocobase-ai/components/tools/echarts-preview.tsx +2 -5
  18. package/registry/nocobase-ai/demo/configuration-gate.tsx +9 -12
  19. package/registry/nocobase-ai/extension.tsx +2 -7
  20. package/registry/nocobase-i18n/README.md +53 -0
  21. package/registry/nocobase-i18n/components/index.ts +1 -0
  22. package/registry/nocobase-i18n/components/language-switcher.tsx +138 -0
  23. package/registry/nocobase-i18n/demo/index.tsx +209 -0
  24. package/registry/nocobase-i18n/demo/prompt-generator.tsx +155 -0
  25. package/registry/nocobase-i18n/extension.tsx +46 -0
  26. package/registry/nocobase-i18n/i18n-provider.ts +9 -0
  27. package/registry/nocobase-i18n/index.ts +6 -0
  28. package/registry/nocobase-i18n/locale-store.ts +134 -0
  29. package/registry/nocobase-i18n/locales/en-US.ts +89 -0
  30. package/registry/nocobase-i18n/locales/zh-CN.ts +85 -0
  31. package/registry/nocobase-i18n/provider.tsx +32 -0
  32. package/registry/nocobase-i18n/runtime.ts +118 -0
  33. package/registry/nocobase-i18n/server-resources.ts +184 -0
  34. package/registry/nocobase-i18n/tests/i18n-regression.mjs +42 -0
  35. package/registry.config.json +34 -14
  36. package/scripts/registry.mjs +5 -0
  37. package/src/App.tsx +27 -86
  38. package/src/app/extension.ts +3 -1
  39. package/src/app/extensions.tsx +21 -0
  40. package/src/components/access-control/acl-gate.tsx +4 -10
  41. package/src/components/app-shell/breadcrumb.tsx +39 -3
  42. package/src/components/app-shell/document-title-handler.tsx +11 -20
  43. package/src/components/app-shell/header.tsx +18 -3
  44. package/src/components/app-shell/loading-overlay.tsx +2 -4
  45. package/src/components/app-shell/loading-state.tsx +15 -0
  46. package/src/components/app-shell/sidebar.tsx +75 -47
  47. package/src/components/data-table/data-table-pagination.tsx +27 -7
  48. package/src/components/data-table/data-table-styles.ts +57 -0
  49. package/src/components/data-table/data-table.tsx +14 -59
  50. package/src/components/resources/resource-label.ts +101 -0
  51. package/src/components/resources/views/create-view.tsx +17 -14
  52. package/src/components/resources/views/edit-view.tsx +17 -15
  53. package/src/components/resources/views/list-view.tsx +20 -10
  54. package/src/components/resources/views/show-view.tsx +20 -18
  55. package/src/extensions/nocobase-acl/README.md +19 -0
  56. package/src/extensions/nocobase-acl/components/acl-boundary.tsx +106 -0
  57. package/src/extensions/nocobase-acl/components/index.ts +3 -0
  58. package/src/extensions/nocobase-acl/components/role-options.ts +36 -0
  59. package/src/extensions/nocobase-acl/components/role-switcher.tsx +135 -0
  60. package/src/extensions/nocobase-acl/demo/boundary-api.tsx +186 -0
  61. package/src/extensions/nocobase-acl/demo/components.tsx +231 -0
  62. package/src/extensions/nocobase-acl/demo/index.tsx +505 -0
  63. package/src/extensions/nocobase-acl/demo/lazy-route.tsx +10 -0
  64. package/src/extensions/nocobase-acl/demo/prompt-generator.tsx +134 -0
  65. package/src/extensions/nocobase-acl/demo/role-switcher-prompt-generator.tsx +198 -0
  66. package/src/extensions/nocobase-acl/demo/scenario-section.tsx +37 -0
  67. package/src/extensions/nocobase-acl/extension.tsx +71 -0
  68. package/src/extensions/nocobase-acl/index.ts +1 -0
  69. package/src/extensions/nocobase-ai/README.md +70 -0
  70. package/src/extensions/nocobase-ai/adapters/react-hook-form.ts +20 -0
  71. package/src/extensions/nocobase-ai/assets/nocobase-ai-chat.svg +50 -0
  72. package/src/extensions/nocobase-ai/components/ai-root-provider.tsx +32 -0
  73. package/src/extensions/nocobase-ai/components/chat/ai-employee-avatar.tsx +31 -0
  74. package/src/extensions/nocobase-ai/components/chat/chat-attachment.tsx +92 -0
  75. package/src/extensions/nocobase-ai/components/chat/chat-compact.tsx +90 -0
  76. package/src/extensions/nocobase-ai/components/chat/chat-composer.tsx +429 -0
  77. package/src/extensions/nocobase-ai/components/chat/chat-empty-state.tsx +51 -0
  78. package/src/extensions/nocobase-ai/components/chat/chat-header.tsx +88 -0
  79. package/src/extensions/nocobase-ai/components/chat/chat-history-dialog.tsx +77 -0
  80. package/src/extensions/nocobase-ai/components/chat/chat-message.tsx +252 -0
  81. package/src/extensions/nocobase-ai/components/chat/chat-messages.tsx +173 -0
  82. package/src/extensions/nocobase-ai/components/chat/chat-window.tsx +118 -0
  83. package/src/extensions/nocobase-ai/components/chat/conversation-list.tsx +343 -0
  84. package/src/extensions/nocobase-ai/components/chat/markdown-message.tsx +165 -0
  85. package/src/extensions/nocobase-ai/components/chat/model-select-options.tsx +30 -0
  86. package/src/extensions/nocobase-ai/components/chat/reasoning-panel.tsx +29 -0
  87. package/src/extensions/nocobase-ai/components/chat/sub-agent-conversation.tsx +142 -0
  88. package/src/extensions/nocobase-ai/components/chat/tool-call-card.tsx +316 -0
  89. package/src/extensions/nocobase-ai/components/chat/user-prompt-editor.tsx +99 -0
  90. package/src/extensions/nocobase-ai/components/chat/work-context-chip.tsx +114 -0
  91. package/src/extensions/nocobase-ai/components/index.ts +59 -0
  92. package/src/extensions/nocobase-ai/components/page-elements/ai-form.tsx +44 -0
  93. package/src/extensions/nocobase-ai/components/page-elements/page-element-provider.tsx +469 -0
  94. package/src/extensions/nocobase-ai/components/surfaces/chat-dialog.tsx +31 -0
  95. package/src/extensions/nocobase-ai/components/surfaces/chat-inline.tsx +18 -0
  96. package/src/extensions/nocobase-ai/components/surfaces/chat-page.tsx +18 -0
  97. package/src/extensions/nocobase-ai/components/surfaces/chat-side-panel-layout.tsx +54 -0
  98. package/src/extensions/nocobase-ai/components/surfaces/chat-side-panel.tsx +74 -0
  99. package/src/extensions/nocobase-ai/components/surfaces/chat-surface-actions.tsx +54 -0
  100. package/src/extensions/nocobase-ai/components/tools/builtin-tool-renderers.tsx +31 -0
  101. package/src/extensions/nocobase-ai/components/tools/business-report-dialog.tsx +364 -0
  102. package/src/extensions/nocobase-ai/components/tools/business-report-renderer.tsx +122 -0
  103. package/src/extensions/nocobase-ai/components/tools/business-report-utils.tsx +282 -0
  104. package/src/extensions/nocobase-ai/components/tools/chart-renderer.tsx +99 -0
  105. package/src/extensions/nocobase-ai/components/tools/echarts-preview.tsx +59 -0
  106. package/src/extensions/nocobase-ai/components/tools/echarts-runtime-advanced.ts +18 -0
  107. package/src/extensions/nocobase-ai/components/tools/echarts-runtime-common.ts +20 -0
  108. package/src/extensions/nocobase-ai/components/tools/echarts-runtime-components.ts +34 -0
  109. package/src/extensions/nocobase-ai/components/tools/echarts-runtime-hierarchy.ts +24 -0
  110. package/src/extensions/nocobase-ai/components/tools/echarts-runtime.ts +138 -0
  111. package/src/extensions/nocobase-ai/components/tools/sub-agent-renderer.tsx +67 -0
  112. package/src/extensions/nocobase-ai/components/tools/suggestions-renderer.tsx +59 -0
  113. package/src/extensions/nocobase-ai/components/tools/tool-renderer-provider.tsx +55 -0
  114. package/src/extensions/nocobase-ai/components/tools/tool-renderer-utils.ts +18 -0
  115. package/src/extensions/nocobase-ai/components/tools/workflow-renderer.tsx +108 -0
  116. package/src/extensions/nocobase-ai/components/triggers/ai-chat-floating-trigger.tsx +73 -0
  117. package/src/extensions/nocobase-ai/components/triggers/ai-employee-shortcut.tsx +149 -0
  118. package/src/extensions/nocobase-ai/demo/configuration-gate.tsx +49 -0
  119. package/src/extensions/nocobase-ai/demo/container-showcase.tsx +210 -0
  120. package/src/extensions/nocobase-ai/demo/floating.tsx +134 -0
  121. package/src/extensions/nocobase-ai/demo/index.tsx +346 -0
  122. package/src/extensions/nocobase-ai/demo/interaction-showcase.tsx +151 -0
  123. package/src/extensions/nocobase-ai/demo/page-context-prompt-generator.tsx +355 -0
  124. package/src/extensions/nocobase-ai/demo/page-context.tsx +884 -0
  125. package/src/extensions/nocobase-ai/demo/page-element-showcase.tsx +197 -0
  126. package/src/extensions/nocobase-ai/demo/prompt-card.tsx +41 -0
  127. package/src/extensions/nocobase-ai/demo/prompt-generator.tsx +452 -0
  128. package/src/extensions/nocobase-ai/demo/shortcut.tsx +1207 -0
  129. package/src/extensions/nocobase-ai/demo/tool-cards.tsx +530 -0
  130. package/src/extensions/nocobase-ai/extension.tsx +158 -0
  131. package/src/extensions/nocobase-ai/global-ai-chat.tsx +213 -0
  132. package/src/extensions/nocobase-ai/index.ts +3 -0
  133. package/src/extensions/nocobase-ai/providers/ai-provider.tsx +323 -0
  134. package/src/extensions/nocobase-ai/providers/avatars.ts +817 -0
  135. package/src/extensions/nocobase-ai/providers/chat-context.tsx +109 -0
  136. package/src/extensions/nocobase-ai/providers/chat-controller.ts +84 -0
  137. package/src/extensions/nocobase-ai/providers/chat-message-utils.ts +198 -0
  138. package/src/extensions/nocobase-ai/providers/chat-provider.tsx +871 -0
  139. package/src/extensions/nocobase-ai/providers/chat-reducer.ts +147 -0
  140. package/src/extensions/nocobase-ai/providers/chat-task-utils.ts +105 -0
  141. package/src/extensions/nocobase-ai/providers/chat-transport.ts +184 -0
  142. package/src/extensions/nocobase-ai/providers/form-registry.tsx +271 -0
  143. package/src/extensions/nocobase-ai/providers/frontend-tool-registry.tsx +231 -0
  144. package/src/extensions/nocobase-ai/providers/index.ts +61 -0
  145. package/src/extensions/nocobase-ai/providers/model.ts +32 -0
  146. package/src/extensions/nocobase-ai/providers/page-context.tsx +136 -0
  147. package/src/extensions/nocobase-ai/providers/stream-coalescer.ts +60 -0
  148. package/src/extensions/nocobase-ai/providers/stream-event-utils.ts +82 -0
  149. package/src/extensions/nocobase-ai/providers/stream-parser.ts +61 -0
  150. package/src/extensions/nocobase-ai/providers/sub-agent-stream.ts +316 -0
  151. package/src/extensions/nocobase-ai/providers/types.ts +183 -0
  152. package/src/extensions/nocobase-ai/providers/ui-message-stream.ts +463 -0
  153. package/src/extensions/nocobase-ai/providers/use-automatic-tool-approval.ts +94 -0
  154. package/src/extensions/nocobase-ai/providers/use-chat-attachments.ts +206 -0
  155. package/src/extensions/nocobase-ai/providers/use-chat-message-actions.ts +554 -0
  156. package/src/extensions/nocobase-ai/providers/use-chat-runtime.ts +210 -0
  157. package/src/extensions/nocobase-ai/providers/use-chat-work-context.ts +93 -0
  158. package/src/extensions/nocobase-ai/providers/use-conversation-catalog.ts +108 -0
  159. package/src/extensions/nocobase-ai/providers/use-conversation-history.ts +178 -0
  160. package/src/extensions/nocobase-ai/services/index.ts +7 -0
  161. package/src/extensions/nocobase-ai/services/nocobase-ai-service.ts +454 -0
  162. package/src/extensions/nocobase-ai/services/types.ts +82 -0
  163. package/src/index.tsx +2 -3
  164. package/src/lib/i18n.ts +161 -0
  165. package/src/lib/nocobase/client.ts +19 -2
  166. package/src/locales/en-US.ts +82 -0
  167. package/src/locales/index.ts +13 -0
  168. package/src/locales/zh-CN.ts +79 -0
  169. package/src/pages/users/create.tsx +98 -0
  170. package/src/pages/users/edit.tsx +92 -0
  171. package/src/pages/users/form-context.ts +57 -0
  172. package/src/pages/users/form-fields.tsx +211 -0
  173. package/src/pages/users/list.tsx +205 -0
  174. package/src/pages/users/show.tsx +187 -0
  175. package/src/pages/users/types.ts +20 -0
  176. package/src/providers/constants.ts +1 -0
  177. package/src/pages/blog-posts/create.tsx +0 -177
  178. package/src/pages/blog-posts/edit.tsx +0 -197
  179. package/src/pages/blog-posts/list.tsx +0 -131
  180. package/src/pages/blog-posts/show.tsx +0 -81
  181. package/src/pages/categories/create.tsx +0 -73
  182. package/src/pages/categories/edit.tsx +0 -73
  183. package/src/pages/categories/index.ts +0 -4
  184. package/src/pages/categories/list.tsx +0 -86
  185. package/src/pages/categories/show.tsx +0 -37
  186. /package/src/pages/{blog-posts → users}/index.ts +0 -0
@@ -0,0 +1,210 @@
1
+ import { Chat } from "@ai-sdk/react";
2
+ import {
3
+ useCallback,
4
+ useRef,
5
+ type Dispatch,
6
+ type MutableRefObject,
7
+ } from "react";
8
+ import type { AIChatAction, AIChatState } from "./chat-reducer";
9
+ import { NocoBaseChatTransport } from "./chat-transport";
10
+ import { findAIModel, getAIModelKey } from "./model";
11
+ import type { useAI } from "./ai-provider";
12
+ import {
13
+ AI_DRAFT_CONVERSATION_ID,
14
+ type AIChatMessage,
15
+ type AIChatTaskRuntime,
16
+ } from "./types";
17
+
18
+ type AIContextValue = ReturnType<typeof useAI>;
19
+
20
+ export type AIConversationRuntimeContext = {
21
+ employeeUsername: string;
22
+ model: string;
23
+ task?: AIChatTaskRuntime;
24
+ };
25
+
26
+ export function useChatRuntime({
27
+ id,
28
+ ai,
29
+ stateRef,
30
+ taskRuntimeRef,
31
+ webSearchRef,
32
+ conversationFinishedHandlerRef,
33
+ moveAttachments,
34
+ moveWorkContext,
35
+ dispatch,
36
+ }: {
37
+ id: string;
38
+ ai: AIContextValue;
39
+ stateRef: MutableRefObject<AIChatState>;
40
+ taskRuntimeRef: MutableRefObject<AIChatTaskRuntime | undefined>;
41
+ webSearchRef: MutableRefObject<boolean>;
42
+ conversationFinishedHandlerRef: MutableRefObject<
43
+ | ((conversationId: string, chat: Chat<AIChatMessage>) => Promise<void>)
44
+ | undefined
45
+ >;
46
+ moveAttachments: (from: string, to: string) => void;
47
+ moveWorkContext: (from: string, to: string) => void;
48
+ dispatch: Dispatch<AIChatAction>;
49
+ }) {
50
+ const chatsRef = useRef(new Map<string, Chat<AIChatMessage>>());
51
+ const transportsRef = useRef(new Map<string, NocoBaseChatTransport>());
52
+ const runtimeContextsRef = useRef(
53
+ new Map<string, AIConversationRuntimeContext>()
54
+ );
55
+
56
+ const getRuntimeContext = useCallback(
57
+ (conversationId: string): AIConversationRuntimeContext => {
58
+ const existing = runtimeContextsRef.current.get(conversationId);
59
+ if (existing) return existing;
60
+
61
+ const latestState = stateRef.current;
62
+ const conversation = latestState.conversations.find(
63
+ (item) => item.id === conversationId
64
+ );
65
+ const conversationModel = conversation?.model
66
+ ? ai.models.find(
67
+ (item) =>
68
+ item.value === conversation.model?.model &&
69
+ (!conversation.model.llmService ||
70
+ item.llmService === conversation.model.llmService)
71
+ )
72
+ : undefined;
73
+ const context = {
74
+ employeeUsername:
75
+ conversation?.employeeUsername ??
76
+ latestState.selectedEmployeeUsername,
77
+ model: conversationModel
78
+ ? getAIModelKey(conversationModel)
79
+ : latestState.selectedModel,
80
+ task:
81
+ conversationId === AI_DRAFT_CONVERSATION_ID
82
+ ? taskRuntimeRef.current
83
+ : undefined,
84
+ };
85
+ runtimeContextsRef.current.set(conversationId, context);
86
+ return context;
87
+ },
88
+ [ai.models, stateRef, taskRuntimeRef]
89
+ );
90
+
91
+ const getChat = useCallback(
92
+ (conversationId: string) => {
93
+ const existing = chatsRef.current.get(conversationId);
94
+ if (existing) return existing;
95
+
96
+ let runtimeConversationId = conversationId;
97
+ const transport = ai.createTransport({
98
+ chatId: `${id}:${conversationId}`,
99
+ getContext: () => {
100
+ const runtimeContext = getRuntimeContext(runtimeConversationId);
101
+ const employee =
102
+ ai.employees.find(
103
+ (item) => item.username === runtimeContext.employeeUsername
104
+ ) ?? ai.employees[0];
105
+ const model =
106
+ findAIModel(ai.models, runtimeContext.model) ?? ai.models[0];
107
+ if (!employee || !model) {
108
+ throw new Error(
109
+ "AIProvider requires at least one employee and model"
110
+ );
111
+ }
112
+ const task = runtimeContext.task;
113
+ return {
114
+ sessionId:
115
+ runtimeConversationId === AI_DRAFT_CONVERSATION_ID
116
+ ? undefined
117
+ : runtimeConversationId,
118
+ employee,
119
+ model,
120
+ task: task
121
+ ? {
122
+ ...task,
123
+ webSearch: task.webSearch ?? webSearchRef.current,
124
+ }
125
+ : webSearchRef.current
126
+ ? { workContext: [], webSearch: true }
127
+ : undefined,
128
+ };
129
+ },
130
+ onSessionCreated: (sessionId) => {
131
+ const previousConversationId = runtimeConversationId;
132
+ chatsRef.current.delete(runtimeConversationId);
133
+ chatsRef.current.set(sessionId, chat);
134
+ if (transport instanceof NocoBaseChatTransport) {
135
+ transportsRef.current.delete(previousConversationId);
136
+ transportsRef.current.set(sessionId, transport);
137
+ }
138
+ const runtimeContext = runtimeContextsRef.current.get(
139
+ previousConversationId
140
+ );
141
+ if (runtimeContext) {
142
+ runtimeContextsRef.current.delete(previousConversationId);
143
+ runtimeContextsRef.current.set(sessionId, runtimeContext);
144
+ }
145
+ moveAttachments(previousConversationId, sessionId);
146
+ moveWorkContext(previousConversationId, sessionId);
147
+ dispatch({
148
+ type: "replace-conversation-id",
149
+ from: runtimeConversationId,
150
+ to: sessionId,
151
+ });
152
+ runtimeConversationId = sessionId;
153
+ },
154
+ });
155
+ const chat = new Chat<AIChatMessage>({
156
+ id: `${id}:${conversationId}`,
157
+ messages: [],
158
+ onFinish: () => {
159
+ const finishedConversationId = runtimeConversationId;
160
+ if (finishedConversationId === AI_DRAFT_CONVERSATION_ID) {
161
+ return;
162
+ }
163
+ queueMicrotask(() => {
164
+ void conversationFinishedHandlerRef.current?.(
165
+ finishedConversationId,
166
+ chat
167
+ );
168
+ });
169
+ },
170
+ transport,
171
+ });
172
+ chatsRef.current.set(conversationId, chat);
173
+ if (transport instanceof NocoBaseChatTransport) {
174
+ transportsRef.current.set(conversationId, transport);
175
+ }
176
+ return chat;
177
+ },
178
+ [
179
+ ai,
180
+ conversationFinishedHandlerRef,
181
+ dispatch,
182
+ getRuntimeContext,
183
+ id,
184
+ moveAttachments,
185
+ moveWorkContext,
186
+ webSearchRef,
187
+ ]
188
+ );
189
+
190
+ const getTransport = useCallback(
191
+ (conversationId: string) => transportsRef.current.get(conversationId),
192
+ []
193
+ );
194
+
195
+ const remove = useCallback((conversationId: string) => {
196
+ chatsRef.current.delete(conversationId);
197
+ transportsRef.current.delete(conversationId);
198
+ runtimeContextsRef.current.delete(conversationId);
199
+ }, []);
200
+
201
+ return {
202
+ chatsRef,
203
+ transportsRef,
204
+ runtimeContextsRef,
205
+ getRuntimeContext,
206
+ getChat,
207
+ getTransport,
208
+ remove,
209
+ };
210
+ }
@@ -0,0 +1,93 @@
1
+ import { useCallback, useRef, useState } from "react";
2
+ import type { AIWorkContextItem } from "./types";
3
+
4
+ const EMPTY_CONTEXT: AIWorkContextItem[] = [];
5
+
6
+ const contextItemKey = (item: AIWorkContextItem) =>
7
+ `${item.type}:${item.id ?? item.title ?? "context"}`;
8
+
9
+ export function useChatWorkContext(activeConversationId: string) {
10
+ const [drafts, setDrafts] = useState<Record<string, AIWorkContextItem[]>>({});
11
+ const draftsRef = useRef(drafts);
12
+ draftsRef.current = drafts;
13
+
14
+ const setConversationWorkContext = useCallback(
15
+ (
16
+ conversationId: string,
17
+ value:
18
+ | AIWorkContextItem[]
19
+ | ((current: AIWorkContextItem[]) => AIWorkContextItem[])
20
+ ) => {
21
+ setDrafts((current) => ({
22
+ ...current,
23
+ [conversationId]:
24
+ typeof value === "function"
25
+ ? value(current[conversationId] ?? [])
26
+ : value,
27
+ }));
28
+ },
29
+ []
30
+ );
31
+
32
+ const addWorkContext = useCallback(
33
+ (item: AIWorkContextItem) => {
34
+ setConversationWorkContext(activeConversationId, (current) => {
35
+ const key = contextItemKey(item);
36
+ return [
37
+ ...current.filter((entry) => contextItemKey(entry) !== key),
38
+ item,
39
+ ];
40
+ });
41
+ },
42
+ [activeConversationId, setConversationWorkContext]
43
+ );
44
+
45
+ const removeWorkContext = useCallback(
46
+ (item: AIWorkContextItem) => {
47
+ const key = contextItemKey(item);
48
+ setConversationWorkContext(activeConversationId, (current) =>
49
+ current.filter((entry) => contextItemKey(entry) !== key)
50
+ );
51
+ },
52
+ [activeConversationId, setConversationWorkContext]
53
+ );
54
+
55
+ const moveWorkContext = useCallback((from: string, to: string) => {
56
+ setDrafts((current) => {
57
+ if (!current[from]) return current;
58
+ const next = { ...current, [to]: current[from] };
59
+ delete next[from];
60
+ return next;
61
+ });
62
+ }, []);
63
+
64
+ const removeConversationWorkContext = useCallback(
65
+ (conversationId: string) => {
66
+ setDrafts((current) => {
67
+ if (!current[conversationId]) return current;
68
+ const next = { ...current };
69
+ delete next[conversationId];
70
+ return next;
71
+ });
72
+ },
73
+ []
74
+ );
75
+
76
+ const clearWorkContext = useCallback(() => setDrafts({}), []);
77
+ const getConversationWorkContext = useCallback(
78
+ (conversationId: string) => draftsRef.current[conversationId] ?? [],
79
+ []
80
+ );
81
+ const workContext = drafts[activeConversationId] ?? EMPTY_CONTEXT;
82
+
83
+ return {
84
+ workContext,
85
+ addWorkContext,
86
+ removeWorkContext,
87
+ setConversationWorkContext,
88
+ moveWorkContext,
89
+ removeConversationWorkContext,
90
+ clearWorkContext,
91
+ getConversationWorkContext,
92
+ };
93
+ }
@@ -0,0 +1,108 @@
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
+ import type { AIConfigurationStatus, AIConversation } from "./types";
3
+
4
+ export function useConversationCatalog({
5
+ configurationStatus,
6
+ listConversations,
7
+ onChange,
8
+ onError,
9
+ }: {
10
+ configurationStatus: AIConfigurationStatus;
11
+ listConversations: (keyword?: string) => Promise<AIConversation[]>;
12
+ onChange: (conversations: AIConversation[]) => void;
13
+ onError: (error?: Error) => void;
14
+ }) {
15
+ const [loading, setLoading] = useState(false);
16
+ const [search, setSearch] = useState("");
17
+ const searchRef = useRef("");
18
+ const requestRef = useRef(0);
19
+ const catalogRef = useRef<AIConversation[]>([]);
20
+
21
+ const apply = useCallback(
22
+ (conversations: AIConversation[], keyword = searchRef.current) => {
23
+ if (!keyword) catalogRef.current = conversations;
24
+ onChange(conversations);
25
+ },
26
+ [onChange]
27
+ );
28
+
29
+ const refresh = useCallback(async () => {
30
+ const requestId = requestRef.current + 1;
31
+ requestRef.current = requestId;
32
+ const keyword = searchRef.current;
33
+ const conversations = await listConversations(keyword);
34
+ if (requestRef.current === requestId) apply(conversations, keyword);
35
+ return conversations;
36
+ }, [apply, listConversations]);
37
+
38
+ useEffect(() => {
39
+ if (configurationStatus !== "ready") return;
40
+ let active = true;
41
+ setLoading(true);
42
+ onError(undefined);
43
+ void listConversations()
44
+ .then((conversations) => {
45
+ if (!active || searchRef.current) return;
46
+ apply(conversations, "");
47
+ })
48
+ .catch((error: unknown) => {
49
+ if (!active) return;
50
+ onError(
51
+ error instanceof Error
52
+ ? error
53
+ : new Error("Unable to load conversations")
54
+ );
55
+ })
56
+ .finally(() => {
57
+ if (active) setLoading(false);
58
+ });
59
+ return () => {
60
+ active = false;
61
+ };
62
+ }, [apply, configurationStatus, listConversations, onError]);
63
+
64
+ const searchConversations = useCallback(
65
+ async (keyword: string) => {
66
+ const normalizedKeyword = keyword.trim();
67
+ setSearch(keyword);
68
+ searchRef.current = normalizedKeyword;
69
+
70
+ const requestId = requestRef.current + 1;
71
+ requestRef.current = requestId;
72
+ setLoading(true);
73
+ onError(undefined);
74
+ try {
75
+ const conversations = await listConversations(normalizedKeyword);
76
+ if (requestRef.current !== requestId) return;
77
+ apply(conversations, normalizedKeyword);
78
+ } catch (error) {
79
+ if (requestRef.current !== requestId) return;
80
+ const resolvedError =
81
+ error instanceof Error
82
+ ? error
83
+ : new Error("Unable to search conversations");
84
+ onError(resolvedError);
85
+ throw resolvedError;
86
+ } finally {
87
+ if (requestRef.current === requestId) setLoading(false);
88
+ }
89
+ },
90
+ [apply, listConversations, onError]
91
+ );
92
+
93
+ const updateCatalog = useCallback(
94
+ (updater: (conversations: AIConversation[]) => AIConversation[]) => {
95
+ catalogRef.current = updater(catalogRef.current);
96
+ },
97
+ []
98
+ );
99
+
100
+ return {
101
+ loading,
102
+ search,
103
+ searchRef,
104
+ refresh,
105
+ searchConversations,
106
+ updateCatalog,
107
+ };
108
+ }
@@ -0,0 +1,178 @@
1
+ import type { Chat } from "@ai-sdk/react";
2
+ import { useCallback, useEffect, useRef, useState } from "react";
3
+ import type { NocoBaseChatTransport } from "./chat-transport";
4
+ import { reconcileRefreshedMessages } from "./chat-message-utils";
5
+ import { AI_DRAFT_CONVERSATION_ID, type AIChatMessage } from "./types";
6
+
7
+ const isChatRunning = (chat: Chat<AIChatMessage>) =>
8
+ chat.status === "streaming" || chat.status === "submitted";
9
+
10
+ export function useConversationHistory({
11
+ chatSurfaceOpen,
12
+ activeConversationId,
13
+ getActiveConversationId,
14
+ getChat,
15
+ getTransport,
16
+ getConversationMessages,
17
+ getConversationActiveState,
18
+ onMarkRead,
19
+ onError,
20
+ }: {
21
+ chatSurfaceOpen: boolean;
22
+ activeConversationId: string;
23
+ getActiveConversationId: () => string;
24
+ getChat: (conversationId: string) => Chat<AIChatMessage>;
25
+ getTransport: (conversationId: string) => NocoBaseChatTransport | undefined;
26
+ getConversationMessages: (
27
+ conversationId: string,
28
+ options?: { updateRead?: boolean }
29
+ ) => Promise<AIChatMessage[]>;
30
+ getConversationActiveState: (
31
+ conversationId: string
32
+ ) => Promise<string | undefined>;
33
+ onMarkRead: (conversationId: string) => void;
34
+ onError: (error?: Error) => void;
35
+ }) {
36
+ const surfaceOpenRef = useRef(chatSurfaceOpen);
37
+ const openVersionRef = useRef(0);
38
+ const pendingRequestsRef = useRef(
39
+ new Map<string, Promise<AIChatMessage[]>>()
40
+ );
41
+ const [loadingId, setLoadingId] = useState<string>();
42
+ surfaceOpenRef.current = chatSurfaceOpen;
43
+
44
+ const refresh = useCallback(
45
+ (
46
+ conversationId: string,
47
+ targetChat: Chat<AIChatMessage>,
48
+ options: { updateRead?: boolean } = {}
49
+ ) => {
50
+ const updateRead = options.updateRead === true;
51
+ const requestKey = `${conversationId}:${updateRead ? "read" : "peek"}`;
52
+ const pending = pendingRequestsRef.current.get(requestKey);
53
+ if (pending) return pending;
54
+
55
+ const request = getConversationMessages(conversationId, { updateRead })
56
+ .then((messages) => {
57
+ const reconciled = reconcileRefreshedMessages(
58
+ targetChat.messages,
59
+ messages
60
+ );
61
+ targetChat.messages = reconciled;
62
+ return reconciled;
63
+ })
64
+ .finally(() => pendingRequestsRef.current.delete(requestKey));
65
+ pendingRequestsRef.current.set(requestKey, request);
66
+ return request;
67
+ },
68
+ [getConversationMessages]
69
+ );
70
+
71
+ const load = useCallback(
72
+ async (conversationId: string) => {
73
+ const targetChat = getChat(conversationId);
74
+ if (isChatRunning(targetChat)) return;
75
+
76
+ const openVersion = openVersionRef.current + 1;
77
+ openVersionRef.current = openVersion;
78
+ setLoadingId(conversationId);
79
+ onError(undefined);
80
+ try {
81
+ await refresh(conversationId, targetChat, { updateRead: true });
82
+ onMarkRead(conversationId);
83
+ } catch (error) {
84
+ onError(
85
+ error instanceof Error
86
+ ? error
87
+ : new Error("Unable to load conversation messages")
88
+ );
89
+ return;
90
+ } finally {
91
+ setLoadingId((current) =>
92
+ current === conversationId ? undefined : current
93
+ );
94
+ }
95
+
96
+ if (
97
+ openVersionRef.current !== openVersion ||
98
+ getActiveConversationId() !== conversationId ||
99
+ isChatRunning(targetChat)
100
+ ) {
101
+ return;
102
+ }
103
+
104
+ try {
105
+ const activeState = await getConversationActiveState(conversationId);
106
+ if (
107
+ openVersionRef.current !== openVersion ||
108
+ getActiveConversationId() !== conversationId ||
109
+ isChatRunning(targetChat) ||
110
+ activeState !== "streaming"
111
+ ) {
112
+ return;
113
+ }
114
+ getTransport(conversationId)?.prepareConversationResume(
115
+ targetChat.messages
116
+ );
117
+ await targetChat.resumeStream();
118
+ } catch (error) {
119
+ onError(
120
+ error instanceof Error
121
+ ? error
122
+ : new Error("Unable to resume the conversation stream")
123
+ );
124
+ }
125
+ },
126
+ [
127
+ getActiveConversationId,
128
+ getChat,
129
+ getConversationActiveState,
130
+ getTransport,
131
+ onError,
132
+ onMarkRead,
133
+ refresh,
134
+ ]
135
+ );
136
+
137
+ useEffect(() => {
138
+ if (
139
+ !chatSurfaceOpen ||
140
+ activeConversationId === AI_DRAFT_CONVERSATION_ID
141
+ ) {
142
+ return;
143
+ }
144
+ const targetChat = getChat(activeConversationId);
145
+ if (isChatRunning(targetChat)) return;
146
+
147
+ void refresh(activeConversationId, targetChat, { updateRead: true })
148
+ .then(() => {
149
+ if (
150
+ surfaceOpenRef.current &&
151
+ getActiveConversationId() === activeConversationId
152
+ ) {
153
+ onMarkRead(activeConversationId);
154
+ }
155
+ })
156
+ .catch((error: unknown) => {
157
+ onError(
158
+ error instanceof Error
159
+ ? error
160
+ : new Error("Unable to mark the conversation as read")
161
+ );
162
+ });
163
+ }, [
164
+ activeConversationId,
165
+ chatSurfaceOpen,
166
+ getActiveConversationId,
167
+ getChat,
168
+ onError,
169
+ onMarkRead,
170
+ refresh,
171
+ ]);
172
+
173
+ const invalidate = useCallback(() => {
174
+ openVersionRef.current += 1;
175
+ }, []);
176
+
177
+ return { invalidate, load, loadingId, refresh };
178
+ }
@@ -0,0 +1,7 @@
1
+ export { NocoBaseAIService, nocobaseAIService } from "./nocobase-ai-service";
2
+ export type {
3
+ AIService,
4
+ CreateAIConversationOptions,
5
+ UpdateToolCallDecisionOptions,
6
+ UpdatedToolCall,
7
+ } from "./types";