Spaces:
Running
Running
<html lang="en" class="h-full"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>OCR Time Capsule</title> | |
<!-- External Dependencies --> | |
<script src="https://unpkg.com/[email protected]"></script> | |
<script src="https://unpkg.com/[email protected]/dist/cdn.min.js" defer></script> | |
<script src="https://cdn.tailwindcss.com"></script> | |
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> | |
<!-- Tailwind Config --> | |
<script> | |
tailwind.config = { | |
darkMode: 'class', | |
theme: { | |
extend: { | |
animation: { | |
'fade-in': 'fadeIn 0.3s ease-in-out', | |
}, | |
keyframes: { | |
fadeIn: { | |
'0%': { opacity: '0' }, | |
'100%': { opacity: '1' }, | |
} | |
} | |
} | |
} | |
} | |
</script> | |
<!-- Custom Styles --> | |
<link rel="stylesheet" href="css/styles.css"> | |
</head> | |
<body class="h-full bg-gray-50 dark:bg-gray-900" x-data="ocrExplorer"> | |
<!-- Header --> | |
<header class="bg-white dark:bg-gray-800 shadow-sm border-b border-gray-200 dark:border-gray-700"> | |
<div class="px-4 sm:px-6 lg:px-8 py-4"> | |
<div class="flex items-center justify-between"> | |
<div class="flex-1"> | |
<div class="flex items-center space-x-4"> | |
<h1 class="text-xl font-semibold text-gray-900 dark:text-white"> | |
π¦ OCR Time Capsule | |
</h1> | |
<span class="text-sm text-gray-600 dark:text-gray-400 hidden sm:inline"> | |
Compare original and AI-improved OCR text from historical documents | |
</span> | |
</div> | |
<div class="flex items-center space-x-2 mt-3"> | |
<input | |
type="text" | |
x-model="datasetId" | |
@keyup.enter="loadDataset()" | |
class="px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-700 dark:border-gray-600 dark:text-white" | |
placeholder="Dataset ID (e.g., username/dataset-name)" | |
style="width: 300px;" | |
> | |
<button | |
@click="loadDataset()" | |
class="px-4 py-1.5 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500" | |
> | |
Load | |
</button> | |
<!-- Example Dataset Selector --> | |
<div class="relative group"> | |
<button class="px-3 py-1.5 text-sm text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200 border border-gray-300 dark:border-gray-600 rounded-md flex items-center space-x-1"> | |
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path> | |
</svg> | |
<span>Examples</span> | |
</button> | |
<div class="absolute left-0 mt-1 w-72 bg-white dark:bg-gray-800 rounded-md shadow-lg border border-gray-200 dark:border-gray-700 hidden group-hover:block z-50"> | |
<template x-for="dataset in exampleDatasets" :key="dataset.id"> | |
<button | |
@click="selectDataset(dataset.id)" | |
class="block w-full text-left px-4 py-3 hover:bg-gray-50 dark:hover:bg-gray-700 border-b border-gray-100 dark:border-gray-600 last:border-b-0" | |
> | |
<div class="font-medium text-sm text-gray-900 dark:text-gray-100" x-text="dataset.name"></div> | |
<div class="text-xs text-gray-500 dark:text-gray-400 mt-1" x-text="dataset.description"></div> | |
</button> | |
</template> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- Settings --> | |
<div class="flex items-center space-x-4"> | |
<button | |
@click="showAbout = !showAbout" | |
class="px-3 py-1.5 text-sm text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200" | |
title="About this tool" | |
> | |
<svg class="w-5 h-5 inline-block mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path> | |
</svg> | |
<span class="hidden sm:inline">About</span> | |
</button> | |
<button | |
@click="darkMode = !darkMode" | |
class="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200" | |
title="Toggle dark mode" | |
> | |
<svg x-show="!darkMode" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"></path> | |
</svg> | |
<svg x-show="darkMode" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"></path> | |
</svg> | |
</button> | |
<select | |
x-model="diffMode" | |
class="px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" | |
> | |
<option value="char">Character Diff</option> | |
<option value="word">Word Diff</option> | |
<option value="line">Line Diff</option> | |
<option value="markdown" x-show="hasMarkdown">Markdown Diff</option> | |
</select> | |
<button | |
x-show="hasMarkdown" | |
@click="renderMarkdown = !renderMarkdown" | |
:class="renderMarkdown ? 'bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300' : 'text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200'" | |
class="p-2 rounded-md transition-colors" | |
title="Toggle markdown rendering" | |
> | |
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path> | |
</svg> | |
</button> | |
<button | |
@click="exportComparison()" | |
class="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200" | |
title="Export comparison" | |
> | |
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path> | |
</svg> | |
</button> | |
</div> | |
</div> | |
</div> | |
</header> | |
<!-- About Panel --> | |
<div x-show="showAbout" x-transition class="bg-blue-50 dark:bg-blue-950/30 border-b border-blue-200 dark:border-blue-800"> | |
<div class="px-4 sm:px-6 lg:px-8 py-6"> | |
<div class="max-w-4xl"> | |
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-3">About OCR Time Capsule</h2> | |
<div class="prose prose-sm dark:prose-invert text-gray-700 dark:text-gray-300"> | |
<p class="mb-3"> | |
OCR Time Capsule helps researchers and digital humanities professionals compare original OCR text | |
with AI-improved versions from historical documents. This tool is designed for browsing pre-processed | |
OCR improvements stored in HuggingFace datasets. | |
</p> | |
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4"> | |
<div> | |
<h3 class="font-medium mb-2">π― Use Cases</h3> | |
<ul class="list-disc list-inside space-y-1 text-sm"> | |
<li>Review OCR corrections from historical newspapers</li> | |
<li>Quality assessment of digitization projects</li> | |
<li>Training data validation for OCR models</li> | |
<li>Accessibility improvements for scanned texts</li> | |
</ul> | |
</div> | |
<div> | |
<h3 class="font-medium mb-2">β‘ Key Features</h3> | |
<ul class="list-disc list-inside space-y-1 text-sm"> | |
<li>Side-by-side text comparison</li> | |
<li>Character, word, and line-level diffs</li> | |
<li>Keyboard navigation (J/K or arrows)</li> | |
<li>Direct HuggingFace dataset integration</li> | |
</ul> | |
</div> | |
</div> | |
<p class="text-sm"> | |
π‘ <strong>Tip:</strong> For live OCR processing with vision-language models, check out | |
<a href="https://huggingface.co/spaces/davanstrien/ocr-time-machine" class="text-blue-600 dark:text-blue-400 underline">OCR Time Machine</a>. | |
OCR Time Capsule focuses on exploring already-processed datasets for faster navigation and analysis. | |
</p> | |
</div> | |
<button | |
@click="showAbout = false" | |
class="mt-4 text-sm text-blue-600 dark:text-blue-400 hover:underline" | |
> | |
Close | |
</button> | |
</div> | |
</div> | |
</div> | |
<!-- Main Content --> | |
<main class="h-full flex"> | |
<!-- Loading State --> | |
<div x-show="loading" class="flex-1 flex items-center justify-center"> | |
<div class="text-center"> | |
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div> | |
<p class="mt-4 text-gray-600 dark:text-gray-400">Loading dataset...</p> | |
</div> | |
</div> | |
<!-- Error State --> | |
<div x-show="error" class="flex-1 flex items-center justify-center p-8"> | |
<div class="max-w-md w-full bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-6"> | |
<h3 class="text-lg font-medium text-red-800 dark:text-red-400 mb-2">Error</h3> | |
<p class="text-red-600 dark:text-red-300" x-text="error"></p> | |
<button | |
@click="error = null; loadDataset()" | |
class="mt-4 px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700" | |
> | |
Try Again | |
</button> | |
</div> | |
</div> | |
<!-- Content Area --> | |
<div x-show="!loading && !error && currentSample" class="flex-1 flex h-full"> | |
<!-- Image Panel --> | |
<div class="w-1/3 bg-gray-100 dark:bg-gray-800 p-4 overflow-y-auto border-r border-gray-200 dark:border-gray-700"> | |
<div> | |
<div class="bg-white dark:bg-gray-700 rounded-lg shadow-sm overflow-hidden"> | |
<img | |
:src="getImageSrc()" | |
:alt="`Page ${currentIndex + 1}`" | |
class="w-full h-auto" | |
@error="handleImageError" | |
> | |
<div class="p-3 border-t border-gray-200 dark:border-gray-600"> | |
<p class="text-sm text-gray-600 dark:text-gray-400"> | |
<span x-text="`Page ${currentIndex + 1} of ${totalSamples || '?'}`"></span> | |
<span x-show="getImageDimensions()" class="ml-2"> | |
β’ <span x-text="getImageDimensions()"></span> | |
</span> | |
</p> | |
</div> | |
</div> | |
<!-- Model Info Panel --> | |
<div x-show="modelInfo" x-transition class="mt-4 bg-white dark:bg-gray-700 rounded-lg shadow-sm p-4"> | |
<h3 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3 flex items-center"> | |
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path> | |
</svg> | |
Model Information | |
</h3> | |
<div class="space-y-2 text-xs"> | |
<div class="flex justify-between items-center"> | |
<span class="text-gray-600 dark:text-gray-400">Model</span> | |
<span class="font-medium text-gray-900 dark:text-gray-100" x-text="modelInfo?.modelName || '-'"></span> | |
</div> | |
<div x-show="modelInfo?.processingDate" class="flex justify-between items-center"> | |
<span class="text-gray-600 dark:text-gray-400">Processed</span> | |
<span class="text-gray-900 dark:text-gray-100" x-text="modelInfo?.processingDate || '-'"></span> | |
</div> | |
<div x-show="modelInfo?.batchSize" class="flex justify-between items-center"> | |
<span class="text-gray-600 dark:text-gray-400">Batch Size</span> | |
<span class="text-gray-900 dark:text-gray-100" x-text="modelInfo?.batchSize || '-'"></span> | |
</div> | |
<div x-show="modelInfo?.maxTokens" class="flex justify-between items-center"> | |
<span class="text-gray-600 dark:text-gray-400">Max Tokens</span> | |
<span class="text-gray-900 dark:text-gray-100" x-text="modelInfo?.maxTokens?.toLocaleString() || '-'"></span> | |
</div> | |
<div x-show="modelInfo?.scriptUrl" class="mt-2 pt-2 border-t border-gray-200 dark:border-gray-600"> | |
<a :href="modelInfo?.scriptUrl" | |
target="_blank" | |
class="text-blue-600 dark:text-blue-400 hover:underline flex items-center"> | |
<svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"></path> | |
</svg> | |
View Script | |
</a> | |
</div> | |
</div> | |
</div> | |
<!-- Statistics Panel --> | |
<div class="mt-4 bg-white dark:bg-gray-700 rounded-lg shadow-sm p-4"> | |
<h3 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">OCR Quality Metrics</h3> | |
<div class="space-y-2"> | |
<div class="flex justify-between items-center"> | |
<span class="text-xs text-gray-600 dark:text-gray-400">Similarity</span> | |
<span class="text-xs font-medium text-gray-900 dark:text-gray-100" x-text="`${similarity}%`"></span> | |
</div> | |
<div class="w-full bg-gray-200 dark:bg-gray-600 rounded-full h-2"> | |
<div class="bg-blue-600 h-2 rounded-full transition-all duration-300" :style="`width: ${similarity}%`"></div> | |
</div> | |
<div class="grid grid-cols-3 gap-2 mt-3"> | |
<div class="text-center"> | |
<div class="text-lg font-semibold text-gray-900 dark:text-gray-100" x-text="charStats.total || '-'"></div> | |
<div class="text-xs text-gray-600 dark:text-gray-400">Characters</div> | |
</div> | |
<div class="text-center"> | |
<div class="text-lg font-semibold text-green-600 dark:text-green-400" x-text="charStats.added || '0'"></div> | |
<div class="text-xs text-gray-600 dark:text-gray-400">Added</div> | |
</div> | |
<div class="text-center"> | |
<div class="text-lg font-semibold text-red-600 dark:text-red-400" x-text="charStats.removed || '0'"></div> | |
<div class="text-xs text-gray-600 dark:text-gray-400">Removed</div> | |
</div> | |
</div> | |
<div class="mt-3 pt-3 border-t border-gray-200 dark:border-gray-600"> | |
<div class="flex justify-between text-xs"> | |
<span class="text-gray-600 dark:text-gray-400">Words</span> | |
<span class="text-gray-900 dark:text-gray-100"> | |
<span x-text="wordStats.original || '-'"></span> β <span x-text="wordStats.improved || '-'"></span> | |
</span> | |
</div> | |
<div x-show="hasMarkdown" class="mt-2 flex items-center justify-center"> | |
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200"> | |
<svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path> | |
</svg> | |
Markdown Detected | |
</span> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- Text Comparison Panel --> | |
<div class="flex-1 bg-white dark:bg-gray-900 overflow-hidden"> | |
<!-- Tab Navigation --> | |
<div class="border-b border-gray-200 dark:border-gray-700"> | |
<nav class="flex -mb-px"> | |
<button | |
@click="activeTab = 'comparison'" | |
:class="activeTab === 'comparison' ? 'border-blue-500 text-blue-600 dark:text-blue-400' : 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200'" | |
class="px-6 py-3 border-b-2 font-medium text-sm transition-colors" | |
> | |
Side by Side | |
</button> | |
<button | |
@click="activeTab = 'diff'" | |
:class="activeTab === 'diff' ? 'border-blue-500 text-blue-600 dark:text-blue-400' : 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200'" | |
class="px-6 py-3 border-b-2 font-medium text-sm transition-colors" | |
> | |
Inline Diff | |
</button> | |
<button | |
@click="activeTab = 'improved'" | |
:class="activeTab === 'improved' ? 'border-blue-500 text-blue-600 dark:text-blue-400' : 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200'" | |
class="px-6 py-3 border-b-2 font-medium text-sm transition-colors" | |
> | |
Improved Only | |
</button> | |
</nav> | |
</div> | |
<!-- Tab Content --> | |
<div class="p-6 overflow-y-auto" style="height: calc(100% - 49px);"> | |
<!-- Side by Side Comparison --> | |
<div x-show="activeTab === 'comparison'" class="grid grid-cols-2 gap-6"> | |
<div> | |
<h3 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">Original OCR</h3> | |
<div class="prose prose-sm dark:prose-invert max-w-none"> | |
<pre class="whitespace-pre-wrap font-mono text-xs bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-gray-100 p-4 rounded-lg" x-text="getOriginalText()"></pre> | |
</div> | |
</div> | |
<div> | |
<h3 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3"> | |
Improved OCR | |
<span x-show="renderMarkdown && hasMarkdown" class="ml-2 text-xs bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 px-2 py-1 rounded"> | |
Markdown | |
</span> | |
</h3> | |
<div class="max-w-none"> | |
<div x-show="!renderMarkdown"> | |
<pre class="whitespace-pre-wrap font-mono text-xs bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-gray-100 p-4 rounded-lg" x-text="getImprovedText()"></pre> | |
</div> | |
<div x-show="renderMarkdown" x-html="getImprovedTextRendered()" class="markdown-content"></div> | |
</div> | |
</div> | |
</div> | |
<!-- Inline Diff --> | |
<div x-show="activeTab === 'diff'" class="prose prose-sm dark:prose-invert max-w-none"> | |
<div x-html="diffHtml" class="diff-content"></div> | |
</div> | |
<!-- Improved Only --> | |
<div x-show="activeTab === 'improved'" class="max-w-none"> | |
<div x-show="!renderMarkdown"> | |
<pre class="whitespace-pre-wrap font-mono text-xs bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-gray-100 p-4 rounded-lg" x-text="getImprovedText()"></pre> | |
</div> | |
<div x-show="renderMarkdown" x-html="getImprovedTextRendered()" class="markdown-content"></div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</main> | |
<!-- Navigation Footer --> | |
<footer x-show="!loading && !error && currentSample" class="fixed bottom-0 left-0 right-0"> | |
<!-- Enhanced Visual Page Browser --> | |
<div x-show="showDock" | |
x-transition:enter="transition ease-out duration-300" | |
x-transition:enter-start="transform translate-y-full opacity-0" | |
x-transition:enter-end="transform translate-y-0 opacity-100" | |
x-transition:leave="transition ease-in duration-200" | |
x-transition:leave-start="transform translate-y-0 opacity-100" | |
x-transition:leave-end="transform translate-y-full opacity-0" | |
@mouseenter="showDockPreview()" | |
@mouseleave="hideDockPreview()" | |
class="bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 shadow-lg"> | |
<div class="relative px-16 py-4"> | |
<!-- Left scroll button --> | |
<button @click="scrollDockLeft()" | |
:disabled="dockStartIndex <= 0" | |
class="absolute left-2 top-1/2 -translate-y-1/2 p-2 rounded-full bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 disabled:opacity-50 disabled:cursor-not-allowed transition-all"> | |
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path> | |
</svg> | |
</button> | |
<!-- Thumbnails container --> | |
<div class="flex items-center justify-center space-x-3 overflow-hidden"> | |
<template x-for="item in dockItems" :key="item.index"> | |
<div @click="jumpToDockPage(item.index)" | |
class="relative cursor-pointer transition-all duration-200 hover:scale-105 flex-shrink-0" | |
:class="item.index === currentIndex ? 'ring-2 ring-blue-500 scale-105' : ''"> | |
<img :src="item.imageSrc" | |
:alt="`Page ${item.index + 1}`" | |
class="w-32 h-44 object-cover rounded shadow-lg" | |
@error="handleFlowImageError($event, item.index)"> | |
<div class="absolute bottom-0 inset-x-0 bg-gradient-to-t from-black/80 to-transparent p-2 rounded-b"> | |
<p class="text-sm text-white font-medium text-center" x-text="`${item.index + 1}`"></p> | |
</div> | |
</div> | |
</template> | |
</div> | |
<!-- Right scroll button --> | |
<button @click="scrollDockRight()" | |
:disabled="dockStartIndex >= totalSamples - dockVisibleCount" | |
class="absolute right-2 top-1/2 -translate-y-1/2 p-2 rounded-full bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 disabled:opacity-50 disabled:cursor-not-allowed transition-all"> | |
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path> | |
</svg> | |
</button> | |
<!-- Position indicator --> | |
<div class="absolute bottom-1 left-1/2 -translate-x-1/2 flex items-center space-x-2"> | |
<div class="w-32 h-1 bg-gray-200 dark:bg-gray-600 rounded-full overflow-hidden"> | |
<div class="h-full bg-blue-500 transition-all duration-300" | |
:style="`width: ${((dockStartIndex + Math.floor(dockVisibleCount/2)) / (totalSamples - 1)) * 100}%`"></div> | |
</div> | |
<span class="text-xs text-gray-500 dark:text-gray-400" | |
x-text="`${dockStartIndex + 1}-${Math.min(dockStartIndex + dockVisibleCount, totalSamples)} of ${totalSamples}`"></span> | |
</div> | |
</div> | |
</div> | |
<!-- Navigation controls --> | |
<div class="bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 px-6 py-3"> | |
<div class="flex items-center justify-between"> | |
<div class="flex items-center space-x-4"> | |
<button | |
@click="previousSample()" | |
:disabled="currentIndex <= 0" | |
class="px-4 py-2 bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-md hover:bg-gray-300 dark:hover:bg-gray-600 disabled:opacity-50 disabled:cursor-not-allowed" | |
> | |
β Previous | |
</button> | |
<!-- Interactive Page Counter with Dock Trigger --> | |
<div class="relative"> | |
<div @mouseenter="showDockPreview()" | |
@mouseleave="hideDockPreview()" | |
class="flex items-center space-x-2 px-4 py-2 rounded-md cursor-pointer transition-all duration-200 hover:bg-gray-100 dark:hover:bg-gray-700"> | |
<div class="flex items-center space-x-2"> | |
<span class="text-sm font-medium text-gray-700 dark:text-gray-300"> | |
Page <span x-text="currentIndex + 1"></span> of <span x-text="totalSamples || '?'"></span> | |
</span> | |
<svg class="w-4 h-4 text-gray-400 transition-transform duration-200" | |
:class="showDock ? 'rotate-180' : ''" | |
fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7"></path> | |
</svg> | |
</div> | |
<!-- Visual progress bar --> | |
<div class="w-32 h-1 bg-gray-200 dark:bg-gray-600 rounded-full overflow-hidden"> | |
<div class="h-full bg-blue-500 transition-all duration-300" | |
:style="`width: ${(currentIndex / (totalSamples - 1)) * 100}%`"></div> | |
</div> | |
</div> | |
</div> | |
<div class="flex items-center space-x-1"> | |
<input | |
type="number" | |
x-model.number="jumpToPage" | |
@keyup.enter="jumpToSample()" | |
:min="1" | |
:max="totalSamples" | |
class="w-20 px-2 py-1 text-sm border border-gray-300 rounded focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-700 dark:border-gray-600 dark:text-white" | |
placeholder="Go to" | |
> | |
<button | |
@click="jumpToSample()" | |
class="px-2 py-1 text-sm bg-blue-600 text-white rounded hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500" | |
> | |
Go | |
</button> | |
</div> | |
<button | |
@click="nextSample()" | |
:disabled="currentIndex >= (totalSamples - 1)" | |
class="px-4 py-2 bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-md hover:bg-gray-300 dark:hover:bg-gray-600 disabled:opacity-50 disabled:cursor-not-allowed" | |
> | |
Next β | |
</button> | |
</div> | |
<div class="text-sm text-gray-500 dark:text-gray-400"> | |
Press <kbd class="px-2 py-1 bg-gray-100 dark:bg-gray-700 rounded">J</kbd> / <kbd class="px-2 py-1 bg-gray-100 dark:bg-gray-700 rounded">K</kbd> or <kbd class="px-2 py-1 bg-gray-100 dark:bg-gray-700 rounded">β</kbd> / <kbd class="px-2 py-1 bg-gray-100 dark:bg-gray-700 rounded">β</kbd> to navigate | <kbd class="px-2 py-1 bg-gray-100 dark:bg-gray-700 rounded">V</kbd> for visual browser | |
</div> | |
</div> | |
</footer> | |
<!-- Local Scripts --> | |
<script src="js/diff-utils.js"></script> | |
<script src="js/dataset-api.js"></script> | |
<script src="js/app.js"></script> | |
</body> | |
</html> |