@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,59 +0,0 @@
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";
@@ -1,44 +0,0 @@
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
- }
@@ -1,469 +0,0 @@
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
- }
@@ -1,31 +0,0 @@
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
- }
@@ -1,18 +0,0 @@
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
- }
@@ -1,18 +0,0 @@
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
- }
@@ -1,54 +0,0 @@
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
- }