pmx-canvas 0.1.20 → 0.1.21
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 +85 -0
- package/dist/canvas/global.css +88 -0
- package/dist/canvas/index.js +87 -53
- package/dist/types/client/nodes/HtmlNode.d.ts +12 -1
- package/dist/types/client/types.d.ts +1 -1
- package/dist/types/server/html-node-summary.d.ts +2 -0
- package/dist/types/server/html-primitives.d.ts +9 -1
- package/dist/types/server/index.d.ts +8 -1
- package/docs/http-api.md +1 -1
- package/docs/mcp.md +4 -0
- package/docs/node-types.md +27 -5
- package/docs/screenshot.png +0 -0
- package/docs/sdk.md +1 -0
- package/package.json +1 -1
- package/skills/pmx-canvas/SKILL.md +10 -4
- package/skills/pmx-canvas/references/html-primitives.md +132 -0
- package/src/cli/agent.ts +9 -0
- package/src/cli/index.ts +1 -1
- package/src/client/App.tsx +1 -1
- package/src/client/canvas/CommandPalette.tsx +1 -1
- package/src/client/canvas/ExpandedNodeOverlay.tsx +105 -2
- package/src/client/canvas/auto-fit.ts +5 -1
- package/src/client/nodes/HtmlNode.tsx +125 -13
- package/src/client/state/sse-bridge.ts +1 -1
- package/src/client/theme/global.css +88 -0
- package/src/mcp/canvas-access.ts +31 -1
- package/src/mcp/server.ts +17 -3
- package/src/server/agent-context.ts +23 -1
- package/src/server/canvas-operations.ts +10 -2
- package/src/server/canvas-provenance.ts +8 -6
- package/src/server/canvas-schema.ts +11 -0
- package/src/server/canvas-serialization.ts +10 -5
- package/src/server/html-node-summary.ts +141 -0
- package/src/server/html-primitives.ts +318 -8
- package/src/server/index.ts +22 -3
- package/src/server/server.ts +17 -4
- package/src/server/spatial-analysis.ts +4 -2
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
const HTML_CONTENT_SUMMARY_MAX_LENGTH = 900;
|
|
2
|
+
const HTML_AGENT_SUMMARY_MAX_LENGTH = 1200;
|
|
3
|
+
const HTML_REFERENCE_LIMIT = 12;
|
|
4
|
+
|
|
5
|
+
function pickString(value: unknown): string | null {
|
|
6
|
+
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function strings(value: unknown): string[] {
|
|
10
|
+
return Array.isArray(value)
|
|
11
|
+
? value.filter((item): item is string => typeof item === 'string' && item.trim().length > 0)
|
|
12
|
+
: [];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function truncateText(value: string, maxLength: number): string {
|
|
16
|
+
if (value.length <= maxLength) return value;
|
|
17
|
+
return `${value.slice(0, Math.max(0, maxLength - 3)).trimEnd()}...`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function normalizeWhitespace(value: string): string {
|
|
21
|
+
return value.replace(/\s+/g, ' ').trim();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function decodeHtmlEntities(value: string): string {
|
|
25
|
+
const named: Record<string, string> = {
|
|
26
|
+
amp: '&',
|
|
27
|
+
gt: '>',
|
|
28
|
+
lt: '<',
|
|
29
|
+
nbsp: ' ',
|
|
30
|
+
quot: '"',
|
|
31
|
+
apos: "'",
|
|
32
|
+
};
|
|
33
|
+
return value.replace(/&(#x?[0-9a-f]+|[a-z]+);/gi, (match, entity) => {
|
|
34
|
+
const lower = entity.toLowerCase();
|
|
35
|
+
if (lower.startsWith('#x')) {
|
|
36
|
+
const codePoint = Number.parseInt(lower.slice(2), 16);
|
|
37
|
+
return Number.isInteger(codePoint) && codePoint >= 0 && codePoint <= 0x10ffff ? String.fromCodePoint(codePoint) : match;
|
|
38
|
+
}
|
|
39
|
+
if (lower.startsWith('#')) {
|
|
40
|
+
const codePoint = Number.parseInt(lower.slice(1), 10);
|
|
41
|
+
return Number.isInteger(codePoint) && codePoint >= 0 && codePoint <= 0x10ffff ? String.fromCodePoint(codePoint) : match;
|
|
42
|
+
}
|
|
43
|
+
return named[lower] ?? match;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function summarizeHtmlText(html: string): string | null {
|
|
48
|
+
const withoutNoise = html
|
|
49
|
+
.replace(/<!--[\s\S]*?-->/g, ' ')
|
|
50
|
+
.replace(/<script\b[\s\S]*?<\/script>/gi, ' ')
|
|
51
|
+
.replace(/<style\b[\s\S]*?<\/style>/gi, ' ')
|
|
52
|
+
.replace(/<noscript\b[\s\S]*?<\/noscript>/gi, ' ');
|
|
53
|
+
const text = withoutNoise
|
|
54
|
+
.replace(/<(?:h[1-6]|p|li|br|div|section|article|header|footer|main|aside|summary|figcaption|blockquote|tr|td|th)\b[^>]*>/gi, '\n')
|
|
55
|
+
.replace(/<[^>]+>/g, ' ');
|
|
56
|
+
const normalized = normalizeWhitespace(decodeHtmlEntities(text));
|
|
57
|
+
return normalized.length > 0 ? truncateText(normalized, HTML_CONTENT_SUMMARY_MAX_LENGTH) : null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function uniqueLimited(values: string[]): string[] {
|
|
61
|
+
const seen = new Set<string>();
|
|
62
|
+
const unique: string[] = [];
|
|
63
|
+
for (const value of values) {
|
|
64
|
+
const trimmed = value.trim();
|
|
65
|
+
if (!trimmed || seen.has(trimmed)) continue;
|
|
66
|
+
seen.add(trimmed);
|
|
67
|
+
unique.push(trimmed);
|
|
68
|
+
if (unique.length >= HTML_REFERENCE_LIMIT) break;
|
|
69
|
+
}
|
|
70
|
+
return unique;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function extractHtmlNodeIds(html: string): string[] {
|
|
74
|
+
const ids: string[] = [];
|
|
75
|
+
for (const match of html.matchAll(/\b(?:node|graph|json-render|web-artifact|mcp-app|group)-[a-z0-9-]+\b/gi)) {
|
|
76
|
+
ids.push(match[0]);
|
|
77
|
+
}
|
|
78
|
+
return uniqueLimited(ids);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function extractHtmlUrls(html: string): string[] {
|
|
82
|
+
const urls: string[] = [];
|
|
83
|
+
for (const match of html.matchAll(/\b(?:src|href)\s*=\s*["']([^"']+)["']/gi)) {
|
|
84
|
+
const url = match[1]?.trim();
|
|
85
|
+
if (!url) continue;
|
|
86
|
+
if (/^(?:https?:)?\/\//i.test(url) || url.startsWith('/') || url.startsWith('ui://')) {
|
|
87
|
+
urls.push(url);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return uniqueLimited(urls);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function joinSummaryParts(parts: string[]): string | null {
|
|
94
|
+
const summary = parts
|
|
95
|
+
.map((part) => part.trim())
|
|
96
|
+
.filter(Boolean)
|
|
97
|
+
.filter((part, index, all) => all.findIndex((candidate) => candidate === part) === index)
|
|
98
|
+
.join('\n');
|
|
99
|
+
return summary ? truncateText(summary, HTML_AGENT_SUMMARY_MAX_LENGTH) : null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function normalizeHtmlNodeSemanticData<T extends Record<string, unknown>>(data: T): T {
|
|
103
|
+
const {
|
|
104
|
+
agentSummary: _agentSummary,
|
|
105
|
+
contentSummary: _contentSummary,
|
|
106
|
+
embeddedNodeIds: _embeddedNodeIds,
|
|
107
|
+
embeddedUrls: _embeddedUrls,
|
|
108
|
+
embeddedNodeId: _embeddedNodeId,
|
|
109
|
+
embeddedGraphId: _embeddedGraphId,
|
|
110
|
+
sourceNodeId: _sourceNodeId,
|
|
111
|
+
...base
|
|
112
|
+
} = data;
|
|
113
|
+
|
|
114
|
+
const html = pickString(base.html);
|
|
115
|
+
const explicitSummary = pickString(base.summary) ?? pickString(base.description);
|
|
116
|
+
const primitive = pickString(base.htmlPrimitive);
|
|
117
|
+
const contentSummary = html ? summarizeHtmlText(html) : null;
|
|
118
|
+
const explicitNodeIds = [
|
|
119
|
+
...strings(data.embeddedNodeIds),
|
|
120
|
+
pickString(data.embeddedNodeId),
|
|
121
|
+
pickString(data.embeddedGraphId),
|
|
122
|
+
pickString(data.sourceNodeId),
|
|
123
|
+
].filter((value): value is string => value !== null);
|
|
124
|
+
const embeddedNodeIds = uniqueLimited([...explicitNodeIds, ...(html ? extractHtmlNodeIds(html) : [])]);
|
|
125
|
+
const embeddedUrls = uniqueLimited([...strings(data.embeddedUrls), ...(html ? extractHtmlUrls(html) : [])]);
|
|
126
|
+
const agentSummary = pickString(data.agentSummary) ?? joinSummaryParts([
|
|
127
|
+
primitive ? `HTML primitive: ${primitive}` : '',
|
|
128
|
+
explicitSummary ?? '',
|
|
129
|
+
contentSummary ?? '',
|
|
130
|
+
embeddedNodeIds.length > 0 ? `Embedded canvas nodes: ${embeddedNodeIds.join(', ')}` : '',
|
|
131
|
+
embeddedUrls.length > 0 ? `Embedded URLs: ${embeddedUrls.join(', ')}` : '',
|
|
132
|
+
]);
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
...base,
|
|
136
|
+
...(contentSummary ? { contentSummary } : {}),
|
|
137
|
+
...(agentSummary ? { agentSummary } : {}),
|
|
138
|
+
...(embeddedNodeIds.length > 0 ? { embeddedNodeIds } : {}),
|
|
139
|
+
...(embeddedUrls.length > 0 ? { embeddedUrls } : {}),
|
|
140
|
+
} as T;
|
|
141
|
+
}
|
|
@@ -10,6 +10,7 @@ export const HTML_PRIMITIVE_KINDS = [
|
|
|
10
10
|
'interaction-prototype',
|
|
11
11
|
'flowchart',
|
|
12
12
|
'deck',
|
|
13
|
+
'presentation',
|
|
13
14
|
'illustration-set',
|
|
14
15
|
'explainer',
|
|
15
16
|
'status-report',
|
|
@@ -46,6 +47,14 @@ export interface HtmlPrimitiveBuildResult {
|
|
|
46
47
|
data: Record<string, unknown>;
|
|
47
48
|
}
|
|
48
49
|
|
|
50
|
+
export interface HtmlPrimitiveSemanticMetadata {
|
|
51
|
+
presentation?: true;
|
|
52
|
+
slideCount?: number;
|
|
53
|
+
slideTitles?: string[];
|
|
54
|
+
speakerNotes?: string[];
|
|
55
|
+
presentationTheme?: string | Record<string, string>;
|
|
56
|
+
}
|
|
57
|
+
|
|
49
58
|
type PrimitiveRenderer = (input: { title: string; data: Record<string, unknown>; descriptor: HtmlPrimitiveDescriptor }) => string;
|
|
50
59
|
|
|
51
60
|
const DESCRIPTORS: HtmlPrimitiveDescriptor[] = [
|
|
@@ -222,6 +231,25 @@ const DESCRIPTORS: HtmlPrimitiveDescriptor[] = [
|
|
|
222
231
|
},
|
|
223
232
|
},
|
|
224
233
|
},
|
|
234
|
+
{
|
|
235
|
+
kind: 'presentation',
|
|
236
|
+
title: 'HTML Presentation',
|
|
237
|
+
description: 'PowerPoint-style fullscreen-ready HTML presentation with slide navigation, progress, speaker notes, and presentation metadata.',
|
|
238
|
+
useWhen: 'Use when the human asks for a presentation, pitch deck, briefing, workshop walkthrough, or PowerPoint-like deliverable.',
|
|
239
|
+
defaultSize: { width: 1120, height: 700 },
|
|
240
|
+
dataShape: '{ subtitle?, theme?: "canvas"|"midnight"|"paper"|"aurora"|{ bg?, panel?, surface?, border?, text?, textSecondary?, textMuted?, accent? }, slides: [{ title, kicker?, body?, bullets?: string[], metrics?: [{ label, value, detail? }], note? }] }',
|
|
241
|
+
example: {
|
|
242
|
+
kind: 'presentation',
|
|
243
|
+
title: 'Project Briefing',
|
|
244
|
+
data: {
|
|
245
|
+
subtitle: 'A meeting-ready narrative for review.',
|
|
246
|
+
slides: [
|
|
247
|
+
{ title: 'Why this matters', kicker: '01', body: 'Frame the decision and outcome.', bullets: ['Human-readable', 'Fullscreen-ready'] },
|
|
248
|
+
{ title: 'What changes', kicker: '02', bullets: ['Show the before/after', 'End with clear next steps'] },
|
|
249
|
+
],
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
},
|
|
225
253
|
{
|
|
226
254
|
kind: 'illustration-set',
|
|
227
255
|
title: 'Illustration Set',
|
|
@@ -358,6 +386,43 @@ function fieldRecords(data: Record<string, unknown>, key: string, fallback: Reco
|
|
|
358
386
|
return found.length > 0 ? found : fallback;
|
|
359
387
|
}
|
|
360
388
|
|
|
389
|
+
const DEFAULT_DECK_SLIDES: Record<string, unknown>[] = [
|
|
390
|
+
{ title: 'HTML keeps humans in the loop', kicker: 'Thesis', bullets: ['Higher information density', 'Visual clarity', 'Two-way interaction'] },
|
|
391
|
+
{ title: 'Use the lightest tier that works', bullets: ['json-render for structured UI', 'html primitives for rich documents', 'web artifacts for full React apps'] },
|
|
392
|
+
];
|
|
393
|
+
|
|
394
|
+
const DEFAULT_PRESENTATION_SLIDES: Record<string, unknown>[] = [
|
|
395
|
+
{ title: 'Set the frame', kicker: '01', body: 'Open with the decision, audience, and outcome this presentation supports.' },
|
|
396
|
+
{ title: 'Show the evidence', kicker: '02', bullets: ['Use concrete facts', 'Keep one idea per slide', 'Make risks visible'] },
|
|
397
|
+
{ title: 'Close with action', kicker: '03', bullets: ['Decision needed', 'Owner and next step', 'Timing'] },
|
|
398
|
+
];
|
|
399
|
+
|
|
400
|
+
function presentationSlides(data: Record<string, unknown>, fallback = DEFAULT_PRESENTATION_SLIDES): Record<string, unknown>[] {
|
|
401
|
+
return fieldRecords(data, 'slides', fallback);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function enrichPresentationData(
|
|
405
|
+
kind: HtmlPrimitiveKind,
|
|
406
|
+
data: Record<string, unknown>,
|
|
407
|
+
): Record<string, unknown> {
|
|
408
|
+
if (kind !== 'deck' && kind !== 'presentation') return data;
|
|
409
|
+
const slides = presentationSlides(data, kind === 'deck' ? DEFAULT_DECK_SLIDES : DEFAULT_PRESENTATION_SLIDES);
|
|
410
|
+
const slideTitles = slides.map((slide, index) => itemTitle(slide, `Slide ${index + 1}`));
|
|
411
|
+
const speakerNotes = slides
|
|
412
|
+
.map((slide) => text(slide.note).trim())
|
|
413
|
+
.filter(Boolean);
|
|
414
|
+
const theme = kind === 'presentation' ? presentationThemeMetadata(data) : undefined;
|
|
415
|
+
return {
|
|
416
|
+
...data,
|
|
417
|
+
slides,
|
|
418
|
+
presentation: true,
|
|
419
|
+
slideCount: slides.length,
|
|
420
|
+
slideTitles,
|
|
421
|
+
...(speakerNotes.length > 0 ? { speakerNotes } : {}),
|
|
422
|
+
...(theme !== undefined ? { presentationTheme: theme } : {}),
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
361
426
|
function fieldStrings(data: Record<string, unknown>, key: string, fallback: string[]): string[] {
|
|
362
427
|
const found = strings(data[key]);
|
|
363
428
|
return found.length > 0 ? found : fallback;
|
|
@@ -374,11 +439,120 @@ function escapeHtml(value: string): string {
|
|
|
374
439
|
|
|
375
440
|
function safeCssColor(value: string): string {
|
|
376
441
|
const trimmed = value.trim();
|
|
442
|
+
if (/^var\(--[a-z0-9-]+\)$/i.test(trimmed)) return trimmed;
|
|
377
443
|
if (/^#[0-9a-f]{3,8}$/i.test(trimmed)) return trimmed;
|
|
378
444
|
if (/^(?:rgb|hsl)a?\([\d\s.,%+-]+\)$/i.test(trimmed)) return trimmed;
|
|
379
445
|
return 'transparent';
|
|
380
446
|
}
|
|
381
447
|
|
|
448
|
+
type PresentationThemeName = 'canvas' | 'midnight' | 'paper' | 'aurora';
|
|
449
|
+
|
|
450
|
+
interface PresentationThemeTokens {
|
|
451
|
+
name: PresentationThemeName | 'custom';
|
|
452
|
+
bg: string;
|
|
453
|
+
panel: string;
|
|
454
|
+
surface: string;
|
|
455
|
+
border: string;
|
|
456
|
+
text: string;
|
|
457
|
+
textSecondary: string;
|
|
458
|
+
textMuted: string;
|
|
459
|
+
accent: string;
|
|
460
|
+
colorScheme: string;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const PRESENTATION_THEMES: Record<PresentationThemeName, PresentationThemeTokens> = {
|
|
464
|
+
canvas: {
|
|
465
|
+
name: 'canvas',
|
|
466
|
+
bg: 'var(--color-bg, #081524)',
|
|
467
|
+
panel: 'var(--color-panel, #0f1d31)',
|
|
468
|
+
surface: 'var(--color-surface, #10213a)',
|
|
469
|
+
border: 'var(--color-border, #1b2c44)',
|
|
470
|
+
text: 'var(--color-text, #e6eef7)',
|
|
471
|
+
textSecondary: 'var(--color-text-secondary, #c7d3ea)',
|
|
472
|
+
textMuted: 'var(--color-text-muted, #8ea3bd)',
|
|
473
|
+
accent: 'var(--color-accent, #4BBCFF)',
|
|
474
|
+
colorScheme: 'dark light',
|
|
475
|
+
},
|
|
476
|
+
midnight: {
|
|
477
|
+
name: 'midnight',
|
|
478
|
+
bg: '#081524',
|
|
479
|
+
panel: '#0f1d31',
|
|
480
|
+
surface: '#10213a',
|
|
481
|
+
border: '#1b2c44',
|
|
482
|
+
text: '#e6eef7',
|
|
483
|
+
textSecondary: '#c7d3ea',
|
|
484
|
+
textMuted: '#8ea3bd',
|
|
485
|
+
accent: '#4BBCFF',
|
|
486
|
+
colorScheme: 'dark',
|
|
487
|
+
},
|
|
488
|
+
paper: {
|
|
489
|
+
name: 'paper',
|
|
490
|
+
bg: '#F4EFE6',
|
|
491
|
+
panel: '#EFE7D4',
|
|
492
|
+
surface: '#FAF6EE',
|
|
493
|
+
border: '#D6CBB4',
|
|
494
|
+
text: '#081524',
|
|
495
|
+
textSecondary: '#3d4d63',
|
|
496
|
+
textMuted: '#5c6b80',
|
|
497
|
+
accent: '#1A7ABF',
|
|
498
|
+
colorScheme: 'light',
|
|
499
|
+
},
|
|
500
|
+
aurora: {
|
|
501
|
+
name: 'aurora',
|
|
502
|
+
bg: '#090f1f',
|
|
503
|
+
panel: '#101a32',
|
|
504
|
+
surface: '#12263b',
|
|
505
|
+
border: '#24415f',
|
|
506
|
+
text: '#f5fbff',
|
|
507
|
+
textSecondary: '#d5e8f7',
|
|
508
|
+
textMuted: '#95adc2',
|
|
509
|
+
accent: '#8cffd2',
|
|
510
|
+
colorScheme: 'dark',
|
|
511
|
+
},
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
function presentationTheme(data: Record<string, unknown>): PresentationThemeTokens {
|
|
515
|
+
const raw = data.theme ?? data.presentationTheme;
|
|
516
|
+
if (typeof raw === 'string') {
|
|
517
|
+
return PRESENTATION_THEMES[raw as PresentationThemeName] ?? PRESENTATION_THEMES.canvas;
|
|
518
|
+
}
|
|
519
|
+
if (!isRecord(raw)) return PRESENTATION_THEMES.canvas;
|
|
520
|
+
const baseName = typeof raw.base === 'string' && raw.base in PRESENTATION_THEMES
|
|
521
|
+
? raw.base as PresentationThemeName
|
|
522
|
+
: 'canvas';
|
|
523
|
+
const base = PRESENTATION_THEMES[baseName];
|
|
524
|
+
const readColor = (key: string, fallback: string): string => {
|
|
525
|
+
const value = text(raw[key]);
|
|
526
|
+
if (!value) return fallback;
|
|
527
|
+
const color = safeCssColor(value);
|
|
528
|
+
return color === 'transparent' ? fallback : color;
|
|
529
|
+
};
|
|
530
|
+
const colorScheme = raw.colorScheme === 'light' ? 'light' : raw.colorScheme === 'dark' ? 'dark' : base.colorScheme;
|
|
531
|
+
return {
|
|
532
|
+
name: 'custom',
|
|
533
|
+
bg: readColor('bg', base.bg),
|
|
534
|
+
panel: readColor('panel', base.panel),
|
|
535
|
+
surface: readColor('surface', base.surface),
|
|
536
|
+
border: readColor('border', base.border),
|
|
537
|
+
text: readColor('text', base.text),
|
|
538
|
+
textSecondary: readColor('textSecondary', base.textSecondary),
|
|
539
|
+
textMuted: readColor('textMuted', base.textMuted),
|
|
540
|
+
accent: readColor('accent', base.accent),
|
|
541
|
+
colorScheme,
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function presentationThemeMetadata(data: Record<string, unknown>): string | Record<string, string> | undefined {
|
|
546
|
+
const raw = data.theme ?? data.presentationTheme;
|
|
547
|
+
if (typeof raw === 'string') return raw;
|
|
548
|
+
if (!isRecord(raw)) return undefined;
|
|
549
|
+
const result: Record<string, string> = {};
|
|
550
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
551
|
+
if (typeof value === 'string') result[key] = value;
|
|
552
|
+
}
|
|
553
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
554
|
+
}
|
|
555
|
+
|
|
382
556
|
function number(value: unknown, fallback = 0): number {
|
|
383
557
|
return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
|
384
558
|
}
|
|
@@ -760,10 +934,7 @@ document.querySelectorAll('[data-copy-svg]').forEach((button) => button.addEvent
|
|
|
760
934
|
}
|
|
761
935
|
|
|
762
936
|
function renderDeck({ title, data, descriptor }: Parameters<PrimitiveRenderer>[0]): string {
|
|
763
|
-
const slides =
|
|
764
|
-
{ title: 'HTML keeps humans in the loop', kicker: 'Thesis', bullets: ['Higher information density', 'Visual clarity', 'Two-way interaction'] },
|
|
765
|
-
{ title: 'Use the lightest tier that works', bullets: ['json-render for structured UI', 'html primitives for rich documents', 'web artifacts for full React apps'] },
|
|
766
|
-
]);
|
|
937
|
+
const slides = presentationSlides(data, DEFAULT_DECK_SLIDES);
|
|
767
938
|
const body = `<section class="panel"><div class="small"><span id="slide-count">1</span> / ${slides.length} - use left/right arrows</div>${slides.map((item, index) => `<article class="slide ${index === 0 ? 'active' : ''}" data-slide="${index}"><div><div class="kicker">${escapeHtml(text(item.kicker, `Slide ${index + 1}`))}</div><h2>${escapeHtml(itemTitle(item, 'Slide'))}</h2><p>${escapeHtml(text(item.body, ''))}</p>${list(strings(item.bullets))}<p class="small">${escapeHtml(text(item.note, ''))}</p></div></article>`).join('')}</section>`;
|
|
768
939
|
return page({
|
|
769
940
|
title,
|
|
@@ -780,12 +951,132 @@ function showSlide(index) {
|
|
|
780
951
|
document.getElementById('slide-count').textContent = String(currentSlide + 1);
|
|
781
952
|
}
|
|
782
953
|
document.addEventListener('keydown', (event) => {
|
|
783
|
-
if (event.key === 'ArrowRight') showSlide(currentSlide + 1);
|
|
784
|
-
if (event.key === 'ArrowLeft') showSlide(currentSlide - 1);
|
|
954
|
+
if (event.key === 'ArrowRight' || event.key === 'PageDown' || event.key === ' ') showSlide(currentSlide + 1);
|
|
955
|
+
if (event.key === 'ArrowLeft' || event.key === 'PageUp') showSlide(currentSlide - 1);
|
|
785
956
|
});`,
|
|
786
957
|
});
|
|
787
958
|
}
|
|
788
959
|
|
|
960
|
+
function renderPresentation({ title, data, descriptor }: Parameters<PrimitiveRenderer>[0]): string {
|
|
961
|
+
const slides = presentationSlides(data);
|
|
962
|
+
const subtitle = text(data.subtitle, descriptor.description);
|
|
963
|
+
const theme = presentationTheme(data);
|
|
964
|
+
const accentOverride = safeCssColor(text(data.accent, ''));
|
|
965
|
+
const accent = accentOverride === 'transparent' ? theme.accent : accentOverride;
|
|
966
|
+
const slideMarkup = slides.map((item, index) => {
|
|
967
|
+
const metrics = records(item.metrics);
|
|
968
|
+
return `<article class="slide ${index === 0 ? 'active' : ''}" data-slide="${index}">
|
|
969
|
+
<div class="slide-grid ${metrics.length > 0 ? 'with-metrics' : 'without-metrics'}">
|
|
970
|
+
<div class="slide-copy">
|
|
971
|
+
<div class="kicker">${escapeHtml(text(item.kicker, `Slide ${index + 1}`))}</div>
|
|
972
|
+
<h2>${escapeHtml(itemTitle(item, 'Slide'))}</h2>
|
|
973
|
+
${text(item.body) ? `<p class="lede">${escapeHtml(text(item.body))}</p>` : ''}
|
|
974
|
+
${list(strings(item.bullets))}
|
|
975
|
+
</div>
|
|
976
|
+
${metrics.length > 0 ? `<div class="metrics">${metrics.map((metric) => `<div class="metric"><span>${escapeHtml(text(metric.label, 'Metric'))}</span><strong>${escapeHtml(text(metric.value, '0'))}</strong>${text(metric.detail) ? `<p>${escapeHtml(text(metric.detail))}</p>` : ''}</div>`).join('')}</div>` : ''}
|
|
977
|
+
</div>
|
|
978
|
+
${text(item.note) ? `<aside class="speaker-note"><span>Speaker note</span>${escapeHtml(text(item.note))}</aside>` : ''}
|
|
979
|
+
</article>`;
|
|
980
|
+
}).join('');
|
|
981
|
+
|
|
982
|
+
return `<!doctype html>
|
|
983
|
+
<html lang="en">
|
|
984
|
+
<head>
|
|
985
|
+
<meta charset="utf-8">
|
|
986
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
987
|
+
<title>${escapeHtml(title)}</title>
|
|
988
|
+
<style>
|
|
989
|
+
:root { color-scheme: ${theme.colorScheme}; --deck-accent: ${accent}; --deck-bg: ${theme.bg}; --deck-panel: ${theme.panel}; --deck-surface: ${theme.surface}; --deck-border: ${theme.border}; --deck-text: ${theme.text}; --deck-text-secondary: ${theme.textSecondary}; --deck-text-muted: ${theme.textMuted}; }
|
|
990
|
+
* { box-sizing: border-box; }
|
|
991
|
+
html, body { width: 100%; height: 100%; overflow: hidden; }
|
|
992
|
+
body { margin: 0; padding: 0; background: var(--deck-bg); color: var(--deck-text); font-family: var(--font-sans, ui-sans-serif, system-ui, sans-serif); }
|
|
993
|
+
.deck { height: 100vh; min-height: 0; display: grid; grid-template-rows: auto minmax(0, 1fr) auto; background: radial-gradient(circle at 10% 10%, color-mix(in srgb, var(--deck-accent) 32%, transparent), transparent 28rem), linear-gradient(135deg, color-mix(in srgb, var(--deck-panel) 88%, black), var(--deck-bg)); }
|
|
994
|
+
.topbar, .bottombar { display: flex; align-items: center; justify-content: space-between; gap: 14px; padding: clamp(12px, 2.5vmin, 24px) clamp(18px, 4vw, 48px); color: var(--deck-text-secondary); }
|
|
995
|
+
.brand { display: grid; gap: 2px; }
|
|
996
|
+
.brand p { margin: 0; }
|
|
997
|
+
.eyebrow { color: var(--deck-accent); font-size: 11px; font-weight: 900; letter-spacing: .18em; text-transform: uppercase; }
|
|
998
|
+
.title { max-width: 70vw; overflow: hidden; color: var(--deck-text); font-size: clamp(16px, 2vw, 24px); font-weight: 850; letter-spacing: -.03em; text-overflow: ellipsis; white-space: nowrap; }
|
|
999
|
+
.controls { display: flex; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }
|
|
1000
|
+
button { border: 1px solid color-mix(in srgb, var(--deck-border) 82%, white); background: color-mix(in srgb, var(--deck-panel) 82%, transparent); color: var(--deck-text); border-radius: 999px; padding: 8px 12px; font: 700 12px/1 var(--font-sans, ui-sans-serif, system-ui, sans-serif); cursor: pointer; }
|
|
1001
|
+
button:hover, button.active { border-color: var(--deck-accent); color: var(--deck-text); background: color-mix(in srgb, var(--deck-accent) 18%, var(--deck-panel)); }
|
|
1002
|
+
.slides { min-height: 0; overflow-x: hidden; overflow-y: auto; overscroll-behavior: contain; scrollbar-gutter: stable; }
|
|
1003
|
+
.slide { display: none; min-height: 100%; align-content: center; gap: clamp(16px, 3vmin, 28px); padding: clamp(24px, 5vmin, 64px) clamp(28px, 6vw, 92px); }
|
|
1004
|
+
.slide.active { display: grid; }
|
|
1005
|
+
.slide-grid { display: grid; gap: clamp(24px, 5vw, 72px); align-items: center; }
|
|
1006
|
+
.slide-grid.with-metrics { grid-template-columns: minmax(0, 1.15fr) minmax(260px, .85fr); }
|
|
1007
|
+
.slide-grid.without-metrics { grid-template-columns: minmax(0, 1fr); }
|
|
1008
|
+
.slide-copy { max-width: min(1120px, 100%); }
|
|
1009
|
+
.kicker { color: var(--deck-accent); font-size: clamp(12px, 1.8vw, 18px); font-weight: 950; letter-spacing: .18em; text-transform: uppercase; }
|
|
1010
|
+
h2 { margin: 10px 0 18px; max-width: 16ch; font-size: clamp(40px, 8vmin, 104px); line-height: .9; letter-spacing: -.07em; }
|
|
1011
|
+
.lede { max-width: 900px; margin: 0 0 20px; color: var(--deck-text-secondary); font-size: clamp(18px, 3vmin, 32px); line-height: 1.14; letter-spacing: -.03em; }
|
|
1012
|
+
ul { display: grid; gap: 12px; max-width: 780px; margin: 0; padding: 0; list-style: none; }
|
|
1013
|
+
li { position: relative; padding-left: 30px; color: var(--deck-text-secondary); font-size: clamp(17px, 2.2vmin, 25px); line-height: 1.22; }
|
|
1014
|
+
li::before { content: ''; position: absolute; left: 0; top: .42em; width: 12px; height: 12px; border-radius: 50%; background: var(--deck-accent); box-shadow: 0 0 24px color-mix(in srgb, var(--deck-accent) 60%, transparent); }
|
|
1015
|
+
.metrics { display: grid; gap: 14px; }
|
|
1016
|
+
.metric { border: 1px solid color-mix(in srgb, var(--deck-accent) 42%, var(--deck-border)); border-radius: 28px; padding: 22px; background: color-mix(in srgb, var(--deck-panel) 78%, transparent); box-shadow: 0 24px 70px rgba(0,0,0,.26); }
|
|
1017
|
+
.metric span { color: var(--deck-text-muted); font-size: 11px; font-weight: 900; letter-spacing: .14em; text-transform: uppercase; }
|
|
1018
|
+
.metric strong { display: block; margin-top: 8px; font-size: clamp(34px, 6vw, 78px); line-height: .9; letter-spacing: -.06em; }
|
|
1019
|
+
.metric p { margin: 10px 0 0; color: var(--deck-text-secondary); }
|
|
1020
|
+
.speaker-note { max-width: min(1120px, 100%); border-left: 4px solid var(--deck-accent); padding: 10px 14px; color: var(--deck-text-muted); background: color-mix(in srgb, var(--deck-panel) 76%, transparent); border-radius: 14px; }
|
|
1021
|
+
.speaker-note span { display: block; margin-bottom: 2px; color: var(--deck-accent); font-size: 10px; font-weight: 900; letter-spacing: .12em; text-transform: uppercase; }
|
|
1022
|
+
.dots { display: flex; gap: 7px; align-items: center; }
|
|
1023
|
+
.dot { width: 30px; height: 7px; border: 0; border-radius: 999px; padding: 0; background: color-mix(in srgb, var(--deck-text) 24%, transparent); }
|
|
1024
|
+
.dot.active { width: 54px; background: var(--deck-accent); }
|
|
1025
|
+
.hint { font-size: 12px; color: var(--deck-text-muted); }
|
|
1026
|
+
html[data-pmx-presentation-mode="present"] .hint { display: none; }
|
|
1027
|
+
.progress { height: 3px; width: 180px; overflow: hidden; border-radius: 999px; background: color-mix(in srgb, var(--deck-text) 18%, transparent); }
|
|
1028
|
+
.progress span { display: block; height: 100%; width: 0; background: var(--deck-accent); transition: width .2s ease; }
|
|
1029
|
+
@media (max-width: 820px) { .slide-grid { grid-template-columns: 1fr; } .metrics { grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); } h2 { max-width: none; font-size: clamp(42px, 14vw, 78px); } .lede, li { font-size: 21px; } .topbar { align-items: flex-start; flex-direction: column; } .title { max-width: 100%; } }
|
|
1030
|
+
</style>
|
|
1031
|
+
</head>
|
|
1032
|
+
<body>
|
|
1033
|
+
<main class="deck">
|
|
1034
|
+
<header class="topbar">
|
|
1035
|
+
<div class="brand"><div class="eyebrow">PMX presentation</div><div class="title">${escapeHtml(title)}</div><p>${escapeHtml(subtitle)}</p></div>
|
|
1036
|
+
</header>
|
|
1037
|
+
<section class="slides">${slideMarkup}</section>
|
|
1038
|
+
<footer class="bottombar">
|
|
1039
|
+
<div class="dots">${slides.map((_, index) => `<button class="dot ${index === 0 ? 'active' : ''}" type="button" data-dot="${index}" aria-label="Go to slide ${index + 1}"></button>`).join('')}</div>
|
|
1040
|
+
<div class="hint"><span id="slide-current">1</span> / ${slides.length} - Arrow keys, Space, Page Up/Down</div>
|
|
1041
|
+
<div class="progress" aria-hidden="true"><span id="slide-progress"></span></div>
|
|
1042
|
+
</footer>
|
|
1043
|
+
</main>
|
|
1044
|
+
<script type="application/json" id="pmx-data">${safeJson(data)}</script>
|
|
1045
|
+
<script>
|
|
1046
|
+
let currentSlide = 0;
|
|
1047
|
+
const slides = Array.from(document.querySelectorAll('[data-slide]'));
|
|
1048
|
+
const dots = Array.from(document.querySelectorAll('[data-dot]'));
|
|
1049
|
+
function showSlide(index) {
|
|
1050
|
+
currentSlide = Math.max(0, Math.min(slides.length - 1, index));
|
|
1051
|
+
slides.forEach((slide, i) => slide.classList.toggle('active', i === currentSlide));
|
|
1052
|
+
dots.forEach((dot, i) => dot.classList.toggle('active', i === currentSlide));
|
|
1053
|
+
document.getElementById('slide-current').textContent = String(currentSlide + 1);
|
|
1054
|
+
document.getElementById('slide-progress').style.width = String(((currentSlide + 1) / slides.length) * 100) + '%';
|
|
1055
|
+
}
|
|
1056
|
+
dots.forEach((dot) => dot.addEventListener('click', () => showSlide(Number(dot.getAttribute('data-dot')))));
|
|
1057
|
+
function handlePresentationKey(key) {
|
|
1058
|
+
if (key === 'ArrowRight' || key === 'PageDown' || key === ' ') { showSlide(currentSlide + 1); return true; }
|
|
1059
|
+
if (key === 'ArrowLeft' || key === 'PageUp') { showSlide(currentSlide - 1); return true; }
|
|
1060
|
+
if (key === 'Home') { showSlide(0); return true; }
|
|
1061
|
+
if (key === 'End') { showSlide(slides.length - 1); return true; }
|
|
1062
|
+
return false;
|
|
1063
|
+
}
|
|
1064
|
+
window.PMX_CANVAS_PRESENTATION_HANDLE_KEY = handlePresentationKey;
|
|
1065
|
+
document.addEventListener('pmx-presentation-key', (event) => {
|
|
1066
|
+
if (!event.detail || typeof event.detail.key !== 'string') return;
|
|
1067
|
+
handlePresentationKey(event.detail.key);
|
|
1068
|
+
});
|
|
1069
|
+
document.addEventListener('keydown', (event) => {
|
|
1070
|
+
const tag = event.target && event.target.tagName;
|
|
1071
|
+
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return;
|
|
1072
|
+
if (handlePresentationKey(event.key)) event.preventDefault();
|
|
1073
|
+
});
|
|
1074
|
+
showSlide(0);
|
|
1075
|
+
</script>
|
|
1076
|
+
</body>
|
|
1077
|
+
</html>`;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
789
1080
|
function renderExplainer({ title, data, descriptor }: Parameters<PrimitiveRenderer>[0]): string {
|
|
790
1081
|
const steps = fieldRecords(data, 'steps', [{ title: 'Start here', detail: 'Add request path, data flow, or concept steps.' }]);
|
|
791
1082
|
const snippets = fieldRecords(data, 'snippets', []);
|
|
@@ -952,6 +1243,7 @@ const RENDERERS: Record<HtmlPrimitiveKind, PrimitiveRenderer> = {
|
|
|
952
1243
|
flowchart: renderFlowchart,
|
|
953
1244
|
'illustration-set': renderIllustrationSet,
|
|
954
1245
|
deck: renderDeck,
|
|
1246
|
+
presentation: renderPresentation,
|
|
955
1247
|
explainer: renderExplainer,
|
|
956
1248
|
'status-report': renderStatusReport,
|
|
957
1249
|
'incident-report': renderIncidentReport,
|
|
@@ -974,16 +1266,34 @@ export function listHtmlPrimitiveDescriptors(): HtmlPrimitiveDescriptor[] {
|
|
|
974
1266
|
return JSON.parse(JSON.stringify(DESCRIPTORS)) as HtmlPrimitiveDescriptor[];
|
|
975
1267
|
}
|
|
976
1268
|
|
|
1269
|
+
export function getHtmlPrimitiveSemanticMetadata(data: Record<string, unknown>): HtmlPrimitiveSemanticMetadata {
|
|
1270
|
+
if (data.presentation !== true) return {};
|
|
1271
|
+
const slideTitles = strings(data.slideTitles);
|
|
1272
|
+
const speakerNotes = strings(data.speakerNotes);
|
|
1273
|
+
const theme = presentationThemeMetadata(data);
|
|
1274
|
+
return {
|
|
1275
|
+
presentation: true,
|
|
1276
|
+
...(typeof data.slideCount === 'number' && Number.isFinite(data.slideCount) ? { slideCount: data.slideCount } : {}),
|
|
1277
|
+
...(slideTitles.length > 0 ? { slideTitles } : {}),
|
|
1278
|
+
...(speakerNotes.length > 0 ? { speakerNotes } : {}),
|
|
1279
|
+
...(theme !== undefined ? { presentationTheme: theme } : {}),
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
|
|
977
1283
|
export function buildHtmlPrimitive(input: HtmlPrimitiveInput): HtmlPrimitiveBuildResult {
|
|
978
1284
|
const descriptor = getHtmlPrimitiveDescriptor(input.kind);
|
|
979
1285
|
const title = input.title ?? descriptor.title;
|
|
980
|
-
const data = input.data ?? {};
|
|
1286
|
+
const data = enrichPresentationData(input.kind, input.data ?? {});
|
|
981
1287
|
const renderer = RENDERERS[input.kind];
|
|
1288
|
+
const slideTitles = strings(data.slideTitles);
|
|
1289
|
+
const summary = slideTitles.length > 0
|
|
1290
|
+
? `${descriptor.description} Slides: ${slideTitles.join(', ')}.`
|
|
1291
|
+
: descriptor.description;
|
|
982
1292
|
return {
|
|
983
1293
|
kind: input.kind,
|
|
984
1294
|
title,
|
|
985
1295
|
html: renderer({ title, data, descriptor }),
|
|
986
|
-
summary
|
|
1296
|
+
summary,
|
|
987
1297
|
defaultSize: descriptor.defaultSize,
|
|
988
1298
|
data,
|
|
989
1299
|
};
|
package/src/server/index.ts
CHANGED
|
@@ -40,7 +40,7 @@ import {
|
|
|
40
40
|
} from './canvas-operations.js';
|
|
41
41
|
import { validateCanvasLayout } from './canvas-validation.js';
|
|
42
42
|
import { describeCanvasSchema, validateStructuredCanvasPayload } from './canvas-schema.js';
|
|
43
|
-
import { buildHtmlPrimitive, isHtmlPrimitiveKind, listHtmlPrimitiveDescriptors } from './html-primitives.js';
|
|
43
|
+
import { buildHtmlPrimitive, getHtmlPrimitiveSemanticMetadata, isHtmlPrimitiveKind, listHtmlPrimitiveDescriptors } from './html-primitives.js';
|
|
44
44
|
import type { HtmlPrimitiveKind } from './html-primitives.js';
|
|
45
45
|
import {
|
|
46
46
|
buildWebArtifactOnCanvas,
|
|
@@ -647,6 +647,13 @@ export class PmxCanvas extends EventEmitter {
|
|
|
647
647
|
addHtmlNode(input: {
|
|
648
648
|
html: string;
|
|
649
649
|
title?: string;
|
|
650
|
+
summary?: string;
|
|
651
|
+
agentSummary?: string;
|
|
652
|
+
description?: string;
|
|
653
|
+
presentation?: boolean;
|
|
654
|
+
slideTitles?: string[];
|
|
655
|
+
embeddedNodeIds?: string[];
|
|
656
|
+
embeddedUrls?: string[];
|
|
650
657
|
x?: number;
|
|
651
658
|
y?: number;
|
|
652
659
|
width?: number;
|
|
@@ -656,7 +663,16 @@ export class PmxCanvas extends EventEmitter {
|
|
|
656
663
|
const { id } = addCanvasNode({
|
|
657
664
|
type: 'html',
|
|
658
665
|
...(typeof input.title === 'string' ? { title: input.title } : {}),
|
|
659
|
-
data: {
|
|
666
|
+
data: {
|
|
667
|
+
html: input.html,
|
|
668
|
+
...(typeof input.summary === 'string' ? { summary: input.summary } : {}),
|
|
669
|
+
...(typeof input.agentSummary === 'string' ? { agentSummary: input.agentSummary } : {}),
|
|
670
|
+
...(typeof input.description === 'string' ? { description: input.description } : {}),
|
|
671
|
+
...(input.presentation === true ? { presentation: true } : {}),
|
|
672
|
+
...(Array.isArray(input.slideTitles) ? { slideTitles: input.slideTitles } : {}),
|
|
673
|
+
...(Array.isArray(input.embeddedNodeIds) ? { embeddedNodeIds: input.embeddedNodeIds } : {}),
|
|
674
|
+
...(Array.isArray(input.embeddedUrls) ? { embeddedUrls: input.embeddedUrls } : {}),
|
|
675
|
+
},
|
|
660
676
|
...(typeof input.x === 'number' ? { x: input.x } : {}),
|
|
661
677
|
...(typeof input.y === 'number' ? { y: input.y } : {}),
|
|
662
678
|
...(typeof input.width === 'number' ? { width: input.width } : {}),
|
|
@@ -692,6 +708,9 @@ export class PmxCanvas extends EventEmitter {
|
|
|
692
708
|
htmlPrimitive: built.kind,
|
|
693
709
|
primitiveData: built.data,
|
|
694
710
|
description: built.summary,
|
|
711
|
+
agentSummary: typeof input.data?.agentSummary === 'string' ? input.data.agentSummary : built.summary,
|
|
712
|
+
...(typeof input.data?.summary === 'string' ? { summary: input.data.summary } : {}),
|
|
713
|
+
...getHtmlPrimitiveSemanticMetadata(built.data),
|
|
695
714
|
},
|
|
696
715
|
...(typeof input.x === 'number' ? { x: input.x } : {}),
|
|
697
716
|
...(typeof input.y === 'number' ? { y: input.y } : {}),
|
|
@@ -786,7 +805,7 @@ export type { SpatialCluster, SpatialContext, SpatialNeighbor, NodeSpatialInfo }
|
|
|
786
805
|
export { mutationHistory, diffLayouts, formatDiff } from './mutation-history.js';
|
|
787
806
|
export { recomputeCodeGraph, buildCodeGraphSummary, formatCodeGraph } from './code-graph.js';
|
|
788
807
|
export { describeCanvasSchema, validateStructuredCanvasPayload } from './canvas-schema.js';
|
|
789
|
-
export { buildHtmlPrimitive, isHtmlPrimitiveKind, listHtmlPrimitiveDescriptors } from './html-primitives.js';
|
|
808
|
+
export { buildHtmlPrimitive, getHtmlPrimitiveSemanticMetadata, isHtmlPrimitiveKind, listHtmlPrimitiveDescriptors } from './html-primitives.js';
|
|
790
809
|
export {
|
|
791
810
|
buildWebArtifactOnCanvas,
|
|
792
811
|
executeWebArtifactBuild,
|
package/src/server/server.ts
CHANGED
|
@@ -111,7 +111,7 @@ import {
|
|
|
111
111
|
} from './canvas-operations.js';
|
|
112
112
|
import { validateCanvasLayout } from './canvas-validation.js';
|
|
113
113
|
import { describeCanvasSchema, validateStructuredCanvasPayload } from './canvas-schema.js';
|
|
114
|
-
import { buildHtmlPrimitive, isHtmlPrimitiveKind } from './html-primitives.js';
|
|
114
|
+
import { buildHtmlPrimitive, getHtmlPrimitiveSemanticMetadata, isHtmlPrimitiveKind } from './html-primitives.js';
|
|
115
115
|
import {
|
|
116
116
|
EXCALIDRAW_READ_CHECKPOINT_TOOL,
|
|
117
117
|
EXCALIDRAW_SAVE_CHECKPOINT_TOOL,
|
|
@@ -1487,8 +1487,18 @@ async function handleCanvasAddNode(req: Request): Promise<Response> {
|
|
|
1487
1487
|
: body.content;
|
|
1488
1488
|
// For html nodes, accept top-level `html` field and merge into data so callers
|
|
1489
1489
|
// can POST { type: 'html', title, html } without nesting under `data`.
|
|
1490
|
-
const htmlMergedData = type === 'html'
|
|
1491
|
-
? {
|
|
1490
|
+
const htmlMergedData = type === 'html'
|
|
1491
|
+
? {
|
|
1492
|
+
...(extraData ?? {}),
|
|
1493
|
+
...(typeof body.html === 'string' ? { html: body.html } : {}),
|
|
1494
|
+
...(typeof body.summary === 'string' ? { summary: body.summary } : {}),
|
|
1495
|
+
...(typeof body.agentSummary === 'string' ? { agentSummary: body.agentSummary } : {}),
|
|
1496
|
+
...(typeof body.description === 'string' ? { description: body.description } : {}),
|
|
1497
|
+
...(body.presentation === true ? { presentation: true } : {}),
|
|
1498
|
+
...(Array.isArray(body.slideTitles) ? { slideTitles: body.slideTitles } : {}),
|
|
1499
|
+
...(Array.isArray(body.embeddedNodeIds) ? { embeddedNodeIds: body.embeddedNodeIds } : {}),
|
|
1500
|
+
...(Array.isArray(body.embeddedUrls) ? { embeddedUrls: body.embeddedUrls } : {}),
|
|
1501
|
+
}
|
|
1492
1502
|
: extraData;
|
|
1493
1503
|
let added: ReturnType<typeof addCanvasNode>;
|
|
1494
1504
|
const geometry = resolveCreateGeometry(body);
|
|
@@ -1497,7 +1507,7 @@ async function handleCanvasAddNode(req: Request): Promise<Response> {
|
|
|
1497
1507
|
type: type as CanvasNodeState['type'],
|
|
1498
1508
|
...(typeof body.title === 'string' ? { title: body.title } : {}),
|
|
1499
1509
|
...(typeof content === 'string' ? { content } : {}),
|
|
1500
|
-
...(htmlMergedData ? { data: htmlMergedData } : {}),
|
|
1510
|
+
...(htmlMergedData && Object.keys(htmlMergedData).length > 0 ? { data: htmlMergedData } : {}),
|
|
1501
1511
|
...(type === 'trace' && typeof body.toolName === 'string' ? { toolName: body.toolName } : {}),
|
|
1502
1512
|
...(type === 'trace' && typeof body.category === 'string' ? { category: body.category } : {}),
|
|
1503
1513
|
...(type === 'trace' && typeof body.status === 'string' ? { status: body.status } : {}),
|
|
@@ -1545,6 +1555,9 @@ function createCanvasHtmlPrimitiveNode(body: Record<string, unknown>): Response
|
|
|
1545
1555
|
htmlPrimitive: built.kind,
|
|
1546
1556
|
primitiveData: built.data,
|
|
1547
1557
|
description: built.summary,
|
|
1558
|
+
agentSummary: typeof data.agentSummary === 'string' ? data.agentSummary : built.summary,
|
|
1559
|
+
...(typeof data.summary === 'string' ? { summary: data.summary } : {}),
|
|
1560
|
+
...getHtmlPrimitiveSemanticMetadata(built.data),
|
|
1548
1561
|
},
|
|
1549
1562
|
...(body.strictSize === true ? { strictSize: true } : {}),
|
|
1550
1563
|
...geometry,
|