Download static/problem.js from JetBrains-Research/ml4se-evals-visualization: direct link, hf CLI and curl.
- Browser
- Download file 53.4 kB
-
https://huggingface.co/spaces/JetBrains-Research/ml4se-evals-visualization/resolve/main/static/problem.js
- Command line
-
hf download hf://spaces/JetBrains-Research/ml4se-evals-visualization/static/problem.js
-
curl -L -o problem.js https://huggingface.co/spaces/JetBrains-Research/ml4se-evals-visualization/resolve/main/static/problem.js
53.4 kB
| /* global problemIdx, datasetSlug, datasetName, hasGroundTruth, hasTasks */ | |
| function badgeClass(source) { | |
| return 'badge-' + source.toLowerCase().replace(/[^a-z0-9]/g, ''); | |
| } | |
| async function loadProblem() { | |
| try { | |
| const response = await fetch(`/api/${datasetSlug}/problem/${problemIdx}`); | |
| const problem = await response.json(); | |
| if (problem.error) { | |
| document.getElementById('problem-content').innerHTML = | |
| '<div class="card"><p style="color: red;">Error: ' + problem.error + '</p></div>'; | |
| return; | |
| } | |
| renderProblem(problem); | |
| } catch (error) { | |
| document.getElementById('problem-content').innerHTML = | |
| '<div class="card"><p style="color: red;">Error loading problem: ' + error.message + '</p></div>'; | |
| } | |
| } | |
| function renderProblem(problem) { | |
| const container = document.getElementById('problem-content'); | |
| // Main problem info card (shared by all datasets) | |
| let html = ` | |
| <div class="card"> | |
| <div class="problem-header"> | |
| <h2>${escapeHtml(problem.entry_point)}</h2> | |
| <span class="badge ${badgeClass(problem.source)}">${escapeHtml(problem.source)}</span> | |
| </div> | |
| <div class="problem-meta"> | |
| <div class="meta-item"> | |
| <span class="meta-label">Task ID:</span> | |
| <span class="meta-value">${escapeHtml(problem.task_id)}</span> | |
| </div> | |
| <div class="meta-item"> | |
| <span class="meta-label">Index:</span> | |
| <span class="meta-value">${problem.idx}</span> | |
| </div> | |
| <div class="meta-item"> | |
| <span class="meta-label">Dataset:</span> | |
| <span class="meta-value">${escapeHtml(datasetName)}</span> | |
| </div> | |
| ${problem.inputs.length > 0 ? ` | |
| <div class="meta-item"> | |
| <span class="meta-label">Test Inputs:</span> | |
| <span class="meta-value">${problem.inputs.length}</span> | |
| </div>` : ''} | |
| </div> | |
| </div> | |
| `; | |
| // --- BigOBench view (problem description + per-solution code & complexity) --- | |
| if (problem.solutions && problem.solutions.length > 0) { | |
| // Problem description | |
| if (problem.description) { | |
| html += ` | |
| <div class="card"> | |
| <h2>Problem Statement</h2> | |
| <pre class="code-block" style="white-space: pre-wrap;">${escapeHtml(problem.description)}</pre> | |
| </div> | |
| `; | |
| } | |
| // Each solution: code + complexity/language badges | |
| problem.solutions.forEach((sol, i) => { | |
| html += ` | |
| <div class="card"> | |
| <h2>Solution ${i + 1} <span style="font-size:0.8rem;color:#7f8c8d;font-weight:400;">${escapeHtml(sol.solution_id)}</span></h2> | |
| <div class="complexity-badges" style="margin-bottom: 15px;"> | |
| `; | |
| if (sol.language) { | |
| html += ` | |
| <div class="complexity-item"> | |
| <span class="complexity-label">Language</span> | |
| <span class="badge badge-info">${escapeHtml(sol.language)}</span> | |
| </div>`; | |
| } | |
| if (sol.time_complexity) { | |
| html += ` | |
| <div class="complexity-item"> | |
| <span class="complexity-label">Time</span> | |
| <span class="complexity-value">${escapeHtml(sol.time_complexity)}</span> | |
| </div>`; | |
| } | |
| if (sol.space_complexity) { | |
| html += ` | |
| <div class="complexity-item"> | |
| <span class="complexity-label">Space</span> | |
| <span class="complexity-value">${escapeHtml(sol.space_complexity)}</span> | |
| </div>`; | |
| } | |
| html += ` | |
| </div> | |
| <div class="code-with-tasks"> | |
| ${sol.highlighted_code} | |
| </div> | |
| </div> | |
| `; | |
| }); | |
| // Navigation hint | |
| html += ` | |
| <div class="navigation-hint"> | |
| <strong>Tip:</strong> Use the Previous/Next buttons at the top to browse through problems, | |
| or return to the list view to filter by dataset source or search by name. | |
| </div> | |
| `; | |
| container.innerHTML = html; | |
| window.currentProblem = problem; | |
| return; | |
| } | |
| // --- DebugBench before/after view (buggy → fixed) --- | |
| if (problem.buggy_code !== undefined && problem.fixed_code !== undefined) { | |
| // Problem description | |
| if (problem.description) { | |
| html += ` | |
| <div class="card"> | |
| <h2>Problem Statement</h2> | |
| <pre class="code-block" style="white-space: pre-wrap;">${escapeHtml(problem.description)}</pre> | |
| </div> | |
| `; | |
| } | |
| // Bug info | |
| html += ` | |
| <div class="card"> | |
| <h2>Bug Information</h2> | |
| <div class="bug-info"> | |
| <span class="bug-category">${escapeHtml(problem.bug_category || '')}</span> | |
| <span class="bug-subtype">${escapeHtml(problem.bug_subtype || '')}</span> | |
| </div> | |
| <p style="margin-top: 10px;">${escapeHtml(problem.bug_explanation || '')}</p> | |
| `; | |
| if (problem.difficulty) { | |
| html += `<p style="margin-top: 8px; color: #7f8c8d;">Difficulty: <strong>${escapeHtml(problem.difficulty)}</strong></p>`; | |
| } | |
| html += `</div>`; | |
| // Unified diff view of buggy → fixed | |
| const unifiedDiff = computeUnifiedDiff(problem.buggy_code, problem.fixed_code); | |
| html += ` | |
| <div class="card"> | |
| <h2>Changes</h2> | |
| <div class="diff-view">${renderComputedDiff(unifiedDiff)}</div> | |
| </div> | |
| `; | |
| // Side-by-side buggy/fixed code | |
| html += ` | |
| <div class="card"> | |
| <h2>Full Code Comparison</h2> | |
| <div class="diff-container"> | |
| <div class="diff-panel"> | |
| <h3><span class="diff-label-buggy">Before</span></h3> | |
| <div class="code-with-tasks">${problem.buggy_highlighted_code}</div> | |
| </div> | |
| <div class="diff-panel"> | |
| <h3><span class="diff-label-fixed">After</span></h3> | |
| <div class="code-with-tasks">${problem.fixed_highlighted_code}</div> | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| // Examples | |
| if (problem.examples && problem.examples.length > 0) { | |
| html += `<div class="card"><h2>Examples</h2>`; | |
| problem.examples.forEach((ex, i) => { | |
| html += `<pre class="code-block" style="margin-bottom: 10px; white-space: pre-wrap;">${escapeHtml(ex)}</pre>`; | |
| }); | |
| html += `</div>`; | |
| } | |
| html += ` | |
| <div class="navigation-hint"> | |
| <strong>Tip:</strong> Use the Previous/Next buttons at the top to browse through problems, | |
| or return to the list view to filter by dataset source or search by name. | |
| </div> | |
| `; | |
| container.innerHTML = html; | |
| window.currentProblem = problem; | |
| return; | |
| } | |
| // --- HumanEval-X / HumanEvalPack multi-language view --- | |
| if (problem.lang_solutions && problem.lang_solutions.length > 0) { | |
| // Check if this is HumanEvalPack (has buggy_code in solutions) | |
| const hasBuggy = problem.lang_solutions.some(sol => sol.buggy_code); | |
| // Bug info (HumanEvalPack only) | |
| if (hasBuggy && (problem.bug_type || problem.failure_symptoms)) { | |
| html += ` | |
| <div class="card"> | |
| <h2>Bug Information</h2> | |
| <div class="bug-info"> | |
| ${problem.bug_type ? `<span class="bug-category">${escapeHtml(problem.bug_type)}</span>` : ''} | |
| ${problem.failure_symptoms ? `<span class="bug-subtype">${escapeHtml(problem.failure_symptoms)}</span>` : ''} | |
| </div> | |
| </div> | |
| `; | |
| } | |
| // Language tabs with code panels | |
| html += ` | |
| <div class="card"> | |
| <h2>Source Code</h2> | |
| `; | |
| // Buggy/Canonical toggle for HumanEvalPack | |
| if (hasBuggy) { | |
| html += ` | |
| <div class="lang-tabs" id="code-mode-tabs" style="margin-bottom: 10px;"> | |
| <button class="lang-tab active" onclick="toggleCodeMode('canonical')">Canonical</button> | |
| <button class="lang-tab" onclick="toggleCodeMode('buggy')">Buggy</button> | |
| </div> | |
| `; | |
| } | |
| html += `<div class="lang-tabs" id="lang-tabs">`; | |
| problem.lang_solutions.forEach((sol, i) => { | |
| const label = sol.language_label || sol.language; | |
| html += `<button class="lang-tab ${i === 0 ? 'active' : ''}" onclick="showLangTab(${i})">${escapeHtml(label)}</button>`; | |
| }); | |
| html += `</div>`; | |
| problem.lang_solutions.forEach((sol, i) => { | |
| html += ` | |
| <div class="lang-code-panel ${i === 0 ? 'active' : ''}" id="lang-panel-${i}"> | |
| <div class="code-with-tasks" id="lang-code-canonical-${i}">${sol.highlighted_code}</div> | |
| ${sol.buggy_code ? `<div class="code-with-tasks" id="lang-code-buggy-${i}" style="display:none;">${sol.buggy_highlighted_code}</div>` : ''} | |
| </div> | |
| `; | |
| }); | |
| html += `</div>`; | |
| // Test suite for current language | |
| html += `<div class="card" id="lang-test-container">`; | |
| if (problem.lang_solutions[0].test) { | |
| html += `<h2>Test Suite</h2><pre class="code-block">${escapeHtml(problem.lang_solutions[0].test)}</pre>`; | |
| } | |
| html += `</div>`; | |
| // Description | |
| if (problem.description) { | |
| html += ` | |
| <div class="card"> | |
| <h2>Problem Description</h2> | |
| <div style="padding: 10px; line-height: 1.6; white-space: pre-wrap;">${escapeHtml(problem.description)}</div> | |
| </div> | |
| `; | |
| } | |
| html += ` | |
| <div class="navigation-hint"> | |
| <strong>Tip:</strong> Use the Previous/Next buttons at the top to browse through problems, | |
| or return to the list view to filter by dataset source or search by name. | |
| </div> | |
| `; | |
| container.innerHTML = html; | |
| window.currentProblem = problem; | |
| window._currentCodeMode = 'canonical'; | |
| return; | |
| } | |
| // --- SWE-bench / CommitBench diff view (unified diff patch) --- | |
| if (problem.patch !== undefined) { | |
| // GitHub links bar (SWE-bench variants) | |
| const ghLinks = []; | |
| if (problem.repo_url) ghLinks.push(`<a href="${escapeHtml(problem.repo_url)}" target="_blank" class="gh-link">Repository</a>`); | |
| if (problem.issue_url) ghLinks.push(`<a href="${escapeHtml(problem.issue_url)}" target="_blank" class="gh-link">Issue</a>`); | |
| if (problem.commit_url) ghLinks.push(`<a href="${escapeHtml(problem.commit_url)}" target="_blank" class="gh-link">Base Commit</a>`); | |
| if (ghLinks.length > 0) { | |
| html += `<div class="card gh-links-bar">${ghLinks.join('')}</div>`; | |
| } | |
| // Metadata badges (version, date) | |
| const metaBadges = []; | |
| if (problem.version) metaBadges.push(`<span class="badge badge-info">v${escapeHtml(problem.version)}</span>`); | |
| if (problem.created_at) metaBadges.push(`<span class="badge badge-info">${escapeHtml(problem.created_at.split('T')[0])}</span>`); | |
| if (problem.commit_hash) metaBadges.push(`<span class="badge badge-info">${escapeHtml(problem.commit_hash.substring(0, 12))}</span>`); | |
| if (problem.diff_languages) metaBadges.push(`<span class="badge badge-info">${escapeHtml(problem.diff_languages)}</span>`); | |
| if (metaBadges.length > 0) { | |
| html += `<div style="margin-bottom: 15px;">${metaBadges.join(' ')}</div>`; | |
| } | |
| // Problem statement (issue text / commit message) | |
| if (problem.description) { | |
| html += ` | |
| <div class="card"> | |
| <h2>${problem.issue_url ? 'Issue Description' : 'Description'}</h2> | |
| <div class="issue-statement">${escapeHtml(problem.description)}</div> | |
| </div> | |
| `; | |
| } | |
| // Render unified diff with per-file sections | |
| html += renderDiffFiles(problem.patch, 'Solution Patch'); | |
| // Test patch if available | |
| if (problem.test_patch) { | |
| html += renderDiffFiles(problem.test_patch, 'Test Patch'); | |
| } | |
| // Failing tests | |
| if (problem.fail_to_pass && problem.fail_to_pass.length > 0) { | |
| html += `<div class="card"><h2>Tests: Fail → Pass</h2><ul class="test-id-list">`; | |
| problem.fail_to_pass.forEach(t => { | |
| html += `<li>${escapeHtml(t)}</li>`; | |
| }); | |
| html += `</ul></div>`; | |
| } | |
| // Hints | |
| if (problem.hints) { | |
| html += ` | |
| <div class="card"> | |
| <h2>Hints</h2> | |
| <div class="issue-statement">${escapeHtml(problem.hints)}</div> | |
| </div> | |
| `; | |
| } | |
| html += ` | |
| <div class="navigation-hint"> | |
| <strong>Tip:</strong> Use the Previous/Next buttons at the top to browse through problems, | |
| or return to the list view to filter by dataset source or search by name. | |
| </div> | |
| `; | |
| container.innerHTML = html; | |
| window.currentProblem = problem; | |
| return; | |
| } | |
| // --- SAFIM Fill-in-the-Middle view --- | |
| if (problem.fim_ground_truth !== undefined) { | |
| // Tab bar: "With Gap" | "Completed" | "Completion Only" | |
| html += ` | |
| <div class="card"> | |
| <h2>Fill-in-the-Middle</h2> | |
| <div class="lang-tabs" id="fim-tabs"> | |
| <button class="lang-tab" onclick="showFimTab(0)">With Gap</button> | |
| <button class="lang-tab active" onclick="showFimTab(1)">Completed</button> | |
| <button class="lang-tab" onclick="showFimTab(2)">Completion Only</button> | |
| </div> | |
| <div class="lang-code-panel" id="fim-panel-0"> | |
| <div class="code-with-tasks">${problem.highlighted_code}</div> | |
| </div> | |
| <div class="lang-code-panel active" id="fim-panel-1"> | |
| <div class="fim-merged-legend"> | |
| <span class="coverage-swatch" style="background:#ffffcc; border:1px solid #ccc;"></span> | |
| Inserted completion (lines ${problem.fim_gt_start_line}–${problem.fim_gt_end_line}) | |
| </div> | |
| <div class="code-with-tasks">${problem.fim_merged_highlighted}</div> | |
| </div> | |
| <div class="lang-code-panel" id="fim-panel-2"> | |
| <div class="fim-answer">${problem.fim_ground_truth_highlighted || escapeHtml(problem.fim_ground_truth)}</div> | |
| </div> | |
| </div> | |
| `; | |
| html += ` | |
| <div class="navigation-hint"> | |
| <strong>Tip:</strong> Use the Previous/Next buttons at the top to browse through problems, | |
| or return to the list view to filter by dataset source or search by name. | |
| </div> | |
| `; | |
| container.innerHTML = html; | |
| window.currentProblem = problem; | |
| return; | |
| } | |
| // --- Vulnerability view (BigVul, DiverseVul, PrimeVul) --- | |
| if (problem.vulnerable_code !== undefined || problem.is_vulnerable !== undefined) { | |
| // Vulnerability status and CWE info | |
| const isVuln = problem.is_vulnerable; | |
| html += ` | |
| <div class="card"> | |
| <h2>Vulnerability Information</h2> | |
| <div style="margin-bottom: 10px;"> | |
| <span class="vuln-status ${isVuln ? 'vuln-status-vulnerable' : 'vuln-status-patched'}"> | |
| ${isVuln ? 'Vulnerable' : 'Patched'} | |
| </span> | |
| ${problem.cwe_id ? `<span class="cwe-badge">${escapeHtml(problem.cwe_id)}</span>` : ''} | |
| ${problem.cve_id ? `<span class="badge badge-info">${escapeHtml(problem.cve_id)}</span>` : ''} | |
| ${problem.project ? `<span class="badge badge-info">${escapeHtml(problem.project)}</span>` : ''} | |
| </div> | |
| ${problem.description ? `<p style="margin-top: 10px; color: #555;">${escapeHtml(problem.description).substring(0, 500)}</p>` : ''} | |
| </div> | |
| `; | |
| // Show code with vuln/patched side-by-side if both available | |
| if (problem.vulnerable_code && problem.patched_code) { | |
| const vulnDiff = computeUnifiedDiff(problem.vulnerable_code, problem.patched_code); | |
| html += ` | |
| <div class="card"> | |
| <h2>Changes</h2> | |
| <div class="diff-view">${renderComputedDiff(vulnDiff)}</div> | |
| </div> | |
| `; | |
| html += ` | |
| <div class="card"> | |
| <h2>Full Code Comparison</h2> | |
| <div class="diff-container"> | |
| <div class="diff-panel"> | |
| <h3><span class="diff-label-buggy">Vulnerable</span></h3> | |
| <div class="code-with-tasks">${problem.vulnerable_highlighted_code}</div> | |
| </div> | |
| <div class="diff-panel"> | |
| <h3><span class="diff-label-fixed">Patched</span></h3> | |
| <div class="code-with-tasks">${problem.patched_highlighted_code}</div> | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| } else { | |
| // Single code view | |
| html += ` | |
| <div class="card"> | |
| <h2>Source Code</h2> | |
| <div class="code-with-tasks">${problem.highlighted_code}</div> | |
| </div> | |
| `; | |
| } | |
| html += ` | |
| <div class="navigation-hint"> | |
| <strong>Tip:</strong> Use the Previous/Next buttons at the top to browse through problems, | |
| or return to the list view to filter by dataset source or search by name. | |
| </div> | |
| `; | |
| container.innerHTML = html; | |
| window.currentProblem = problem; | |
| return; | |
| } | |
| // Source Code card | |
| html += ` | |
| <div class="card"> | |
| <h2>Source Code</h2> | |
| <div class="code-with-tasks" id="code-container"> | |
| ${problem.highlighted_code} | |
| </div> | |
| </div> | |
| `; | |
| // --- Non-DREval (simple) view --- | |
| if (!hasTasks) { | |
| // GitHub / external links (e.g. LCA adapters) | |
| const simpleLinks = []; | |
| if (problem.repo_url) simpleLinks.push(`<a href="${escapeHtml(problem.repo_url)}" target="_blank" class="gh-link">Repository</a>`); | |
| if (problem.issue_url) simpleLinks.push(`<a href="${escapeHtml(problem.issue_url)}" target="_blank" class="gh-link">Issue</a>`); | |
| if (problem.commit_url) simpleLinks.push(`<a href="${escapeHtml(problem.commit_url)}" target="_blank" class="gh-link">Commit</a>`); | |
| if (simpleLinks.length > 0) { | |
| html += `<div class="card gh-links-bar">${simpleLinks.join('')}</div>`; | |
| } | |
| // Show description if available (e.g. LiveCodeBench, MBPP+, ClassEval) | |
| if (problem.description) { | |
| html += ` | |
| <div class="card"> | |
| <h2>Problem Description</h2> | |
| <div style="padding: 10px; line-height: 1.6; white-space: pre-wrap;">${escapeHtml(problem.description)}</div> | |
| </div> | |
| `; | |
| } | |
| // Show difficulty, contest date, tags, rating if available | |
| if (problem.difficulty || problem.contest_date || problem.tags || problem.cf_rating) { | |
| let metaHtml = ''; | |
| if (problem.difficulty) { | |
| metaHtml += `<span class="badge badge-info">Difficulty: ${escapeHtml(problem.difficulty)}</span>`; | |
| } | |
| if (problem.cf_rating) { | |
| metaHtml += `<span class="badge badge-info">Rating: ${problem.cf_rating}</span>`; | |
| } | |
| if (problem.contest_date) { | |
| metaHtml += `<span class="badge badge-info">Date: ${escapeHtml(problem.contest_date.split('T')[0])}</span>`; | |
| } | |
| if (problem.tags && problem.tags.length > 0) { | |
| problem.tags.forEach(tag => { | |
| metaHtml += `<span class="badge badge-info">${escapeHtml(tag)}</span>`; | |
| }); | |
| } | |
| html += `<div style="margin-bottom: 15px;">${metaHtml}</div>`; | |
| } | |
| // Show inputs/outputs if available | |
| if (problem.inputs && problem.inputs.length > 0) { | |
| html += `<div class="card"><h2>Inputs & Outputs</h2>`; | |
| problem.inputs.forEach((inp, i) => { | |
| const out = (problem.outputs && problem.outputs[i]) || ''; | |
| html += ` | |
| <div class="io-section" style="margin-bottom: 15px;"> | |
| <div class="task-section"> | |
| <h3>Input ${i + 1}</h3> | |
| <pre class="code-block">${escapeHtml(inp)}</pre> | |
| </div> | |
| <div class="task-section"> | |
| <h3>Output</h3> | |
| <pre class="code-block">${escapeHtml(out)}</pre> | |
| </div> | |
| </div> | |
| `; | |
| }); | |
| html += `</div>`; | |
| } | |
| // Show test suite if available | |
| if (problem.test) { | |
| html += ` | |
| <div class="card"> | |
| <h2>Test Suite</h2> | |
| <pre class="code-block">${escapeHtml(problem.test)}</pre> | |
| </div> | |
| `; | |
| } | |
| // Navigation hint | |
| html += ` | |
| <div class="navigation-hint"> | |
| <strong>Tip:</strong> Use the Previous/Next buttons at the top to browse through problems, | |
| or return to the list view to filter by dataset source or search by name. | |
| </div> | |
| `; | |
| container.innerHTML = html; | |
| window.currentProblem = problem; | |
| return; | |
| } | |
| // --- CRUXEval task view (tasks have given/predict fields, no task_items) --- | |
| if (problem.tasks.length > 0 && problem.tasks[0].given !== undefined) { | |
| // Task selector | |
| html += ` | |
| <div class="card"> | |
| <h2>Tasks</h2> | |
| <div class="task-selector" id="task-selector"> | |
| `; | |
| problem.tasks.forEach((task, idx) => { | |
| html += ` | |
| <button class="task-btn ${idx === 0 ? 'active' : ''}" | |
| onclick="showCruxTask(${idx})"> | |
| ${escapeHtml(task.name)} | |
| </button> | |
| `; | |
| }); | |
| html += ` | |
| </div> | |
| <div id="task-content"></div> | |
| </div> | |
| `; | |
| // Navigation hint | |
| html += ` | |
| <div class="navigation-hint"> | |
| <strong>Tip:</strong> Use the Previous/Next buttons at the top to browse through problems, | |
| or return to the list view to filter by dataset source or search by name. | |
| </div> | |
| `; | |
| container.innerHTML = html; | |
| window.currentProblem = problem; | |
| showCruxTask(0); | |
| return; | |
| } | |
| // --- DREval (full) view with tasks, coverage, arrows --- | |
| // Rebuild html cleanly with coverage legend and SVG overlay | |
| html = ` | |
| <div class="card"> | |
| <div class="problem-header"> | |
| <h2>${escapeHtml(problem.entry_point)}</h2> | |
| <span class="badge ${badgeClass(problem.source)}">${escapeHtml(problem.source)}</span> | |
| </div> | |
| <div class="problem-meta"> | |
| <div class="meta-item"> | |
| <span class="meta-label">Task ID:</span> | |
| <span class="meta-value">${escapeHtml(problem.task_id)}</span> | |
| </div> | |
| <div class="meta-item"> | |
| <span class="meta-label">Index:</span> | |
| <span class="meta-value">${problem.idx}</span> | |
| </div> | |
| <div class="meta-item"> | |
| <span class="meta-label">Dataset:</span> | |
| <span class="meta-value">${escapeHtml(datasetName)}</span> | |
| </div> | |
| <div class="meta-item"> | |
| <span class="meta-label">Test Inputs:</span> | |
| <span class="meta-value">${problem.inputs.length}</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <h2>Source Code</h2> | |
| <div class="coverage-legend" id="coverage-legend"> | |
| <strong>Coverage:</strong> | |
| <span class="coverage-legend-item"> | |
| <span class="coverage-swatch" style="background:#d4edda; border:1px solid #28a745;"></span> | |
| Executed | |
| </span> | |
| <span class="coverage-legend-item"> | |
| <span class="coverage-swatch" style="background:#f8d7da; border:1px solid #dc3545;"></span> | |
| Not executed | |
| </span> | |
| </div> | |
| <div class="code-with-tasks" id="code-container"> | |
| ${problem.highlighted_code} | |
| <svg id="arrow-overlay" xmlns="http://www.w3.org/2000/svg"> | |
| <defs> | |
| <marker id="arrowhead" markerWidth="8" markerHeight="6" | |
| refX="8" refY="3" orient="auto"> | |
| <polygon points="0 0, 8 3, 0 6" class="exec-arrow-head"/> | |
| </marker> | |
| </defs> | |
| </svg> | |
| </div> | |
| </div> | |
| `; | |
| // Task selector | |
| html += ` | |
| <div class="card"> | |
| <h2>Test Cases & Tasks</h2> | |
| <p>Select a test input to view associated reasoning tasks:</p> | |
| <div class="task-selector" id="task-selector"> | |
| `; | |
| problem.tasks.forEach((task, idx) => { | |
| html += ` | |
| <button class="task-btn ${idx === 0 ? 'active' : ''}" | |
| onclick="showTask(${idx})"> | |
| Input ${task.input_idx + 1} | |
| </button> | |
| `; | |
| }); | |
| html += ` | |
| </div> | |
| <div id="task-content"></div> | |
| </div> | |
| `; | |
| // Navigation hint | |
| html += ` | |
| <div class="navigation-hint"> | |
| <strong>Tip:</strong> Use the Previous/Next buttons at the top to browse through problems, | |
| or return to the list view to filter by dataset source or search by name. | |
| </div> | |
| `; | |
| container.innerHTML = html; | |
| // Store problem data globally | |
| window.currentProblem = problem; | |
| // Show first task by default | |
| showTask(0); | |
| } | |
| function injectTaskMarkers(taskItems) { | |
| const codePre = document.querySelector('.source .code pre'); | |
| // Save the pristine original innerHTML once, before any modification. | |
| if (codePre && !window._codePreOriginalHtml) { | |
| window._codePreOriginalHtml = codePre.innerHTML; | |
| } | |
| // Invalidate span cache (rebuilt lazily on next arrow draw) | |
| window._linenoSpanCache = null; | |
| // Store current task items so applyCoverage can re-add markers after wrapping. | |
| window._currentTaskItems = taskItems || []; | |
| // Reset code pre to original, then add markers from scratch. | |
| if (codePre && window._codePreOriginalHtml) { | |
| codePre.innerHTML = window._codePreOriginalHtml; | |
| } | |
| if (!taskItems || taskItems.length === 0) { | |
| return; | |
| } | |
| // Group tasks by line number | |
| const tasksByLine = {}; | |
| taskItems.forEach(item => { | |
| if (!tasksByLine[item.lineno]) tasksByLine[item.lineno] = []; | |
| tasksByLine[item.lineno].push(item.var); | |
| }); | |
| // Inject task marker badges into the code pre | |
| if (!codePre) return; | |
| const codeLines = codePre.innerHTML.split('\n'); | |
| codePre.innerHTML = codeLines.map((line, idx) => { | |
| const lineNum = idx + 1; | |
| if (tasksByLine[lineNum] && line.trim() !== '') { | |
| const vars = tasksByLine[lineNum]; | |
| return line + `<span class="task-marker" data-lineno="${lineNum}" data-vars="${escapeHtml(vars.join(', '))}">${escapeHtml(vars.join(', '))}</span>`; | |
| } | |
| return line; | |
| }).join('\n'); | |
| } | |
| function applyCoverage(coverageSet, totalLines) { | |
| // Remove previous coverage classes from lineno spans. | |
| document.querySelectorAll('td.linenos .normal').forEach(el => { | |
| el.classList.remove('line-executed', 'line-not-executed'); | |
| }); | |
| if (!coverageSet) { | |
| const legend = document.getElementById('coverage-legend'); | |
| if (legend) legend.style.display = 'none'; | |
| return; | |
| } | |
| const legend = document.getElementById('coverage-legend'); | |
| if (legend) legend.style.display = 'block'; | |
| // Color lineno spans only. | |
| document.querySelectorAll('td.linenos .normal').forEach(span => { | |
| const lineNum = parseInt(span.textContent.trim()); | |
| if (!isNaN(lineNum) && lineNum <= totalLines) { | |
| span.classList.add(coverageSet.has(lineNum) ? 'line-executed' : 'line-not-executed'); | |
| } | |
| }); | |
| } | |
| // Global map: lineno -> list of next line numbers (1-indexed; -1 = end of trace) | |
| window._nextLinesMap = {}; | |
| async function loadAndApplyGroundTruth(problemIdx, inputIdx, taskItems) { | |
| // Show "loading" placeholders on all task items | |
| taskItems.forEach(item => { | |
| const el = document.getElementById(`gt-${item.lineno}-${item.var}`); | |
| if (el) { el.textContent = '…'; el.className = 'gt-answer loading'; } | |
| }); | |
| // Clear next-lines data from previous input | |
| window._nextLinesMap = {}; | |
| try { | |
| const resp = await fetch(`/api/${datasetSlug}/problem/${problemIdx}/ground_truth/${inputIdx}`); | |
| const gt = await resp.json(); | |
| if (gt.status !== 'ok') { | |
| taskItems.forEach(item => { | |
| const el = document.getElementById(`gt-${item.lineno}-${item.var}`); | |
| if (el) { el.textContent = gt.status === 'error' ? '(exec error)' : '(unavailable)'; el.className = 'gt-answer'; } | |
| }); | |
| applyCoverage(null, 0); | |
| return; | |
| } | |
| // Apply coverage highlighting | |
| const coverageSet = new Set(gt.coverage); | |
| applyCoverage(coverageSet, gt.total_lines); | |
| // Fill in variable answers | |
| const answerMap = {}; | |
| gt.variable_answers.forEach(a => { | |
| answerMap[`${a.lineno}-${a.var}`] = a.answer_str; | |
| }); | |
| taskItems.forEach(item => { | |
| const el = document.getElementById(`gt-${item.lineno}-${item.var}`); | |
| if (el) { | |
| const answer = answerMap[`${item.lineno}-${item.var}`] || '(not available)'; | |
| el.textContent = answer; | |
| el.className = 'gt-answer'; | |
| } | |
| }); | |
| // Store next-lines data for arrow visualization | |
| if (gt.next_lines_answers) { | |
| gt.next_lines_answers.forEach(a => { | |
| window._nextLinesMap[a.lineno] = a.next_lines; | |
| }); | |
| } | |
| // Attach hover handlers to task-marker spans now that we have next-lines data | |
| attachArrowHoverHandlers(); | |
| } catch (e) { | |
| taskItems.forEach(item => { | |
| const el = document.getElementById(`gt-${item.lineno}-${item.var}`); | |
| if (el) { el.textContent = '(error)'; el.className = 'gt-answer'; } | |
| }); | |
| } | |
| } | |
| // Cache of lineNum → DOM span, rebuilt whenever injectTaskMarkers runs. | |
| window._linenoSpanCache = null; | |
| function buildLinenoSpanCache(container) { | |
| const cache = {}; | |
| container.querySelectorAll('td.linenos .normal').forEach(span => { | |
| const n = parseInt(span.textContent.trim()); | |
| if (!isNaN(n)) cache[n] = span; | |
| }); | |
| window._linenoSpanCache = cache; | |
| } | |
| /** | |
| * Get the bounding rect of the lineno span for a given 1-indexed line number, | |
| * relative to the code container element. Uses a cached span map. | |
| */ | |
| function getLinenoSpanRect(lineNum, container) { | |
| if (!window._linenoSpanCache) buildLinenoSpanCache(container); | |
| const span = window._linenoSpanCache[lineNum]; | |
| if (!span) return null; | |
| const spanRect = span.getBoundingClientRect(); | |
| const containerRect = container.getBoundingClientRect(); | |
| return { | |
| top: spanRect.top - containerRect.top + container.scrollTop, | |
| bottom: spanRect.bottom - containerRect.top + container.scrollTop, | |
| left: spanRect.left - containerRect.left, | |
| right: spanRect.right - containerRect.left, | |
| width: spanRect.width, | |
| height: spanRect.height, | |
| midY: (spanRect.top + spanRect.bottom) / 2 - containerRect.top + container.scrollTop, | |
| }; | |
| } | |
| /** | |
| * Draw arrows from sourceLine to each of the targetLines in the SVG overlay. | |
| * Lines are 1-indexed. -1 means "end of execution" (no arrow drawn). | |
| */ | |
| function drawArrows(sourceLineNum, targetLineNums) { | |
| const container = document.getElementById('code-container'); | |
| const svg = document.getElementById('arrow-overlay'); | |
| if (!container || !svg) return; | |
| // Remove previous arrows (but keep defs) | |
| svg.querySelectorAll('.arrow-path').forEach(el => el.remove()); | |
| const srcRect = getLinenoSpanRect(sourceLineNum, container); | |
| if (!srcRect) return; | |
| // Update SVG height to match container | |
| svg.setAttribute('height', container.scrollHeight); | |
| targetLineNums.forEach(targetLineNum => { | |
| if (targetLineNum === -1) return; // end of trace — no arrow | |
| const dstRect = getLinenoSpanRect(targetLineNum, container); | |
| if (!dstRect) return; | |
| // Start point: right edge of source lineno span, vertically centered | |
| const x1 = srcRect.right + 2; | |
| const y1 = srcRect.midY; | |
| // End point: right edge of target lineno span, vertically centered | |
| const x2 = dstRect.right + 2; | |
| const y2 = dstRect.midY; | |
| // Horizontal offset for the bezier control points — curves to the right | |
| const curveOffset = Math.max(30, Math.abs(y2 - y1) * 0.4); | |
| // Cubic bezier: both control points extend to the right of the lineno column | |
| const cx1 = x1 + curveOffset; | |
| const cy1 = y1; | |
| const cx2 = x2 + curveOffset; | |
| const cy2 = y2; | |
| const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); | |
| path.setAttribute('d', `M ${x1} ${y1} C ${cx1} ${cy1}, ${cx2} ${cy2}, ${x2} ${y2}`); | |
| path.setAttribute('class', 'exec-arrow arrow-path'); | |
| path.setAttribute('marker-end', 'url(#arrowhead)'); | |
| svg.appendChild(path); | |
| }); | |
| } | |
| /** | |
| * Clear all arrows from the SVG overlay. | |
| */ | |
| function clearArrows() { | |
| const svg = document.getElementById('arrow-overlay'); | |
| if (svg) { | |
| svg.querySelectorAll('.arrow-path').forEach(el => el.remove()); | |
| } | |
| } | |
| // AbortController for the current set of marker hover listeners. | |
| let _markerListenersAbort = null; | |
| /** | |
| * Attach mouseenter/mouseleave handlers to all .task-marker spans so that | |
| * hovering shows execution-flow arrows to next lines. | |
| */ | |
| function attachArrowHoverHandlers() { | |
| // Cancel any previously attached listeners without touching the DOM. | |
| if (_markerListenersAbort) _markerListenersAbort.abort(); | |
| _markerListenersAbort = new AbortController(); | |
| const { signal } = _markerListenersAbort; | |
| document.querySelectorAll('.task-marker').forEach(marker => { | |
| marker.addEventListener('mouseenter', () => { | |
| const lineNum = parseInt(marker.dataset.lineno); | |
| if (!lineNum) return; | |
| const nextLines = window._nextLinesMap[lineNum]; | |
| if (nextLines && nextLines.length > 0) { | |
| drawArrows(lineNum, nextLines); | |
| } | |
| }, { signal }); | |
| marker.addEventListener('mouseleave', () => { | |
| clearArrows(); | |
| }, { signal }); | |
| }); | |
| } | |
| function showCruxTask(taskIdx) { | |
| const problem = window.currentProblem; | |
| const task = problem.tasks[taskIdx]; | |
| // Update active button | |
| document.querySelectorAll('.task-btn').forEach((btn, idx) => { | |
| btn.classList.toggle('active', idx === taskIdx); | |
| }); | |
| const givenLabel = task.given === 'input' ? 'Input (given)' : 'Output (given)'; | |
| const predictLabel = task.predict === 'output' ? 'Output (predict)' : 'Input (predict)'; | |
| const givenValue = task.given === 'input' ? task.input : task.output; | |
| const predictValue = task.predict === 'output' ? task.output : task.input; | |
| const html = ` | |
| <div class="task-details"> | |
| <div class="task-section"> | |
| <p style="margin-bottom: 12px; color: #7f8c8d;">${escapeHtml(task.description)}</p> | |
| </div> | |
| <div class="io-section"> | |
| <div class="task-section"> | |
| <h3>${escapeHtml(givenLabel)}</h3> | |
| <pre class="code-block">${escapeHtml(givenValue)}</pre> | |
| </div> | |
| <div class="task-section"> | |
| <h3>${escapeHtml(predictLabel)}</h3> | |
| <pre class="code-block crux-answer">${escapeHtml(predictValue)}</pre> | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| document.getElementById('task-content').innerHTML = html; | |
| } | |
| function showTask(taskIdx) { | |
| const problem = window.currentProblem; | |
| const task = problem.tasks[taskIdx]; | |
| // Update active button | |
| const buttons = document.querySelectorAll('.task-btn'); | |
| buttons.forEach((btn, idx) => { | |
| if (idx === taskIdx) { | |
| btn.classList.add('active'); | |
| } else { | |
| btn.classList.remove('active'); | |
| } | |
| }); | |
| // Inject task markers into the code | |
| injectTaskMarkers(task.task_items); | |
| // Clear previous coverage while new one loads | |
| applyCoverage(null, 0); | |
| // Render task content | |
| const ioSection = task.test_class_code | |
| ? `<div class="io-section"> | |
| <div class="task-section"> | |
| <h3>Input</h3> | |
| <pre class="code-block">${escapeHtml(task.input)}</pre> | |
| </div> | |
| </div> | |
| <div class="task-section"> | |
| <h3>Test Class — <code>${escapeHtml(task.test_class_name)}</code></h3> | |
| <pre class="code-block">${escapeHtml(task.test_class_code)}</pre> | |
| </div>` | |
| : `<div class="io-section"> | |
| <div class="task-section"> | |
| <h3>Input</h3> | |
| <pre class="code-block">${escapeHtml(task.input)}</pre> | |
| </div> | |
| <div class="task-section"> | |
| <h3>Expected Output</h3> | |
| <pre class="code-block">${escapeHtml(task.output)}</pre> | |
| </div> | |
| </div>`; | |
| let html = ` | |
| <div class="task-details"> | |
| ${ioSection} | |
| `; | |
| // Show task items with ground truth answer placeholders | |
| if (task.task_items && task.task_items.length > 0) { | |
| html += ` | |
| <div class="task-section"> | |
| <h3>Reasoning Tasks</h3> | |
| <p style="margin-bottom: 10px; color: #7f8c8d;"> | |
| Variable state at each execution point (correct answer shown in | |
| <span style="background:#17a2b8;color:white;padding:1px 6px;border-radius:3px;font-size:0.82rem;">teal</span>): | |
| </p> | |
| <ul class="task-items-list"> | |
| `; | |
| task.task_items.forEach(item => { | |
| html += ` | |
| <li> | |
| <span class="line-ref">Line ${item.lineno}</span> | |
| <span class="var-name">${escapeHtml(item.var)}</span> | |
| <span class="gt-answer loading" id="gt-${item.lineno}-${item.var}">…</span> | |
| </li> | |
| `; | |
| }); | |
| html += ` | |
| </ul> | |
| </div> | |
| `; | |
| } | |
| // Show output prediction task if exists | |
| if (task.output_pred) { | |
| html += ` | |
| <div class="task-section"> | |
| <h3>Output Completion Task</h3> | |
| <p style="margin-bottom: 10px; color: #7f8c8d;"> | |
| The model needs to complete this test assertion: | |
| </p> | |
| <pre class="code-block">${escapeHtml(task.output_pred)}</pre> | |
| </div> | |
| `; | |
| } | |
| html += `</div>`; | |
| document.getElementById('task-content').innerHTML = html; | |
| // Fetch and apply ground truth (coverage + variable answers) | |
| if (hasGroundTruth && task.task_items) { | |
| loadAndApplyGroundTruth(problem.idx, task.input_idx, task.task_items); | |
| } | |
| } | |
| function showLangTab(idx) { | |
| // Only toggle tabs within the #lang-tabs container (not code-mode tabs) | |
| const langTabsContainer = document.getElementById('lang-tabs'); | |
| if (langTabsContainer) { | |
| langTabsContainer.querySelectorAll('.lang-tab').forEach((tab, i) => { | |
| tab.classList.toggle('active', i === idx); | |
| }); | |
| } | |
| document.querySelectorAll('.lang-code-panel').forEach((panel, i) => { | |
| panel.classList.toggle('active', i === idx); | |
| }); | |
| // Re-apply the current code mode (canonical/buggy) to the newly visible panel | |
| if (window._currentCodeMode) { | |
| const problem = window.currentProblem; | |
| if (problem && problem.lang_solutions) { | |
| const sol = problem.lang_solutions[idx]; | |
| if (sol) { | |
| const canonical = document.getElementById('lang-code-canonical-' + idx); | |
| const buggy = document.getElementById('lang-code-buggy-' + idx); | |
| if (canonical) canonical.style.display = window._currentCodeMode === 'canonical' ? '' : 'none'; | |
| if (buggy) buggy.style.display = window._currentCodeMode === 'buggy' ? '' : 'none'; | |
| // If buggy mode is selected but this language has no buggy code, fall back to canonical | |
| if (window._currentCodeMode === 'buggy' && !buggy && canonical) { | |
| canonical.style.display = ''; | |
| } | |
| } | |
| } | |
| } | |
| // Update test section | |
| const problem = window.currentProblem; | |
| if (problem && problem.lang_solutions) { | |
| const sol = problem.lang_solutions[idx]; | |
| const testContainer = document.getElementById('lang-test-container'); | |
| if (testContainer && sol && sol.test) { | |
| testContainer.innerHTML = `<h2>Test Suite</h2><pre class="code-block">${escapeHtml(sol.test)}</pre>`; | |
| } else if (testContainer) { | |
| testContainer.innerHTML = ''; | |
| } | |
| } | |
| } | |
| function toggleCodeMode(mode) { | |
| window._currentCodeMode = mode; | |
| const problem = window.currentProblem; | |
| if (!problem || !problem.lang_solutions) return; | |
| // Update mode tabs | |
| const modeTabs = document.querySelectorAll('#code-mode-tabs .lang-tab'); | |
| modeTabs.forEach(tab => { | |
| tab.classList.toggle('active', tab.textContent.trim().toLowerCase() === mode); | |
| }); | |
| // Toggle visibility of canonical/buggy code in all panels | |
| problem.lang_solutions.forEach((sol, i) => { | |
| const canonical = document.getElementById('lang-code-canonical-' + i); | |
| const buggy = document.getElementById('lang-code-buggy-' + i); | |
| if (canonical) canonical.style.display = mode === 'canonical' ? '' : 'none'; | |
| if (buggy) buggy.style.display = mode === 'buggy' ? '' : 'none'; | |
| }); | |
| } | |
| function showFimTab(idx) { | |
| const tabs = document.querySelectorAll('#fim-tabs .lang-tab'); | |
| tabs.forEach((tab, i) => tab.classList.toggle('active', i === idx)); | |
| for (let i = 0; i < 3; i++) { | |
| const panel = document.getElementById('fim-panel-' + i); | |
| if (panel) panel.classList.toggle('active', i === idx); | |
| } | |
| } | |
| /** | |
| * Split a unified diff into per-file sections and render each with a GitHub-style | |
| * file header bar. Returns an HTML string with one card per file. | |
| */ | |
| function renderDiffFiles(diffText, title) { | |
| if (!diffText) return ''; | |
| // Split into per-file chunks by "diff --git" boundaries | |
| const files = []; | |
| let current = null; | |
| diffText.split('\n').forEach(line => { | |
| if (line.startsWith('diff --git')) { | |
| if (current) files.push(current); | |
| // Extract file path from "diff --git a/path b/path" | |
| const m = line.match(/^diff --git a\/(.+?) b\/(.+)/); | |
| const filePath = m ? m[2] : line; | |
| current = { path: filePath, lines: [line] }; | |
| } else if (current) { | |
| current.lines.push(line); | |
| } else { | |
| // Lines before any diff header — create a default section | |
| if (!current) current = { path: '', lines: [] }; | |
| current.lines.push(line); | |
| } | |
| }); | |
| if (current) files.push(current); | |
| if (files.length === 0) return ''; | |
| let html = ''; | |
| if (files.length === 1 && !files[0].path) { | |
| // Single unnamed diff — render as before | |
| html += `<div class="card"><h2>${escapeHtml(title)}</h2><div class="diff-view">${renderDiff(diffText)}</div></div>`; | |
| } else { | |
| html += `<div class="card"><h2>${escapeHtml(title)}</h2>`; | |
| files.forEach(file => { | |
| const diffChunk = file.lines.join('\n'); | |
| // Count additions/deletions | |
| let adds = 0, dels = 0; | |
| file.lines.forEach(l => { | |
| if (l.startsWith('+') && !l.startsWith('+++')) adds++; | |
| if (l.startsWith('-') && !l.startsWith('---')) dels++; | |
| }); | |
| const statsHtml = `<span class="diff-file-stats"><span class="diff-stat-add">+${adds}</span> <span class="diff-stat-del">-${dels}</span></span>`; | |
| html += ` | |
| <div class="diff-file-section"> | |
| <div class="diff-file-header"> | |
| <span class="diff-file-path">${escapeHtml(file.path)}</span> | |
| ${statsHtml} | |
| </div> | |
| <div class="diff-view">${renderDiff(diffChunk)}</div> | |
| </div> | |
| `; | |
| }); | |
| html += `</div>`; | |
| } | |
| return html; | |
| } | |
| /** | |
| * Render a unified diff with line numbers and file headers (GitHub-style). | |
| */ | |
| function renderDiff(diffText) { | |
| if (!diffText) return ''; | |
| const lines = diffText.split('\n'); | |
| let oldLine = 0, newLine = 0; | |
| const rows = []; | |
| lines.forEach(line => { | |
| if (line.startsWith('diff ')) { | |
| rows.push(`<tr class="diff-tr-header"><td class="diff-ln"></td><td class="diff-ln"></td><td class="diff-td-header">${escapeHtml(line)}</td></tr>`); | |
| return; | |
| } | |
| if (line.startsWith('---') || line.startsWith('+++')) { | |
| rows.push(`<tr class="diff-tr-header"><td class="diff-ln"></td><td class="diff-ln"></td><td class="diff-td-header">${escapeHtml(line)}</td></tr>`); | |
| return; | |
| } | |
| if (line.startsWith('@@')) { | |
| // Parse hunk header: @@ -oldStart,oldCount +newStart,newCount @@ | |
| const m = line.match(/@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/); | |
| if (m) { | |
| oldLine = parseInt(m[1]); | |
| newLine = parseInt(m[2]); | |
| } | |
| rows.push(`<tr class="diff-tr-hunk"><td class="diff-ln"></td><td class="diff-ln"></td><td class="diff-td-hunk">${escapeHtml(line)}</td></tr>`); | |
| return; | |
| } | |
| if (line.startsWith('+')) { | |
| rows.push(`<tr class="diff-tr-add"><td class="diff-ln"></td><td class="diff-ln">${newLine}</td><td class="diff-td-add">${escapeHtml(line.substring(1))}</td></tr>`); | |
| newLine++; | |
| } else if (line.startsWith('-')) { | |
| rows.push(`<tr class="diff-tr-del"><td class="diff-ln">${oldLine}</td><td class="diff-ln"></td><td class="diff-td-del">${escapeHtml(line.substring(1))}</td></tr>`); | |
| oldLine++; | |
| } else if (line.startsWith(' ')) { | |
| rows.push(`<tr class="diff-tr-ctx"><td class="diff-ln">${oldLine}</td><td class="diff-ln">${newLine}</td><td class="diff-td-ctx">${escapeHtml(line.substring(1))}</td></tr>`); | |
| oldLine++; | |
| newLine++; | |
| } else if (line.trim() === '') { | |
| // Empty trailing line | |
| } else { | |
| rows.push(`<tr class="diff-tr-ctx"><td class="diff-ln">${oldLine}</td><td class="diff-ln">${newLine}</td><td class="diff-td-ctx">${escapeHtml(line)}</td></tr>`); | |
| oldLine++; | |
| newLine++; | |
| } | |
| }); | |
| return `<table class="diff-table">${rows.join('')}</table>`; | |
| } | |
| /** | |
| * Simple line-by-line diff (LCS-based) between two code strings. | |
| * Returns an array of {type: 'context'|'add'|'del', line: string}. | |
| */ | |
| function computeUnifiedDiff(oldText, newText) { | |
| const oldLines = (oldText || '').split('\n'); | |
| const newLines = (newText || '').split('\n'); | |
| // LCS for line sequences | |
| const m = oldLines.length, n = newLines.length; | |
| // For very large files, just show both in full instead of computing LCS | |
| if (m * n > 500000) { | |
| const result = []; | |
| oldLines.forEach(l => result.push({type: 'del', line: l})); | |
| newLines.forEach(l => result.push({type: 'add', line: l})); | |
| return result; | |
| } | |
| const dp = Array.from({length: m + 1}, () => new Uint16Array(n + 1)); | |
| for (let i = 1; i <= m; i++) { | |
| for (let j = 1; j <= n; j++) { | |
| if (oldLines[i - 1] === newLines[j - 1]) { | |
| dp[i][j] = dp[i - 1][j - 1] + 1; | |
| } else { | |
| dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); | |
| } | |
| } | |
| } | |
| // Backtrack to build diff | |
| const result = []; | |
| let i = m, j = n; | |
| while (i > 0 || j > 0) { | |
| if (i > 0 && j > 0 && oldLines[i - 1] === newLines[j - 1]) { | |
| result.push({type: 'context', line: oldLines[i - 1]}); | |
| i--; j--; | |
| } else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) { | |
| result.push({type: 'add', line: newLines[j - 1]}); | |
| j--; | |
| } else { | |
| result.push({type: 'del', line: oldLines[i - 1]}); | |
| i--; | |
| } | |
| } | |
| result.reverse(); | |
| // Compact: only show hunks with context (3 lines around changes) | |
| const contextSize = 3; | |
| const hasChange = result.map(r => r.type !== 'context'); | |
| const show = new Uint8Array(result.length); | |
| for (let k = 0; k < result.length; k++) { | |
| if (hasChange[k]) { | |
| for (let c = Math.max(0, k - contextSize); c <= Math.min(result.length - 1, k + contextSize); c++) { | |
| show[c] = 1; | |
| } | |
| } | |
| } | |
| const compacted = []; | |
| let lastShown = -1; | |
| for (let k = 0; k < result.length; k++) { | |
| if (show[k]) { | |
| if (lastShown >= 0 && k - lastShown > 1) { | |
| compacted.push({type: 'separator', line: '...'}); | |
| } | |
| compacted.push(result[k]); | |
| lastShown = k; | |
| } | |
| } | |
| return compacted.length > 0 ? compacted : result; | |
| } | |
| /** | |
| * Render the output of computeUnifiedDiff into diff HTML with line numbers. | |
| */ | |
| function renderComputedDiff(diffEntries) { | |
| let oldLine = 1, newLine = 1; | |
| const rows = diffEntries.map(entry => { | |
| if (entry.type === 'separator') { | |
| return `<tr class="diff-tr-hunk"><td class="diff-ln"></td><td class="diff-ln"></td><td class="diff-td-hunk">${escapeHtml(entry.line)}</td></tr>`; | |
| } | |
| if (entry.type === 'del') { | |
| const row = `<tr class="diff-tr-del"><td class="diff-ln">${oldLine}</td><td class="diff-ln"></td><td class="diff-td-del">${escapeHtml(entry.line)}</td></tr>`; | |
| oldLine++; | |
| return row; | |
| } | |
| if (entry.type === 'add') { | |
| const row = `<tr class="diff-tr-add"><td class="diff-ln"></td><td class="diff-ln">${newLine}</td><td class="diff-td-add">${escapeHtml(entry.line)}</td></tr>`; | |
| newLine++; | |
| return row; | |
| } | |
| // context | |
| const row = `<tr class="diff-tr-ctx"><td class="diff-ln">${oldLine}</td><td class="diff-ln">${newLine}</td><td class="diff-td-ctx">${escapeHtml(entry.line)}</td></tr>`; | |
| oldLine++; | |
| newLine++; | |
| return row; | |
| }); | |
| return `<table class="diff-table">${rows.join('')}</table>`; | |
| } | |
| function escapeHtml(text) { | |
| if (text === null || text === undefined) return ''; | |
| const div = document.createElement('div'); | |
| div.textContent = text; | |
| return div.innerHTML; | |
| } | |
| loadProblem(); | |