|
console.log("Script execution started."); |
|
|
|
let generatedCode = []; |
|
let currentCleanedCode = []; |
|
|
|
let evolutionTimeline = []; |
|
let activeTimelineIndex = -1; |
|
let selectedVariationGridIndex = -1; |
|
let currentFullscreenHistoryIndex = -1; |
|
|
|
let originalUserPromptForCurrentGeneration = ''; |
|
let lastPreviewUpdateTime = []; |
|
let previewUpdateInterval = 500; |
|
let numVariationsToGenerate = 4; |
|
let activeApiControllers = []; |
|
|
|
let lastGenerationConfig = { |
|
prompt: '', |
|
isRefinement: false, |
|
numVariations: 4, |
|
refinedTimelineIndex: -1 |
|
}; |
|
|
|
|
|
|
|
let apiKeyEl, codeOutputEl, errorMessageEl; |
|
let modelSelEl; |
|
let selectButtons = []; |
|
let fullscreenButtons = []; |
|
let previewItems = []; |
|
|
|
let refinementLoadingIndicator; |
|
let mainContentEl, configButtonEl; |
|
let intervalSliderEl, intervalValueDisplayEl; |
|
|
|
let fullscreenOverlayEl, fullscreenIframeEl, exitFullscreenBtnEl; |
|
let perspectiveViewportEl, previewGridWrapperEl; |
|
|
|
let historyPanelEl, historyPanelPlaceholderEl; |
|
let selectedCodeTitleH3El; |
|
let mainContentTitleH1El, mainContentSubtitleH2El; |
|
let fullscreenHistoryNavEl, historyNavPrevBtnEl, historyNavNextBtnEl; |
|
let promptModalOverlayEl, promptModalContentEl, modalUserPromptEl, modalGenerateBtnEl, modalCancelBtnEl, modalLoadingIndicatorEl; |
|
let modalRefinementCheckboxEl, numVariationsSliderEl, numVariationsValueDisplayEl; |
|
let configModalOverlayEl, configModalContentEl, configModalCloseBtnEl, copyCodeButtonEl; |
|
let historyToggleButtonEl, historyArrowDownEl, historyArrowUpEl; |
|
let exportCodeButtonEl; |
|
let newButtonEl; |
|
let confirmModalOverlayEl, confirmModalMessageEl, confirmModalConfirmBtnEl, confirmModalCancelBtnEl; |
|
let currentConfirmCallback = null; |
|
let historyNavLeftBtnEl, historyNavRightBtnEl; |
|
let promptDisplayModalOverlayEl, promptDisplayModalContentEl, fullPromptTextEl, promptDisplayModalCloseBtnEl; |
|
let showPromptModalButtonEl; |
|
let modalContextSizeValueEl, modalMaxTokensValueEl, modalMaxTokensSliderEl; |
|
|
|
|
|
|
|
const API_BASE_URL = 'https://api.novita.ai/v3/openai'; |
|
|
|
const MODEL_DETAILS = { |
|
"deepseek/deepseek-v3-turbo": { context: 64000, maxOutput: 16000, defaultOutput: 8192 }, |
|
"deepseek/deepseek-r1-turbo": { context: 64000, maxOutput: 16000, defaultOutput: 8192 }, |
|
"qwen/qwen3-235b-a22b-fp8": { context: 128000, maxOutput: 128000, defaultOutput: 16384 }, |
|
"qwen/qwen3-30b-a3b-fp8": { context: 128000, maxOutput: 128000, defaultOutput: 16384 }, |
|
"qwen/qwen3-32b-fp8": { context: 128000, maxOutput: 128000, defaultOutput: 16384 }, |
|
"google/gemma-3-27b-it": { context: 32000, maxOutput: 32000, defaultOutput: 8192 }, |
|
"mistralai/mistral-nemo": { context: 131072, maxOutput: 131072, defaultOutput: 16384 }, |
|
"meta-llama/llama-4-scout-17b-16e-instruct": { context: 131072, maxOutput: 131072, defaultOutput: 16384 }, |
|
"meta-llama/llama-4-maverick-17b-128e-instruct-fp8": { context: 1048576, maxOutput: 1048576, defaultOutput: 16384 } |
|
}; |
|
|
|
|
|
|
|
async function processStreamForVariation(apiKey, userPrompt, variationIndex, modelName, signal) { |
|
console.log(`[Variation ${variationIndex + 1}] Starting generation with model ${modelName}...`); |
|
const previewFrame = document.getElementById(`preview-frame-${variationIndex + 1}`); |
|
const loader = document.getElementById(`preview-loader-${variationIndex + 1}`); |
|
const selectBtn = selectButtons[variationIndex]; |
|
const fullscreenBtn = fullscreenButtons[variationIndex]; |
|
|
|
if (!previewFrame) { console.error(`[Variation ${variationIndex + 1}] Preview frame missing.`); return false; } |
|
if (!loader) { console.warn(`[Variation ${variationIndex + 1}] Loader missing.`); } |
|
|
|
|
|
if (loader) loader.classList.remove('hidden'); |
|
if (selectBtn) selectBtn.disabled = true; |
|
if (fullscreenBtn) fullscreenBtn.disabled = true; |
|
|
|
if (previewFrame) updateLivePreviewInGrid(variationIndex, '<div class="flex items-center justify-center h-full"><p class="text-slate-500">Generating...</p></div>', false); |
|
if(lastPreviewUpdateTime[variationIndex] !== undefined) lastPreviewUpdateTime[variationIndex] = 0; |
|
|
|
|
|
const API_ENDPOINT = `${API_BASE_URL}/chat/completions`; |
|
let rawGeneratedCode = ''; |
|
let htmlBlockStarted = false; |
|
let success = false; |
|
let isInsideThinkBlock = false; |
|
|
|
|
|
|
|
const conversationHistory = [ |
|
{ role: "user", content: userPrompt } |
|
]; |
|
|
|
try { |
|
console.log(`[Variation ${variationIndex + 1}] Novita API call with model: ${modelName}`); |
|
const response = await fetch(API_ENDPOINT, { |
|
method: 'POST', |
|
headers: { |
|
'Content-Type': 'application/json', |
|
'Authorization': `Bearer ${apiKey}` |
|
}, |
|
body: JSON.stringify({ |
|
model: modelName, |
|
messages: conversationHistory, |
|
stream: true, |
|
reasoning: { exclude: true }, |
|
max_tokens: parseInt(modalMaxTokensSliderEl.value, 10) |
|
}), |
|
signal: signal |
|
}); |
|
|
|
if (!response.ok) { |
|
let errorDetails = `API Error: ${response.status} ${response.statusText}`; |
|
try { |
|
const errorDataText = await response.text(); |
|
console.error(`[Variation ${variationIndex + 1}] Novita API Error Response Text:`, errorDataText); |
|
const errorData = JSON.parse(errorDataText); |
|
errorDetails += ` - ${errorData?.error?.message || errorData?.detail || errorDataText || 'No specific message.'}`; |
|
} catch (e) { |
|
errorDetails += ` - Could not parse error response.`; |
|
} |
|
throw new Error(errorDetails); |
|
} |
|
|
|
const reader = response.body.getReader(); |
|
const decoder = new TextDecoder(); |
|
let sseBuffer = ""; |
|
let streamEnded = false; |
|
|
|
while (!streamEnded) { |
|
const { done, value } = await reader.read(); |
|
if (done) { |
|
streamEnded = true; |
|
break; |
|
} |
|
|
|
sseBuffer += decoder.decode(value, { stream: true }); |
|
let eolIndex; |
|
while ((eolIndex = sseBuffer.indexOf('\n')) >= 0) { |
|
const line = sseBuffer.substring(0, eolIndex).trim(); |
|
sseBuffer = sseBuffer.substring(eolIndex + 1); |
|
|
|
if (line.startsWith('data: ')) { |
|
const data = line.substring(6).trim(); |
|
if (data === '[DONE]') { |
|
streamEnded = true; |
|
break; |
|
} |
|
try { |
|
const json = JSON.parse(data); |
|
const deltaContent = json.choices?.[0]?.delta?.content; |
|
|
|
if (deltaContent) { |
|
let currentChunk = deltaContent; |
|
let processedChunkForThisDelta = ''; |
|
|
|
while (currentChunk.length > 0) { |
|
if (isInsideThinkBlock) { |
|
const endThinkTagIndex = currentChunk.indexOf('</think>'); |
|
if (endThinkTagIndex !== -1) { |
|
|
|
processedChunkForThisDelta += currentChunk.substring(endThinkTagIndex + '</think>'.length); |
|
isInsideThinkBlock = false; |
|
currentChunk = ''; |
|
} else { |
|
|
|
currentChunk = ''; |
|
} |
|
} else { |
|
const startThinkTagIndex = currentChunk.indexOf('<think>'); |
|
if (startThinkTagIndex !== -1) { |
|
|
|
processedChunkForThisDelta += currentChunk.substring(0, startThinkTagIndex); |
|
isInsideThinkBlock = true; |
|
|
|
currentChunk = currentChunk.substring(startThinkTagIndex + '<think>'.length); |
|
|
|
|
|
} else { |
|
|
|
processedChunkForThisDelta += currentChunk; |
|
currentChunk = ''; |
|
} |
|
} |
|
} |
|
|
|
if (processedChunkForThisDelta.length > 0) { |
|
rawGeneratedCode += processedChunkForThisDelta; |
|
|
|
|
|
if (!htmlBlockStarted) { |
|
|
|
const marker = "```html"; |
|
let tempAccumulated = rawGeneratedCode; |
|
const markerIndex = tempAccumulated.indexOf(marker); |
|
if (markerIndex !== -1) { |
|
htmlBlockStarted = true; |
|
let codeStartIndex = markerIndex + marker.length; |
|
if (tempAccumulated[codeStartIndex] === '\n') { |
|
codeStartIndex++; |
|
} |
|
rawGeneratedCode = tempAccumulated.substring(codeStartIndex); |
|
} else if (tempAccumulated.trim().startsWith("<!DOCTYPE html") || tempAccumulated.trim().startsWith("<html")) { |
|
|
|
htmlBlockStarted = true; |
|
} |
|
} |
|
|
|
if (htmlBlockStarted || !(rawGeneratedCode.includes("```html"))) { |
|
const now = Date.now(); |
|
if (lastPreviewUpdateTime[variationIndex] !== undefined && (now - lastPreviewUpdateTime[variationIndex] >= previewUpdateInterval)) { |
|
|
|
let previewCode = rawGeneratedCode; |
|
if (previewCode.includes("```html")) { |
|
previewCode = previewCode.substring(previewCode.indexOf("```html") + 7).trimStart(); |
|
} |
|
if (previewCode.endsWith("```")) { |
|
previewCode = previewCode.substring(0, previewCode.length - 3).trimEnd(); |
|
} |
|
updateLivePreviewInGrid(variationIndex, previewCode, true); |
|
lastPreviewUpdateTime[variationIndex] = now; |
|
} |
|
} |
|
} |
|
} |
|
} catch (e) { |
|
console.warn(`[Var ${variationIndex + 1}] JSON parse error in stream:`, e, "Problematic Line:", line, "Data:", data); |
|
} |
|
} |
|
} |
|
if (streamEnded) break; |
|
} |
|
|
|
console.log(`[Variation ${variationIndex + 1}] Streaming finished. Raw content length: ${rawGeneratedCode.length}`); |
|
let finalExtractedHtml = null; |
|
let processingErrorMessage = null; |
|
|
|
|
|
|
|
let tempHtml = rawGeneratedCode.trim(); |
|
const htmlMarker = "```html"; |
|
const markerIndex = tempHtml.indexOf(htmlMarker); |
|
|
|
if (markerIndex !== -1) { |
|
htmlBlockStarted = true; |
|
tempHtml = tempHtml.substring(markerIndex + htmlMarker.length).trimStart(); |
|
} |
|
|
|
if (tempHtml.endsWith("```")) { |
|
tempHtml = tempHtml.substring(0, tempHtml.length - 3).trimEnd(); |
|
} |
|
|
|
|
|
if (!htmlBlockStarted && (tempHtml.startsWith("<!DOCTYPE") || tempHtml.startsWith("<html"))) { |
|
htmlBlockStarted = true; |
|
} |
|
|
|
if (htmlBlockStarted && tempHtml.length > 0) { |
|
finalExtractedHtml = tempHtml; |
|
const minLength = 20; |
|
const hasStructuralTag = /<!DOCTYPE html|<html[^>]*>|<head[^>]*>|<body[^>]*>/i.test(finalExtractedHtml); |
|
const hasAnyTag = /<[a-zA-Z][^>]*>/.test(finalExtractedHtml); |
|
|
|
if (finalExtractedHtml.length >= minLength && (hasStructuralTag || hasAnyTag)) { |
|
success = true; |
|
console.log(`[Variation ${variationIndex + 1}] HTML validation SUCCEEDED.`); |
|
} else { |
|
success = false; |
|
console.warn("[Variation " + (variationIndex + 1) + "] HTML validation FAILED (length: " + finalExtractedHtml.length + ", structural: " + hasStructuralTag + ", anyTag: " + hasAnyTag + "). Review content manually."); |
|
processingErrorMessage = "// Warning: Generated content did not pass basic HTML validation."; |
|
if (!finalExtractedHtml) finalExtractedHtml = processingErrorMessage; |
|
} |
|
} else if (tempHtml.length > 0 && !htmlBlockStarted) { |
|
processingErrorMessage = `// Warning: Content received, but 'Written by Novita AI' or similar marker not found, or content doesn't look like HTML. Length: ${tempHtml.length}`; |
|
console.warn(`[Variation ${variationIndex + 1}] ${processingErrorMessage}`); |
|
finalExtractedHtml = tempHtml; |
|
success = false; |
|
} else { |
|
processingErrorMessage = `// Error: No meaningful content generated or 'Written by Novita AI' marker not found.`; |
|
console.warn(`[Variation ${variationIndex + 1}] ${processingErrorMessage}`); |
|
finalExtractedHtml = processingErrorMessage; |
|
success = false; |
|
} |
|
|
|
if (success && finalExtractedHtml) { |
|
let processedHtml = finalExtractedHtml; |
|
const requiredHeadContent = ` |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<script src="https://cdn.tailwindcss.com"><\\/script> |
|
<link rel="preconnect" href="https://rsms.me/"> |
|
<link rel="stylesheet" href="https://rsms.me/inter/inter.css"> |
|
<style> |
|
html { font-family: 'Inter', sans-serif; } |
|
@supports (font-variation-settings: normal) { |
|
html { font-family: 'Inter var', sans-serif; } |
|
} |
|
body { |
|
background-color: #f8fafc; /* slate-50 */ |
|
color: #0f172a; /* slate-900 */ |
|
padding: 1rem; |
|
} |
|
</style>`; |
|
|
|
if (!processedHtml.includes('<head>')) { |
|
processedHtml = `<head>${requiredHeadContent}</head><body>${processedHtml}</body>`; |
|
} else { |
|
let tempRequired = ""; |
|
if (!processedHtml.includes('cdn.tailwindcss.com')) tempRequired += ` <script src="https://cdn.tailwindcss.com"><\\/script>\\n`; |
|
if (!processedHtml.includes('inter.css')) tempRequired += ` <link rel="preconnect" href="https://rsms.me/">\\n <link rel="stylesheet" href="https://rsms.me/inter/inter.css">\\n`; |
|
if (!processedHtml.includes("html { font-family: 'Inter', sans-serif; }")) { |
|
tempRequired += ` <style>\\n html { font-family: 'Inter', sans-serif; }\\n @supports (font-variation-settings: normal) { html { font-family: 'Inter var', sans-serif; } }\\n body { background-color: #f8fafc; color: #0f172a; padding: 1rem; }\\n </style>\\n`; |
|
} |
|
if (tempRequired) { |
|
processedHtml = processedHtml.replace(/<\/head>/i, `${tempRequired}</head>`); |
|
} |
|
} |
|
if(currentCleanedCode[variationIndex] !== undefined) currentCleanedCode[variationIndex] = processedHtml; |
|
|
|
const interactionScript = `<script>(function() { const VARIATION_INDEX = ${variationIndex}; })(); <\\/script>`; |
|
const bodyEndIndex = processedHtml.lastIndexOf('</body>'); |
|
if(generatedCode[variationIndex] !== undefined) { |
|
generatedCode[variationIndex] = (bodyEndIndex !== -1) |
|
? processedHtml.slice(0, bodyEndIndex) + interactionScript + processedHtml.slice(bodyEndIndex) |
|
: processedHtml + interactionScript; |
|
} |
|
updateLivePreviewInGrid(variationIndex, null, true); |
|
} else { |
|
generatedCode[variationIndex] = ''; |
|
if (finalExtractedHtml) { |
|
if(currentCleanedCode[variationIndex] !== undefined) currentCleanedCode[variationIndex] = finalExtractedHtml; |
|
updateLivePreviewInGrid(variationIndex, finalExtractedHtml, false); |
|
} else { |
|
const displayError = processingErrorMessage || "// Unknown error during generation."; |
|
if(currentCleanedCode[variationIndex] !== undefined) currentCleanedCode[variationIndex] = displayError; |
|
updateLivePreviewInGrid(variationIndex, `<div class="p-4 text-red-400 font-medium">${displayError.replace(/\\n/g, '<br>')}</div>`, false); |
|
} |
|
} |
|
|
|
} catch (error) { |
|
if (error.name === 'AbortError') { |
|
console.log(`[Variation ${variationIndex + 1}] Fetch aborted.`); |
|
updateLivePreviewInGrid(variationIndex, `<div class="p-4 text-yellow-400 font-medium">Generation Cancelled.</div>`, false); |
|
} else { |
|
console.error(`[Variation ${variationIndex + 1}] Generation error:`, error); |
|
updateLivePreviewInGrid(variationIndex, `<div class="p-4 text-red-400 font-medium">Error: ${error.message}</div>`, false); |
|
} |
|
if(currentCleanedCode[variationIndex] !== undefined) currentCleanedCode[variationIndex] = `// Error: ${error.message}`; |
|
if(generatedCode[variationIndex] !== undefined) generatedCode[variationIndex] = ''; |
|
success = false; |
|
} finally { |
|
const finalLoader = document.getElementById(`preview-loader-${variationIndex + 1}`); |
|
if (finalLoader) finalLoader.classList.add('hidden'); |
|
if (selectBtn) selectBtn.disabled = !success; |
|
if (fullscreenBtn) fullscreenBtn.disabled = !success; |
|
} |
|
return success; |
|
} |
|
|
|
|
|
async function generateVariations() { |
|
console.log("generateVariations called."); |
|
const userPromptText = modalUserPromptEl.value.trim(); |
|
|
|
if (!apiKeyEl || !modalUserPromptEl || !modelSelEl || !previewGridWrapperEl || !modalGenerateBtnEl || !modalLoadingIndicatorEl || !errorMessageEl || !codeOutputEl) { |
|
console.error("Cannot generate variations: One or more critical elements are missing."); |
|
if (errorMessageEl) errorMessageEl.textContent = "Initialization error. Cannot generate."; |
|
return; |
|
} |
|
|
|
const apiKey = apiKeyEl.value.trim(); |
|
const selectedModel = modelSelEl.value; |
|
const currentIsRefinementMode = modalRefinementCheckboxEl.checked; |
|
const currentNumVariations = parseInt(numVariationsSliderEl.value, 10); |
|
|
|
if (!apiKey || !userPromptText) { |
|
errorMessageEl.textContent = 'Error: API Key and Prompt (via Alt+P) are required.'; |
|
if (!userPromptText && !promptModalOverlayEl.classList.contains('visible')) showPromptModal(); |
|
return; |
|
} |
|
if (!selectedModel) { |
|
errorMessageEl.textContent = 'Error: Please select a model.'; |
|
return; |
|
} |
|
|
|
lastGenerationConfig = { |
|
prompt: userPromptText, |
|
isRefinement: currentIsRefinementMode, |
|
numVariations: currentNumVariations, |
|
refinedTimelineIndex: currentIsRefinementMode ? activeTimelineIndex : -1 |
|
}; |
|
|
|
|
|
errorMessageEl.textContent = ''; |
|
console.log(`Mode: ${currentIsRefinementMode ? 'Refinement' : 'Initial'}, Model: ${selectedModel}, Variations: ${currentNumVariations}`); |
|
|
|
let baseCodeForRefinement = null; |
|
let contextPromptForRefinement = ''; |
|
originalUserPromptForCurrentGeneration = userPromptText; |
|
|
|
if (currentIsRefinementMode && activeTimelineIndex !== -1 && evolutionTimeline[activeTimelineIndex]) { |
|
baseCodeForRefinement = evolutionTimeline[activeTimelineIndex].code; |
|
contextPromptForRefinement = evolutionTimeline[activeTimelineIndex].originalUserPrompt; |
|
console.log(`Refining Evolution ${activeTimelineIndex + 1}. Original context: "${contextPromptForRefinement}"`); |
|
} else if (currentIsRefinementMode) { |
|
errorMessageEl.textContent = 'Error: No active evolution selected to refine. Uncheck "refine" or select an evolution from history.'; |
|
return; |
|
} |
|
|
|
modalGenerateBtnEl.disabled = true; |
|
modalLoadingIndicatorEl.classList.remove('hidden'); |
|
if (codeOutputEl && selectedCodeTitleH3El) { |
|
codeOutputEl.innerHTML = '<code class="language-html">// Select a variation to view its code.</code>'; |
|
selectedCodeTitleH3El.textContent = "Selected Code:"; |
|
} |
|
selectedVariationGridIndex = -1; |
|
|
|
numVariationsToGenerate = currentNumVariations; |
|
generatedCode = Array(numVariationsToGenerate).fill(''); |
|
currentCleanedCode = Array(numVariationsToGenerate).fill(''); |
|
lastPreviewUpdateTime = Array(numVariationsToGenerate).fill(0); |
|
selectButtons = Array(numVariationsToGenerate).fill(null); |
|
fullscreenButtons = Array(numVariationsToGenerate).fill(null); |
|
previewItems = Array(numVariationsToGenerate).fill(null); |
|
|
|
|
|
showFourGridPreviewUI(); |
|
|
|
activeApiControllers = Array(numVariationsToGenerate).fill(null).map(() => new AbortController()); |
|
|
|
|
|
const promptsForNovita = Array(numVariationsToGenerate).fill(null).map((_, i) => { |
|
if (currentIsRefinementMode && baseCodeForRefinement) { |
|
|
|
|
|
return ` |
|
CONTEXT: You are an expert web developer specializing in HTML, Tailwind CSS, and JavaScript. |
|
Original User Request (for context of the base code): "${contextPromptForRefinement}" |
|
Base HTML Code to Refine: |
|
\`\`\`html |
|
${baseCodeForRefinement} |
|
\`\`\` |
|
Your Task: Implement the following refinement instructions for Variation ${i + 1} of ${numVariationsToGenerate}. |
|
User's Refinement Instructions: "${userPromptText}" |
|
|
|
Instructions for your output: |
|
1. Analyze the Base Code and the User's Refinement Instructions. |
|
2. Modify ONLY the necessary parts of the Base Code to implement the refinement. |
|
3. Try a slightly different approach if possible, as this is one of several variations. |
|
4. Ensure the refined code remains a single, complete, runnable HTML document. |
|
5. ALWAYS include Tailwind CSS CDN (<script src="https://cdn.tailwindcss.com"><\\/script>) and Inter font (<link rel="stylesheet" href="https://rsms.me/inter/inter.css">) in the <head>. |
|
6. Add basic body styling for readability: <style>html { font-family: 'Inter', sans-serif; } body { background-color: #f8fafc; color: #0f172a; padding: 1rem; }</style> |
|
7. Output ONLY the raw, complete, refined HTML code. Start your response with \`\`\`html and end with \`\`\` |
|
Refined Code:`; |
|
} else { |
|
return ` |
|
CONTEXT: You are an expert web developer specializing in clean, modern HTML, CSS (using Tailwind CSS classes), and JavaScript. |
|
Your Task: Generate the complete, runnable HTML code for Variation ${i + 1} of ${numVariationsToGenerate} based on the User Request below. |
|
|
|
Instructions for your output: |
|
1. Ensure the code is self-contained. |
|
2. ALWAYS include Tailwind CSS CDN (<script src="https://cdn.tailwindcss.com"><\\/script>) in the <head>. |
|
3. ALWAYS include Inter font (<link rel="stylesheet" href="https://rsms.me/inter/inter.css">) and set html { font-family: 'Inter', sans-serif; } in a <style> tag in the <head>. |
|
4. Add basic body styling for readability: body { background-color: #f8fafc; color: #0f172a; padding: 1rem; } in the same <style> tag. |
|
5. Add HTML comments to explain the code if helpful. |
|
6. Try to make this variation distinct. |
|
7. Output ONLY the raw HTML code. Start your response with \`\`\`html and end with \`\`\` |
|
|
|
User Request: |
|
"${originalUserPromptForCurrentGeneration}" |
|
Code:`; |
|
} |
|
}); |
|
|
|
const generationPromises = promptsForNovita.map((promptContent, index) => |
|
processStreamForVariation(apiKey, promptContent, index, selectedModel, activeApiControllers[index].signal) |
|
); |
|
|
|
try { |
|
const results = await Promise.allSettled(generationPromises); |
|
console.log("Generation promises settled:", results); |
|
const successfulGenerations = results.filter(r => r.status === 'fulfilled' && r.value === true).length; |
|
if (successfulGenerations === 0 && !results.some(r => r.status === 'rejected' && r.reason.name === 'AbortError')) { |
|
errorMessageElement.textContent = 'Error: All variations failed.'; |
|
} |
|
else if (successfulGenerations < numVariationsToGenerate && !results.every(r => r.status === 'rejected' && r.reason.name === 'AbortError')) { |
|
errorMessageElement.textContent = `Warning: ${numVariationsToGenerate - successfulGenerations} var(s) failed or were cancelled.`; |
|
} |
|
|
|
updateMainContentTitles("Select a Variation", "Click 'Select' on a preview below."); |
|
|
|
} catch (error) { |
|
console.error("Parallel generation error:", error); |
|
errorMessageElement.textContent = `Unexpected Error: ${error.message}`; |
|
showInitialPreviewStateUI(); |
|
} finally { |
|
modalGenerateBtnEl.disabled = false; |
|
modalLoadingIndicatorEl.classList.add('hidden'); |
|
console.log("Generation process finished."); |
|
} |
|
} |
|
|
|
|
|
function createPreviewItemDOM(index, isGridItem = true) { |
|
const item = document.createElement('div'); |
|
item.id = `preview-item-${index + 1}`; |
|
item.className = isGridItem ? 'preview-item-perspective' : 'single-preview-item'; |
|
if (isGridItem) item.dataset.variationGridIndex = index; |
|
|
|
const header = document.createElement('div'); header.className = 'preview-header'; |
|
const title = document.createElement('span'); title.className = 'preview-header-title'; |
|
title.textContent = isGridItem ? `Variation ${index + 1}` : (evolutionTimeline[activeTimelineIndex]?.prompt.substring(0,30) + '...' || `Evolution ${activeTimelineIndex + 1}`); |
|
header.appendChild(title); |
|
|
|
const btns = document.createElement('div'); btns.className = 'preview-header-buttons'; |
|
|
|
const fsBtn = document.createElement('button'); |
|
fsBtn.className = 'fullscreen-btn p-1 focus:outline-none focus:ring-1 focus:ring-cyan-500 rounded disabled:opacity-50'; |
|
fsBtn.dataset.idx = index; |
|
fsBtn.title = 'Full Screen'; fsBtn.disabled = true; |
|
fsBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/></svg>`; |
|
fsBtn.addEventListener('click', (e) => { |
|
const idxToFullscreen = isGridItem ? parseInt(e.currentTarget.dataset.idx) : activeTimelineIndex; |
|
const fromHistory = !isGridItem; |
|
if (!isNaN(idxToFullscreen) && (isGridItem ? generatedCode[idxToFullscreen] : evolutionTimeline[idxToFullscreen]?.code)) { |
|
enterFullscreen(idxToFullscreen, fromHistory); |
|
} |
|
}); |
|
|
|
btns.appendChild(fsBtn); |
|
|
|
if (isGridItem) { |
|
const selBtn = document.createElement('button'); |
|
selBtn.className = 'select-variation-btn futuristic-button px-3 py-1 text-xs'; |
|
selBtn.dataset.variationGridIndex = index; selBtn.disabled = true; selBtn.textContent = 'Select'; |
|
selBtn.addEventListener('click', handleSelectVariationFromGrid); |
|
selectButtons[index] = selBtn; |
|
btns.appendChild(selBtn); |
|
} |
|
|
|
header.appendChild(btns); |
|
item.appendChild(header); |
|
|
|
const bodyEl = document.createElement('div'); bodyEl.className = 'preview-body'; |
|
const loaderDiv = document.createElement('div'); loaderDiv.id = `preview-loader-${index + 1}`; |
|
loaderDiv.className = 'preview-loader ' + (isGridItem ? 'hidden' : ''); |
|
loaderDiv.innerHTML = '<div class="spinner"></div>'; |
|
bodyEl.appendChild(loaderDiv); |
|
|
|
const iframeEl = document.createElement('iframe'); |
|
iframeEl.id = isGridItem ? `preview-frame-${index + 1}` : 'single-large-preview-frame'; |
|
iframeEl.title = `Preview ${index + 1}`; iframeEl.className = 'preview-frame'; |
|
iframeEl.srcdoc = '<div class="flex items-center justify-center h-full"><p class="text-slate-400">Preparing...</p></div>'; |
|
bodyEl.appendChild(iframeEl); |
|
item.appendChild(bodyEl); |
|
|
|
if (isGridItem) { |
|
previewItems[index] = item; |
|
fullscreenButtons[index] = fsBtn; |
|
} |
|
return item; |
|
} |
|
|
|
|
|
function showFourGridPreviewUI() { |
|
previewGridWrapperEl.innerHTML = ''; |
|
if (numVariationsToGenerate === 1) { |
|
previewGridWrapperEl.className = 'single-mode'; |
|
if(perspectiveViewportEl) perspectiveViewportEl.style.perspective = 'none'; |
|
const item = createPreviewItemDOM(0, true); |
|
previewGridWrapperEl.appendChild(item); |
|
|
|
} else if (numVariationsToGenerate === 2) { |
|
previewGridWrapperEl.className = 'grid grid-cols-2 grid-rows-1 gap-6'; |
|
if(perspectiveViewportEl) perspectiveViewportEl.style.perspective = '1500px'; |
|
for (let i = 0; i < numVariationsToGenerate; i++) { |
|
const item = createPreviewItemDOM(i, true); |
|
previewGridWrapperEl.appendChild(item); |
|
} |
|
} else { |
|
previewGridWrapperEl.className = 'grid-mode'; |
|
if(perspectiveViewportEl) perspectiveViewportEl.style.perspective = '1500px'; |
|
for (let i = 0; i < numVariationsToGenerate; i++) { |
|
const item = createPreviewItemDOM(i, true); |
|
previewGridWrapperEl.appendChild(item); |
|
} |
|
} |
|
updateSelectedGridItemUI(); |
|
} |
|
|
|
function showSingleLargePreviewUI(htmlContent, titleText, fullPromptText) { |
|
previewGridWrapperEl.innerHTML = ''; |
|
previewGridWrapperEl.className = 'single-mode'; |
|
if(perspectiveViewportEl) perspectiveViewportEl.style.perspective = 'none'; |
|
|
|
const item = document.createElement('div'); |
|
item.className = 'single-preview-item'; |
|
|
|
const bodyEl = document.createElement('div'); |
|
bodyEl.className = 'preview-body'; |
|
|
|
const iframeEl = document.createElement('iframe'); |
|
iframeEl.id = `single-large-preview-frame`; |
|
iframeEl.title = titleText; |
|
iframeEl.className = 'preview-frame'; |
|
iframeEl.srcdoc = htmlContent; |
|
|
|
bodyEl.appendChild(iframeEl); |
|
item.appendChild(bodyEl); |
|
previewGridWrapperEl.appendChild(item); |
|
|
|
|
|
const maxPromptLength = 50; |
|
let displaySubtitle = fullPromptText; |
|
mainContentSubtitleH2El.classList.remove('prompt-truncated'); |
|
delete mainContentSubtitleH2El.dataset.fullPrompt; |
|
|
|
if (fullPromptText && fullPromptText.length > maxPromptLength) { |
|
displaySubtitle = fullPromptText.substring(0, maxPromptLength) + "... (click to view full)"; |
|
mainContentSubtitleH2El.classList.add('prompt-truncated'); |
|
mainContentSubtitleH2El.dataset.fullPrompt = fullPromptText; |
|
} |
|
|
|
updateMainContentTitles(titleText, displaySubtitle); |
|
} |
|
|
|
function showInitialPreviewStateUI() { |
|
previewGridWrapperEl.innerHTML = ''; |
|
|
|
const initialViewContainer = document.createElement('div'); |
|
initialViewContainer.id = 'initial-view-content'; |
|
initialViewContainer.className = 'flex flex-col items-center justify-center text-slate-300 text-lg p-8 space-y-4 h-full initial-view-animate'; |
|
|
|
|
|
const logoContainer = document.createElement('div'); |
|
logoContainer.className = 'w-40 mb-2 novita-logo-container'; |
|
initialViewContainer.appendChild(logoContainer); |
|
|
|
fetch('https://novita.ai/logo/logo.svg') |
|
.then(response => { |
|
if (!response.ok) { |
|
throw new Error(`HTTP error! status: ${response.status}`); |
|
} |
|
return response.text(); |
|
}) |
|
.then(svgText => { |
|
logoContainer.innerHTML = svgText; |
|
const svgElement = logoContainer.querySelector('svg'); |
|
if (svgElement) { |
|
svgElement.classList.add('novita-logo-svg-themed'); |
|
|
|
svgElement.setAttribute('width', '100%'); |
|
svgElement.setAttribute('height', '100%'); |
|
} else { |
|
console.warn('SVG element not found after fetching Novita logo.'); |
|
|
|
logoContainer.innerHTML = '<img src="https://novita.ai/logo/logo.svg" alt="Novita.AI Logo" class="w-full h-full" />'; |
|
} |
|
}) |
|
.catch(error => { |
|
console.error('Error fetching Novita.AI logo:', error); |
|
|
|
logoContainer.innerHTML = '<img src="https://novita.ai/logo/logo.svg" alt="Novita.AI Logo" class="w-full h-full" />'; |
|
}); |
|
|
|
|
|
const apiKeySection = document.createElement('div'); |
|
apiKeySection.className = 'w-full max-w-md text-center'; |
|
|
|
const apiKeyInput = document.createElement('input'); |
|
apiKeyInput.type = 'password'; |
|
apiKeyInput.id = 'initial-api-key-input'; |
|
apiKeyInput.className = 'futuristic-input w-full text-center'; |
|
apiKeyInput.placeholder = 'Paste Novita.AI API Key here'; |
|
if (apiKeyEl && apiKeyEl.value) { |
|
apiKeyInput.value = apiKeyEl.value; |
|
} |
|
apiKeyInput.addEventListener('input', (e) => { |
|
if (apiKeyEl) { |
|
apiKeyEl.value = e.target.value; |
|
} |
|
}); |
|
apiKeySection.appendChild(apiKeyInput); |
|
|
|
const apiKeyLink = document.createElement('p'); |
|
apiKeyLink.className = 'text-xs text-slate-400 mt-2'; |
|
apiKeyLink.innerHTML = 'Get your API Key from <a href="https://novita.ai/" target="_blank" class="text-cyan-400 hover:text-cyan-300 underline">Novita.AI</a>.'; |
|
apiKeySection.appendChild(apiKeyLink); |
|
initialViewContainer.appendChild(apiKeySection); |
|
|
|
|
|
const examplePromptsSection = document.createElement('div'); |
|
examplePromptsSection.className = 'w-full max-w-lg text-center pt-4'; |
|
|
|
const examplePromptsContainer = document.createElement('div'); |
|
examplePromptsContainer.id = 'example-prompts-container'; |
|
examplePromptsContainer.className = 'flex flex-wrap justify-center gap-3'; |
|
|
|
const examplePrompts = [ |
|
"A modern landing page for a new meditation app", |
|
"A portfolio website for a photographer", |
|
"A simple to-do list application", |
|
"An e-commerce product card for a smartwatch" |
|
]; |
|
|
|
examplePrompts.forEach(promptText => { |
|
const button = document.createElement('button'); |
|
button.className = 'example-prompt-button'; |
|
button.textContent = promptText; |
|
button.addEventListener('click', () => { |
|
if (modalUserPromptEl) { |
|
modalUserPromptEl.value = promptText; |
|
} |
|
|
|
if (apiKeyEl && !apiKeyEl.value && apiKeyInput.value) { |
|
apiKeyEl.value = apiKeyInput.value; |
|
} |
|
showPromptModal(); |
|
}); |
|
examplePromptsContainer.appendChild(button); |
|
}); |
|
examplePromptsSection.appendChild(examplePromptsContainer); |
|
initialViewContainer.appendChild(examplePromptsSection); |
|
|
|
|
|
const instructionText = document.createElement('p'); |
|
instructionText.className = 'text-sm text-slate-400 mt-6'; |
|
instructionText.innerHTML = 'Once you have an API key and a prompt (either typed or from an example),<br>click the <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-pencil inline-block align-middle -mt-1"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/><path d="m15 5 4 4"/></svg> icon or press Alt+P to open the prompt window and generate.'; |
|
initialViewContainer.appendChild(instructionText); |
|
|
|
previewGridWrapperEl.appendChild(initialViewContainer); |
|
|
|
previewGridWrapperEl.className = 'grid-mode initial-view-active'; |
|
if(perspectiveViewportEl) perspectiveViewportEl.style.perspective = 'none'; |
|
|
|
updateMainContentTitles("Welcome!", "Setup your API Key and enter a prompt to begin."); |
|
if (codeOutputEl) codeOutputEl.innerHTML = '<code class="language-html">// Code for the selected evolution will appear here.</code>'; |
|
if (selectedCodeTitleH3El) selectedCodeTitleH3El.textContent = "Selected Code:"; |
|
selectedVariationGridIndex = -1; |
|
if (mainContentSubtitleH2El) { |
|
mainContentSubtitleH2El.classList.remove('prompt-truncated'); |
|
delete mainContentSubtitleH2El.dataset.fullPrompt; |
|
} |
|
} |
|
|
|
function updateMainContentTitles(title, subtitle) { |
|
if (mainContentTitleH1El) mainContentTitleH1El.textContent = title; |
|
if (mainContentSubtitleH2El) mainContentSubtitleH2El.textContent = subtitle; |
|
} |
|
|
|
function updateSelectedGridItemUI() { |
|
previewItems.forEach((item, index) => { |
|
if (!item) return; |
|
item.classList.toggle('selected', index === selectedVariationGridIndex); |
|
}); |
|
selectButtons.forEach((button, index) => { |
|
if (!button) return; |
|
|
|
|
|
|
|
|
|
|
|
button.classList.remove('selected-state'); |
|
if (index === selectedVariationGridIndex) { |
|
button.textContent = 'Selected'; |
|
button.classList.add('selected-state'); |
|
} else { |
|
button.textContent = 'Select'; |
|
} |
|
}); |
|
} |
|
|
|
function updateLivePreviewInGrid(index, codeToRender = null, applyZoom = false) { |
|
let baseHtml = codeToRender !== null ? codeToRender : generatedCode[index]; |
|
const frame = document.getElementById(`preview-frame-${index + 1}`); |
|
if (!frame) { console.warn(`[updateLivePreviewInGrid][Var ${index + 1}] Frame not found.`); return; } |
|
|
|
if (typeof baseHtml !== 'string') { |
|
baseHtml = '<div class="p-4 text-orange-500">Invalid content received</div>'; |
|
applyZoom = false; |
|
} |
|
let finalHtml = baseHtml; |
|
try { |
|
if (applyZoom && numVariationsToGenerate > 1) { |
|
const scaleStyle = `<style>html { transform: scale(0.5); transform-origin: 0 0; width: 200%; height: 200%; overflow: auto !important; } body { overflow: visible !important; min-height: 100% !important; height: auto !important; width: auto !important; }</style>`; |
|
const headEndIndex = finalHtml.toLowerCase().lastIndexOf('</head>'); |
|
if (headEndIndex !== -1) { |
|
finalHtml = finalHtml.slice(0, headEndIndex) + scaleStyle + finalHtml.slice(headEndIndex); |
|
} else { |
|
finalHtml = scaleStyle + finalHtml; |
|
} |
|
} |
|
frame.srcdoc = finalHtml; |
|
} catch (e) { |
|
console.error(`[Var ${index + 1}] Error setting srcdoc for grid:`, e); |
|
try { |
|
frame.srcdoc = `<div class="p-4 text-red-500 font-semibold">Preview Render Error</div>`; |
|
} catch (finalError) { console.error("Failed to display error in grid iframe:", finalError); } |
|
} |
|
} |
|
|
|
|
|
function handleSelectVariationFromGrid(event) { |
|
const idx = parseInt(event.target.dataset.variationGridIndex, 10); |
|
|
|
if (isNaN(idx) || !currentCleanedCode[idx] || currentCleanedCode[idx].startsWith("// Error")) { |
|
console.warn(`Cannot select variation ${idx + 1}, code is invalid or generation failed.`); |
|
return; |
|
} |
|
|
|
activeApiControllers.forEach((controller, controllerIndex) => { |
|
if (controllerIndex !== idx && controller) { |
|
console.log(`Aborting request for variation ${controllerIndex + 1}`); |
|
controller.abort(); |
|
} |
|
}); |
|
activeApiControllers = []; |
|
|
|
|
|
selectedVariationGridIndex = idx; |
|
const displayCode = generatedCode[idx]; |
|
const storeCode = currentCleanedCode[idx]; |
|
|
|
let originalPromptForThisEvolution; |
|
let parentTimelineIdx = null; |
|
|
|
const wasThisGenerationARefinement = lastGenerationConfig.isRefinement; |
|
const refinedIndexForThisGen = lastGenerationConfig.refinedTimelineIndex; |
|
|
|
if (wasThisGenerationARefinement && refinedIndexForThisGen !== -1 && evolutionTimeline[refinedIndexForThisGen]) { |
|
originalPromptForThisEvolution = evolutionTimeline[refinedIndexForThisGen].originalUserPrompt; |
|
parentTimelineIdx = refinedIndexForThisGen; |
|
} else { |
|
originalPromptForThisEvolution = originalUserPromptForCurrentGeneration; |
|
} |
|
|
|
const newHistoryEntry = { |
|
id: 'evo-' + Date.now() + '-' + Math.random().toString(36).substr(2, 5), |
|
prompt: originalUserPromptForCurrentGeneration, |
|
originalUserPrompt: originalPromptForThisEvolution, |
|
code: storeCode, |
|
timestamp: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit'}), |
|
parentIndex: parentTimelineIdx |
|
}; |
|
evolutionTimeline.push(newHistoryEntry); |
|
activeTimelineIndex = evolutionTimeline.length - 1; |
|
|
|
console.log(`Variation ${idx + 1} selected from grid. Added to timeline as Evolution ${activeTimelineIndex + 1}.`); |
|
|
|
renderHistoryPanel(); |
|
showSingleLargePreviewUI(displayCode, `Evolution ${activeTimelineIndex + 1}: Active`, `Prompt: "${newHistoryEntry.prompt}"`); |
|
|
|
if (codeOutputEl) { |
|
codeOutputEl.textContent = storeCode; |
|
if (selectedCodeTitleH3El) selectedCodeTitleH3El.textContent = `Code (Evolution ${activeTimelineIndex + 1}):`; |
|
codeOutputEl.classList.remove('text-slate-200'); |
|
codeOutputEl.classList.add('text-slate-400'); |
|
if (codeOutputEl?.parentElement) { codeOutputEl.parentElement.scrollTop = 0; } |
|
} |
|
|
|
|
|
updateSelectedGridItemUI(); |
|
updateHistoryNavigationButtons(); |
|
|
|
if(modalUserPromptEl) modalUserPromptEl.value = ''; |
|
if(modalUserPromptEl) modalUserPromptEl.placeholder = `Refine Evolution ${activeTimelineIndex + 1}...`; |
|
if(modalRefinementCheckboxEl) modalRefinementCheckboxEl.checked = true; |
|
} |
|
|
|
function handleHistoryItemClick(timelineIdxToView) { |
|
if (timelineIdxToView < 0 || timelineIdxToView >= evolutionTimeline.length) { |
|
console.warn("Invalid history index clicked:", timelineIdxToView); |
|
return; |
|
} |
|
activeTimelineIndex = timelineIdxToView; |
|
const historyEntry = evolutionTimeline[activeTimelineIndex]; |
|
|
|
console.log(`History item ${activeTimelineIndex + 1} selected.`); |
|
|
|
const displayCodeForHistory = historyEntry.code; |
|
|
|
showSingleLargePreviewUI(displayCodeForHistory, `Evolution ${activeTimelineIndex + 1}: Active (Historical)`, `Prompt: "${historyEntry.prompt}"`); |
|
|
|
if (codeOutputEl) { |
|
codeOutputEl.textContent = historyEntry.code; |
|
if (selectedCodeTitleH3El) selectedCodeTitleH3El.textContent = `Code (Evolution ${activeTimelineIndex + 1} - Historical):`; |
|
codeOutputEl.classList.remove('text-slate-200'); |
|
codeOutputEl.classList.add('text-slate-400'); |
|
if (codeOutputEl?.parentElement) { codeOutputEl.parentElement.scrollTop = 0; } |
|
} |
|
|
|
renderHistoryPanel(); |
|
updateHistoryNavigationButtons(); |
|
|
|
if(modalUserPromptEl) modalUserPromptEl.value = ''; |
|
if(modalUserPromptEl) modalUserPromptEl.placeholder = `Refine Evolution ${activeTimelineIndex + 1}...`; |
|
if(modalRefinementCheckboxEl) modalRefinementCheckboxEl.checked = true; |
|
selectedVariationGridIndex = -1; |
|
updateSelectedGridItemUI(); |
|
} |
|
|
|
|
|
|
|
function createHistoryThumbnailDOM(entry, index) { |
|
const thumbItem = document.createElement('div'); |
|
thumbItem.className = 'history-thumbnail-item group'; |
|
thumbItem.dataset.timelineIndex = index; |
|
thumbItem.setAttribute('aria-label', `Evolution Step ${index + 1}: ${entry.prompt.substring(0, 30)}...`); |
|
|
|
|
|
|
|
const previewContainer = document.createElement('div'); |
|
previewContainer.className = 'history-thumbnail-preview-container'; |
|
previewContainer.title = `Click to view Evolution ${index + 1}`; |
|
previewContainer.addEventListener('click', () => handleHistoryItemClick(index)); |
|
|
|
const iframe = document.createElement('iframe'); |
|
iframe.className = 'history-thumbnail-preview'; |
|
iframe.title = `Preview of Evolution ${index + 1}`; |
|
const scaledContent = ` |
|
<style> |
|
html { transform: scale(0.25); transform-origin: 0 0; width: 400%; height: 400%; overflow: hidden !important; background-color: #fff; } |
|
body { width: 100%; height: 100%; overflow: hidden !important; padding: 0 !important; margin: 0 !important; } |
|
</style> |
|
${entry.code} |
|
`; |
|
iframe.srcdoc = scaledContent; |
|
previewContainer.appendChild(iframe); |
|
|
|
const titleEl = document.createElement('div'); |
|
titleEl.className = 'history-thumbnail-title'; |
|
titleEl.textContent = `Evo ${index + 1}: ${entry.prompt.substring(0, 20)}${entry.prompt.length > 20 ? '...' : ''}`; |
|
titleEl.title = `Prompt: ${entry.prompt}\\nClick to view Evolution ${index + 1}`; |
|
titleEl.addEventListener('click', () => handleHistoryItemClick(index)); |
|
|
|
const fsBtn = document.createElement('button'); |
|
fsBtn.className = 'history-thumbnail-fullscreen-btn'; |
|
fsBtn.title = 'View Fullscreen'; |
|
fsBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/></svg>`; |
|
fsBtn.addEventListener('click', (e) => { |
|
e.stopPropagation(); |
|
enterFullscreen(index, true); |
|
}); |
|
|
|
thumbItem.appendChild(previewContainer); |
|
thumbItem.appendChild(titleEl); |
|
thumbItem.appendChild(fsBtn); |
|
return thumbItem; |
|
} |
|
|
|
function renderHistoryPanel() { |
|
if (!historyPanelEl || !historyPanelPlaceholderEl) return; |
|
historyPanelEl.innerHTML = ''; |
|
|
|
if (evolutionTimeline.length === 0) { |
|
historyPanelEl.appendChild(historyPanelPlaceholderEl); |
|
historyPanelPlaceholderEl.classList.remove('hidden'); |
|
return; |
|
} |
|
|
|
historyPanelPlaceholderEl.classList.add('hidden'); |
|
|
|
const totalItems = evolutionTimeline.length; |
|
const middleIndex = Math.floor(totalItems / 2); |
|
|
|
const yOffsetFactor = 4; |
|
const zOffsetFactor = -15; |
|
const baseZIndex = 10; |
|
|
|
|
|
const activeYOffset = -15; |
|
const activeZOffset = 25; |
|
|
|
evolutionTimeline.forEach((entry, index) => { |
|
const thumbItem = createHistoryThumbnailDOM(entry, index); |
|
let finalTransform; |
|
let finalZIndex; |
|
|
|
if (index === activeTimelineIndex) { |
|
thumbItem.classList.add('active-history-item'); |
|
|
|
finalTransform = `translateY(${activeYOffset}px) translateZ(${activeZOffset}px) rotate(0deg)`; |
|
|
|
|
|
finalZIndex = 50; |
|
} else { |
|
|
|
const deltaFromMiddle = index - middleIndex; |
|
const rotation = 0; |
|
const translateY = Math.abs(deltaFromMiddle) * yOffsetFactor; |
|
const translateZ = Math.abs(deltaFromMiddle) * zOffsetFactor; |
|
|
|
finalTransform = `translateY(${translateY}px) translateZ(${translateZ}px) rotate(${rotation}deg)`; |
|
finalZIndex = baseZIndex - Math.abs(deltaFromMiddle); |
|
} |
|
|
|
thumbItem.style.zIndex = `${finalZIndex}`; |
|
thumbItem.style.transform = finalTransform; |
|
|
|
historyPanelEl.appendChild(thumbItem); |
|
}); |
|
const activeThumb = historyPanelEl.querySelector('.active-history-item'); |
|
if (activeThumb) { |
|
|
|
activeThumb.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' }); |
|
} |
|
} |
|
|
|
|
|
function enterFullscreen(index, isFromHistory = false) { |
|
if (!fullscreenIframeEl || !fullscreenOverlayEl || !document.body || !fullscreenHistoryNavEl || !historyNavPrevBtnEl || !historyNavNextBtnEl) { |
|
console.error("Cannot enter fullscreen: Overlay or nav elements missing."); |
|
return; |
|
} |
|
|
|
let codeToDisplay; |
|
currentFullscreenHistoryIndex = -1; |
|
fullscreenHistoryNavEl.classList.remove('visible'); |
|
|
|
if (isFromHistory) { |
|
if (index < 0 || index >= evolutionTimeline.length || !evolutionTimeline[index]) { |
|
console.warn("Cannot enter fullscreen for history item", index); return; |
|
} |
|
codeToDisplay = evolutionTimeline[index].code; |
|
currentFullscreenHistoryIndex = index; |
|
|
|
fullscreenHistoryNavEl.classList.add('visible'); |
|
historyNavPrevBtnEl.disabled = (currentFullscreenHistoryIndex <= 0); |
|
historyNavNextBtnEl.disabled = (currentFullscreenHistoryIndex >= evolutionTimeline.length - 1); |
|
|
|
} else { |
|
if (index < 0 || index >= numVariationsToGenerate || !generatedCode[index]) { |
|
console.warn("Cannot enter fullscreen for variation grid item", index); return; |
|
} |
|
codeToDisplay = generatedCode[index]; |
|
} |
|
|
|
fullscreenIframeEl.srcdoc = codeToDisplay; |
|
fullscreenOverlayEl.classList.add('visible'); |
|
document.body.classList.add('fullscreen-active'); |
|
document.documentElement.style.overflow = 'hidden'; |
|
} |
|
function exitFullscreen() { |
|
if (!fullscreenOverlayEl || !document.body || !fullscreenIframeEl || !fullscreenHistoryNavEl) { |
|
console.error("Cannot exit fullscreen: Overlay or nav elements missing."); |
|
return; |
|
} |
|
fullscreenOverlayEl.classList.remove('visible'); |
|
document.body.classList.remove('fullscreen-active'); |
|
document.documentElement.style.overflow = ''; |
|
currentFullscreenHistoryIndex = -1; |
|
fullscreenHistoryNavEl.classList.remove('visible'); |
|
setTimeout(() => { if (fullscreenIframeEl) fullscreenIframeEl.srcdoc = 'about:blank'; }, 300); |
|
} |
|
|
|
function showPreviousHistoryInFullscreen() { |
|
if (currentFullscreenHistoryIndex > 0) { |
|
currentFullscreenHistoryIndex--; |
|
activeTimelineIndex = currentFullscreenHistoryIndex; |
|
enterFullscreen(activeTimelineIndex, true); |
|
renderHistoryPanel(); |
|
const historyEntry = evolutionTimeline[activeTimelineIndex]; |
|
if (historyEntry && codeOutputEl) { |
|
codeOutputEl.textContent = historyEntry.code; |
|
if(selectedCodeTitleH3El) selectedCodeTitleH3El.textContent = `Code (Evolution ${activeTimelineIndex + 1} - Historical):`; |
|
} |
|
} |
|
} |
|
|
|
function showNextHistoryInFullscreen() { |
|
if (currentFullscreenHistoryIndex < evolutionTimeline.length - 1) { |
|
currentFullscreenHistoryIndex++; |
|
activeTimelineIndex = currentFullscreenHistoryIndex; |
|
enterFullscreen(activeTimelineIndex, true); |
|
renderHistoryPanel(); |
|
const historyEntry = evolutionTimeline[activeTimelineIndex]; |
|
if (historyEntry && codeOutputEl) { |
|
codeOutputEl.textContent = historyEntry.code; |
|
if(selectedCodeTitleH3El) selectedCodeTitleH3El.textContent = `Code (Evolution ${activeTimelineIndex + 1} - Historical):`; |
|
} |
|
} |
|
} |
|
|
|
|
|
function showPromptModal() { |
|
if (!promptModalOverlayEl || !modalUserPromptEl || !modalRefinementCheckboxEl || !numVariationsSliderEl || !modelSelEl || !modalContextSizeValueEl || !modalMaxTokensValueEl || !modalMaxTokensSliderEl) return; |
|
|
|
updateModelSpecificUI(modelSelEl.value); |
|
|
|
modalRefinementCheckboxEl.checked = (activeTimelineIndex !== -1); |
|
numVariationsSliderEl.value = lastGenerationConfig.numVariations; |
|
if(numVariationsValueDisplayEl) numVariationsValueDisplayEl.textContent = numVariationsSliderEl.value; |
|
|
|
|
|
|
|
|
|
|
|
promptModalOverlayEl.classList.remove('modal-anim-fade-out'); |
|
promptModalOverlayEl.style.display = 'flex'; |
|
promptModalOverlayEl.classList.add('modal-anim-fade-in'); |
|
|
|
|
|
modalUserPromptEl.focus(); |
|
} |
|
|
|
function hidePromptModal() { |
|
if (!promptModalOverlayEl) return; |
|
|
|
promptModalOverlayEl.classList.remove('modal-anim-fade-in'); |
|
promptModalOverlayEl.classList.add('modal-anim-fade-out'); |
|
|
|
|
|
const handleAnimationEnd = () => { |
|
promptModalOverlayEl.style.display = 'none'; |
|
|
|
promptModalOverlayEl.removeEventListener('animationend', handleAnimationEnd); |
|
}; |
|
promptModalOverlayEl.addEventListener('animationend', handleAnimationEnd); |
|
} |
|
|
|
function handleModalGenerate() { |
|
if (!modalUserPromptEl || !modalGenerateBtnEl) return; |
|
const modalPrompt = modalUserPromptEl.value.trim(); |
|
if (modalPrompt) { |
|
hidePromptModal(); |
|
if (!modalGenerateBtnEl.disabled) { |
|
generateVariations(); |
|
} |
|
} else { |
|
console.warn("Modal prompt is empty. Not generating."); |
|
} |
|
} |
|
|
|
|
|
function showConfigModal() { |
|
if (!configModalOverlayEl) return; |
|
|
|
|
|
configModalOverlayEl.classList.remove('modal-anim-fade-out'); |
|
configModalOverlayEl.style.display = 'flex'; |
|
configModalOverlayEl.classList.add('modal-anim-fade-in'); |
|
|
|
} |
|
function hideConfigModal() { |
|
if (!configModalOverlayEl) return; |
|
|
|
configModalOverlayEl.classList.remove('modal-anim-fade-in'); |
|
configModalOverlayEl.classList.add('modal-anim-fade-out'); |
|
|
|
const handleAnimationEnd = () => { |
|
configModalOverlayEl.style.display = 'none'; |
|
|
|
configModalOverlayEl.removeEventListener('animationend', handleAnimationEnd); |
|
}; |
|
configModalOverlayEl.addEventListener('animationend', handleAnimationEnd); |
|
} |
|
|
|
|
|
function showConfirmModal(message, onConfirmCallback) { |
|
if (!confirmModalOverlayEl || !confirmModalMessageEl || !confirmModalConfirmBtnEl || !confirmModalCancelBtnEl) { |
|
console.error("Confirmation modal elements not found!"); |
|
|
|
if (confirm(message)) { |
|
onConfirmCallback(); |
|
} |
|
return; |
|
} |
|
|
|
confirmModalMessageEl.textContent = message; |
|
currentConfirmCallback = onConfirmCallback; |
|
|
|
|
|
confirmModalConfirmBtnEl.replaceWith(confirmModalConfirmBtnEl.cloneNode(true)); |
|
confirmModalCancelBtnEl.replaceWith(confirmModalCancelBtnEl.cloneNode(true)); |
|
|
|
confirmModalConfirmBtnEl = document.getElementById('confirm-modal-confirm-button'); |
|
confirmModalCancelBtnEl = document.getElementById('confirm-modal-cancel-button'); |
|
|
|
|
|
confirmModalConfirmBtnEl.addEventListener('click', handleConfirm); |
|
confirmModalCancelBtnEl.addEventListener('click', hideConfirmModal); |
|
|
|
|
|
confirmModalOverlayEl.classList.remove('modal-anim-fade-out'); |
|
confirmModalOverlayEl.style.display = 'flex'; |
|
confirmModalOverlayEl.classList.add('modal-anim-fade-in'); |
|
} |
|
|
|
function hideConfirmModal() { |
|
if (!confirmModalOverlayEl) return; |
|
|
|
confirmModalOverlayEl.classList.remove('modal-anim-fade-in'); |
|
confirmModalOverlayEl.classList.add('modal-anim-fade-out'); |
|
|
|
const handleAnimationEnd = () => { |
|
confirmModalOverlayEl.style.display = 'none'; |
|
currentConfirmCallback = null; |
|
confirmModalOverlayEl.removeEventListener('animationend', handleAnimationEnd); |
|
}; |
|
confirmModalOverlayEl.addEventListener('animationend', handleAnimationEnd); |
|
} |
|
|
|
function handleConfirm() { |
|
hideConfirmModal(); |
|
if (typeof currentConfirmCallback === 'function') { |
|
currentConfirmCallback(); |
|
} |
|
currentConfirmCallback = null; |
|
} |
|
|
|
|
|
function navigateToPreviousHistory() { |
|
if (activeTimelineIndex > 0) { |
|
handleHistoryItemClick(activeTimelineIndex - 1); |
|
} |
|
} |
|
|
|
function navigateToNextHistory() { |
|
if (activeTimelineIndex < evolutionTimeline.length - 1) { |
|
handleHistoryItemClick(activeTimelineIndex + 1); |
|
} |
|
} |
|
|
|
function updateHistoryNavigationButtons() { |
|
if (!historyNavLeftBtnEl || !historyNavRightBtnEl) return; |
|
historyNavLeftBtnEl.disabled = activeTimelineIndex <= 0; |
|
historyNavRightBtnEl.disabled = activeTimelineIndex >= evolutionTimeline.length - 1; |
|
} |
|
|
|
|
|
async function fetchCodeSplitFromGemini(apiKey, fullHtmlContent) { |
|
const exportModelName = "gemini-2.5-flash-preview-04-17"; |
|
const API_ENDPOINT = `${API_BASE_URL}${exportModelName}:generateContent?key=${apiKey}`; |
|
const prompt = ` |
|
You are an expert web developer. You are given a single HTML document that might contain inline CSS within <style> tags and inline JavaScript within <script> tags. Your task is to separate this document into three distinct components: HTML structure, CSS styles, and JavaScript code. |
|
|
|
Follow these instructions carefully: |
|
1. **HTML Output**: This should be the main HTML structure. |
|
* If there were inline <style> tags, remove them. Add a <link rel="stylesheet" href="style.css"> in the <head> instead. |
|
* If there were inline <script> tags (especially those not setting up initial variables or configurations that need to be in the head), try to move their content to what will become an external script.js file. Add <script src="script.js" defer><\\/script> before the closing </body> tag. For simple, short scripts that are clearly for page setup and are in the head, they can sometimes remain, but prefer externalizing functional code. |
|
* Ensure the HTML output is clean and well-formed. |
|
2. **CSS Output**: This should contain ALL CSS rules extracted from any <style>...</style> blocks in the original HTML. If no <style> blocks were present, this should be an empty string or a comment indicating no CSS. |
|
3. **JavaScript Output**: This should contain ALL JavaScript code extracted from any <script>...</script> blocks (that are not JSON-LD or other non-executable script types). If no functional <script> blocks were present, this should be an empty string or a comment indicating no JavaScript. |
|
|
|
Provide the output STRICTLY as a JSON object with the following keys: "html_code", "css_code", "js_code". |
|
|
|
Example of desired JSON output format: |
|
{ |
|
"html_code": "<!DOCTYPE html>...<link rel=\\\"stylesheet\\\" href=\\\"style.css\\\"><script src=\\\"script.js\\\" defer><\\/script></body></html>", |
|
"css_code": "body { font-family: sans-serif; } ...", |
|
"js_code": "console.log(\\\'Hello World!\\\'); ...\" |
|
} |
|
|
|
Original HTML content: |
|
\`\`\`html |
|
${fullHtmlContent}\n\`\`\` |
|
|
|
Return ONLY the JSON object. Do not include any other explanatory text or markdown formatting outside the JSON structure itself. |
|
`; |
|
|
|
try { |
|
const response = await fetch(API_ENDPOINT, { |
|
method: 'POST', |
|
headers: { 'Content-Type': 'application/json' }, |
|
body: JSON.stringify({ contents: [{ parts: [{ text: prompt }] }] }) |
|
}); |
|
|
|
if (!response.ok) { |
|
const errorData = await response.json().catch(() => null); |
|
const errorMsg = errorData?.error?.message || `HTTP Error: ${response.status}`; |
|
console.error('Gemini API Error:', errorMsg); |
|
throw new Error(`API Error: ${errorMsg}`); |
|
} |
|
|
|
const responseData = await response.json(); |
|
const candidate = responseData.candidates?.[0]; |
|
if (candidate?.content?.parts?.[0]?.text) { |
|
let rawText = candidate.content.parts[0].text; |
|
console.log("Raw response from Gemini for splitting:", rawText); |
|
|
|
let jsonString = null; |
|
|
|
|
|
const markdownJsonMatch = rawText.match(/```json\n(\{[\s\S]*\})\n```/s) || rawText.match(/```\n(\{[\s\S]*\})\n```/s); |
|
if (markdownJsonMatch && markdownJsonMatch[1]) { |
|
jsonString = markdownJsonMatch[1]; |
|
} else { |
|
|
|
const directJsonMatch = rawText.match(/\{.*\}/s); |
|
if (directJsonMatch) { |
|
jsonString = directJsonMatch[0]; |
|
} |
|
} |
|
|
|
if (jsonString) { |
|
try { |
|
return JSON.parse(jsonString); |
|
} catch (parseError) { |
|
console.error("Failed to parse JSON string from model:", jsonString, parseError); |
|
throw new Error("JSON parsing failed after attempting to clean model response."); |
|
} |
|
} |
|
throw new Error("Clean JSON object not found in model's response after attempting to extract."); |
|
} |
|
throw new Error("No valid content found in model's response."); |
|
|
|
} catch (error) { |
|
console.error('Error fetching or parsing split code:', error); |
|
throw error; |
|
} |
|
} |
|
|
|
|
|
function showFullPromptModal(fullPrompt) { |
|
if (!promptDisplayModalOverlayEl || !fullPromptTextEl) return; |
|
|
|
fullPromptTextEl.textContent = fullPrompt; |
|
|
|
|
|
promptDisplayModalOverlayEl.classList.remove('modal-anim-fade-out'); |
|
promptDisplayModalOverlayEl.style.display = 'flex'; |
|
promptDisplayModalOverlayEl.classList.add('modal-anim-fade-in'); |
|
} |
|
|
|
function hideFullPromptModal() { |
|
if (!promptDisplayModalOverlayEl) return; |
|
|
|
promptDisplayModalOverlayEl.classList.remove('modal-anim-fade-in'); |
|
promptDisplayModalOverlayEl.classList.add('modal-anim-fade-out'); |
|
|
|
const handleAnimationEnd = () => { |
|
promptDisplayModalOverlayEl.style.display = 'none'; |
|
promptDisplayModalOverlayEl.removeEventListener('animationend', handleAnimationEnd); |
|
}; |
|
promptDisplayModalOverlayEl.addEventListener('animationend', handleAnimationEnd); |
|
} |
|
|
|
|
|
document.addEventListener('DOMContentLoaded', () => { |
|
console.log("DOMContentLoaded event fired."); |
|
|
|
apiKeyEl = document.getElementById('api-key'); |
|
modelSelEl = document.getElementById('model-select'); |
|
const codeOutputPre = document.getElementById('code-output'); |
|
if (codeOutputPre) { codeOutputEl = codeOutputPre.querySelector('code'); } else { console.error("Code output <pre> not found");} |
|
errorMessageEl = document.getElementById('error-message'); |
|
refinementLoadingIndicator = document.getElementById('refinement-loading-indicator'); |
|
mainContentEl = document.getElementById('main-content'); |
|
configButtonEl = document.getElementById('config-button'); |
|
intervalSliderEl = document.getElementById('preview-interval-slider'); |
|
intervalValueDisplayEl = document.getElementById('interval-value'); |
|
fullscreenOverlayEl = document.getElementById('fullscreen-overlay'); |
|
fullscreenIframeEl = document.getElementById('fullscreen-iframe'); |
|
exitFullscreenBtnEl = document.getElementById('exit-fullscreen-btn'); |
|
perspectiveViewportEl = document.getElementById('perspective-viewport'); |
|
previewGridWrapperEl = document.getElementById('preview-grid-wrapper'); |
|
historyPanelEl = document.getElementById('history-panel'); |
|
historyPanelPlaceholderEl = document.getElementById('history-panel-placeholder'); |
|
selectedCodeTitleH3El = document.getElementById('selected-code-title'); |
|
mainContentTitleH1El = document.getElementById('main-content-title'); |
|
mainContentSubtitleH2El = document.getElementById('main-content-subtitle'); |
|
fullscreenHistoryNavEl = document.getElementById('fullscreen-history-nav'); |
|
historyNavPrevBtnEl = document.getElementById('history-nav-prev'); |
|
historyNavNextBtnEl = document.getElementById('history-nav-next'); |
|
promptModalOverlayEl = document.getElementById('prompt-modal-overlay'); |
|
promptModalContentEl = document.getElementById('prompt-modal-content'); |
|
modalUserPromptEl = document.getElementById('modal-user-prompt'); |
|
modalGenerateBtnEl = document.getElementById('modal-generate-button'); |
|
modalCancelBtnEl = document.getElementById('modal-cancel-button'); |
|
modalLoadingIndicatorEl = document.getElementById('modal-loading-indicator'); |
|
modalRefinementCheckboxEl = document.getElementById('modal-refinement-checkbox'); |
|
numVariationsSliderEl = document.getElementById('num-variations-slider'); |
|
numVariationsValueDisplayEl = document.getElementById('num-variations-value'); |
|
configModalOverlayEl = document.getElementById('config-modal-overlay'); |
|
configModalContentEl = document.getElementById('config-modal-content'); |
|
configModalCloseBtnEl = document.getElementById('config-modal-close-button'); |
|
copyCodeButtonEl = document.getElementById('copy-code-button'); |
|
exportCodeButtonEl = document.getElementById('export-code-button'); |
|
historyToggleButtonEl = document.getElementById('history-toggle-button'); |
|
historyArrowDownEl = document.getElementById('history-arrow-down'); |
|
historyArrowUpEl = document.getElementById('history-arrow-up'); |
|
newButtonEl = document.getElementById('new-button'); |
|
confirmModalOverlayEl = document.getElementById('confirm-modal-overlay'); |
|
confirmModalMessageEl = document.getElementById('confirm-modal-message'); |
|
confirmModalConfirmBtnEl = document.getElementById('confirm-modal-confirm-button'); |
|
confirmModalCancelBtnEl = document.getElementById('confirm-modal-cancel-button'); |
|
historyNavLeftBtnEl = document.getElementById('history-nav-left-button'); |
|
historyNavRightBtnEl = document.getElementById('history-nav-right-button'); |
|
promptDisplayModalOverlayEl = document.getElementById('prompt-display-modal-overlay'); |
|
promptDisplayModalContentEl = document.getElementById('prompt-display-modal-content'); |
|
fullPromptTextEl = document.getElementById('full-prompt-text'); |
|
promptDisplayModalCloseBtnEl = document.getElementById('prompt-display-modal-close-button'); |
|
showPromptModalButtonEl = document.getElementById('show-prompt-modal-button'); |
|
modalContextSizeValueEl = document.getElementById('modal-context-size-value'); |
|
modalMaxTokensValueEl = document.getElementById('modal-max-tokens-value'); |
|
modalMaxTokensSliderEl = document.getElementById('modal-max-tokens-slider'); |
|
|
|
console.log("Checking history toggle elements:"); |
|
console.log("historyToggleButtonEl:", historyToggleButtonEl); |
|
console.log("historyPanelEl:", historyPanelEl); |
|
console.log("historyArrowDownEl:", historyArrowDownEl); |
|
console.log("historyArrowUpEl:", historyArrowUpEl); |
|
|
|
|
|
let missingElements = []; |
|
const requiredElements = { apiKeyEl, modelSelEl, codeOutputEl, errorMessageEl, refinementLoadingIndicator, mainContentEl, configButtonEl, intervalSliderEl, intervalValueDisplayEl, fullscreenOverlayEl, fullscreenIframeEl, exitFullscreenBtnEl, perspectiveViewportEl, previewGridWrapperEl, historyPanelEl, historyPanelPlaceholderEl, selectedCodeTitleH3El, mainContentTitleH1El, mainContentSubtitleH2El, fullscreenHistoryNavEl, historyNavPrevBtnEl, historyNavNextBtnEl, promptModalOverlayEl, promptModalContentEl, modalUserPromptEl, modalGenerateBtnEl, modalCancelBtnEl, modalLoadingIndicatorEl, modalRefinementCheckboxEl, numVariationsSliderEl, numVariationsValueDisplayEl, configModalOverlayEl, configModalContentEl, configModalCloseBtnEl, copyCodeButtonEl, exportCodeButtonEl, historyToggleButtonEl, historyArrowDownEl, historyArrowUpEl, newButtonEl, confirmModalOverlayEl, confirmModalMessageEl, confirmModalConfirmBtnEl, confirmModalCancelBtnEl, historyNavLeftBtnEl, historyNavRightBtnEl, promptDisplayModalOverlayEl, promptDisplayModalContentEl, fullPromptTextEl, promptDisplayModalCloseBtnEl, showPromptModalButtonEl, modalContextSizeValueEl, modalMaxTokensValueEl, modalMaxTokensSliderEl }; |
|
for (const key in requiredElements) { if (!requiredElements[key]) { missingElements.push(key); } } |
|
|
|
if (missingElements.length > 0) { |
|
console.error("Initialization Error: Critical elements missing!", missingElements); |
|
if (document.body) { document.body.innerHTML = `<div class="fixed inset-0 bg-red-900 text-red-200 p-8 flex flex-col items-center justify-center text-center"><h2 class="text-2xl font-bold mb-4">Application Initialization Error</h2><p class="mb-2">Could not find element(s): ${missingElements.join(', ')}.</p><p>Please ensure the HTML structure is correct.</p></div>`; } |
|
else { alert(`Initialization Error: Critical elements missing: ${missingElements.join(', ')}.`); } |
|
return; |
|
} |
|
|
|
|
|
showInitialPreviewStateUI(); |
|
renderHistoryPanel(); |
|
updateHistoryNavigationButtons(); |
|
|
|
|
|
if (newButtonEl) { |
|
newButtonEl.addEventListener('click', () => { |
|
showConfirmModal( |
|
'Start a new session? This will clear the current state.', |
|
() => { location.reload(); } |
|
); |
|
}); |
|
} |
|
|
|
if (configButtonEl) configButtonEl.addEventListener('click', showConfigModal); |
|
if (showPromptModalButtonEl) showPromptModalButtonEl.addEventListener('click', showPromptModal); |
|
if (configModalCloseBtnEl) configModalCloseBtnEl.addEventListener('click', hideConfigModal); |
|
if (configModalOverlayEl) configModalOverlayEl.addEventListener('click', (e) => { |
|
if (e.target === configModalOverlayEl) { hideConfigModal(); } |
|
}); |
|
|
|
if (exitFullscreenBtnEl) exitFullscreenBtnEl.addEventListener('click', exitFullscreen); |
|
if (historyNavPrevBtnEl) historyNavPrevBtnEl.addEventListener('click', showPreviousHistoryInFullscreen); |
|
if (historyNavNextBtnEl) historyNavNextBtnEl.addEventListener('click', showNextHistoryInFullscreen); |
|
|
|
if (modalGenerateBtnEl) modalGenerateBtnEl.addEventListener('click', handleModalGenerate); |
|
if (modalCancelBtnEl) modalCancelBtnEl.addEventListener('click', hidePromptModal); |
|
if (promptModalOverlayEl) promptModalOverlayEl.addEventListener('click', (e) => { |
|
if (e.target === promptModalOverlayEl) { hidePromptModal(); } |
|
}); |
|
if (confirmModalOverlayEl) { |
|
confirmModalOverlayEl.addEventListener('click', (e) => { |
|
if (e.target === confirmModalOverlayEl) { hideConfirmModal(); } |
|
}); |
|
} |
|
if (promptDisplayModalOverlayEl) { |
|
promptDisplayModalOverlayEl.addEventListener('click', (e) => { |
|
if (e.target === promptDisplayModalOverlayEl) { hideFullPromptModal(); } |
|
}); |
|
} |
|
if (promptDisplayModalCloseBtnEl) { |
|
promptDisplayModalCloseBtnEl.addEventListener('click', hideFullPromptModal); |
|
} |
|
if (mainContentSubtitleH2El) { |
|
mainContentSubtitleH2El.addEventListener('click', (e) => { |
|
const fullPrompt = e.target.dataset.fullPrompt; |
|
if (fullPrompt) { |
|
showFullPromptModal(fullPrompt); |
|
} |
|
}); |
|
} |
|
if (modalUserPromptEl) modalUserPromptEl.addEventListener('keydown', (event) => { |
|
if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) { |
|
event.preventDefault(); handleModalGenerate(); |
|
} |
|
}); |
|
if (numVariationsSliderEl) { |
|
numVariationsSliderEl.addEventListener('input', (event) => { |
|
if(numVariationsValueDisplayEl) numVariationsValueDisplayEl.textContent = event.target.value; |
|
}); |
|
if(numVariationsValueDisplayEl) numVariationsValueDisplayEl.textContent = numVariationsSliderEl.value; |
|
} |
|
|
|
if (intervalSliderEl) { |
|
intervalSliderEl.addEventListener('input', (event) => { |
|
previewUpdateInterval = parseInt(event.target.value, 10); |
|
if(intervalValueDisplayEl) intervalValueDisplayEl.textContent = previewUpdateInterval; |
|
}); |
|
previewUpdateInterval = parseInt(intervalSliderEl.value, 10); |
|
if(intervalValueDisplayEl) intervalValueDisplayEl.textContent = previewUpdateInterval; |
|
} |
|
|
|
if (copyCodeButtonEl) { |
|
copyCodeButtonEl.addEventListener('click', () => { |
|
if (codeOutputEl && codeOutputEl.textContent && codeOutputEl.textContent !== '// Select a variation or history item to view its code.') { |
|
const textToCopy = codeOutputEl.textContent; |
|
const textArea = document.createElement("textarea"); |
|
textArea.value = textToCopy; |
|
textArea.style.position = "fixed"; |
|
document.body.appendChild(textArea); |
|
textArea.focus(); |
|
textArea.select(); |
|
try { |
|
const successful = document.execCommand('copy'); |
|
if (successful) { |
|
const originalText = copyCodeButtonEl.innerHTML; |
|
copyCodeButtonEl.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-check inline-block mr-1"><polyline points="20 6 9 17 4 12"></polyline></svg> Copied!`; |
|
copyCodeButtonEl.classList.add('copied'); |
|
setTimeout(() => { |
|
copyCodeButtonEl.innerHTML = originalText; |
|
copyCodeButtonEl.classList.remove('copied'); |
|
}, 2000); |
|
} else { |
|
console.error('Fallback: Failed to copy code using execCommand.'); |
|
} |
|
} catch (err) { |
|
console.error('Fallback: Error copying code using execCommand: ', err); |
|
} |
|
document.body.removeChild(textArea); |
|
} |
|
}); |
|
} |
|
|
|
if (exportCodeButtonEl) { |
|
exportCodeButtonEl.addEventListener('click', async () => { |
|
const currentCode = codeOutputEl?.textContent; |
|
const apiKey = apiKeyEl?.value; |
|
|
|
if (!currentCode || currentCode.startsWith('//') || currentCode.trim() === '') { |
|
alert('No code selected or available to export.'); |
|
return; |
|
} |
|
if (!apiKey) { |
|
alert('API Key is missing. Please configure it in settings.'); |
|
showConfigModal(); |
|
return; |
|
} |
|
|
|
const originalButtonContent = exportCodeButtonEl.innerHTML; |
|
exportCodeButtonEl.disabled = true; |
|
exportCodeButtonEl.innerHTML = `<svg class="animate-spin h-4 w-4 inline-block mr-1" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> |
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> |
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> |
|
</svg> Exporting...`; |
|
|
|
try { |
|
const response = await fetch(`${API_BASE_URL}/chat/completions`, { |
|
method: 'POST', |
|
headers: { |
|
'Content-Type': 'application/json', |
|
'Authorization': `Bearer ${apiKey}` |
|
}, |
|
body: JSON.stringify({ |
|
model: 'qwen/qwen3-235b-a22b-fp8', |
|
messages: [{ |
|
role: "user", |
|
content: `Please analyze and optimize the following HTML code for export. Ensure it follows best practices and is production-ready: |
|
|
|
\`\`\`html |
|
${currentCode} |
|
\`\`\` |
|
|
|
Please provide the optimized code with any necessary improvements for production use.` |
|
}], |
|
stream: false, |
|
max_tokens: MODEL_DETAILS['qwen/qwen3-235b-a22b-fp8'].defaultOutput |
|
}) |
|
}); |
|
|
|
if (!response.ok) { |
|
throw new Error(`API Error: ${response.status} ${response.statusText}`); |
|
} |
|
|
|
const data = await response.json(); |
|
const optimizedCode = data.choices[0].message.content; |
|
|
|
|
|
const blob = new Blob([optimizedCode], { type: 'text/html' }); |
|
const url = window.URL.createObjectURL(blob); |
|
const a = document.createElement('a'); |
|
a.href = url; |
|
a.download = 'optimized-code.html'; |
|
document.body.appendChild(a); |
|
a.click(); |
|
window.URL.revokeObjectURL(url); |
|
document.body.removeChild(a); |
|
|
|
exportCodeButtonEl.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-check inline-block mr-1"><polyline points="20 6 9 17 4 12"></polyline></svg> Exported!`; |
|
setTimeout(() => { |
|
exportCodeButtonEl.innerHTML = originalButtonContent; |
|
}, 2000); |
|
} catch (error) { |
|
console.error('Export error:', error); |
|
alert(`Export failed: ${error.message}`); |
|
exportCodeButtonEl.innerHTML = originalButtonContent; |
|
} finally { |
|
exportCodeButtonEl.disabled = false; |
|
} |
|
}); |
|
} |
|
|
|
if (historyToggleButtonEl && historyPanelEl && historyArrowDownEl && historyArrowUpEl) { |
|
historyToggleButtonEl.addEventListener('click', () => { |
|
const isCollapsed = historyPanelEl.classList.toggle('history-collapsed'); |
|
const rootStyles = getComputedStyle(document.documentElement); |
|
const expandedHeight = rootStyles.getPropertyValue('--history-panel-expanded-height').trim(); |
|
const collapsedHeight = rootStyles.getPropertyValue('--history-panel-collapsed-height').trim(); |
|
|
|
if (isCollapsed) { |
|
document.documentElement.style.setProperty('--history-panel-current-height', collapsedHeight); |
|
historyArrowDownEl.classList.add('hidden'); |
|
historyArrowUpEl.classList.remove('hidden'); |
|
} else { |
|
document.documentElement.style.setProperty('--history-panel-current-height', expandedHeight); |
|
historyArrowDownEl.classList.remove('hidden'); |
|
historyArrowUpEl.classList.add('hidden'); |
|
|
|
|
|
renderHistoryPanel(); |
|
} |
|
}); |
|
} |
|
|
|
if (historyNavLeftBtnEl) { |
|
historyNavLeftBtnEl.addEventListener('click', navigateToPreviousHistory); |
|
} |
|
if (historyNavRightBtnEl) { |
|
historyNavRightBtnEl.addEventListener('click', navigateToNextHistory); |
|
} |
|
|
|
|
|
document.addEventListener('keydown', (event) => { |
|
if (event.key === 'Escape') { |
|
|
|
if (configModalOverlayEl.style.display !== 'none' && !configModalOverlayEl.classList.contains('modal-anim-fade-out')) { |
|
hideConfigModal(); |
|
} else if (promptModalOverlayEl.style.display !== 'none' && !promptModalOverlayEl.classList.contains('modal-anim-fade-out')) { |
|
hidePromptModal(); |
|
} else if (confirmModalOverlayEl.style.display !== 'none' && !confirmModalOverlayEl.classList.contains('modal-anim-fade-out')) { |
|
hideConfirmModal(); |
|
} else if (promptDisplayModalOverlayEl.style.display !== 'none' && !promptDisplayModalOverlayEl.classList.contains('modal-anim-fade-out')) { |
|
hideFullPromptModal(); |
|
} else if (document.body.classList.contains('fullscreen-active')) { |
|
exitFullscreen(); |
|
} |
|
} |
|
const targetTagName = event.target ? event.target.tagName.toLowerCase() : null; |
|
const isTypingInInputOrTextarea = targetTagName === 'input' || targetTagName === 'textarea'; |
|
|
|
if (document.body.classList.contains('fullscreen-active') && currentFullscreenHistoryIndex !== -1 && !isTypingInInputOrTextarea) { |
|
if (event.key.toLowerCase() === 'w') { |
|
event.preventDefault(); |
|
showPreviousHistoryInFullscreen(); |
|
} else if (event.key.toLowerCase() === 'd') { |
|
event.preventDefault(); |
|
showNextHistoryInFullscreen(); |
|
} |
|
} |
|
if (event.altKey && !isTypingInInputOrTextarea) { |
|
if (event.key.toLowerCase() === 'p' || event.code === 'KeyP') { |
|
event.preventDefault(); |
|
if (!promptModalOverlayEl.classList.contains('visible') && !configModalOverlayEl.classList.contains('visible')) { |
|
showPromptModal(); |
|
} |
|
} |
|
else if (event.key.toLowerCase() === 'j' || event.code === 'KeyJ') { |
|
event.preventDefault(); |
|
if (lastGenerationConfig.prompt) { |
|
console.log("Alt+J: Regenerating with last settings:", lastGenerationConfig); |
|
modalUserPromptEl.value = lastGenerationConfig.prompt; |
|
modalRefinementCheckboxEl.checked = lastGenerationConfig.isRefinement; |
|
numVariationsSliderEl.value = lastGenerationConfig.numVariations; |
|
if(numVariationsValueDisplayEl) numVariationsValueDisplayEl.textContent = numVariationsSliderEl.value; |
|
|
|
|
|
|
|
|
|
if (lastGenerationConfig.isRefinement) { |
|
activeTimelineIndex = lastGenerationConfig.refinedTimelineIndex; |
|
} |
|
handleModalGenerate(); |
|
} else { |
|
console.log("Alt+J: No last generation settings found. Opening prompt modal."); |
|
showPromptModal(); |
|
} |
|
} |
|
else if (event.key.toLowerCase() === 'o' || event.code === 'KeyO') { |
|
event.preventDefault(); |
|
if (!configModalOverlayEl.classList.contains('visible') && !promptModalOverlayEl.classList.contains('visible')) { |
|
showConfigModal(); |
|
} |
|
} |
|
|
|
if (event.key === 'PageUp') { |
|
event.preventDefault(); |
|
navigateToPreviousHistory(); |
|
} else if (event.key === 'PageDown') { |
|
event.preventDefault(); |
|
navigateToNextHistory(); |
|
} |
|
} |
|
}); |
|
console.log("Initialization setup complete."); |
|
|
|
if (modelSelEl) { |
|
modelSelEl.addEventListener('change', (event) => { |
|
updateModelSpecificUI(event.target.value); |
|
}); |
|
} |
|
|
|
if (modalMaxTokensSliderEl) { |
|
modalMaxTokensSliderEl.addEventListener('input', (event) => { |
|
if (modalMaxTokensValueEl) { |
|
|
|
const numericValue = parseInt(event.target.value, 10); |
|
modalMaxTokensValueEl.textContent = numericValue.toLocaleString(); |
|
} |
|
}); |
|
} |
|
}); |
|
|
|
|
|
function updateModelSpecificUI(selectedModelName) { |
|
if (!modalContextSizeValueEl || !modalMaxTokensValueEl || !modalMaxTokensSliderEl) return; |
|
|
|
const details = MODEL_DETAILS[selectedModelName]; |
|
if (details) { |
|
modalContextSizeValueEl.textContent = details.context.toLocaleString(); |
|
|
|
modalMaxTokensSliderEl.min = 256; |
|
modalMaxTokensSliderEl.max = details.maxOutput; |
|
|
|
|
|
let defaultValue = details.maxOutput; |
|
|
|
defaultValue = Math.max(parseInt(modalMaxTokensSliderEl.min, 10), defaultValue); |
|
modalMaxTokensSliderEl.value = defaultValue; |
|
modalMaxTokensValueEl.textContent = defaultValue.toLocaleString(); |
|
|
|
|
|
|
|
|
|
if (details.maxOutput > 32000) { |
|
modalMaxTokensSliderEl.step = 512; |
|
} else if (details.maxOutput > 8000) { |
|
modalMaxTokensSliderEl.step = 256; |
|
} else { |
|
modalMaxTokensSliderEl.step = 128; |
|
} |
|
|
|
} else { |
|
modalContextSizeValueEl.textContent = "N/A"; |
|
modalMaxTokensValueEl.textContent = "N/A"; |
|
modalMaxTokensSliderEl.min = 256; |
|
modalMaxTokensSliderEl.max = 4096; |
|
modalMaxTokensSliderEl.value = 1024; |
|
modalMaxTokensSliderEl.step = 128; |
|
} |
|
} |