CoolFace
Apppublic

uv-scripts/sam3-detection-browser

sourceHugging Faceupdated 6mo agoView on Hugging Face
5likes
index.html954 linesDownload Raw Back to root
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                    &nbsp;&nbsp;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