sitesmith: canvas summary serializer with unit tests
This commit is contained in:
24
craft/src/utils/canvas-summary.test.ts
Normal file
24
craft/src/utils/canvas-summary.test.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { describe, test, expect } from 'vitest';
|
||||||
|
import { summarizeCanvas } from './canvas-summary';
|
||||||
|
|
||||||
|
const fixture = {
|
||||||
|
ROOT: { type: { resolvedName: 'Container' }, props: { aiName: 'Page Root', node_id: 'ai-root-1' }, nodes: ['n1','n2'], parent: null },
|
||||||
|
n1: { type: { resolvedName: 'Heading' }, props: { aiName: 'Hero Title', node_id: 'ai-hero-1', text: 'Welcome', level: 1, style: { color: '#fff' } }, nodes: [], parent: 'ROOT' },
|
||||||
|
n2: { type: { resolvedName: 'HtmlBlock' }, props: { aiName: 'Custom Embed', node_id: 'ai-html-1', code: '<div>opaque</div>' }, nodes: [], parent: 'ROOT' },
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('summarizeCanvas', () => {
|
||||||
|
test('one line per node with id and aiName', () => {
|
||||||
|
const out = summarizeCanvas(fixture as any);
|
||||||
|
expect(out).toContain('Container id=ai-root-1');
|
||||||
|
expect(out).toContain('Heading id=ai-hero-1 name="Hero Title"');
|
||||||
|
});
|
||||||
|
test('excludes style props', () => {
|
||||||
|
expect(summarizeCanvas(fixture as any)).not.toContain('color=');
|
||||||
|
});
|
||||||
|
test('truncates to maxChars', () => {
|
||||||
|
const out = summarizeCanvas(fixture as any, 60);
|
||||||
|
expect(out.length).toBeLessThanOrEqual(60);
|
||||||
|
expect(out).toContain('truncated');
|
||||||
|
});
|
||||||
|
});
|
||||||
32
craft/src/utils/canvas-summary.ts
Normal file
32
craft/src/utils/canvas-summary.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { SerializedNodes } from '@craftjs/core';
|
||||||
|
|
||||||
|
export function summarizeCanvas(state: SerializedNodes, maxChars = 6000): string {
|
||||||
|
const root = state['ROOT'];
|
||||||
|
if (!root) return '(empty canvas)';
|
||||||
|
const lines: string[] = [];
|
||||||
|
const visit = (id: string, depth: number) => {
|
||||||
|
const node = state[id];
|
||||||
|
if (!node) return;
|
||||||
|
const indent = ' '.repeat(depth);
|
||||||
|
const type = typeof node.type === 'object' ? (node.type as any).resolvedName : String(node.type);
|
||||||
|
const props = node.props || {};
|
||||||
|
const aiName = (props as any).aiName ?? '';
|
||||||
|
const nodeId = (props as any).node_id ?? id;
|
||||||
|
const interesting: string[] = [];
|
||||||
|
for (const [k, v] of Object.entries(props)) {
|
||||||
|
if (k === 'aiName' || k === 'node_id' || k === 'style') continue;
|
||||||
|
if (v == null) continue;
|
||||||
|
const repr = typeof v === 'string' ? v : JSON.stringify(v);
|
||||||
|
const truncated = repr.length > 60 ? repr.slice(0, 57) + '…' : repr;
|
||||||
|
interesting.push(`${k}=${truncated}`);
|
||||||
|
if (interesting.length >= 3) break;
|
||||||
|
}
|
||||||
|
lines.push(`${indent}- ${type} id=${nodeId} name="${aiName}" {${interesting.join(', ')}}`);
|
||||||
|
if (type === 'HtmlBlock') return;
|
||||||
|
for (const childId of node.nodes || []) visit(childId, depth + 1);
|
||||||
|
};
|
||||||
|
visit('ROOT', 0);
|
||||||
|
let out = lines.join('\n');
|
||||||
|
if (out.length > maxChars) out = out.slice(0, maxChars - 30) + '\n… (truncated)';
|
||||||
|
return out;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user