feat: Rot-Dialog fuer mehrdeutige Erkennung

This commit is contained in:
vchuser
2026-07-28 17:12:26 +02:00
parent d20f964399
commit e5f8952b95
2 changed files with 86 additions and 7 deletions
+8 -7
View File
@@ -1,15 +1,16 @@
import { renderScanView } from './ui/scan-view.js';
import { showResult } from './ui/result-overlay.js';
import { askForStack } from './ui/ambiguous-dialog.js';
import { startCamera } from './camera.js';
const app = document.querySelector('#app');
const view = renderScanView(app, {
onCapture: () =>
showResult(app, {
stackId: 'B',
spec: { capacityGb: 64, rank: '4DRx4', speed: 'PC4-2400', formFactor: 'LRDIMM', partNumber: 'M386A8K40BM1-CRC4Y' },
confidence: 'green',
}),
onCapture: async () => {
const answer = await askForStack(app, {
spec: { capacityGb: 64, rank: null, speed: null, formFactor: null, partNumber: null },
candidates: ['A', 'B'],
});
view.setLast(`Antwort: ${JSON.stringify(answer)}`);
},
onUndo: () => view.setLast('zurueckgenommen'),
onOpenList: () => {},
onPickFile: () => {},
+78
View File
@@ -0,0 +1,78 @@
/**
* Fasst die erkannten Merkmale zu einer kurzen Lesehilfe zusammen.
* Rein darstellend - keine fachliche Bewertung.
*/
function describeReading(spec) {
const parts = [
spec.capacityGb ? `${spec.capacityGb}GB` : null,
spec.rank,
spec.speed,
spec.formFactor,
spec.partNumber,
].filter(Boolean);
return parts.length ? parts.join(' ') : 'nichts Verwertbares';
}
/**
* Haelt den Ablauf an und laesst den Nutzer entscheiden.
* Der einzige Punkt der App, an dem eine Eingabe verlangt wird.
* Reine Darstellung: kennt weder Stapel-Logik noch Erkennung,
* bekommt die zur Wahl stehenden Stapel als Liste herein.
*
* @param {HTMLElement} root
* @param {{spec: object, candidates: string[]}} args
* @returns {Promise<{action: 'stack', stackId: string}|{action: 'new'}|{action: 'retry'}>}
*/
export function askForStack(root, { spec, candidates }) {
const overlay = document.createElement('div');
overlay.className = 'overlay red';
overlay.setAttribute('role', 'dialog');
overlay.setAttribute('aria-modal', 'true');
overlay.setAttribute('aria-labelledby', 'ambiguous-dialog-heading');
overlay.innerHTML = `
<div class="stack-id" id="ambiguous-dialog-heading" style="font-size:34px">Nicht eindeutig</div>
<div class="detail">Gelesen: <span id="read"></span></div>
<div class="choices" id="choices"></div>
`;
// Der gelesene Wert stammt aus dem Etikett (Fremdeingabe) - als Text setzen, nicht als Auszeichnung.
overlay.querySelector('#read').textContent = describeReading(spec);
const choices = overlay.querySelector('#choices');
root.appendChild(overlay);
return new Promise((resolve) => {
// Schuetzt vor Mehrfachausloesung bei hastigem Mehrfachtippen:
// das Versprechen darf genau einmal eingeloest werden.
let settled = false;
const finish = (result) => {
if (settled) return;
settled = true;
overlay.remove();
resolve(result);
};
for (const stackId of candidates) {
const button = document.createElement('button');
button.className = 'action';
button.textContent = `Stapel ${stackId}`;
button.addEventListener('click', () => finish({ action: 'stack', stackId }));
choices.appendChild(button);
}
const newStackButton = document.createElement('button');
newStackButton.className = 'action secondary';
newStackButton.textContent = 'neuer Stapel';
newStackButton.addEventListener('click', () => finish({ action: 'new' }));
choices.appendChild(newStackButton);
const retryButton = document.createElement('button');
retryButton.className = 'action secondary';
retryButton.textContent = 'nochmal scannen';
retryButton.addEventListener('click', () => finish({ action: 'retry' }));
choices.appendChild(retryButton);
// Auch ohne Stapel-Kandidaten kommt der Nutzer ueber "neuer Stapel"
// oder "nochmal scannen" weiter - der Dialog blockiert nie.
choices.querySelector('button').focus();
});
}