@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,74 +0,0 @@
1
- import { Button } from "@/components/ui/button";
2
- import { cn } from "@/lib/utils";
3
- import { PanelLeftClose, PanelRightClose } from "lucide-react";
4
- import { useEffect, type CSSProperties, type PropsWithChildren } from "react";
5
-
6
- export type ChatSidePanelProps = PropsWithChildren<{
7
- open: boolean;
8
- onOpenChange: (open: boolean) => void;
9
- side?: "left" | "right";
10
- width?: number | string;
11
- closeOnEscape?: boolean;
12
- showCloseHandle?: boolean;
13
- }>;
14
-
15
- export function ChatSidePanel({
16
- open,
17
- onOpenChange,
18
- side = "right",
19
- width = 450,
20
- closeOnEscape = true,
21
- showCloseHandle = true,
22
- children,
23
- }: ChatSidePanelProps) {
24
- useEffect(() => {
25
- if (!open || !closeOnEscape) return;
26
- const onKeyDown = (event: KeyboardEvent) => {
27
- if (event.key !== "Escape" || event.defaultPrevented) return;
28
- const openDialog = document.querySelector(
29
- '[role="dialog"][data-state="open"], [role="alertdialog"][data-state="open"]'
30
- );
31
- if (openDialog) return;
32
- onOpenChange(false);
33
- };
34
- window.addEventListener("keydown", onKeyDown);
35
- return () => window.removeEventListener("keydown", onKeyDown);
36
- }, [closeOnEscape, onOpenChange, open]);
37
-
38
- if (!open) return null;
39
-
40
- const panelWidth = typeof width === "number" ? `${width}px` : width;
41
-
42
- return (
43
- <aside
44
- role="complementary"
45
- aria-label="NocoBase AI employee"
46
- data-side={side}
47
- className={cn(
48
- "fixed inset-y-0 z-50 max-w-full overscroll-contain bg-background shadow-2xl animate-in duration-200",
49
- side === "right"
50
- ? "right-0 border-l slide-in-from-right"
51
- : "left-0 border-r slide-in-from-left"
52
- )}
53
- style={{ width: panelWidth } as CSSProperties}
54
- >
55
- {showCloseHandle ? (
56
- <Button
57
- variant="outline"
58
- size="icon-sm"
59
- className={cn(
60
- "absolute top-1/2 z-40 size-9 -translate-y-1/2 rounded-full bg-background shadow-md before:absolute before:-inset-2",
61
- side === "right"
62
- ? "left-0 -translate-x-1/2"
63
- : "right-0 translate-x-1/2"
64
- )}
65
- aria-label="Close side panel"
66
- onClick={() => onOpenChange(false)}
67
- >
68
- {side === "right" ? <PanelRightClose /> : <PanelLeftClose />}
69
- </Button>
70
- ) : null}
71
- {children}
72
- </aside>
73
- );
74
- }
@@ -1,54 +0,0 @@
1
- import { Button } from "@/components/ui/button";
2
- import {
3
- Tooltip,
4
- TooltipContent,
5
- TooltipTrigger,
6
- } from "@/components/ui/tooltip";
7
- import { Maximize2, PanelRight, X } from "lucide-react";
8
-
9
- export function ChatSurfaceActions({
10
- expanded,
11
- onExpandedChange,
12
- onClose,
13
- }: {
14
- expanded: boolean;
15
- onExpandedChange: (expanded: boolean) => void;
16
- onClose: () => void;
17
- }) {
18
- return (
19
- <>
20
- <Tooltip>
21
- <TooltipTrigger
22
- render={
23
- <Button
24
- variant="ghost"
25
- size="icon-sm"
26
- aria-label={expanded ? "Collapse to side panel" : "Expand panel"}
27
- onClick={() => onExpandedChange(!expanded)}
28
- />
29
- }
30
- >
31
- {expanded ? <PanelRight /> : <Maximize2 />}
32
- </TooltipTrigger>
33
- <TooltipContent>
34
- {expanded ? "Collapse to side panel" : "Expand panel"}
35
- </TooltipContent>
36
- </Tooltip>
37
- <Tooltip>
38
- <TooltipTrigger
39
- render={
40
- <Button
41
- variant="ghost"
42
- size="icon-sm"
43
- aria-label="Close AI chat"
44
- onClick={onClose}
45
- />
46
- }
47
- >
48
- <X />
49
- </TooltipTrigger>
50
- <TooltipContent>Close</TooltipContent>
51
- </Tooltip>
52
- </>
53
- );
54
- }
@@ -1,31 +0,0 @@
1
- import type { AIToolRendererMap } from "./tool-renderer-provider";
2
- import { BusinessReportRenderer } from "./business-report-renderer";
3
- import { ChartRenderer } from "./chart-renderer";
4
- import { SubAgentRenderer } from "./sub-agent-renderer";
5
- import { SuggestionsRenderer } from "./suggestions-renderer";
6
- import { WorkflowRenderer } from "./workflow-renderer";
7
-
8
- export const builtInToolRenderers: AIToolRendererMap = {
9
- suggestions: {
10
- component: SuggestionsRenderer,
11
- handlesApproval: true,
12
- standalone: true,
13
- },
14
- businessReportGenerator: {
15
- component: BusinessReportRenderer,
16
- standalone: true,
17
- },
18
- chartGenerator: {
19
- component: ChartRenderer,
20
- standalone: true,
21
- },
22
- "dispatch-sub-agent-task": {
23
- component: SubAgentRenderer,
24
- standalone: true,
25
- },
26
- aiEmployeeWorkflowTaskOutput: {
27
- component: WorkflowRenderer,
28
- handlesApproval: true,
29
- standalone: true,
30
- },
31
- };
@@ -1,364 +0,0 @@
1
- import { MarkdownMessage } from "../chat/markdown-message";
2
- import { LoadingState } from "@/components/app-shell/loading-state";
3
- import { Button } from "@/components/ui/button";
4
- import {
5
- Dialog,
6
- DialogContent,
7
- DialogDescription,
8
- DialogTitle,
9
- } from "@/components/ui/dialog";
10
- import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
11
- import { Download, FileCode2, LoaderCircle, Printer } from "lucide-react";
12
- import {
13
- createContext,
14
- lazy,
15
- type PropsWithChildren,
16
- Suspense,
17
- useCallback,
18
- useContext,
19
- useEffect,
20
- useMemo,
21
- useState,
22
- } from "react";
23
- import {
24
- buildBusinessReportHtml,
25
- buildBusinessReportMarkdown,
26
- downloadBusinessReportFile,
27
- getBusinessReportFileName,
28
- printBusinessReport,
29
- splitBusinessReportMarkdown,
30
- type BusinessReportData,
31
- } from "./business-report-utils";
32
-
33
- type BusinessReportDialogSnapshot = {
34
- open: boolean;
35
- toolCallId?: string;
36
- report?: BusinessReportData;
37
- ready: boolean;
38
- };
39
-
40
- const closedSnapshot: BusinessReportDialogSnapshot = {
41
- open: false,
42
- ready: false,
43
- };
44
- const EChartsPreview = lazy(() => import("./echarts-preview"));
45
-
46
- const sameCharts = (
47
- left: BusinessReportData["charts"],
48
- right: BusinessReportData["charts"]
49
- ) => left === right || JSON.stringify(left) === JSON.stringify(right);
50
-
51
- const sameReport = (
52
- left: BusinessReportData | undefined,
53
- right: BusinessReportData
54
- ) =>
55
- left?.title === right.title &&
56
- left.summary === right.summary &&
57
- left.markdown === right.markdown &&
58
- left.fileName === right.fileName &&
59
- sameCharts(left.charts, right.charts);
60
-
61
- type BusinessReportDialogController = {
62
- open: (
63
- toolCallId: string,
64
- report: BusinessReportData,
65
- ready: boolean
66
- ) => void;
67
- update: (
68
- toolCallId: string,
69
- report: BusinessReportData,
70
- ready: boolean
71
- ) => void;
72
- };
73
-
74
- const BusinessReportDialogContext =
75
- createContext<BusinessReportDialogController | null>(null);
76
-
77
- export function useBusinessReportDialog() {
78
- const value = useContext(BusinessReportDialogContext);
79
- if (!value) {
80
- throw new Error(
81
- "useBusinessReportDialog must be used inside BusinessReportDialogProvider"
82
- );
83
- }
84
- return value;
85
- }
86
-
87
- export function BusinessReportDialogProvider({ children }: PropsWithChildren) {
88
- const [state, setState] = useState(closedSnapshot);
89
- const open = useCallback(
90
- (toolCallId: string, report: BusinessReportData, ready: boolean) =>
91
- setState({ open: true, toolCallId, report, ready }),
92
- []
93
- );
94
- const update = useCallback(
95
- (toolCallId: string, report: BusinessReportData, ready: boolean) =>
96
- setState((current) => {
97
- if (current.toolCallId !== toolCallId) return current;
98
- if (current.ready === ready && sameReport(current.report, report)) {
99
- return current;
100
- }
101
- return { ...current, report, ready };
102
- }),
103
- []
104
- );
105
- const controller = useMemo(() => ({ open, update }), [open, update]);
106
- return (
107
- <BusinessReportDialogContext.Provider value={controller}>
108
- {children}
109
- <BusinessReportDialogHost
110
- state={state}
111
- onOpenChange={(nextOpen) =>
112
- setState((current) =>
113
- current.open === nextOpen ? current : { ...current, open: nextOpen }
114
- )
115
- }
116
- />
117
- </BusinessReportDialogContext.Provider>
118
- );
119
- }
120
-
121
- function ChartPreview({ options }: { options: Record<string, unknown> }) {
122
- return (
123
- <Suspense fallback={<LoadingState className="h-[280px]" />}>
124
- <EChartsPreview options={options} />
125
- </Suspense>
126
- );
127
- }
128
-
129
- function BusinessReportDialogHost({
130
- state,
131
- onOpenChange,
132
- }: {
133
- state: BusinessReportDialogSnapshot;
134
- onOpenChange: (open: boolean) => void;
135
- }) {
136
- const report = state.report;
137
- const [activeTab, setActiveTab] = useState("preview");
138
- const [htmlPreview, setHtmlPreview] = useState("");
139
- const [htmlPreviewSignature, setHtmlPreviewSignature] = useState("");
140
- const [htmlLoading, setHtmlLoading] = useState(false);
141
- const [exporting, setExporting] = useState<"html" | "pdf">();
142
- const [exportError, setExportError] = useState<string>();
143
- const reportSignature = useMemo(
144
- () => (state.ready && report ? JSON.stringify(report) : ""),
145
- [report, state.ready]
146
- );
147
- const reportMarkdown = useMemo(
148
- () =>
149
- state.open && state.ready && report
150
- ? buildBusinessReportMarkdown(report)
151
- : "",
152
- [report, state.open, state.ready]
153
- );
154
- const previewParts = useMemo(
155
- () =>
156
- activeTab === "preview" && reportMarkdown
157
- ? splitBusinessReportMarkdown(reportMarkdown)
158
- : [],
159
- [activeTab, reportMarkdown]
160
- );
161
-
162
- useEffect(() => {
163
- setActiveTab("preview");
164
- setHtmlPreview("");
165
- setHtmlPreviewSignature("");
166
- setExportError(undefined);
167
- }, [state.toolCallId]);
168
-
169
- useEffect(() => {
170
- if (
171
- !state.open ||
172
- activeTab !== "html" ||
173
- !state.ready ||
174
- !report?.markdown ||
175
- (htmlPreview && htmlPreviewSignature === reportSignature)
176
- ) {
177
- return;
178
- }
179
- let active = true;
180
- setHtmlLoading(true);
181
- setHtmlPreview("");
182
- void buildBusinessReportHtml(report)
183
- .then((html) => {
184
- if (!active) return;
185
- setHtmlPreview(html);
186
- setHtmlPreviewSignature(reportSignature);
187
- })
188
- .catch((error: unknown) => {
189
- if (active) {
190
- setExportError(
191
- error instanceof Error ? error.message : "Unable to build HTML"
192
- );
193
- }
194
- })
195
- .finally(() => {
196
- if (active) setHtmlLoading(false);
197
- });
198
- return () => {
199
- active = false;
200
- };
201
- }, [
202
- activeTab,
203
- htmlPreview,
204
- htmlPreviewSignature,
205
- report,
206
- reportSignature,
207
- state.open,
208
- state.ready,
209
- ]);
210
-
211
- if (!report) return null;
212
- const summary =
213
- report.summary || "Open the report to review the generated analysis.";
214
- const fileName = getBusinessReportFileName(report);
215
-
216
- return (
217
- <Dialog open={state.open} onOpenChange={onOpenChange}>
218
- <DialogContent className="h-[86svh] w-[min(980px,calc(100vw-2rem))] max-w-[980px] grid-rows-[auto_1fr_auto] overflow-hidden p-0 sm:max-w-[980px]">
219
- <div className="border-b px-5 py-4">
220
- <DialogTitle>{report.title}</DialogTitle>
221
- <DialogDescription className="mt-1">{summary}</DialogDescription>
222
- </div>
223
- <Tabs
224
- value={activeTab}
225
- onValueChange={(value) => setActiveTab(String(value))}
226
- className="min-h-0 overflow-hidden px-5 py-4"
227
- >
228
- <TabsList>
229
- <TabsTrigger value="preview">Preview</TabsTrigger>
230
- <TabsTrigger value="markdown">Markdown</TabsTrigger>
231
- <TabsTrigger value="html">HTML</TabsTrigger>
232
- </TabsList>
233
- <TabsContent
234
- value="preview"
235
- className="mt-3 min-h-0 overflow-auto rounded-lg border bg-background p-5"
236
- >
237
- <div className="space-y-4">
238
- {previewParts.map((item, index) =>
239
- item.type === "markdown" ? (
240
- <div key={index} className="ai-markdown">
241
- <MarkdownMessage variant="document">
242
- {item.content}
243
- </MarkdownMessage>
244
- </div>
245
- ) : (
246
- <div key={index} className="rounded-lg border p-3">
247
- <ChartPreview options={item.options} />
248
- </div>
249
- )
250
- )}
251
- </div>
252
- </TabsContent>
253
- <TabsContent
254
- value="markdown"
255
- className="mt-3 min-h-0 overflow-auto rounded-lg bg-muted p-4"
256
- >
257
- <pre className="whitespace-pre-wrap break-words font-mono text-xs leading-5">
258
- {reportMarkdown}
259
- </pre>
260
- </TabsContent>
261
- <TabsContent
262
- value="html"
263
- className="mt-3 min-h-0 overflow-hidden rounded-lg border bg-background"
264
- >
265
- {htmlPreview ? (
266
- <iframe
267
- title={`${report.title} HTML preview`}
268
- srcDoc={htmlPreview}
269
- className="size-full min-h-[480px] border-0 bg-white"
270
- />
271
- ) : htmlLoading ? (
272
- <LoadingState className="h-full min-h-[480px]" />
273
- ) : (
274
- <div className="flex h-full min-h-[480px] items-center justify-center gap-2 text-sm text-muted-foreground">
275
- HTML is unavailable
276
- </div>
277
- )}
278
- </TabsContent>
279
- </Tabs>
280
- <div className="flex flex-wrap items-center justify-end gap-2 border-t px-5 py-3">
281
- {exportError ? (
282
- <p className="mr-auto text-xs text-destructive">{exportError}</p>
283
- ) : null}
284
- <Button
285
- variant="outline"
286
- disabled={!report.markdown}
287
- onClick={() =>
288
- downloadBusinessReportFile(
289
- `${fileName}.md`,
290
- reportMarkdown,
291
- "text/markdown;charset=utf-8"
292
- )
293
- }
294
- >
295
- <Download /> Download Markdown
296
- </Button>
297
- <Button
298
- variant="outline"
299
- disabled={!report.markdown || exporting !== undefined}
300
- onClick={async () => {
301
- setExportError(undefined);
302
- setExporting("html");
303
- try {
304
- const html = await buildBusinessReportHtml(report, {
305
- printMode: true,
306
- });
307
- downloadBusinessReportFile(
308
- `${fileName}.html`,
309
- html,
310
- "text/html;charset=utf-8"
311
- );
312
- } catch (error) {
313
- setExportError(
314
- error instanceof Error
315
- ? error.message
316
- : "Unable to export HTML"
317
- );
318
- } finally {
319
- setExporting(undefined);
320
- }
321
- }}
322
- >
323
- {exporting === "html" ? (
324
- <LoaderCircle className="animate-spin" />
325
- ) : (
326
- <FileCode2 />
327
- )}
328
- Download HTML
329
- </Button>
330
- <Button
331
- disabled={!report.markdown || exporting !== undefined}
332
- onClick={async () => {
333
- setExportError(undefined);
334
- setExporting("pdf");
335
- try {
336
- const opened = await printBusinessReport(report);
337
- if (!opened) {
338
- setExportError(
339
- "Popup blocked. Allow popups and try printing again."
340
- );
341
- }
342
- } catch (error) {
343
- setExportError(
344
- error instanceof Error
345
- ? error.message
346
- : "Unable to print report"
347
- );
348
- } finally {
349
- setExporting(undefined);
350
- }
351
- }}
352
- >
353
- {exporting === "pdf" ? (
354
- <LoaderCircle className="animate-spin" />
355
- ) : (
356
- <Printer />
357
- )}
358
- Print PDF
359
- </Button>
360
- </div>
361
- </DialogContent>
362
- </Dialog>
363
- );
364
- }
@@ -1,122 +0,0 @@
1
- import { Badge } from "@/components/ui/badge";
2
- import { cn } from "@/lib/utils";
3
- import { FileText, LoaderCircle } from "lucide-react";
4
- import { useEffect, useMemo, useRef } from "react";
5
- import { getNocoBaseToolCallMetadata } from "../chat/tool-call-card";
6
- import type { AIToolRendererProps } from "./tool-renderer-provider";
7
- import {
8
- normalizeBusinessReportCharts,
9
- type BusinessReportData,
10
- } from "./business-report-utils";
11
- import {
12
- useBusinessReportDialog,
13
- } from "./business-report-dialog";
14
- import { asRecord, asString } from "./tool-renderer-utils";
15
-
16
- function ReportGeneratingProgress() {
17
- return (
18
- <div
19
- role="progressbar"
20
- aria-label="Generating business report"
21
- className="mt-3 h-1 w-full overflow-hidden rounded-full bg-muted"
22
- >
23
- <svg
24
- viewBox="0 0 100 4"
25
- preserveAspectRatio="none"
26
- aria-hidden="true"
27
- className="block size-full motion-reduce:hidden"
28
- >
29
- <rect x="-28" y="0" width="28" height="4" rx="2" className="fill-primary">
30
- <animate
31
- attributeName="x"
32
- values="-28;100"
33
- dur="1.35s"
34
- repeatCount="indefinite"
35
- />
36
- </rect>
37
- </svg>
38
- <div className="hidden h-full w-2/3 rounded-full bg-primary motion-reduce:block" />
39
- </div>
40
- );
41
- }
42
-
43
- export function BusinessReportRenderer({ part }: AIToolRendererProps) {
44
- const reportDialog = useBusinessReportDialog();
45
- const input = asRecord(part.input);
46
- const title = asString(input.title) || "Business analysis report";
47
- const reportSummary = asString(input.summary);
48
- const summary =
49
- reportSummary || "Open the report to review the generated analysis.";
50
- const charts = useMemo(
51
- () => normalizeBusinessReportCharts(input.charts),
52
- [input.charts]
53
- );
54
- const report = useMemo<BusinessReportData>(
55
- () => ({
56
- title,
57
- summary: reportSummary || undefined,
58
- markdown: asString(input.markdown),
59
- charts,
60
- fileName: asString(input.fileName) || undefined,
61
- }),
62
- [charts, input.fileName, input.markdown, reportSummary, title]
63
- );
64
- const metadata = getNocoBaseToolCallMetadata(part);
65
- const ready =
66
- part.state === "output-available" ||
67
- (metadata?.status === "success" &&
68
- ["done", "confirmed"].includes(metadata.invokeStatus ?? ""));
69
- const generating = !ready && part.state !== "output-error";
70
- const wasGenerating = useRef(false);
71
-
72
- useEffect(() => {
73
- reportDialog.update(part.toolCallId, report, ready);
74
- }, [part.toolCallId, ready, report, reportDialog]);
75
-
76
- useEffect(() => {
77
- if (generating) {
78
- wasGenerating.current = true;
79
- return;
80
- }
81
- if (wasGenerating.current && ready) {
82
- wasGenerating.current = false;
83
- reportDialog.open(part.toolCallId, report, ready);
84
- }
85
- }, [generating, part.toolCallId, ready, report, reportDialog]);
86
-
87
- return (
88
- <button
89
- type="button"
90
- className={cn(
91
- "w-full rounded-lg border bg-background p-3 text-left transition-colors",
92
- ready && "hover:bg-muted/40"
93
- )}
94
- disabled={!ready}
95
- onClick={() => reportDialog.open(part.toolCallId, report, ready)}
96
- >
97
- <div className="flex items-start gap-3">
98
- <div className="flex size-9 shrink-0 items-center justify-center rounded-lg border bg-muted/30">
99
- {generating ? (
100
- <LoaderCircle className="size-4 animate-spin" />
101
- ) : (
102
- <FileText className="size-4" />
103
- )}
104
- </div>
105
- <div className="min-w-0 flex-1">
106
- <div className="font-medium">{title}</div>
107
- <p className="mt-1 line-clamp-2 text-xs leading-5 text-muted-foreground">
108
- {summary}
109
- </p>
110
- <div className="mt-2 flex flex-wrap gap-1.5">
111
- <Badge variant="secondary">
112
- {generating ? "Generating" : "Markdown"}
113
- </Badge>
114
- <Badge variant="outline">{charts.length} charts</Badge>
115
- <Badge variant="outline">Preview and export</Badge>
116
- </div>
117
- {generating ? <ReportGeneratingProgress /> : null}
118
- </div>
119
- </div>
120
- </button>
121
- );
122
- }