Download frontend/scripts/notebook-engine.js from Snapkitty/rowm-polymorphic-notebook: direct link, hf CLI and curl.
- Browser
- Download file 11.8 kB
-
https://huggingface.co/Snapkitty/rowm-polymorphic-notebook/resolve/main/frontend/scripts/notebook-engine.js
- Command line
-
hf download hf://Snapkitty/rowm-polymorphic-notebook/frontend/scripts/notebook-engine.js
-
curl -L -o notebook-engine.js https://huggingface.co/Snapkitty/rowm-polymorphic-notebook/resolve/main/frontend/scripts/notebook-engine.js
11.8 kB
| /** | |
| * Notebook Engine | |
| * Core notebook execution, cell management, and state orchestration | |
| */ | |
| class NotebookEngine { | |
| constructor() { | |
| this.cells = []; | |
| this.selectedCell = null; | |
| this.metadata = {}; | |
| this.initDOM(); | |
| } | |
| /** | |
| * Initialize DOM elements | |
| */ | |
| initDOM() { | |
| this.elements = { | |
| cellsContainer: document.getElementById('notebook-cells'), | |
| statusText: document.getElementById('status-text'), | |
| executionStats: document.getElementById('execution-stats'), | |
| metadataList: document.getElementById('metadata-list'), | |
| dependencySvg: document.getElementById('dependency-svg'), | |
| }; | |
| } | |
| /** | |
| * Add a new cell | |
| */ | |
| addCell(type = 'code', index = -1) { | |
| if (index === -1) { | |
| index = this.cells.length; | |
| } | |
| const cell = { | |
| index: index, | |
| type: type, | |
| source: '', | |
| output: '', | |
| hash: '', | |
| executionTime: 0, | |
| status: 'ready', | |
| }; | |
| this.cells.push(cell); | |
| this.renderCell(cell); | |
| return cell; | |
| } | |
| /** | |
| * Render cell in DOM | |
| */ | |
| renderCell(cell) { | |
| const cellDiv = document.createElement('div'); | |
| cellDiv.className = 'notebook-cell'; | |
| cellDiv.dataset.index = cell.index; | |
| cellDiv.dataset.cellType = cell.type; | |
| cellDiv.innerHTML = ` | |
| <div class="cell-index">Cell [${cell.index}] • ${cell.type}</div> | |
| <div class="cell-editor"> | |
| <textarea placeholder="Enter ${cell.type} here..." spellcheck="false"></textarea> | |
| </div> | |
| <div class="cell-output"></div> | |
| <div class="cell-receipt" style="display: none;"></div> | |
| `; | |
| // Add event listeners | |
| const textarea = cellDiv.querySelector('textarea'); | |
| textarea.addEventListener('input', (e) => { | |
| cell.source = e.target.value; | |
| this.updateCellHash(cell); | |
| }); | |
| textarea.addEventListener('keydown', (e) => { | |
| if (e.ctrlKey && e.key === 'Enter') { | |
| this.executeCell(cell.index); | |
| } | |
| }); | |
| cellDiv.addEventListener('click', () => { | |
| this.selectCell(cell.index); | |
| }); | |
| this.elements.cellsContainer.appendChild(cellDiv); | |
| } | |
| /** | |
| * Select a cell | |
| */ | |
| selectCell(index) { | |
| if (this.selectedCell !== null) { | |
| const prevCell = this.elements.cellsContainer.querySelector( | |
| `[data-index="${this.selectedCell}"]` | |
| ); | |
| if (prevCell) { | |
| prevCell.style.borderColor = ''; | |
| } | |
| } | |
| this.selectedCell = index; | |
| const cellDiv = this.elements.cellsContainer.querySelector( | |
| `[data-index="${index}"]` | |
| ); | |
| if (cellDiv) { | |
| cellDiv.style.borderColor = 'var(--color-cyan)'; | |
| } | |
| } | |
| /** | |
| * Execute a cell | |
| */ | |
| async executeCell(index) { | |
| const cell = this.cells[index]; | |
| if (!cell) return; | |
| cell.status = 'running'; | |
| const startTime = performance.now(); | |
| const cellDiv = this.elements.cellsContainer.querySelector(`[data-index="${index}"]`); | |
| const outputDiv = cellDiv.querySelector('.cell-output'); | |
| try { | |
| // Simulate execution (in real implementation, would dispatch to runtime) | |
| await new Promise(resolve => setTimeout(resolve, 500)); | |
| // Mock output | |
| cell.output = `Output from cell [${index}]: ${cell.source.substring(0, 50)}...`; | |
| outputDiv.textContent = cell.output; | |
| outputDiv.classList.remove('error'); | |
| outputDiv.classList.add('success'); | |
| cell.status = 'success'; | |
| } catch (error) { | |
| cell.output = `Error: ${error.message}`; | |
| outputDiv.textContent = cell.output; | |
| outputDiv.classList.remove('success'); | |
| outputDiv.classList.add('error'); | |
| cell.status = 'error'; | |
| } | |
| cell.executionTime = performance.now() - startTime; | |
| this.updateStats(); | |
| } | |
| /** | |
| * Execute all cells in order | |
| */ | |
| async executeAll() { | |
| this.updateStatus('Executing all cells...'); | |
| for (let i = 0; i < this.cells.length; i++) { | |
| await this.executeCell(i); | |
| } | |
| this.updateStatus('All cells executed'); | |
| } | |
| /** | |
| * Update cell hash (for tamper detection) | |
| */ | |
| updateCellHash(cell) { | |
| const data = cell.source + cell.output; | |
| let hash = 0; | |
| for (let i = 0; i < data.length; i++) { | |
| const char = data.charCodeAt(i); | |
| hash = ((hash << 5) - hash) + char; | |
| } | |
| cell.hash = Math.abs(hash).toString(16); | |
| } | |
| /** | |
| * Update statistics | |
| */ | |
| updateStats() { | |
| const totalExecutionTime = this.cells.reduce((sum, c) => sum + c.executionTime, 0); | |
| const totalCells = this.cells.length; | |
| this.elements.executionStats.textContent = | |
| `${totalCells} cells | ${Math.round(totalExecutionTime)}ms`; | |
| this.updateMetadata(); | |
| } | |
| /** | |
| * Update metadata display | |
| */ | |
| updateMetadata() { | |
| this.elements.metadataList.innerHTML = ''; | |
| const metadata = [ | |
| { label: 'Cells', value: this.cells.length }, | |
| { label: 'Successful', value: this.cells.filter(c => c.status === 'success').length }, | |
| { label: 'Errors', value: this.cells.filter(c => c.status === 'error').length }, | |
| { label: 'Total time', value: `${Math.round(this.cells.reduce((s, c) => s + c.executionTime, 0))}ms` }, | |
| ]; | |
| for (const item of metadata) { | |
| const div = document.createElement('div'); | |
| div.innerHTML = `<span>${item.label}:</span> ${item.value}`; | |
| this.elements.metadataList.appendChild(div); | |
| } | |
| } | |
| /** | |
| * Update status text | |
| */ | |
| updateStatus(text) { | |
| this.elements.statusText.textContent = text; | |
| } | |
| /** | |
| * Render dependency graph | |
| */ | |
| renderDependencyGraph() { | |
| const svg = this.elements.dependencySvg; | |
| if (!svg) return; | |
| const width = svg.width.baseVal.value; | |
| const height = svg.height.baseVal.value; | |
| // Clear previous | |
| svg.innerHTML = ''; | |
| // Simple graph layout | |
| const nodeCount = Math.min(this.cells.length, 10); | |
| const radius = 15; | |
| const padding = 20; | |
| const cx = width / 2; | |
| const cy = height / 2; | |
| const maxRadius = Math.min(width, height) / 2 - padding; | |
| // Draw nodes (cells) | |
| for (let i = 0; i < nodeCount; i++) { | |
| const angle = (i / nodeCount) * 2 * Math.PI; | |
| const x = cx + maxRadius * Math.cos(angle); | |
| const y = cy + maxRadius * Math.sin(angle); | |
| // Node circle | |
| const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); | |
| circle.setAttribute('cx', x); | |
| circle.setAttribute('cy', y); | |
| circle.setAttribute('r', radius); | |
| svg.appendChild(circle); | |
| // Label | |
| const text = document.createElementNS('http://www.w3.org/2000/svg', 'text'); | |
| text.setAttribute('x', x); | |
| text.setAttribute('y', y + 3); | |
| text.textContent = i; | |
| svg.appendChild(text); | |
| } | |
| // Draw connections (simple: each cell to next) | |
| for (let i = 0; i < nodeCount - 1; i++) { | |
| const angle1 = (i / nodeCount) * 2 * Math.PI; | |
| const angle2 = ((i + 1) / nodeCount) * 2 * Math.PI; | |
| const x1 = cx + maxRadius * Math.cos(angle1); | |
| const y1 = cy + maxRadius * Math.sin(angle1); | |
| const x2 = cx + maxRadius * Math.cos(angle2); | |
| const y2 = cy + maxRadius * Math.sin(angle2); | |
| const line = document.createElementNS('http://www.w3.org/2000/svg', 'line'); | |
| line.setAttribute('x1', x1); | |
| line.setAttribute('y1', y1); | |
| line.setAttribute('x2', x2); | |
| line.setAttribute('y2', y2); | |
| svg.appendChild(line); | |
| } | |
| } | |
| /** | |
| * Export notebook as JSON | |
| */ | |
| exportJSON() { | |
| return { | |
| title: document.querySelector('#notebook-header h1')?.textContent || 'Untitled', | |
| cells: this.cells.map(c => ({ | |
| index: c.index, | |
| type: c.type, | |
| source: c.source, | |
| output: c.output, | |
| })), | |
| timestamp: new Date().toISOString(), | |
| }; | |
| } | |
| /** | |
| * Import notebook from JSON | |
| */ | |
| importJSON(data) { | |
| this.cells = []; | |
| this.elements.cellsContainer.innerHTML = ''; | |
| for (const cellData of data.cells || []) { | |
| const cell = this.addCell(cellData.type, cellData.index); | |
| cell.source = cellData.source; | |
| cell.output = cellData.output; | |
| // Update DOM | |
| const textarea = this.elements.cellsContainer.querySelector( | |
| `[data-index="${cell.index}"] textarea` | |
| ); | |
| if (textarea) { | |
| textarea.value = cell.source; | |
| } | |
| const outputDiv = this.elements.cellsContainer.querySelector( | |
| `[data-index="${cell.index}"] .cell-output` | |
| ); | |
| if (outputDiv && cell.output) { | |
| outputDiv.textContent = cell.output; | |
| } | |
| } | |
| this.updateStats(); | |
| this.renderDependencyGraph(); | |
| } | |
| /** | |
| * Get statistics | |
| */ | |
| getStats() { | |
| return { | |
| cellCount: this.cells.length, | |
| successCount: this.cells.filter(c => c.status === 'success').length, | |
| errorCount: this.cells.filter(c => c.status === 'error').length, | |
| totalTime: this.cells.reduce((s, c) => s + c.executionTime, 0), | |
| }; | |
| } | |
| } | |
| // Initialize notebook engine on DOM ready | |
| if (document.readyState === 'loading') { | |
| document.addEventListener('DOMContentLoaded', () => { | |
| window.notebookEngine = new NotebookEngine(); | |
| // Add sample cells | |
| window.notebookEngine.addCell('code'); | |
| window.notebookEngine.addCell('markdown'); | |
| window.notebookEngine.addCell('code'); | |
| // Setup header buttons | |
| document.getElementById('btn-run-all')?.addEventListener('click', () => { | |
| window.notebookEngine.executeAll(); | |
| }); | |
| // Setup export receipt button | |
| document.getElementById('btn-export-receipt')?.addEventListener('click', () => { | |
| const data = window.notebookEngine.exportJSON(); | |
| const json = JSON.stringify(data, null, 2); | |
| const blob = new Blob([json], { type: 'application/json' }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = `notebook-${Date.now()}.json`; | |
| a.click(); | |
| }); | |
| window.notebookEngine.updateStats(); | |
| window.notebookEngine.renderDependencyGraph(); | |
| }); | |
| } else { | |
| window.notebookEngine = new NotebookEngine(); | |
| } | |
| // Export for use in other modules | |
| if (typeof module !== 'undefined' && module.exports) { | |
| module.exports = NotebookEngine; | |
| } | |