@nocobase/portal-template-default 1.0.2 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/package.json +1 -1
  2. package/scripts/registry.mjs +2 -0
  3. package/src/extensions/nocobase-acl/README.md +19 -0
  4. package/src/extensions/nocobase-acl/components/acl-boundary.tsx +106 -0
  5. package/src/extensions/nocobase-acl/components/index.ts +3 -0
  6. package/src/extensions/nocobase-acl/components/role-options.ts +36 -0
  7. package/src/extensions/nocobase-acl/components/role-switcher.tsx +135 -0
  8. package/src/extensions/nocobase-acl/demo/boundary-api.tsx +186 -0
  9. package/src/extensions/nocobase-acl/demo/components.tsx +231 -0
  10. package/src/extensions/nocobase-acl/demo/index.tsx +505 -0
  11. package/src/extensions/nocobase-acl/demo/lazy-route.tsx +10 -0
  12. package/src/extensions/nocobase-acl/demo/prompt-generator.tsx +134 -0
  13. package/src/extensions/nocobase-acl/demo/role-switcher-prompt-generator.tsx +198 -0
  14. package/src/extensions/nocobase-acl/demo/scenario-section.tsx +37 -0
  15. package/src/extensions/nocobase-acl/extension.tsx +71 -0
  16. package/src/extensions/nocobase-acl/index.ts +1 -0
  17. package/src/extensions/nocobase-ai/README.md +70 -0
  18. package/src/extensions/nocobase-ai/adapters/react-hook-form.ts +20 -0
  19. package/src/extensions/nocobase-ai/assets/nocobase-ai-chat.svg +50 -0
  20. package/src/extensions/nocobase-ai/components/ai-root-provider.tsx +32 -0
  21. package/src/extensions/nocobase-ai/components/chat/ai-employee-avatar.tsx +31 -0
  22. package/src/extensions/nocobase-ai/components/chat/chat-attachment.tsx +92 -0
  23. package/src/extensions/nocobase-ai/components/chat/chat-compact.tsx +90 -0
  24. package/src/extensions/nocobase-ai/components/chat/chat-composer.tsx +429 -0
  25. package/src/extensions/nocobase-ai/components/chat/chat-empty-state.tsx +51 -0
  26. package/src/extensions/nocobase-ai/components/chat/chat-header.tsx +88 -0
  27. package/src/extensions/nocobase-ai/components/chat/chat-history-dialog.tsx +77 -0
  28. package/src/extensions/nocobase-ai/components/chat/chat-message.tsx +252 -0
  29. package/src/extensions/nocobase-ai/components/chat/chat-messages.tsx +173 -0
  30. package/src/extensions/nocobase-ai/components/chat/chat-window.tsx +118 -0
  31. package/src/extensions/nocobase-ai/components/chat/conversation-list.tsx +343 -0
  32. package/src/extensions/nocobase-ai/components/chat/markdown-message.tsx +165 -0
  33. package/src/extensions/nocobase-ai/components/chat/model-select-options.tsx +30 -0
  34. package/src/extensions/nocobase-ai/components/chat/reasoning-panel.tsx +29 -0
  35. package/src/extensions/nocobase-ai/components/chat/sub-agent-conversation.tsx +142 -0
  36. package/src/extensions/nocobase-ai/components/chat/tool-call-card.tsx +316 -0
  37. package/src/extensions/nocobase-ai/components/chat/user-prompt-editor.tsx +99 -0
  38. package/src/extensions/nocobase-ai/components/chat/work-context-chip.tsx +114 -0
  39. package/src/extensions/nocobase-ai/components/index.ts +59 -0
  40. package/src/extensions/nocobase-ai/components/page-elements/ai-form.tsx +44 -0
  41. package/src/extensions/nocobase-ai/components/page-elements/page-element-provider.tsx +469 -0
  42. package/src/extensions/nocobase-ai/components/surfaces/chat-dialog.tsx +31 -0
  43. package/src/extensions/nocobase-ai/components/surfaces/chat-inline.tsx +18 -0
  44. package/src/extensions/nocobase-ai/components/surfaces/chat-page.tsx +18 -0
  45. package/src/extensions/nocobase-ai/components/surfaces/chat-side-panel-layout.tsx +54 -0
  46. package/src/extensions/nocobase-ai/components/surfaces/chat-side-panel.tsx +74 -0
  47. package/src/extensions/nocobase-ai/components/surfaces/chat-surface-actions.tsx +54 -0
  48. package/src/extensions/nocobase-ai/components/tools/builtin-tool-renderers.tsx +31 -0
  49. package/src/extensions/nocobase-ai/components/tools/business-report-dialog.tsx +364 -0
  50. package/src/extensions/nocobase-ai/components/tools/business-report-renderer.tsx +122 -0
  51. package/src/extensions/nocobase-ai/components/tools/business-report-utils.tsx +282 -0
  52. package/src/extensions/nocobase-ai/components/tools/chart-renderer.tsx +99 -0
  53. package/src/extensions/nocobase-ai/components/tools/echarts-preview.tsx +59 -0
  54. package/src/extensions/nocobase-ai/components/tools/echarts-runtime-advanced.ts +18 -0
  55. package/src/extensions/nocobase-ai/components/tools/echarts-runtime-common.ts +20 -0
  56. package/src/extensions/nocobase-ai/components/tools/echarts-runtime-components.ts +34 -0
  57. package/src/extensions/nocobase-ai/components/tools/echarts-runtime-hierarchy.ts +24 -0
  58. package/src/extensions/nocobase-ai/components/tools/echarts-runtime.ts +138 -0
  59. package/src/extensions/nocobase-ai/components/tools/sub-agent-renderer.tsx +67 -0
  60. package/src/extensions/nocobase-ai/components/tools/suggestions-renderer.tsx +59 -0
  61. package/src/extensions/nocobase-ai/components/tools/tool-renderer-provider.tsx +55 -0
  62. package/src/extensions/nocobase-ai/components/tools/tool-renderer-utils.ts +18 -0
  63. package/src/extensions/nocobase-ai/components/tools/workflow-renderer.tsx +108 -0
  64. package/src/extensions/nocobase-ai/components/triggers/ai-chat-floating-trigger.tsx +73 -0
  65. package/src/extensions/nocobase-ai/components/triggers/ai-employee-shortcut.tsx +149 -0
  66. package/src/extensions/nocobase-ai/demo/configuration-gate.tsx +49 -0
  67. package/src/extensions/nocobase-ai/demo/container-showcase.tsx +210 -0
  68. package/src/extensions/nocobase-ai/demo/floating.tsx +134 -0
  69. package/src/extensions/nocobase-ai/demo/index.tsx +346 -0
  70. package/src/extensions/nocobase-ai/demo/interaction-showcase.tsx +151 -0
  71. package/src/extensions/nocobase-ai/demo/page-context-prompt-generator.tsx +355 -0
  72. package/src/extensions/nocobase-ai/demo/page-context.tsx +884 -0
  73. package/src/extensions/nocobase-ai/demo/page-element-showcase.tsx +197 -0
  74. package/src/extensions/nocobase-ai/demo/prompt-card.tsx +41 -0
  75. package/src/extensions/nocobase-ai/demo/prompt-generator.tsx +452 -0
  76. package/src/extensions/nocobase-ai/demo/shortcut.tsx +1207 -0
  77. package/src/extensions/nocobase-ai/demo/tool-cards.tsx +530 -0
  78. package/src/extensions/nocobase-ai/extension.tsx +158 -0
  79. package/src/extensions/nocobase-ai/global-ai-chat.tsx +213 -0
  80. package/src/extensions/nocobase-ai/index.ts +3 -0
  81. package/src/extensions/nocobase-ai/providers/ai-provider.tsx +323 -0
  82. package/src/extensions/nocobase-ai/providers/avatars.ts +817 -0
  83. package/src/extensions/nocobase-ai/providers/chat-context.tsx +109 -0
  84. package/src/extensions/nocobase-ai/providers/chat-controller.ts +84 -0
  85. package/src/extensions/nocobase-ai/providers/chat-message-utils.ts +198 -0
  86. package/src/extensions/nocobase-ai/providers/chat-provider.tsx +871 -0
  87. package/src/extensions/nocobase-ai/providers/chat-reducer.ts +147 -0
  88. package/src/extensions/nocobase-ai/providers/chat-task-utils.ts +105 -0
  89. package/src/extensions/nocobase-ai/providers/chat-transport.ts +184 -0
  90. package/src/extensions/nocobase-ai/providers/form-registry.tsx +271 -0
  91. package/src/extensions/nocobase-ai/providers/frontend-tool-registry.tsx +231 -0
  92. package/src/extensions/nocobase-ai/providers/index.ts +61 -0
  93. package/src/extensions/nocobase-ai/providers/model.ts +32 -0
  94. package/src/extensions/nocobase-ai/providers/page-context.tsx +136 -0
  95. package/src/extensions/nocobase-ai/providers/stream-coalescer.ts +60 -0
  96. package/src/extensions/nocobase-ai/providers/stream-event-utils.ts +82 -0
  97. package/src/extensions/nocobase-ai/providers/stream-parser.ts +61 -0
  98. package/src/extensions/nocobase-ai/providers/sub-agent-stream.ts +316 -0
  99. package/src/extensions/nocobase-ai/providers/types.ts +183 -0
  100. package/src/extensions/nocobase-ai/providers/ui-message-stream.ts +463 -0
  101. package/src/extensions/nocobase-ai/providers/use-automatic-tool-approval.ts +94 -0
  102. package/src/extensions/nocobase-ai/providers/use-chat-attachments.ts +206 -0
  103. package/src/extensions/nocobase-ai/providers/use-chat-message-actions.ts +554 -0
  104. package/src/extensions/nocobase-ai/providers/use-chat-runtime.ts +210 -0
  105. package/src/extensions/nocobase-ai/providers/use-chat-work-context.ts +93 -0
  106. package/src/extensions/nocobase-ai/providers/use-conversation-catalog.ts +108 -0
  107. package/src/extensions/nocobase-ai/providers/use-conversation-history.ts +178 -0
  108. package/src/extensions/nocobase-ai/services/index.ts +7 -0
  109. package/src/extensions/nocobase-ai/services/nocobase-ai-service.ts +454 -0
  110. package/src/extensions/nocobase-ai/services/types.ts +82 -0
