CMMS fork: pcb AI bridge — robustness for the first-message race
The chat's get-buffer request can arrive before studio.js init() has built the DB / selected a board (or while an existing project ZIP is still streaming in), which read back as "the editor is still loading". - buildProjectView() waits up to 4.5s for db + a board, then still returns a valid (possibly empty) project view with a "no board open" note rather than null - only a missing db is "still loading". - init() with no loadUrl (a "+ New board RE") now ensures a starter board exists (studio.js only auto-creates one when the whole DB is empty, not when the selected device just has no boards). - reply prefers evt.source, falls back to a DOM query for the chat iframe (cad-editor hit an unusable cross-realm Window). - [cmms-pcb-bridge] console tracing on every step.
This commit is contained in:
parent
c15ba490b2
commit
04d29dc0ac
1 changed files with 70 additions and 9 deletions
|
|
@ -113,19 +113,41 @@
|
|||
|
||||
// ---- AI Assistant editor-buffer bridge -------------------------------
|
||||
// The chat embed (an iframe) postMessages here; this top-level page is the
|
||||
// "editor" side of the cmms-ai-editor-* protocol (like cad-editor's Rust
|
||||
// listener). read: serialise the open board; apply: merge a JSON patch of
|
||||
// proposed components + nets. Nothing is saved.
|
||||
// "editor" side of the cmms-ai-editor-* protocol (like cad-editor, whose
|
||||
// WASM app is also the top-level page). read: serialise the open board;
|
||||
// apply: merge a JSON patch of proposed components + nets. Nothing is saved.
|
||||
const trace = (...a) => { try { console.log('[cmms-pcb-bridge]', ...a); } catch (e) {} };
|
||||
|
||||
// The chat's get-buffer request can land before studio.js's init() has
|
||||
// finished creating the DB and selecting a board (or while an existing
|
||||
// project ZIP is still downloading). Wait briefly rather than replying
|
||||
// "not ready" - the widget times out at 2.5s, so stay under that.
|
||||
const ready = () => !!(db && currentBomId);
|
||||
async function waitReady(ms) {
|
||||
const t0 = Date.now();
|
||||
while (!ready() && Date.now() - t0 < ms) {
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
}
|
||||
return ready();
|
||||
}
|
||||
|
||||
async function buildProjectView() {
|
||||
if (!currentDeviceId || !currentBomId) return null;
|
||||
// The widget waits 6s for a pcb reply (ai-assistant-widget.js); stay
|
||||
// under that so a "still initialising" answer still gets through.
|
||||
if (!(await waitReady(4500))) {
|
||||
trace('buildProjectView: db/board not ready after 4.5s (db:', !!db, 'board:', !!currentBomId, ')');
|
||||
// db exists but no board -> a real (empty) project the model can
|
||||
// still work from; only a missing db means "still loading".
|
||||
if (!db) return null;
|
||||
}
|
||||
const dev = deviceList.find(d => d.id === currentDeviceId);
|
||||
const board = bomList.find(b => b.id === currentBomId);
|
||||
const allNets = await db.getNets();
|
||||
const allNets = currentBomId ? await db.getNets() : [];
|
||||
const nets = allNets.filter(n => n.projectId === currentBomId);
|
||||
return JSON.stringify({
|
||||
const view = {
|
||||
device: dev ? dev.name : null,
|
||||
board: board ? board.name : null,
|
||||
note: currentBomId ? undefined : 'No board is open yet - the user needs to create or select one before components/nets can be added.',
|
||||
image_count: bomImages.length,
|
||||
images: bomImages.map(i => ({ id: i.id, name: i.name || '' })),
|
||||
components: bomData.map(c => ({
|
||||
|
|
@ -133,7 +155,9 @@
|
|||
x: Math.round(c.x), y: Math.round(c.y),
|
||||
})),
|
||||
nets: nets.map(n => ({ name: n.name, pins: (n.nodes || []).map(nd => nd.label) })),
|
||||
});
|
||||
};
|
||||
trace('buildProjectView:', view.components.length, 'components,', view.nets.length, 'nets,', view.image_count, 'photos');
|
||||
return JSON.stringify(view);
|
||||
}
|
||||
|
||||
async function applyProjectPatch(patchJson) {
|
||||
|
|
@ -184,22 +208,57 @@
|
|||
status(`AI proposal applied (${nc} component${nc === 1 ? '' : 's'}, ${nn} net${nn === 1 ? '' : 's'}) — check each against the photo, then Save`);
|
||||
}
|
||||
|
||||
// Reply to the chat iframe. Prefer evt.source; fall back to a DOM query
|
||||
// (cad-editor hit a case where event.source was an unusable cross-realm
|
||||
// Window - belt and braces here too).
|
||||
function replyToChat(evtSource, msg) {
|
||||
let posted = false;
|
||||
try { if (evtSource && evtSource.postMessage) { evtSource.postMessage(msg, window.location.origin); posted = true; } } catch (e) {}
|
||||
if (!posted) {
|
||||
const f = document.querySelector('#ai-assistant-editor-embed iframe');
|
||||
try { if (f && f.contentWindow) { f.contentWindow.postMessage(msg, window.location.origin); posted = true; } } catch (e) {}
|
||||
}
|
||||
trace('reply', msg.type, posted ? 'sent' : 'DROPPED');
|
||||
}
|
||||
|
||||
window.addEventListener('message', async (evt) => {
|
||||
if (evt.origin !== window.location.origin || !evt.data || typeof evt.data !== 'object') return;
|
||||
const d = evt.data;
|
||||
if (d.type === 'cmms-ai-editor-get-buffer') {
|
||||
trace('get-buffer received');
|
||||
let buffer = null;
|
||||
try { buffer = await buildProjectView(); } catch (e) { /* reply null */ }
|
||||
try { evt.source.postMessage({ type: 'cmms-ai-editor-buffer', buffer: buffer }, window.location.origin); } catch (e) {}
|
||||
try { buffer = await buildProjectView(); } catch (e) { trace('buildProjectView threw:', e && e.message); }
|
||||
replyToChat(evt.source, { type: 'cmms-ai-editor-buffer', buffer: buffer });
|
||||
} else if (d.type === 'cmms-ai-editor-apply' && d.editor === 'pcb' && typeof d.new_source === 'string') {
|
||||
trace('apply received');
|
||||
try {
|
||||
await applyProjectPatch(d.new_source);
|
||||
} catch (e) {
|
||||
trace('applyProjectPatch threw:', e && e.message);
|
||||
status('Could not apply the AI proposal: ' + (e.message || e), true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// A "+ New board RE" launch with no board on the selected device (studio.js
|
||||
// only auto-creates one when the whole DB is empty) - make one so the user
|
||||
// lands on a working board, not an empty "No Boards" shell.
|
||||
async function ensureBoard() {
|
||||
if (currentBomId) return;
|
||||
let devId = currentDeviceId;
|
||||
if (!devId || !deviceList.find(d => d.id === devId)) {
|
||||
devId = uuid();
|
||||
await db.addDevice({ id: devId, name: 'Board' });
|
||||
currentDeviceId = devId;
|
||||
try { localStorage.setItem('pcb_device_id', devId); } catch (e) {}
|
||||
}
|
||||
await db.addProject({ id: uuid(), deviceId: devId, name: 'Board 1', sortMode: 'none' });
|
||||
deviceList = await db.getDevices();
|
||||
updateDeviceDropdown();
|
||||
await loadDeviceBoms();
|
||||
trace('ensureBoard: created a starter board');
|
||||
}
|
||||
|
||||
async function init() {
|
||||
if (CFG.dark) document.documentElement.setAttribute('data-theme', 'dark');
|
||||
injectControls();
|
||||
|
|
@ -215,6 +274,8 @@
|
|||
} catch (e) {
|
||||
status('Could not load the saved board: ' + (e.message || e), true);
|
||||
}
|
||||
} else {
|
||||
try { await ensureBoard(); } catch (e) { trace('ensureBoard threw:', e && e.message); }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue