ocr-time-capsule / index.html
davanstrien's picture
davanstrien HF Staff
Add support for davanstrien/rolm-test dataset with model info display
1e32a60
<!DOCTYPE html>
<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>