@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,59 @@
1
+ export {
2
+ NocoBaseAIRootProvider,
3
+ type NocoBaseAIRootProviderProps,
4
+ } from "./ai-root-provider";
5
+ export { AIChatWindow, type AIChatWindowProps } from "./chat/chat-window";
6
+ export { ChatComposer, type AIChatComposerAction } from "./chat/chat-composer";
7
+ export { AIChatCompact, type AIChatCompactProps } from "./chat/chat-compact";
8
+ export { AIChatHistoryDialog } from "./chat/chat-history-dialog";
9
+ export {
10
+ AIChatMessageList,
11
+ ChatMessages,
12
+ type AIChatMessageListProps,
13
+ } from "./chat/chat-messages";
14
+ export { AIModelSelectOptions } from "./chat/model-select-options";
15
+ export { ChatDialog } from "./surfaces/chat-dialog";
16
+ export { ChatSurfaceActions } from "./surfaces/chat-surface-actions";
17
+ export { ChatInline } from "./surfaces/chat-inline";
18
+ export { ChatPage } from "./surfaces/chat-page";
19
+ export {
20
+ ChatSidePanel,
21
+ type ChatSidePanelProps,
22
+ } from "./surfaces/chat-side-panel";
23
+ export {
24
+ ChatSidePanelLayout,
25
+ type ChatSidePanelLayoutProps,
26
+ } from "./surfaces/chat-side-panel-layout";
27
+ export {
28
+ AIChatFloatingTrigger,
29
+ type AIChatFloatingTriggerProps,
30
+ } from "./triggers/ai-chat-floating-trigger";
31
+ export {
32
+ AIEmployeeShortcut,
33
+ type AIEmployeeShortcutProps,
34
+ } from "./triggers/ai-employee-shortcut";
35
+ export {
36
+ AIPageElementProvider,
37
+ useAIPageElement,
38
+ useAIPageElementHandle,
39
+ useAIPageElementPicker,
40
+ AIPageContextResolutionError,
41
+ type AIPageContextFailurePolicy,
42
+ type AIPageElementDescriptor,
43
+ type AIPageElementHandle,
44
+ type AIPageElementPickerOptions,
45
+ type AIPageElementProviderProps,
46
+ } from "./page-elements/page-element-provider";
47
+ export {
48
+ useAIForm,
49
+ type AIFormDescriptor,
50
+ } from "./page-elements/ai-form";
51
+ export {
52
+ AIToolRendererProvider,
53
+ useAIToolRenderer,
54
+ type AIToolRenderer,
55
+ type AIToolRendererDefinition,
56
+ type AIToolRendererEntry,
57
+ type AIToolRendererMap,
58
+ type AIToolRendererProps,
59
+ } from "./tools/tool-renderer-provider";
@@ -0,0 +1,44 @@
1
+ import { useEffect, useRef, type RefCallback } from "react";
2
+ import { useAIFormRegistry, type AIFormField } from "../../providers";
3
+ import { useAIPageElement } from "./page-element-provider";
4
+
5
+ export type AIFormDescriptor = {
6
+ id: string;
7
+ title: string;
8
+ fields: AIFormField[];
9
+ getValues: () => unknown | Promise<unknown>;
10
+ setValues: (values: Record<string, unknown>) => void | Promise<void>;
11
+ };
12
+
13
+ export function useAIForm(
14
+ descriptor: AIFormDescriptor
15
+ ): RefCallback<HTMLElement> {
16
+ const registry = useAIFormRegistry();
17
+ const descriptorRef = useRef(descriptor);
18
+ descriptorRef.current = descriptor;
19
+
20
+ useEffect(
21
+ () =>
22
+ registry.register({
23
+ id: descriptor.id,
24
+ title: descriptor.title,
25
+ fields: descriptor.fields,
26
+ getValues: () => descriptorRef.current.getValues(),
27
+ setValues: (values) => descriptorRef.current.setValues(values),
28
+ }),
29
+ [descriptor.fields, descriptor.id, descriptor.title, registry]
30
+ );
31
+
32
+ return useAIPageElement({
33
+ id: descriptor.id,
34
+ title: descriptor.title,
35
+ kind: "form",
36
+ getContext: async () => ({
37
+ form: descriptor.id,
38
+ title: descriptor.title,
39
+ fields: descriptorRef.current.fields,
40
+ value: await descriptorRef.current.getValues(),
41
+ instructions: `Use the formFiller tool with form "${descriptor.id}" to fill this form. The tool only changes visible field values and does not submit the form.`,
42
+ }),
43
+ });
44
+ }
@@ -0,0 +1,469 @@
1
+ import { Button } from "@/components/ui/button";
2
+ import { cn } from "@/lib/utils";
3
+ import { MousePointer2, X } from "lucide-react";
4
+ import {
5
+ createContext,
6
+ useCallback,
7
+ useContext,
8
+ useEffect,
9
+ useId,
10
+ useMemo,
11
+ useRef,
12
+ useState,
13
+ type PropsWithChildren,
14
+ type RefCallback,
15
+ } from "react";
16
+ import { createPortal } from "react-dom";
17
+ import {
18
+ AIPageContextResolverProvider,
19
+ createAIPageContextReference,
20
+ useOptionalAIFrontendToolRegistry,
21
+ type AIFrontendToolManifest,
22
+ type AIFrontendToolRegistration,
23
+ type AIWorkContextItem,
24
+ } from "../../providers";
25
+
26
+ const PAGE_ELEMENT_ATTRIBUTE = "data-ai-page-element";
27
+
28
+ export type AIPageElementDescriptor = {
29
+ id?: string;
30
+ title: string;
31
+ kind?: string;
32
+ getContext: () => unknown | Promise<unknown>;
33
+ tools?: AIFrontendToolRegistration[];
34
+ };
35
+
36
+ type AIPageElementRuntimeDescriptor = AIPageElementDescriptor & {
37
+ frontendTools?: AIFrontendToolManifest[];
38
+ };
39
+
40
+ export type AIPageElementPickerOptions = {
41
+ chatId?: string;
42
+ onSelect: (item: AIWorkContextItem) => void;
43
+ onCancel?: () => void;
44
+ };
45
+
46
+ export type AIPageContextFailurePolicy = "throw" | "omit";
47
+
48
+ export type AIPageElementProviderProps = PropsWithChildren<{
49
+ contextFailurePolicy?: AIPageContextFailurePolicy;
50
+ }>;
51
+
52
+ export class AIPageContextResolutionError extends Error {
53
+ constructor(
54
+ message: string,
55
+ readonly failures: Array<{ item: AIWorkContextItem; reason: unknown }>
56
+ ) {
57
+ super(message);
58
+ this.name = "AIPageContextResolutionError";
59
+ }
60
+ }
61
+
62
+ type RegisteredPageElement = {
63
+ element: HTMLElement;
64
+ getDescriptor: () => AIPageElementRuntimeDescriptor;
65
+ };
66
+
67
+ type PickerRequest = AIPageElementPickerOptions & {
68
+ token: symbol;
69
+ resolving: boolean;
70
+ error?: string;
71
+ };
72
+
73
+ type AIPageElementContextValue = {
74
+ picking: boolean;
75
+ registeredCount: number;
76
+ register: (
77
+ runtimeId: string,
78
+ element: HTMLElement,
79
+ getDescriptor: () => AIPageElementRuntimeDescriptor
80
+ ) => () => void;
81
+ startPicking: (options: AIPageElementPickerOptions) => void;
82
+ cancelPicking: () => void;
83
+ };
84
+
85
+ const AIPageElementContext = createContext<AIPageElementContextValue | null>(
86
+ null
87
+ );
88
+
89
+ const findRegisteredElement = (
90
+ target: EventTarget | null,
91
+ registry: Map<string, RegisteredPageElement>
92
+ ) => {
93
+ if (!(target instanceof Element)) return undefined;
94
+ const element = target.closest<HTMLElement>(`[${PAGE_ELEMENT_ATTRIBUTE}]`);
95
+ if (!element) return undefined;
96
+ const runtimeId = element.getAttribute(PAGE_ELEMENT_ATTRIBUTE);
97
+ if (!runtimeId) return undefined;
98
+ const registered = registry.get(runtimeId);
99
+ return registered ? { runtimeId, registered } : undefined;
100
+ };
101
+
102
+ export function AIPageElementProvider({
103
+ children,
104
+ contextFailurePolicy = "throw",
105
+ }: AIPageElementProviderProps) {
106
+ const registryRef = useRef(new Map<string, RegisteredPageElement>());
107
+ const [registeredCount, setRegisteredCount] = useState(0);
108
+ const [request, setRequest] = useState<PickerRequest>();
109
+ const [hoveredId, setHoveredId] = useState<string>();
110
+ const [hoveredRect, setHoveredRect] = useState<DOMRect>();
111
+ const picking = Boolean(request);
112
+ const requestRef = useRef(request);
113
+ const hoveredIdRef = useRef(hoveredId);
114
+ requestRef.current = request;
115
+ hoveredIdRef.current = hoveredId;
116
+
117
+ const register = useCallback<AIPageElementContextValue["register"]>(
118
+ (runtimeId, element, getDescriptor) => {
119
+ const contextId = getDescriptor().id ?? runtimeId;
120
+ const duplicate = [...registryRef.current.entries()].find(
121
+ ([registeredRuntimeId, entry]) =>
122
+ registeredRuntimeId !== runtimeId &&
123
+ (entry.getDescriptor().id ?? registeredRuntimeId) === contextId
124
+ );
125
+ if (duplicate) {
126
+ throw new Error(
127
+ `AI page context id "${contextId}" is already registered`
128
+ );
129
+ }
130
+ element.setAttribute(PAGE_ELEMENT_ATTRIBUTE, runtimeId);
131
+ registryRef.current.set(runtimeId, { element, getDescriptor });
132
+ setRegisteredCount(registryRef.current.size);
133
+ return () => {
134
+ if (element.getAttribute(PAGE_ELEMENT_ATTRIBUTE) === runtimeId) {
135
+ element.removeAttribute(PAGE_ELEMENT_ATTRIBUTE);
136
+ }
137
+ if (registryRef.current.get(runtimeId)?.element === element) {
138
+ registryRef.current.delete(runtimeId);
139
+ }
140
+ if (hoveredIdRef.current === runtimeId) {
141
+ hoveredIdRef.current = undefined;
142
+ setHoveredId(undefined);
143
+ setHoveredRect(undefined);
144
+ }
145
+ setRegisteredCount(registryRef.current.size);
146
+ };
147
+ },
148
+ []
149
+ );
150
+
151
+ const cancelPicking = useCallback(() => {
152
+ const current = requestRef.current;
153
+ requestRef.current = undefined;
154
+ setRequest(undefined);
155
+ setHoveredId(undefined);
156
+ setHoveredRect(undefined);
157
+ current?.onCancel?.();
158
+ }, []);
159
+
160
+ const startPicking = useCallback((options: AIPageElementPickerOptions) => {
161
+ const current = requestRef.current;
162
+ const nextRequest: PickerRequest = {
163
+ ...options,
164
+ token: Symbol("page-element-picker"),
165
+ resolving: false,
166
+ };
167
+ requestRef.current = nextRequest;
168
+ setRequest(nextRequest);
169
+ setHoveredId(undefined);
170
+ setHoveredRect(undefined);
171
+ current?.onCancel?.();
172
+ }, []);
173
+
174
+ const resolvePageContext = useCallback(
175
+ async (items: AIWorkContextItem[]) => {
176
+ const resolved = await Promise.allSettled(
177
+ items.map(async (item) => {
178
+ if (item.type !== "page-element") return item;
179
+ const registeredEntry = [...registryRef.current.entries()].find(
180
+ ([runtimeId, entry]) =>
181
+ (entry.getDescriptor().id ?? runtimeId) === item.id
182
+ );
183
+ if (!registeredEntry) {
184
+ if (item.content !== undefined) return item;
185
+ throw new Error(
186
+ `Page context "${item.title ?? item.id ?? "unknown"}" is not mounted`
187
+ );
188
+ }
189
+ const [runtimeId, entry] = registeredEntry;
190
+ const descriptor = entry.getDescriptor();
191
+ const contextId = item.id ?? descriptor.id ?? runtimeId;
192
+ const frontendTools = descriptor.frontendTools ?? [];
193
+ return {
194
+ ...item,
195
+ id: contextId,
196
+ title: item.title ?? descriptor.title,
197
+ kind: item.kind ?? descriptor.kind,
198
+ content: await descriptor.getContext(),
199
+ ...(frontendTools.length ? { uid: contextId, frontendTools } : {}),
200
+ } satisfies AIWorkContextItem;
201
+ })
202
+ );
203
+ const failures = resolved.flatMap((result, index) =>
204
+ result.status === "rejected"
205
+ ? [{ item: items[index], reason: result.reason }]
206
+ : []
207
+ );
208
+ if (failures.length && contextFailurePolicy === "throw") {
209
+ const labels = failures.map(({ item, reason }) => {
210
+ const label = item.title ?? item.id ?? "unknown page context";
211
+ return reason instanceof Error
212
+ ? `${label} (${reason.message})`
213
+ : label;
214
+ });
215
+ throw new AIPageContextResolutionError(
216
+ `Unable to read page context: ${labels.join(", ")}`,
217
+ failures
218
+ );
219
+ }
220
+ return resolved.flatMap((result) =>
221
+ result.status === "fulfilled" ? [result.value] : []
222
+ );
223
+ },
224
+ [contextFailurePolicy]
225
+ );
226
+
227
+ useEffect(() => {
228
+ if (!picking) return;
229
+
230
+ const updateHoveredElement = (event: PointerEvent) => {
231
+ const match = findRegisteredElement(event.target, registryRef.current);
232
+ setHoveredId(match?.runtimeId);
233
+ setHoveredRect(match?.registered.element.getBoundingClientRect());
234
+ };
235
+ const clearHoveredElement = () => {
236
+ setHoveredId(undefined);
237
+ setHoveredRect(undefined);
238
+ };
239
+ const updateHoveredRect = () => {
240
+ const currentHoveredId = hoveredIdRef.current;
241
+ if (!currentHoveredId) return;
242
+ const entry = registryRef.current.get(currentHoveredId);
243
+ setHoveredRect(entry?.element.getBoundingClientRect());
244
+ };
245
+ const handleKeyDown = (event: KeyboardEvent) => {
246
+ if (event.key === "Escape") cancelPicking();
247
+ };
248
+ const handleClick = async (event: MouseEvent) => {
249
+ const match = findRegisteredElement(event.target, registryRef.current);
250
+ const currentRequest = requestRef.current;
251
+ if (!match || !currentRequest || currentRequest.resolving) return;
252
+ event.preventDefault();
253
+ event.stopPropagation();
254
+ const descriptor = match.registered.getDescriptor();
255
+ const resolvingRequest = {
256
+ ...currentRequest,
257
+ resolving: true,
258
+ error: undefined,
259
+ };
260
+ requestRef.current = resolvingRequest;
261
+ setRequest(resolvingRequest);
262
+ try {
263
+ const content = await descriptor.getContext();
264
+ if (requestRef.current?.token !== currentRequest.token) return;
265
+ const contextId = descriptor.id ?? match.runtimeId;
266
+ const frontendTools = descriptor.frontendTools ?? [];
267
+ currentRequest.onSelect({
268
+ type: "page-element",
269
+ id: contextId,
270
+ title: descriptor.title,
271
+ kind: descriptor.kind,
272
+ content,
273
+ ...(frontendTools.length ? { uid: contextId, frontendTools } : {}),
274
+ });
275
+ requestRef.current = undefined;
276
+ setRequest(undefined);
277
+ clearHoveredElement();
278
+ } catch (error) {
279
+ if (requestRef.current?.token !== currentRequest.token) return;
280
+ const failedRequest = {
281
+ ...currentRequest,
282
+ resolving: false,
283
+ error:
284
+ error instanceof Error
285
+ ? error.message
286
+ : "Unable to read this page element",
287
+ };
288
+ requestRef.current = failedRequest;
289
+ setRequest(failedRequest);
290
+ }
291
+ };
292
+
293
+ document.addEventListener("pointermove", updateHoveredElement, true);
294
+ document.addEventListener("pointerleave", clearHoveredElement, true);
295
+ document.addEventListener("click", handleClick, true);
296
+ document.addEventListener("keydown", handleKeyDown, true);
297
+ window.addEventListener("scroll", updateHoveredRect, true);
298
+ window.addEventListener("resize", updateHoveredRect);
299
+ return () => {
300
+ document.removeEventListener("pointermove", updateHoveredElement, true);
301
+ document.removeEventListener("pointerleave", clearHoveredElement, true);
302
+ document.removeEventListener("click", handleClick, true);
303
+ document.removeEventListener("keydown", handleKeyDown, true);
304
+ window.removeEventListener("scroll", updateHoveredRect, true);
305
+ window.removeEventListener("resize", updateHoveredRect);
306
+ };
307
+ }, [cancelPicking, picking]);
308
+
309
+ const value = useMemo<AIPageElementContextValue>(
310
+ () => ({
311
+ picking,
312
+ registeredCount,
313
+ register,
314
+ startPicking,
315
+ cancelPicking,
316
+ }),
317
+ [cancelPicking, picking, register, registeredCount, startPicking]
318
+ );
319
+
320
+ return (
321
+ <AIPageContextResolverProvider resolve={resolvePageContext}>
322
+ <AIPageElementContext.Provider value={value}>
323
+ {children}
324
+ {request && typeof document !== "undefined"
325
+ ? createPortal(
326
+ <>
327
+ {hoveredRect ? (
328
+ <div
329
+ className="pointer-events-none fixed z-[2000] rounded-lg border-2 border-foreground bg-foreground/5 shadow-[0_0_0_9999px_rgba(0,0,0,0.08)]"
330
+ style={{
331
+ left: hoveredRect.left,
332
+ top: hoveredRect.top,
333
+ width: hoveredRect.width,
334
+ height: hoveredRect.height,
335
+ }}
336
+ >
337
+ <div className="absolute -top-7 left-0 max-w-[min(320px,80vw)] truncate rounded-md bg-foreground px-2 py-1 text-xs font-medium text-background shadow-sm">
338
+ {hoveredId
339
+ ? registryRef.current.get(hoveredId)?.getDescriptor()
340
+ .title
341
+ : null}
342
+ </div>
343
+ </div>
344
+ ) : null}
345
+ <div className="fixed bottom-6 left-1/2 z-[2001] flex max-w-[calc(100vw-2rem)] -translate-x-1/2 items-center gap-3 rounded-xl border bg-background px-3 py-2 shadow-xl">
346
+ <MousePointer2
347
+ className={cn(
348
+ "size-4 shrink-0",
349
+ request.resolving && "animate-pulse"
350
+ )}
351
+ />
352
+ <div className="min-w-0">
353
+ <div className="truncate text-sm font-medium">
354
+ {request.resolving
355
+ ? "Reading page element…"
356
+ : "Pick a page element"}
357
+ </div>
358
+ <div
359
+ className={cn(
360
+ "truncate text-xs text-muted-foreground",
361
+ request.error && "text-destructive"
362
+ )}
363
+ >
364
+ {request.error ??
365
+ "Hover a highlighted element, then click to add it."}
366
+ </div>
367
+ </div>
368
+ <Button
369
+ variant="ghost"
370
+ size="icon-sm"
371
+ aria-label="Cancel picking page element"
372
+ disabled={request.resolving}
373
+ onClick={cancelPicking}
374
+ >
375
+ <X />
376
+ </Button>
377
+ </div>
378
+ </>,
379
+ document.body
380
+ )
381
+ : null}
382
+ </AIPageElementContext.Provider>
383
+ </AIPageContextResolverProvider>
384
+ );
385
+ }
386
+
387
+ export function useAIPageElementPicker() {
388
+ const value = useContext(AIPageElementContext);
389
+ if (!value) {
390
+ throw new Error(
391
+ "useAIPageElementPicker must be used inside AIPageElementProvider"
392
+ );
393
+ }
394
+ return value;
395
+ }
396
+
397
+ export function useAIPageElement(
398
+ descriptor: AIPageElementDescriptor
399
+ ): RefCallback<HTMLElement> {
400
+ const { register } = useAIPageElementPicker();
401
+ const frontendTools = useOptionalAIFrontendToolRegistry();
402
+ const reactId = useId();
403
+ const descriptorRef = useRef(descriptor);
404
+ descriptorRef.current = descriptor;
405
+ const runtimeIdRef = useRef(`page-element-${reactId.replace(/:/g, "")}`);
406
+ const toolManifestsRef = useRef<AIFrontendToolManifest[]>([]);
407
+ const unregisterRef = useRef<() => void>(() => undefined);
408
+
409
+ useEffect(() => {
410
+ if (!frontendTools) {
411
+ if (descriptor.tools?.length) {
412
+ throw new Error(
413
+ "Page element frontend Tools require AIProvider above AIPageElementProvider"
414
+ );
415
+ }
416
+ toolManifestsRef.current = [];
417
+ return;
418
+ }
419
+ const contextId = descriptor.id ?? runtimeIdRef.current;
420
+ const unregisterTools: Array<() => void> = [];
421
+ try {
422
+ for (const tool of descriptor.tools ?? []) {
423
+ unregisterTools.push(frontendTools.register(contextId, tool));
424
+ }
425
+ } catch (error) {
426
+ unregisterTools.forEach((unregister) => unregister());
427
+ throw error;
428
+ }
429
+ toolManifestsRef.current = frontendTools.list(contextId);
430
+ return () => {
431
+ unregisterTools.forEach((unregister) => unregister());
432
+ toolManifestsRef.current = [];
433
+ };
434
+ }, [descriptor.id, descriptor.tools, frontendTools]);
435
+
436
+ return useCallback(
437
+ (element) => {
438
+ unregisterRef.current();
439
+ unregisterRef.current = element
440
+ ? register(runtimeIdRef.current, element, () => ({
441
+ ...descriptorRef.current,
442
+ frontendTools: toolManifestsRef.current,
443
+ }))
444
+ : () => undefined;
445
+ },
446
+ [register]
447
+ );
448
+ }
449
+
450
+ export type AIPageElementHandle = {
451
+ ref: RefCallback<HTMLElement>;
452
+ context: AIWorkContextItem;
453
+ };
454
+
455
+ export function useAIPageElementHandle(
456
+ descriptor: AIPageElementDescriptor & { id: string }
457
+ ): AIPageElementHandle {
458
+ const ref = useAIPageElement(descriptor);
459
+ const context = useMemo(
460
+ () =>
461
+ createAIPageContextReference({
462
+ id: descriptor.id,
463
+ title: descriptor.title,
464
+ kind: descriptor.kind,
465
+ }),
466
+ [descriptor.id, descriptor.kind, descriptor.title]
467
+ );
468
+ return useMemo(() => ({ ref, context }), [context, ref]);
469
+ }
@@ -0,0 +1,31 @@
1
+ import {
2
+ Dialog,
3
+ DialogContent,
4
+ DialogDescription,
5
+ DialogTitle,
6
+ } from "@/components/ui/dialog";
7
+ import type { PropsWithChildren } from "react";
8
+
9
+ export function ChatDialog({
10
+ open,
11
+ onOpenChange,
12
+ children,
13
+ }: PropsWithChildren<{
14
+ open: boolean;
15
+ onOpenChange: (open: boolean) => void;
16
+ }>) {
17
+ return (
18
+ <Dialog open={open} onOpenChange={onOpenChange}>
19
+ <DialogContent
20
+ showCloseButton={false}
21
+ className="h-[95svh] w-[95vw] max-w-[95vw] gap-0 overflow-hidden rounded-xl p-0 sm:max-w-[95vw]"
22
+ >
23
+ <DialogTitle className="sr-only">NocoBase AI employee</DialogTitle>
24
+ <DialogDescription className="sr-only">
25
+ Expanded AI conversation window.
26
+ </DialogDescription>
27
+ {children}
28
+ </DialogContent>
29
+ </Dialog>
30
+ );
31
+ }
@@ -0,0 +1,18 @@
1
+ import { cn } from "@/lib/utils";
2
+ import type { PropsWithChildren } from "react";
3
+
4
+ export function ChatInline({
5
+ className,
6
+ children,
7
+ }: PropsWithChildren<{ className?: string }>) {
8
+ return (
9
+ <div
10
+ className={cn(
11
+ "min-h-96 overflow-hidden rounded-xl border bg-card",
12
+ className
13
+ )}
14
+ >
15
+ {children}
16
+ </div>
17
+ );
18
+ }
@@ -0,0 +1,18 @@
1
+ import { cn } from "@/lib/utils";
2
+ import type { PropsWithChildren } from "react";
3
+
4
+ export function ChatPage({
5
+ className,
6
+ children,
7
+ }: PropsWithChildren<{ className?: string }>) {
8
+ return (
9
+ <div
10
+ className={cn(
11
+ "h-[calc(100svh-11rem)] min-h-[560px] overflow-hidden rounded-xl border bg-card shadow-[0_1px_2px_rgba(0,0,0,0.04)]",
12
+ className
13
+ )}
14
+ >
15
+ {children}
16
+ </div>
17
+ );
18
+ }
@@ -0,0 +1,54 @@
1
+ import { cn } from "@/lib/utils";
2
+ import type { CSSProperties, ReactNode } from "react";
3
+ import { ChatSidePanel } from "./chat-side-panel";
4
+
5
+ export type ChatSidePanelLayoutProps = {
6
+ open: boolean;
7
+ onOpenChange: (open: boolean) => void;
8
+ panel: ReactNode;
9
+ children: ReactNode;
10
+ side?: "left" | "right";
11
+ width?: number | string;
12
+ closeOnEscape?: boolean;
13
+ showCloseHandle?: boolean;
14
+ className?: string;
15
+ };
16
+
17
+ export function ChatSidePanelLayout({
18
+ open,
19
+ onOpenChange,
20
+ panel,
21
+ children,
22
+ side = "right",
23
+ width = 450,
24
+ closeOnEscape = true,
25
+ showCloseHandle = true,
26
+ className,
27
+ }: ChatSidePanelLayoutProps) {
28
+ const panelWidth = typeof width === "number" ? `${width}px` : width;
29
+
30
+ return (
31
+ <div
32
+ data-open={open}
33
+ data-side={side}
34
+ className={cn("chat-side-panel-layout @container min-w-0", className)}
35
+ style={
36
+ {
37
+ "--chat-side-panel-width": panelWidth,
38
+ } as CSSProperties
39
+ }
40
+ >
41
+ <div className="min-w-0">{children}</div>
42
+ <ChatSidePanel
43
+ open={open}
44
+ onOpenChange={onOpenChange}
45
+ side={side}
46
+ width={width}
47
+ closeOnEscape={closeOnEscape}
48
+ showCloseHandle={showCloseHandle}
49
+ >
50
+ {panel}
51
+ </ChatSidePanel>
52
+ </div>
53
+ );
54
+ }