|
1 | 1 | const view = document.querySelector('#view'); |
2 | 2 | let dashboard = null; |
| 3 | +const sandboxHistory = []; |
| 4 | +const sandboxSamples = [ |
| 5 | + { label: 'Вывод текста', code: "print('Привет, Python!')\n", inputs: [] }, |
| 6 | + { label: 'Небольшой расчёт', code: 'price = 120\ncount = 3\nprint(price * count)\n', inputs: [] }, |
| 7 | + { label: 'Ввод имени', code: "name = input('Имя: ')\nprint(f'Привет, {name}!')\n", inputs: ['Аня'] }, |
| 8 | +]; |
3 | 9 |
|
4 | 10 | const esc = (value = '') => String(value) |
5 | 11 | .replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", '''); |
@@ -36,7 +42,7 @@ function route() { |
36 | 42 | } |
37 | 43 |
|
38 | 44 | function setActiveNavigation(current) { |
39 | | - const active = current.startsWith('/projects') ? '/projects' : current.startsWith('/practice') ? '/practice' : '/'; |
| 45 | + const active = current.startsWith('/sandbox') ? '/sandbox' : current.startsWith('/projects') ? '/projects' : current.startsWith('/practice') ? '/practice' : '/'; |
40 | 46 | document.querySelectorAll('[data-nav]').forEach((node) => node.classList.toggle('active', node.dataset.nav === active)); |
41 | 47 | } |
42 | 48 |
|
@@ -110,7 +116,7 @@ function questionTemplate(question, number, stageLabel = '') { |
110 | 116 | const exampleInputs = (question.input_example || []).join('\n'); |
111 | 117 | field = `<textarea class="code-editor" data-answer-q="${question.id}" autocomplete="off" autocapitalize="off" autocorrect="off" spellcheck="false">${esc(question.starter)}</textarea> |
112 | 118 | <details class="code-input-panel" ${exampleInputs ? 'open' : ''}><summary>⌨️ Данные для input() <small>необязательно</small></summary><p>Одна строка — один ответ. Они подставятся по порядку при запуске.</p><textarea class="code-stdin" data-input-q="${question.id}" placeholder="Например: Аня" autocomplete="off" autocapitalize="off" autocorrect="off" spellcheck="false">${esc(exampleInputs)}</textarea></details> |
113 | | - <div class="code-actions"><button class="button ghost" type="button" data-run-code="${question.id}">▷ Запустить</button><button class="button blue" type="button" data-check-code="${question.id}">✓ Проверить по заданию</button></div> |
| 119 | + <div class="code-actions"><button class="button ghost" type="button" data-run-code="${question.id}">▷ Запустить</button><button class="button blue" type="button" data-check-code="${question.id}">✓ Проверить по заданию</button><a class="button ghost sandbox-link" href="#/sandbox">⌨️ Песочница</a></div> |
114 | 120 | <details class="hint"><summary>Нужна подсказка?</summary><p>${esc(question.hint)}</p></details>`; |
115 | 121 | } |
116 | 122 | return `<article class="question-card card" data-question="${question.id}">${head}${guide}${field}<div class="inline-result" id="result-${question.id}"></div></article>`; |
@@ -302,6 +308,54 @@ async function renderPractice(mode = 'guided', moduleId = '') { |
302 | 308 | renderStep(); |
303 | 309 | } |
304 | 310 |
|
| 311 | +function renderSandboxHistory() { |
| 312 | + const node = document.querySelector('#sandbox-history'); |
| 313 | + if (!node) return; |
| 314 | + if (!sandboxHistory.length) { |
| 315 | + node.innerHTML = '<p class="sandbox-empty">Запуски появятся здесь. Не бойся менять код и запускать снова.</p>'; |
| 316 | + return; |
| 317 | + } |
| 318 | + node.innerHTML = sandboxHistory.map((item, index) => `<article class="sandbox-run ${item.correct ? '' : 'failed'}"><div><strong>Запуск ${sandboxHistory.length - index}</strong><span>${item.correct ? 'Код выполнился' : esc(item.message)}</span></div><pre class="execution-source">${esc(item.source)}</pre><span>Консоль</span><pre class="code-output">${esc(item.output || '— программа ничего не вывела —')}</pre></article>`).join(''); |
| 319 | +} |
| 320 | + |
| 321 | +function renderSandbox() { |
| 322 | + const sample = sandboxSamples[2]; |
| 323 | + view.innerHTML = `<section class="sandbox-wrap"><a class="back-link" href="#/">← К маршруту</a><article class="sandbox-hero card"><p class="eyebrow">Свободная практика</p><h1>Песочница Python</h1><p class="lead">Пиши обычный код, меняй его и запускай сколько угодно раз. Здесь нет оценки и нет единственного правильного ответа — есть код, консоль и эксперимент.</p></article><aside class="learning-roadmap"><strong>Рабочий цикл</strong><span>1. Напиши идею</span><span>2. Запусти код</span><span>3. Прочитай вывод</span><p>Можно использовать базовые функции Python, условия, циклы, функции и input(). Импорты, файлы и системные команды остаются закрыты для безопасности.</p></aside><section class="sandbox-card card"><div class="sandbox-toolbar"><strong>Быстрый старт</strong><div>${sandboxSamples.map((item, index) => `<button class="sandbox-sample" type="button" data-sandbox-sample="${index}">${esc(item.label)}</button>`).join('')}</div></div><textarea id="sandbox-code" class="code-editor sandbox-editor" autocomplete="off" autocapitalize="off" autocorrect="off" spellcheck="false">${esc(sample.code)}</textarea><details class="code-input-panel" open><summary>⌨️ Данные для input() <small>необязательно</small></summary><p>Одна строка — один ответ. Если код вызывает input() дважды, напиши два ответа в разных строках.</p><textarea id="sandbox-input" class="code-stdin" placeholder="Например: Аня" autocomplete="off" autocapitalize="off" autocorrect="off" spellcheck="false">${esc(sample.inputs.join('\n'))}</textarea></details><div class="code-actions"><button class="button blue" type="button" id="sandbox-run">▷ Запустить код</button><button class="button ghost" type="button" id="sandbox-reset">↺ Очистить</button></div></section><section class="sandbox-console card"><div class="sandbox-console-head"><div><p class="eyebrow">История</p><h2>Консоль запусков</h2></div><span>Каждый запуск сохраняется, пока открыта страница</span></div><div id="sandbox-history"></div></section></section>`; |
| 324 | + const code = document.querySelector('#sandbox-code'); |
| 325 | + const input = document.querySelector('#sandbox-input'); |
| 326 | + const runButton = document.querySelector('#sandbox-run'); |
| 327 | + document.querySelectorAll('[data-sandbox-sample]').forEach((button) => { |
| 328 | + button.addEventListener('click', () => { |
| 329 | + const selected = sandboxSamples[Number(button.dataset.sandboxSample)]; |
| 330 | + code.value = selected.code; |
| 331 | + input.value = selected.inputs.join('\n'); |
| 332 | + code.focus(); |
| 333 | + }); |
| 334 | + }); |
| 335 | + document.querySelector('#sandbox-reset').addEventListener('click', () => { |
| 336 | + code.value = '# Здесь можно писать и запускать любой учебный код\n'; |
| 337 | + input.value = ''; |
| 338 | + sandboxHistory.length = 0; |
| 339 | + renderSandboxHistory(); |
| 340 | + code.focus(); |
| 341 | + }); |
| 342 | + runButton.addEventListener('click', async () => { |
| 343 | + runButton.disabled = true; |
| 344 | + runButton.textContent = 'Запускаем…'; |
| 345 | + try { |
| 346 | + const result = await api('/api/sandbox/run', { method: 'POST', body: JSON.stringify({ answer: code.value, inputs: input.value ? input.value.replaceAll('\r\n', '\n').split('\n') : [] }) }); |
| 347 | + sandboxHistory.unshift({ source: code.value, output: result.output, correct: result.correct, message: result.message }); |
| 348 | + renderSandboxHistory(); |
| 349 | + } catch (error) { |
| 350 | + sandboxHistory.unshift({ source: code.value, output: '', correct: false, message: error.message }); |
| 351 | + renderSandboxHistory(); |
| 352 | + } |
| 353 | + runButton.disabled = false; |
| 354 | + runButton.textContent = '▷ Запустить код'; |
| 355 | + }); |
| 356 | + renderSandboxHistory(); |
| 357 | +} |
| 358 | + |
305 | 359 | async function renderProjects() { |
306 | 360 | loading(); |
307 | 361 | const data = await api('/api/projects'); |
@@ -380,6 +434,7 @@ async function render() { |
380 | 434 | try { |
381 | 435 | if (current === '/') { loading(); renderDashboard(await refreshDashboard()); } |
382 | 436 | else if (current === '/practice') await renderPractice(); |
| 437 | + else if (current === '/sandbox') renderSandbox(); |
383 | 438 | else if (current === '/projects') await renderProjects(); |
384 | 439 | else if (current.startsWith('/projects/')) await renderProject(current.split('/')[2]); |
385 | 440 | else if (current.startsWith('/lesson/')) await renderLesson(current.split('/')[2]); |
|
0 commit comments