/** * ⚡ UNIVERSAL MODULAR MONACO WEB-IDE ⚡ * ======================================= * Sprachunabhängige Entwicklungsumgebung mit: * - Dynamischer Monaco Editor Initialisierung (Python, JavaScript, SQL, Rust etc.) * - Delegierung an pluggbare Language Runner via RunnerRegistry * - Tastatur-Shortcuts: Strg+Enter (Run), Strg+Shift+Enter (Test), Strg+S (Save) * - Reaktives Terminal, Auto-Save & Gamification Integration */ (function () { let monacoEditor = null; let currentChapterData = null; let currentLanguage = "python"; let autoSaveTimeout = null; function escapeHtml(text) { if (!text) return ""; return String(text) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } function getActiveRunner() { if (window.RunnerRegistry) { const runner = window.RunnerRegistry.get(currentLanguage); if (runner) return runner; } return null; } // 1. Initialisiere Monaco Editor function initMonacoEditor(initialCode = "", langId = "python") { currentLanguage = langId; const editorContainer = document.getElementById("monaco-editor-container"); if (!editorContainer) return; if (window.monaco) { createMonacoInstance(initialCode, langId); return; } const loaderScript = document.createElement("script"); loaderScript.src = "https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.45.0/min/vs/loader.min.js"; loaderScript.onload = () => { window.require.config({ paths: { vs: "https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.45.0/min/vs" } }); window.require(["vs/editor/editor.main"], () => { createMonacoInstance(initialCode, langId); }); }; loaderScript.onerror = () => { editorContainer.innerHTML = ``; }; document.head.appendChild(loaderScript); } function createMonacoInstance(initialCode, langId) { const editorContainer = document.getElementById("monaco-editor-container"); if (!editorContainer) return; editorContainer.innerHTML = ""; const runner = getActiveRunner(); const monacoLang = runner ? runner.monacoLanguage : langId; monacoEditor = window.monaco.editor.create(editorContainer, { value: initialCode, language: monacoLang, theme: "vs-dark", fontSize: 14, fontFamily: "'JetBrains Mono', 'Fira Code', Consolas, monospace", automaticLayout: true, minimap: { enabled: false }, scrollBeyondLastLine: false, tabSize: 4, insertSpaces: true, lineNumbers: "on", bracketPairColorization: { enabled: true } }); monacoEditor.onDidChangeModelContent(() => { triggerAutoSave(); }); monacoEditor.addCommand(window.monaco.KeyMod.CtrlCmd | window.monaco.KeyCode.Enter, () => { runCode(); }); monacoEditor.addCommand(window.monaco.KeyMod.CtrlCmd | window.monaco.KeyMod.Shift | window.monaco.KeyCode.Enter, () => { runTests(); }); monacoEditor.addCommand(window.monaco.KeyMod.CtrlCmd | window.monaco.KeyCode.KeyS, () => { saveProgressDraft(); }); } window.addEventListener("keydown", (e) => { if ((e.ctrlKey || e.metaKey) && e.key === "s") { e.preventDefault(); saveProgressDraft(); } }); // 2. Code-Ausführung via aktivem Language-Runner async function runCode() { if (window.AUTH && !window.AUTH.isLoggedIn()) { window.AUTH.requireAuth({ allowClose: false, title: '🔒 Registrierung erforderlich', message: 'Um Code in der Web-IDE auszuführen, erstelle bitte einen kostenlosen Account oder melde dich an.' }); return; } const code = getEditorCode(); clearTerminal(); const runner = getActiveRunner(); if (!runner) { logToTerminal(`❌ Kein Language-Runner für '${currentLanguage}' registriert.\n`, "error"); return; } logToTerminal(`▶ Starte Ausführung mit ${runner.displayName} (Strg+Enter)...\n----------------------------------------\n`, "info"); const result = await runner.runCode(code, logToTerminal); if (result.success) { logToTerminal("\n----------------------------------------\n✨ Erfolgreich ausgeführt!\n", "success"); } else if (result.error) { suggestErrorInterpreter(result.error); } } // 3. Test-Ausführung via aktivem Language-Runner async function runTests() { if (window.AUTH && !window.AUTH.isLoggedIn()) { window.AUTH.requireAuth({ allowClose: false, title: '🔒 Registrierung erforderlich', message: 'Um automatisierte Tests auszuführen und Lernfortschritt zu speichern, erstelle bitte einen kostenlosen Account oder melde dich an.' }); return; } if (!currentChapterData || !currentChapterData.testCode) { logToTerminal("❌ Keine Unittests für dieses Kapitel gefunden.\n", "error"); return; } const userCode = getEditorCode(); clearTerminal(); const runner = getActiveRunner(); if (!runner) { logToTerminal(`❌ Kein Test-Runner für '${currentLanguage}' verfügbar.\n`, "error"); return; } logToTerminal(`🧪 Führe automatisierte Tests aus (Strg+Shift+Enter)...\n----------------------------------------\n`, "info"); const result = await runner.runTests(userCode, currentChapterData.testCode, logToTerminal, currentChapterData); const isGenuineSuccess = Boolean( result && result.success === true && result.total > 0 && (!result.failures || result.failures === 0) && (!result.errors || result.errors === 0) ); if (isGenuineSuccess) { logToTerminal(`\n🎉 HERVORRAGEND! Alle ${result.total || 'erforderlichen'} Tests erfolgreich bestanden! (+100 XP)\n`, "success"); triggerSuccessCelebration(); recordChapterSolved(); } else { const failCount = (result && (result.failures || result.errors)) ? (result.failures + (result.errors || 0)) : 1; const errorMsg = (result && result.error) ? `\n❌ ${result.error}\n` : `\n⚠️ ${failCount} Test(s) fehlgeschlagen oder unvollständig. Überarbeite deinen Code!\n`; logToTerminal(errorMsg, "warning"); if (result && result.rawOutput) { suggestErrorInterpreter(result.rawOutput); } } // Strukturierten visuellen Test-Report mit Diff-Inspektor rendern if (result && (result.total > 0 || result.rawOutput)) { renderVisualTestReport(result); } } window.jumpToEditorLine = function(line) { if (monacoEditor && line) { monacoEditor.revealLineInCenter(line); monacoEditor.setPosition({ lineNumber: line, column: 1 }); monacoEditor.focus(); } }; function renderVisualTestReport(result) { const term = document.getElementById("workspace-terminal-output"); if (!term) return; const cases = []; const raw = result.rawOutput || ""; const lineRegex = /(test_\w+)\s+\([^)]+\)\s+\.\.\.\s+(ok|FAIL|ERROR)/g; let match; while ((match = lineRegex.exec(raw)) !== null) { cases.push({ name: match[1], passed: match[2] === "ok", status: match[2] }); } const fileLineMatch = raw.match(/File\s+["'].*?aufgabe\.\w+["'],\s+line\s+(\d+)/i); const errorLine = fileLineMatch ? parseInt(fileLineMatch[1], 10) : null; let expectedVal = null; let actualVal = null; const assertEqualMatch = raw.match(/AssertionError:\s*(.*?)\s*!=\s*(.*)/); if (assertEqualMatch) { actualVal = assertEqualMatch[1].trim(); expectedVal = assertEqualMatch[2].trim(); } const reportCard = document.createElement("div"); reportCard.style.marginTop = "14px"; const total = result.total || cases.length || 1; const passed = result.passed !== undefined ? result.passed : (result.success ? total : 0); const isSuccess = result.success; reportCard.innerHTML = `
${isSuccess ? '🎉 Alle ' + total + ' Tests bestanden!' : '⚠️ ' + (total - passed) + ' von ' + total + ' Test(s) fehlgeschlagen'} ${passed} / ${total} Tests OK
`; if (cases.length > 0) { cases.forEach(c => { const card = document.createElement("div"); card.className = "test-case-card"; const hasErr = !c.passed; card.innerHTML = `
${c.passed ? '✓' : '✕'} ${escapeHtml(c.name)} ${c.passed ? 'Bestanden' : 'Fehlgeschlagen ▼'}
${hasErr ? `
Meldung: Test fehlgeschlagen.
${expectedVal ? `
Erwartet: ${escapeHtml(expectedVal)}
Erhalten: ${escapeHtml(actualVal)}
` : ''} ${errorLine ? ` ` : ''} ` : '
Dieser Testfall wurde ohne Fehler bestanden.
'}
`; reportCard.appendChild(card); }); } term.appendChild(reportCard); term.scrollTop = term.scrollHeight; } // 4. Hilfsfunktionen function getEditorCode() { if (monacoEditor) { return monacoEditor.getValue(); } const fallback = document.getElementById("fallback-code-editor"); return fallback ? fallback.value : ""; } function setEditorCode(code) { if (monacoEditor) { monacoEditor.setValue(code); } else { const fallback = document.getElementById("fallback-code-editor"); if (fallback) fallback.value = code; } } function logToTerminal(text, type = "stdout") { const term = document.getElementById("workspace-terminal-output"); if (!term) return; const span = document.createElement("span"); span.className = `term-line term-${type}`; span.innerText = text; term.appendChild(span); term.scrollTop = term.scrollHeight; } function clearTerminal() { const term = document.getElementById("workspace-terminal-output"); if (term) term.innerHTML = ""; } function triggerAutoSave() { clearTimeout(autoSaveTimeout); autoSaveTimeout = setTimeout(() => { saveProgressDraft(); }, 1500); } async function saveProgressDraft() { if (!currentChapterData || !currentChapterData.chapterId) return; const code = getEditorCode(); localStorage.setItem(`code_draft_${currentChapterData.chapterId}`, code); const token = localStorage.getItem("auth_token"); if (token) { try { await fetch("/api/progress/save", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` }, body: JSON.stringify({ chapter_id: currentChapterData.chapterId, code_draft: code }) }); } catch (e) {} } const saveBadge = document.getElementById("editor-save-indicator"); if (saveBadge) { saveBadge.innerText = "💾 Gespeichert"; saveBadge.style.opacity = "1"; setTimeout(() => { saveBadge.style.opacity = "0.5"; }, 2000); } } async function recordChapterSolved() { if (!currentChapterData || !currentChapterData.chapterId) return; if (window.addXP) { window.addXP(100, currentChapterData.chapterId); } const token = localStorage.getItem("auth_token"); if (token) { try { await fetch("/api/progress/solve", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` }, body: JSON.stringify({ chapter_id: currentChapterData.chapterId, xp_reward: 100 }) }); } catch (e) {} } } function triggerSuccessCelebration() { const banner = document.getElementById("success-modal-banner"); if (banner) { banner.style.display = "flex"; } } function suggestErrorInterpreter(traceback) { const errorTab = document.getElementById("tab-btn-interpreter"); const errorInput = document.getElementById("workspace-error-input"); if (errorInput) { errorInput.value = traceback; } if (errorTab) { errorTab.style.borderColor = "var(--danger)"; errorTab.innerHTML = "🩺 Fehler-Dolmetscher ⚠️"; } if (window.interpretWorkspaceError) { window.interpretWorkspaceError(); } } // 5. Init Workspace Controller window.initWorkspace = function (chapterData, language = "python") { currentChapterData = chapterData; currentLanguage = language || "python"; const savedDraft = localStorage.getItem(`code_draft_${chapterData.chapterId}`); const initialCode = savedDraft || chapterData.starterCode || ""; initMonacoEditor(initialCode, currentLanguage); document.getElementById("btn-run-code")?.addEventListener("click", runCode); document.getElementById("btn-run-tests")?.addEventListener("click", runTests); document.getElementById("btn-save-code")?.addEventListener("click", saveProgressDraft); document.getElementById("btn-clear-terminal")?.addEventListener("click", clearTerminal); document.getElementById("btn-reset-code")?.addEventListener("click", () => { if (confirm("Möchtest du deinen Code wirklich auf den Original-Startercode zurücksetzen?")) { setEditorCode(chapterData.starterCode || ""); } }); }; })();