Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Anycoder - Dataset Explorer</title> | |
| <!-- Google Fonts --> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"> | |
| <!-- Phosphor Icons --> | |
| <script src="https://unpkg.com/@phosphor-icons/web"></script> | |
| <style> | |
| /* --- CSS Variables & Reset --- */ | |
| :root { | |
| /* Modern Palette */ | |
| --bg-body: #0f1115; | |
| --bg-sidebar: #161b22; | |
| --bg-card: #1c2128; | |
| --bg-hover: #2a303c; | |
| --text-main: #f0f6fc; | |
| --text-muted: #8b949e; | |
| --accent-primary: #58a6ff; /* GitHub Blue */ | |
| --accent-secondary: #3fb950; /* Success Green */ | |
| --accent-warn: #d29922; | |
| --accent-danger: #f85149; | |
| --border-subtle: #30363d; | |
| --border-focus: #58a6ff; | |
| --radius-md: 8px; | |
| --radius-lg: 12px; | |
| --shadow-card: 0 4px 12px rgba(0, 0, 0, 0.4); | |
| --header-height: 64px; | |
| --sidebar-width: 280px; | |
| } | |
| * { box-sizing: border-box; margin: 0; padding: 0; outline: none; } | |
| body { | |
| font-family: 'Inter', sans-serif; | |
| background-color: var(--bg-body); | |
| color: var(--text-main); | |
| height: 100vh; | |
| display: flex; | |
| flex-direction: column; | |
| overflow: hidden; | |
| -webkit-font-smoothing: antialiased; | |
| } | |
| /* --- Scrollbar --- */ | |
| ::-webkit-scrollbar { width: 8px; height: 8px; } | |
| ::-webkit-scrollbar-track { background: var(--bg-body); } | |
| ::-webkit-scrollbar-thumb { background: var(--border-subtle); border-radius: 4px; } | |
| ::-webkit-scrollbar-thumb:hover { background: var(--text-muted); } | |
| /* --- Header --- */ | |
| .header { | |
| height: var(--header-height); | |
| background: var(--bg-sidebar); | |
| border-bottom: 1px solid var(--border-subtle); | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 0 20px; | |
| flex-shrink: 0; | |
| z-index: 50; | |
| } | |
| .header-left { display: flex; align-items: center; gap: 16px; } | |
| .btn-icon { | |
| background: transparent; | |
| border: none; | |
| color: var(--text-muted); | |
| font-size: 1.25rem; | |
| cursor: pointer; | |
| padding: 6px; | |
| border-radius: var(--radius-md); | |
| transition: all 0.2s; | |
| } | |
| .btn-icon:hover { background: var(--bg-hover); color: var(--text-main); } | |
| .logo { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| font-weight: 700; | |
| font-size: 1.1rem; | |
| color: var(--text-main); | |
| } | |
| .logo i { color: var(--accent-primary); font-size: 1.5rem; } | |
| .header-actions { display: flex; align-items: center; gap: 12px; } | |
| .btn { | |
| padding: 8px 16px; | |
| border-radius: var(--radius-md); | |
| font-size: 0.85rem; | |
| font-weight: 500; | |
| cursor: pointer; | |
| border: 1px solid var(--border-subtle); | |
| background: var(--bg-card); | |
| color: var(--text-main); | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 8px; | |
| transition: all 0.2s ease; | |
| } | |
| .btn:hover { background: var(--bg-hover); border-color: var(--text-muted); } | |
| .btn-primary { background: var(--text-main); color: var(--bg-body); border-color: var(--text-main); } | |
| .btn-primary:hover { opacity: 0.9; } | |
| /* --- Layout --- */ | |
| .app-container { | |
| display: flex; | |
| flex: 1; | |
| overflow: hidden; | |
| position: relative; | |
| } | |
| /* --- Sidebar --- */ | |
| .sidebar { | |
| width: var(--sidebar-width); | |
| background: var(--bg-sidebar); | |
| border-right: 1px solid var(--border-subtle); | |
| display: flex; | |
| flex-direction: column; | |
| padding: 20px; | |
| overflow-y: auto; | |
| transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |
| z-index: 40; | |
| } | |
| .sidebar-section { margin-bottom: 24px; } | |
| .sidebar-title { | |
| font-size: 0.7rem; | |
| text-transform: uppercase; | |
| letter-spacing: 0.05em; | |
| color: var(--text-muted); | |
| margin-bottom: 12px; | |
| font-weight: 600; | |
| } | |
| .file-list { display: flex; flex-direction: column; gap: 4px; } | |
| .file-item { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| padding: 8px 12px; | |
| border-radius: var(--radius-md); | |
| color: var(--text-muted); | |
| font-size: 0.9rem; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| } | |
| .file-item:hover { background: var(--bg-hover); color: var(--text-main); } | |
| .file-item.active { background: rgba(88, 166, 255, 0.1); color: var(--accent-primary); font-weight: 500; } | |
| .file-item i { font-size: 1.1rem; } | |
| .info-card { | |
| background: var(--bg-card); | |
| padding: 16px; | |
| border-radius: var(--radius-md); | |
| border: 1px solid var(--border-subtle); | |
| } | |
| .info-row { display: flex; justify-content: space-between; margin-bottom: 8px; font-size: 0.85rem; } | |
| .info-row:last-child { margin-bottom: 0; } | |
| .info-label { color: var(--text-muted); } | |
| .info-val { font-weight: 500; color: var(--text-main); } | |
| /* --- Main Content --- */ | |
| .main-content { | |
| flex: 1; | |
| display: flex; | |
| flex-direction: column; | |
| background: var(--bg-body); | |
| overflow: hidden; | |
| position: relative; | |
| } | |
| /* Toolbar */ | |
| .toolbar { | |
| height: 60px; | |
| border-bottom: 1px solid var(--border-subtle); | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 0 20px; | |
| background: var(--bg-body); | |
| } | |
| .toolbar-group { display: flex; align-items: center; gap: 16px; } | |
| .select-control { | |
| background: var(--bg-card); | |
| border: 1px solid var(--border-subtle); | |
| color: var(--text-main); | |
| padding: 6px 12px; | |
| border-radius: var(--radius-md); | |
| font-size: 0.9rem; | |
| cursor: pointer; | |
| } | |
| .search-wrapper { | |
| position: relative; | |
| width: 300px; | |
| } | |
| .search-wrapper i { | |
| position: absolute; | |
| left: 12px; | |
| top: 50%; | |
| transform: translateY(-50%); | |
| color: var(--text-muted); | |
| } | |
| .search-input { | |
| width: 100%; | |
| background: var(--bg-card); | |
| border: 1px solid var(--border-subtle); | |
| color: var(--text-main); | |
| padding: 8px 12px 8px 36px; | |
| border-radius: var(--radius-md); | |
| font-size: 0.9rem; | |
| transition: border-color 0.2s; | |
| } | |
| .search-input:focus { border-color: var(--accent-primary); } | |
| /* Table Area */ | |
| .table-wrapper { | |
| flex: 1; | |
| overflow: auto; | |
| position: relative; | |
| } | |
| table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| font-size: 0.85rem; | |
| min-width: 800px; | |
| } | |
| th { | |
| background: var(--bg-sidebar); | |
| color: var(--text-muted); | |
| font-weight: 600; | |
| text-align: left; | |
| padding: 12px 20px; | |
| position: sticky; | |
| top: 0; | |
| z-index: 10; | |
| border-bottom: 1px solid var(--border-subtle); | |
| cursor: pointer; | |
| user-select: none; | |
| } | |
| th:hover { color: var(--text-main); background: #1a1e23; } | |
| th i { margin-left: 6px; font-size: 0.8rem; opacity: 0.5; } | |
| td { | |
| padding: 12px 20px; | |
| border-bottom: 1px solid var(--border-subtle); | |
| color: var(--text-main); | |
| vertical-align: middle; | |
| max-width: 300px; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| white-space: nowrap; | |
| } | |
| tr:hover td { background: rgba(255, 255, 255, 0.02); } | |
| /* Cell Types */ | |
| .cell-img { | |
| width: 40px; | |
| height: 40px; | |
| border-radius: 6px; | |
| object-fit: cover; | |
| border: 1px solid var(--border-subtle); | |
| background: #000; | |
| } | |
| .cell-link { | |
| color: var(--accent-primary); | |
| text-decoration: none; | |
| cursor: pointer; | |
| } | |
| .cell-link:hover { text-decoration: underline; } | |
| /* Pagination */ | |
| .pagination { | |
| height: 50px; | |
| border-top: 1px solid var(--border-subtle); | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 0 20px; | |
| background: var(--bg-sidebar); | |
| font-size: 0.85rem; | |
| color: var(--text-muted); | |
| } | |
| /* Empty State */ | |
| .empty-state { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| height: 100%; | |
| color: var(--text-muted); | |
| text-align: center; | |
| } | |
| .empty-state i { font-size: 3rem; margin-bottom: 16px; color: var(--border-subtle); opacity: 0.5; } | |
| /* Modal */ | |
| .modal-overlay { | |
| position: fixed; | |
| top: 0; left: 0; width: 100%; height: 100%; | |
| background: rgba(0,0,0,0.7); | |
| backdrop-filter: blur(4px); | |
| z-index: 100; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| opacity: 0; | |
| pointer-events: none; | |
| transition: opacity 0.2s; | |
| } | |
| .modal-overlay.open { opacity: 1; pointer-events: auto; } | |
| .modal { | |
| background: var(--bg-card); | |
| width: 90%; | |
| max-width: 700px; | |
| max-height: 85vh; | |
| border-radius: var(--radius-lg); | |
| border: 1px solid var(--border-subtle); | |
| box-shadow: var(--shadow-card); | |
| display: flex; | |
| flex-direction: column; | |
| transform: scale(0.95); | |
| transition: transform 0.2s; | |
| } | |
| .modal-overlay.open .modal { transform: scale(1); } | |
| .modal-header { | |
| padding: 16px 24px; | |
| border-bottom: 1px solid var(--border-subtle); | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| font-weight: 600; | |
| color: var(--text-main); | |
| } | |
| .modal-body { | |
| padding: 24px; | |
| overflow: auto; | |
| font-family: 'Menlo', 'Monaco', monospace; | |
| font-size: 0.9rem; | |
| color: var(--text-main); | |
| background: #0d1117; | |
| white-space: pre-wrap; | |
| } | |
| .close-modal { | |
| background: none; border: none; color: var(--text-muted); | |
| font-size: 1.5rem; cursor: pointer; | |
| } | |
| .close-modal:hover { color: var(--text-main); } | |
| /* Toast */ | |
| .toast { | |
| position: fixed; | |
| bottom: 24px; | |
| right: 24px; | |
| background: var(--bg-sidebar); | |
| color: var(--text-main); | |
| padding: 12px 20px; | |
| border-radius: var(--radius-md); | |
| border: 1px solid var(--border-subtle); | |
| box-shadow: var(--shadow-card); | |
| transform: translateY(100px); | |
| opacity: 0; | |
| transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); | |
| z-index: 200; | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .toast.show { transform: translateY(0); opacity: 1; } | |
| .toast-success { border-left: 3px solid var(--accent-secondary); } | |
| .toast-error { border-left: 3px solid var(--accent-danger); } | |
| /* Responsive */ | |
| @media (max-width: 768px) { | |
| .sidebar { | |
| position: absolute; | |
| height: 100%; | |
| transform: translateX(-100%); | |
| box-shadow: 10px 0 20px rgba(0,0,0,0.5); | |
| } | |
| .sidebar.open { transform: translateX(0); } | |
| .toolbar { padding: 0 12px; } | |
| .search-wrapper { width: 140px; } | |
| .header-actions span { display: none; } /* Hide text in header */ | |
| .logo span { display: none; } | |
| .logo i { display: block; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- Header --> | |
| <header class="header"> | |
| <div class="header-left"> | |
| <button class="btn-icon" id="menu-toggle" aria-label="Toggle Menu"> | |
| <i class="ph ph-list"></i> | |
| </button> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="logo"> | |
| <i class="ph-fill ph-cube-transparent"></i> | |
| <span>Anycoder</span> | |
| </a> | |
| </div> | |
| <div class="header-actions"> | |
| <button class="btn" id="upload-btn"> | |
| <i class="ph ph-upload-simple"></i> | |
| <span>Load Data</span> | |
| </button> | |
| <input type="file" id="file-input" hidden accept=".json,.csv,.parquet"> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="btn btn-primary" style="padding: 8px 14px;"> | |
| <i class="ph-fill ph-code"></i> | |
| <span>Spaces</span> | |
| </a> | |
| </div> | |
| </header> | |
| <div class="app-container"> | |
| <!-- Sidebar --> | |
| <aside class="sidebar" id="sidebar"> | |
| <div class="sidebar-section"> | |
| <div class="sidebar-title">Dataset Overview</div> | |
| <div class="info-card"> | |
| <div class="info-row"> | |
| <span class="info-label">Name</span> | |
| <span class="info-val" id="dataset-name">Sample Dataset</span> | |
| </div> | |
| <div class="info-row"> | |
| <span class="info-label">Size</span> | |
| <span class="info-val" id="dataset-size">--</span> | |
| </div> | |
| <div class="info-row"> | |
| <span class="info-label">Columns</span> | |
| <span class="info-val" id="dataset-cols">--</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="sidebar-section"> | |
| <div class="sidebar-title">Files</div> | |
| <ul class="file-list"> | |
| <li class="file-item active"> | |
| <i class="ph ph-file-json"></i> | |
| <span>train_data.json</span> | |
| </li> | |
| <li class="file-item"> | |
| <i class="ph ph-file-code"></i> | |
| <span>config.json</span> | |
| </li> | |
| <li class="file-item"> | |
| <i class="ph ph-database"></i> | |
| <span>metadata.parquet</span> | |
| </li> | |
| </ul> | |
| </div> | |
| </aside> | |
| <!-- Main Content --> | |
| <main class="main-content"> | |
| <!-- Toolbar --> | |
| <div class="toolbar"> | |
| <div class="toolbar-group"> | |
| <select class="select-control" id="split-select"> | |
| <option value="train">Train Split</option> | |
| <option value="test">Test Split</option> | |
| </select> | |
| <div class="search-wrapper"> | |
| <i class="ph ph-magnifying-glass"></i> | |
| <input type="text" class="search-input" placeholder="Search..." id="search-input"> | |
| </div> | |
| </div> | |
| <div class="toolbar-group"> | |
| <button class="btn" id="columns-btn"> | |
| <i class="ph ph-table"></i> <span>Columns</span> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Table --> | |
| <div class="table-wrapper" id="table-container"> | |
| <table id="data-table"> | |
| <thead> | |
| <tr id="table-header"> | |
| <!-- Headers injected via JS --> | |
| </tr> | |
| </thead> | |
| <tbody id="table-body"> | |
| <!-- Rows injected via JS --> | |
| </tbody> | |
| </table> | |
| </div> | |
| <!-- Empty State --> | |
| <div class="empty-state" id="empty-state" style="display: none;"> | |
| <i class="ph ph-files"></i> | |
| <h3>No Data Loaded</h3> | |
| <p>Click "Load Data" to upload a JSON or CSV file.</p> | |
| </div> | |
| <!-- Pagination --> | |
| <div class="pagination"> | |
| <span id="page-info">Showing 0 - 0 of 0</span> | |
| <div style="display: flex; gap: 8px;"> | |
| <button class="btn" id="prev-btn" style="padding: 4px 12px; font-size: 0.8rem;"><i class="ph ph-caret-left"></i> Prev</button> | |
| <button class="btn" id="next-btn" style="padding: 4px 12px; font-size: 0.8rem;">Next <i class="ph ph-caret-right"></i></button> | |
| </div> | |
| </div> | |
| </main> | |
| </div> | |
| <!-- Modal --> | |
| <div class="modal-overlay" id="modal-overlay"> | |
| <div class="modal"> | |
| <div class="modal-header"> | |
| <span id="modal-title">Cell Content</span> | |
| <button class="close-modal" id="close-modal">×</button> | |
| </div> | |
| <div class="modal-body" id="modal-content"></div> | |
| </div> | |
| </div> | |
| <!-- Toast --> | |
| <div class="toast" id="toast"> | |
| <i class="ph-fill ph-check-circle" id="toast-icon"></i> | |
| <span id="toast-msg">Operation successful</span> | |
| </div> | |
| <script> | |
| // --- State Management --- | |
| const state = { | |
| data: [], | |
| filteredData: [], | |
| columns: [], | |
| currentPage: 1, | |
| rowsPerPage: 50, | |
| sortCol: null, | |
| sortAsc: true, | |
| totalRows: 0 | |
| }; | |
| // --- Mock Data Generator --- | |
| function generateMockData(count = 500) { | |
| const data = []; | |
| const items = ['Laptop', 'Coffee', 'Phone', 'Keyboard', 'Mouse']; | |
| for (let i = 0; i < count; i++) { | |
| data.push({ | |
| id: i, | |
| item: items[Math.floor(Math.random() * items.length)], | |
| price: (Math.random() * 1000).toFixed(2), | |
| in_stock: Math.random() > 0.2, | |
| rating: (Math.random() * 5).toFixed(1), | |
| category: ['Electronics', 'Office', 'Home'][Math.floor(Math.random() * 3)], | |
| image_url: `https://picsum.photos/seed/${i}/100/100`, | |
| description: `High-quality ${items[Math.floor(Math.random() * items.length)]} for professional use.` | |
| }); | |
| } | |
| return data; | |
| } | |
| // --- Core Application Logic --- | |
| const App = { | |
| init() { | |
| this.cacheDOM(); | |
| this.bindEvents(); | |
| this.loadMockData(); | |
| }, | |
| cacheDOM() { | |
| this.dom = { | |
| tableHeader: document.getElementById('table-header'), | |
| tableBody: document.getElementById('table-body'), | |
| pageInfo: document.getElementById('page-info'), | |
| prevBtn: document.getElementById('prev-btn'), | |
| nextBtn: document.getElementById('next-btn'), | |
| searchInput: document.getElementById('search-input'), | |
| fileInput: document.getElementById('file-input'), | |
| uploadBtn: document.getElementById('upload-btn'), | |
| modalOverlay: document.getElementById('modal-overlay'), | |
| modalContent: document.getElementById('modal-content'), | |
| closeModal: document.getElementById('close-modal'), | |
| menuToggle: document.getElementById('menu-toggle'), | |
| sidebar: document.getElementById('sidebar'), | |
| toast: document.getElementById('toast'), | |
| toastMsg: document.getElementById('toast-msg'), | |
| datasetName: document.getElementById('dataset-name'), | |
| datasetSize: document.getElementById('dataset-size'), | |
| datasetCols: document.getElementById('dataset-cols'), | |
| emptyState: document.getElementById('empty-state'), | |
| tableContainer: document.getElementById('table-container') | |
| }; | |
| }, | |
| bindEvents() { | |
| // Pagination | |
| this.dom.prevBtn.addEventListener('click', () => this.changePage(-1)); | |
| this.dom.nextBtn.addEventListener('click', () => this.changePage(1)); | |
| // Search | |
| this.dom.searchInput.addEventListener('input', (e) => this.handleSearch(e.target.value)); | |
| // File Upload | |
| this.dom.uploadBtn.addEventListener('click', () => this.dom.fileInput.click()); | |
| this.dom.fileInput.addEventListener('change', (e) => this.handleFileUpload(e)); | |
| // Modal | |
| this.dom.closeModal.addEventListener('click', () => this.closeModal()); | |
| this.dom.modalOverlay.addEventListener('click', (e) => { | |
| if (e.target === this.dom.modalOverlay) this.closeModal(); | |
| }); | |
| // Sidebar Toggle | |
| this.dom.menuToggle.addEventListener('click', () => { | |
| this.dom.sidebar.classList.toggle('open'); | |
| }); | |
| }, | |
| loadMockData() { | |
| const mockData = generateMockData(2000); | |
| this.processData(mockData); | |
| this.showToast("Sample dataset loaded", "success"); | |
| }, | |
| processData(rawData) { | |
| if (!rawData || rawData.length === 0) { | |
| this.dom.tableContainer.style.display = 'none'; | |
| document.querySelector('.pagination').style.display = 'none'; | |
| this.dom.emptyState.style.display = 'flex'; | |
| return; | |
| } | |
| this.dom.tableContainer.style.display = 'block'; | |
| document.querySelector('.pagination').style.display = 'flex'; | |
| this.dom.emptyState.style.display = 'none'; | |
| state.data = rawData; | |
| state.filteredData = rawData; | |
| state.columns = Object.keys(rawData[0]); | |
| state.totalRows = rawData.length; | |
| state.currentPage = 1; | |
| this.renderHeaders(); | |
| this.renderTable(); | |
| this.updatePagination(); | |
| this.updateSidebarInfo(); | |
| }, | |
| renderHeaders() { | |
| this.dom.tableHeader.innerHTML = ''; | |
| state.columns.forEach(col => { | |
| const th = document.createElement('th'); | |
| th.innerHTML = `${col} <i class="ph ph-caret-up-down"></i>`; | |
| th.addEventListener('click', () => this.handleSort(col)); | |
| this.dom.tableHeader.appendChild(th); | |
| }); | |
| }, | |
| renderTable() { | |
| this.dom.tableBody.innerHTML = ''; | |
| const start = (state.currentPage - 1) * state.rowsPerPage; | |
| const end = start + state.rowsPerPage; | |
| const pageData = state.filteredData.slice(start, end); | |
| pageData.forEach(row => { | |
| const tr = document.createElement('tr'); | |
| state.columns.forEach(col => { | |
| const td = document.createElement('td'); | |
| const val = row[col]; | |
| if (val === null || val === undefined) { | |
| td.textContent = 'null'; | |
| td.style.color = 'var(--text-muted)'; | |
| } else if (typeof val === 'object') { | |
| // Image check | |
| if (col === 'image_url' || col === 'image') { | |
| td.innerHTML = `<img src="${val}" class="cell-img" loading="lazy" alt="preview">`; | |
| } else { | |
| td.innerHTML = `<span class="cell-link" onclick="App.openModal(${JSON.stringify(val)})">View JSON</span>`; | |
| } | |
| } else if (Array.isArray(val)) { | |
| td.innerHTML = `<span class="cell-link" onclick="App.openModal(${JSON.stringify(val)})">Array[${val.length}]</span>`; | |
| } else { | |
| td.textContent = val; | |
| } | |
| tr.appendChild(td); | |
| }); | |
| this.dom.tableBody.appendChild(tr); | |
| }); | |
| }, | |
| updatePagination() { | |
| const total = state.filteredData.length; | |
| const start = total === 0 ? 0 : (state.currentPage - 1) * state.rowsPerPage + 1; | |
| const end = Math.min(start + state.rowsPerPage - 1, total); | |
| this.dom.pageInfo.textContent = `Showing ${start}-${end} of ${total.toLocaleString()}`; | |
| this.dom.prevBtn.disabled = state.currentPage === 1; | |
| this.dom.nextBtn.disabled = end >= total; | |
| }, | |
| changePage(delta) { | |
| const totalPages = Math.ceil(state.filteredData.length / state.rowsPerPage); | |
| const newPage = state.currentPage + delta; | |
| if (newPage >= 1 && newPage <= totalPages) { | |
| state.currentPage = newPage; | |
| this.renderTable(); | |
| this.updatePagination(); | |
| this.dom.tableContainer.scrollTop = 0; | |
| } | |
| }, | |
| handleSearch(query) { | |
| const lowerQ = query.toLowerCase(); | |
| state.filteredData = state.data.filter(row => { | |
| return state.columns.some(col => { | |
| const val = row[col]; | |
| if (val == null) return false; | |
| return String(val).toLowerCase().includes(lowerQ); | |
| }); | |
| }); | |
| state.currentPage = 1; | |
| this.renderTable(); | |
| this.updatePagination(); | |
| }, | |
| handleSort(col) { | |
| if (state.sortCol === col) { | |
| state.sortAsc = !state.sortAsc; | |
| } else { | |
| state.sortCol = col; | |
| state.sortAsc = true; | |
| } | |
| state.filteredData.sort((a, b) => { | |
| let valA = a[col]; | |
| let valB = b[col]; | |
| if (typeof valA === 'string') valA = valA.toLowerCase(); | |
| if (typeof valB === 'string') valB = valB.toLowerCase(); | |
| if (valA < valB) return state.sortAsc ? -1 : 1; | |
| if (valA > valB) return state.sortAsc ? 1 : -1; | |
| return 0; | |
| }); | |
| this.renderTable(); | |
| }, | |
| handleFileUpload(e) { | |
| const file = e.target.files[0]; | |
| if (!file) return; | |
| this.dom.datasetName.textContent = file.name; | |
| this.dom.datasetSize.textContent = (file.size / 1024).toFixed(1) + ' KB'; | |
| this.showToast(`Loading ${file.name}...`); | |
| const reader = new FileReader(); | |
| reader.onload = (event) => { | |
| try { | |
| if (file.name.endsWith('.json')) { | |
| const json = JSON.parse(event.target.result); | |
| const data = Array.isArray(json) ? json : (json.data || json); | |
| this.processData(data); | |
| this.showToast("JSON loaded successfully", "success"); | |
| } else if (file.name.endsWith('.csv')) { | |
| const csv = this.parseCSV(event.target.result); | |
| this.processData(csv); | |
| this.showToast("CSV loaded successfully", "success"); | |
| } else { | |
| this.showToast("Unsupported file type", "error"); | |
| } | |
| } catch (err) { | |
| console.error(err); | |
| this.showToast("Error parsing file", "error"); | |
| } | |
| }; | |
| reader.readAsText(file); | |
| }, | |
| parseCSV(text) { | |
| const lines = text.trim().split('\n'); | |
| const headers = lines[0].split(',').map(h => h.trim().replace(/"/g, '')); | |
| const data = []; | |
| for (let i = 1; i < lines.length; i++) { | |
| const row = lines[i].split(','); | |
| const obj = {}; | |
| headers.forEach((h, index) => { | |
| let val = row[index] ? row[index].trim().replace(/"/g, '') : null; | |
| // Simple type conversion | |
| if (!isNaN(val) && val !== '') val = Number(val); | |
| obj[h] = val; | |
| }); | |
| data.push(obj); | |
| } | |
| return data; | |
| }, | |
| updateSidebarInfo() { | |
| this.dom.datasetCols.textContent = state.columns.length; | |
| }, | |
| openModal(content) { | |
| // Handle circular JSON or non-string objects safely | |
| let formatted; | |
| try { | |
| formatted = JSON.stringify(content, null, 2); | |
| } catch (e) { | |
| formatted = content.toString(); | |
| } | |
| this.dom.modalContent.textContent = formatted; | |
| this.dom.modalOverlay.classList.add('open'); | |
| }, | |
| closeModal() { | |
| this.dom.modalOverlay.classList.remove('open'); | |
| }, | |
| showToast(msg, type = 'success') { | |
| this.dom.toastMsg.textContent = msg; | |
| this.dom.toast.className = `toast toast-${type} show`; | |
| const icon = document.getElementById('toast-icon'); | |
| icon.className = type === 'success' ? 'ph-fill ph-check-circle' : 'ph-fill ph-warning-circle'; | |
| setTimeout(() => { | |
| this.dom.toast.classList.remove('show'); | |
| }, 3000); | |
| } | |
| }; | |
| // Start App | |
| document.addEventListener('DOMContentLoaded', () => { | |
| App.init(); | |
| }); | |
| </script> | |
| </body> | |
| </html> |