feat: manual guess input + continuous text preview rendering

- Replace redaction marker span with editable input for manual guesses
- Typing in the guess field sets r.preview and shows Accept button
- drawRedactionPreview now renders left+guess+right as continuous text
  with natural font kerning (no fixed gap width during preview)
- Active preview: green left, yellow highlighted guess, green right
- Inactive preview: full merged text in yellow

Co-Authored-By: Claude Opus 4.6 <[email protected]>
This commit is contained in:
Alex-GilbertandClaude Opus 4.6 committed 2026-02-20 15:45:28 -06:00
1 parent a4863350ef
commit 33b88f078f
6 files changed
+50 -29

No files matched your search

+23 -23
View File
@@ -199,57 +199,57 @@ function drawRedactionPreview(r, isActive) {
const fontName = state.fonts.find(f => f.id === (o.fontId ?? a.font.id))?.name ?? a.font.name;
const fontSize = o.fontSize ?? a.font.size;
const fontStr = `${fontSize}px "${fontName}"`;
const gapW = o.gapWidth ?? a.gap.w;
const leftText = o.leftText ?? "";
const rightText = o.rightText ?? "";
const mergedText = leftText + r.preview + rightText;
if (isActive) {
const startX = a.line.x + (o.offsetX ?? 0);
const startY = a.line.y + (o.offsetY ?? 0);
ctx.textBaseline = "top";
ctx.font = fontStr;
let cursorX = startX;
const leftText = o.leftText ?? "";
// Draw as one continuous string for natural kerning
// Left portion in green
if (leftText) {
cursorX += drawStyledText(leftText, cursorX, startY, fontName, fontSize, "rgba(0, 200, 0, 0.7)");
drawStyledText(leftText, startX, startY, fontName, fontSize, "rgba(0, 200, 0, 0.7)");
}
// Measure where the preview text starts and ends
const leftW = leftText ? measureStyledText(leftText, fontName, fontSize) : 0;
const previewX = startX + leftW;
const previewW = ctx.measureText(r.preview).width;
// Highlight behind the preview portion
const pad = fontSize * 0.15;
ctx.fillStyle = "rgba(255, 200, 0, 0.2)";
ctx.fillRect(cursorX, startY - pad, gapW, fontSize + pad * 2);
ctx.strokeStyle = "rgba(255, 200, 0, 0.8)";
ctx.lineWidth = 2;
ctx.strokeRect(cursorX, startY - pad, gapW, fontSize + pad * 2);
ctx.fillRect(previewX, startY - pad, previewW, fontSize + pad * 2);
// Preview text in yellow
ctx.fillStyle = "rgba(255, 200, 0, 0.9)";
ctx.font = fontStr;
ctx.fillText(r.preview, cursorX, startY);
ctx.fillText(r.preview, previewX, startY);
cursorX += gapW;
const rightText = o.rightText ?? "";
// Right portion in green
if (rightText) {
drawStyledText(rightText, cursorX, startY, fontName, fontSize, "rgba(0, 200, 0, 0.7)");
const rightX = previewX + previewW;
drawStyledText(rightText, rightX, startY, fontName, fontSize, "rgba(0, 200, 0, 0.7)");
}
ctx.strokeStyle = "rgba(0, 200, 0, 0.3)";
ctx.lineWidth = 1;
ctx.strokeRect(a.line.x, a.line.y, a.line.w, a.line.h);
} else {
const gapX = a.gap.x + (o.offsetX ?? 0);
// Inactive: draw full merged text in yellow at line position
const startX = a.line.x + (o.offsetX ?? 0);
const textY = a.line.y + (o.offsetY ?? 0);
const pad = fontSize * 0.1;
ctx.fillStyle = "rgba(255, 200, 0, 0.12)";
ctx.fillRect(gapX, textY - pad, gapW, fontSize + pad * 2);
ctx.strokeStyle = "rgba(255, 200, 0, 0.5)";
ctx.lineWidth = 1;
ctx.strokeRect(gapX, textY - pad, gapW, fontSize + pad * 2);
ctx.font = fontStr;
ctx.textBaseline = "top";
ctx.fillStyle = "rgba(255, 200, 0, 0.9)";
ctx.fillText(r.preview, gapX, textY);
ctx.fillText(mergedText, startX, textY);
}
}
+1 -1
View File
@@ -52,7 +52,7 @@ export const gapValue = document.getElementById("gap-value");
export const textEditBar = document.getElementById("text-edit-bar");
export const leftTextInput = /** @type {HTMLInputElement} */ (document.getElementById("left-text-input"));
export const rightTextInput = /** @type {HTMLInputElement} */ (document.getElementById("right-text-input"));
export const redactionMarker = document.getElementById("redaction-marker");
export const redactionMarker = /** @type {HTMLInputElement} */ (document.getElementById("redaction-marker"));
export const textReset = document.getElementById("text-reset");
export const toastContainer = document.getElementById("toast-container");
+1 -1
View File
@@ -135,7 +135,7 @@
</div>
<div id="text-edit-bar" hidden>
<input type="text" id="left-text-input" class="seg-input" spellcheck="false" autocomplete="off" placeholder="left text">
<span id="redaction-marker" class="redaction-marker">???</span>
<input type="text" id="redaction-marker" class="redaction-marker" spellcheck="false" autocomplete="off" placeholder="???">
<input type="text" id="right-text-input" class="seg-input" spellcheck="false" autocomplete="off" placeholder="right text">
<button id="text-reset" class="tb-btn" title="Reset to OCR">Reset</button>
</div>
+11 -3
View File
@@ -44,8 +44,6 @@ export function openPopover(id) {
r.preview = r.solution.text;
r.status = "analyzed";
solveAccept.hidden = true;
redactionMarker.textContent = r.solution.text;
redactionMarker.className = "redaction-marker preview";
} else {
solveAccept.hidden = r.preview === null;
}
@@ -62,7 +60,7 @@ export function openPopover(id) {
textEditBar.hidden = false;
leftTextInput.value = r.overrides.leftText;
rightTextInput.value = r.overrides.rightText;
redactionMarker.textContent = r.preview || "???";
redactionMarker.value = r.preview || "";
redactionMarker.className = r.preview ? "redaction-marker preview" : "redaction-marker";
}
@@ -172,6 +170,16 @@ export function initPopover() {
});
redactionMarker.addEventListener("input", () => {
const r = state.redactions[state.activeRedaction];
if (!r) return;
const guess = redactionMarker.value.trim();
r.preview = guess || null;
redactionMarker.className = guess ? "redaction-marker preview" : "redaction-marker";
solveAccept.hidden = !guess;
renderCanvas();
});
solveTolerance.addEventListener("input", () => {
solveTolValue.textContent = solveTolerance.value;
});
+1 -1
View File
@@ -145,7 +145,7 @@ function handleSolveEvent(data, redactionId) {
const r = state.redactions[redactionId];
if (!r) return;
r.preview = data.text;
redactionMarker.textContent = data.text;
redactionMarker.value = data.text;
redactionMarker.className = "redaction-marker preview";
renderCanvas();
solveResults.querySelectorAll(".solve-result").forEach(el => el.classList.remove("active"));
+13
View File
@@ -684,12 +684,25 @@ main {
.redaction-marker {
flex-shrink: 0;
min-width: 3rem;
max-width: 12rem;
padding: 0.25rem 0.4rem;
background: var(--red);
color: #fff;
font-size: 0.65rem;
font-weight: 700;
border: none;
border-radius: 3px;
text-align: center;
}
.redaction-marker::placeholder {
color: rgba(255, 255, 255, 0.6);
}
.redaction-marker:focus {
outline: none;
box-shadow: 0 0 0 1px var(--accent);
}
.redaction-marker.preview {