GLMOCR_Text_extraction / sidebar.html
Sam20202's picture
Initial deploy
0533780
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>GLM-OCR Result</title>
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;700&family=DM+Serif+Display:ital@0;1&family=DM+Sans:wght@400;500&display=swap" rel="stylesheet"/>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--ink: #0f0e0d;
--paper: #f5f0e8;
--warm: #ede8dc;
--border: #d4cfc3;
--muted: #8f8880;
--accent: #c94a1f;
--green: #1a6b4a;
--mono: 'IBM Plex Mono', monospace;
--serif: 'DM Serif Display', serif;
--sans: 'DM Sans', sans-serif;
}
html, body {
height: 100%;
background: var(--paper);
color: var(--ink);
font-family: var(--sans);
overflow: hidden;
}
body::before {
content: '';
position: fixed; inset: 0;
background-image: radial-gradient(circle, rgba(0,0,0,0.05) 1px, transparent 1px);
background-size: 16px 16px;
pointer-events: none;
}
.sidebar {
position: relative;
height: 100vh;
display: flex;
flex-direction: column;
}
/* ── Header ── */
.sb-header {
padding: 14px 16px;
border-bottom: 2px solid var(--ink);
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
}
.sb-title {
font-family: var(--serif);
font-size: 1rem;
letter-spacing: -0.01em;
}
.sb-title em { font-style: italic; color: var(--accent); }
.sb-close {
font-family: var(--mono);
font-size: 0.6rem;
padding: 5px 10px;
border: 1px solid var(--border);
background: transparent;
cursor: pointer;
border-radius: 2px;
color: var(--muted);
transition: all 0.12s;
}
.sb-close:hover { border-color: var(--ink); color: var(--ink); }
/* ── Scrollable body ── */
.sb-body {
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
}
/* ── Loading ── */
.sb-loading {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 16px;
padding: 24px;
}
.scan-bar-wrap { width: 140px; height: 3px; background: var(--border); border-radius: 2px; overflow: hidden; }
.scan-bar { height: 100%; background: var(--accent); border-radius: 2px; animation: scan 1.4s ease-in-out infinite; }
@keyframes scan { 0%{transform:translateX(-100%)} 50%{transform:translateX(0)} 100%{transform:translateX(100%)} }
.loading-label {
font-family: var(--mono);
font-size: 0.68rem;
color: var(--muted);
animation: blink 1.4s ease-in-out infinite;
}
@keyframes blink { 0%,100%{opacity:1} 50%{opacity:0.3} }
/* ── Error ── */
.sb-error {
margin: 16px;
background: #fff0f0;
border: 1px solid rgba(201,74,31,0.3);
border-radius: 2px;
padding: 14px;
font-family: var(--mono);
font-size: 0.72rem;
color: var(--accent);
line-height: 1.7;
}
/* ── Image preview ── */
.sb-image-wrap {
padding: 14px 16px 0;
flex-shrink: 0;
}
.sb-image-label {
font-family: var(--mono);
font-size: 0.58rem;
color: var(--muted);
letter-spacing: 0.1em;
text-transform: uppercase;
margin-bottom: 8px;
}
.sb-image {
width: 100%;
max-height: 160px;
object-fit: contain;
border: 1px solid var(--border);
border-radius: 2px;
background: var(--warm);
}
/* ── Meta chips ── */
.sb-meta {
padding: 10px 16px;
display: flex;
gap: 10px;
flex-wrap: wrap;
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.chip {
font-family: var(--mono);
font-size: 0.6rem;
color: var(--muted);
}
.chip strong { color: var(--green); }
/* ── Extracted text ── */
.sb-text-section {
padding: 14px 16px;
display: flex;
flex-direction: column;
gap: 8px;
flex: 1;
min-height: 0;
}
.sb-text-label {
font-family: var(--mono);
font-size: 0.58rem;
color: var(--muted);
letter-spacing: 0.1em;
text-transform: uppercase;
flex-shrink: 0;
}
.sb-text {
background: var(--warm);
border: 1px solid var(--border);
border-radius: 2px;
padding: 14px;
font-family: var(--mono);
font-size: 0.78rem;
line-height: 1.85;
white-space: pre-wrap;
word-break: break-word;
overflow-y: auto;
flex: 1;
min-height: 120px;
}
/* ── Actions ── */
.sb-actions {
padding: 12px 16px;
border-top: 1px solid var(--border);
display: flex;
gap: 8px;
flex-shrink: 0;
}
.action-btn {
font-family: var(--mono);
font-size: 0.62rem;
letter-spacing: 0.04em;
padding: 9px 12px;
border: 1px solid var(--border);
background: transparent;
color: var(--ink);
cursor: pointer;
border-radius: 2px;
transition: all 0.12s;
flex: 1;
}
.action-btn:hover { border-color: var(--ink); }
.action-btn.primary {
background: var(--accent);
border-color: var(--accent);
color: white;
}
.action-btn.primary:hover { background: #b53d15; }
/* ── Toast ── */
.toast {
position: fixed;
bottom: 16px;
left: 50%;
transform: translateX(-50%) translateY(40px);
opacity: 0;
background: var(--ink);
color: var(--paper);
font-family: var(--mono);
font-size: 0.65rem;
padding: 8px 16px;
border-radius: 2px;
white-space: nowrap;
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
z-index: 999;
}
.toast.show {
transform: translateX(-50%) translateY(0);
opacity: 1;
}
</style>
</head>
<body>
<div class="sidebar">
<!-- Header -->
<div class="sb-header">
<div class="sb-title">GLM-<em>OCR</em> Result</div>
<button class="sb-close" id="close-btn">βœ• Close</button>
</div>
<!-- Body -->
<div class="sb-body" id="sb-body">
<!-- Loading state (default) -->
<div class="sb-loading" id="state-loading">
<div class="scan-bar-wrap"><div class="scan-bar"></div></div>
<div class="loading-label">Running GLM-OCR…</div>
</div>
</div>
<!-- Actions (shown after result) -->
<div class="sb-actions" id="sb-actions" style="display:none">
<button class="action-btn primary" id="new-btn">βœ‚ New Selection</button>
<button class="action-btn" id="copy-btn">Copy</button>
<button class="action-btn" id="dl-btn">↓ .txt</button>
</div>
</div>
<div class="toast" id="toast"></div>
<script>
let extractedText = "";
// ── Receive data from content.js ──────────────────────────────────────────
window.addEventListener("message", (e) => {
if (e.data?.type !== "SIDEBAR_DATA") return;
const data = e.data.data;
if (data.loading) return; // already showing loading state
renderResult(data);
});
function renderResult(data) {
const body = document.getElementById("sb-body");
const actions = document.getElementById("sb-actions");
if (data.error) {
body.innerHTML = `<div class="sb-error">⚠ ${data.error}<br><br>Make sure the GLM-OCR server is running at localhost:8000.</div>`;
actions.style.display = "flex";
return;
}
extractedText = data.text || "";
const latency = data.latency_ms ? `${(data.latency_ms / 1000).toFixed(2)}s` : "β€”";
body.innerHTML = `
<div class="sb-image-wrap">
<div class="sb-image-label">Selected Region</div>
<img class="sb-image" src="${data.imageDataUrl || ''}" alt="Selection"/>
</div>
<div class="sb-meta">
<span class="chip">words: <strong>${data.word_count || 0}</strong></span>
<span class="chip">chars: <strong>${data.char_count || 0}</strong></span>
<span class="chip">latency: <strong>${latency}</strong></span>
<span class="chip">device: <strong>${data.device || 'β€”'}</strong></span>
</div>
<div class="sb-text-section">
<div class="sb-text-label">Extracted Text</div>
<div class="sb-text" id="result-text">${data.text ? escapeHtml(data.text) : '<span style="color:var(--muted);">[No text detected]</span>'}</div>
</div>
`;
actions.style.display = "flex";
}
function escapeHtml(str) {
return str
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
}
// ── Close ─────────────────────────────────────────────────────────────────
document.getElementById("close-btn").addEventListener("click", () => {
window.parent.postMessage({ type: "CLOSE_SIDEBAR" }, "*");
});
// ── New selection ─────────────────────────────────────────────────────────
document.getElementById("new-btn").addEventListener("click", () => {
window.parent.postMessage({ type: "START_NEW_SELECTION" }, "*");
});
// ── Copy ──────────────────────────────────────────────────────────────────
document.getElementById("copy-btn").addEventListener("click", async () => {
try {
await navigator.clipboard.writeText(extractedText);
toast("Copied!");
} catch {
// fallback: select all text in the result box
const el = document.getElementById("result-text");
if (el) {
const range = document.createRange();
range.selectNodeContents(el);
const sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
}
toast("Select text above and copy manually.");
}
});
// ── Download ──────────────────────────────────────────────────────────────
document.getElementById("dl-btn").addEventListener("click", () => {
const blob = new Blob([extractedText], { type: "text/plain" });
const a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = `glm-ocr-${Date.now()}.txt`;
a.click();
URL.revokeObjectURL(a.href);
});
// ── Toast ─────────────────────────────────────────────────────────────────
function toast(msg) {
const t = document.getElementById("toast");
t.textContent = msg;
t.classList.add("show");
setTimeout(() => t.classList.remove("show"), 2000);
}
</script>
</body>
</html>