/* global React, PaperCard, Button, Chip, WaxSeal, StageChip, ProgressBar, LogLine, Icons */ // Palimpsest — Workshop (real API integration) const { useState, useEffect, useRef, useMemo, useCallback } = React; // -------------------------------------------------------------------------- // COST TABLE — per page, mirrors backend's accepted models // -------------------------------------------------------------------------- const COSTS = { 'o4-mini': { withImg: 0.027, noImg: 0.023, label: 'o4-mini' }, 'gpt-4.1': { withImg: 0.041, noImg: 0.033, label: 'gpt-4.1' }, 'gpt-4o': { withImg: 0.049, noImg: 0.039, label: 'gpt-4o' }, 'gpt-4.1-mini': { withImg: 0.016, noImg: 0.015, label: 'gpt-4.1-mini' }, 'gpt-4.1-nano': { withImg: 0.012, noImg: 0.011, label: 'gpt-4.1-nano' }, 'claude-opus-4-20250514': { withImg: 0.098, noImg: 0.078, label: 'claude-opus-4' }, 'claude-sonnet-4-20250514': { withImg: 0.063, noImg: 0.051, label: 'claude-sonnet-4' }, 'o1': { withImg: 0.243, noImg: 0.183, label: 'o1' }, }; const MATHPIX_SURCHARGE = 0.005; function estimateCost(cfg, pages) { const info = COSTS[cfg.model]; if (!info || !pages) return null; const per = (cfg.src === 'preserve images' ? info.withImg : info.noImg) + (cfg.ocr === 'mathpix' ? MATHPIX_SURCHARGE : 0); return pages * per; } // -------------------------------------------------------------------------- // STATE → STAGE MAPPING // stages: [preprocess, ocr, rewrite, compile] // -------------------------------------------------------------------------- function deriveStages(state, currentStage) { if (state === 'idle') return ['pending', 'pending', 'pending', 'pending']; if (state === 'done') return ['done', 'done', 'done', 'done']; if (state === 'error') { // mark the failing stage const s = currentStage || ''; if (s.startsWith('rewrite')) return ['done', 'done', 'failed', 'pending']; if (s.startsWith('ocr')) return ['done', 'failed', 'pending', 'pending']; if (s.startsWith('compile')) return ['done', 'done', 'done', 'failed']; if (s.startsWith('pre')) return ['failed', 'pending', 'pending', 'pending']; return ['done', 'failed', 'pending', 'pending']; } // processing const s = currentStage || 'preprocessing'; if (s.startsWith('pre')) return ['active', 'pending', 'pending', 'pending']; if (s.startsWith('ocr')) return ['done', 'active', 'pending', 'pending']; if (s.startsWith('rewrite')) return ['done', 'done', 'active', 'pending']; if (s.startsWith('compile')) return ['done', 'done', 'done', 'active']; return ['active', 'pending', 'pending', 'pending']; } function stageStatuses(state, currentStage, page, total) { const stages = deriveStages(state, currentStage); const pages = (i) => { const st = stages[i]; if (st === 'done') return 'complete'; if (st === 'failed') return 'failed'; if (st === 'pending') return 'pending'; if (st === 'active') return total ? `page ${String(page).padStart(2, '0')}` : '…'; return ''; }; return [pages(0), pages(1), pages(2), pages(3)]; } // overall progress (0-100) weighted across the four stages function overallProgress(state, currentStage, page, total) { if (state === 'idle') return { value: 0, tone: 'ai', label: '0%', sub: 'awaiting input' }; if (state === 'done') return { value: 100, tone: 'green', label: '100%', sub: 'complete' }; if (state === 'error') return { value: 0, tone: 'warn', label: '!', sub: 'halted' }; const t = total || 1; const frac = Math.min(1, (page || 0) / t); const s = currentStage || 'preprocessing'; let value = 0; if (s.startsWith('pre')) value = frac * 25; else if (s.startsWith('ocr')) value = 25 + frac * 30; else if (s.startsWith('rewrite')) value = 55 + frac * 40; else if (s.startsWith('compile')) value = 95 + frac * 5; value = Math.round(value); let sub = ''; if (total) sub = `${page}/${total} · ${s}`; else sub = s; return { value, tone: 'ai', label: `${value}%`, sub }; } function chipFor(state, page, total) { if (state === 'idle') return queued; if (state === 'processing') return live{total ? ` · ${String(page).padStart(2, '0')}/${total}` : ''}; if (state === 'done') return done{total ? ` · ${total}/${total} ✓` : ' ✓'}; if (state === 'error') return error; } // -------------------------------------------------------------------------- // HEADER // -------------------------------------------------------------------------- function PaperHeader() { return (
scan ocr llm LaTeX
MS · 2026 / vol. iii

