From c15ba490b2794bf937a6190e4ab0f9cd8c7b05df Mon Sep 17 00:00:00 2001 From: Stuart Date: Thu, 3 Sep 2026 14:48:55 +1000 Subject: [PATCH] CMMS fork: AI Assistant editor-buffer bridge (pcb kind) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cmms-bridge.js gains a window 'message' listener answering the CMMS AI assistant's cmms-ai-editor-* protocol (same role as cad-editor's Rust listener, since studio.html is the top-level page here): - cmms-ai-editor-get-buffer -> replies { type:'cmms-ai-editor-buffer', buffer: } — device/board name, placed components ({label,value,desc,x,y}), nets ({name,pins:[...]}), and the imported board-photo list. - cmms-ai-editor-apply (editor:'pcb') -> parses a JSON patch of proposed components + nets and merges it into the open project (upsert components by label, nets by name; new parts with no coordinate are staggered so they're draggable; net pins inherit their component's position if it exists). Refreshes the BOM list / map / nets view. Nothing is saved - the user checks each against the board photo and clicks Save. Inert unless window.PCBRETRACE_CONFIG is set. --- docs/cmms-bridge.js | 89 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/docs/cmms-bridge.js b/docs/cmms-bridge.js index afeb566..5948c02 100644 --- a/docs/cmms-bridge.js +++ b/docs/cmms-bridge.js @@ -111,6 +111,95 @@ } } + // ---- 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. + + async function buildProjectView() { + if (!currentDeviceId || !currentBomId) return null; + const dev = deviceList.find(d => d.id === currentDeviceId); + const board = bomList.find(b => b.id === currentBomId); + const allNets = await db.getNets(); + const nets = allNets.filter(n => n.projectId === currentBomId); + return JSON.stringify({ + device: dev ? dev.name : null, + board: board ? board.name : null, + image_count: bomImages.length, + images: bomImages.map(i => ({ id: i.id, name: i.name || '' })), + components: bomData.map(c => ({ + label: c.label, value: c.value || '', desc: c.desc || '', + x: Math.round(c.x), y: Math.round(c.y), + })), + nets: nets.map(n => ({ name: n.name, pins: (n.nodes || []).map(nd => nd.label) })), + }); + } + + async function applyProjectPatch(patchJson) { + const patch = JSON.parse(patchJson); + if (!currentBomId) { status('Select or create a board first', true); return; } + + const byLabel = l => bomData.find(c => (c.label || '').toUpperCase() === l.toUpperCase()); + const fallbackImg = currentImgId || (bomImages[0] && bomImages[0].id) || null; + let spread = 40; + + for (const c of (patch.components || [])) { + const existing = byLabel(c.label); + const rec = existing || { id: uuid(), boardId: currentBomId, label: c.label, x: 0, y: 0 }; + if (c.value !== undefined) rec.value = c.value; + if (c.desc !== undefined) rec.desc = c.desc; + if (c.x !== undefined) rec.x = Math.round(c.x); + if (c.y !== undefined) rec.y = Math.round(c.y); + // New part with no coordinate → stagger it so it's draggable, not stacked. + if (!existing && c.x === undefined) { rec.x = spread; rec.y = spread; spread += 40; } + rec.source = 'ai_suggested'; + await db.addComponent(rec); + } + + const allNets = await db.getNets(); + const projectNets = allNets.filter(n => n.projectId === currentBomId); + for (const n of (patch.nets || [])) { + let net = projectNets.find(x => x.name === n.name); + if (!net) net = { id: uuid(), name: n.name, projectId: currentBomId, nodes: [] }; + const have = new Set((net.nodes || []).map(nd => nd.label)); + for (const pin of (n.pins || [])) { + if (have.has(pin)) continue; + const comp = byLabel(pin.split('.')[0]); + net.nodes.push({ + id: uuid(), + imgId: fallbackImg, + x: comp ? Math.round(comp.x) : 0, + y: comp ? Math.round(comp.y) : 0, + label: pin, + }); + } + await db.addNet(net); + } + + await loadProjectData(); + if (window.netManager) await window.netManager.render(); + if (window.inspector && window.inspector.updateNetNodeCache) window.inspector.updateNetNodeCache(); + const nc = (patch.components || []).length, nn = (patch.nets || []).length; + status(`AI proposal applied (${nc} component${nc === 1 ? '' : 's'}, ${nn} net${nn === 1 ? '' : 's'}) — check each against the photo, then Save`); + } + + 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') { + 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) {} + } else if (d.type === 'cmms-ai-editor-apply' && d.editor === 'pcb' && typeof d.new_source === 'string') { + try { + await applyProjectPatch(d.new_source); + } catch (e) { + status('Could not apply the AI proposal: ' + (e.message || e), true); + } + } + }); + async function init() { if (CFG.dark) document.documentElement.setAttribute('data-theme', 'dark'); injectControls();