Spaces:
Build error
Build error
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>PageParse - Universal File Analyzer</title> | |
| <style> | |
| * { margin: 0; padding: 0; box-sizing: border-box; } | |
| :root { --bg: #0d1117; --card: #161b22; --border: #30363d; --text: #e6edf3; --text-muted: #8b949e; --accent: #58a6ff; --accent-hover: #79c0ff; --success: #3fb950; --warning: #d29922; --danger: #f85149; --radius: 8px; } | |
| body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: var(--bg); color: var(--text); min-height: 100vh; } | |
| .container { max-width: 1400px; margin: 0 auto; padding: 20px; } | |
| header { display: flex; align-items: center; justify-content: space-between; padding: 16px 0; border-bottom: 1px solid var(--border); margin-bottom: 24px; } | |
| header h1 { font-size: 24px; background: linear-gradient(135deg, var(--accent), #a371f7); -webkit-background-clip: text; -webkit-text-fill-color: transparent; } | |
| header nav { display: flex; gap: 8px; } | |
| .nav-btn { background: var(--card); border: 1px solid var(--border); color: var(--text); padding: 8px 16px; border-radius: var(--radius); cursor: pointer; font-size: 14px; } | |
| .nav-btn:hover { border-color: var(--accent); } | |
| .nav-btn.active { background: var(--accent); color: #fff; border-color: var(--accent); } | |
| .panel { display: none; } | |
| .panel.active { display: block; } | |
| .upload-zone { border: 2px dashed var(--border); border-radius: 12px; padding: 60px 20px; text-align: center; cursor: pointer; transition: all .3s; background: var(--card); margin-bottom: 24px; } | |
| .upload-zone:hover, .upload-zone.dragover { border-color: var(--accent); background: rgba(88,166,255,0.05); } | |
| .upload-zone-icon { font-size: 48px; margin-bottom: 16px; } | |
| .upload-zone h2 { font-size: 20px; margin-bottom: 8px; } | |
| .upload-zone p { color: var(--text-muted); font-size: 14px; } | |
| .upload-zone input { display: none; } | |
| .file-info { background: var(--card); border: 1px solid var(--border); border-radius: var(--radius); padding: 16px; margin-bottom: 16px; display: flex; align-items: center; gap: 12px; } | |
| .file-info .icon { font-size: 32px; } | |
| .file-info .name { flex: 1; font-weight: 600; } | |
| .file-info .size { color: var(--text-muted); font-size: 13px; } | |
| .file-info .remove-btn { background: none; border: none; color: var(--danger); cursor: pointer; font-size: 20px; padding: 4px; } | |
| .card { background: var(--card); border: 1px solid var(--border); border-radius: var(--radius); padding: 20px; margin-bottom: 16px; } | |
| .card h3 { font-size: 16px; margin-bottom: 12px; color: var(--accent); } | |
| .card .label { font-size: 12px; color: var(--text-muted); text-transform: uppercase; letter-spacing: .5px; margin-bottom: 4px; } | |
| .card .value { font-size: 14px; line-height: 1.6; } | |
| .code-block { background: #010409; border: 1px solid var(--border); border-radius: var(--radius); padding: 16px; font-family: 'JetBrains Mono', 'Fira Code', monospace; font-size: 13px; overflow-x: auto; line-height: 1.5; margin: 8px 0; max-height: 400px; overflow-y: auto; } | |
| .step-item { padding: 10px 12px; border-left: 3px solid var(--accent); margin-bottom: 8px; background: rgba(88,166,255,0.05); border-radius: 0 var(--radius) var(--radius) 0; font-size: 14px; line-height: 1.5; } | |
| .step-num { display: inline-block; background: var(--accent); color: #fff; border-radius: 50%; width: 22px; height: 22px; text-align: center; line-height: 22px; font-size: 12px; margin-right: 8px; flex-shrink: 0; } | |
| .summary-box { background: linear-gradient(135deg, rgba(63,185,80,0.1), rgba(88,166,255,0.1)); border: 1px solid var(--success); border-radius: var(--radius); padding: 16px; font-size: 15px; line-height: 1.6; } | |
| .grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; } | |
| .grid-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 16px; } | |
| .badge { display: inline-block; padding: 2px 10px; border-radius: 12px; font-size: 12px; font-weight: 600; } | |
| .badge-blue { background: rgba(88,166,255,0.15); color: var(--accent); } | |
| .badge-green { background: rgba(63,185,80,0.15); color: var(--success); } | |
| .badge-yellow { background: rgba(210,153,34,0.15); color: var(--warning); } | |
| .badge-red { background: rgba(248,81,73,0.15); color: var(--danger); } | |
| .dsa-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 12px; margin-bottom: 20px; } | |
| .dsa-card { background: var(--card); border: 1px solid var(--border); border-radius: var(--radius); padding: 16px; cursor: pointer; transition: all .2s; } | |
| .dsa-card:hover { border-color: var(--accent); transform: translateY(-2px); } | |
| .dsa-card h4 { font-size: 15px; margin-bottom: 6px; } | |
| .dsa-card p { font-size: 13px; color: var(--text-muted); line-height: 1.5; } | |
| .dsa-card .complexity { display: flex; gap: 12px; margin-top: 8px; font-size: 12px; } | |
| .array-bars { display: flex; align-items: flex-end; gap: 4px; height: 200px; padding: 20px 0; justify-content: center; } | |
| .array-bar { background: var(--accent); border-radius: 4px 4px 0 0; transition: all .3s; min-width: 30px; display: flex; align-items: flex-end; justify-content: center; font-size: 11px; color: #fff; padding-bottom: 4px; } | |
| .array-bar.highlight { background: var(--warning); } | |
| .array-bar.sorted { background: var(--success); } | |
| .viz-controls { display: flex; align-items: center; gap: 12px; margin: 16px 0; } | |
| .viz-controls button { background: var(--card); border: 1px solid var(--border); color: var(--text); padding: 8px 16px; border-radius: var(--radius); cursor: pointer; font-size: 14px; } | |
| .viz-controls button:hover { border-color: var(--accent); } | |
| .viz-controls button:disabled { opacity: 0.4; cursor: not-allowed; } | |
| .viz-controls .step-info { color: var(--text-muted); font-size: 13px; flex: 1; text-align: center; } | |
| .viz-desc { font-size: 14px; color: var(--text-muted); margin: 8px 0; padding: 8px 12px; background: rgba(88,166,255,0.05); border-radius: var(--radius); border-left: 3px solid var(--accent); } | |
| .tree-canvas, .ll-canvas { width: 100%; height: 300px; border: 1px solid var(--border); border-radius: var(--radius); margin: 12px 0; } | |
| .template-select { width: 100%; padding: 10px 12px; background: var(--card); border: 1px solid var(--border); border-radius: var(--radius); color: var(--text); font-size: 14px; margin-bottom: 12px; } | |
| .input-data { width: 100%; padding: 10px 12px; background: var(--card); border: 1px solid var(--border); border-radius: var(--radius); color: var(--text); font-size: 14px; margin-bottom: 12px; } | |
| .btn-primary { background: var(--accent); border: none; color: #fff; padding: 10px 24px; border-radius: var(--radius); cursor: pointer; font-size: 14px; font-weight: 600; } | |
| .btn-primary:hover { background: var(--accent-hover); } | |
| .loading { text-align: center; padding: 40px; color: var(--text-muted); } | |
| .loading::after { content: ''; display: inline-block; width: 24px; height: 24px; border: 2px solid var(--border); border-top-color: var(--accent); border-radius: 50%; animation: spin .8s linear infinite; margin-left: 8px; vertical-align: middle; } | |
| @keyframes spin { to { transform: rotate(360deg); } } | |
| .tabs { display: flex; gap: 4px; margin-bottom: 16px; background: var(--card); border-radius: var(--radius); padding: 4px; border: 1px solid var(--border); } | |
| .tab { padding: 8px 20px; border-radius: 6px; cursor: pointer; font-size: 14px; border: none; background: none; color: var(--text-muted); } | |
| .tab:hover { color: var(--text); } | |
| .tab.active { background: var(--accent); color: #fff; } | |
| .tab-content { display: none; } | |
| .tab-content.active { display: block; } | |
| textarea.code-input { width: 100%; min-height: 200px; background: #010409; border: 1px solid var(--border); border-radius: var(--radius); padding: 16px; color: var(--text); font-family: 'JetBrains Mono', monospace; font-size: 13px; line-height: 1.5; resize: vertical; } | |
| .tag { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; margin: 2px; } | |
| .tag-py { background: #306998; color: #fff; } | |
| .tag-js { background: #f0db4f; color: #323330; } | |
| .tag-html { background: #e44d26; color: #fff; } | |
| .tag-css { background: #264de4; color: #fff; } | |
| .tag-java { background: #b07219; color: #fff; } | |
| .tag-c { background: #555; color: #fff; } | |
| .tag-cpp { background: #00599c; color: #fff; } | |
| .image-preview { max-width: 100%; max-height: 400px; border-radius: var(--radius); border: 1px solid var(--border); margin: 12px 0; } | |
| @media (max-width: 768px) { .grid-2, .grid-3 { grid-template-columns: 1fr; } } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <header> | |
| <h1>PageParse</h1> | |
| <nav> | |
| <button class="nav-btn active" data-panel="upload">Upload</button> | |
| <button class="nav-btn" data-panel="dsa">DSA Visualizer</button> | |
| <button class="nav-btn" data-panel="chat">Chat</button> | |
| </nav> | |
| </header> | |
| <div id="panel-upload" class="panel active"> | |
| <div class="upload-zone" id="dropZone"> | |
| <div class="upload-zone-icon">📁</div> | |
| <h2>Upload any file</h2> | |
| <p>Drop a file here or click to browse — Images, Audio, Video, Code (.py .js .c .cpp .java .html .css etc.)</p> | |
| <input type="file" id="fileInput"> | |
| </div> | |
| <div id="fileInfo" class="file-info" style="display:none"> | |
| <div class="icon" id="fileIcon">📄</div> | |
| <div><div class="name" id="fileName"></div><div class="size" id="fileSize"></div></div> | |
| <button class="remove-btn" onclick="resetUpload()">×</button> | |
| </div> | |
| <div id="results" style="display:none"> | |
| <div class="tabs"> | |
| <button class="tab active" data-tab="overview">Overview</button> | |
| <button class="tab" data-tab="steps">Step-by-Step</button> | |
| <button class="tab" data-tab="preview">Preview</button> | |
| </div> | |
| <div id="tab-overview" class="tab-content active"> | |
| <div class="summary-box" id="overviewSummary"></div> | |
| <div id="ovImageContainer" style="display:none;text-align:center;margin:16px 0"> | |
| <img id="ovImage" class="image-preview" alt="Uploaded image"> | |
| </div> | |
| <div class="grid-3" style="margin-top:16px"> | |
| <div class="card"><div class="label">File Type</div><div class="value" id="ovFileType"></div></div> | |
| <div class="card"><div class="label">Size</div><div class="value" id="ovSize"></div></div> | |
| <div class="card"><div class="label">Lines of Code</div><div class="value" id="ovLines"></div></div> | |
| </div> | |
| <div class="card" id="ovExplanationCard"> | |
| <h3>Explanation</h3> | |
| <div class="value" id="ovExplanation" style="white-space:pre-wrap"></div> | |
| </div> | |
| <div class="card" id="ovComplexityCard" style="display:none"> | |
| <h3>Complexity Analysis</h3> | |
| <div class="grid-2"> | |
| <div><div class="label">Time Complexity</div><div class="value" id="ovTime"></div></div> | |
| <div><div class="label">Space Complexity</div><div class="value" id="ovSpace"></div></div> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="tab-steps" class="tab-content"> | |
| <div class="card"><h3>Step-by-Step Breakdown</h3><div id="stepsList"></div></div> | |
| </div> | |
| <div id="tab-preview" class="tab-content"> | |
| <div class="card"><h3>Content Preview</h3><div id="previewContent"></div></div> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="panel-dsa" class="panel"> | |
| <div class="card"> | |
| <h3>DSA Visualizer</h3> | |
| <p style="color:var(--text-muted);font-size:14px;margin-bottom:12px">Explore Data Structures & Algorithms with interactive step-by-step visualization</p> | |
| <select class="template-select" id="dsaTemplate"> | |
| <option value="">-- Select a built-in template --</option> | |
| </select> | |
| <textarea class="code-input" id="dsaCode" placeholder="Or paste your code here..."></textarea> | |
| <div class="grid-2" style="margin-top:12px"> | |
| <input class="input-data" id="dsaInput" placeholder="Input data (comma-separated, e.g. 64,34,25,12)"> | |
| <div style="display:flex;gap:8px"> | |
| <button class="btn-primary" onclick="runVisualization()">Visualize</button> | |
| <button class="btn-primary" style="background:var(--card);color:var(--text);border:1px solid var(--border)" onclick="explainCode()">Explain Code</button> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="dsaResult" style="display:none"> | |
| <div class="summary-box" id="dsaSummary"></div> | |
| <div class="card"> | |
| <h3>Visualization</h3> | |
| <div class="viz-controls"> | |
| <button id="vizPrev" onclick="prevStep()">◀ Prev</button> | |
| <div class="step-info">Step <span id="vizStepNum">0</span> / <span id="vizTotalSteps">0</span></div> | |
| <button id="vizNext" onclick="nextStep()">Next ▶</button> | |
| <button onclick="playSteps()">▶ Play</button> | |
| </div> | |
| <div id="vizDescription" class="viz-desc"></div> | |
| <div id="vizContainer"></div> | |
| </div> | |
| <div class="card" id="dsaExplanationCard" style="display:none"> | |
| <h3>Detailed Explanation</h3> | |
| <div class="value" id="dsaExplanation"></div> | |
| </div> | |
| <div class="card" id="dsaCodeDisplay" style="display:none"> | |
| <h3>Code</h3> | |
| <pre class="code-block" id="dsaCodeBlock"></pre> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="panel-chat" class="panel"> | |
| <div class="card"> | |
| <h3>AI Assistant</h3> | |
| <p style="color:var(--text-muted);font-size:14px;margin-bottom:12px">Ask questions about your uploaded files or get help with code, text, and more.</p> | |
| <div id="chatMessages" style="height:400px;overflow-y:auto;border:1px solid var(--border);border-radius:var(--radius);padding:12px;margin-bottom:12px;background:#010409;"> | |
| <div style="text-align:center;color:var(--text-muted);padding:40px 0;font-size:14px;" id="chatPlaceholder">Upload a file or ask a question to get started.</div> | |
| </div> | |
| <div style="display:flex;gap:8px"> | |
| <input class="input-data" id="chatInput" placeholder="Ask something about your files..." style="flex:1" onkeydown="if(event.key==='Enter') sendChat()"> | |
| <button class="btn-primary" onclick="sendChat()">Send</button> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| let currentFile = null; | |
| let vizSteps = []; | |
| let currentStep = 0; | |
| let playInterval = null; | |
| // Navigation | |
| document.querySelectorAll('.nav-btn').forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| document.querySelectorAll('.nav-btn').forEach(b => b.classList.remove('active')); | |
| btn.classList.add('active'); | |
| document.querySelectorAll('.panel').forEach(p => p.classList.remove('active')); | |
| document.getElementById('panel-' + btn.dataset.panel).classList.add('active'); | |
| }); | |
| }); | |
| // Tabs | |
| document.querySelectorAll('.tab').forEach(tab => { | |
| tab.addEventListener('click', () => { | |
| const parent = tab.parentElement.parentElement; | |
| parent.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); | |
| tab.classList.add('active'); | |
| parent.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active')); | |
| parent.querySelector('#tab-' + tab.dataset.tab).classList.add('active'); | |
| }); | |
| }); | |
| // File upload | |
| const dropZone = document.getElementById('dropZone'); | |
| const fileInput = document.getElementById('fileInput'); | |
| dropZone.addEventListener('click', () => fileInput.click()); | |
| dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('dragover'); }); | |
| dropZone.addEventListener('dragleave', () => dropZone.classList.remove('dragover')); | |
| dropZone.addEventListener('drop', e => { e.preventDefault(); dropZone.classList.remove('dragover'); if (e.dataTransfer.files.length) handleFile(e.dataTransfer.files[0]); }); | |
| fileInput.addEventListener('change', () => { if (fileInput.files.length) handleFile(fileInput.files[0]); }); | |
| function handleFile(file) { | |
| currentFile = file; | |
| document.getElementById('fileInfo').style.display = 'flex'; | |
| document.getElementById('fileName').textContent = file.name; | |
| document.getElementById('fileSize').textContent = (file.size / 1024).toFixed(1) + ' KB'; | |
| const ext = '.' + file.name.split('.').pop().toLowerCase(); | |
| const iconMap = { | |
| '.py':'🐍','.js':'🌐','.html':'🌐','.css':'🎨', | |
| '.c':'⚙','.cpp':'⚙','.java':'☕', | |
| '.jpg':'🖼','.jpeg':'🖼','.png':'🖼','.gif':'🖼', | |
| '.mp3':'🎵','.wav':'🎵', | |
| '.mp4':'🎬','.mkv':'🎬', | |
| '.pdf':'📄','.txt':'📝','.csv':'📊' | |
| }; | |
| document.getElementById('fileIcon').innerHTML = iconMap[ext] || '📄'; | |
| uploadFile(file); | |
| } | |
| function resetUpload() { | |
| currentFile = null; | |
| document.getElementById('fileInfo').style.display = 'none'; | |
| document.getElementById('results').style.display = 'none'; | |
| fileInput.value = ''; | |
| } | |
| async function uploadFile(file) { | |
| document.getElementById('results').style.display = 'block'; | |
| document.getElementById('overviewSummary').textContent = 'Loading...'; | |
| document.getElementById('tab-overview').style.display = 'none'; | |
| document.getElementById('tab-steps').style.display = 'none'; | |
| document.getElementById('tab-preview').style.display = 'none'; | |
| const formData = new FormData(); | |
| formData.append('file', file); | |
| try { | |
| const resp = await fetch('/analyze-file', { method: 'POST', body: formData }); | |
| const data = await resp.json(); | |
| displayResults(data); | |
| } catch (err) { | |
| document.getElementById('overviewSummary').textContent = 'Error: ' + err.message; | |
| } | |
| } | |
| function displayResults(data) { | |
| document.getElementById('results').style.display = 'block'; | |
| document.querySelectorAll('#results .tab')[0].click(); | |
| document.getElementById('overviewSummary').textContent = data.summary || 'No summary available.'; | |
| document.getElementById('ovFileType').innerHTML = (data.file_type || 'unknown') + (data.dsa_type ? ' <span class="badge badge-blue">' + data.dsa_type + '</span>' : ''); | |
| document.getElementById('ovSize').textContent = data.size_kb + ' KB'; | |
| document.getElementById('ovLines').textContent = data.total_lines || '-'; | |
| document.getElementById('ovExplanation').textContent = data.explanation || 'No explanation available.'; | |
| if (data.image_data_url) { | |
| document.getElementById('ovImageContainer').style.display = 'block'; | |
| document.getElementById('ovImage').src = data.image_data_url; | |
| document.getElementById('ovImage').alt = data.filename || 'Uploaded image'; | |
| document.getElementById('previewContent').innerHTML = '<img src="' + data.image_data_url + '" class="image-preview" alt="' + (data.filename || 'image') + '">'; | |
| } else { | |
| document.getElementById('ovImageContainer').style.display = 'none'; | |
| document.getElementById('previewContent').innerHTML = '<pre class="code-block">' + escapeHtml(data.content_preview || 'No preview available') + '</pre>'; | |
| } | |
| if (data.time_complexity || data.space_complexity) { | |
| document.getElementById('ovComplexityCard').style.display = 'block'; | |
| document.getElementById('ovTime').textContent = data.time_complexity || '-'; | |
| document.getElementById('ovSpace').textContent = data.space_complexity || '-'; | |
| } else { | |
| document.getElementById('ovComplexityCard').style.display = 'none'; | |
| } | |
| const stepsList = document.getElementById('stepsList'); | |
| stepsList.innerHTML = ''; | |
| if (data.step_by_step && data.step_by_step.length) { | |
| data.step_by_step.forEach((step, i) => { | |
| const div = document.createElement('div'); | |
| div.className = 'step-item'; | |
| div.innerHTML = '<span class="step-num">' + (i+1) + '</span>' + escapeHtml(step); | |
| stepsList.appendChild(div); | |
| }); | |
| } else { | |
| stepsList.innerHTML = '<p style="color:var(--text-muted)">No step-by-step breakdown available.</p>'; | |
| } | |
| } | |
| function escapeHtml(text) { | |
| if (!text) return ''; | |
| const div = document.createElement('div'); | |
| div.textContent = text; | |
| return div.innerHTML; | |
| } | |
| // DSA Templates | |
| async function loadTemplates() { | |
| try { | |
| const resp = await fetch('/dsa/templates'); | |
| const data = await resp.json(); | |
| const sel = document.getElementById('dsaTemplate'); | |
| sel.innerHTML = '<option value="">-- Select a built-in template --</option>'; | |
| for (const [key, tmpl] of Object.entries(data.templates)) { | |
| const opt = document.createElement('option'); | |
| opt.value = key; | |
| opt.textContent = tmpl.title + ' (' + tmpl.time + ')'; | |
| sel.appendChild(opt); | |
| } | |
| } catch (e) { console.error('Failed to load templates', e); } | |
| } | |
| loadTemplates(); | |
| document.getElementById('dsaTemplate').addEventListener('change', async function() { | |
| if (!this.value) return; | |
| try { | |
| const resp = await fetch('/dsa/visualize-template', { | |
| method: 'POST', | |
| headers: {'Content-Type':'application/json'}, | |
| body: JSON.stringify({template: this.value}) | |
| }); | |
| const data = await resp.json(); | |
| document.getElementById('dsaCode').value = data.code || '// Template loaded'; | |
| if (data.steps && data.steps.length) { | |
| vizSteps = data.steps; | |
| currentStep = 0; | |
| renderVisualization(); | |
| document.getElementById('dsaResult').style.display = 'block'; | |
| } | |
| document.getElementById('dsaSummary').innerHTML = '<strong>' + data.title + '</strong> — ' + (data.summary || data.description || ''); | |
| if (data.explanation) { | |
| document.getElementById('dsaExplanationCard').style.display = 'block'; | |
| document.getElementById('dsaExplanation').textContent = data.explanation; | |
| } else { | |
| document.getElementById('dsaExplanationCard').style.display = 'none'; | |
| } | |
| if (data.code) { | |
| document.getElementById('dsaCodeDisplay').style.display = 'block'; | |
| document.getElementById('dsaCodeBlock').textContent = data.code; | |
| } else { | |
| document.getElementById('dsaCodeDisplay').style.display = 'none'; | |
| } | |
| } catch (e) { console.error(e); } | |
| }); | |
| async function runVisualization() { | |
| const code = document.getElementById('dsaCode').value.trim(); | |
| const inputRaw = document.getElementById('dsaInput').value.trim(); | |
| const template = document.getElementById('dsaTemplate').value; | |
| if (!code && !template) { alert('Select a template or paste code'); return; } | |
| let inputData = null; | |
| if (inputRaw) { | |
| inputData = inputRaw.split(',').map(s => { | |
| const n = Number(s.trim()); | |
| return isNaN(n) ? s.trim() : n; | |
| }); | |
| } | |
| try { | |
| let resp; | |
| if (template && !code) { | |
| resp = await fetch('/dsa/visualize-template', { | |
| method: 'POST', headers: {'Content-Type':'application/json'}, | |
| body: JSON.stringify({template, input_data: inputData}) | |
| }); | |
| } else { | |
| resp = await fetch('/dsa/visualize', { | |
| method: 'POST', headers: {'Content-Type':'application/json'}, | |
| body: JSON.stringify({code, input_data: inputData}) | |
| }); | |
| } | |
| const data = await resp.json(); | |
| document.getElementById('dsaResult').style.display = 'block'; | |
| document.getElementById('dsaSummary').innerHTML = '<strong>' + (data.title || 'Visualization') + '</strong> — ' + (data.summary || data.description || ''); | |
| if (data.explanation) { | |
| document.getElementById('dsaExplanationCard').style.display = 'block'; | |
| document.getElementById('dsaExplanation').textContent = data.explanation; | |
| } else { | |
| document.getElementById('dsaExplanationCard').style.display = 'none'; | |
| } | |
| if (data.code) { | |
| document.getElementById('dsaCodeDisplay').style.display = 'block'; | |
| document.getElementById('dsaCodeBlock').textContent = data.code; | |
| } else { | |
| document.getElementById('dsaCodeDisplay').style.display = 'none'; | |
| } | |
| if (data.steps && data.steps.length) { | |
| vizSteps = data.steps; | |
| currentStep = 0; | |
| renderVisualization(); | |
| } | |
| } catch (e) { alert('Error: ' + e.message); } | |
| } | |
| async function explainCode() { | |
| const code = document.getElementById('dsaCode').value.trim(); | |
| if (!code) { alert('Paste some code first'); return; } | |
| try { | |
| const resp = await fetch('/dsa/explain-code', { | |
| method: 'POST', headers: {'Content-Type':'application/json'}, | |
| body: JSON.stringify({code}) | |
| }); | |
| const data = await resp.json(); | |
| document.getElementById('dsaResult').style.display = 'block'; | |
| document.getElementById('dsaSummary').innerHTML = '<strong>' + data.title + '</strong> — ' + (data.summary || ''); | |
| document.getElementById('dsaExplanationCard').style.display = 'block'; | |
| document.getElementById('dsaExplanation').textContent = data.explanation || data.summary || 'Analysis complete.'; | |
| document.getElementById('dsaCodeDisplay').style.display = 'block'; | |
| document.getElementById('dsaCodeBlock').textContent = code; | |
| vizSteps = []; | |
| document.getElementById('vizContainer').innerHTML = ''; | |
| document.getElementById('vizDescription').textContent = ''; | |
| document.querySelector('.viz-controls').style.display = 'none'; | |
| } catch (e) { alert('Error: ' + e.message); } | |
| } | |
| // Visualization rendering | |
| function renderVisualization() { | |
| if (!vizSteps.length) return; | |
| currentStep = Math.max(0, Math.min(currentStep, vizSteps.length - 1)); | |
| const step = vizSteps[currentStep]; | |
| document.getElementById('vizStepNum').textContent = currentStep + 1; | |
| document.getElementById('vizTotalSteps').textContent = vizSteps.length; | |
| document.getElementById('vizPrev').disabled = currentStep === 0; | |
| document.getElementById('vizNext').disabled = currentStep === vizSteps.length - 1; | |
| document.getElementById('vizDescription').textContent = step.description || ''; | |
| document.querySelector('.viz-controls').style.display = 'flex'; | |
| const container = document.getElementById('vizContainer'); | |
| if (step.array_state) { | |
| renderArrayBars(container, step.array_state, step.highlight_nodes || []); | |
| } else if (step.tree_state) { | |
| renderTree(container, step.tree_state, step.highlight_nodes || []); | |
| } else if (step.linked_list_state) { | |
| renderLinkedList(container, step.linked_list_state, step.highlight_nodes || []); | |
| } else { | |
| container.innerHTML = '<div class="loading">Step ' + (currentStep + 1) + '</div>'; | |
| } | |
| } | |
| function renderArrayBars(container, arr, highlights) { | |
| const maxVal = Math.max(...arr.filter(v => typeof v === 'number')); | |
| container.innerHTML = '<div class="array-bars">' + | |
| arr.map((v, i) => { | |
| const h = typeof v === 'number' ? (v / maxVal * 180) : 100; | |
| const cls = highlights.includes(i) ? 'array-bar highlight' : 'array-bar'; | |
| return '<div class="' + cls + '" style="height:' + h + 'px;flex:' + (1/arr.length*100) + '%">' + v + '</div>'; | |
| }).join('') + | |
| '</div>'; | |
| } | |
| function renderTree(container, tree, highlights) { | |
| const canvas = document.createElement('canvas'); | |
| canvas.className = 'tree-canvas'; | |
| canvas.width = container.offsetWidth || 800; | |
| canvas.height = 300; | |
| container.innerHTML = ''; | |
| container.appendChild(canvas); | |
| const ctx = canvas.getContext('2d'); | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| function drawNode(node, x, y, level, maxLevel) { | |
| if (!node) return; | |
| const radius = 22; | |
| const spacing = Math.max(40, canvas.width / Math.pow(2, level + 1)); | |
| ctx.beginPath(); | |
| ctx.arc(x, y, radius, 0, Math.PI * 2); | |
| const isHighlight = highlights.includes(node.id); | |
| ctx.fillStyle = isHighlight ? '#d29922' : '#58a6ff'; | |
| ctx.fill(); | |
| ctx.strokeStyle = '#30363d'; | |
| ctx.lineWidth = 2; | |
| ctx.stroke(); | |
| ctx.fillStyle = '#fff'; | |
| ctx.font = 'bold 13px sans-serif'; | |
| ctx.textAlign = 'center'; | |
| ctx.textBaseline = 'middle'; | |
| ctx.fillText(String(node.val || ''), x, y); | |
| const childY = y + 70; | |
| if (node.left) { | |
| ctx.beginPath(); | |
| ctx.moveTo(x - 10, y + radius); | |
| ctx.lineTo(x - spacing / 2, childY - radius); | |
| ctx.strokeStyle = '#666'; | |
| ctx.lineWidth = 1.5; | |
| ctx.stroke(); | |
| drawNode(node.left, x - spacing / 2, childY, level + 1, maxLevel); | |
| } | |
| if (node.right) { | |
| ctx.beginPath(); | |
| ctx.moveTo(x + 10, y + radius); | |
| ctx.lineTo(x + spacing / 2, childY - radius); | |
| ctx.strokeStyle = '#666'; | |
| ctx.lineWidth = 1.5; | |
| ctx.stroke(); | |
| drawNode(node.right, x + spacing / 2, childY, level + 1, maxLevel); | |
| } | |
| } | |
| drawNode(tree, canvas.width / 2, 40, 0, 5); | |
| } | |
| function renderLinkedList(container, nodes, highlights) { | |
| const canvas = document.createElement('canvas'); | |
| canvas.className = 'll-canvas'; | |
| canvas.width = container.offsetWidth || 800; | |
| canvas.height = 200; | |
| container.innerHTML = ''; | |
| container.appendChild(canvas); | |
| const ctx = canvas.getContext('2d'); | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| const nodeW = 70, nodeH = 40; | |
| const startX = 30, y = 80; | |
| nodes.forEach((node, i) => { | |
| const x = startX + i * (nodeW + 50); | |
| const isHighlight = highlights.includes(node.id); | |
| ctx.fillStyle = isHighlight ? '#d29922' : '#58a6ff'; | |
| ctx.fillRect(x, y - nodeH / 2, nodeW, nodeH); | |
| ctx.strokeStyle = '#30363d'; | |
| ctx.lineWidth = 2; | |
| ctx.strokeRect(x, y - nodeH / 2, nodeW, nodeH); | |
| ctx.fillStyle = '#fff'; | |
| ctx.font = 'bold 14px sans-serif'; | |
| ctx.textAlign = 'center'; | |
| ctx.textBaseline = 'middle'; | |
| ctx.fillText(String(node.value || ''), x + nodeW / 2, y); | |
| if (node.next) { | |
| const arrowX = x + nodeW; | |
| ctx.beginPath(); | |
| ctx.moveTo(arrowX, y); | |
| ctx.lineTo(arrowX + 40, y); | |
| ctx.strokeStyle = '#666'; | |
| ctx.lineWidth = 2; | |
| ctx.stroke(); | |
| ctx.beginPath(); | |
| ctx.moveTo(arrowX + 40, y - 6); | |
| ctx.lineTo(arrowX + 50, y); | |
| ctx.lineTo(arrowX + 40, y + 6); | |
| ctx.fillStyle = '#666'; | |
| ctx.fill(); | |
| } | |
| }); | |
| } | |
| function nextStep() { if (currentStep < vizSteps.length - 1) { currentStep++; renderVisualization(); } } | |
| function prevStep() { if (currentStep > 0) { currentStep--; renderVisualization(); } } | |
| function playSteps() { | |
| if (playInterval) { clearInterval(playInterval); playInterval = null; return; } | |
| playInterval = setInterval(() => { | |
| if (currentStep < vizSteps.length - 1) { currentStep++; renderVisualization(); } | |
| else { clearInterval(playInterval); playInterval = null; } | |
| }, 1000); | |
| } | |
| // Chat | |
| let chatHistory = []; | |
| async function sendChat() { | |
| const input = document.getElementById('chatInput'); | |
| const msg = input.value.trim(); | |
| if (!msg) return; | |
| input.value = ''; | |
| document.getElementById('chatPlaceholder').style.display = 'none'; | |
| const container = document.getElementById('chatMessages'); | |
| const userDiv = document.createElement('div'); | |
| userDiv.style.cssText = 'background:var(--accent);color:#fff;padding:10px 14px;border-radius:12px 12px 2px 12px;margin:6px 0 6px auto;max-width:80%;width:fit-content;font-size:14px;line-height:1.5;'; | |
| userDiv.textContent = msg; | |
| container.appendChild(userDiv); | |
| container.scrollTop = container.scrollHeight; | |
| chatHistory.push({role:'user',content:msg}); | |
| const loadingDiv = document.createElement('div'); | |
| loadingDiv.style.cssText = 'text-align:center;padding:12px;color:var(--text-muted);font-size:13px;'; | |
| loadingDiv.textContent = 'Thinking...'; | |
| container.appendChild(loadingDiv); | |
| try { | |
| const resp = await fetch('/chat', { | |
| method:'POST', | |
| headers:{'Content-Type':'application/json'}, | |
| body:JSON.stringify({prompt:msg}) | |
| }); | |
| const data = await resp.json(); | |
| loadingDiv.remove(); | |
| const text = data.response || 'No response'; | |
| const aiDiv = document.createElement('div'); | |
| aiDiv.style.cssText = 'background:var(--card);border:1px solid var(--border);color:var(--text);padding:10px 14px;border-radius:12px 12px 12px 2px;margin:6px auto 6px 0;max-width:80%;width:fit-content;font-size:14px;line-height:1.5;white-space:pre-wrap;'; | |
| aiDiv.textContent = text; | |
| container.appendChild(aiDiv); | |
| chatHistory.push({role:'assistant',content:text}); | |
| } catch(e) { | |
| loadingDiv.remove(); | |
| const errDiv = document.createElement('div'); | |
| errDiv.style.cssText = 'color:var(--danger);padding:8px 12px;font-size:13px;text-align:center;'; | |
| errDiv.textContent = 'AI assistant unavailable (Ollama not running). Try again later.'; | |
| container.appendChild(errDiv); | |
| } | |
| container.scrollTop = container.scrollHeight; | |
| } | |
| document.addEventListener('keydown', e => { | |
| if (e.key === 'ArrowRight') nextStep(); | |
| if (e.key === 'ArrowLeft') prevStep(); | |
| }); | |
| </script> | |
| </body> | |
| </html> | |