Add actionable diagnostics with one-click repair actions

This commit is contained in:
Rbanh 2026-02-18 22:12:57 -05:00
parent 486092e884
commit 72ea3609bb
4 changed files with 113 additions and 4 deletions

View File

@ -1322,6 +1322,96 @@ function renderSelected() {
el.netEditor.classList.add("hidden"); el.netEditor.classList.add("hidden");
} }
function issueById(issueId) {
const issues = [...(state.compile?.errors ?? []), ...(state.compile?.warnings ?? [])];
return issues.find((i) => i.id === issueId) ?? null;
}
function parseRefPinFromIssueMessage(issue) {
const m = /'([A-Za-z][A-Za-z0-9_]*)\.([A-Za-z0-9_]+)'/.exec(String(issue?.message ?? ""));
if (m) {
return { ref: m[1], pin: m[2] };
}
return null;
}
function ensureNet(name, netClass = "signal") {
if (!state.model) {
return null;
}
let net = netByName(name);
if (!net) {
net = { name, class: netClass, nodes: [] };
state.model.nets.push(net);
}
return net;
}
function issueFixAction(issue) {
if (!issue?.code) {
return null;
}
if (issue.code === "ground_net_missing") {
return { label: "Create GND Net", action: "create_ground" };
}
if (issue.code === "floating_input") {
const rp = parseRefPinFromIssueMessage(issue);
if (rp) {
return { label: "Create Signal Net", action: "connect_signal", ref: rp.ref, pin: rp.pin };
}
}
if (issue.code === "required_power_unconnected") {
const rp = parseRefPinFromIssueMessage(issue);
if (rp) {
return { label: "Connect Power", action: "connect_power", ref: rp.ref, pin: rp.pin };
}
}
return null;
}
async function applyIssueFix(issueId) {
if (!state.model) {
return;
}
const issue = issueById(issueId);
const fix = issueFixAction(issue);
if (!issue || !fix) {
el.jsonFeedback.textContent = "No automatic fix available for this issue.";
return;
}
pushHistory("issue-fix");
let applied = false;
if (fix.action === "create_ground") {
ensureNet("GND", "ground");
applied = true;
} else if (fix.action === "connect_signal") {
const name = normalizeNetName(`NET_${fix.ref}_${fix.pin}`);
applied = connectPinToNet(fix.ref, fix.pin, name || nextAutoNetName(), { netClass: "signal" }).ok;
} else if (fix.action === "connect_power") {
const upperPin = String(fix.pin).toUpperCase();
if (upperPin.includes("GND")) {
applied = connectPinToNet(fix.ref, fix.pin, "GND", { netClass: "ground" }).ok;
} else if (netByName("3V3")) {
applied = connectPinToNet(fix.ref, fix.pin, "3V3", { netClass: "power" }).ok;
} else if (netByName("5V")) {
applied = connectPinToNet(fix.ref, fix.pin, "5V", { netClass: "power" }).ok;
} else {
applied = connectPinToNet(fix.ref, fix.pin, normalizeNetName(`PWR_${fix.ref}_${fix.pin}`), { netClass: "power" }).ok;
}
}
if (!applied) {
el.jsonFeedback.textContent = "Automatic fix could not be applied safely.";
return;
}
await compileModel(state.model, { keepView: true, source: "issue-fix" });
el.jsonFeedback.textContent = `Applied fix for ${issue.code}.`;
focusIssue(issueId);
}
function renderIssues() { function renderIssues() {
const errors = state.compile?.errors ?? []; const errors = state.compile?.errors ?? [];
const warnings = state.compile?.warnings ?? []; const warnings = state.compile?.warnings ?? [];
@ -1333,12 +1423,16 @@ function renderIssues() {
const rows = [ const rows = [
...errors.map( ...errors.map(
(issue) => (issue) => {
`<div class="issueRow issueErr" data-issue-id="${issue.id}"><div class="issueTitle">[E] ${issue.message}</div><div class="issueMeta">${issue.code} · ${issue.path ?? "-"}</div><div class="issueMeta">${issue.suggestion ?? ""}</div></div>` const fix = issueFixAction(issue);
return `<div class="issueRow issueErr" data-issue-id="${issue.id}"><div class="issueTitle">[E] ${issue.message}</div><div class="issueMeta">${issue.code} · ${issue.path ?? "-"}</div><div class="issueMeta">${issue.suggestion ?? ""}</div>${fix ? `<div class="issueActions"><button type="button" data-fix-issue="${issue.id}">${fix.label}</button></div>` : ""}</div>`;
}
), ),
...warnings.map( ...warnings.map(
(issue) => (issue) => {
`<div class="issueRow issueWarn" data-issue-id="${issue.id}"><div class="issueTitle">[W] ${issue.message}</div><div class="issueMeta">${issue.code} · ${issue.path ?? "-"}</div><div class="issueMeta">${issue.suggestion ?? ""}</div></div>` const fix = issueFixAction(issue);
return `<div class="issueRow issueWarn" data-issue-id="${issue.id}"><div class="issueTitle">[W] ${issue.message}</div><div class="issueMeta">${issue.code} · ${issue.path ?? "-"}</div><div class="issueMeta">${issue.suggestion ?? ""}</div>${fix ? `<div class="issueActions"><button type="button" data-fix-issue="${issue.id}">${fix.label}</button></div>` : ""}</div>`;
}
) )
]; ];
@ -2394,6 +2488,12 @@ function setupEvents() {
}); });
el.issues.addEventListener("click", (evt) => { el.issues.addEventListener("click", (evt) => {
const fixBtn = evt.target.closest("[data-fix-issue]");
if (fixBtn) {
evt.stopPropagation();
void applyIssueFix(fixBtn.getAttribute("data-fix-issue"));
return;
}
const row = evt.target.closest("[data-issue-id]"); const row = evt.target.closest("[data-issue-id]");
if (!row) { if (!row) {
return; return;

View File

@ -575,6 +575,15 @@ textarea {
color: var(--ink-muted); color: var(--ink-muted);
} }
.issueActions {
margin-top: 6px;
}
.issueActions button {
font-size: 0.74rem;
padding: 3px 8px;
}
.modal { .modal {
position: fixed; position: fixed;
inset: 0; inset: 0;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 174 KiB

After

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 173 KiB

After

Width:  |  Height:  |  Size: 174 KiB