/* 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 (
// Palimpsest
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 ↗