document.addEventListener("DOMContentLoaded", () => { // --- Element Selectors --- const promptInput = document.getElementById("prompt-input"); const generateBtn = document.getElementById("generate-btn"); const modificationInput = document.getElementById("modification-input"); const modifyBtn = document.getElementById("modify-btn"); const downloadBtn = document.getElementById("download-btn"); const openTabBtn = document.getElementById("open-tab-btn"); const copyCodeBtn = document.getElementById("copy-code-btn"); const togglePreviewBtn = document.getElementById("toggle-preview-btn"); const toggleCodeBtn = document.getElementById("toggle-code-btn"); const refreshPreviewBtn = document.getElementById("refresh-preview-btn"); const codeContainer = document.querySelector(".code-container"); const previewContainer = document.querySelector(".preview-container"); const monacoContainer = document.getElementById("monaco-editor"); const gameIframe = document.getElementById("game-iframe"); const initialGenerationPanel = document.getElementById("initial-generation"); const modificationPanel = document.getElementById("modification-panel"); const spinner = document.querySelector(".spinner-container"); const analysisContainer = document.getElementById("analysis-container"); const aiAnalysis = document.getElementById("ai-analysis"); const changesContainer = document.getElementById("changes-container"); const summaryOfChanges = document.getElementById("summary-of-changes"); const instructionsContainer = document.getElementById( "instructions-container" ); const gameInstructions = document.getElementById("game-instructions"); const consoleContainer = document.getElementById("console-container"); const consoleOutput = document.getElementById("console-output"); const followUpInput = document.getElementById("follow-up-input"); const followUpBtn = document.getElementById("follow-up-btn"); const followUpSpinner = document.getElementById("follow-up-spinner"); const followUpOutputContainer = document.getElementById( "follow-up-output-container" ); const followUpOutput = document.getElementById("follow-up-output"); const versionHistoryControls = document.getElementById( "version-history-controls" ); const versionHistorySelect = document.getElementById( "version-history-select" ); const loadSessionBtn = document.getElementById("load-session-btn"); const saveSessionBtn = document.getElementById("save-session-btn"); const loadSessionPopup = document.getElementById("load-session-popup"); const saveSessionPopup = document.getElementById("save-session-popup"); const loadSessionPopupClose = document.getElementById( "load-session-popup-close" ); const saveSessionPopupClose = document.getElementById( "save-session-popup-close" ); const followupFloat = document.getElementById("followup-float"); const followupBtn = document.getElementById("followup-btn"); const followupPopup = document.getElementById("followup-popup"); const followupPopupClose = document.getElementById("followup-popup-close"); const sessionSelect = document.getElementById("session-select"); const sessionNameInput = document.getElementById("session-name-input"); const loadSessionActionBtn = document.getElementById( "load-session-action-btn" ); const saveSessionActionBtn = document.getElementById( "save-session-action-btn" ); const deleteSessionBtn = document.getElementById("delete-session-btn"); const newSessionBtn = document.getElementById("new-session-btn"); const hamburgerBtn = document.getElementById("hamburger-btn"); const sidebar = document.getElementById("sidebar"); const sidebarOverlay = document.getElementById("sidebar-overlay"); const themeToggle = document.getElementById("theme-toggle"); const generateFileInput = document.getElementById("generate-file-input"); const generateFileList = document.getElementById("generate-file-list"); const modifyFileInput = document.getElementById("modify-file-input"); const modifyFileList = document.getElementById("modify-file-list"); // --- State Variables --- let promptHistory = []; let consoleLogs = []; let abortController = null; let versionHistory = []; let currentSessionId = null; const clientSideAssets = new Map(); const DB_NAME = "CodeCanvasDB"; const DB_VERSION = 1; const STORE_NAME = "sessions"; let marked = null; let db = null; let monacoEditor = null; let monaco = null; let lastScrollPosition = { lineNumber: 1, column: 1 }; let hasGeneratedContent = false; const mainContainer = document.querySelector(".main-container"); let autoScrollEnabled = true; // --- IndexedDB Functions --- function initDB() { return new Promise((resolve, reject) => { const request = indexedDB.open(DB_NAME, DB_VERSION); request.onerror = () => reject(request.error); request.onsuccess = () => { db = request.result; resolve(db); }; request.onupgradeneeded = (event) => { db = event.target.result; if (!db.objectStoreNames.contains(STORE_NAME)) { const store = db.createObjectStore(STORE_NAME, { keyPath: "id" }); store.createIndex("name", "name", { unique: false }); store.createIndex("savedAt", "savedAt", { unique: false }); } }; }); } async function saveSessionToDB(sessionData) { if (!db) await initDB(); return new Promise((resolve, reject) => { const transaction = db.transaction([STORE_NAME], "readwrite"); const store = transaction.objectStore(STORE_NAME); const request = store.put(sessionData); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); } async function getSessionsFromDB() { if (!db) await initDB(); return new Promise((resolve, reject) => { const transaction = db.transaction([STORE_NAME], "readonly"); const store = transaction.objectStore(STORE_NAME); const request = store.getAll(); request.onsuccess = () => resolve(request.result || []); request.onerror = () => reject(request.error); }); } async function deleteSessionFromDB(sessionId) { if (!db) await initDB(); return new Promise((resolve, reject) => { const transaction = db.transaction([STORE_NAME], "readwrite"); const store = transaction.objectStore(STORE_NAME); const request = store.delete(sessionId); request.onsuccess = () => resolve(); request.onerror = () => reject(request.error); }); } // --- Core Application Logic --- class StreamParser { constructor() { this.reset(); } reset() { this.analysis = ""; this.changes = ""; this.instructions = ""; this.html = ""; this.currentSection = null; this.buffer = ""; } processChunk(chunk) { this.buffer += chunk; const lines = this.buffer.split("\n"); this.buffer = lines.pop(); for (const line of lines) { const trimmedLine = line.trim(); if (trimmedLine === "[ANALYSIS]") { this.currentSection = "ANALYSIS"; continue; } if (trimmedLine === "[END_ANALYSIS]") { this.currentSection = null; continue; } if (trimmedLine === "[CHANGES]") { this.currentSection = "CHANGES"; continue; } if (trimmedLine === "[END_CHANGES]") { this.currentSection = null; continue; } if (trimmedLine === "[INSTRUCTIONS]") { this.currentSection = "INSTRUCTIONS"; continue; } if (trimmedLine === "[END_INSTRUCTIONS]") { this.currentSection = "HTML"; continue; } switch (this.currentSection) { case "ANALYSIS": this.analysis += line + "\n"; break; case "CHANGES": this.changes += line + "\n"; break; case "INSTRUCTIONS": this.instructions += line + "\n"; break; case "HTML": this.html += line + "\n"; break; default: if ( trimmedLine.startsWith("") || this.currentSection === "HTML" ) { this.currentSection = "HTML"; this.html += line + "\n"; } break; } } } /** * BUGFIX: Returns the current state NON-DESTRUCTIVELY for live UI updates. * It does NOT process the buffer, preventing duplication errors. */ getCurrentState() { return { analysis: this.analysis.trim(), changes: this.changes.trim(), instructions: this.instructions.trim(), html: this.cleanHtml(this.html), }; } /** * BUGFIX: Finalizes the stream by processing the remaining buffer. * This should ONLY be called once at the very end. */ finalize() { if (this.buffer) { // Assume any remaining buffer content belongs to the last active section, defaulting to HTML const targetSection = this.currentSection || "HTML"; switch (targetSection) { case "ANALYSIS": this.analysis += this.buffer; break; case "CHANGES": this.changes += this.buffer; break; case "INSTRUCTIONS": this.instructions += this.buffer; break; case "HTML": default: this.html += this.buffer; break; } this.buffer = ""; // Clear the buffer after finalizing } return this.getCurrentState(); } cleanHtml(htmlString) { return htmlString.replace(/^```html\s*|\s*```$/g, "").trim(); } } async function streamResponse(url, body, targetElement) { setLoading(true, url); const parser = new StreamParser(); if (!targetElement) { setView("code"); if (monacoEditor) monacoEditor.setValue(""); clearInfoPanels(); } if (abortController) abortController.abort(); abortController = new AbortController(); try { const isFormData = body instanceof FormData; const response = await fetch(url, { method: "POST", body: isFormData ? body : JSON.stringify(body), headers: isFormData ? {} : { "Content-Type": "application/json" }, signal: abortController.signal, }); if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); const reader = response.body.getReader(); const decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; let chunk = decoder.decode(value, { stream: true }); if (chunk.includes("[STREAM_RESTART]")) { console.warn("Stream restart signal received."); showNotification( "Primary model failed; switching to fallback.", "warning" ); parser.reset(); clearInfoPanels(); if (monacoEditor) monacoEditor.setValue(""); chunk = chunk.replace(/\[STREAM_RESTART\]\s*\n?/, ""); } if (targetElement) { targetElement.textContent += chunk; targetElement.scrollTop = targetElement.scrollHeight; } else { parser.processChunk(chunk); // BUGFIX: Use the non-destructive getter for live updates const currentData = parser.getCurrentState(); updateUIFromStream(currentData); } } if (!targetElement) { const prompt = isFormData ? body.get("prompt") : body.prompt; // BUGFIX: Call finalize() only once at the end of the stream const finalData = parser.finalize(); // Final UI update with the fully parsed data updateUIFromStream(finalData); if (monacoEditor) { const totalLines = monacoEditor.getModel().getLineCount(); monacoEditor.revealLineNearTop( totalLines, monaco.editor.ScrollType.Smooth ); } // Update iframe and save version with the final, clean data updateIframe(finalData.html); saveVersion({ ...finalData, prompt }); showModificationPanel(); setTimeout(minimizeInfoPanels, 2000); } } catch (error) { if (error.name !== "AbortError") { console.error("Streaming failed:", error); const errorMessage = `Error: Failed to get response. Details: ${error.message}`; if (targetElement) { targetElement.textContent = "Error: Could not get response."; } else if (monacoEditor) { monacoEditor.setValue(errorMessage); setView("code"); } } } finally { setLoading(false, url); abortController = null; } } function updateUIFromStream({ analysis, changes, instructions, html }) { if (analysis) { analysisContainer.classList.remove("hidden"); aiAnalysis.innerHTML = marked ? marked.parse(analysis) : analysis; aiAnalysis.scrollTop = aiAnalysis.scrollHeight; } if (changes) { changesContainer.classList.remove("hidden"); summaryOfChanges.innerHTML = marked ? marked.parse(changes) : changes; summaryOfChanges.scrollTop = summaryOfChanges.scrollHeight; } if (instructions) { instructionsContainer.classList.remove("hidden"); gameInstructions.innerHTML = marked ? marked.parse(instructions) : instructions; gameInstructions.scrollTop = gameInstructions.scrollHeight; } if (html && monacoEditor) { const currentContent = monacoEditor.getValue(); if (html !== currentContent) { const model = monacoEditor.getModel(); monacoEditor.executeEdits(null, [ { range: model.getFullModelRange(), text: html }, ]); // --- Force scroll to bottom every time --- const scrollHeight = monacoEditor.getScrollHeight(); monacoEditor.setScrollTop(scrollHeight); } } } // --- Client-Side Asset Handling --- function handleFileSelection(event, fileListElement) { for (const file of event.target.files) { if (!clientSideAssets.has(file.name)) { clientSideAssets.set(file.name, { file: file, blobUrl: URL.createObjectURL(file), }); } } renderSelectedFiles(fileListElement); event.target.value = ""; } function renderSelectedFiles(fileListElement) { fileListElement.innerHTML = ""; fileListElement.classList.toggle("hidden", clientSideAssets.size === 0); clientSideAssets.forEach((asset, fileName) => { const fileItem = document.createElement("div"); fileItem.className = "file-item"; const nameSpan = document.createElement("span"); nameSpan.textContent = fileName; const removeBtn = document.createElement("button"); removeBtn.innerHTML = "×"; removeBtn.className = "file-remove-btn"; removeBtn.onclick = () => { URL.revokeObjectURL(asset.blobUrl); clientSideAssets.delete(fileName); renderSelectedFiles(generateFileList); renderSelectedFiles(modifyFileList); }; fileItem.appendChild(nameSpan); fileItem.appendChild(removeBtn); fileListElement.appendChild(fileItem); }); } async function deserializeAssets(assets) { clientSideAssets.clear(); for (const assetData of assets) { try { const blob = new Blob([assetData.data], { type: assetData.type }); const file = new File([blob], assetData.fileName, { type: assetData.type, }); clientSideAssets.set(assetData.fileName, { file: file, blobUrl: URL.createObjectURL(blob), }); } catch (error) { console.error(`Failed to restore asset ${assetData.fileName}:`, error); } } renderSelectedFiles(generateFileList); renderSelectedFiles(modifyFileList); } // --- Event Handlers --- loadSessionBtn.addEventListener("click", () => { populateSessionDropdown(); openPopup(loadSessionPopup); }); saveSessionBtn.addEventListener("click", () => { openPopup(saveSessionPopup); }); generateFileInput.addEventListener("change", (e) => handleFileSelection(e, generateFileList) ); modifyFileInput.addEventListener("change", (e) => handleFileSelection(e, modifyFileList) ); generateBtn.addEventListener("click", async () => { const prompt = promptInput.value.trim(); if (!prompt) { showNotification("Please enter a prompt", "error"); return; } hasGeneratedContent = true; updateLayoutState(); versionHistory = []; promptHistory = []; currentSessionId = null; sessionNameInput.value = prompt.substring(0, 50); const formData = new FormData(); formData.append("prompt", prompt); clientSideAssets.forEach((asset) => formData.append("files", asset.file, asset.file.name) ); streamResponse("/generate", formData); if (window.innerWidth <= 768) closeSidebar(); }); modifyBtn.addEventListener("click", async () => { const modification = modificationInput.value.trim(); if (!modification) { showNotification("Please enter a modification request", "error"); return; } hasGeneratedContent = true; updateLayoutState(); const currentHtml = monacoEditor ? monacoEditor.getValue() : ""; if (!currentHtml) return alert("There is no code to modify!"); const formData = new FormData(); formData.append("prompt", modification); formData.append("current_code", currentHtml); formData.append("console_logs", consoleLogs.join("\n")); promptHistory.forEach((p) => formData.append("prompt_history", p)); clientSideAssets.forEach((asset) => formData.append("files", asset.file, asset.file.name) ); streamResponse("/modify", formData); if (window.innerWidth <= 768) closeSidebar(); }); async function streamResponseMarkdown(url, body, targetElement) { setLoading(true, url); if (abortController) abortController.abort(); abortController = new AbortController(); let accumulatedText = ""; try { const isFormData = body instanceof FormData; const response = await fetch(url, { method: "POST", body: isFormData ? body : JSON.stringify(body), headers: isFormData ? {} : { "Content-Type": "application/json" }, signal: abortController.signal, }); if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); const reader = response.body.getReader(); const decoder = new TextDecoder(); // Show loading indicator while streaming targetElement.innerHTML = '