SmartestHome/digest-engine/render/digest-canvas-sdk/render.js

183 lines
5.8 KiB
JavaScript

/*
* DigestRender — turns digest JSON into a canvas of DigestWindows.
*
* Everything in here is written around one requirement from the plan's testing
* checklist: "If a run's LLM output produces malformed canvas-SDK calls, does the
* digest fall back to plain text instead of a broken/blank page?" A local model
* emitting near-miss JSON four times a day is a routine event, not an exception,
* so every layer degrades instead of throwing:
*
* - unparseable input -> a <pre> dump of the raw text
* - a section that isn't a doc -> a <pre> dump of that section, others still render
* - a window that throws -> a <pre> dump of that window, siblings still render
* - a globe with no markers -> the globe still draws, just empty
*
* A blank page is the one outcome that must never happen.
*/
(function (global) {
'use strict';
function rawDump(container, value, note) {
var pre = document.createElement('pre');
pre.className = 'digest-canvas-raw';
var text = typeof value === 'string' ? value : safeStringify(value);
pre.textContent = (note ? note + '\n\n' : '') + text;
container.appendChild(pre);
return pre;
}
function safeStringify(value) {
try {
return JSON.stringify(value, null, 2);
} catch (err) {
return String(value);
}
}
function parse(input) {
if (typeof input !== 'string') { return input; }
try {
return JSON.parse(input);
} catch (err) {
return { __unparseable: input };
}
}
// Accepts what run.py writes ({sections: {compact: [...], full: [...]}}), a bare
// array of section documents, or a single section document.
function toSectionList(parsed, detailLevel) {
if (Array.isArray(parsed)) { return parsed; }
if (!parsed || typeof parsed !== 'object') { return []; }
if (parsed.sections && typeof parsed.sections === 'object') {
var sections = parsed.sections;
if (Array.isArray(sections)) { return sections; }
var picked = sections[detailLevel] || sections.full || sections.compact;
return Array.isArray(picked) ? picked : [];
}
if (parsed.windows) { return [parsed]; }
return [];
}
function renderGlobeWindow(container, win) {
var mount = document.createElement('div');
var el = global.DigestWindow.open({
title: win.title || 'Globe',
content: mount,
container: container,
variant: 'globe'
});
var globe = new global.DigestGlobe(mount);
(win.globe_markers || []).forEach(function (marker) {
try {
globe.addMarker(marker.lat, marker.lon, {
icon: marker.icon,
color: marker.color,
glow: marker.glow,
label: marker.label
});
} catch (err) {
// One bad marker must not cost the whole globe.
if (global.console) { global.console.warn('digest: skipped a globe marker', err); }
}
});
if (typeof win.content === 'string' && win.content.trim()) {
var caption = document.createElement('p');
caption.className = 'digest-globe-caption';
caption.textContent = win.content;
mount.parentNode.appendChild(caption);
}
return el;
}
function renderWindow(container, win) {
if (!win || typeof win !== 'object') {
return rawDump(container, win, '// malformed window');
}
if (win.kind === 'globe') {
return renderGlobeWindow(container, win);
}
return global.DigestWindow.open({
title: win.title || '',
content: win.content,
container: container,
x: win.x,
y: win.y,
w: win.w,
h: win.h
});
}
function renderSection(container, doc) {
if (!doc || typeof doc !== 'object' || !Array.isArray(doc.windows)) {
return rawDump(container, doc, '// section did not match the digest schema');
}
doc.windows.forEach(function (win) {
try {
var el = renderWindow(container, win);
if (el && doc.section) {
el.dataset.section = doc.section;
}
} catch (err) {
if (global.console) { global.console.warn('digest: window fell back to plain text', err); }
rawDump(container, win, '// window failed to render: ' + err);
}
});
}
var DigestRender = {
render: function (container, input, options) {
options = options || {};
container.innerHTML = '';
container.classList.add('digest-canvas');
var parsed = parse(input);
if (parsed && parsed.__unparseable !== undefined) {
rawDump(container, parsed.__unparseable, '// digest JSON did not parse');
return;
}
var sections = toSectionList(parsed, options.detailLevel || 'full');
if (!sections.length) {
rawDump(container, parsed, '// no digest sections found in this document');
return;
}
sections.forEach(function (doc) {
try {
renderSection(container, doc);
} catch (err) {
if (global.console) { global.console.warn('digest: section fell back to plain text', err); }
rawDump(container, doc, '// section failed to render: ' + err);
}
});
},
// Convenience used by both templates: fetch, render, and put the failure on
// screen rather than only in the console if the fetch itself fails.
load: function (container, url, options) {
return global.fetch(url, { cache: 'no-store' })
.then(function (response) {
if (!response.ok) { throw new Error(response.status + ' ' + response.statusText); }
return response.text();
})
.then(function (text) {
DigestRender.render(container, text, options);
})
.catch(function (err) {
container.innerHTML = '';
container.classList.add('digest-canvas');
rawDump(container, String(err), '// could not load ' + url);
});
}
};
global.DigestRender = DigestRender;
})(window);