<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>mathr notebook</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/marked@12.0.0/marked.min.js"></script>
<style>
:root {
--bg: #1e1e2e;
--surface: #313244;
--surface2: #45475a;
--text: #cdd6f4;
--text-dim: #a6adc8;
--accent: #89b4fa;
--accent2: #f9e2af;
--green: #a6e3a1;
--red: #f38ba8;
--border: #585b70;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, monospace;
background: var(--bg);
color: var(--text);
min-height: 100vh;
}
header {
background: var(--surface);
padding: 12px 24px;
display: flex;
align-items: center;
gap: 16px;
border-bottom: 2px solid var(--border);
position: sticky;
top: 0;
z-index: 100;
}
header h1 {
font-size: 1.3rem;
color: var(--accent);
font-weight: 600;
}
header .spacer { flex: 1; }
button {
background: var(--surface2);
color: var(--text);
border: 1px solid var(--border);
padding: 6px 14px;
border-radius: 6px;
cursor: pointer;
font-size: 0.85rem;
transition: background 0.15s;
}
button:hover { background: var(--accent); color: var(--bg); }
button.danger:hover { background: var(--red); color: var(--bg); }
button.success { background: var(--green); color: var(--bg); }
button.success:hover { opacity: 0.85; }
#notebook {
max-width: 900px;
margin: 24px auto;
padding: 0 16px;
}
.cell {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
margin-bottom: 12px;
overflow: hidden;
}
.cell-header {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
background: var(--surface2);
font-size: 0.75rem;
color: var(--text-dim);
}
.cell-header .cell-id {
font-weight: 600;
color: var(--accent2);
min-width: 40px;
}
.cell-header .cell-type-badge {
font-size: 0.65rem;
padding: 1px 6px;
border-radius: 4px;
background: var(--accent);
color: var(--bg);
font-weight: 600;
text-transform: uppercase;
cursor: pointer;
}
.cell-header .cell-type-badge.text {
background: var(--accent2);
}
.cell-header .cell-type-badge.mathml {
background: #6b5b95;
}
.cell-header .spacer { flex: 1; }
.cell-header button {
padding: 3px 10px;
font-size: 0.75rem;
}
.cell-header .move-btns {
display: flex;
gap: 2px;
}
.cell-header .move-btns button {
padding: 3px 7px;
font-size: 0.7rem;
}
.cell-status {
font-size: 0.65rem;
padding: 1px 6px;
border-radius: 4px;
margin-left: 4px;
}
.cell-status.idle { color: var(--text-dim); }
.cell-status.running { color: var(--accent2); }
.cell-status.done { color: var(--green); }
.cell-status.error { color: var(--red); }
.cell-input {
padding: 12px 16px;
}
.cell-input textarea {
width: 100%;
min-height: 40px;
background: var(--bg);
color: var(--text);
border: 1px solid var(--border);
border-radius: 6px;
padding: 8px 12px;
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
font-size: 0.9rem;
resize: vertical;
outline: none;
transition: border-color 0.15s;
}
.cell-input textarea:focus { border-color: var(--accent); }
.cell-output {
padding: 8px 16px 12px;
border-top: 1px solid var(--border);
min-height: 32px;
}
.cell-input-preview {
padding: 8px 4px 0;
font-size: 1rem;
color: var(--accent2);
overflow-x: auto;
}
.cell-input-preview .preview-label {
font-size: 0.7rem;
color: var(--text-dim);
margin-bottom: 4px;
}
.cell-output .output-label {
font-size: 0.7rem;
color: var(--text-dim);
margin-bottom: 4px;
}
.cell-output .output-rendered {
font-size: 1.05rem;
color: var(--green);
overflow-x: auto;
padding: 2px 0;
}
.cell-output .step-text {
color: var(--text-dim);
font-size: 0.9rem;
margin-right: 4px;
}
.cell-output .output-raw {
font-family: 'SF Mono', 'Fira Code', monospace;
font-size: 0.8rem;
color: var(--text-dim);
white-space: pre-wrap;
word-break: break-word;
margin-top: 4px;
}
.cell-output .output-error {
color: var(--red);
}
.cell-output .output-placeholder {
color: var(--text-dim);
font-style: italic;
font-size: 0.8rem;
}
.cell.text-cell .cell-input textarea {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 0.95rem;
min-height: 60px;
}
.cell.text-cell .cell-output {
display: none;
}
.cell.text-cell .cell-input-preview {
display: none;
}
.context-panel {
max-width: 900px;
margin: 0 auto 12px;
padding: 0 16px;
}
.context-panel summary {
cursor: pointer;
font-size: 0.8rem;
color: var(--text-dim);
padding: 6px 0;
}
.context-panel .context-content {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 6px;
padding: 10px 14px;
font-size: 0.8rem;
margin-top: 6px;
}
.context-panel .context-vars {
color: var(--green);
}
.context-panel .context-funcs {
color: var(--accent2);
margin-top: 6px;
}
.cell-output .output-image {
margin: 8px 0;
text-align: center;
}
.cell-output .output-image img {
max-width: 100%;
border-radius: 6px;
border: 1px solid var(--border);
}
.cell-output .output-mathml {
margin-top: 8px;
padding: 6px 8px;
border-radius: 6px;
background: var(--bg-alt);
border: 1px solid var(--border);
}
.cell.text-cell .cell-input .markdown-preview {
padding: 8px 4px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 0.95rem;
line-height: 1.5;
color: var(--text);
overflow-x: auto;
}
.cell.text-cell .markdown-preview h1,
.cell.text-cell .markdown-preview h2,
.cell.text-cell .markdown-preview h3 {
color: var(--accent);
margin: 8px 0 4px;
}
.cell.text-cell .markdown-preview h1 { font-size: 1.3rem; }
.cell.text-cell .markdown-preview h2 { font-size: 1.15rem; }
.cell.text-cell .markdown-preview h3 { font-size: 1.0rem; }
.cell.text-cell .markdown-preview p { margin: 4px 0; }
.cell.text-cell .markdown-preview code {
background: var(--bg);
padding: 1px 4px;
border-radius: 3px;
font-family: 'SF Mono', 'Fira Code', monospace;
font-size: 0.85rem;
}
.cell.text-cell .markdown-preview pre {
background: var(--bg);
padding: 8px 12px;
border-radius: 6px;
overflow-x: auto;
margin: 6px 0;
}
.cell.text-cell .markdown-preview ul,
.cell.text-cell .markdown-preview ol {
margin: 4px 0 4px 20px;
}
.cell.text-cell .markdown-preview blockquote {
border-left: 3px solid var(--accent);
margin: 6px 0;
padding-left: 12px;
color: var(--text-dim);
}
.cell-exec-counter {
font-size: 0.65rem;
color: var(--text-dim);
font-family: 'SF Mono', monospace;
min-width: 50px;
}
.add-cell-bar {
text-align: center;
padding: 16px;
}
.add-cell-bar button {
background: var(--accent);
color: var(--bg);
font-weight: 600;
padding: 8px 24px;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: var(--text-dim);
}
.empty-state h2 { font-size: 1.1rem; margin-bottom: 8px; }
.empty-state p { font-size: 0.85rem; }
.katex { font-size: 1.05em; }
.katex-display { margin: 0.5em 0; }
</style>
</head>
<body>
<header>
<h1>mathr notebook</h1>
<span id="filename" style="color:var(--text-dim);font-size:0.8rem;"></span>
<span class="spacer"></span>
<button onclick="resetAndRunAll()" class="success">Reset & Run All</button>
<button onclick="runAll()">Run All</button>
<button onclick="resetContext()">Reset Context</button>
<button onclick="saveNotebook()">Save</button>
<button onclick="addCell('math')">+ Math</button>
<button onclick="addCell('text')">+ Text</button>
<button onclick="addCell('mathml')">+ MathML</button>
<button onclick="toggleMathML()" id="mathml-toggle">Show MathML</button>
</header>
<div class="context-panel">
<details>
<summary id="context-summary">Context: 0 variables, 0 functions</summary>
<div class="context-content" id="context-content">
<div class="context-vars" id="context-vars">No variables bound.</div>
<div class="context-funcs" id="context-funcs">No user functions.</div>
</div>
</details>
</div>
<div id="notebook">
<div id="cells"></div>
<div class="add-cell-bar">
<button onclick="addCell('math')">+ Add Math Cell</button>
<button onclick="addCell('text')">+ Add Text Cell</button>
<button onclick="addCell('mathml')">+ Add MathML Cell</button>
</div>
</div>
<script>
let notebook = { cells: [] };
let nextId = 0;
let execCounter = 0;
let showMathML = false;
function toggleMathML() {
showMathML = !showMathML;
document.getElementById('mathml-toggle').textContent = showMathML ? 'Hide MathML' : 'Show MathML';
renderAll();
}
function decimalToFraction(str) {
const n = parseFloat(str);
if (isNaN(n) || !isFinite(n)) return null;
if (Number.isInteger(n)) return null;
const dotIdx = str.indexOf('.');
if (dotIdx < 0) return null;
const decPlaces = str.length - dotIdx - 1;
if (decPlaces > 10) return null;
const denom = Math.pow(10, decPlaces);
let numer = Math.round(n * denom);
function gcd(a, b) { return b === 0 ? a : gcd(b, a % b); }
const g = gcd(Math.abs(numer), denom);
numer = numer / g;
const d = denom / g;
if (d > 1000) return null;
if (Math.abs(numer / d - n) > 1e-9) return null;
if (d === 1) return String(numer);
return '\\frac{' + numer + '}{' + d + '}';
}
function mathToLatex(expr) {
let s = expr.trim();
if (!s) return '';
s = s.replace(/^\$+/, '').replace(/\$+$/, '');
if (s.startsWith('\\')) return s;
const commands = [
'diff', 'int', 'integrate', 'solve', 'simplify', 'plot',
'taylor', 'laurent', 'rat', 'fourier', 'mc', 'sample',
'dist', 'fft', 'conv', 'stats', 'poly-roots', 'isolate-roots',
'pdiff', 'gradient', 'romberg', 'let', 'fn',
'gcd', 'lcm', 'is-prime', 'factor', 'fib', 'binom', 'fact',
'mr-prime', 'jacobi', 'cf', 'diophantine', 'dlog',
'lu', 'cholesky', 'svd', 'eig', 'symlig', 'hessenberg',
'schur', 'rank', 'tikhonov', 'spline', 'chebyshev', 'legendre',
'det', 'fast',
];
const cmdMatch = s.match(/^([\w-]+)\s+/);
let prefix = '';
if (cmdMatch && commands.includes(cmdMatch[1])) {
prefix = '\\text{' + cmdMatch[1] + '}\\;';
s = s.slice(cmdMatch[1].length).trim();
}
s = s.replace(/\s*\+\s*-/g, ' - ');
s = s.replace(/(?:^|[^a-zA-Z_])(\d+\.\d+)(?![a-zA-Z_])/g, function(match, num, offset) {
const frac = decimalToFraction(num);
if (frac) {
const prefix = match.slice(0, match.length - num.length);
return prefix + frac;
}
return match;
});
const repls = [
[/\bpi\b/g, '\\pi'],
[/\binf\b/g, '\\infty'],
[/\binfinity\b/g, '\\infty'],
[/\btau\b/g, '\\tau'],
[/\bsqrt\s*\(([^)]+)\)/g, '\\sqrt{$1}'],
[/\bsin\s*\(/g, '\\sin('],
[/\bcos\s*\(/g, '\\cos('],
[/\btan\s*\(/g, '\\tan('],
[/\basin\s*\(/g, '\\arcsin('],
[/\bacos\s*\(/g, '\\arccos('],
[/\batan\s*\(/g, '\\arctan('],
[/\bsinh\s*\(/g, '\\sinh('],
[/\bcosh\s*\(/g, '\\cosh('],
[/\btanh\s*\(/g, '\\tanh('],
[/\bln\s*\(/g, '\\ln('],
[/\blog\s*\(/g, '\\log('],
[/\bexp\s*\(/g, '\\exp('],
[/\bgamma\s*\(/g, '\\Gamma('],
[/\bGamma\s*\(/g, '\\Gamma('],
[/\berf\s*\(/g, '\\operatorname{erf}('],
[/\berfc\s*\(/g, '\\operatorname{erfc}('],
];
for (const [p, r] of repls) s = s.replace(p, r);
s = s.replace(/\^(\w+)/g, '^{$1}');
s = s.replace(/\^\(([^)]+)\)/g, '^{$1}');
s = s.replace(/\s*\*\s*/g, ' \\cdot ');
s = s.replace(/(\d+)\/(\d+)/g, '\\frac{$1}{$2}');
s = s.replace(/([a-zA-Z]\w*)\/([a-zA-Z]\w*)/g, '\\frac{$1}{$2}');
return prefix + s;
}
function renderKatex(latex, element, displayMode) {
if (typeof katex === 'undefined') {
element.textContent = latex;
return;
}
try {
katex.render(latex, element, {
displayMode: displayMode !== false,
throwOnError: false,
strict: false
});
} catch(e) {
element.textContent = latex;
}
}
function init() {
fetch('/api/notebook')
.then(r => r.json())
.then(data => {
notebook = data;
nextId = data.cells.length;
notebook.cells.forEach(c => { if (!c.cell_type) c.cell_type = 'math'; });
if (data.cells.length === 0) { addCell('math'); }
else { renderAll(); }
})
.catch(() => { addCell('math'); });
updateContextPanel();
}
function renderAll() {
const container = document.getElementById('cells');
container.innerHTML = '';
notebook.cells.forEach((cell, i) => {
container.appendChild(createCellElement(cell, i));
});
notebook.cells.forEach((cell, i) => {
if (cell.cell_type === 'text') updateMarkdownPreview(i);
else if (cell.cell_type === 'mathml') updateMathMLPreview(i);
else updateInputPreview(i);
});
}
function createCellElement(cell, index) {
const div = document.createElement('div');
const isText = (cell.cell_type === 'text');
const isMathML = (cell.cell_type === 'mathml');
const isEval = !isText; div.className = 'cell' + (isText ? ' text-cell' : '');
div.dataset.index = index;
const header = document.createElement('div');
header.className = 'cell-header';
const typeLabel = isText ? 'TEXT' : (isMathML ? 'MATHML' : 'MATH');
const typeClass = isText ? 'text' : (isMathML ? 'mathml' : '');
const execNum = cell.exec_num ? `In [${cell.exec_num}]` : '';
header.innerHTML = `
<span class="cell-id">[${cell.id}]</span>
<span class="cell-exec-counter" id="exec-${index}">${execNum}</span>
<span class="cell-type-badge ${typeClass}" onclick="toggleCellType(${index})" title="Click to toggle cell type">${typeLabel}</span>
<span class="cell-status idle" id="status-${index}"></span>
<span class="spacer"></span>
<span class="move-btns">
<button onclick="moveCellUp(${index})" title="Move up">↑</button>
<button onclick="moveCellDown(${index})" title="Move down">↓</button>
</span>
<button onclick="duplicateCell(${index})" title="Duplicate">Dup</button>
${isText ? '' : `<button onclick="runCell(${index})">Run</button>`}
<button onclick="deleteCell(${index})" class="danger">Del</button>
`;
div.appendChild(header);
const inputDiv = document.createElement('div');
inputDiv.className = 'cell-input';
const textarea = document.createElement('textarea');
textarea.placeholder = isText
? 'Enter text/markdown notes...'
: isMathML
? 'Enter Presentation MathML (e.g. <mfrac><mn>1</mn><mn>2</mn></mfrac>)'
: 'Enter math (e.g. sin(pi/4), \\\\frac{1}{2}+1, diff x^3, let x = 5, plot sin(x) 0 6.28)';
textarea.value = cell.input;
textarea.onkeydown = (e) => cellKeydown(e, index);
textarea.oninput = () => {
notebook.cells[index].input = textarea.value;
if (isText) updateMarkdownPreview(index);
else if (isMathML) updateMathMLPreview(index);
else updateInputPreview(index);
};
inputDiv.appendChild(textarea);
if (isText) {
const previewDiv = document.createElement('div');
previewDiv.className = 'markdown-preview';
previewDiv.id = `md-preview-${index}`;
inputDiv.appendChild(previewDiv);
} else if (isMathML) {
const previewDiv = document.createElement('div');
previewDiv.className = 'cell-input-preview';
previewDiv.innerHTML = '<span class="preview-label">Rendered MathML:</span><div class="preview-math"></div>';
inputDiv.appendChild(previewDiv);
} else {
const previewDiv = document.createElement('div');
previewDiv.className = 'cell-input-preview';
previewDiv.innerHTML = '<span class="preview-label">Rendered:</span><div class="preview-math"></div>';
inputDiv.appendChild(previewDiv);
}
div.appendChild(inputDiv);
if (isEval) {
const outputDiv = document.createElement('div');
outputDiv.className = 'cell-output';
outputDiv.id = `output-${index}`;
if (cell.output) {
renderOutputInto(cell.output, outputDiv);
} else {
outputDiv.innerHTML = '<span class="output-placeholder">Not yet evaluated</span>';
}
div.appendChild(outputDiv);
}
return div;
}
function updateInputPreview(index) {
const cellDiv = document.querySelector(`[data-index="${index}"]`);
if (!cellDiv) return;
const textarea = cellDiv.querySelector('textarea');
const previewMath = cellDiv.querySelector('.preview-math');
if (!previewMath) return;
const input = textarea.value.trim();
if (!input) { previewMath.innerHTML = ''; return; }
const latex = mathToLatex(input);
previewMath.innerHTML = '';
renderKatex(latex, previewMath, true);
notebook.cells[index].input = textarea.value;
}
function updateMathMLPreview(index) {
const cellDiv = document.querySelector(`[data-index="${index}"]`);
if (!cellDiv) return;
const textarea = cellDiv.querySelector('textarea');
const previewMath = cellDiv.querySelector('.preview-math');
if (!previewMath) return;
const input = textarea.value.trim();
if (!input) { previewMath.innerHTML = ''; return; }
const wrapped = input.startsWith('<math') ? input : `<math xmlns="http://www.w3.org/1998/Math/MathML">${input}</math>`;
previewMath.innerHTML = wrapped;
notebook.cells[index].input = textarea.value;
}
function updateMarkdownPreview(index) {
const previewDiv = document.getElementById(`md-preview-${index}`);
if (!previewDiv) return;
const cellDiv = document.querySelector(`[data-index="${index}"]`);
if (!cellDiv) return;
const textarea = cellDiv.querySelector('textarea');
const input = textarea.value.trim();
if (!input) { previewDiv.innerHTML = ''; return; }
if (typeof marked !== 'undefined') {
previewDiv.innerHTML = marked.parse(input);
} else {
previewDiv.textContent = input;
}
}
function renderStep(step) {
const labelPatterns = [
/^(Taylor expansion around .*)$/i,
/^(Laurent expansion around .*)$/i,
/^(order\s*=\s*\d+)$/i,
/^(pole order\s*=.*)$/i,
/^(method:.*)$/i,
/^(initial guess\s*=.*)$/i,
];
for (const pat of labelPatterns) {
if (pat.test(step)) {
return {type: 'text', text: step, math: ''};
}
}
const colonMatch = step.match(/^(\w[\w\s]*):\s*(.+)$/);
if (colonMatch) {
const label = colonMatch[1];
const math = colonMatch[2];
const mathWords = new Set(['sin','cos','tan','exp','sqrt','log','ln','f']);
const labelWords = label.split(/\s+/).filter(w => w.length >= 4 && !mathWords.has(w.toLowerCase()));
if (labelWords.length > 0) {
return {type: 'text', text: label + ': ', math: mathToLatex(math)};
}
}
const eqMatch = step.match(/^(\w[\w\s]*)\s*=\s*(.+)$/);
if (eqMatch) {
const label = eqMatch[1].trim();
const math = eqMatch[2];
const mathWords = new Set(['sin','cos','tan','exp','sqrt','log','ln','f','x','y','z']);
const labelWords = label.split(/\s+/).filter(w => w.length >= 4 && !mathWords.has(w.toLowerCase()));
if (labelWords.length > 0 && label.toLowerCase() !== 'f') {
return {type: 'text', text: label + ' = ', math: mathToLatex(math)};
}
}
return {type: 'math', math: mathToLatex(step), text: ''};
}
function renderOutputInto(output, outputDiv) {
renderOutputWithSteps([output], outputDiv);
}
function renderOutputWithSteps(steps, outputDiv) {
if (!steps || steps.length === 0 || (steps.length === 1 && !steps[0].trim())) {
outputDiv.innerHTML = '<span class="output-placeholder">Empty result</span>';
return;
}
if (steps[0].startsWith('Error:') || steps[0].startsWith('error')) {
outputDiv.innerHTML = `<span class="output-error">${escapeHtml(steps[0])}</span>`;
return;
}
outputDiv.innerHTML = '';
const label = document.createElement('div');
label.className = 'output-label';
label.textContent = steps.length > 1 ? 'Steps:' : 'Result:';
outputDiv.appendChild(label);
steps.forEach((step, i) => {
const trimmed = step.trim();
if (!trimmed) return;
const stepDiv = document.createElement('div');
stepDiv.className = 'output-rendered';
if (steps.length > 1) {
const numSpan = document.createElement('span');
numSpan.style.cssText = 'color:var(--text-dim);font-size:0.75rem;margin-right:6px;';
numSpan.textContent = `${i + 1}.`;
stepDiv.appendChild(numSpan);
}
const renderResult = renderStep(trimmed);
if (renderResult.type === 'text') {
const textSpan = document.createElement('span');
textSpan.className = 'step-text';
textSpan.textContent = renderResult.text;
stepDiv.appendChild(textSpan);
if (renderResult.math) {
const mathSpan = document.createElement('span');
stepDiv.appendChild(mathSpan);
renderKatex(renderResult.math, mathSpan, false);
}
} else {
const mathDiv = document.createElement('span');
stepDiv.appendChild(mathDiv);
renderKatex(renderResult.math, mathDiv, false);
}
outputDiv.appendChild(stepDiv);
});
const rawDiv = document.createElement('div');
rawDiv.className = 'output-raw';
rawDiv.textContent = steps.join('\n');
outputDiv.appendChild(rawDiv);
}
function cellKeydown(event, index) {
if (event.key === 'Enter' && (event.shiftKey || event.metaKey || event.ctrlKey)) {
event.preventDefault();
runCell(index);
}
if (event.key === 'Enter' && event.altKey) {
event.preventDefault();
runCell(index).then(() => {
addCell('math');
setTimeout(() => {
const cells = document.querySelectorAll('[data-index]');
const last = cells[cells.length - 1];
if (last) last.querySelector('textarea').focus();
}, 50);
});
}
}
async function runCell(index) {
const cellDiv = document.querySelector(`[data-index="${index}"]`);
const textarea = cellDiv.querySelector('textarea');
const input = textarea.value;
notebook.cells[index].input = input;
if (notebook.cells[index].cell_type === 'text') return;
const outputDiv = document.getElementById(`output-${index}`);
const statusEl = document.getElementById(`status-${index}`);
if (statusEl) { statusEl.textContent = 'running'; statusEl.className = 'cell-status running'; }
outputDiv.innerHTML = '<span class="output-placeholder">Evaluating...</span>';
try {
const resp = await fetch('/api/eval', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({input: input})
});
const data = await resp.json();
notebook.cells[index].output = data.output;
notebook.cells[index].mathml = data.mathml || '';
if (data.image) {
outputDiv.innerHTML = '';
const label = document.createElement('div');
label.className = 'output-label';
label.textContent = 'Plot:';
outputDiv.appendChild(label);
const imgDiv = document.createElement('div');
imgDiv.className = 'output-image';
const img = document.createElement('img');
img.src = 'data:image/png;base64,' + data.image;
imgDiv.appendChild(img);
outputDiv.appendChild(imgDiv);
} else {
renderOutputWithSteps(data.steps || [data.output], outputDiv);
}
if (showMathML && data.mathml && !data.image) {
const mlDiv = document.createElement('div');
mlDiv.className = 'output-mathml';
const mlLabel = document.createElement('div');
mlLabel.className = 'output-label';
mlLabel.textContent = 'MathML:';
mlDiv.appendChild(mlLabel);
const mlPre = document.createElement('pre');
mlPre.textContent = data.mathml;
mlPre.style.cssText = 'font-size:0.75rem;white-space:pre-wrap;word-break:break-all;color:var(--text-dim);';
mlDiv.appendChild(mlPre);
outputDiv.appendChild(mlDiv);
}
execCounter++;
notebook.cells[index].exec_num = execCounter;
const execEl = document.getElementById(`exec-${index}`);
if (execEl) execEl.textContent = `In [${execCounter}]`;
if (statusEl) {
const isError = data.steps && data.steps[0] && data.steps[0].startsWith('Error:');
statusEl.textContent = isError ? 'error' : 'done';
statusEl.className = 'cell-status ' + (isError ? 'error' : 'done');
}
updateContextPanel();
} catch(e) {
outputDiv.innerHTML = `<span class="output-error">Request failed: ${escapeHtml(e.message)}</span>`;
if (statusEl) { statusEl.textContent = 'error'; statusEl.className = 'cell-status error'; }
}
}
async function runAll() {
for (let i = 0; i < notebook.cells.length; i++) {
if (notebook.cells[i].cell_type !== 'text') {
await runCell(i);
}
}
}
function renderMathMLNative(mathmlStr, element) {
element.innerHTML = mathmlStr;
}
function addCell(type) {
const cellType = type || 'math';
const cell = { id: nextId++, input: '', output: '', cell_type: cellType };
notebook.cells.push(cell);
const container = document.getElementById('cells');
container.appendChild(createCellElement(cell, notebook.cells.length - 1));
}
function deleteCell(index) {
notebook.cells.splice(index, 1);
notebook.cells.forEach((c, i) => c.id = i);
renderAll();
}
function moveCellUp(index) {
if (index === 0) return;
const tmp = notebook.cells[index];
notebook.cells[index] = notebook.cells[index - 1];
notebook.cells[index - 1] = tmp;
notebook.cells.forEach((c, i) => c.id = i);
renderAll();
}
function moveCellDown(index) {
if (index >= notebook.cells.length - 1) return;
const tmp = notebook.cells[index];
notebook.cells[index] = notebook.cells[index + 1];
notebook.cells[index + 1] = tmp;
notebook.cells.forEach((c, i) => c.id = i);
renderAll();
}
function duplicateCell(index) {
const clone = { ...notebook.cells[index] };
notebook.cells.splice(index + 1, 0, clone);
notebook.cells.forEach((c, i) => c.id = i);
renderAll();
}
function toggleCellType(index) {
const cell = notebook.cells[index];
const cycle = { 'math': 'text', 'text': 'mathml', 'mathml': 'math' };
cell.cell_type = cycle[cell.cell_type] || 'math';
cell.output = '';
renderAll();
}
async function resetContext() {
try {
await fetch('/api/reset', { method: 'POST' });
execCounter = 0;
notebook.cells.forEach(c => { c.exec_num = null; });
renderAll();
updateContextPanel();
showStatus('Context reset');
} catch(e) {
showStatus('Reset failed: ' + e.message, true);
}
}
async function resetAndRunAll() {
try {
await fetch('/api/reset', { method: 'POST' });
execCounter = 0;
notebook.cells.forEach(c => { c.exec_num = null; });
renderAll();
} catch(e) {
showStatus('Reset failed: ' + e.message, true);
return;
}
await runAll();
}
async function updateContextPanel() {
try {
const resp = await fetch('/api/context');
const data = await resp.json();
const vars = data.vars || [];
const funcs = data.funcs || [];
document.getElementById('context-summary').textContent =
`Context: ${vars.length} variable${vars.length !== 1 ? 's' : ''}, ${funcs.length} function${funcs.length !== 1 ? 's' : ''}`;
document.getElementById('context-vars').textContent =
vars.length > 0 ? vars.join(', ') : 'No variables bound.';
document.getElementById('context-funcs').textContent =
funcs.length > 0 ? funcs.join(', ') : 'No user functions.';
} catch(e) {
}
}
async function saveNotebook() {
const cellDivs = document.querySelectorAll('[data-index]');
cellDivs.forEach(div => {
const index = parseInt(div.dataset.index);
const textarea = div.querySelector('textarea');
if (textarea) notebook.cells[index].input = textarea.value;
});
try {
await fetch('/api/notebook', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(notebook)
});
showStatus('Saved');
} catch(e) {
showStatus('Save failed: ' + e.message, true);
}
}
function showStatus(msg, isError) {
const el = document.createElement('div');
el.style.cssText = `position:fixed;top:60px;right:20px;padding:8px 16px;border-radius:6px;z-index:200;${isError ? 'background:var(--red);color:var(--bg)' : 'background:var(--green);color:var(--bg)'}`;
el.textContent = msg;
document.body.appendChild(el);
setTimeout(() => el.remove(), 2000);
}
function escapeHtml(s) {
return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
}
function waitForLibs(cb, n) {
n = n || 0;
if ((typeof katex !== 'undefined' || n > 50) && (typeof marked !== 'undefined' || n > 50)) cb();
else setTimeout(() => waitForLibs(cb, n + 1), 100);
}
waitForLibs(init);
</script>
</body>
</html>