Spaces:
Sleeping
Sleeping
| <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, "&") | |
| .replace(/</g, "<") | |
| .replace(/>/g, ">"); | |
| } | |
| // ββ 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> | |