diff --git a/docs/cmms-bridge.js b/docs/cmms-bridge.js index 5948c02..fdaedbc 100644 --- a/docs/cmms-bridge.js +++ b/docs/cmms-bridge.js @@ -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); } } }