pmx-canvas 0.1.0
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.
- package/CHANGELOG.md +38 -0
- package/LICENSE +21 -0
- package/Readme.md +865 -0
- package/dist/canvas/global.css +3173 -0
- package/dist/canvas/index.js +183 -0
- package/dist/json-render/index.css +2 -0
- package/dist/json-render/index.js +389 -0
- package/dist/types/cli/agent.d.ts +13 -0
- package/dist/types/cli/index.d.ts +2 -0
- package/dist/types/cli/watch.d.ts +5 -0
- package/dist/types/client/App.d.ts +1 -0
- package/dist/types/client/canvas/AttentionHistory.d.ts +1 -0
- package/dist/types/client/canvas/AttentionToast.d.ts +1 -0
- package/dist/types/client/canvas/CanvasNode.d.ts +8 -0
- package/dist/types/client/canvas/CanvasViewport.d.ts +8 -0
- package/dist/types/client/canvas/CommandPalette.d.ts +4 -0
- package/dist/types/client/canvas/ContextMenu.d.ts +24 -0
- package/dist/types/client/canvas/ContextPinBar.d.ts +1 -0
- package/dist/types/client/canvas/ContextPinHud.d.ts +1 -0
- package/dist/types/client/canvas/DockedNode.d.ts +4 -0
- package/dist/types/client/canvas/EdgeLayer.d.ts +8 -0
- package/dist/types/client/canvas/ExpandedNodeOverlay.d.ts +1 -0
- package/dist/types/client/canvas/FocusFieldLayer.d.ts +1 -0
- package/dist/types/client/canvas/Minimap.d.ts +23 -0
- package/dist/types/client/canvas/SelectionBar.d.ts +1 -0
- package/dist/types/client/canvas/ShortcutOverlay.d.ts +3 -0
- package/dist/types/client/canvas/SnapshotPanel.d.ts +7 -0
- package/dist/types/client/canvas/snap-guides.d.ts +23 -0
- package/dist/types/client/canvas/use-node-drag.d.ts +15 -0
- package/dist/types/client/canvas/use-node-resize.d.ts +15 -0
- package/dist/types/client/canvas/use-pan-zoom.d.ts +16 -0
- package/dist/types/client/ext-app/bridge.d.ts +161 -0
- package/dist/types/client/icons.d.ts +70 -0
- package/dist/types/client/index.d.ts +1 -0
- package/dist/types/client/nodes/ContextNode.d.ts +34 -0
- package/dist/types/client/nodes/ExtAppFrame.d.ts +18 -0
- package/dist/types/client/nodes/FileNode.d.ts +5 -0
- package/dist/types/client/nodes/GroupNode.d.ts +6 -0
- package/dist/types/client/nodes/ImageNode.d.ts +10 -0
- package/dist/types/client/nodes/InlineFormatBar.d.ts +7 -0
- package/dist/types/client/nodes/InlineMarkdownEditor.d.ts +14 -0
- package/dist/types/client/nodes/LedgerNode.d.ts +4 -0
- package/dist/types/client/nodes/MarkdownNode.d.ts +6 -0
- package/dist/types/client/nodes/McpAppNode.d.ts +4 -0
- package/dist/types/client/nodes/MdFormatBar.d.ts +8 -0
- package/dist/types/client/nodes/PromptNode.d.ts +5 -0
- package/dist/types/client/nodes/ResponseNode.d.ts +5 -0
- package/dist/types/client/nodes/StatusNode.d.ts +4 -0
- package/dist/types/client/nodes/StatusSummary.d.ts +4 -0
- package/dist/types/client/nodes/TraceNode.d.ts +4 -0
- package/dist/types/client/nodes/WebpageNode.d.ts +5 -0
- package/dist/types/client/nodes/image-warnings.d.ts +6 -0
- package/dist/types/client/nodes/inline-editor-commands.d.ts +11 -0
- package/dist/types/client/nodes/md-format.d.ts +25 -0
- package/dist/types/client/state/attention-bridge.d.ts +3 -0
- package/dist/types/client/state/attention-store.d.ts +25 -0
- package/dist/types/client/state/canvas-store.d.ts +74 -0
- package/dist/types/client/state/intent-bridge.d.ts +158 -0
- package/dist/types/client/state/sse-bridge.d.ts +5 -0
- package/dist/types/client/theme/tokens.d.ts +27 -0
- package/dist/types/client/types.d.ts +40 -0
- package/dist/types/client/utils/ext-app-tool-result.d.ts +1 -0
- package/dist/types/client/utils/placement.d.ts +1 -0
- package/dist/types/client/utils/platform.d.ts +2 -0
- package/dist/types/json-render/catalog.d.ts +815 -0
- package/dist/types/json-render/charts/components.d.ts +54 -0
- package/dist/types/json-render/charts/definitions.d.ts +103 -0
- package/dist/types/json-render/charts/extra-components.d.ts +58 -0
- package/dist/types/json-render/charts/extra-definitions.d.ts +181 -0
- package/dist/types/json-render/renderer/index.d.ts +16 -0
- package/dist/types/json-render/schema.d.ts +46 -0
- package/dist/types/json-render/server.d.ts +55 -0
- package/dist/types/mcp/server.d.ts +22 -0
- package/dist/types/server/agent-context.d.ts +21 -0
- package/dist/types/server/artifact-paths.d.ts +3 -0
- package/dist/types/server/canvas-operations.d.ts +154 -0
- package/dist/types/server/canvas-provenance.d.ts +13 -0
- package/dist/types/server/canvas-schema.d.ts +49 -0
- package/dist/types/server/canvas-serialization.d.ts +25 -0
- package/dist/types/server/canvas-state.d.ts +174 -0
- package/dist/types/server/canvas-validation.d.ts +33 -0
- package/dist/types/server/chart-template.d.ts +29 -0
- package/dist/types/server/code-graph.d.ts +67 -0
- package/dist/types/server/context-cards.d.ts +24 -0
- package/dist/types/server/diagram-presets.d.ts +28 -0
- package/dist/types/server/ext-app-call-registry.d.ts +16 -0
- package/dist/types/server/ext-app-tool-result.d.ts +1 -0
- package/dist/types/server/file-watcher.d.ts +16 -0
- package/dist/types/server/index.d.ts +243 -0
- package/dist/types/server/mcp-app-candidate.d.ts +25 -0
- package/dist/types/server/mcp-app-host.d.ts +65 -0
- package/dist/types/server/mcp-app-runtime.d.ts +47 -0
- package/dist/types/server/mutation-history.d.ts +105 -0
- package/dist/types/server/placement.d.ts +37 -0
- package/dist/types/server/server.d.ts +103 -0
- package/dist/types/server/spatial-analysis.d.ts +87 -0
- package/dist/types/server/trace-manager.d.ts +48 -0
- package/dist/types/server/web-artifacts.d.ts +50 -0
- package/dist/types/server/webpage-node.d.ts +25 -0
- package/dist/types/shared/auto-arrange.d.ts +29 -0
- package/dist/types/shared/ext-app-tool-result.d.ts +9 -0
- package/dist/types/shared/placement.d.ts +26 -0
- package/dist/types/shared/semantic-attention.d.ts +97 -0
- package/package.json +109 -0
- package/skills/data-analysis/SKILL.md +324 -0
- package/skills/doc-coauthoring/SKILL.md +375 -0
- package/skills/frontend-design/SKILL.md +45 -0
- package/skills/json-render-codegen/SKILL.md +112 -0
- package/skills/json-render-core/SKILL.md +265 -0
- package/skills/json-render-ink/SKILL.md +273 -0
- package/skills/json-render-mcp/SKILL.md +132 -0
- package/skills/json-render-react/SKILL.md +264 -0
- package/skills/json-render-shadcn/SKILL.md +159 -0
- package/skills/playwright-cli/SKILL.md +67 -0
- package/skills/pmx-canvas/SKILL.md +668 -0
- package/skills/pmx-canvas/evals/evals.json +186 -0
- package/skills/pmx-canvas-testing/SKILL.md +78 -0
- package/skills/published-consumer-e2e/SKILL.md +43 -0
- package/skills/published-consumer-e2e/scripts/run-published-consumer-e2e.sh +241 -0
- package/skills/web-artifacts-builder/SKILL.md +80 -0
- package/skills/web-artifacts-builder/scripts/bundle-artifact.sh +167 -0
- package/skills/web-artifacts-builder/scripts/init-artifact.sh +425 -0
- package/skills/web-artifacts-builder/scripts/shadcn-components.tar.gz +0 -0
- package/skills/web-design-guidelines/SKILL.md +39 -0
- package/src/cli/agent.ts +2144 -0
- package/src/cli/index.ts +622 -0
- package/src/cli/watch.ts +88 -0
- package/src/client/App.tsx +507 -0
- package/src/client/canvas/AttentionHistory.tsx +81 -0
- package/src/client/canvas/AttentionToast.tsx +19 -0
- package/src/client/canvas/CanvasNode.tsx +363 -0
- package/src/client/canvas/CanvasViewport.tsx +590 -0
- package/src/client/canvas/CommandPalette.tsx +302 -0
- package/src/client/canvas/ContextMenu.tsx +601 -0
- package/src/client/canvas/ContextPinBar.tsx +25 -0
- package/src/client/canvas/ContextPinHud.tsx +22 -0
- package/src/client/canvas/DockedNode.tsx +66 -0
- package/src/client/canvas/EdgeLayer.tsx +280 -0
- package/src/client/canvas/ExpandedNodeOverlay.tsx +260 -0
- package/src/client/canvas/FocusFieldLayer.tsx +107 -0
- package/src/client/canvas/Minimap.tsx +301 -0
- package/src/client/canvas/SelectionBar.tsx +69 -0
- package/src/client/canvas/ShortcutOverlay.tsx +69 -0
- package/src/client/canvas/SnapshotPanel.tsx +236 -0
- package/src/client/canvas/snap-guides.ts +170 -0
- package/src/client/canvas/use-node-drag.ts +51 -0
- package/src/client/canvas/use-node-resize.ts +59 -0
- package/src/client/canvas/use-pan-zoom.ts +191 -0
- package/src/client/ext-app/bridge.ts +542 -0
- package/src/client/icons.tsx +424 -0
- package/src/client/index.tsx +7 -0
- package/src/client/nodes/ContextNode.tsx +412 -0
- package/src/client/nodes/ExtAppFrame.tsx +509 -0
- package/src/client/nodes/FileNode.tsx +256 -0
- package/src/client/nodes/GroupNode.tsx +39 -0
- package/src/client/nodes/ImageNode.tsx +160 -0
- package/src/client/nodes/InlineFormatBar.tsx +169 -0
- package/src/client/nodes/InlineMarkdownEditor.tsx +123 -0
- package/src/client/nodes/LedgerNode.tsx +37 -0
- package/src/client/nodes/MarkdownNode.tsx +359 -0
- package/src/client/nodes/McpAppNode.tsx +85 -0
- package/src/client/nodes/MdFormatBar.tsx +109 -0
- package/src/client/nodes/PromptNode.tsx +597 -0
- package/src/client/nodes/ResponseNode.tsx +153 -0
- package/src/client/nodes/StatusNode.tsx +84 -0
- package/src/client/nodes/StatusSummary.tsx +38 -0
- package/src/client/nodes/TraceNode.tsx +120 -0
- package/src/client/nodes/WebpageNode.tsx +288 -0
- package/src/client/nodes/image-warnings.ts +95 -0
- package/src/client/nodes/inline-editor-commands.ts +37 -0
- package/src/client/nodes/md-format.ts +206 -0
- package/src/client/state/attention-bridge.ts +328 -0
- package/src/client/state/attention-store.ts +73 -0
- package/src/client/state/canvas-store.ts +631 -0
- package/src/client/state/intent-bridge.ts +315 -0
- package/src/client/state/sse-bridge.ts +965 -0
- package/src/client/theme/global.css +3173 -0
- package/src/client/theme/tokens.ts +72 -0
- package/src/client/types-shims.d.ts +5 -0
- package/src/client/types.ts +81 -0
- package/src/client/utils/ext-app-tool-result.ts +4 -0
- package/src/client/utils/placement.ts +4 -0
- package/src/client/utils/platform.ts +2 -0
- package/src/json-render/catalog.ts +256 -0
- package/src/json-render/charts/components.tsx +198 -0
- package/src/json-render/charts/definitions.ts +81 -0
- package/src/json-render/charts/extra-components.tsx +267 -0
- package/src/json-render/charts/extra-definitions.ts +145 -0
- package/src/json-render/renderer/index.css +174 -0
- package/src/json-render/renderer/index.tsx +86 -0
- package/src/json-render/schema.ts +62 -0
- package/src/json-render/server.ts +597 -0
- package/src/mcp/server.ts +1377 -0
- package/src/server/agent-context.ts +242 -0
- package/src/server/artifact-paths.ts +17 -0
- package/src/server/canvas-operations.ts +1279 -0
- package/src/server/canvas-provenance.ts +243 -0
- package/src/server/canvas-schema.ts +432 -0
- package/src/server/canvas-serialization.ts +95 -0
- package/src/server/canvas-state.ts +1134 -0
- package/src/server/canvas-validation.ts +114 -0
- package/src/server/chart-template.ts +449 -0
- package/src/server/code-graph.ts +370 -0
- package/src/server/context-cards.ts +31 -0
- package/src/server/diagram-presets.ts +71 -0
- package/src/server/ext-app-call-registry.ts +77 -0
- package/src/server/ext-app-tool-result.ts +4 -0
- package/src/server/file-watcher.ts +121 -0
- package/src/server/index.ts +647 -0
- package/src/server/mcp-app-candidate.ts +174 -0
- package/src/server/mcp-app-host.ts +814 -0
- package/src/server/mcp-app-runtime.ts +459 -0
- package/src/server/mutation-history.ts +350 -0
- package/src/server/placement.ts +125 -0
- package/src/server/server.ts +3846 -0
- package/src/server/spatial-analysis.ts +356 -0
- package/src/server/trace-manager.ts +333 -0
- package/src/server/web-artifacts/scripts/bundle-artifact.sh +167 -0
- package/src/server/web-artifacts/scripts/init-artifact.sh +426 -0
- package/src/server/web-artifacts/scripts/shadcn-components.tar.gz +0 -0
- package/src/server/web-artifacts.ts +442 -0
- package/src/server/webpage-node.ts +328 -0
- package/src/shared/auto-arrange.ts +439 -0
- package/src/shared/ext-app-tool-result.ts +76 -0
- package/src/shared/placement.ts +81 -0
- package/src/shared/semantic-attention.ts +598 -0
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { buildAppHtml } from '@json-render/mcp/build-app-html';
|
|
5
|
+
import { allComponentDefinitions, catalog, validateShadcnElementProps, type JsonRenderIssue } from './catalog.js';
|
|
6
|
+
|
|
7
|
+
export interface JsonRenderSpec {
|
|
8
|
+
root: string;
|
|
9
|
+
elements: Record<string, unknown>;
|
|
10
|
+
state?: Record<string, unknown>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface JsonRenderNodeInput {
|
|
14
|
+
title: string;
|
|
15
|
+
spec: unknown;
|
|
16
|
+
x?: number;
|
|
17
|
+
y?: number;
|
|
18
|
+
width?: number;
|
|
19
|
+
height?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface GraphNodeInput {
|
|
23
|
+
title?: string;
|
|
24
|
+
graphType: string;
|
|
25
|
+
data: Array<Record<string, unknown>>;
|
|
26
|
+
xKey?: string;
|
|
27
|
+
yKey?: string;
|
|
28
|
+
zKey?: string;
|
|
29
|
+
nameKey?: string;
|
|
30
|
+
valueKey?: string;
|
|
31
|
+
axisKey?: string;
|
|
32
|
+
metrics?: string[];
|
|
33
|
+
series?: string[];
|
|
34
|
+
barKey?: string;
|
|
35
|
+
lineKey?: string;
|
|
36
|
+
aggregate?: 'sum' | 'count' | 'avg';
|
|
37
|
+
color?: string;
|
|
38
|
+
barColor?: string;
|
|
39
|
+
lineColor?: string;
|
|
40
|
+
height?: number;
|
|
41
|
+
x?: number;
|
|
42
|
+
y?: number;
|
|
43
|
+
width?: number;
|
|
44
|
+
heightPx?: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const JSON_RENDER_NODE_SIZE = { width: 840, height: 620 };
|
|
48
|
+
export const GRAPH_NODE_SIZE = { width: 760, height: 520 };
|
|
49
|
+
|
|
50
|
+
export type GraphChartType =
|
|
51
|
+
| 'LineChart'
|
|
52
|
+
| 'BarChart'
|
|
53
|
+
| 'PieChart'
|
|
54
|
+
| 'AreaChart'
|
|
55
|
+
| 'ScatterChart'
|
|
56
|
+
| 'RadarChart'
|
|
57
|
+
| 'StackedBarChart'
|
|
58
|
+
| 'ComposedChart';
|
|
59
|
+
|
|
60
|
+
const GRAPH_TYPE_ALIASES: Record<string, GraphChartType> = {
|
|
61
|
+
line: 'LineChart',
|
|
62
|
+
linechart: 'LineChart',
|
|
63
|
+
chart: 'LineChart',
|
|
64
|
+
graph: 'LineChart',
|
|
65
|
+
bar: 'BarChart',
|
|
66
|
+
barchart: 'BarChart',
|
|
67
|
+
pie: 'PieChart',
|
|
68
|
+
piechart: 'PieChart',
|
|
69
|
+
area: 'AreaChart',
|
|
70
|
+
areachart: 'AreaChart',
|
|
71
|
+
scatter: 'ScatterChart',
|
|
72
|
+
scatterchart: 'ScatterChart',
|
|
73
|
+
scatterplot: 'ScatterChart',
|
|
74
|
+
radar: 'RadarChart',
|
|
75
|
+
radarchart: 'RadarChart',
|
|
76
|
+
stackedbar: 'StackedBarChart',
|
|
77
|
+
stackedbarchart: 'StackedBarChart',
|
|
78
|
+
stack: 'StackedBarChart',
|
|
79
|
+
composed: 'ComposedChart',
|
|
80
|
+
composedchart: 'ComposedChart',
|
|
81
|
+
combo: 'ComposedChart',
|
|
82
|
+
combochart: 'ComposedChart',
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const COERCIBLE_STRING_PROPS = [
|
|
86
|
+
'title',
|
|
87
|
+
'text',
|
|
88
|
+
'message',
|
|
89
|
+
'label',
|
|
90
|
+
'name',
|
|
91
|
+
'content',
|
|
92
|
+
'description',
|
|
93
|
+
'placeholder',
|
|
94
|
+
] as const;
|
|
95
|
+
|
|
96
|
+
let rebuildInFlight: Promise<void> | null = null;
|
|
97
|
+
|
|
98
|
+
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
99
|
+
return value && typeof value === 'object' ? (value as Record<string, unknown>) : null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function hasString(value: unknown): value is string {
|
|
103
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function bundleDir(): string {
|
|
107
|
+
const candidates = [
|
|
108
|
+
join(import.meta.dir, '..', '..', 'dist', 'json-render'),
|
|
109
|
+
join(process.cwd(), 'dist', 'json-render'),
|
|
110
|
+
];
|
|
111
|
+
for (const dir of candidates) {
|
|
112
|
+
if (existsSync(join(dir, 'index.js'))) return dir;
|
|
113
|
+
}
|
|
114
|
+
return candidates[candidates.length - 1];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function escapeInlineScriptSource(source: string): string {
|
|
118
|
+
return source.replace(/<\/script/gi, '<\\/script');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function escapeHtml(value: string): string {
|
|
122
|
+
return value
|
|
123
|
+
.replaceAll('&', '&')
|
|
124
|
+
.replaceAll('<', '<')
|
|
125
|
+
.replaceAll('>', '>')
|
|
126
|
+
.replaceAll('"', '"')
|
|
127
|
+
.replaceAll("'", ''');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function buildErrorHtml(message: string): string {
|
|
131
|
+
const safe = escapeHtml(message);
|
|
132
|
+
return `<!DOCTYPE html>
|
|
133
|
+
<html lang="en">
|
|
134
|
+
<head>
|
|
135
|
+
<meta charset="utf-8" />
|
|
136
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
137
|
+
<title>PMX Canvas json-render error</title>
|
|
138
|
+
</head>
|
|
139
|
+
<body style="margin:0;background:#0b1120;color:#f8fafc;font:14px/1.5 system-ui,sans-serif;">
|
|
140
|
+
<main style="padding:20px;">
|
|
141
|
+
<h1 style="margin:0 0 10px;font-size:16px;">json-render unavailable</h1>
|
|
142
|
+
<pre style="white-space:pre-wrap;margin:0;">${safe}</pre>
|
|
143
|
+
</main>
|
|
144
|
+
</body>
|
|
145
|
+
</html>`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function rebuildJsonRenderBundle(): Promise<void> {
|
|
149
|
+
if (rebuildInFlight) return rebuildInFlight;
|
|
150
|
+
rebuildInFlight = new Promise<void>((resolvePromise) => {
|
|
151
|
+
const child = spawn('bun', ['run', 'build:json-render'], {
|
|
152
|
+
cwd: process.cwd(),
|
|
153
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
154
|
+
});
|
|
155
|
+
const timer = setTimeout(() => {
|
|
156
|
+
child.kill('SIGKILL');
|
|
157
|
+
}, 60_000);
|
|
158
|
+
child.on('close', () => {
|
|
159
|
+
clearTimeout(timer);
|
|
160
|
+
rebuildInFlight = null;
|
|
161
|
+
resolvePromise();
|
|
162
|
+
});
|
|
163
|
+
child.on('error', () => {
|
|
164
|
+
clearTimeout(timer);
|
|
165
|
+
rebuildInFlight = null;
|
|
166
|
+
resolvePromise();
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
return rebuildInFlight;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function ensureJsonRenderBundle(): Promise<void> {
|
|
173
|
+
const dir = bundleDir();
|
|
174
|
+
const jsPath = join(dir, 'index.js');
|
|
175
|
+
const cssPath = join(dir, 'index.css');
|
|
176
|
+
// The renderer bundle is shipped in dist/ and built explicitly via bun run build.
|
|
177
|
+
// Avoid live source-vs-dist rebuild checks here because Bun's bundler can stall on
|
|
178
|
+
// the @json-render/shadcn dependency graph during request-time viewer generation.
|
|
179
|
+
const needsBuild =
|
|
180
|
+
!existsSync(jsPath) ||
|
|
181
|
+
!existsSync(cssPath) ||
|
|
182
|
+
process.env.PMX_CANVAS_FORCE_JSON_RENDER_REBUILD === '1';
|
|
183
|
+
|
|
184
|
+
if (needsBuild) {
|
|
185
|
+
await rebuildJsonRenderBundle();
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function formatValidationError(
|
|
190
|
+
error: { issues?: Array<{ path?: PropertyKey[]; message?: string }> } | undefined,
|
|
191
|
+
): string {
|
|
192
|
+
const issues = Array.isArray(error?.issues) ? error.issues : [];
|
|
193
|
+
if (issues.length === 0) return 'Invalid json-render spec.';
|
|
194
|
+
|
|
195
|
+
const summary = issues
|
|
196
|
+
.slice(0, 5)
|
|
197
|
+
.map((issue) => {
|
|
198
|
+
const path = Array.isArray(issue.path) && issue.path.length > 0 ? issue.path.join('.') : 'spec';
|
|
199
|
+
return `${path}: ${issue.message ?? 'invalid value'}`;
|
|
200
|
+
})
|
|
201
|
+
.join('; ');
|
|
202
|
+
|
|
203
|
+
const extra = issues.length > 5 ? `; +${issues.length - 5} more issue(s)` : '';
|
|
204
|
+
return `Invalid json-render spec: ${summary}${extra}`;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function stripNullishDeep(value: unknown): unknown {
|
|
208
|
+
if (value === null || value === undefined) return undefined;
|
|
209
|
+
if (Array.isArray(value)) {
|
|
210
|
+
return value.map((item) => stripNullishDeep(item)).filter((item) => item !== undefined);
|
|
211
|
+
}
|
|
212
|
+
const record = asRecord(value);
|
|
213
|
+
if (!record) return value;
|
|
214
|
+
return Object.fromEntries(
|
|
215
|
+
Object.entries(record).flatMap(([key, nested]) => {
|
|
216
|
+
const normalized = stripNullishDeep(nested);
|
|
217
|
+
return normalized === undefined ? [] : [[key, normalized]];
|
|
218
|
+
}),
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function normalizeLabelArray(value: unknown): unknown {
|
|
223
|
+
if (!Array.isArray(value)) return value;
|
|
224
|
+
return value.map((item, index) => {
|
|
225
|
+
if (hasString(item)) return item;
|
|
226
|
+
const record = asRecord(item);
|
|
227
|
+
if (record) {
|
|
228
|
+
if (hasString(record.label)) return record.label;
|
|
229
|
+
if (hasString(record.value)) return record.value;
|
|
230
|
+
}
|
|
231
|
+
return `Option ${index + 1}`;
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function normalizeItemArray(value: unknown): unknown {
|
|
236
|
+
if (!Array.isArray(value)) return value;
|
|
237
|
+
return value.map((item, index) => {
|
|
238
|
+
if (hasString(item)) return { label: item, value: item };
|
|
239
|
+
const record = asRecord(item);
|
|
240
|
+
const label = hasString(record?.label) ? record.label : hasString(record?.text) ? record.text : null;
|
|
241
|
+
const resolvedValue = hasString(record?.value) ? record.value : label ?? `option-${index + 1}`;
|
|
242
|
+
return {
|
|
243
|
+
label: label ?? resolvedValue,
|
|
244
|
+
value: resolvedValue,
|
|
245
|
+
};
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function normalizeStringMatrix(value: unknown): unknown {
|
|
250
|
+
if (!Array.isArray(value)) return value;
|
|
251
|
+
return value.map((row) => (
|
|
252
|
+
Array.isArray(row)
|
|
253
|
+
? row.map((cell) => String(cell ?? ''))
|
|
254
|
+
: [String(row ?? '')]
|
|
255
|
+
));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function normalizeButtonVariant(value: unknown): unknown {
|
|
259
|
+
if (value === 'default') return 'primary';
|
|
260
|
+
if (value === 'outline') return 'secondary';
|
|
261
|
+
if (value === 'destructive') return 'danger';
|
|
262
|
+
return value;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function deriveElementName(elementKey: string): string {
|
|
266
|
+
const normalized = elementKey.replace(/[^a-zA-Z0-9_-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
267
|
+
return normalized || 'field';
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const COMPONENT_KEYS = Object.keys(allComponentDefinitions);
|
|
271
|
+
const TYPE_ALIASES: Record<string, string> = {
|
|
272
|
+
NativePanel: 'Card',
|
|
273
|
+
Panel: 'Card',
|
|
274
|
+
Container: 'Stack',
|
|
275
|
+
Section: 'Card',
|
|
276
|
+
FormField: 'Input',
|
|
277
|
+
Paragraph: 'Text',
|
|
278
|
+
Label: 'Text',
|
|
279
|
+
Header: 'Heading',
|
|
280
|
+
Title: 'Heading',
|
|
281
|
+
Chart: 'LineChart',
|
|
282
|
+
Line: 'LineChart',
|
|
283
|
+
Graph: 'LineChart',
|
|
284
|
+
Bar: 'BarChart',
|
|
285
|
+
Pie: 'PieChart',
|
|
286
|
+
};
|
|
287
|
+
let canonicalTypeMap: Map<string, string> | null = null;
|
|
288
|
+
|
|
289
|
+
function buildCanonicalTypeMap(componentKeys: string[]): Map<string, string> {
|
|
290
|
+
const map = new Map<string, string>();
|
|
291
|
+
for (const key of componentKeys) {
|
|
292
|
+
map.set(key.toLowerCase().replace(/[-_\s]/g, ''), key);
|
|
293
|
+
}
|
|
294
|
+
for (const [alias, canonical] of Object.entries(TYPE_ALIASES)) {
|
|
295
|
+
map.set(alias.toLowerCase().replace(/[-_\s]/g, ''), canonical);
|
|
296
|
+
}
|
|
297
|
+
return map;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function normalizeElementType(rawType: string): string {
|
|
301
|
+
if (COMPONENT_KEYS.includes(rawType)) return rawType;
|
|
302
|
+
if (!canonicalTypeMap) canonicalTypeMap = buildCanonicalTypeMap(COMPONENT_KEYS);
|
|
303
|
+
const normalized = rawType.toLowerCase().replace(/[-_\s]/g, '');
|
|
304
|
+
return canonicalTypeMap.get(normalized) ?? 'Card';
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function normalizeElementProps(
|
|
308
|
+
elementKey: string,
|
|
309
|
+
type: string,
|
|
310
|
+
rawProps: Record<string, unknown>,
|
|
311
|
+
): Record<string, unknown> {
|
|
312
|
+
const props = (stripNullishDeep(rawProps) as Record<string, unknown> | undefined) ?? {};
|
|
313
|
+
|
|
314
|
+
for (const key of COERCIBLE_STRING_PROPS) {
|
|
315
|
+
if (key in props && typeof props[key] !== 'string' && props[key] !== undefined) {
|
|
316
|
+
props[key] = String(props[key]);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (!hasString(props.name) && ['Input', 'Textarea', 'Select', 'Checkbox', 'Radio', 'Switch'].includes(type)) {
|
|
321
|
+
props.name = deriveElementName(elementKey);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (!hasString(props.text) && hasString(props.content) && ['Text', 'Heading', 'Badge'].includes(type)) {
|
|
325
|
+
props.text = props.content;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (type === 'Select' || type === 'Radio') {
|
|
329
|
+
if (!Array.isArray(props.options) && Array.isArray(props.items)) {
|
|
330
|
+
props.options = normalizeLabelArray(props.items);
|
|
331
|
+
} else if (Array.isArray(props.options)) {
|
|
332
|
+
props.options = normalizeLabelArray(props.options);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (type === 'ToggleGroup') {
|
|
337
|
+
if (!Array.isArray(props.items) && Array.isArray(props.options)) {
|
|
338
|
+
props.items = normalizeItemArray(props.options);
|
|
339
|
+
} else if (Array.isArray(props.items)) {
|
|
340
|
+
props.items = normalizeItemArray(props.items);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (type === 'ButtonGroup') {
|
|
345
|
+
if (!Array.isArray(props.buttons) && Array.isArray(props.items)) {
|
|
346
|
+
props.buttons = normalizeItemArray(props.items);
|
|
347
|
+
} else if (!Array.isArray(props.buttons) && Array.isArray(props.options)) {
|
|
348
|
+
props.buttons = normalizeItemArray(props.options);
|
|
349
|
+
} else if (Array.isArray(props.buttons)) {
|
|
350
|
+
props.buttons = normalizeItemArray(props.buttons);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (type === 'DropdownMenu') {
|
|
355
|
+
if (!Array.isArray(props.items) && Array.isArray(props.options)) {
|
|
356
|
+
props.items = normalizeItemArray(props.options);
|
|
357
|
+
} else if (Array.isArray(props.items)) {
|
|
358
|
+
props.items = normalizeItemArray(props.items);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (type === 'Tabs') {
|
|
363
|
+
if (!Array.isArray(props.tabs) && Array.isArray(props.items)) {
|
|
364
|
+
props.tabs = normalizeItemArray(props.items);
|
|
365
|
+
} else if (Array.isArray(props.tabs)) {
|
|
366
|
+
props.tabs = normalizeItemArray(props.tabs);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (type === 'Button') {
|
|
371
|
+
props.variant = normalizeButtonVariant(props.variant);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (type === 'Table') {
|
|
375
|
+
if (Array.isArray(props.columns)) {
|
|
376
|
+
props.columns = props.columns.map((column) => String(column ?? ''));
|
|
377
|
+
}
|
|
378
|
+
if (Array.isArray(props.rows)) {
|
|
379
|
+
props.rows = normalizeStringMatrix(props.rows);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return props;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function normalizeSpec(spec: Record<string, unknown>): Record<string, unknown> {
|
|
387
|
+
const elements = asRecord(spec.elements);
|
|
388
|
+
if (!elements) return spec;
|
|
389
|
+
|
|
390
|
+
let changed = false;
|
|
391
|
+
const normalizedElements: Record<string, unknown> = {};
|
|
392
|
+
|
|
393
|
+
for (const [elementKey, rawElement] of Object.entries(elements)) {
|
|
394
|
+
const element = asRecord(rawElement);
|
|
395
|
+
if (!element || typeof element.type !== 'string') {
|
|
396
|
+
normalizedElements[elementKey] = rawElement;
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const resolvedType = normalizeElementType(element.type);
|
|
401
|
+
const rawProps = asRecord(element.props) ?? {};
|
|
402
|
+
const normalizedProps = normalizeElementProps(elementKey, resolvedType, rawProps);
|
|
403
|
+
const normalizedChildren = Array.isArray(element.children)
|
|
404
|
+
? element.children.filter((child: unknown) => typeof child === 'string')
|
|
405
|
+
: [];
|
|
406
|
+
const elementChanged =
|
|
407
|
+
resolvedType !== element.type ||
|
|
408
|
+
JSON.stringify(normalizedProps) !== JSON.stringify(rawProps) ||
|
|
409
|
+
!Array.isArray(element.children) ||
|
|
410
|
+
normalizedChildren.length !== element.children.length;
|
|
411
|
+
|
|
412
|
+
normalizedElements[elementKey] = elementChanged
|
|
413
|
+
? {
|
|
414
|
+
...element,
|
|
415
|
+
type: resolvedType,
|
|
416
|
+
props: normalizedProps,
|
|
417
|
+
children: normalizedChildren,
|
|
418
|
+
}
|
|
419
|
+
: rawElement;
|
|
420
|
+
changed ||= elementChanged;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return changed ? { ...spec, elements: normalizedElements } : spec;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
export function normalizeAndValidateJsonRenderSpec(spec: unknown): JsonRenderSpec {
|
|
427
|
+
const specRecord = asRecord(spec);
|
|
428
|
+
if (!specRecord || typeof specRecord.root !== 'string' || !asRecord(specRecord.elements)) {
|
|
429
|
+
throw new Error('Missing root and elements in spec.');
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const normalizedSpec = normalizeSpec(specRecord);
|
|
433
|
+
const validation = catalog.validate(normalizedSpec);
|
|
434
|
+
if (!validation.success || !validation.data) {
|
|
435
|
+
throw new Error(formatValidationError(validation.error));
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const propsValidation = validateShadcnElementProps(validation.data);
|
|
439
|
+
if (!propsValidation.success || !propsValidation.data) {
|
|
440
|
+
throw new Error(formatValidationError(propsValidation.error));
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return propsValidation.data as JsonRenderSpec;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
export function normalizeGraphType(value: string): GraphChartType {
|
|
447
|
+
const normalized = value.toLowerCase().replace(/[-_\s]/g, '');
|
|
448
|
+
return GRAPH_TYPE_ALIASES[normalized] ?? 'LineChart';
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function inferKeysFromData(data: Array<Record<string, unknown>>, exclude: string[] = []): string[] {
|
|
452
|
+
const first = data[0];
|
|
453
|
+
if (!first) return [];
|
|
454
|
+
return Object.keys(first).filter((key) => !exclude.includes(key));
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
export function buildGraphSpec(input: GraphNodeInput): JsonRenderSpec {
|
|
458
|
+
const title = input.title?.trim() || 'Graph';
|
|
459
|
+
const chartType = normalizeGraphType(input.graphType);
|
|
460
|
+
if (!Array.isArray(input.data)) {
|
|
461
|
+
throw new Error('Graph data must be an array of objects.');
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const chartProps: Record<string, unknown> = {
|
|
465
|
+
data: input.data,
|
|
466
|
+
height: input.height ?? 320,
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
switch (chartType) {
|
|
470
|
+
case 'PieChart': {
|
|
471
|
+
chartProps.nameKey = input.nameKey ?? 'name';
|
|
472
|
+
chartProps.valueKey = input.valueKey ?? 'value';
|
|
473
|
+
break;
|
|
474
|
+
}
|
|
475
|
+
case 'ScatterChart': {
|
|
476
|
+
chartProps.xKey = input.xKey ?? 'x';
|
|
477
|
+
chartProps.yKey = input.yKey ?? 'y';
|
|
478
|
+
chartProps.zKey = input.zKey ?? null;
|
|
479
|
+
chartProps.color = input.color ?? null;
|
|
480
|
+
break;
|
|
481
|
+
}
|
|
482
|
+
case 'RadarChart': {
|
|
483
|
+
const axisKey = input.axisKey ?? input.xKey ?? 'axis';
|
|
484
|
+
const metrics = input.metrics?.length
|
|
485
|
+
? input.metrics
|
|
486
|
+
: inferKeysFromData(input.data, [axisKey]);
|
|
487
|
+
if (metrics.length === 0) {
|
|
488
|
+
throw new Error('RadarChart requires at least one metric key (provide `metrics` or include numeric columns in `data`).');
|
|
489
|
+
}
|
|
490
|
+
chartProps.axisKey = axisKey;
|
|
491
|
+
chartProps.metrics = metrics;
|
|
492
|
+
break;
|
|
493
|
+
}
|
|
494
|
+
case 'StackedBarChart': {
|
|
495
|
+
const xKey = input.xKey ?? 'label';
|
|
496
|
+
const series = input.series?.length
|
|
497
|
+
? input.series
|
|
498
|
+
: inferKeysFromData(input.data, [xKey]);
|
|
499
|
+
if (series.length === 0) {
|
|
500
|
+
throw new Error('StackedBarChart requires at least one series key (provide `series` or include numeric columns in `data`).');
|
|
501
|
+
}
|
|
502
|
+
chartProps.xKey = xKey;
|
|
503
|
+
chartProps.series = series;
|
|
504
|
+
chartProps.aggregate = input.aggregate ?? null;
|
|
505
|
+
break;
|
|
506
|
+
}
|
|
507
|
+
case 'ComposedChart': {
|
|
508
|
+
chartProps.xKey = input.xKey ?? 'label';
|
|
509
|
+
chartProps.barKey = input.barKey ?? input.yKey ?? 'value';
|
|
510
|
+
chartProps.lineKey = input.lineKey
|
|
511
|
+
?? inferKeysFromData(input.data, [chartProps.xKey as string, chartProps.barKey as string])[0]
|
|
512
|
+
?? 'rate';
|
|
513
|
+
chartProps.barColor = input.barColor ?? null;
|
|
514
|
+
chartProps.lineColor = input.lineColor ?? null;
|
|
515
|
+
break;
|
|
516
|
+
}
|
|
517
|
+
case 'AreaChart':
|
|
518
|
+
case 'LineChart':
|
|
519
|
+
case 'BarChart':
|
|
520
|
+
default: {
|
|
521
|
+
chartProps.xKey = input.xKey ?? 'label';
|
|
522
|
+
chartProps.yKey = input.yKey ?? 'value';
|
|
523
|
+
chartProps.aggregate = input.aggregate ?? null;
|
|
524
|
+
chartProps.color = input.color ?? null;
|
|
525
|
+
break;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
return normalizeAndValidateJsonRenderSpec({
|
|
530
|
+
root: 'card',
|
|
531
|
+
elements: {
|
|
532
|
+
card: {
|
|
533
|
+
type: 'Card',
|
|
534
|
+
props: {
|
|
535
|
+
title,
|
|
536
|
+
description: null,
|
|
537
|
+
maxWidth: 'full',
|
|
538
|
+
centered: false,
|
|
539
|
+
},
|
|
540
|
+
children: ['chart'],
|
|
541
|
+
},
|
|
542
|
+
chart: {
|
|
543
|
+
type: chartType,
|
|
544
|
+
props: chartProps,
|
|
545
|
+
children: [],
|
|
546
|
+
},
|
|
547
|
+
},
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
export function createJsonRenderNodeData(
|
|
552
|
+
nodeId: string,
|
|
553
|
+
title: string,
|
|
554
|
+
spec: JsonRenderSpec,
|
|
555
|
+
extra: Record<string, unknown> = {},
|
|
556
|
+
): Record<string, unknown> {
|
|
557
|
+
return {
|
|
558
|
+
title,
|
|
559
|
+
spec,
|
|
560
|
+
url: `/api/canvas/json-render/view?nodeId=${encodeURIComponent(nodeId)}`,
|
|
561
|
+
trustedDomain: true,
|
|
562
|
+
sourceServer: 'pmx-canvas',
|
|
563
|
+
hostMode: 'hosted',
|
|
564
|
+
...extra,
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
export async function buildJsonRenderViewerHtml(options: {
|
|
569
|
+
title: string;
|
|
570
|
+
spec: JsonRenderSpec;
|
|
571
|
+
theme?: 'dark' | 'light' | 'high-contrast';
|
|
572
|
+
}): Promise<string> {
|
|
573
|
+
try {
|
|
574
|
+
await ensureJsonRenderBundle();
|
|
575
|
+
const dir = bundleDir();
|
|
576
|
+
const jsPath = join(dir, 'index.js');
|
|
577
|
+
const cssPath = join(dir, 'index.css');
|
|
578
|
+
const jsBundle = existsSync(jsPath)
|
|
579
|
+
? readFileSync(jsPath, 'utf-8')
|
|
580
|
+
: 'document.body.innerHTML = "<pre>json-render bundle missing</pre>";';
|
|
581
|
+
const cssBundle = existsSync(cssPath) ? readFileSync(cssPath, 'utf-8') : '';
|
|
582
|
+
const boot = [
|
|
583
|
+
`window.__PMX_CANVAS_JSON_RENDER_SPEC__ = ${JSON.stringify(options.spec)};`,
|
|
584
|
+
...(options.theme ? [`window.__PMX_CANVAS_JSON_RENDER_THEME__ = ${JSON.stringify(options.theme)};`] : []),
|
|
585
|
+
jsBundle,
|
|
586
|
+
].join('\n');
|
|
587
|
+
return buildAppHtml({
|
|
588
|
+
title: options.title,
|
|
589
|
+
css: cssBundle,
|
|
590
|
+
js: escapeInlineScriptSource(boot),
|
|
591
|
+
head: '<meta name="color-scheme" content="light dark" />',
|
|
592
|
+
});
|
|
593
|
+
} catch (error) {
|
|
594
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
595
|
+
return buildErrorHtml(`Failed to load the json-render viewer bundle.\n\n${message}`);
|
|
596
|
+
}
|
|
597
|
+
}
|