425 lines
14 KiB
JavaScript
425 lines
14 KiB
JavaScript
/**
|
|
* ⚡ 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, """)
|
|
.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 = `<textarea id="fallback-code-editor" style="width: 100%; height: 100%; background: #0b0f19; color: #f8fafc; font-family: monospace; font-size: 14px; padding: 14px; border: none; outline: none;">${escapeHtml(initialCode)}</textarea>`;
|
|
};
|
|
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 = `
|
|
<div class="test-summary-badge ${isSuccess ? 'passed' : 'failed'}">
|
|
<span>${isSuccess ? '🎉 Alle ' + total + ' Tests bestanden!' : '⚠️ ' + (total - passed) + ' von ' + total + ' Test(s) fehlgeschlagen'}</span>
|
|
<span>${passed} / ${total} Tests OK</span>
|
|
</div>
|
|
`;
|
|
|
|
if (cases.length > 0) {
|
|
cases.forEach(c => {
|
|
const card = document.createElement("div");
|
|
card.className = "test-case-card";
|
|
const hasErr = !c.passed;
|
|
|
|
card.innerHTML = `
|
|
<div class="test-case-header" onclick="const d = this.nextElementSibling; if(d) d.style.display = d.style.display === 'none' ? 'block' : 'none';">
|
|
<span style="color: ${c.passed ? '#4ade80' : '#f87171'}; font-weight: 700;">
|
|
${c.passed ? '✓' : '✕'} ${escapeHtml(c.name)}
|
|
</span>
|
|
<span style="color: #94a3b8; font-size: 0.76rem;">${c.passed ? 'Bestanden' : 'Fehlgeschlagen ▼'}</span>
|
|
</div>
|
|
<div class="test-case-details" style="display: ${hasErr ? 'block' : 'none'};">
|
|
${hasErr ? `
|
|
<div style="color: #fca5a5; margin-bottom: 6px;">
|
|
<strong>Meldung:</strong> Test fehlgeschlagen.
|
|
</div>
|
|
${expectedVal ? `
|
|
<div class="test-diff-inspector">
|
|
<div><span style="color: #94a3b8;">Erwartet: </span><span class="test-diff-expected">${escapeHtml(expectedVal)}</span></div>
|
|
<div><span style="color: #94a3b8;">Erhalten: </span><span class="test-diff-actual">${escapeHtml(actualVal)}</span></div>
|
|
</div>
|
|
` : ''}
|
|
${errorLine ? `
|
|
<button type="button" class="btn-jump-code" onclick="window.jumpToEditorLine(${errorLine})">
|
|
📍 Zu Zeile ${errorLine} in deiner Aufgabe springen
|
|
</button>
|
|
` : ''}
|
|
` : '<div style="color: #4ade80;">Dieser Testfall wurde ohne Fehler bestanden.</div>'}
|
|
</div>
|
|
`;
|
|
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 || "");
|
|
}
|
|
});
|
|
};
|
|
})();
|