CMMS fork: AI Assistant editor-buffer bridge (pcb kind)

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: <JSON> } — 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.
This commit is contained in:
Stuart 2026-09-03 14:48:55 +10:00
commit c15ba490b2

View file

@ -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();