@@ -0,0 +1,282 @@
1
+ import ReactMarkdown from "react-markdown";
2
+ import remarkGfm from "remark-gfm";
3
+
4
+ export type BusinessReportChart = {
5
+ title?: string;
6
+ summary?: string;
7
+ options: Record<string, unknown>;
8
+ };
9
+
10
+ export type BusinessReportData = {
11
+ title: string;
12
+ summary?: string;
13
+ markdown?: string;
14
+ charts: BusinessReportChart[];
15
+ fileName?: string;
16
+ };
17
+
18
+ export type BusinessReportMarkdownPart =
19
+ | { type: "markdown"; content: string }
20
+ | { type: "chart"; options: Record<string, unknown> };
21
+
22
+ const chartTagPattern = /<echarts>([\s\S]*?)<\/echarts>/gi;
23
+ const chartPlaceholderPattern = /\{\{\s*chart\s*:\s*(\d+)\s*\}\}/gi;
24
+
25
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
26
+ Boolean(value) && typeof value === "object" && !Array.isArray(value);
27
+
28
+ export function normalizeBusinessReportCharts(
29
+ value: unknown
30
+ ): BusinessReportChart[] {
31
+ let charts = value;
32
+ if (typeof value === "string") {
33
+ try {
34
+ charts = JSON.parse(value) as unknown;
35
+ } catch {
36
+ return [];
37
+ }
38
+ }
39
+ if (!Array.isArray(charts)) return [];
40
+ return charts.flatMap((item) => {
41
+ if (!isRecord(item) || !isRecord(item.options)) return [];
42
+ return [
43
+ {
44
+ title: typeof item.title === "string" ? item.title : undefined,
45
+ summary: typeof item.summary === "string" ? item.summary : undefined,
46
+ options: item.options,
47
+ },
48
+ ];
49
+ });
50
+ }
51
+
52
+ const buildChartMarkdownBlock = (
53
+ chart: BusinessReportChart,
54
+ index: number
55
+ ) => {
56
+ const parts = [`## ${chart.title || `Chart ${index + 1}`}`];
57
+ if (chart.summary) parts.push(chart.summary);
58
+ parts.push(`<echarts>${JSON.stringify(chart.options, null, 2)}</echarts>`);
59
+ return parts.join("\n\n");
60
+ };
61
+
62
+ export function buildBusinessReportMarkdown(report: BusinessReportData) {
63
+ const usedIndexes = new Set<number>();
64
+ const body = (report.markdown ?? "").replace(
65
+ chartPlaceholderPattern,
66
+ (placeholder, rawIndex: string) => {
67
+ const index = Number(rawIndex) - 1;
68
+ const chart = report.charts[index];
69
+ if (!chart) return placeholder;
70
+ usedIndexes.add(index);
71
+ return buildChartMarkdownBlock(chart, index);
72
+ }
73
+ );
74
+ const sections = [`# ${report.title}`];
75
+ if (report.summary) sections.push(`> ${report.summary}`);
76
+ if (body.trim()) sections.push(body.trim());
77
+ report.charts.forEach((chart, index) => {
78
+ if (!usedIndexes.has(index)) {
79
+ sections.push(buildChartMarkdownBlock(chart, index));
80
+ }
81
+ });
82
+ return sections.join("\n\n");
83
+ }
84
+
85
+ export function splitBusinessReportMarkdown(
86
+ markdown: string
87
+ ): BusinessReportMarkdownPart[] {
88
+ const result: BusinessReportMarkdownPart[] = [];
89
+ let offset = 0;
90
+ for (const match of markdown.matchAll(chartTagPattern)) {
91
+ const index = match.index ?? 0;
92
+ const before = markdown.slice(offset, index);
93
+ if (before.trim()) result.push({ type: "markdown", content: before });
94
+ try {
95
+ const options = JSON.parse(match[1]) as unknown;
96
+ if (isRecord(options)) result.push({ type: "chart", options });
97
+ } catch {
98
+ result.push({ type: "markdown", content: match[0] });
99
+ }
100
+ offset = index + match[0].length;
101
+ }
102
+ const remainder = markdown.slice(offset);
103
+ if (remainder.trim()) result.push({ type: "markdown", content: remainder });
104
+ return result;
105
+ }
106
+
107
+ export function getBusinessReportFileName(report: BusinessReportData) {
108
+ return (report.fileName || report.title || "business-analysis-report")
109
+ .replace(/[\\/:*?"<>|]+/g, "-")
110
+ .trim();
111
+ }
112
+
113
+ export function downloadBusinessReportFile(
114
+ filename: string,
115
+ content: string,
116
+ mimeType: string
117
+ ) {
118
+ const blob = new Blob([content], { type: mimeType });
119
+ const url = URL.createObjectURL(blob);
120
+ const link = document.createElement("a");
121
+ link.href = url;
122
+ link.download = filename;
123
+ link.click();
124
+ window.setTimeout(() => URL.revokeObjectURL(url), 0);
125
+ }
126
+
127
+ export async function renderBusinessReportMarkdownToHtml(markdown: string) {
128
+ // Static rendering is intentionally loaded only for HTML export/preview.
129
+ // Rendering through a temporary client root can lose a section when the
130
+ // root is unmounted immediately after a synchronous render.
131
+ const { prerender } = await import("react-dom/static.browser");
132
+ const { prelude } = await prerender(
133
+ <ReactMarkdown remarkPlugins={[remarkGfm]}>{markdown}</ReactMarkdown>
134
+ );
135
+ return new Response(prelude as unknown as BodyInit).text();
136
+ }
137
+
138
+ const nextFrame = () =>
139
+ new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
140
+
141
+ async function renderChartImage(options: Record<string, unknown>) {
142
+ const { prepareEChartsRuntime } = await import("./echarts-runtime");
143
+ const echarts = await prepareEChartsRuntime(options);
144
+ const host = document.createElement("div");
145
+ host.style.position = "fixed";
146
+ host.style.left = "-100000px";
147
+ host.style.top = "0";
148
+ host.style.width = "900px";
149
+ host.style.height = "360px";
150
+ host.style.pointerEvents = "none";
151
+ document.body.appendChild(host);
152
+ let chart: ReturnType<typeof echarts.init> | undefined;
153
+ try {
154
+ chart = echarts.init(host, "default", { renderer: "canvas" });
155
+ chart.setOption(
156
+ {
157
+ ...options,
158
+ animation: false,
159
+ backgroundColor: "#ffffff",
160
+ toolbox: { show: false },
161
+ },
162
+ true
163
+ );
164
+ await nextFrame();
165
+ await nextFrame();
166
+ const source = chart.getDataURL({
167
+ type: "png",
168
+ pixelRatio: 2,
169
+ backgroundColor: "#ffffff",
170
+ excludeComponents: ["toolbox"],
171
+ });
172
+ return source;
173
+ } finally {
174
+ chart?.dispose();
175
+ host.remove();
176
+ }
177
+ }
178
+
179
+ const escapeHtml = (value: string) =>
180
+ value
181
+ .replace(/&/g, "&amp;")
182
+ .replace(/</g, "&lt;")
183
+ .replace(/>/g, "&gt;")
184
+ .replace(/"/g, "&quot;")
185
+ .replace(/'/g, "&#39;");
186
+
187
+ export async function buildBusinessReportHtml(
188
+ report: BusinessReportData,
189
+ options: { autoPrint?: boolean; printMode?: boolean } = {}
190
+ ) {
191
+ const markdown = buildBusinessReportMarkdown(report);
192
+ const parts = splitBusinessReportMarkdown(markdown);
193
+ const body: string[] = [];
194
+ for (const part of parts) {
195
+ if (part.type === "markdown") {
196
+ body.push(await renderBusinessReportMarkdownToHtml(part.content));
197
+ continue;
198
+ }
199
+ try {
200
+ const source = await renderChartImage(part.options);
201
+ body.push(`<img class="report-chart" src="${source}" alt="" />`);
202
+ } catch (error) {
203
+ body.push(
204
+ `<pre class="chart-error">${escapeHtml(
205
+ error instanceof Error ? error.message : "Unable to render chart"
206
+ )}</pre>`
207
+ );
208
+ }
209
+ }
210
+ const printMode = options.printMode === true;
211
+ return `<!doctype html>
212
+ <html lang="en">
213
+ <head>
214
+ <meta charset="UTF-8" />
215
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
216
+ <title>${escapeHtml(report.title)}</title>
217
+ <style>
218
+ * { box-sizing: border-box; }
219
+ html { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
220
+ body { margin: 0; color: #1f2937; background: ${
221
+ printMode ? "#fff" : "#f5f5f5"
222
+ }; font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
223
+ .report-shell { max-width: ${
224
+ printMode ? "190mm" : "960px"
225
+ }; margin: 0 auto; padding: ${printMode ? "0" : "32px 24px 64px"}; }
226
+ .report-paper { background: #fff; border: ${
227
+ printMode ? "0" : "1px solid #d4d4d4"
228
+ }; border-radius: ${printMode ? "0" : "16px"}; padding: ${
229
+ printMode ? "0" : "40px 48px"
230
+ }; }
231
+ h1, h2, h3 { color: #171717; break-after: avoid-page; }
232
+ h1 { margin-top: 0; font-size: 32px; }
233
+ p, li { line-height: 1.7; }
234
+ blockquote { margin: 16px 0; padding: 12px 16px; border-left: 4px solid #525252; background: #f5f5f5; color: #525252; }
235
+ table { width: 100%; border-collapse: collapse; }
236
+ th, td { border: 1px solid #d4d4d4; padding: 10px 12px; text-align: left; }
237
+ pre { overflow: auto; white-space: pre-wrap; word-break: break-word; }
238
+ .report-chart { display: block; width: 100%; height: auto; margin: 24px 0 32px; border: 1px solid #d4d4d4; break-inside: avoid; }
239
+ .chart-error { color: #b91c1c; }
240
+ @page { size: A4; margin: 12mm; }
241
+ @media print { body { background: #fff; } .report-shell { max-width: none; padding: 0; } .report-paper { border: 0; padding: 0; } }
242
+ </style>
243
+ </head>
244
+ <body>
245
+ <main class="report-shell"><article class="report-paper">${body.join(
246
+ ""
247
+ )}</article></main>
248
+ ${
249
+ options.autoPrint
250
+ ? `<script>
251
+ window.addEventListener('load', async () => {
252
+ if (document.fonts?.ready) {
253
+ try {
254
+ await document.fonts.ready;
255
+ } catch {}
256
+ }
257
+ requestAnimationFrame(() => {
258
+ requestAnimationFrame(() => window.print());
259
+ });
260
+ });
261
+ </script>`
262
+ : ""
263
+ }
264
+ </body>
265
+ </html>`;
266
+ }
267
+
268
+ export async function printBusinessReport(report: BusinessReportData) {
269
+ const html = await buildBusinessReportHtml(report, {
270
+ autoPrint: true,
271
+ printMode: true,
272
+ });
273
+ const blob = new Blob([html], { type: "text/html;charset=utf-8" });
274
+ const url = URL.createObjectURL(blob);
275
+ const opened = window.open(url, "_blank", "noopener,noreferrer");
276
+ if (!opened) {
277
+ URL.revokeObjectURL(url);
278
+ return false;
279
+ }
280
+ window.setTimeout(() => URL.revokeObjectURL(url), 60_000);
281
+ return true;
282
+ }
@@ -0,0 +1,99 @@
1
+ import { Button } from "@/components/ui/button";
2
+ import { LoadingState } from "@/components/app-shell/loading-state";
3
+ import { CheckCircle2, Copy, LoaderCircle } from "lucide-react";
4
+ import { Component, lazy, Suspense, useState, type ReactNode } from "react";
5
+ import type { AIToolRendererProps } from "./tool-renderer-provider";
6
+ import { asRecord } from "./tool-renderer-utils";
7
+
8
+ const EChartsPreview = lazy(() => import("./echarts-preview"));
9
+
10
+ class ChartErrorBoundary extends Component<
11
+ {
12
+ children: ReactNode;
13
+ fallback: (error: Error) => ReactNode;
14
+ resetKey: unknown;
15
+ },
16
+ { error: Error | null; resetKey: unknown }
17
+ > {
18
+ state = { error: null, resetKey: this.props.resetKey } as {
19
+ error: Error | null;
20
+ resetKey: unknown;
21
+ };
22
+
23
+ static getDerivedStateFromError(error: Error) {
24
+ return { error };
25
+ }
26
+
27
+ static getDerivedStateFromProps(
28
+ props: { resetKey: unknown },
29
+ state: { error: Error | null; resetKey: unknown }
30
+ ) {
31
+ return props.resetKey !== state.resetKey
32
+ ? { error: null, resetKey: props.resetKey }
33
+ : null;
34
+ }
35
+
36
+ render() {
37
+ return this.state.error
38
+ ? this.props.fallback(this.state.error)
39
+ : this.props.children;
40
+ }
41
+ }
42
+
43
+ function ChartError({ error }: { error: Error }) {
44
+ const [copied, setCopied] = useState(false);
45
+ return (
46
+ <div className="rounded-lg border border-destructive/30 bg-destructive/5 p-3 text-xs text-destructive">
47
+ <div className="font-medium">Invalid chart options</div>
48
+ <div className="mt-1 flex items-start justify-between gap-3">
49
+ <span className="break-all leading-5">{error.message}</span>
50
+ <Button
51
+ variant="ghost"
52
+ size="icon-xs"
53
+ aria-label="Copy chart error"
54
+ onClick={async () => {
55
+ await navigator.clipboard.writeText(error.message);
56
+ setCopied(true);
57
+ window.setTimeout(() => setCopied(false), 1200);
58
+ }}
59
+ >
60
+ {copied ? <CheckCircle2 /> : <Copy />}
61
+ </Button>
62
+ </div>
63
+ </div>
64
+ );
65
+ }
66
+
67
+ export function ChartPreview({
68
+ options,
69
+ }: {
70
+ options: Record<string, unknown>;
71
+ }) {
72
+ return (
73
+ <Suspense fallback={<LoadingState className="h-[280px]" />}>
74
+ <EChartsPreview options={options} />
75
+ </Suspense>
76
+ );
77
+ }
78
+
79
+ export function ChartRenderer({ part }: AIToolRendererProps) {
80
+ const input = asRecord(part.input);
81
+ const options = asRecord(input.options);
82
+ if (!Object.keys(options).length) {
83
+ return (
84
+ <div className="flex items-center gap-2 py-2 text-xs text-muted-foreground">
85
+ <LoaderCircle className="size-4 animate-spin" /> Generating chart…
86
+ </div>
87
+ );
88
+ }
89
+ return (
90
+ <ChartErrorBoundary
91
+ resetKey={part.input}
92
+ fallback={(error) => <ChartError error={error} />}
93
+ >
94
+ <div className="rounded-lg border bg-background p-3">
95
+ <ChartPreview options={options} />
96
+ </div>
97
+ </ChartErrorBoundary>
98
+ );
99
+ }
@@ -0,0 +1,59 @@
1
+ import ReactEChartsCore from "echarts-for-react/lib/core";
2
+ import { useTheme } from "next-themes";
3
+ import { useEffect, useState } from "react";
4
+ import { LoadingState } from "@/components/app-shell/loading-state";
5
+ import echarts, {
6
+ getEChartsRuntimeSignature,
7
+ prepareEChartsRuntime,
8
+ } from "./echarts-runtime";
9
+
10
+ export default function EChartsPreview({
11
+ options,
12
+ }: {
13
+ options: Record<string, unknown>;
14
+ }) {
15
+ const { resolvedTheme } = useTheme();
16
+ const signature = getEChartsRuntimeSignature(options);
17
+ const [preparedSignature, setPreparedSignature] = useState<string>();
18
+ const [error, setError] = useState<unknown>();
19
+
20
+ useEffect(() => {
21
+ let active = true;
22
+ setError(undefined);
23
+ void prepareEChartsRuntime(options)
24
+ .then(() => {
25
+ if (active) setPreparedSignature(signature);
26
+ })
27
+ .catch((runtimeError: unknown) => {
28
+ if (active) setError(runtimeError);
29
+ });
30
+ return () => {
31
+ active = false;
32
+ };
33
+ }, [options, signature]);
34
+
35
+ if (error) throw error;
36
+ if (preparedSignature !== signature) {
37
+ return <LoadingState className="h-[280px]" />;
38
+ }
39
+
40
+ return (
41
+ <ReactEChartsCore
42
+ echarts={echarts}
43
+ option={{
44
+ ...options,
45
+ animation: false,
46
+ backgroundColor: options.backgroundColor ?? "transparent",
47
+ toolbox: {
48
+ show: true,
49
+ feature: {
50
+ saveAsImage: { title: "Save as image" },
51
+ },
52
+ },
53
+ }}
54
+ theme={resolvedTheme === "dark" ? "nocobase-dark" : undefined}
55
+ notMerge
56
+ style={{ height: 280, width: "100%" }}
57
+ />
58
+ );
59
+ }
@@ -0,0 +1,18 @@
1
+ import {
2
+ CustomChart,
3
+ HeatmapChart,
4
+ LinesChart,
5
+ MapChart,
6
+ ParallelChart,
7
+ ThemeRiverChart,
8
+ } from "echarts/charts";
9
+ import { use as registerEChartsModules } from "echarts/core";
10
+
11
+ registerEChartsModules([
12
+ CustomChart,
13
+ HeatmapChart,
14
+ LinesChart,
15
+ MapChart,
16
+ ParallelChart,
17
+ ThemeRiverChart,
18
+ ]);
@@ -0,0 +1,20 @@
1
+ import {
2
+ BarChart,
3
+ BoxplotChart,
4
+ CandlestickChart,
5
+ EffectScatterChart,
6
+ LineChart,
7
+ PictorialBarChart,
8
+ ScatterChart,
9
+ } from "echarts/charts";
10
+ import { use as registerEChartsModules } from "echarts/core";
11
+
12
+ registerEChartsModules([
13
+ BarChart,
14
+ BoxplotChart,
15
+ CandlestickChart,
16
+ EffectScatterChart,
17
+ LineChart,
18
+ PictorialBarChart,
19
+ ScatterChart,
20
+ ]);
@@ -0,0 +1,34 @@
1
+ import {
2
+ BrushComponent,
3
+ CalendarComponent,
4
+ DataZoomComponent,
5
+ GeoComponent,
6
+ GraphicComponent,
7
+ MarkAreaComponent,
8
+ MarkLineComponent,
9
+ MarkPointComponent,
10
+ ParallelComponent,
11
+ PolarComponent,
12
+ RadarComponent,
13
+ SingleAxisComponent,
14
+ TimelineComponent,
15
+ VisualMapComponent,
16
+ } from "echarts/components";
17
+ import { use as registerEChartsModules } from "echarts/core";
18
+
19
+ registerEChartsModules([
20
+ BrushComponent,
21
+ CalendarComponent,
22
+ DataZoomComponent,
23
+ GeoComponent,
24
+ GraphicComponent,
25
+ MarkAreaComponent,
26
+ MarkLineComponent,
27
+ MarkPointComponent,
28
+ ParallelComponent,
29
+ PolarComponent,
30
+ RadarComponent,
31
+ SingleAxisComponent,
32
+ TimelineComponent,
33
+ VisualMapComponent,
34
+ ]);
@@ -0,0 +1,24 @@
1
+ import {
2
+ FunnelChart,
3
+ GaugeChart,
4
+ GraphChart,
5
+ PieChart,
6
+ RadarChart,
7
+ SankeyChart,
8
+ SunburstChart,
9
+ TreeChart,
10
+ TreemapChart,
11
+ } from "echarts/charts";
12
+ import { use as registerEChartsModules } from "echarts/core";
13
+
14
+ registerEChartsModules([
15
+ FunnelChart,
16
+ GaugeChart,
17
+ GraphChart,
18
+ PieChart,
19
+ RadarChart,
20
+ SankeyChart,
21
+ SunburstChart,
22
+ TreeChart,
23
+ TreemapChart,
24
+ ]);
@@ -0,0 +1,138 @@
1
+ import {
2
+ AriaComponent,
3
+ DatasetComponent,
4
+ GridComponent,
5
+ LegendComponent,
6
+ TitleComponent,
7
+ ToolboxComponent,
8
+ TooltipComponent,
9
+ TransformComponent,
10
+ } from "echarts/components";
11
+ import * as echarts from "echarts/core";
12
+ import { CanvasRenderer } from "echarts/renderers";
13
+
14
+ echarts.use([
15
+ AriaComponent,
16
+ DatasetComponent,
17
+ GridComponent,
18
+ LegendComponent,
19
+ TitleComponent,
20
+ ToolboxComponent,
21
+ TooltipComponent,
22
+ TransformComponent,
23
+ CanvasRenderer,
24
+ ]);
25
+
26
+ echarts.registerTheme("nocobase-dark", {
27
+ darkMode: true,
28
+ backgroundColor: "transparent",
29
+ textStyle: { color: "#d4d4d8" },
30
+ title: { textStyle: { color: "#f4f4f5" } },
31
+ legend: { textStyle: { color: "#d4d4d8" } },
32
+ categoryAxis: {
33
+ axisLine: { lineStyle: { color: "#52525b" } },
34
+ axisLabel: { color: "#a1a1aa" },
35
+ splitLine: { lineStyle: { color: "#27272a" } },
36
+ },
37
+ valueAxis: {
38
+ axisLine: { lineStyle: { color: "#52525b" } },
39
+ axisLabel: { color: "#a1a1aa" },
40
+ splitLine: { lineStyle: { color: "#27272a" } },
41
+ },
42
+ });
43
+
44
+ const commonChartTypes = new Set([
45
+ "bar",
46
+ "boxplot",
47
+ "candlestick",
48
+ "effectScatter",
49
+ "line",
50
+ "pictorialBar",
51
+ "scatter",
52
+ ]);
53
+ const hierarchyChartTypes = new Set([
54
+ "funnel",
55
+ "gauge",
56
+ "graph",
57
+ "pie",
58
+ "radar",
59
+ "sankey",
60
+ "sunburst",
61
+ "tree",
62
+ "treemap",
63
+ ]);
64
+ const advancedChartTypes = new Set([
65
+ "custom",
66
+ "heatmap",
67
+ "lines",
68
+ "map",
69
+ "parallel",
70
+ "themeRiver",
71
+ ]);
72
+
73
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
74
+ Boolean(value) && typeof value === "object" && !Array.isArray(value);
75
+
76
+ const getSeries = (options: Record<string, unknown>) => {
77
+ const value = options.series;
78
+ if (Array.isArray(value)) return value.filter(isRecord);
79
+ return isRecord(value) ? [value] : [];
80
+ };
81
+
82
+ const getRequiredGroups = (options: Record<string, unknown>) => {
83
+ const groups = new Set<string>();
84
+ const series = getSeries(options);
85
+ for (const item of series) {
86
+ const type = typeof item.type === "string" ? item.type : "line";
87
+ if (commonChartTypes.has(type)) groups.add("common");
88
+ else if (hierarchyChartTypes.has(type)) groups.add("hierarchy");
89
+ else if (advancedChartTypes.has(type)) groups.add("advanced");
90
+ }
91
+ const needsOptionalComponents =
92
+ [
93
+ "brush",
94
+ "calendar",
95
+ "dataZoom",
96
+ "geo",
97
+ "graphic",
98
+ "parallel",
99
+ "polar",
100
+ "radar",
101
+ "singleAxis",
102
+ "timeline",
103
+ "visualMap",
104
+ ].some((key) => options[key] !== undefined) ||
105
+ series.some(
106
+ (item) =>
107
+ ["lines", "map", "parallel", "radar"].includes(
108
+ typeof item.type === "string" ? item.type : ""
109
+ ) ||
110
+ item.markArea !== undefined ||
111
+ item.markLine !== undefined ||
112
+ item.markPoint !== undefined
113
+ );
114
+ if (needsOptionalComponents) groups.add("components");
115
+ return [...groups].sort();
116
+ };
117
+
118
+ const groupLoaders: Record<string, () => Promise<unknown>> = {
119
+ common: () => import("./echarts-runtime-common"),
120
+ hierarchy: () => import("./echarts-runtime-hierarchy"),
121
+ advanced: () => import("./echarts-runtime-advanced"),
122
+ components: () => import("./echarts-runtime-components"),
123
+ };
124
+
125
+ export const getEChartsRuntimeSignature = (
126
+ options: Record<string, unknown>
127
+ ) => getRequiredGroups(options).join(":");
128
+
129
+ export async function prepareEChartsRuntime(
130
+ options: Record<string, unknown>
131
+ ) {
132
+ await Promise.all(
133
+ getRequiredGroups(options).map((group) => groupLoaders[group]())
134
+ );
135
+ return echarts;
136
+ }
137
+
138
+ export default echarts;