Drop your old scan, get a clean scientific document back.

palimpsest · a manuscript page scraped clean and written over, with traces of the original text still showing through. That's what this tool does to your scans. read the etymology ↗
); } // -------------------------------------------------------------------------- // DROPZONE // -------------------------------------------------------------------------- function DropzoneBlock({ file, pageCount, onPick, onClear, flash }) { const [hot, setHot] = useState(false); const inputRef = useRef(null); const sizeMB = file ? (file.size / 1048576).toFixed(1) : null; return (
!file && inputRef.current?.click()} onDragOver={e => { e.preventDefault(); if (!file) setHot(true); }} onDragLeave={() => setHot(false)} onDrop={e => { e.preventDefault(); setHot(false); if (file) return; const f = e.dataTransfer.files[0]; if (f && f.name.toLowerCase().endsWith('.pdf')) onPick(f); }} style={{ cursor: file ? 'default' : 'pointer' }} > e.target.files[0] && onPick(e.target.files[0])} />
{file ? 'staged · ready' : 'drop a scanned PDF'}
{file ? 'click start to launch the pipeline' : 'or click to browse · max 60 MB'}
{file && (
{file.name}
{pageCount ? `${pageCount} pages · ` : ''}{sizeMB} MB{pageCount ? ' · scanned' : ''}
staged
)} ); } // -------------------------------------------------------------------------- // SPECIMEN CONFIG // -------------------------------------------------------------------------- function SpecimenCard({ cfg, setCfg, cost }) { return (
setCfg({ ...cfg, ocr: v })} /> setCfg({ ...cfg, src: v })} compact />
{cost != null ? `$${cost.toFixed(2)}` : '—'} per run
); } function SpecField({ label, children }) { return (
{label}
{children}
); } function Toggle({ options, value, onChange, compact }) { return (
{options.map(o => ( ))}
); } // -------------------------------------------------------------------------- // TERMINAL HEADER // -------------------------------------------------------------------------- function TermHeader({ state, page, total, file, jobNo, wsConnected }) { return (
palimpsest@workshop ~/jobs/{jobNo || '—'} ws · {wsConnected ? 'connected' : 'idle'}
{file || '— no job —'} {jobNo && #{jobNo}} {chipFor(state, page, total)}
); } // -------------------------------------------------------------------------- // STAGE ROW // -------------------------------------------------------------------------- function StageRow({ state, currentStage, page, total }) { const states = deriveStages(state, currentStage); const statuses = stageStatuses(state, currentStage, page, total); const names = ['preprocess','ocr','rewrite','compile']; return (
{names.map((n,i) => ( ))}
); } // -------------------------------------------------------------------------- // PROGRESS BLOCK // -------------------------------------------------------------------------- function ProgressBlock({ state, currentStage, page, total }) { const p = overallProgress(state, currentStage, page, total); return (
// progress {p.label} · {p.sub}
); } // -------------------------------------------------------------------------- // LOG STREAM // -------------------------------------------------------------------------- function LogStream({ state, log, mobile }) { const ref = useRef(null); useEffect(() => { if (ref.current) ref.current.scrollTop = ref.current.scrollHeight; }, [log]); return (
# pipeline.log tail -f {state === 'processing' ? 'live' : state === 'done' ? 'complete' : state === 'error' ? 'halted' : 'idle'}
{log.map((l, i) => )} {state !== 'error' && (
$
)}
); } // -------------------------------------------------------------------------- // OUTPUTS ROW // -------------------------------------------------------------------------- function OutputsRow({ state, jobId, hasPdf, errorMsg }) { if (state === 'idle') { return (
↳ drop a PDF and start the pipeline to unlock outputs
); } if (state === 'processing') { return (
↳ outputs unlock at compile
); } if (state === 'done') { return (
{hasPdf && ( download pdf )} .tex Overleaf
); } if (state === 'error') { return (
{jobId && ( .tex if any )} ↳ {errorMsg || 'pipeline halted'}
); } } // -------------------------------------------------------------------------- // WORKSHOP (page body — desktop) // -------------------------------------------------------------------------- function Workshop(props) { const { state, page, total, currentStage, log, jobId, file, pageCount, cfg, setCfg, cost, wsConnected, hasPdf, errorMsg, onPick, onClear, onStart, flash, canStart, uploading, } = props; return (
{/* PAPER half */}
made with ♥ by{' '} @cmrabdu {' · '} source ↗
{/* SCANNER SEAM */} ); } Object.assign(window, { Workshop, COSTS, estimateCost });