uv-scripts/sam3-detection-browser
5
1<!DOCTYPE html>2<html lang="en">3<head>4 <meta charset="UTF-8">5 <meta name="viewport" content="width=device-width, initial-scale=1.0">6 <title>SAM3 Results Browser</title>7 <style>8 * {9 margin: 0;10 padding: 0;11 box-sizing: border-box;12 }13 14 body {15 font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;16 line-height: 1.5;17 color: #222;18 background: #fff;19 font-size: 15px;20 }21 22 .container {23 max-width: 1400px;24 margin: 0 auto;25 padding: 40px 20px;26 }27 28 header {29 border-bottom: 1px solid #ddd;30 padding-bottom: 20px;31 margin-bottom: 40px;32 }33 34 h1 {35 color: #222;36 font-weight: 400;37 font-size: 28px;38 margin-bottom: 8px;39 }40 41 .subtitle {42 color: #666;43 font-size: 15px;44 font-weight: 300;45 margin-bottom: 12px;46 }47 48 .header-links {49 font-size: 13px;50 color: #666;51 }52 53 .header-links a {54 color: #222;55 text-decoration: none;56 border-bottom: 1px solid #222;57 }58 59 .header-links a:hover {60 border-bottom-color: #666;61 }62 63 .example-datasets {64 margin-top: 8px;65 font-size: 12px;66 color: #666;67 }68 69 .example-datasets a {70 color: #666;71 text-decoration: none;72 border-bottom: 1px dotted #999;73 cursor: pointer;74 margin-right: 12px;75 }76 77 .example-datasets a:hover {78 color: #222;79 border-bottom-color: #222;80 }81 82 .mode-badge {83 display: inline-block;84 padding: 2px 8px;85 border-radius: 3px;86 font-size: 11px;87 font-weight: 600;88 text-transform: uppercase;89 letter-spacing: 0.5px;90 margin-left: 8px;91 vertical-align: middle;92 }93 94 .mode-badge.detection {95 background: #e8f5e9;96 color: #2e7d32;97 }98 99 .mode-badge.segmentation {100 background: #e3f2fd;101 color: #1565c0;102 }103 104 .controls {105 border: 1px solid #ddd;106 padding: 20px;107 margin-bottom: 40px;108 background: #fafafa;109 }110 111 .control-group {112 margin-bottom: 18px;113 }114 115 .control-group:last-child {116 margin-bottom: 0;117 }118 119 label {120 display: block;121 font-weight: 400;122 margin-bottom: 6px;123 color: #444;124 font-size: 14px;125 }126 127 input[type="text"],128 select {129 width: 100%;130 padding: 8px;131 border: 1px solid #ccc;132 border-radius: 2px;133 font-size: 14px;134 background: white;135 }136 137 input[type="text"]:focus,138 select:focus {139 outline: none;140 border-color: #888;141 }142 143 input[type="range"] {144 width: 100%;145 margin-right: 10px;146 }147 148 .slider-container {149 display: flex;150 align-items: center;151 gap: 15px;152 }153 154 .slider-value {155 min-width: 50px;156 font-weight: 400;157 color: #222;158 }159 160 .checkbox-container {161 display: flex;162 align-items: center;163 gap: 10px;164 }165 166 input[type="checkbox"] {167 width: 18px;168 height: 18px;169 cursor: pointer;170 }171 172 .stats {173 border-top: 1px solid #ddd;174 border-bottom: 1px solid #ddd;175 padding: 25px 0;176 margin-bottom: 40px;177 }178 179 .stats-grid {180 display: grid;181 grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));182 gap: 30px;183 }184 185 .stat-item {186 text-align: center;187 padding: 0;188 }189 190 .stat-value {191 font-size: 36px;192 font-weight: 300;193 color: #222;194 margin-bottom: 4px;195 letter-spacing: -0.5px;196 }197 198 .stat-label {199 font-size: 13px;200 color: #666;201 font-weight: 400;202 text-transform: uppercase;203 letter-spacing: 0.5px;204 }205 206 .loading {207 text-align: center;208 padding: 40px;209 font-size: 14px;210 color: #666;211 }212 213 .error {214 background: #fff;215 border: 1px solid #d00;216 padding: 15px;217 margin: 20px 0;218 color: #d00;219 font-size: 14px;220 }221 222 .image-grid {223 display: grid;224 grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));225 gap: 40px;226 margin-bottom: 40px;227 }228 229 .image-card {230 background: white;231 border: 1px solid #ddd;232 overflow: hidden;233 }234 235 .image-card:hover {236 border-color: #999;237 }238 239 .image-container {240 position: relative;241 width: 100%;242 background: #000;243 }244 245 .image-container img.source-image {246 width: 100%;247 height: auto;248 display: block;249 }250 251 .image-container .mask-overlay {252 position: absolute;253 top: 0;254 left: 0;255 width: 100%;256 height: 100%;257 pointer-events: none;258 z-index: 5;259 mix-blend-mode: normal;260 }261 262 .image-container canvas {263 position: absolute;264 top: 0;265 left: 0;266 width: 100%;267 height: 100%;268 pointer-events: none;269 z-index: 10;270 }271 272 .image-info {273 padding: 12px;274 border-top: 1px solid #eee;275 }276 277 .image-index {278 font-size: 11px;279 color: #999;280 margin-bottom: 6px;281 text-transform: uppercase;282 letter-spacing: 0.5px;283 }284 285 .detections-count {286 font-weight: 400;287 color: #222;288 margin-bottom: 8px;289 font-size: 14px;290 }291 292 .detections-list {293 font-size: 13px;294 color: #666;295 }296 297 .detection-item {298 padding: 3px 0;299 }300 301 .confidence-high {302 color: #27ae60;303 font-weight: 600;304 }305 306 .confidence-medium {307 color: #f39c12;308 font-weight: 600;309 }310 311 .confidence-low {312 color: #e74c3c;313 font-weight: 600;314 }315 316 .pagination {317 display: flex;318 justify-content: center;319 align-items: center;320 gap: 20px;321 margin: 40px 0;322 padding-top: 20px;323 border-top: 1px solid #ddd;324 }325 326 .pagination button {327 padding: 8px 16px;328 border: 1px solid #666;329 background: white;330 color: #222;331 cursor: pointer;332 font-size: 13px;333 transition: all 0.2s;334 }335 336 .pagination button:hover:not(:disabled) {337 background: #222;338 color: white;339 }340 341 .pagination button:disabled {342 border-color: #ddd;343 color: #ccc;344 cursor: not-allowed;345 }346 347 .pagination span {348 color: #666;349 font-size: 13px;350 }351 352 .load-button {353 padding: 10px 20px;354 border: 1px solid #666;355 background: white;356 color: #222;357 cursor: pointer;358 font-size: 14px;359 font-weight: 400;360 transition: all 0.2s;361 width: 100%;362 margin-top: 12px;363 }364 365 .load-button:hover {366 background: #222;367 color: white;368 }369 370 .load-button:disabled {371 border-color: #ddd;372 color: #ccc;373 cursor: not-allowed;374 }375 </style>376</head>377<body>378 <div class="container">379 <header>380 <h1>SAM3 Results Browser <span id="mode-badge" class="mode-badge" style="display:none;"></span></h1>381 <p class="subtitle">Browse object detection and segmentation results from Meta's SAM3 model</p>382 <p class="header-links">Create your own results with the <a href="https://huggingface.co/datasets/uv-scripts/sam3" target="_blank">SAM3 scripts</a></p>383 </header>384 385 <div class="controls">386 <div class="control-group">387 <label for="dataset-id">Dataset ID:</label>388 <input type="text" id="dataset-id" value="davanstrien/newspapers-image-predictions" placeholder="username/dataset-name">389 <div class="example-datasets">390 Detection:391 <a href="#" data-dataset="davanstrien/newspapers-image-predictions">photographs</a>392 <a href="#" data-dataset="davanstrien/newspapers-illustration-predictions">illustrations</a>393 Segmentation:394 <a href="#" data-dataset="davanstrien/sam3-segment-deer-test">deer masks</a>395 </div>396 </div>397 398 <div class="control-group">399 <label for="split">Split:</label>400 <select id="split">401 <option value="train">train</option>402 <option value="validation">validation</option>403 <option value="test">test</option>404 </select>405 </div>406 407 <div class="control-group">408 <label for="confidence-threshold">Confidence Threshold: <span id="confidence-value" class="slider-value">0.50</span></label>409 <div class="slider-container">410 <input type="range" id="confidence-threshold" min="0" max="1" step="0.05" value="0.5">411 </div>412 </div>413 414 <div class="control-group">415 <div class="checkbox-container">416 <input type="checkbox" id="only-detections">417 <label for="only-detections" style="margin-bottom: 0;">Show only images with detections</label>418 </div>419 </div>420 421 <button class="load-button" id="load-dataset">Load Dataset</button>422 </div>423 424 <div id="stats-container"></div>425 <div id="loading" class="loading" style="display: none;">Loading dataset...</div>426 <div id="error" class="error" style="display: none;"></div>427 <div id="image-grid" class="image-grid"></div>428 <div id="pagination" class="pagination" style="display: none;"></div>429 </div>430 431 <script>432 // Global state433 let dataset = [];434 let filteredDataset = [];435 let currentPage = 0;436 const itemsPerPage = 12;437 let confidenceThreshold = 0.5;438 let showOnlyDetections = false;439 // 'detection' = objects column with bbox/score, 'segmentation' = segmentation_map column440 let datasetMode = 'detection';441 442 // Mask overlay colors (RGBA)443 const MASK_COLORS = [444 [231, 76, 60], // red445 [46, 204, 113], // green446 [52, 152, 219], // blue447 [241, 196, 15], // yellow448 [155, 89, 182], // purple449 [230, 126, 34], // orange450 [26, 188, 156], // teal451 [236, 112, 99], // salmon452 ];453 454 // DOM elements455 const datasetIdInput = document.getElementById('dataset-id');456 const splitSelect = document.getElementById('split');457 const confidenceSlider = document.getElementById('confidence-threshold');458 const confidenceValue = document.getElementById('confidence-value');459 const onlyDetectionsCheckbox = document.getElementById('only-detections');460 const loadButton = document.getElementById('load-dataset');461 const statsContainer = document.getElementById('stats-container');462 const loadingDiv = document.getElementById('loading');463 const errorDiv = document.getElementById('error');464 const imageGrid = document.getElementById('image-grid');465 const paginationDiv = document.getElementById('pagination');466 const modeBadge = document.getElementById('mode-badge');467 468 // Initialize state from DOM469 showOnlyDetections = onlyDetectionsCheckbox.checked;470 471 // Event listeners472 confidenceSlider.addEventListener('input', (e) => {473 confidenceThreshold = parseFloat(e.target.value);474 confidenceValue.textContent = confidenceThreshold.toFixed(2);475 if (dataset.length > 0) {476 filterAndRender();477 }478 });479 480 onlyDetectionsCheckbox.addEventListener('change', (e) => {481 showOnlyDetections = e.target.checked;482 if (dataset.length > 0) {483 filterAndRender();484 }485 });486 487 loadButton.addEventListener('click', loadDataset);488 489 // Handle example dataset links490 document.querySelectorAll('.example-datasets a').forEach(link => {491 link.addEventListener('click', (e) => {492 e.preventDefault();493 const datasetId = e.target.getAttribute('data-dataset');494 datasetIdInput.value = datasetId;495 loadDataset();496 });497 });498 499 // Get scores for an item regardless of dataset mode500 function getScores(item) {501 if (datasetMode === 'segmentation') {502 return item.scores || [];503 }504 const objects = item.objects || { bbox: [], category: [], score: [] };505 return objects.score || [];506 }507 508 // Get number of detections above threshold509 function getDetectionCount(item) {510 return getScores(item).filter(s => s >= confidenceThreshold).length;511 }512 513 // Detect dataset mode from first row514 function detectMode(row) {515 if (row.segmentation_map !== undefined) {516 return 'segmentation';517 }518 return 'detection';519 }520 521 // Load dataset from HuggingFace with progressive rendering522 async function loadDataset() {523 const datasetId = datasetIdInput.value.trim();524 const split = splitSelect.value;525 526 if (!datasetId) {527 showError('Please enter a dataset ID');528 return;529 }530 531 loadingDiv.style.display = 'block';532 errorDiv.style.display = 'none';533 statsContainer.innerHTML = '';534 imageGrid.innerHTML = '';535 paginationDiv.style.display = 'none';536 loadButton.disabled = true;537 modeBadge.style.display = 'none';538 539 dataset = [];540 let isLoadingComplete = false;541 542 try {543 let offset = 0;544 const batchSize = 50;545 let hasMore = true;546 let isFirstBatch = true;547 548 while (hasMore) {549 const url = `https://datasets-server.huggingface.co/rows?dataset=${encodeURIComponent(datasetId)}&config=default&split=${split}&offset=${offset}&length=${batchSize}`;550 const response = await fetch(url);551 552 if (!response.ok) {553 if (offset === 0) {554 throw new Error(`Failed to load dataset. Check dataset ID and split name.`);555 }556 break;557 }558 559 const data = await response.json();560 561 if (!data.rows || data.rows.length === 0) {562 hasMore = false;563 break;564 }565 566 // Detect mode from first row567 if (isFirstBatch) {568 datasetMode = detectMode(data.rows[0].row);569 modeBadge.textContent = datasetMode;570 modeBadge.className = `mode-badge ${datasetMode}`;571 modeBadge.style.display = 'inline-block';572 }573 574 const newRows = data.rows.map(item => ({575 index: offset + item.row_idx,576 image: item.row.image,577 objects: item.row.objects || { bbox: [], category: [], score: [] },578 ...item.row579 }));580 581 dataset = dataset.concat(newRows);582 offset += data.rows.length;583 584 if (isFirstBatch) {585 filterAndRender();586 loadingDiv.textContent = `Loading more... (${dataset.length} rows loaded)`;587 isFirstBatch = false;588 } else {589 filterDataset();590 renderStats();591 if (currentPage === 0) {592 renderPage();593 renderPagination();594 }595 loadingDiv.textContent = `Loading more... (${dataset.length} rows loaded)`;596 }597 598 if (data.rows.length < batchSize) {599 hasMore = false;600 }601 602 if (dataset.length >= 10000) {603 hasMore = false;604 }605 }606 607 if (dataset.length === 0) {608 throw new Error('No data found in dataset');609 }610 611 isLoadingComplete = true;612 loadingDiv.style.display = 'none';613 filterAndRender();614 615 } catch (error) {616 loadingDiv.style.display = 'none';617 showError(`Failed to load dataset: ${error.message}`);618 } finally {619 loadButton.disabled = false;620 if (isLoadingComplete) {621 loadingDiv.textContent = 'Loading dataset...';622 }623 }624 }625 626 // Filter dataset based on current filters627 function filterDataset() {628 filteredDataset = dataset.filter(item => {629 const scores = getScores(item);630 const validDetections = scores.filter(score => score >= confidenceThreshold);631 if (showOnlyDetections && validDetections.length === 0) {632 return false;633 }634 return true;635 });636 }637 638 // Filter and render639 function filterAndRender() {640 filterDataset();641 currentPage = 0;642 renderStats();643 renderPage();644 renderPagination();645 }646 647 // Render statistics648 function renderStats() {649 const totalImages = filteredDataset.length;650 const totalDetections = filteredDataset.reduce((sum, item) => {651 return sum + getDetectionCount(item);652 }, 0);653 654 const imagesWithDetections = filteredDataset.filter(item => {655 return getDetectionCount(item) > 0;656 }).length;657 658 const avgDetections = totalImages > 0 ? (totalDetections / totalImages).toFixed(2) : 0;659 660 const modeLabel = datasetMode === 'segmentation' ? 'Instances' : 'Detections';661 662 statsContainer.innerHTML = `663 <div class="stats">664 <div class="stats-grid">665 <div class="stat-item">666 <div class="stat-value">${filteredDataset.length}</div>667 <div class="stat-label">Filtered Images</div>668 </div>669 <div class="stat-item">670 <div class="stat-value">${imagesWithDetections}</div>671 <div class="stat-label">Images with ${modeLabel}</div>672 </div>673 <div class="stat-item">674 <div class="stat-value">${totalDetections}</div>675 <div class="stat-label">Total ${modeLabel}</div>676 </div>677 <div class="stat-item">678 <div class="stat-value">${avgDetections}</div>679 <div class="stat-label">Avg per Image</div>680 </div>681 </div>682 </div>683 `;684 }685 686 // Render current page687 function renderPage() {688 const start = currentPage * itemsPerPage;689 const end = start + itemsPerPage;690 const pageItems = filteredDataset.slice(start, end);691 692 imageGrid.innerHTML = pageItems.map(item => {693 const scores = getScores(item);694 const validDetections = scores695 .map((score, idx) => ({ score, idx }))696 .filter(({ score }) => score >= confidenceThreshold);697 698 const imageUrl = typeof item.image === 'object' ? item.image?.src : item.image;699 700 // For segmentation, get mask image URL701 let maskUrl = '';702 if (datasetMode === 'segmentation' && item.segmentation_map) {703 maskUrl = typeof item.segmentation_map === 'object' ? item.segmentation_map?.src : item.segmentation_map;704 }705 706 const countLabel = datasetMode === 'segmentation' ? 'instance(s)' : 'detection(s)';707 708 return `709 <div class="image-card">710 <div class="image-container" id="container-${item.index}">711 <img class="source-image" src="${imageUrl}" alt="Image ${item.index}" crossorigin="anonymous"712 onload="onImageLoaded(${item.index})">713 ${maskUrl ? `<img class="mask-overlay" src="${maskUrl}" crossorigin="anonymous"714 data-mask-url="${maskUrl}" style="display:none;"715 onload="onMaskLoaded(${item.index})">` : ''}716 <canvas id="canvas-${item.index}"></canvas>717 </div>718 <div class="image-info">719 <div class="image-index">Image #${item.index}</div>720 <div class="detections-count">${validDetections.length} ${countLabel}</div>721 <div class="detections-list">722 ${validDetections.map(({ score, idx }) => {723 const confidenceClass = score >= 0.7 ? 'confidence-high' : score >= 0.4 ? 'confidence-medium' : 'confidence-low';724 return `725 <div class="detection-item">726 <span class="${confidenceClass}">${(score * 100).toFixed(1)}%</span>727 confidence728 </div>729 `;730 }).join('')}731 </div>732 </div>733 </div>734 `;735 }).join('');736 }737 738 // Called when the source image loads739 window.onImageLoaded = function(itemIndex) {740 if (datasetMode === 'detection') {741 drawBoundingBoxes(itemIndex);742 } else {743 // For segmentation, draw boxes if present; mask overlay handled separately744 drawSegmentationBoxes(itemIndex);745 }746 };747 748 // Called when the mask image loads - colorize and overlay it749 window.onMaskLoaded = function(itemIndex) {750 const container = document.getElementById(`container-${itemIndex}`);751 const canvas = document.getElementById(`canvas-${itemIndex}`);752 const sourceImg = container.querySelector('img.source-image');753 const maskImg = container.querySelector('img.mask-overlay');754 755 if (!canvas || !sourceImg || !maskImg) return;756 757 canvas.width = sourceImg.naturalWidth;758 canvas.height = sourceImg.naturalHeight;759 const ctx = canvas.getContext('2d');760 761 // Draw the mask onto a temporary canvas to read pixel data762 const tempCanvas = document.createElement('canvas');763 tempCanvas.width = maskImg.naturalWidth;764 tempCanvas.height = maskImg.naturalHeight;765 const tempCtx = tempCanvas.getContext('2d');766 tempCtx.drawImage(maskImg, 0, 0);767 768 let maskData;769 try {770 maskData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);771 } catch (e) {772 console.warn('Cannot read mask pixels (CORS):', e);773 return;774 }775 776 // Create colored overlay - each unique non-zero pixel value gets a color777 const overlay = ctx.createImageData(canvas.width, canvas.height);778 779 // Scale factors if mask and source image differ in size780 const scaleX = tempCanvas.width / canvas.width;781 const scaleY = tempCanvas.height / canvas.height;782 783 for (let y = 0; y < canvas.height; y++) {784 for (let x = 0; x < canvas.width; x++) {785 const mx = Math.floor(x * scaleX);786 const my = Math.floor(y * scaleY);787 const mi = (my * tempCanvas.width + mx) * 4;788 // Segmentation maps are grayscale: R channel has the instance ID789 const instanceId = maskData.data[mi];790 791 const oi = (y * canvas.width + x) * 4;792 if (instanceId > 0) {793 const color = MASK_COLORS[(instanceId - 1) % MASK_COLORS.length];794 overlay.data[oi] = color[0];795 overlay.data[oi + 1] = color[1];796 overlay.data[oi + 2] = color[2];797 overlay.data[oi + 3] = 100; // semi-transparent798 }799 }800 }801 802 ctx.putImageData(overlay, 0, 0);803 804 // Now draw bounding boxes on top if present805 drawSegmentationBoxes(itemIndex);806 };807 808 // Draw bounding boxes for detection datasets (objects column)809 window.drawBoundingBoxes = function(itemIndex) {810 const item = dataset.find(d => d.index === itemIndex);811 if (!item) return;812 813 const container = document.getElementById(`container-${itemIndex}`);814 const canvas = document.getElementById(`canvas-${itemIndex}`);815 const img = container.querySelector('img.source-image');816 817 if (!canvas || !img) return;818 819 canvas.width = img.naturalWidth;820 canvas.height = img.naturalHeight;821 822 const ctx = canvas.getContext('2d');823 ctx.clearRect(0, 0, canvas.width, canvas.height);824 825 const objects = item.objects || { bbox: [], category: [], score: [] };826 827 objects.bbox.forEach((bbox, idx) => {828 const score = objects.score[idx];829 if (score < confidenceThreshold) return;830 831 const [x, y, width, height] = bbox;832 833 let color;834 if (score >= 0.7) {835 color = '#27ae60';836 } else if (score >= 0.4) {837 color = '#f39c12';838 } else {839 color = '#e74c3c';840 }841 842 ctx.strokeStyle = color;843 ctx.lineWidth = 3;844 ctx.strokeRect(x, y, width, height);845 846 const label = `${(score * 100).toFixed(1)}%`;847 ctx.font = 'bold 16px Arial';848 const textWidth = ctx.measureText(label).width;849 const textHeight = 20;850 851 ctx.fillStyle = color;852 ctx.fillRect(x, y - textHeight - 4, textWidth + 10, textHeight + 4);853 854 ctx.fillStyle = 'white';855 ctx.fillText(label, x + 5, y - 8);856 });857 };858 859 // Draw bounding boxes for segmentation datasets (boxes column at top level)860 window.drawSegmentationBoxes = function(itemIndex) {861 const item = dataset.find(d => d.index === itemIndex);862 if (!item || !item.boxes) return;863 864 const container = document.getElementById(`container-${itemIndex}`);865 const canvas = document.getElementById(`canvas-${itemIndex}`);866 const img = container.querySelector('img.source-image');867 868 if (!canvas || !img) return;869 870 // Don't clear - mask overlay may already be drawn871 const ctx = canvas.getContext('2d');872 const scores = item.scores || [];873 874 item.boxes.forEach((bbox, idx) => {875 const score = scores[idx] !== undefined ? scores[idx] : 1.0;876 if (score < confidenceThreshold) return;877 878 const [x, y, width, height] = bbox;879 880 let color;881 if (score >= 0.7) {882 color = '#27ae60';883 } else if (score >= 0.4) {884 color = '#f39c12';885 } else {886 color = '#e74c3c';887 }888 889 ctx.strokeStyle = color;890 ctx.lineWidth = 3;891 ctx.strokeRect(x, y, width, height);892 893 const label = `${(score * 100).toFixed(1)}%`;894 ctx.font = 'bold 16px Arial';895 const textWidth = ctx.measureText(label).width;896 const textHeight = 20;897 898 ctx.fillStyle = color;899 ctx.fillRect(x, y - textHeight - 4, textWidth + 10, textHeight + 4);900 901 ctx.fillStyle = 'white';902 ctx.fillText(label, x + 5, y - 8);903 });904 };905 906 // Render pagination907 function renderPagination() {908 const totalPages = Math.ceil(filteredDataset.length / itemsPerPage);909 910 if (totalPages <= 1) {911 paginationDiv.style.display = 'none';912 return;913 }914 915 paginationDiv.style.display = 'flex';916 paginationDiv.innerHTML = `917 <button id="prev-btn" ${currentPage === 0 ? 'disabled' : ''}>Previous</button>918 <span>Page ${currentPage + 1} of ${totalPages}</span>919 <button id="next-btn" ${currentPage >= totalPages - 1 ? 'disabled' : ''}>Next</button>920 `;921 922 document.getElementById('prev-btn').addEventListener('click', () => {923 if (currentPage > 0) {924 currentPage--;925 renderPage();926 renderPagination();927 window.scrollTo({ top: 0, behavior: 'smooth' });928 }929 });930 931 document.getElementById('next-btn').addEventListener('click', () => {932 if (currentPage < totalPages - 1) {933 currentPage++;934 renderPage();935 renderPagination();936 window.scrollTo({ top: 0, behavior: 'smooth' });937 }938 });939 }940 941 // Show error message942 function showError(message) {943 errorDiv.textContent = message;944 errorDiv.style.display = 'block';945 }946 947 // Auto-load default dataset on page load948 window.addEventListener('load', () => {949 loadDataset();950 });951 </script>952</body>953</html>954 