We can’t run that request because it looks like it includes personal info.
Please remove things like: full names, email, phone, address, school, exact location, usernames for contacting you, ID numbers, or anything encoded/hidden.
`;
if (window.Swal) {
await Swal.fire({
icon: 'warning',
title: 'Personal Info Detected',
html,
confirmButtonText: 'OK'
});
} else {
alert("We can’t run that request because it looks like it includes personal info.\\n\\nPlease remove things like: full names, email, phone, address, school, exact location, usernames for contacting you, ID numbers, or anything encoded/hidden.");
}
throw buildRequestError('PII detected', errorContext);
}
if (errorData && errorData.error_type === "pii_firewall_error") {
const issueMessage = errorData.error || "We’re experiencing issues running the PII check. Please try again.";
if (window.enhancedToastSystem) {
window.enhancedToastSystem.showError('Request Failed', issueMessage, {
duration: 8000,
requestContext: errorContext
});
} else {
showToast('error', issueMessage);
}
throw buildRequestError('PII firewall error', errorContext);
}
const retryAfter = response.headers.get("Retry-After");
// Handle 402 Payment Required
if (response.status === 402) {
showSubscriptionRequiredModal(errorData ? errorData.upgrade_url : '/payment-required');
throw buildRequestError('Subscription required', errorContext);
} else if (response.status === 429) {
const retryHint = retryAfter ? ` Please try again in ${retryAfter} seconds.` : ' Please try again in a moment.';
displayedMessage = `We are getting a lot of requests right now.${retryHint}`;
} else if (response.status === 502) {
displayedMessage = "Can't reach the server. Please try again later.";
}
// Enhanced error reporting with request tracking
if (window.enhancedToastSystem) {
const toastTitle = response.status === 429 ? 'Too Many Requests' : 'Request Failed';
const detailedMessage = `
Request Details
Endpoint:/${route}
HTTP Status:${response.status}
Response: ${response.statusText}
Attempt: ${attempt}
`;
window.enhancedToastSystem.showError(toastTitle, displayedMessage, {
detailedMessage,
duration: 8000,
requestContext: errorContext
});
} else {
const toastType = response.status === 429 ? 'warning' : 'error';
showToast(toastType, `Error: ${displayedMessage}`);
}
console.error("Error sendmessage: ", response.statusText);
throw buildRequestError(`Error: ${response.statusText}`, errorContext);
}
// Wait 1 second before the next attempt.
console.log("Waiting 1 second before next attempt...");
await new Promise(resolve => setTimeout(resolve, 1000));
}
// If, after the loop, we still don't have a successful response:
if (!response.ok) {
let displayedMessage = "An unexpected error occurred after multiple attempts";
let errorData = null;
try {
errorData = await response.json();
displayedMessage = errorData.error || response.statusText;
} catch (e) {
displayedMessage = response.statusText;
}
const finalResponseServerLogKey = response.headers.get("X-Server-Log-Key");
const errorContext = upsertRequestDebugContext({
clientRequestId,
route: requestRoute,
attempt,
httpStatus: response.status,
responseStatusText: response.statusText,
errorType: errorData && errorData.error_type ? errorData.error_type : `http_${response.status}`,
serverLogKey: (errorData && errorData.server_log_key) || finalResponseServerLogKey || undefined,
serverErrorCode: errorData && errorData.error_code ? errorData.error_code : undefined
});
const retryAfter = response.headers.get("Retry-After");
// Handle 402 Payment Required
if (response.status === 402) {
showSubscriptionRequiredModal(errorData ? errorData.upgrade_url : '/payment-required');
throw buildRequestError('Subscription required', errorContext);
} else if (response.status === 429) {
const retryHint = retryAfter ? ` Please try again in ${retryAfter} seconds.` : ' Please try again in a moment.';
displayedMessage = `We are getting a lot of requests right now.${retryHint}`;
}
// Enhanced error reporting after retries with full context
if (window.enhancedToastSystem) {
const toastTitle = response.status === 429 ? 'Too Many Requests' : 'Request Failed After Retries';
const detailedMessage = `
`;
window.enhancedToastSystem.showSuccess('Request Successful', `${route} completed successfully`, {
detailedMessage,
duration: 3000,
requestContext: successContext
});
}
return data;
}
let previewErrorReportingSuppressed = false;
function updateFixErrorSection(errorMessage) {
if (previewErrorReportingSuppressed) return;
errorManager.handleErrorMessage(errorMessage);
}
// New ErrorManager class for grouping errors, handling formatting, and updating the display
class ErrorManager {
constructor() {
this.errorGroups = {};
this.currentErrorIndex = 0;
// Feature flag for enriched copy mode
this.ENABLE_ENRICHED_COPY = true;
}
// Generate a unique key based on the error message (can be extended if error objects change)
_generateKey(errorMessage) {
return errorMessage;
}
// Add an error to the manager. If the error already exists, increment its count.
addError(errorMessage) {
const key = this._generateKey(errorMessage);
const now = new Date().toISOString();
if (this.errorGroups[key]) {
this.errorGroups[key].count++;
} else {
this.errorGroups[key] = { message: errorMessage, count: 1, firstThrown: now };
}
}
// Retrieve the grouped errors, sorted by the time they were first thrown
getGroupedErrors() {
return Object.values(this.errorGroups).sort(
(a, b) => new Date(a.firstThrown) - new Date(b.firstThrown)
);
}
// Clear all errors and reset the current index
clearErrors() {
this.errorGroups = {};
this.currentErrorIndex = 0;
this.updateDisplay();
const currentErrorSpan = document.getElementById('currentErrorSpan');
if (currentErrorSpan) {
currentErrorSpan.textContent = '';
}
const fixErrorButton = document.getElementById('fixErrorSwipeButton');
if (fixErrorButton) {
fixErrorButton.textContent = 'Fix Error';
}
this.updateNavigationButtons();
}
handleErrorMessage(errorMessage) {
console.log(`Received error: ${errorMessage} from page`);
if (errorMessage === undefined) {
this.clearErrors();
} else {
this.addError(errorMessage);
const groups = this.getGroupedErrors();
this.currentErrorIndex = groups.length - 1;
this.updateDisplay();
// Immediately update the current version's error metadata.
if (
currentProjectId &&
projectVersionHistory[currentProjectId] &&
projectVersionHistory[currentProjectId][currentVersion]
) {
projectVersionHistory[currentProjectId][currentVersion].errors = groups
}
}
}
// Update the error display in the DOM based on the current error group
updateDisplay() {
this.updateContainerVisibility();
const groupedErrors = this.getGroupedErrors();
if (!groupedErrors.length) return;
const currentGroup = groupedErrors[this.currentErrorIndex];
const displayMessage =
currentGroup.count > 1
? `${currentGroup.message} (Occurred ${currentGroup.count}x)`
: currentGroup.message;
const currentErrorSpan = document.getElementById('currentErrorSpan');
if (currentErrorSpan) {
currentErrorSpan.textContent = displayMessage;
}
const fixErrorButton = document.getElementById('fixErrorSwipeButton');
if (fixErrorButton) {
fixErrorButton.textContent = groupedErrors.length > 1
? `Fix ${groupedErrors.length} Errors`
: 'Fix Error';
}
this.updateNavigationButtons();
}
// Show or hide the error container based on whether errors exist
updateContainerVisibility() {
const errorContainer = document.getElementById('error-container');
const groupedErrors = this.getGroupedErrors();
if (errorContainer) {
errorContainer.style.display = groupedErrors.length > 0 ? 'block' : 'none';
}
}
// Update navigation buttons (Previous/Next) based on the current error index
updateNavigationButtons() {
const prevErrorButton = document.getElementById('prevErrorButton');
const nextErrorButton = document.getElementById('nextErrorButton');
const groupedErrors = this.getGroupedErrors();
if (prevErrorButton) {
prevErrorButton.style.display = this.currentErrorIndex > 0 ? 'inline-block' : 'none';
}
if (nextErrorButton) {
nextErrorButton.style.display =
this.currentErrorIndex < groupedErrors.length - 1 ? 'inline-block' : 'none';
}
}
// Event handler for the "Fix Error" button click
onFixErrorClicked() {
const groupedErrors = this.getGroupedErrors();
if (groupedErrors.length === 0) return;
if (groupedErrors.length === 1) {
// Fix the single error
parent.applyActionFromError(groupedErrors[0].message);
} else {
// Fix multiple errors by joining them in a single string
const errorList = groupedErrors
.map((group, index) => {
const countText = group.count > 1 ? ` (Occurred ${group.count}x)` : "";
return `Error ${index + 1}: ${group.message}${countText}`;
})
.join('\n');
parent.applyActionFromError(errorList);
}
}
// Navigate to the previous error
onPrevErrorClicked() {
if (this.currentErrorIndex > 0) {
this.currentErrorIndex--;
this.updateDisplay();
}
}
// Navigate to the next error
onNextErrorClicked() {
const groupedErrors = this.getGroupedErrors();
if (this.currentErrorIndex < groupedErrors.length - 1) {
this.currentErrorIndex++;
this.updateDisplay();
}
}
// Copy the formatted errors to the clipboard (with dual-mode support)
async onCopyErrorClicked(event) {
const groupedErrors = this.getGroupedErrors();
if (!groupedErrors.length) return;
// Check if enriched mode should be used (Shift key + feature flag)
const useEnriched = this.ENABLE_ENRICHED_COPY && event && event.shiftKey;
if (useEnriched) {
try {
// Show loading indicator
if (window.enhancedToastSystem) {
window.enhancedToastSystem.info('Processing', 'Enriching error details... Please wait', { duration: 2000 });
} else {
showToast('info', 'Enriching error details... Please wait');
}
await this.copyEnrichedErrors(groupedErrors);
} catch (error) {
console.warn('Enriched copy failed, falling back to simple format', error);
if (window.enhancedToastSystem) {
window.enhancedToastSystem.warning('Fallback Mode', 'Enriched copy failed, using simple format');
} else {
showToast('warning', 'Enriched copy failed, using simple format');
}
this.copySimpleErrors(groupedErrors);
}
} else {
this.copySimpleErrors(groupedErrors);
}
}
// Simple copy format (original implementation)
copySimpleErrors(groupedErrors) {
const formattedErrors = groupedErrors
.map((group, index) => {
const errorNumber = index + 1;
const separator = '='.repeat(20);
const countText = group.count > 1 ? ` (Occurred ${group.count}x)` : "";
return `Error #${errorNumber}\n${separator}\n${group.message}${countText}\n`;
})
.join('\n');
const totalErrors = groupedErrors.length;
const header = `Total Errors: ${totalErrors}\n${'='.repeat(40)}\n\n`;
const fullText = header + formattedErrors;
navigator.clipboard.writeText(fullText)
.then(() => {
const requestId = window.enhancedToastSystem ? window.enhancedToastSystem.getClientRequestId() : null;
const detailedMessage = requestId ? `
Copy Details
Format: Simple Text
Errors Copied: ${totalErrors}
Request ID:${requestId.split('-')[0]}
Copied error context for debugging purposes.
` : null;
if (window.enhancedToastSystem) {
window.enhancedToastSystem.success('Copied to Clipboard', `All ${totalErrors} errors copied to clipboard!`, {
detailedMessage,
duration: 4000
});
} else {
showToast('info', `All ${totalErrors} errors copied to clipboard!`);
}
})
.catch(err => {
if (window.enhancedToastSystem) {
window.enhancedToastSystem.error('Copy Failed', 'Failed to copy error messages', {
detailedMessage: `
Error: ${err.message}
Your browser may not support clipboard access.
`
});
} else {
showToast('error', 'Failed to copy error messages');
}
console.error('Failed to copy errors:', err);
});
}
// Enriched copy format with code context
async copyEnrichedErrors(groupedErrors) {
const totalErrors = groupedErrors.length;
const header = `Total Errors: ${totalErrors} (Enriched with code context)\n${'='.repeat(60)}\n\n`;
// Process each error with getErrorContexts
const enrichedErrorPromises = groupedErrors.map(async (group, index) => {
const errorNumber = index + 1;
const separator = '='.repeat(40);
const countText = group.count > 1 ? ` (Occurred ${group.count}x)` : "";
try {
// Check if getErrorContexts is available
if (typeof getErrorContexts === 'function') {
// Enrich the error with context (5 second timeout per error)
const enrichedError = await Promise.race([
getErrorContexts(group.message, "pretty_message", 2000, 300),
new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), 5000))
]);
return `Error #${errorNumber}${countText}\n${separator}\n${enrichedError}\n`;
} else {
// Fallback if getErrorContexts is not available
return `Error #${errorNumber}${countText}\n${separator}\n${group.message}\n`;
}
} catch (error) {
console.warn(`Failed to enrich error #${errorNumber}:`, error);
// Fallback to simple format for this error
return `Error #${errorNumber}${countText}\n${separator}\n${group.message}\n`;
}
});
// Wait for all enrichments with overall timeout
const enrichedErrors = await Promise.all(enrichedErrorPromises);
const fullText = header + enrichedErrors.join('\n');
// Copy to clipboard
await navigator.clipboard.writeText(fullText);
const requestId = window.enhancedToastSystem ? window.enhancedToastSystem.getClientRequestId() : null;
const detailedMessage = requestId ? `
Enhanced Copy Details
Format: Enriched with Code Context
Errors Processed: ${totalErrors}
Request ID:${requestId.split('-')[0]}
Features Used:
✅ Code context analysis
✅ Error enrichment
✅ Structured formatting
Enhanced error details copied for advanced debugging.
` : null;
if (window.enhancedToastSystem) {
window.enhancedToastSystem.success('Enhanced Copy Complete', `${totalErrors} enriched errors copied to clipboard!`, {
detailedMessage,
duration: 6000
});
} else {
showToast('success', `${totalErrors} enriched errors copied to clipboard! (Shift+Click for enriched mode)`);
}
}
// Initialize event handlers for the error section
initErrorSection() {
const prevErrorButton = document.getElementById('prevErrorButton');
const nextErrorButton = document.getElementById('nextErrorButton');
const copyErrorButton = document.getElementById('copyErrorButton');
const fixErrorSwipeButton = document.getElementById('fixErrorSwipeButton');
if (fixErrorSwipeButton) {
fixErrorSwipeButton.addEventListener('selected', (e) => {
const swipeAction = e.detail.value; // Expected values: 'default-fix', 'fast-fix-error', 'hard-error', 'custom-error', 'investigate-error'
const groupedErrors = this.getGroupedErrors();
if (groupedErrors.length === 0) return;
const errorMessage = groupedErrors.length === 1
? groupedErrors[0].message
: groupedErrors.map((group, index) => {
const countText = group.count > 1 ? ` (Occurred ${group.count}x)` : "";
return `Error ${index + 1}: ${group.message}${countText}`;
}).join('\n');
parent.applyActionFromError(errorMessage, swipeAction);
});
}
if (prevErrorButton) {
prevErrorButton.addEventListener('click', () => {
this.onPrevErrorClicked();
});
}
if (nextErrorButton) {
nextErrorButton.addEventListener('click', () => {
this.onNextErrorClicked();
});
}
if (copyErrorButton) {
copyErrorButton.addEventListener('click', (event) => {
this.onCopyErrorClicked(event);
});
// Add visual feedback for Shift key
const handleKeyChange = (event) => {
if (event.shiftKey && this.ENABLE_ENRICHED_COPY) {
copyErrorButton.style.backgroundColor = '#28a745';
copyErrorButton.style.color = 'white';
copyErrorButton.title = 'Click now for ENRICHED format with code context';
} else {
copyErrorButton.style.backgroundColor = '';
copyErrorButton.style.color = '';
copyErrorButton.title = 'Copy errors (Shift+Click for enriched format with code context)';
}
};
// Listen for shift key changes
document.addEventListener('keydown', handleKeyChange);
document.addEventListener('keyup', handleKeyChange);
// Also check on hover
copyErrorButton.addEventListener('mouseenter', handleKeyChange);
copyErrorButton.addEventListener('mouseleave', () => {
copyErrorButton.style.backgroundColor = '';
copyErrorButton.style.color = '';
});
}
}
}
const errorManager = new ErrorManager();
/* =============
BOOTSTRAPPING
============= */
var popupWindow;
let authCheckoutInProgress = false;
let authCheckoutWindow = null;
document.addEventListener('DOMContentLoaded', function() {
// Attach our error‐section events once the DOM is ready
errorManager.initErrorSection();
const notifications = document.getElementById('notifications');
popupWindow = new PopupWindow();
notifications.popupWindow = popupWindow;
notifications.addEventListener('pageClick', (event) => {
const pageId = event.detail;
console.log(`Page clicked: ${pageId}`);
parent.openFuzzyCodePage(pageId);
return false;
// Handle the page click event here
});
// Initialize the notifications component
notifications.init();
});
function getStaticBase() {
try {
const b = (window.FUZZYCODE_STATIC_BASE || '/static').trim();
const base = b.endsWith('/') ? b.slice(0, -1) : b;
try { console.log('[FC parent] static base =', base); } catch(_) {}
return base;
} catch (_) {
return '/static';
}
}
// Prefer https for localhost to avoid any accidental downgrades or mixed-content tooling quirks
function getStaticBaseSecure() {
try {
let base = getStaticBase();
if (base.startsWith('http://127.0.0.1:')) {
base = 'https://' + base.slice('http://'.length);
}
return base;
} catch (_) {
return getStaticBase();
}
}
// Helper script injection template (built from static base)
// Direct helper script injection (no fallback). In prod, use same-origin /static; in local testing, set ENV_STATIC to an https base.
const iframe_helper_script = `
`.replace(/\n/g, "");
const ugc_asset_progress_child_script = `
`.replace(/\n/g, "");
const child_screenshotter_script = `
`.replace(/\n/g, "");
// Experimental admin-only helper for inline resource fixtures
const experimental_resource_map_script = `
`.replace(/\n/g, "");
/* Capture and override console.error */
var non_sentry_onErrorScript_reference = `
")
.replace(/\n/g, "");
const error_helper_url = buildStaticUrl('error_helper.js');
const sentry_bundle_url = buildStaticUrl('external/sentry_bundle.js');
var error_helper_script = ``;
var sentryScript_reference = error_helper_script + (` ` + `")
.replace(/\n/g, "");
var custom_alerts_and_prompts = ``;
// Re-post current HTML to temp_page to refresh helpers (e.g., eruda) in cross-origin iframe
async function reRenderIframeWithCurrentState(reason = 'rerender_helpers') {
try {
const pv = projectVersionHistory[currentProjectId]?.[currentVersion];
if (!pv || !pv.html) {
console.warn('[FC parent] No current HTML found to re-render');
return;
}
const payload = {
html_content: pv.html,
origin_html_content: pv.html,
current_version: {
...buildTempPageVersionPayload(pv, reason),
last_action: (pv.lastAction || reason),
last_prompt: (pv.lastPrompt || ''),
}
};
const resp = await fetchWithExponentialBackoff('/temp_page', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await resp.json();
if (data && data.url) {
// Persist and load
projectVersionHistory[currentProjectId][currentVersion].tempUrl = data.url;
try { await window_tab_saveState(); } catch(_) {}
// Avoid calling scoped loadIframe; set the iframe src directly
try {
const iframeEl = document.getElementById('responseIframe');
if (iframeEl) {
iframeEl.src = data.url;
} else {
console.warn('[FC parent] responseIframe not found');
}
} catch (e) {
console.error('[FC parent] Failed to set iframe src during re-render', e);
}
}
} catch (e) {
console.error('[FC parent] Failed to re-render iframe with current state', e);
showToast('error', 'Failed to refresh preview for console tools');
}
}
function isRetriableHttpStatus(status) {
return status === 408 || status === 425 || status === 429 || status >= 500;
}
async function parseFetchErrorPayload(response) {
try {
const readableResponse = typeof response.clone === 'function' ? response.clone() : response;
if (readableResponse && typeof readableResponse.json === 'function') {
return await readableResponse.json();
}
} catch (_) {
// Some error responses are HTML or empty; fall back to the status line.
}
return null;
}
async function buildFetchHttpError(response) {
const payload = await parseFetchErrorPayload(response);
const statusText = response.statusText ? ` ${response.statusText}` : '';
const fallbackMessage = `HTTP error: ${response.status}${statusText}`;
const message = payload?.error || payload?.message || fallbackMessage;
const error = new Error(message);
error.status = response.status;
error.code = payload?.code || null;
error.payload = payload || null;
error.retriable = isRetriableHttpStatus(response.status);
return error;
}
function isRetriableFetchError(error) {
if (typeof error?.retriable === 'boolean') {
return error.retriable;
}
if (Number.isFinite(error?.status)) {
return isRetriableHttpStatus(error.status);
}
return true;
}
function fetchWithExponentialBackoff(url, options, maxRetries = 5) {
let attempt = 0;
const baseDelay = 1000; // 1 second base delay
function attemptFetch() {
return fetch(url, options).then(async response => {
if (!response.ok) {
const error = await buildFetchHttpError(response);
if (response.status === 401) {
showToast('error', 'Missing User Token. Please log in to use this feature.');
}
throw error;
}
return response;
}).catch(error => {
if (!isRetriableFetchError(error)) {
throw error;
}
if (attempt < maxRetries) {
attempt++;
const delay = baseDelay * Math.pow(2, attempt);
console.log(`Attempt ${attempt} failed: ${error.message}. Retrying in ${delay}ms...`);
return new Promise(resolve => setTimeout(resolve, delay)).then(attemptFetch);
} else {
throw error;
}
});
}
return attemptFetch();
}
let lastMissingAssetSheetCount = null;
function checkUnprocessedAssetSheets(htmlContent) {
const tagRe = /<\s*(multi-asset-sheet|multi_asset_sheet)\b[^>]*>/gi;
let match;
let missing = 0;
while ((match = tagRe.exec(htmlContent)) !== null) {
const tag = match[0];
if (!/server_sheet_key\s*=/.test(tag)) {
missing++;
}
}
if (missing > 0 && missing !== lastMissingAssetSheetCount) {
const plural = missing === 1 ? '' : 's';
const message = `Found ${missing} asset sheet${plural} without server_sheet_key. Run an AI change to prewarm these assets.`;
if (window.enhancedToastSystem) {
window.enhancedToastSystem.warning('Asset Sheets Not Processed', message, { duration: 6000 });
} else if (typeof showToast === 'function') {
showToast('warning', message);
}
}
lastMissingAssetSheetCount = missing;
}
function validateMultiAiUrls(htmlContent) {
if (!htmlContent || (!htmlContent.includes('/multi_ai') && !htmlContent.includes('asset_sheet_id='))) return [];
const tagRe = /<\s*(multi-asset-sheet|multi_asset_sheet)\b[^>]*>/gi;
const sheetIds = new Set();
let match;
while ((match = tagRe.exec(htmlContent)) !== null) {
const tag = match[0];
const idMatch = tag.match(/\bid\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+))/i);
const rawId = idMatch ? (idMatch[1] || idMatch[2] || idMatch[3] || '') : '';
const sheetId = rawId.trim();
if (sheetId) sheetIds.add(sheetId);
}
const urlRe = /(?:https?:\/\/|\/\/|\/)[^"'\\s<>)]*\/multi_ai\?[^"'\\s<>)]*/gi;
const bareHostRe = /\b[a-z0-9.-]+\.[a-z]{2,}\/[^"'\\s<>)]*\/multi_ai\?[^"'\\s<>)]*/gi;
const messages = new Set();
function trimTrailingDelims(value) {
let trimmed = value;
while (trimmed && /[`'",;)]$/.test(trimmed)) {
trimmed = trimmed.slice(0, -1);
}
return trimmed;
}
function formatKnownSheetIds(ids) {
const list = Array.from(ids);
const limit = 6;
if (list.length <= limit) return list.join(', ');
return `${list.slice(0, limit).join(', ')} (+${list.length - limit} more)`;
}
function parseMultiAiUrl(rawUrl) {
const cleaned = rawUrl.replace(/&/g, '&');
let query = '';
try {
const parsed = new URL(cleaned, window.location.href);
query = parsed.search ? parsed.search.slice(1) : '';
} catch (_) {
const qIndex = cleaned.indexOf('?');
query = qIndex >= 0 ? cleaned.slice(qIndex + 1) : '';
}
const params = new URLSearchParams(query);
const rawAssetSheetId = params.get('asset_sheet_id') || '';
let assetSheetId = rawAssetSheetId;
if (rawAssetSheetId) {
try {
assetSheetId = decodeURIComponent(rawAssetSheetId);
} catch (_) {}
}
return {
assetSheetId,
sheetKey: params.get('sheet_key') || '',
grab: params.get('grab') || '',
};
}
const urlMatches = new Set();
let urlMatch;
while ((urlMatch = urlRe.exec(htmlContent)) !== null) {
urlMatches.add(urlMatch[0]);
}
while ((urlMatch = bareHostRe.exec(htmlContent)) !== null) {
urlMatches.add(urlMatch[0]);
}
for (const matchUrl of urlMatches) {
const rawUrl = trimTrailingDelims(matchUrl);
const info = parseMultiAiUrl(rawUrl);
if (!info.assetSheetId || info.sheetKey) continue;
if (sheetIds.has(info.assetSheetId)) continue;
let message = `Invalid asset_sheet_id (multi_ai): asset_sheet_id="${info.assetSheetId}"`;
if (info.grab) message += ` grab="${info.grab}"`;
message += '. This asset_sheet_id does not match any in the HTML. ';
message += 'Do NOT remove existing sheets. Fix by changing asset_sheet_id to a valid sheet id and keeping grab=..., ';
message += 'or add a NEW tag for the missing assets and set spritesheet_ref to the original sheet id to match style.';
if (sheetIds.size) {
message += ` Known sheet ids: ${formatKnownSheetIds(sheetIds)}.`;
} else {
message += ' No tags were found in the HTML.';
}
messages.add(message);
}
if (!urlMatches.size) {
const assetIdRe = /asset_sheet_id\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`&]+))/gi;
let assetMatch;
while ((assetMatch = assetIdRe.exec(htmlContent)) !== null) {
const rawAssetSheetId = assetMatch[1] || assetMatch[2] || assetMatch[3] || '';
let assetSheetId = rawAssetSheetId.trim();
if (!assetSheetId) continue;
try {
assetSheetId = decodeURIComponent(assetSheetId);
} catch (_) {}
if (sheetIds.has(assetSheetId)) continue;
let message = `Invalid asset_sheet_id (multi_ai): asset_sheet_id="${assetSheetId}"`;
message += '. This asset_sheet_id does not match any in the HTML. ';
message += 'Do NOT remove existing sheets. Fix by changing asset_sheet_id to a valid sheet id and keeping grab=..., ';
message += 'or add a NEW tag for the missing assets and set spritesheet_ref to the original sheet id to match style.';
if (sheetIds.size) {
message += ` Known sheet ids: ${formatKnownSheetIds(sheetIds)}.`;
} else {
message += ' No tags were found in the HTML.';
}
messages.add(message);
}
}
return Array.from(messages);
}
function reportLintErrors(messages) {
if (!messages || !messages.length) return;
if (!errorManager || typeof errorManager.handleErrorMessage !== 'function') return;
messages.forEach((msg) => errorManager.handleErrorMessage(msg));
}
function refreshIframeWithHTMLContent(htmlContent) {
errorManager.clearErrors();
htmlContent = stripNonHTMLContent(htmlContent);
checkUnprocessedAssetSheets(htmlContent);
const multiAiLintMessages = validateMultiAiUrls(htmlContent);
tokensChanged(htmlContent);
htmlContent = replaceResourceLinks(htmlContent);
const iframeParent = document.getElementById('responseIframe').parentNode;
let iframe = document.createElement('iframe');
iframe.id = 'responseIframe';
iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin allow-popups allow-downloads allow-modals allow-forms allow-pointer-lock');
// Delegate device features to the cross-origin preview iframe as needed
iframe.setAttribute('allow', 'microphone; camera; clipboard-read; clipboard-write');
revokeAllBlobURLs();
let erudaScript = '';
if (erudaOn) {
erudaScript = erudaScript_reference;
}
let sentryScript = '';
let onErrorScript = '';
if (sentryOn) {
sentryScript = sentryScript_reference;
onErrorScript = "";
} else {
onErrorScript = non_sentry_onErrorScript_reference;
sentryScript = "";
}
// Inject script to prevent scrolling in the parent page
const preventScrollScript = `
`;
if (!htmlContent.includes('')) {
const helpers = ''
+ erudaScript
+ sentryScript
+ iframe_helper_script
+ ugc_asset_progress_child_script
+ child_screenshotter_script
+ experimental_resource_map_script
+ onErrorScript
+ custom_alerts_and_prompts
+ '';
// Try to inject right after opening
const headOpenRe = /]*>/i;
if (headOpenRe.test(htmlContent)) {
htmlContent = htmlContent.replace(headOpenRe, (m) => { try { console.log('[FC parent] injecting helpers after '); } catch(_) {}; return m + helpers; });
} else {
// No . If there is an , insert a head section after it.
const htmlOpenRe = /]*>/i;
if (htmlOpenRe.test(htmlContent)) {
htmlContent = htmlContent.replace(htmlOpenRe, (m) => { try { console.log('[FC parent] injecting section after '); } catch(_) {}; return m + '' + helpers + ''; });
} else {
// Fallback: prepend a head section at the top
try { console.log('[FC parent] prepending section at top of document'); } catch(_) {}
htmlContent = '' + helpers + '' + htmlContent;
}
}
}
const titleMatch = htmlContent.match(/(.*?)<\/title>/);
const projectNameInput = document.getElementById('projectName');
if (projectNameInput && (!projectNameInput.value || projectNameInput.value.trim() === '') && titleMatch && titleMatch[1].trim() !== '') {
projectNameInput.value = titleMatch[1].trim();
}
// Get the current version object from version history.
const currentVersionObj = projectVersionHistory[currentProjectId][currentVersion];
const sourceHtmlContent = currentVersionObj?.html || htmlContent;
// Function to perform the POST request to /temp_page.
function postToTempPage() {
return fetchWithExponentialBackoff('/temp_page', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
html_content: htmlContent,
origin_html_content: sourceHtmlContent,
current_version: buildTempPageVersionPayload(currentVersionObj)
})
})
.then(response => response.json())
.then(data => {
// Save the returned URL into the version history.
currentVersionObj.tempUrl = data.url;
window_tab_saveState().catch(err => {
console.error('Background window_tab_saveState failed:', err);
});
return data.url;
});
}
// Shared reload routine used by the error and load event listeners.
function reloadIframeDueToError(currentVersionObj, iframe, iframeParent) {
console.error('Error loading stored URL. Retrying by posting new content.');
delete currentVersionObj.tempUrl;
postToTempPage().then(newUrl => {
loadIframe(newUrl);
}).catch(error => {
loading_spinner.hide();
showToast('error', `Failed to load iframe content: ${error.message}`);
console.error('Failed to load iframe:', error);
});
}
// Function to load the iframe with a given URL.
let allowedIframeOrigin = window.location.origin;
let currentIframeWindow = null;
let lintErrorsReported = false;
function reportLintErrorsOnce() {
if (lintErrorsReported) return;
lintErrorsReported = true;
reportLintErrors(multiAiLintMessages);
}
function loadIframe(url) {
loading_spinner.show(iframe);
const existingIframe = iframeParent.querySelector('#responseIframe');
if (existingIframe) {
iframeParent.removeChild(existingIframe);
}
iframeParent.appendChild(iframe);
try { allowedIframeOrigin = new URL(url, window.location.href).origin; } catch(_) { allowedIframeOrigin = window.location.origin; }
iframe.src = url;
try { currentIframeWindow = iframe.contentWindow || null; } catch(_) { currentIframeWindow = null; }
resumePreviewErrorReporting();
setTimeout(() => {
loading_spinner.hide();
}, 300); //Usually the page is read in less than 300ms, so we hide the spinner after that time.
// Attach load event listener to check for an error response.
iframe.addEventListener('load', function onLoad() {
// Cross-origin: do not attempt to read iframe DOM; rely on error event or child postMessage
try {
const sameOrigin = new URL(iframe.src, window.location.href).origin === window.location.origin;
if (sameOrigin) {
const doc = iframe.contentDocument;
const bodyText = doc && doc.body ? doc.body.textContent.trim() : "";
if (bodyText.startsWith('{"error":')) {
reloadIframeDueToError(currentVersionObj, iframe, iframeParent);
return;
}
}
} catch (_) {}
// Normal load processing.
const activeElement = document.activeElement;
const isUserTyping = activeElement && (
activeElement.tagName === 'TEXTAREA' ||
(activeElement.tagName === 'INPUT' && activeElement.type === 'text') ||
activeElement.isContentEditable
);
// Only focus the iframe if the user isn't typing
if (!isUserTyping) {
iframe.contentWindow.focus();
}
// Title will be updated via postMessage from child when cross-origin
try { updateWindowTitle(); } catch (_) {}
// Add keydown handler only if same-origin; cross-origin child handles this via injected script
try {
const sameOrigin = new URL(iframe.src, window.location.href).origin === window.location.origin;
if (sameOrigin) {
iframe.contentWindow.addEventListener('keydown', (e) => {
const arrowKeys = new Set([37, 38, 39, 40]);
if (arrowKeys.has(e.keyCode)) {
e.preventDefault(); // Stop default arrow key behavior
e.stopPropagation(); // Prevent event from reaching the parent
}
});
}
} catch (_) {}
reportLintErrorsOnce();
});
// Attach error event listener as a fallback.
iframe.addEventListener('error', function () {
reloadIframeDueToError(currentVersionObj, iframe, iframeParent);
});
setTimeout(reportLintErrorsOnce, 0);
setTimeout(reportLintErrorsOnce, 750);
}
// If a stored URL exists, use it; otherwise, post to obtain one.
if (currentVersionObj.tempUrl) {
loadIframe(currentVersionObj.tempUrl);
} else {
postToTempPage().then(newUrl => {
loadIframe(newUrl);
}).catch(error => {
resumePreviewErrorReporting();
loading_spinner.hide();
showToast('error', `Failed to load iframe content: ${error.message}`);
console.error('Failed to load iframe:', error);
});
}
}
var gloalLastSendPrompt = "";
async function handleSendButtonClick() {
const userMessage = document.getElementById('userMessage').value;
if (!userMessage) {
showToast('error', 'Please enter something you would like to make first');
return;
}
// Check if the text is consistent with the intended "CREATE" mode.
if (typeof currentVersion !== 'undefined' && currentVersion > 0) {
if (!(await confirmActionMode(userMessage, "CREATE"))) return;
}
if (isSoundOn) audioPlayer.play();
const button = document.getElementById('sendButton');
updateButtonProgress(button, 'loading');
gloalLastSendPrompt = userMessage;
try {
const post_data = {
userMessage: userMessage
};
if (document.getElementById('sendWithNextRequest').checked) {
const screenshotImage = screenshotter_image_importer.getImage();
if (screenshotImage !== null) {
post_data.screenshotImage = screenshotImage;
}
}
// Using sendmessage to send the userMessage to the `/prompt_to_code` route
var htmlContent = await sendmessage('prompt_to_code', post_data);
// Assuming the Flask server's `/prompt_to_code` function returns the HTML content directly
//htmlContent, lastAction = "None", lastPrompt = "", referenceVersion = 0, referenceProject = "", referencePageID = "")
await saveVersion(htmlContent, 'send', userMessage, 0, currentProjectId, "" );
refreshIframeWithHTMLContent(htmlContent);
updateButtonProgress(button, 'normal');
} catch (error) {
updateButtonProgress(button, 'normal');
console.error('Error updating iframe:', error);
}
}
document.getElementById('sendButton').addEventListener('click', handleSendButtonClick);
async function saveEditedCode(htmlContent)
{
// Compare the current version with the new HTML content
if (htmlContent === getCurrentVersionCode()) {
showToast('info', 'No changes made.');
console.log('No changes made while editing.');
return;
}
const cleanMetadata = await ensureHtmlCleanBeforeVersionSave(
htmlContent,
'code_edit',
currentVersion,
null
);
await saveVersion(htmlContent, 'code_edit', '', currentVersion, currentProjectId, "", null, cleanMetadata);
refreshIframeWithHTMLContent(htmlContent);
const requestId = window.enhancedToastSystem ? window.enhancedToastSystem.getClientRequestId() : null;
const detailedMessage = requestId ? `
Edit Completion
Status: Successfully Applied
Request ID:${requestId.split('-')[0]}
Your changes have been applied to the current version.
` : null;
if (window.enhancedToastSystem) {
window.enhancedToastSystem.success('Edits Applied', 'Your changes have been successfully applied.', {
detailedMessage,
duration: 3000
});
} else {
showToast('info', 'Edits applied.');
}
}
async function ensureHtmlCleanBeforeVersionSave(htmlContent, lastAction, referenceVersion = 0, existingCleanMetadata = null) {
const normalizedHtml = stripNonHTMLContent(htmlContent);
if (!normalizedHtml) {
throw new Error('No valid HTML content found.');
}
if (getHtmlCleanState(existingCleanMetadata) === HTML_CLEAN_STATE_VERIFIED) {
const verifiedHash = String(existingCleanMetadata?.html_sha256 || '').trim();
if (!verifiedHash) {
return existingCleanMetadata;
}
const normalizedHash = await computeFullSha256Hex(normalizedHtml);
if (verifiedHash === normalizedHash) {
return {
...existingCleanMetadata,
html_sha256: normalizedHash
};
}
}
const normalizedReferenceVersion = normalizeVersionNumber(referenceVersion);
const referenceVersionState = getVersionState(currentProjectId, normalizedReferenceVersion);
if (
currentProjectId &&
Number.isFinite(normalizedReferenceVersion) &&
normalizedReferenceVersion > 0 &&
getHtmlCleanState(referenceVersionState) === HTML_CLEAN_STATE_VERIFIED &&
referenceVersionState?.html &&
!referenceVersionState?.tempUrl
) {
try {
await persistVerifiedReferenceVersionForCleanlinessCheck(
currentProjectId,
normalizedReferenceVersion,
referenceVersionState
);
} catch (error) {
console.warn(
'[PII HTML save] Failed to pre-persist trusted reference version; falling back to direct candidate scan.',
error
);
}
}
const response = await fetch('/api/html/verify-cleanliness', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
html_content: normalizedHtml,
project_id: currentProjectId || '',
reference_version: Number.isFinite(normalizedReferenceVersion) ? normalizedReferenceVersion : 0,
last_action: lastAction,
client_request_unique_key: window.lastClientRequestUniqueKey || null
})
});
const payload = await response.json().catch(() => ({}));
if (!response.ok) {
const error = new Error(payload.error || 'Failed to verify HTML cleanliness.');
error.status = response.status;
error.errorType = payload.error_type || null;
error.piiCheckId = payload.pii_check_id || null;
throw error;
}
return payload.clean_metadata || null;
}
async function persistVerifiedReferenceVersionForCleanlinessCheck(projectId, versionNumber, versionState) {
if (!projectId || !versionState || typeof versionState !== 'object') {
return null;
}
if (getHtmlCleanState(versionState) !== HTML_CLEAN_STATE_VERIFIED || !versionState.html) {
return null;
}
const response = await fetchWithExponentialBackoff('/temp_page', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
html_content: versionState.html,
origin_html_content: versionState.html,
current_version: {
...buildTempPageVersionPayload(versionState),
project_id: projectId,
version_number: versionNumber,
}
})
});
const payload = await response.json().catch(() => ({}));
if (!response.ok) {
throw new Error(payload.error || 'Failed to persist the trusted reference version.');
}
if (payload && payload.url) {
versionState.tempUrl = payload.url;
}
return payload;
}
function stripNonHTMLContent(inputString) {
// Use a regular expression to find content that starts with and ends with
// The 's' flag is used to enable dot to match newline characters as well
// Normalize common reload variants with a cross-origin-safe parent fallback.
inputString = inputString.replace(
/(?:\b(?:document|window|self|globalThis)\s*\.\s*location|\blocation)\s*\.\s*reload\s*\(\s*\)/g,
"(function(){try{if(window.parent&&window.parent!==window&&typeof window.parent.rebootIframe==='function'){window.parent.rebootIframe();return;}}catch(_){ }window.location.reload();})()"
);
const htmlContentMatch = inputString.match(//i);
// Check if a match was found
if (htmlContentMatch && htmlContentMatch[0]) {
return htmlContentMatch[0]; // Return the HTML content
} else {
// If no ... content is found, return an error or an empty string
console.error('No valid HTML content found.');
return '';
}
}
function replaceResourceLinks(inputString) {
const getHost = (url) => { try { return new URL(url).hostname; } catch (_) { return ''; } };
const currentHost = (() => { try { return new URL(window.location.origin).hostname; } catch (_) { return window.location.hostname || ''; } })();
const fcHost = (typeof FUZZYCODE_HOST !== 'undefined' && FUZZYCODE_HOST) ||
(window.__serviceUrls__ && getHost(window.__serviceUrls__.fuzzycode)) ||
currentHost;
const ugcHost = (typeof UGC_HOST !== 'undefined' && UGC_HOST) ||
(window.__serviceUrls__ && getHost(window.__serviceUrls__.ugc)) ||
'usercontent.fuzzycode.dev';
const IMAGES_REPL = `${ugcHost}/@images`;
const SOUNDS_REPL = `${ugcHost}/@sounds`;
const CDN_REPL = `${ugcHost}/@cdn`;
const SPRITES_REPL = `${fcHost}/@sprites`;
const PAGES_REPL = `${fcHost}/@pages`;
const UPLOADS_REPL = `${fcHost}/@uploads`;
const ASSETS_REPL = `${fcHost}/@aws`;
const split = (h) => (h || '').toLowerCase().split('.').filter(Boolean);
const parts = split(fcHost);
const apex = parts.slice(-2).join('.');
const prefix = parts.length > 2 ? parts.slice(0, -2).join('.') : '';
const hostsFor = (service) => {
const list = [`${service}.${apex}`];
if (prefix) list.push(`${service}.${prefix}.${apex}`);
return list;
};
const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const replaceMany = (hosts, replacement) => {
for (const host of hosts) {
const rx = new RegExp(`(^|[^A-Za-z0-9_])${esc(host)}([^A-Za-z0-9_]|$)`, 'gi');
inputString = inputString.replace(rx, `$1${replacement}$2`);
}
};
replaceMany(hostsFor('images'), IMAGES_REPL);
replaceMany(hostsFor('sounds'), SOUNDS_REPL);
replaceMany(hostsFor('cdn'), CDN_REPL);
replaceMany(hostsFor('sprites'), SPRITES_REPL);
replaceMany(hostsFor('pages'), PAGES_REPL);
replaceMany(hostsFor('uploads'), UPLOADS_REPL);
replaceMany(hostsFor('aws'), ASSETS_REPL);
const apexHosts = [apex];
if (prefix) apexHosts.push(`${prefix}.${apex}`);
const ensureApexRoutes = () => {
const mappings = [
{ route: 'images', replacement: IMAGES_REPL },
{ route: 'sounds', replacement: SOUNDS_REPL },
{ route: 'cdn', replacement: CDN_REPL },
{ route: 'sprites', replacement: SPRITES_REPL },
{ route: 'pages', replacement: PAGES_REPL },
{ route: 'uploads', replacement: UPLOADS_REPL },
{ route: 'aws', replacement: ASSETS_REPL }
];
for (const host of apexHosts) {
const escapedHost = esc(host);
for (const { route, replacement } of mappings) {
const pattern = new RegExp(`(https?://)${escapedHost}/@${route}`, 'gi');
inputString = inputString.replace(pattern, (_, scheme) => `${scheme}${replacement}`);
}
}
};
ensureApexRoutes();
return inputString;
}
/*document.getElementById('samplePrompts').addEventListener('change', function() {
document.getElementById('userMessage').value = this.value;
});*/
// Listen for sample prompt selections and update the text area.
const dropdownElement = document.getElementById('sampleDropdown');
if (dropdownElement) {
dropdownElement.addEventListener('sample-prompt-selected', (event) => {
const outputArea = document.getElementById('userMessage');
outputArea.value = event.detail.value;
});
}
const saveButton = document.getElementById('saveButton');
if (saveButton) {
saveButton.addEventListener('click', async function() {
const projectName = document.getElementById('projectName').value;
const currentVersionData = projectVersionHistory?.[currentProjectId]?.[currentVersion] || null;
if (projectName && currentVersionData) {
try {
const localProjectRecord = await buildLocalProjectSaveRecord(projectName, currentVersionData);
await saveToIndexedDB(projectName, localProjectRecord);
const requestId = window.enhancedToastSystem ? window.enhancedToastSystem.getClientRequestId() : null;
const detailedMessage = requestId ? `
Save Details
Project Name: ${projectName}
Storage: Local IndexedDB
Request ID:${requestId.split('-')[0]}
Project saved successfully to local storage.
` : null;
if (window.enhancedToastSystem) {
window.enhancedToastSystem.success('Project Saved', `Project '${projectName}' saved!`, {
detailedMessage,
duration: 3000
});
} else {
showToast('info', `Project '${projectName}' saved!`);
}
updateProjectsDropdown(); // Refresh the projects dropdown
} catch (err) {
if (window.enhancedToastSystem) {
window.enhancedToastSystem.error('Save Failed', `Failed to save project: ${err.message}`, {
detailedMessage: `
Save Error Details
Project Name: ${projectName}
Error Type: ${err.name}
Error Message: ${err.message}
Local storage may be full or unavailable.
`
});
} else {
showToast('error', `Failed to save project: ${err.message}`);
}
}
} else if (!projectName) {
showToast('error', 'Please enter a project name.');
} else {
showToast('error', 'No current project version is available to save.');
}
});
}
const loadButton = document.getElementById('loadButton');
if (loadButton) {
loadButton.addEventListener('click', async function() {
const projectName = document.getElementById('projectsDropdown').value;
loadProject(projectName);
});
}
const localHistoryButton = document.getElementById('localHistoryButton');
if (localHistoryButton) {
localHistoryButton.addEventListener('click', function() {
if (popup === null) popup = new PopupWindow();
popup.show({
title: 'Local History',
content: '/local_recent_history',
isIframe: true,
onClose: (event) => {
console.log('Local History popup closed', event);
popup = null; // Clear reference when closed
}
});
});
}
function getDesignSystemRuntime() {
if (typeof window === 'undefined') return null;
const runtime = window.FuzzyCodeDesignSystem;
return runtime && typeof runtime === 'object' ? runtime : null;
}
// Wave dots loading animation helpers
function addLoadingDots(button) {
const runtime = getDesignSystemRuntime();
if (runtime && typeof runtime.addLoadingDots === 'function') {
runtime.addLoadingDots(button);
return;
}
// Remove any existing dots first
const existing = button.querySelector('.loading-dot-container');
if (existing) existing.remove();
// Create container
const container = document.createElement('div');
container.className = 'loading-dot-container';
// Create 3 dots
for (let i = 0; i < 3; i++) {
const dot = document.createElement('div');
dot.className = 'loading-dot';
container.appendChild(dot);
}
// Add to button
button.appendChild(container);
}
function removeLoadingDots(button) {
const runtime = getDesignSystemRuntime();
if (runtime && typeof runtime.removeLoadingDots === 'function') {
runtime.removeLoadingDots(button);
return;
}
const container = button.querySelector('.loading-dot-container');
if (container) container.remove();
}
// Disable/enable LLM-related buttons to prevent double-clicks during processing
function disableLLMButtons(disabled) {
const llmButtons = [
'#generateBtn',
'#applyChangesBtn',
'#clearBtn',
'.suggest-tab',
'.suggestion-card-btn',
'#suggestBackBtn'
];
llmButtons.forEach(selector => {
document.querySelectorAll(selector).forEach(btn => {
btn.disabled = disabled;
if (disabled) {
btn.style.pointerEvents = 'none';
btn.style.opacity = '0.6';
} else {
btn.style.pointerEvents = '';
btn.style.opacity = '';
}
});
});
}
function updateButtonProgress(button, status) {
const runtime = getDesignSystemRuntime();
if (runtime && typeof runtime.setButtonProgress === 'function') {
runtime.setButtonProgress(button, status);
return;
}
if (!button) return;
const isNativeButton = String(button.tagName || '').toUpperCase() === 'BUTTON';
if (status === 'loading') {
button.classList.remove('button-normal');
button.classList.add('button-loading');
if (isNativeButton) {
if (!button.hasAttribute('data-loading-prev-disabled')) {
button.setAttribute('data-loading-prev-disabled', button.disabled ? 'true' : 'false');
}
button.disabled = true;
}
addLoadingDots(button);
} else if (status === 'normal') {
button.classList.remove('button-loading');
button.classList.add('button-normal');
if (isNativeButton) {
const prevDisabled = button.getAttribute('data-loading-prev-disabled');
if (prevDisabled !== null) {
button.disabled = prevDisabled === 'true';
button.removeAttribute('data-loading-prev-disabled');
} else {
button.disabled = false;
}
}
removeLoadingDots(button);
}
}
async function suggestActions(actionType) {
const actionButton = document.querySelector(`.suggest-tab[data-category="${actionType}"]`);
if (actionButton) updateButtonProgress(actionButton, 'loading');
const iframeContent = projectVersionHistory[currentProjectId][currentVersion]["html"];
const post_data = {
actionType: actionType,
iframeContent: iframeContent,
projectId: currentProjectId,
versionNumber: currentVersion,
currentVersionDiff: projectVersionHistory[currentProjectId][currentVersion]?.diff || null
};
try {
var suggestionsResponse = await sendmessage('suggest_actions', post_data);
const suggestions = JSON.parse(suggestionsResponse.suggestions);
displayActionSuggestions(suggestions.Buttons, actionType);
} catch (error) {
console.error(`Error suggesting ${actionType}:`, error);
// Show error in panel
const list = document.getElementById('suggestionsList');
if (list) {
list.innerHTML = '
Failed to load suggestions
';
}
} finally {
if (actionButton) updateButtonProgress(actionButton, 'normal');
}
}
// Clear suggestions cache (call when project/version changes)
// If stayInMode is true, keep suggestion mode open but mark cache as stale (don't auto-refresh)
function clearSuggestionsCache(stayInMode = false) {
const wasInSuggestionMode = document.getElementById('Update')?.classList.contains('suggestion-mode');
// Clear the cache data (suggestions are now stale)
suggestionsCache.fixes = null;
suggestionsCache.improvements = null;
suggestionsCache.newFeatures = null;
const panel = document.getElementById('suggestionsPanel');
const list = document.getElementById('suggestionsList');
const updateTab = document.getElementById('Update');
if (stayInMode && wasInSuggestionMode) {
// Stay in suggestion mode, keep showing current suggestions
// User can click the category button again to refresh, or pick another suggestion
// Don't clear currentSuggestionCategory so UI stays consistent
} else {
// Exit suggestion mode completely
currentSuggestionCategory = null;
document.querySelectorAll('.suggest-tab').forEach(tab => tab.classList.remove('active'));
if (panel) panel.classList.remove('visible');
if (list) list.innerHTML = '';
if (updateTab) updateTab.classList.remove('suggestion-mode');
}
}
// Update the event listener for suggesting fixes to use the new suggestActions function
// Tabbed suggestion button handlers
document.getElementById('suggestFixesButton').addEventListener('click', async () => {
switchSuggestionTab('fixes');
if (!suggestionsCache.fixes) {
await suggestActions('fixes');
}
});
document.getElementById('suggestImprovementsButton').addEventListener('click', async () => {
switchSuggestionTab('improvements');
if (!suggestionsCache.improvements) {
await suggestActions('improvements');
}
});
document.getElementById('suggestNewFeaturesButton').addEventListener('click', async () => {
switchSuggestionTab('newFeatures');
if (!suggestionsCache.newFeatures) {
await suggestActions('newFeatures');
}
});
// Cache for suggestions per category
const suggestionsCache = {
fixes: null,
improvements: null,
newFeatures: null
};
let currentSuggestionCategory = null;
// Display suggestions as full-width cards in the main panel
function displayActionSuggestions(buttons, actionType) {
// Cache the buttons for this category
suggestionsCache[actionType] = buttons;
// Show these suggestions in the main list
showSuggestionsInPanel(buttons, actionType);
}
// Render suggestions in the main panel
function showSuggestionsInPanel(buttons, actionType) {
const list = document.getElementById('suggestionsList');
const panel = document.getElementById('suggestionsPanel');
if (!list || !panel) return;
list.innerHTML = '';
if (!buttons || buttons.length === 0) {
list.innerHTML = '
No suggestions found
';
panel.classList.add('visible');
updateSuggestionsScrollButtons();
return;
}
buttons.forEach(buttonInfo => {
const card = document.createElement('div');
card.className = 'suggestion-card';
// Customize button (edit icon in corner)
const customizeBtn = document.createElement('button');
customizeBtn.className = 'suggestion-card-customize';
customizeBtn.innerHTML = '';
customizeBtn.title = 'Customize this suggestion';
const content = document.createElement('div');
content.className = 'suggestion-card-content';
// Description at top
const desc = document.createElement('div');
desc.className = 'suggestion-card-desc';
desc.textContent = buttonInfo.prompt;
// Full text as native tooltip so long prompts aren't lost when
// the card clips. CSS fades the visible text to suggest there's
// more; the tooltip reveals what.
desc.title = buttonInfo.prompt;
// Action button at bottom with title text
const btn = document.createElement('button');
btn.className = 'suggestion-card-btn';
btn.textContent = buttonInfo.name;
// If the name itself is long enough to get ellipsed, a tooltip
// disambiguates. Cheap to set unconditionally.
btn.title = buttonInfo.name;
card.appendChild(customizeBtn);
content.appendChild(desc);
card.appendChild(content);
card.appendChild(btn);
// Customize click - copy to textarea with customization prompt
customizeBtn.addEventListener('click', (e) => {
e.stopPropagation();
const textarea = document.getElementById('customChanges');
const updateTab = document.getElementById('Update');
if (textarea && updateTab) {
textarea.value = `I'd like to '${buttonInfo.prompt}' but instead of doing exactly that, do this: `;
updateTab.classList.remove('suggestion-mode');
document.querySelectorAll('.suggest-tab').forEach(tab => tab.classList.remove('active'));
textarea.focus();
// Move cursor to end
textarea.selectionStart = textarea.selectionEnd = textarea.value.length;
}
});
// Button click handler with loading state
btn.addEventListener('click', async (e) => {
e.stopPropagation();
if (card.classList.contains('loading')) return;
// Disable all LLM-related buttons
card.classList.add('loading');
btn.classList.add('is-loading');
const originalText = btn.textContent;
addLoadingDots(btn);
disableLLMButtons(true);
try {
var fast_fix = false;
if (currentEditMode == "swipe") {
const approximate_tokens = calculateTokenApproximation(getCurrentVersionCode()).approxTokens;
if (approximate_tokens > 2000) { fast_fix = true; }
}
if (currentEditMode == "swipe_fast") { fast_fix = true; }
await applyAction(buttonInfo.prompt, actionType, fast_fix);
} finally {
card.classList.remove('loading');
btn.classList.remove('is-loading');
removeLoadingDots(btn);
btn.textContent = originalText;
disableLLMButtons(false);
}
});
list.appendChild(card);
});
panel.classList.add('visible');
// Enter suggestion mode
const updateTab = document.getElementById('Update');
if (updateTab) updateTab.classList.add('suggestion-mode');
// Update scroll buttons after rendering
setTimeout(updateSuggestionsScrollButtons, 50);
}
// Update scroll button visibility based on scroll position (debounced)
let scrollButtonUpdateTimeout = null;
function updateSuggestionsScrollButtons() {
// Debounce to prevent rapid toggling during smooth scroll
if (scrollButtonUpdateTimeout) clearTimeout(scrollButtonUpdateTimeout);
scrollButtonUpdateTimeout = setTimeout(() => {
const list = document.getElementById('suggestionsList');
const leftBtn = document.getElementById('suggestionsScrollLeft');
const rightBtn = document.getElementById('suggestionsScrollRight');
if (!list || !leftBtn || !rightBtn) return;
// Use larger threshold to prevent edge flickering
const threshold = 20;
const canScrollLeft = list.scrollLeft > threshold;
const canScrollRight = list.scrollLeft < (list.scrollWidth - list.clientWidth - threshold);
leftBtn.classList.toggle('visible', canScrollLeft);
rightBtn.classList.toggle('visible', canScrollRight);
}, 100);
}
// Initialize suggestions scroll functionality
function initSuggestionsScroll() {
const list = document.getElementById('suggestionsList');
const leftBtn = document.getElementById('suggestionsScrollLeft');
const rightBtn = document.getElementById('suggestionsScrollRight');
if (!list || !leftBtn || !rightBtn) return;
// Scroll amount per click (roughly 1.5 cards)
const scrollAmount = 180;
leftBtn.addEventListener('click', () => {
list.scrollBy({ left: -scrollAmount, behavior: 'smooth' });
});
rightBtn.addEventListener('click', () => {
list.scrollBy({ left: scrollAmount, behavior: 'smooth' });
});
// Update button visibility on scroll
list.addEventListener('scroll', updateSuggestionsScrollButtons);
// Also update on window resize
window.addEventListener('resize', updateSuggestionsScrollButtons);
}
// Initialize on DOM ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initSuggestionsScroll);
} else {
initSuggestionsScroll();
}
// Switch to a suggestion category tab
function switchSuggestionTab(actionType) {
// Update active tab styling
document.querySelectorAll('.suggest-tab').forEach(tab => {
tab.classList.toggle('active', tab.dataset.category === actionType);
});
currentSuggestionCategory = actionType;
// If we have cached suggestions, show them immediately
if (suggestionsCache[actionType]) {
showSuggestionsInPanel(suggestionsCache[actionType], actionType);
} else {
// Show loading state in panel
const list = document.getElementById('suggestionsList');
const panel = document.getElementById('suggestionsPanel');
const updateTab = document.getElementById('Update');
if (list && panel) {
list.innerHTML = '
Loading...
';
panel.classList.add('visible');
if (updateTab) updateTab.classList.add('suggestion-mode');
updateSuggestionsScrollButtons();
}
}
}
const LARGE_PROJECT_FAST_MODE_THRESHOLD = 5000;
function suppressPreviewErrorReporting() {
previewErrorReportingSuppressed = true;
}
function resumePreviewErrorReporting() {
previewErrorReportingSuppressed = false;
}
async function applyAction(actionPrompt,
actionType = null,
fastMode = false,
agentNameOverride = null,
allowLargeProjectPrompt = true) {
const suppressErrorsForAction = actionType === "fixBugFromException";
if (suppressErrorsForAction) {
suppressPreviewErrorReporting();
}
try {
const iframeContent = projectVersionHistory[currentProjectId][currentVersion]["html"];
// Check token count if not already in fast mode
if (!fastMode && allowLargeProjectPrompt) {
const tokenCount = calculateTokenApproximation(iframeContent).approxTokens;
if (tokenCount > LARGE_PROJECT_FAST_MODE_THRESHOLD) {
// Show confirmation dialog using SweetAlert2
const result = await Swal.fire({
title: 'Large Project Detected',
html: `This project has over ${LARGE_PROJECT_FAST_MODE_THRESHOLD.toLocaleString()} tokens which might make changes slower and less reliable.
Would you like to use Fast Mode instead? (Recommended for simpler changes)`,
icon: 'question',
showCancelButton: true,
showDenyButton: true,
confirmButtonText: 'Use Fast Mode',
denyButtonText: 'Continue Normally',
cancelButtonText: 'Cancel',
confirmButtonColor: '#3085d6',
denyButtonColor: '#858585',
cancelButtonColor: '#d33'
});
if (result.isDismissed) {
if (suppressErrorsForAction) {
resumePreviewErrorReporting();
}
return; // User clicked Cancel
}
// Update fastMode based on user choice
fastMode = result.isConfirmed; // true if they clicked "Use Fast Mode"
}
}
var currentContext = "";
if (historyAttachmentsUI && historyAttachmentsUI.currentSelection && historyAttachmentsUI.currentSelection.history && historyAttachmentsUI.currentSelection.history.prompt_text) currentContext = historyAttachmentsUI.currentSelection.history.prompt_text
const post_data = {
htmlContent: iframeContent,
actionPrompt: actionPrompt,
actionType: actionType,
context: currentContext,
currentVersionDiff: projectVersionHistory[currentProjectId][currentVersion]?.diff || null,
projectId: currentProjectId,
versionNumber: currentVersion
};
if (agentNameOverride) {
post_data.agentName = agentNameOverride;
}
if (document.getElementById('sendWithNextRequest').checked) {
const screenshotImage = screenshotter_image_importer.getImage();
if (screenshotImage !== null) {
post_data.screenshotImage = screenshotImage;
}
}
gloalLastMakeChangePrompt = actionPrompt;
if (currentEditMode === 'fast')
fastMode = true;
var response = await sendmessage(fastMode ? 'apply_action_fast' : 'apply_action', post_data);
var modifiedHtmlContent = response.modifiedHtmlContent;
var htmlDiff = response.htmlDiff;
var status = response.status;
// Store server log key for admin debugging
if (response.server_log_key) {
upsertRequestDebugContext({
clientRequestId: post_data.client_request_unique_key,
route: fastMode ? '/apply_action_fast' : '/apply_action',
actionType: actionType || 'General',
serverLogKey: response.server_log_key
});
}
if (status === "no_change_made")
{
if (suppressErrorsForAction) {
resumePreviewErrorReporting();
}
// Enhanced "no changes" notification with context
if (window.enhancedToastSystem) {
const detailedMessage = `
Action Details
Prompt: "${actionPrompt}"
Mode: ${fastMode ? 'Fast Mode' : 'Normal Mode'}
Action Type: ${actionType || 'General'}
Suggestions:
Try rephrasing your request
Be more specific about what you want to change
Check if the target element exists in your code
`;
window.enhancedToastSystem.warning('No Changes Made', 'The AI couldn\'t determine what changes to make from your request.', {
detailedMessage,
duration: 6000
});
} else {
// Enhanced "no changes" notification with context
if (window.enhancedToastSystem) {
const detailedMessage = `
Action Details
Prompt: "${actionPrompt}"
Mode: ${fastMode ? 'Fast Mode' : 'Normal Mode'}
Action Type: ${actionType || 'General'}
Suggestions:
Try rephrasing your request
Be more specific about what you want to change
Check if the target element exists in your code
`;
window.enhancedToastSystem.warning('No Changes Made', 'The AI couldn\'t determine what changes to make from your request.', {
detailedMessage,
duration: 6000
});
} else {
showToast('error', `No code changes made, please try again.`);
}
}
return;
}
suppressPreviewErrorReporting();
await saveVersion(modifiedHtmlContent, 'change', actionPrompt, currentVersion, currentProjectId, "", htmlDiff);
clearResolvedPreviewErrors();
refreshIframeWithHTMLContent(modifiedHtmlContent);
} catch (error) {
resumePreviewErrorReporting();
console.error('Error applying action:', error);
// Enhanced error reporting for applyAction failures
if (window.enhancedToastSystem) {
const requestContext = (error && error.requestContext) ? error.requestContext : getRequestDebugContext();
const detailedMessage = `
Action Details
Prompt: "${actionPrompt}"
Mode: ${fastMode ? 'Fast Mode' : 'Normal Mode'}
Action Type: ${actionType || 'General'}
Error Type: ${error.name}
Technical Details:
${error.message}
This error occurred during the action processing phase.
`;
window.enhancedToastSystem.showError('Action Processing Failed', 'An error occurred while processing your request.', {
detailedMessage,
duration: 8000,
requestContext
});
} else {
showToast('error', 'Error applying action: ' + error.message);
}
}
}
function switchTab(tabName) {
var tabIdMap = {
'Create': 'createTab',
'Update': 'updateTab',
'SaveLoadShare': 'saveloadshareTab'
};
if (tabIdMap[tabName]) {
document.getElementById(tabIdMap[tabName]).click();
}
}
var erudaOn = false;
var sentryOn = false;
var enhancedErrorsEnabled = false;
// Shared Function to Load HTML from URL
async function loadHtmlFromUrl(url, actionType = 'load_from_url') {
try {
const pageHash = extractPagesHashFromUrl(url);
if (pageHash) {
await importPagesHtmlByHash(pageHash, url);
showToast('info', `Published page loaded from URL: ${url}`);
return;
}
if (!window.FUZZY_IS_ADMIN) {
throw new Error('Only verified Pages URLs can be imported directly.');
}
// Fetch the HTML content from the URL
const response = await fetch(url, { credentials: 'include' });
if (!response.ok) {
throw new Error(`Failed to fetch URL: ${response.status} ${response.statusText}`);
}
const htmlContent = await response.text();
// Save the fetched HTML content as a new version
await saveVersion(htmlContent, actionType, '', 0, currentProjectId, url);
// Refresh the iframe with the fetched HTML content
refreshIframeWithHTMLContent(htmlContent);
// Switch to the 'Update' tab
switchTab('Update');
// Notify the user
showToast('info', `HTML content loaded from URL: ${url}`);
} catch (error) {
// Handle any errors that occur during the fetch or processing
console.error(`Error loading HTML from URL (${url}):`, error);
showToast('error', `Failed to load HTML from URL: ${error.message}`);
}
}
document.addEventListener('DOMContentLoaded', async function() {
const urlParams = new URLSearchParams(window.location.search);
// Check for 'eruda' in query string and set erudaOn to true if present
const erudaParam = urlParams.get('eruda');
if (erudaParam !== null) {
erudaOn = true;
document.getElementById('erudaToggle').checked = true;
console.log('Eruda mode is enabled');
}
const sentryParam = urlParams.get('sentry');
if (sentryParam !== null) {
enhancedErrorsEnabled = true;
document.getElementById('enhancedErrorsToggle').checked = true;
console.log('Enhanced errors enabled via URL parameter');
}
const fromURL = urlParams.get('fromURL');
if (fromURL) {
loadHtmlFromUrl(fromURL, 'load_fromURL_parameter');
}
// New code to handle create_prompt and action_prompt query string parameters
const createPrompt = urlParams.get('create_prompt');
if (createPrompt) {
document.getElementById('userMessage').value = createPrompt;
}
const actionPrompt = urlParams.get('action_prompt');
if (actionPrompt) {
updateActionPrompt(actionPrompt);
}
updateProjectsDropdown();
});
function updateActionPrompt(actionPrompt, append) {
const customChangesElement = document.getElementById('customChanges');
if (append) {
customChangesElement.value += actionPrompt;
} else {
customChangesElement.value = actionPrompt;
}
switchTab('Update');
}
//function refreshIframeWithUrl(url) {
// var iframe = document.getElementById('responseIframe'); // Adjust this ID to match //your actual iframe
// iframe.src = url;
//}
function openTab(evt, tabName) {
var i, tabcontent, tablinks;
tabcontent = document.getElementsByClassName("tab-content");
for (i = 0; i < tabcontent.length; i++) {
tabcontent[i].style.display = "none";
}
tablinks = document.getElementsByClassName("tab-button");
for (i = 0; i < tablinks.length; i++) {
tablinks[i].className = tablinks[i].className.replace(" active", "");
}
document.getElementById(tabName).style.display = "flex";
evt.currentTarget.className += " active";
}
// Call this function to default to the first tab on load
document.addEventListener('DOMContentLoaded', function() {
document.getElementsByClassName('tab-button')[0].click();
showToast('info', 'Bring your imagination to life by creating your own games, creating your own drawing apps, music apps, or anything you can imagine (not too too complex)!');
showToast('info', 'Welcome to Fuzzycode!');
});
// Project Version Management
var projectVersionHistory = {}; // Structure: { projectId: { versionNumber: htmlContent, ... }, ... }
var currentProjectId = null;
var currentVersion = 0;
function normalizeVersionNumber(versionNumber) {
if (typeof versionNumber === 'number' && Number.isFinite(versionNumber)) return versionNumber;
if (typeof versionNumber === 'string') {
const trimmed = versionNumber.trim();
if (/^\d+$/.test(trimmed)) return Number(trimmed);
const match = trimmed.match(/(\d+)/);
if (match) return Number(match[1]);
}
return versionNumber;
}
function normalizeProjectVersionHistory(projectId) {
if (!projectId || !projectVersionHistory[projectId]) return;
const history = projectVersionHistory[projectId];
for (const key of Object.keys(history)) {
if (typeof key === 'string') {
const match = key.match(/^v(\d+)$/);
if (match) {
const numericKey = match[1];
if (!history[numericKey]) {
history[numericKey] = history[key];
}
}
}
}
}
function getNumericVersionKeys(projectId) {
if (!projectId || !projectVersionHistory[projectId]) return [];
normalizeProjectVersionHistory(projectId);
const history = projectVersionHistory[projectId] || {};
return Object.keys(history)
.map(key => (/^\d+$/.test(String(key).trim()) ? Number(key) : NaN))
.filter(Number.isFinite);
}
function updateCurrentVersion(versionNumber) {
const normalized = normalizeVersionNumber(versionNumber);
currentVersion = normalized;
if (currentProjectId) normalizeProjectVersionHistory(currentProjectId);
// Clear suggestions cache when version changes (code changed)
// Pass true to stay in suggestion mode if already in it
if (typeof clearSuggestionsCache === 'function') clearSuggestionsCache(true);
setTimeout(() => {
historyAttachmentsUI.reloadHistory(getVersionHistoryPath(normalized), normalized);
}, 300); /// slight delay because project history might not be updated yet
}
function clearResolvedPreviewErrors() {
if (errorManager && typeof errorManager.clearErrors === 'function') {
errorManager.clearErrors();
}
const versionObj = projectVersionHistory?.[currentProjectId]?.[currentVersion];
if (versionObj && Object.prototype.hasOwnProperty.call(versionObj, 'errors')) {
versionObj.errors = [];
}
}
function generateUniqueId() {
// Generate a timestamp-based ID with a leading character and a mix of letters and numbers
const timestamp = new Date().getTime();
const uniquePortion = Math.random().toString(36).substring(2, 15);
const actorPrefix = (window.sessionUser && window.sessionUser._userId) ? "u" : "_";
return `${actorPrefix}${timestamp.toString(36)}${uniquePortion}`;
}
function getMaxVersion()
{
const versions = getNumericVersionKeys(currentProjectId);
if (!versions.length) return 0;
return Math.max(...versions);
}
function updateVersionDisplay() {
//document.getElementById('versionDisplay').textContent = `Project ID: ${currentProjectId || 'N/A'}, Version: ${currentVersion}`;
document.getElementById('currentVersionDisplay').textContent = `${currentVersion}`;
document.getElementById('prevVersionBtn').disabled = currentVersion <= 1;
const maxVersion = getMaxVersion();
document.getElementById('nextVersionBtn').disabled = currentVersion >= maxVersion;
document.getElementById('refreshIframeBtn').disabled = currentVersion < 1;
// Enable or disable the diff button as needed
const diffButton = document.getElementById('diffButton');
if (currentVersion > 0) {
diffButton.disabled = false;
} else {
diffButton.disabled = true;
}
// Populate the version dropdown based on the available versions
populateVersionDropdown();
}
async function getVersionDescription(lastAction, lastPrompt, htmlDiff, htmlContent, pageTitle) {
// If the last action is one of the load types, return an immediate message.
const loadActions = new Set([
"load_from_share_url",
"load_from_pages",
"load_from_load",
"load_from_mongo",
"load_fromURL_parameter",
"load_from_url"
]);
const loadActionSources = {
load_from_share_url: "share URL",
load_from_pages: "Fuzzycode Pages",
load_from_load: "saved project",
load_from_mongo: "MongoDB",
load_fromURL_parameter: "Fuzzycode Pages",
load_from_url: "Fuzzycode Pages"
};
if (loadActions.has(lastAction)) {
return `Loaded page from ${loadActionSources[lastAction]}`;
}
const currentVersionObj = projectVersionHistory?.[currentProjectId]?.[currentVersion];
if (getHtmlCleanState(currentVersionObj) !== HTML_CLEAN_STATE_VERIFIED) {
if (lastPrompt) {
return lastPrompt.split(/\s+/).slice(0, 10).join(" ");
}
return pageTitle ? `Updated ${pageTitle}` : "Updated project";
}
// For "send", skip the AI call and simply use "Create " + pageTitle as the summary.
if (lastAction === "send") {
return `Create ${pageTitle}`;
}
// If there is no relevant prompt and no diff (or htmlContent), skip the AI call.
if ((!lastPrompt && !htmlDiff) && (!lastPrompt && !htmlContent)) {
return "";
}
let versionSummary = "";
try {
const summaryResponse = await fetch('/api/version-summary', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
last_action: lastAction,
last_prompt: lastPrompt,
html_diff: htmlDiff,
page_title: pageTitle,
client_request_unique_key: window.lastClientRequestUniqueKey || null
})
});
const summaryPayload = await summaryResponse.json().catch(() => null);
if (!summaryResponse.ok) {
console.error("Version summary error:", summaryPayload || summaryResponse.status);
} else if (summaryPayload && typeof summaryPayload.version_summary === 'string') {
versionSummary = summaryPayload.version_summary.trim();
}
} catch (err) {
console.error("Failed to get version summary from server:", err);
}
// Fallback: if no valid summary is obtained, use the first 10 words from lastPrompt if available.
if (!versionSummary) {
if (lastPrompt) {
versionSummary = lastPrompt.split(/\s+/).slice(0, 10).join(" ");
} else {
versionSummary = "";
}
}
return versionSummary;
}
// Computes a SHA-256 hash for the given htmlContent and returns the first 8 hex characters.
async function computeNonTimeHash(htmlContent) {
// Encode the content as UTF-8.
const encoder = new TextEncoder();
const data = encoder.encode(htmlContent);
// Compute the SHA-256 hash (returns an ArrayBuffer).
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
// Convert the ArrayBuffer to an array of bytes.
const hashArray = Array.from(new Uint8Array(hashBuffer));
// Convert bytes to a hex string.
const hashHex = hashArray.map(byte => byte.toString(16).padStart(2, '0')).join('');
// Return the first 8 characters.
return hashHex.slice(0, 8);
}
async function computeFullSha256Hex(textContent) {
const encoder = new TextEncoder();
const data = encoder.encode(textContent || '');
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map(byte => byte.toString(16).padStart(2, '0')).join('');
}
const HTML_CLEAN_STATE_VERIFIED = 'verified_clean';
const HTML_CLEAN_STATE_DIRTY = 'dirty_unverified';
const HTML_CLEAN_STATE_BLOCKED = 'blocked';
function getVersionState(projectId, versionNumber) {
if (!projectId || versionNumber === null || versionNumber === undefined) return null;
return projectVersionHistory?.[projectId]?.[versionNumber] || null;
}
function getHtmlCleanState(versionData) {
if (!versionData || typeof versionData !== 'object') return HTML_CLEAN_STATE_DIRTY;
const state = String(versionData.html_clean_state || '').trim().toLowerCase();
if (state === HTML_CLEAN_STATE_VERIFIED) return HTML_CLEAN_STATE_VERIFIED;
if (state === HTML_CLEAN_STATE_BLOCKED) return HTML_CLEAN_STATE_BLOCKED;
return HTML_CLEAN_STATE_DIRTY;
}
function getVersionCleanMetadata(versionData) {
if (!versionData || typeof versionData !== 'object') return null;
return {
html_clean_state: getHtmlCleanState(versionData),
html_clean_basis: versionData.html_clean_basis || '',
html_base_clean_hash: versionData.html_base_clean_hash || null,
html_sha256: versionData.html_sha256 || null,
html_clean_verified_at: versionData.html_clean_verified_at || null,
html_clean_block_reason: versionData.html_clean_block_reason || null,
html_attestation: versionData.html_attestation || null
};
}
function applyHtmlCleanMetadata(targetVersion, cleanMetadata) {
if (!targetVersion || !cleanMetadata || typeof cleanMetadata !== 'object') return;
targetVersion.html_clean_state = getHtmlCleanState(cleanMetadata);
targetVersion.html_clean_basis = cleanMetadata.html_clean_basis || '';
targetVersion.html_base_clean_hash = cleanMetadata.html_base_clean_hash || null;
targetVersion.html_sha256 = cleanMetadata.html_sha256 || null;
targetVersion.html_clean_verified_at = cleanMetadata.html_clean_verified_at || null;
targetVersion.html_clean_block_reason = cleanMetadata.html_clean_block_reason || null;
targetVersion.html_attestation = cleanMetadata.html_attestation || null;
}
function isVerifiedCleanMetadataForHtml(cleanMetadata, htmlSha256) {
if (!cleanMetadata || typeof cleanMetadata !== 'object') return false;
if (getHtmlCleanState(cleanMetadata) !== HTML_CLEAN_STATE_VERIFIED) return false;
const metadataHash = String(cleanMetadata.html_sha256 || '').trim();
return !metadataHash || !htmlSha256 || metadataHash === htmlSha256;
}
function normalizeResolvedCleanMetadata(cleanMetadata, fullHtmlHash, versionTime, fallbackBasis = 'trusted_import') {
if (!cleanMetadata || typeof cleanMetadata !== 'object') {
return {
html_clean_state: HTML_CLEAN_STATE_DIRTY,
html_clean_basis: fallbackBasis,
html_base_clean_hash: null,
html_sha256: fullHtmlHash,
html_clean_verified_at: null,
html_clean_block_reason: null,
html_attestation: null
};
}
const normalizedState = getHtmlCleanState(cleanMetadata);
return {
html_clean_state: normalizedState,
html_clean_basis: cleanMetadata.html_clean_basis || fallbackBasis,
html_base_clean_hash: cleanMetadata.html_base_clean_hash || null,
html_sha256: cleanMetadata.html_sha256 || fullHtmlHash,
html_clean_verified_at: normalizedState === HTML_CLEAN_STATE_VERIFIED
? (cleanMetadata.html_clean_verified_at || versionTime)
: null,
html_clean_block_reason: cleanMetadata.html_clean_block_reason || null,
html_attestation: cleanMetadata.html_attestation || null
};
}
function buildTempPageVersionPayload(versionData, fallbackAction = 'manual_change') {
const metadata = versionData && typeof versionData === 'object' ? versionData : {};
return {
project_id: currentProjectId,
version_number: currentVersion,
last_action: metadata.lastAction || fallbackAction,
last_prompt: metadata.lastPrompt || '',
reference_version: metadata.referenceVersion,
reference_project: metadata.referenceProject,
reference_page_id: metadata.referencePageID,
diff: metadata.diff || null,
html_clean_state: getHtmlCleanState(metadata),
html_clean_basis: metadata.html_clean_basis || '',
html_base_clean_hash: metadata.html_base_clean_hash || null,
html_sha256: metadata.html_sha256 || null,
html_clean_verified_at: metadata.html_clean_verified_at || null,
html_clean_block_reason: metadata.html_clean_block_reason || null,
html_attestation: metadata.html_attestation || null,
};
}
function deriveVersionCleanState(lastAction, referenceVersionObj) {
if (lastAction === 'send') {
return {
html_clean_state: HTML_CLEAN_STATE_VERIFIED,
html_clean_basis: 'create_ai',
html_base_clean_hash: null,
};
}
if (lastAction === 'change' && getHtmlCleanState(referenceVersionObj) === HTML_CLEAN_STATE_VERIFIED) {
return {
html_clean_state: HTML_CLEAN_STATE_VERIFIED,
html_clean_basis: 'update_ai',
html_base_clean_hash: referenceVersionObj?.html_sha256 || null,
};
}
return {
html_clean_state: HTML_CLEAN_STATE_DIRTY,
html_clean_basis: lastAction || 'manual_change',
html_base_clean_hash: null,
};
}
function extractPagesHashFromUrl(rawUrl) {
try {
const parsed = new URL(String(rawUrl || '').trim(), window.location.href);
const expectedHost = new URL(PAGES_ORIGIN).hostname;
if (parsed.hostname !== expectedHost) return null;
const parts = parsed.pathname.split('/').filter(Boolean);
if (parts.length >= 2 && parts[0] === 'page') return parts[1];
if (parts.length >= 3 && parts[0] === 'api' && parts[1] === 'signed_import') return parts[2];
return null;
} catch (error) {
return null;
}
}
async function importPagesHtmlByHash(pageHash, importUrl) {
const response = await fetch(`/api/pages/import/${encodeURIComponent(pageHash)}`, {
method: 'GET',
credentials: 'include',
headers: {
'Accept': 'application/json'
}
});
const payload = await response.json().catch(() => ({}));
if (!response.ok) {
const error = new Error(payload.error || payload.details || 'Failed to import the published page.');
error.status = response.status;
error.errorCode = payload.error_code || null;
throw error;
}
await saveVersion(
payload.html || '',
'load_from_pages',
'',
0,
currentProjectId,
importUrl || buildServiceUrl(PAGES_ORIGIN, `page/${pageHash}`),
null,
payload.clean_metadata || null
);
refreshIframeWithHTMLContent(payload.html || '');
switchTab('Update');
}
async function saveVersion(htmlContent, lastAction = "None", lastPrompt = "", referenceVersion = 0, referenceProject = "", referencePageID = "", htmlDiff = null, cleanMetadata = null) {
// Capture the current time as an ISO string.
const versionTime = new Date().toISOString();
// Generate diff if not provided.
if (htmlDiff == null) {
try {
if (
currentProjectId &&
currentVersion &&
referenceVersion &&
projectVersionHistory[currentProjectId][referenceVersion] &&
projectVersionHistory[currentProjectId][referenceVersion].html
) {
htmlDiff = generateUnifiedDiffGitStyle(
projectVersionHistory[currentProjectId][referenceVersion].html,
htmlContent
);
}
} catch(e) {
console.error('Error generating unified diff, but continuing:', e);
}
}
// Strip any non-HTML content.
htmlContent = stripNonHTMLContent(htmlContent);
// **Extract the page title quickly using a regex.**
const titleMatch = htmlContent.match(/(.*?)<\/title>/i);
const pageTitle = titleMatch ? titleMatch[1].trim() : "";
// Compute the non-time hash for the htmlContent.
const htmlHash = await computeNonTimeHash(htmlContent);
const fullHtmlHash = await computeFullSha256Hex(htmlContent);
// Instead of waiting for getVersionDescription, use a placeholder.
let versionSummary = "Pending summary";
// Initialize a project if none exists.
if (!currentProjectId) {
currentProjectId = generateUniqueId();
projectVersionHistory[currentProjectId] = {};
// Update the URL with the project ID without reloading.
const url = new URL(window.location.href);
url.searchParams.set('projectId', currentProjectId);
window.history.pushState(null, '', url.toString());
}
// Determine the new version number.
const versionKeys = getNumericVersionKeys(currentProjectId);
const maxVersion = versionKeys.length ? Math.max(...versionKeys) : 0;
const nextVersionNumber = maxVersion + 1;
const resolvedReferenceVersion = Number.isFinite(normalizeVersionNumber(referenceVersion))
? normalizeVersionNumber(referenceVersion)
: referenceVersion;
const referenceVersionObj = getVersionState(currentProjectId, resolvedReferenceVersion);
const derivedCleanMetadata = deriveVersionCleanState(lastAction, referenceVersionObj);
const providedCleanMetadata = cleanMetadata && typeof cleanMetadata === 'object'
? cleanMetadata
: null;
let effectiveCleanMetadata = null;
if (isVerifiedCleanMetadataForHtml(providedCleanMetadata, fullHtmlHash)) {
effectiveCleanMetadata = providedCleanMetadata;
} else if (providedCleanMetadata && getHtmlCleanState(providedCleanMetadata) === HTML_CLEAN_STATE_VERIFIED) {
console.warn('[PII HTML save] Ignoring mismatched verified clean metadata; re-verifying candidate HTML before save.');
}
if (!effectiveCleanMetadata && getHtmlCleanState(derivedCleanMetadata) === HTML_CLEAN_STATE_VERIFIED) {
effectiveCleanMetadata = {
...derivedCleanMetadata,
html_sha256: fullHtmlHash,
html_clean_verified_at: versionTime,
html_clean_block_reason: null,
html_attestation: null
};
}
if (!effectiveCleanMetadata) {
effectiveCleanMetadata = await ensureHtmlCleanBeforeVersionSave(
htmlContent,
lastAction,
resolvedReferenceVersion,
providedCleanMetadata
);
}
const resolvedCleanMetadata = normalizeResolvedCleanMetadata(
effectiveCleanMetadata,
fullHtmlHash,
versionTime,
derivedCleanMetadata.html_clean_basis || 'manual_change'
);
// Build additional metadata and now include the page title.
const additionalMetadata = {
// Save the agent name (which can include version info).
agentName: document.getElementById('agentSelect') ? document.getElementById('agentSelect').value : 'default',
// Save the client request unique key if it exists (set it wherever you generate it).
clientRequestUniqueKey: window.lastClientRequestUniqueKey || null,
// Use sessionUser._userId if available; otherwise, compute a simple hash of the username.
userId: (window.sessionUser && window.sessionUser._userId) ? window.sessionUser._userId : "unknown",
htmlHash: htmlHash,
page_title: pageTitle,
...resolvedCleanMetadata
};
lastClientRequestUniqueKey = null; //clearing it so we don't repeat it unless sent again
updateCurrentVersion(nextVersionNumber);
// Save the version along with all data.
projectVersionHistory[currentProjectId][currentVersion] = {
html: htmlContent,
lastAction: lastAction,
lastPrompt: lastPrompt,
referenceVersion: referenceVersion,
referenceProject: referenceProject,
referencePageID: referencePageID,
diff: htmlDiff,
timestamp: versionTime,
errors: [],
versionSummary: versionSummary,
...additionalMetadata
};
// Update the version display in the UI.
updateVersionDisplay();
// Asynchronously update the version summary and save last version.
getVersionDescription(lastAction, lastPrompt, htmlDiff, htmlContent, pageTitle)
.then((finalSummary) => {
projectVersionHistory[currentProjectId][currentVersion].versionSummary = finalSummary;
updateVersionDisplay();
saveLastVersion(currentVersion, htmlContent);
window_tab_saveState().catch(err => {
console.error('Background window_tab_saveState failed:', err);
});
})
.catch((err) => {
console.error("Failed to get version summary from Edge AI:", err);
updateVersionDisplay();
saveLastVersion(currentVersion, htmlContent);
window_tab_saveState().catch(err => {
console.error('Background window_tab_saveState failed:', err);
});
});
}
document.getElementById('prevVersionBtn').addEventListener('click', () => {
if (currentVersion > 1) {
updateCurrentVersion(currentVersion - 1);
refreshIframeWithHTMLContent(projectVersionHistory[currentProjectId][currentVersion]["html"]);
updateVersionDisplay();
}
});
document.getElementById('refreshIframeBtn').addEventListener('click', () => {
const responseIframe = document.getElementById('responseIframe');
// Cross-origin safe: rely on src attribute rather than contentWindow.location.href
const srcAttr = (responseIframe && (responseIframe.getAttribute('src') || '')).trim();
if (!srcAttr || srcAttr === 'about:blank') {
// Load HTML content if source is blank/about:blank
refreshIframeWithHTMLContent(projectVersionHistory[currentProjectId][currentVersion]["html"]);
} else {
// Otherwise, reload the iframe preserving current URL
rebootIframe();
}
});
document.getElementById('copyCodeBtn').addEventListener('click', function() {
const htmlContent = projectVersionHistory[currentProjectId][currentVersion]["html"];
if (htmlContent) {
navigator.clipboard.writeText(htmlContent).then(function() {
showToast('info', 'HTML code copied to clipboard!');
}, function(err) {
showToast('error', 'Failed to copy HTML code: ' + err);
});
} else {
showToast('error', 'No HTML content found to copy.');
}
});
document.getElementById('nextVersionBtn').addEventListener('click', () => {
if (currentProjectId in projectVersionHistory && currentVersion < Object.keys(projectVersionHistory[currentProjectId]).length) {
updateCurrentVersion(currentVersion + 1);
refreshIframeWithHTMLContent(projectVersionHistory[currentProjectId][currentVersion]["html"]);
updateVersionDisplay();
}
});
// New helper function to build error-fix context from version history
function getErrorFixContext() {
// Ensure there is a valid project and at least one previous version.
if (!currentProjectId || currentVersion <= 1) {
return "";
}
const prevVersionNumber = currentVersion - 1;
const prevVersionObj = projectVersionHistory[currentProjectId][prevVersionNumber];
if (!prevVersionObj) return "";
// Only add context if the previous update wasn’t a new creation ("send"),
// has a lastPrompt, and did not record any errors.
if (prevVersionObj.lastAction === "send" || !prevVersionObj.lastPrompt) {
return "";
}
if (prevVersionObj.errors && prevVersionObj.errors.length > 0) {
return "";
}
// Build a context string. You can customize this message as needed.
let context = `In the previous version (Version ${prevVersionNumber}), the update was performed with the prompt: "${prevVersionObj.lastPrompt}".`;
if (prevVersionObj.diff) {
context += ` The changes diff was: "${prevVersionObj.diff}".`;
}
context += " No errors were encountered in that previous version. It is possible this new error was just introduced in this change, or that the error just wasn't reproduced in testing the previous version.";
return context;
}
// Updated error-fixing function with custom-error support
async function applyActionFromError(errorMessage, swipeAction) {
const button = document.getElementById('fixErrorSwipeButton');
updateButtonProgress(button, 'loading');
// Get extra context from version history
const extraContext = getErrorFixContext();
let prompt = `Please fix the error '${errorMessage}'.`;
if (extraContext) {
prompt += "\n\n" + extraContext;
}
let fast_fix = false;
switch (swipeAction) {
case 'default-fix': {
const tokens = calculateTokenApproximation(getCurrentVersionCode()).approxTokens;
if (tokens > 2000) fast_fix = true;
break;
}
case 'fast-fix-error': {
fast_fix = true;
break;
}
case 'hard-error': {
if (typeof getErrorContexts === 'function') {
const enrichedError = await getErrorContexts(errorMessage, "pretty_message", 2000, 300);
prompt = `Please fix the error '${enrichedError}'.`;
}
const tokensHard = calculateTokenApproximation(getCurrentVersionCode()).approxTokens;
if (tokensHard > 2000) fast_fix = true;
break;
}
case 'custom-error': {
// Ask the user for additional context about the error.
const { value: additionalInfo, isConfirmed } = await Swal.fire({
title: 'Additional Error Context',
text: 'Can you provide extra details about this error? For example, does it occur under certain conditions or appear random?',
input: 'textarea',
inputPlaceholder: 'Enter any extra details here (optional)...',
showCancelButton: true,
confirmButtonText: 'Submit',
cancelButtonText: 'Cancel'
});
if (!isConfirmed) {
updateButtonProgress(button, 'normal');
return;
}
// Optionally enrich the error message if available.
if (typeof getErrorContexts === 'function') {
const enrichedError = await getErrorContexts(errorMessage, "pretty_message", 2000, 300);
prompt = `Please fix the error '${enrichedError}'.`;
} else {
prompt = `Please fix the error '${errorMessage}'.`;
}
// Append the user-provided details in quotes.
if (additionalInfo && additionalInfo.trim() !== "") {
prompt += "\n\nUser provided context: \"" + additionalInfo.trim() + "\"";
}
const tokensCustom = calculateTokenApproximation(getCurrentVersionCode()).approxTokens;
if (tokensCustom > 2000) fast_fix = true;
break;
}
case 'investigate-error': {
// In future we will allow the AI to ask questions about how the error occurs to help clarify.
alert("Investigate error functionality is not implemented yet.");
updateButtonProgress(button, 'normal');
return;
}
default: {
const tokensDefault = calculateTokenApproximation(getCurrentVersionCode()).approxTokens;
if (tokensDefault > 2000) fast_fix = true;
break;
}
}
gloalLastMakeChangePrompt = prompt;
await applyAction(prompt, "fixBugFromException", fast_fix);
updateButtonProgress(button, 'normal');
}
var gloalLastMakeChangePrompt = "";
// State variables to track fullscreen status
let isIframeFullscreen = false;
let isMobileIframeFullscreen = false; // New state for mobile iframe fullscreen
function getPreviewFullscreenElement() {
return document.fullscreenElement ||
document.webkitFullscreenElement ||
document.mozFullScreenElement ||
document.msFullscreenElement ||
null;
}
function isNativeIframeFullscreen(elem) {
const active = getPreviewFullscreenElement();
return !!active && active === elem;
}
function isNativeSiteFullscreen() {
return getPreviewFullscreenElement() === document.documentElement;
}
function isMobileSiteFullscreenFallbackActive() {
return document.documentElement.classList.contains('fc-mobile-site-fullscreen-active') ||
document.body.classList.contains('fc-mobile-site-fullscreen-active');
}
function getFullscreenButtonParts() {
const fullscreenBtn = document.getElementById('fullscreenBtn');
if (!fullscreenBtn) return { fullscreenBtn: null, fullscreenBtnText: null, fullscreenIcon: null };
return {
fullscreenBtn,
fullscreenBtnText: fullscreenBtn.querySelector('span'),
fullscreenIcon: fullscreenBtn.querySelector('i')
};
}
function updateFullscreenButtonUI(forceActive) {
const { fullscreenBtnText, fullscreenIcon } = getFullscreenButtonParts();
if (!fullscreenBtnText || !fullscreenIcon) return;
if (forceActive) {
fullscreenIcon.className = 'fa fa-compress-arrows-alt';
fullscreenBtnText.textContent = ' Exit Fullscreen';
} else {
fullscreenIcon.className = 'fa fa-expand-arrows-alt';
fullscreenBtnText.textContent = ' Fullscreen';
}
}
function setForcedIframeFullscreen(enabled) {
const elem = document.querySelector(".iframe-container");
if (!elem) return;
elem.classList.toggle('iframe-container-forced-fullscreen', enabled);
document.documentElement.classList.toggle('fc-iframe-forced-fullscreen-active', enabled);
document.body.classList.toggle('fc-iframe-forced-fullscreen-active', enabled);
isMobileIframeFullscreen = enabled;
}
function setMobileSiteFullscreenFallback(enabled) {
document.documentElement.classList.toggle('fc-mobile-site-fullscreen-active', enabled);
document.body.classList.toggle('fc-mobile-site-fullscreen-active', enabled);
}
function syncFullscreenState() {
const elem = document.querySelector(".iframe-container");
const nativeIframeActive = !!(elem && isNativeIframeFullscreen(elem));
const nativeSiteActive = isNativeSiteFullscreen();
const forcedActive = !!(elem && elem.classList.contains('iframe-container-forced-fullscreen'));
isIframeFullscreen = nativeIframeActive;
isMobileIframeFullscreen = forcedActive;
const shouldShowActive = isMobile ? forcedActive : (nativeIframeActive || nativeSiteActive || forcedActive);
updateFullscreenButtonUI(shouldShowActive);
}
function requestFullscreenForTarget(target) {
if (!target) return Promise.resolve(false);
try {
if (target.requestFullscreen) {
try {
const maybePromise = target.requestFullscreen({ navigationUI: 'hide' });
if (maybePromise && typeof maybePromise.then === 'function') {
return maybePromise.then(() => true).catch(() => false);
}
return Promise.resolve(true);
} catch (_) {
const fallbackPromise = target.requestFullscreen();
if (fallbackPromise && typeof fallbackPromise.then === 'function') {
return fallbackPromise.then(() => true).catch(() => false);
}
return Promise.resolve(true);
}
}
const legacyRequest = target.mozRequestFullScreen ||
target.webkitRequestFullscreen ||
target.msRequestFullscreen;
if (!legacyRequest) return Promise.resolve(false);
legacyRequest.call(target);
return Promise.resolve(true);
} catch (error) {
return Promise.resolve(false);
}
}
async function requestNativePreviewFullscreen(elem) {
const targets = [elem, document.documentElement];
for (const target of targets) {
if (!target) continue;
const success = await requestFullscreenForTarget(target);
if (success) return true;
}
return false;
}
function exitNativeFullscreen() {
try {
if (document.exitFullscreen) {
const maybePromise = document.exitFullscreen();
if (maybePromise && typeof maybePromise.then === 'function') {
return maybePromise.then(() => true).catch(() => false);
}
return Promise.resolve(true);
}
const legacyExit = document.mozCancelFullScreen ||
document.webkitExitFullscreen ||
document.msExitFullscreen;
if (!legacyExit) return Promise.resolve(false);
legacyExit.call(document);
return Promise.resolve(true);
} catch (error) {
return Promise.resolve(false);
}
}
function handleNativeFullscreenChange() {
const elem = document.querySelector(".iframe-container");
const nativeIframeActive = !!(elem && isNativeIframeFullscreen(elem));
if (nativeIframeActive) {
setForcedIframeFullscreen(false);
}
if (isNativeSiteFullscreen()) {
setMobileSiteFullscreenFallback(false);
}
syncFullscreenState();
}
document.addEventListener('fullscreenchange', handleNativeFullscreenChange);
document.addEventListener('webkitfullscreenchange', handleNativeFullscreenChange);
document.addEventListener('mozfullscreenchange', handleNativeFullscreenChange);
document.addEventListener('MSFullscreenChange', handleNativeFullscreenChange);
async function ensureMobileSiteFullscreen() {
if (!isMobile) return false;
if (isNativeSiteFullscreen() || isMobileSiteFullscreenFallbackActive()) return true;
const nativeWorked = await requestFullscreenForTarget(document.documentElement);
if (nativeWorked) {
setMobileSiteFullscreenFallback(false);
return true;
}
setMobileSiteFullscreenFallback(true);
return true;
}
async function toggleFullscreen(srcButton) {
const elem = document.querySelector(".iframe-container");
if (!elem) return;
const nativeIframeActive = isNativeIframeFullscreen(elem);
const nativeSiteActive = isNativeSiteFullscreen();
const forcedActive = elem.classList.contains('iframe-container-forced-fullscreen');
if (srcButton === 'click') {
if (isMobile && !nativeSiteActive && !isMobileSiteFullscreenFallbackActive()) {
await ensureMobileSiteFullscreen();
syncFullscreenState();
}
return;
}
if (isMobile) {
if (forcedActive) {
setForcedIframeFullscreen(false);
} else {
await ensureMobileSiteFullscreen();
setForcedIframeFullscreen(true);
}
syncFullscreenState();
return;
}
if (nativeIframeActive || nativeSiteActive) {
await exitNativeFullscreen();
syncFullscreenState();
return;
}
if (forcedActive) {
setForcedIframeFullscreen(false);
syncFullscreenState();
return;
}
const nativeWorked = await requestNativePreviewFullscreen(elem);
if (!nativeWorked) {
setForcedIframeFullscreen(true);
}
syncFullscreenState();
}
document.getElementById('fullscreenBtn').addEventListener('click', () => toggleFullscreen('fullscreenBtn'));
var searchParams = new URLSearchParams(window.location.search);
async function loadProject(projectName) {
try {
const storedProject = await loadFromIndexedDB(projectName);
const loadedProject = await normalizeLoadedLocalProjectRecord(storedProject);
if (loadedProject && loadedProject.html) {
await saveVersion(
loadedProject.html,
'load_from_load',
'',
0,
currentProjectId,
"",
null,
loadedProject.cleanMetadata || null
);
refreshIframeWithHTMLContent(loadedProject.html);
const requestId = window.enhancedToastSystem ? window.enhancedToastSystem.getClientRequestId() : null;
const detailedMessage = requestId ? `
Load Details
Project Name: ${projectName}
Source: Local IndexedDB
Request ID:${requestId.split('-')[0]}
Project loaded successfully from local storage.
` : null;
if (window.enhancedToastSystem) {
window.enhancedToastSystem.success('Project Loaded', `Project '${projectName}' loaded!`, {
detailedMessage,
duration: 3000
});
} else {
showToast('info', `Project '${projectName}' loaded!`);
}
} else {
if (window.enhancedToastSystem) {
window.enhancedToastSystem.error('Project Not Found', 'The requested project could not be found in local storage.', {
detailedMessage: `
Load Error Details
Project Name: ${projectName}
Storage: Local IndexedDB
The project may have been deleted or never saved.
`
});
} else {
showToast('error', 'Project not found.');
}
}
} catch (err) {
if (window.enhancedToastSystem) {
window.enhancedToastSystem.error('Load Failed', `Failed to load project: ${err.message}`, {
detailedMessage: `
Load Error Details
Project Name: ${projectName}
Error Type: ${err.name}
Error Message: ${err.message}
There was an issue accessing local storage.
`
});
} else {
showToast('error', `Failed to load project: ${err.message}`);
}
}
}
async function updateProjectsDropdown() {
try {
const projectNames = await listProjectsFromIndexedDB();
const dropdown = document.getElementById('projectsDropdown');
if (!dropdown) {
return;
}
dropdown.innerHTML = ''; // Clear current options
projectNames.forEach(projectName => {
const option = new Option(projectName, projectName);
dropdown.add(option);
});
} catch (err) {
console.error('Failed to update projects dropdown:', err);
showToast('error', 'Failed to load project list');
}
}
var cropper = null;
function getPublishScreenshotDataUri(options = {}) {
const logWarnings = !!options.logWarnings;
if (cropper && typeof cropper.getCroppedCanvas === 'function') {
try {
const croppedCanvas = cropper.getCroppedCanvas();
if (croppedCanvas) {
return croppedCanvas.toDataURL('image/png');
}
} catch (error) {
if (logWarnings) {
console.warn('Unable to use cropped screenshot.', error);
}
}
}
const screenshotImage = document.getElementById('screenshotImage');
if (screenshotImage) {
const screenshotDataUri = screenshotImage.src;
if (screenshotDataUri && screenshotDataUri.startsWith('data:image')) {
return screenshotDataUri;
}
if (logWarnings) {
console.warn('Screenshot image does not have a valid data URI.');
}
} else if (logWarnings) {
console.warn('Screenshot image element not found.');
}
return '';
}
async function publishPageToFuzzycodePages(duplication_strategy, previous_page_key = null, screenshotDataUri = null) {
const publishButton = document.getElementById('publishToPagesButton');
updateButtonProgress(publishButton, 'loading');
try {
const htmlContent = projectVersionHistory[currentProjectId][currentVersion]["html"];
const metadata = projectVersionHistory[currentProjectId][currentVersion];
const projectName = document.getElementById('projectName').value;
const attestationResponse = await fetch('/api/pages/attest-publish', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
html_content: htmlContent,
title: projectName || '',
project_id: currentProjectId,
version_number: currentVersion,
client_request_unique_key: window.lastClientRequestUniqueKey || null,
})
});
const attestationPayload = await attestationResponse.json().catch(() => ({}));
if (!attestationResponse.ok) {
throw new Error(attestationPayload.error || 'Failed to verify page content before publish.');
}
applyHtmlCleanMetadata(metadata, {
html_clean_state: attestationPayload.clean_state || HTML_CLEAN_STATE_VERIFIED,
html_clean_basis: attestationPayload.clean_basis || 'pages_publish_attestation',
html_base_clean_hash: attestationPayload.claims?.base_clean_hash || metadata?.html_base_clean_hash || null,
html_sha256: attestationPayload.html_hash || metadata?.html_sha256 || null,
html_clean_verified_at: attestationPayload.claims?.attested_at || new Date().toISOString(),
html_clean_block_reason: null,
html_attestation: {
claims: attestationPayload.claims,
signature: attestationPayload.signature,
},
});
const modifiedMetadata = { ...metadata };
modifiedMetadata.html_content = modifiedMetadata.html;
delete modifiedMetadata.html; // Remove the original html key
if (projectName) {
modifiedMetadata.title = projectName;
}
modifiedMetadata.pii_clean_attestation = {
claims: attestationPayload.claims,
signature: attestationPayload.signature,
};
const resolvedScreenshotDataUri = typeof screenshotDataUri === 'string'
? screenshotDataUri
: getPublishScreenshotDataUri({ logWarnings: true });
if (resolvedScreenshotDataUri) {
modifiedMetadata.screenshot_data_uri = resolvedScreenshotDataUri;
}
modifiedMetadata.access_level = getRequestedPublishAccessLevel();
modifiedMetadata.duplication_strategy = duplication_strategy;
if (previous_page_key) {
modifiedMetadata.previous_page_key = previous_page_key;
}
const data = JSON.stringify(modifiedMetadata);
const response = await fetch(buildServiceUrl(FUZZYCODE_ORIGIN, '@pages/submit'), {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
body: data
});
if (response.status === 401) {
showToast('error', 'Missing User Token. Please log in to use this feature.');
throw new Error('HTTP 401 Unauthorized');
}
const responseData = await response.json();
if (responseData.warning) {
handleDuplicationWarning(responseData.matching_pages);
} else if (responseData.url) {
projectVersionHistory[currentProjectId][currentVersion].referencePageID = responseData.url;
projectVersionHistory[currentProjectId][currentVersion].published_timestamp = new Date().toISOString();
updateVersionDisplay();
window_tab_saveState().catch(err => {
console.error('Background window_tab_saveState failed:', err);
});
showPublishOutcomeModal(responseData, modifiedMetadata.screenshot_data_uri, modifiedMetadata.title, responseData.hash_key);
} else {
console.error('No URL returned from the server.');
}
updateButtonProgress(publishButton, 'normal');
} catch (error) {
console.error('Error accessing iframe content:', error);
updateButtonProgress(publishButton, 'normal');
}
}
const PUBLISH_VISIBILITY_CONFIG = {
private: {
shortLabel: 'Private',
buttonHtml: 'Save Private',
help: 'Private pages stay visible only in your own account. Use Share later if you want to request an unlisted link.',
},
public: {
shortLabel: 'Public',
buttonHtml: 'Request Public Approval',
help: 'Public asks a parent to approve public listing and discovery. The page stays private until that approval happens.',
},
};
function normalizePublishAccessLevel(value, fallback = 'private') {
const normalized = String(value || '').trim().toLowerCase();
if (normalized === 'private' || normalized === 'public' || normalized === 'unlisted') {
return normalized;
}
return fallback;
}
function getRequestedPublishAccessLevel() {
const visibilitySelect = document.getElementById('publishVisibilitySelect');
if (visibilitySelect) {
return normalizePublishAccessLevel(visibilitySelect.value);
}
const visibilityToggle = document.getElementById('visibilityToggle');
if (visibilityToggle) {
return visibilityToggle.checked ? 'public' : 'private';
}
return 'private';
}
function syncPublishVisibilityUi() {
const accessLevel = getRequestedPublishAccessLevel();
const config = PUBLISH_VISIBILITY_CONFIG[accessLevel] || PUBLISH_VISIBILITY_CONFIG.private;
const visibilityHelp = document.getElementById('publishVisibilityHelp');
const publishButton = document.getElementById('publishToPagesButton');
const legacyVisibilityLabel = document.querySelector('.visibility-label');
const visibilitySelect = document.getElementById('publishVisibilitySelect');
const visibilityToggle = document.getElementById('visibilityToggle');
if (visibilityHelp) {
visibilityHelp.textContent = config.help;
}
if (publishButton && !publishButton.classList.contains('button-loading')) {
publishButton.innerHTML = config.buttonHtml;
}
if (legacyVisibilityLabel) {
legacyVisibilityLabel.textContent = config.shortLabel;
}
if (visibilitySelect) {
visibilitySelect.value = accessLevel;
}
if (visibilityToggle) {
visibilityToggle.checked = accessLevel === 'public';
}
}
function getPublishOutcomeContent(responseData) {
const requestedAccessLevel = normalizePublishAccessLevel(
responseData?.requested_access_level,
getRequestedPublishAccessLevel(),
);
const storedAccessLevel = normalizePublishAccessLevel(
responseData?.stored_access_level,
requestedAccessLevel,
);
const pendingParentApproval = Boolean(responseData?.pending_parent_approval);
const pageUrl = typeof responseData?.url === 'string' ? responseData.url.trim() : '';
const shareUrl = typeof responseData?.share_url === 'string' ? responseData.share_url.trim() : '';
if (pendingParentApproval && requestedAccessLevel === 'public') {
return {
modalTitle: 'Public Publish Requested',
headline: 'Public Publish Needs Parent Approval',
description: 'This page is still private right now.',
supporting: 'A verified parent can approve public listing later in Manage Children > Visibility approvals. You can still use Share if you want an unlisted link request instead.',
openUrl: pageUrl,
openLabel: 'Open Private Draft',
allowShare: true,
};
}
if (storedAccessLevel === 'unlisted' && shareUrl) {
return {
modalTitle: 'Unlisted Page Ready',
headline: 'Unlisted Page Ready',
description: 'This page now has an unlisted share link.',
supporting: 'It stays out of public listings, but anyone with the link can open it.',
openUrl: shareUrl,
openLabel: 'Open Unlisted Page',
allowShare: false,
};
}
if (storedAccessLevel === 'public') {
return {
modalTitle: 'Page Published',
headline: 'Page Published Successfully!',
description: 'This page is live on the public pages listing.',
supporting: 'Anyone can open it from the public pages surface.',
openUrl: pageUrl,
openLabel: 'Open Published Page',
allowShare: true,
};
}
return {
modalTitle: 'Private Page Saved',
headline: 'Private Page Saved',
description: 'This page is private and is not publicly listed or shareable.',
supporting: 'Use Share whenever you want to send it to approved friends or request a parent-reviewed unlisted link later.',
openUrl: pageUrl,
openLabel: 'Open Private Page',
allowShare: true,
};
}
var sharePagePopup = null;
function openSharePagePopup(hashKey, pageTitle) {
const safeTitle = typeof pageTitle === 'string' && pageTitle.trim()
? pageTitle.trim()
: 'Untitled Page';
const postData = {
category: "social",
notification_type: "share_page",
type_id: 1,
page_id: hashKey,
page_title: safeTitle,
action: "share_page",
};
if (sharePagePopup === null) sharePagePopup = new PopupWindow();
sharePagePopup.show({
title: 'Share Page',
content: buildServiceUrl(FUZZYCODE_ORIGIN, 'send_notification_page'),
isIframe: true,
postData: postData,
onClose: () => console.log('Share popup closed'),
onMaximize: () => console.log('Share popup maximized'),
onRestore: () => console.log('Share popup restored'),
customIcons: [
{
class: 'fas fa-info-circle',
action: 'info',
onClick: () => console.log('Info icon clicked'),
},
],
});
}
function showPublishOutcomeModal(responseData, screenshot, pageTitle, hashKey) {
const outcome = getPublishOutcomeContent(responseData);
const screenshotMarkup = screenshot
? ``
: `
${matchingPagesHtml}
`,
icon: 'question',
showCancelButton: true,
confirmButtonText: 'Publish Duplicate',
cancelButtonText: 'Cancel'
}).then((result) => {
if (result.isConfirmed) {
publishPageToFuzzycodePages('duplicate');
}
});
}
function handleUpdatePage(hash_key) {
Swal.close(); // Close the Swal popup
publishPageToFuzzycodePages('update', hash_key);
}
function handleSharePage(hash_key, pageTitle) {
Swal.close(); // Close the Swal popup
openSharePagePopup(hash_key, pageTitle);
}
function getIframeTitleFallback() {
try {
const iframe = document.getElementById('responseIframe');
if (!iframe) {
return '';
}
const doc = iframe.contentDocument || iframe.contentWindow?.document;
return doc?.title ? doc.title.trim() : '';
} catch (error) {
console.warn('Unable to access iframe title:', error);
return '';
}
}
function deriveProjectTitleFromCurrentState() {
if (
currentProjectId &&
projectVersionHistory[currentProjectId] &&
projectVersionHistory[currentProjectId][currentVersion] &&
typeof projectVersionHistory[currentProjectId][currentVersion].html === 'string'
) {
const htmlContent = projectVersionHistory[currentProjectId][currentVersion].html;
const titleMatch = htmlContent.match(/(.*?)<\/title>/i);
if (titleMatch && titleMatch[1].trim()) {
return titleMatch[1].trim();
}
}
return getIframeTitleFallback();
}
function refreshProjectNameFromIframeTitle() {
const projectNameInput = document.getElementById('projectName');
if (!projectNameInput) {
return;
}
const derivedTitle = deriveProjectTitleFromCurrentState();
projectNameInput.value = derivedTitle;
projectNameInput.dispatchEvent(new Event('input', { bubbles: true }));
projectNameInput.dispatchEvent(new Event('change', { bubbles: true }));
}
const refreshProjectNameBtn = document.getElementById('refreshProjectNameBtn');
if (refreshProjectNameBtn) {
refreshProjectNameBtn.addEventListener('click', refreshProjectNameFromIframeTitle);
}
document.getElementById('publishToPagesButton').addEventListener('click', publishPage);
async function publishPage() {
var duplication_strategy = "prevent";
const screenshotDataUri = getPublishScreenshotDataUri();
if (!screenshotDataUri) {
const result = await Swal.fire({
title: 'No Screenshot Found',
text: 'This publish does not include a screenshot yet. Publish without one or take a screenshot first?',
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'Publish without screenshot',
cancelButtonText: 'Take screenshot'
});
if (result.isConfirmed) {
publishPageToFuzzycodePages(duplication_strategy, null, screenshotDataUri);
} else if (result.dismiss === Swal.DismissReason.cancel) {
const takeScreenshotButton = document.getElementById('takeScreenshotBtn');
if (takeScreenshotButton) {
takeScreenshotButton.scrollIntoView({ behavior: 'smooth', block: 'center' });
takeScreenshotButton.focus();
}
}
return;
}
publishPageToFuzzycodePages(duplication_strategy, null, screenshotDataUri);
}
document.getElementById('clearCreateTextArea').addEventListener('click', function() {
document.getElementById('userMessage').value = ''; // Clears the textarea in the Create tab
});
document.getElementById('clearUpdateTextArea').addEventListener('click', function() {
document.getElementById('customChanges').value = ''; // Clears the textarea in the Update tab
});
var isMobile = false;
function mobileDetected() {
isMobile=true;
document.getElementById('touch-pad-responsive').checked = true;
updatePromptModifiers();
}
document.addEventListener('DOMContentLoaded', function() {
// Function to detect mobile devices
// Attach event listeners for the first touch/click to trigger fullscreen
document.addEventListener('touchstart', mobileDetected, {once: true});
document.addEventListener('click', () => toggleFullscreen('click'), {once: true});
syncFullscreenState();
});
function updateWindowTitle() {
// Get the iframe element by its ID
var iframe = document.getElementById('responseIframe');
// Ensure the iframe is present
if (!iframe) {
console.error('The iframe with id "responseIframe" does not exist.');
return;
}
// Attempt to access the document inside the iframe (only if same-origin)
try {
var sameOrigin = new URL(iframe.src, window.location.href).origin === window.location.origin;
if (!sameOrigin) return; // child will postMessage title
} catch (_) { return; }
var iframeDocument = iframe.contentDocument || iframe.contentWindow.document;
// Check if the document is loaded and accessible
if (!iframeDocument) {
console.error('Failed to access the document inside the iframe.');
return;
}
// Get the title from the iframe's document
var iframeTitle = iframeDocument.title;
// Update the window's title using the format "({iframe_title}) - FuzzyCode"
document.title = `${iframeTitle} - FuzzyCode - Learn in Fun`;
}
function updatePromptModifiers()
{
persistPromptModifiersState(collectPromptModifiersFromDom());
}
document.addEventListener('DOMContentLoaded', function () {
const storedPromptModifiers =
readStoredPromptModifiers() || migrateLegacyPromptModifiersCookie();
applyPromptModifiersToDom(storedPromptModifiers);
// Update the cookie when any 'prompt_modifier' changes
document.querySelectorAll('.prompt_modifier').forEach(function(input) {
input.addEventListener('change', function() {
updatePromptModifiers();
});
});
// Optionally, initialize cookie with current states on page load
let initialState = {};
document.querySelectorAll('.prompt_modifier').forEach(function(el) {
initialState[el.id] = el.checked;
});
persistPromptModifiersState(initialState);
});
var anon_user_name = "anonymous_user"
var fuzzycode_username = anon_user_name; // Default value
async function resolveCurrentActorDisplayName() {
try {
if (window.sessionUser && typeof window.sessionUser.get_user_name === 'function') {
const canonicalName = await window.sessionUser.get_user_name();
if (canonicalName) {
return canonicalName;
}
}
} catch (error) {
console.warn('Failed to resolve canonical current-actor name from session runtime:', error);
}
if (window.sessionUser && window.sessionUser._userName) {
return window.sessionUser._userName;
}
if (window.sessionUser && window.sessionUser._userId) {
return 'account';
}
return anon_user_name;
}
async function updateUsername()
{
fuzzycode_username = await resolveCurrentActorDisplayName();
var userProfileIcon = document.querySelector('.user-profile-icon');
if (!userProfileIcon) {
return;
}
var userIcon = userProfileIcon.querySelector('i');
if (!userIcon) {
return;
}
if (fuzzycode_username === anon_user_name) {
userIcon.className = 'fa-solid fa-user-slash';
userProfileIcon.className = 'user-profile-icon';
} else {
userIcon.className = 'fa-solid fa-user';
userProfileIcon.className = 'user-profile-icon';
}
console.log('Resolved current actor label:', fuzzycode_username);
userProfileIcon.title = fuzzycode_username === anon_user_name ? 'Not Logged In' : fuzzycode_username;
// Function to retry calling loadProfilePicture and getNotifications
function retryLoadingProfileAndNotifications(retries) {
if (retries <= 0) {
console.error('Failed to load loadProfilePicture or getNotifications after multiple retries.');
return;
}
}
// Retry up to 10 times with 1 second interval
retryLoadingProfileAndNotifications(10);
}
document.addEventListener('DOMContentLoaded', function() {
//updateUsername();
var userProfileIcon = document.querySelector('.user-profile-icon');
userProfileIcon.addEventListener('click', openUserManagement);
});
function openFuzzyCodePageInPopup(page_id) {
if (popup === null) popup = new PopupWindow();
popup.show({
title: 'Fuzzycode Page',
content: buildServiceUrl(PAGES_ORIGIN, `page/${page_id}`),
isIframe: true,
onClose: (event) => {
console.log('Fuzzycode Page popup closed', event);
popup = null; // Clear reference when closed
}
});
}
async function openFuzzyCodePage(page_id) {
const url = buildServiceUrl(PAGES_ORIGIN, `page/${page_id}`);
importPagesHtmlByHash(page_id, url)
.catch(error => {
console.error('Error fetching content from URL:', error);
if (typeof showToast === 'function') {
const message = error?.errorCode === 'pages_import_not_attested'
? 'This published page can’t be loaded into FuzzyCode yet because it still needs verified clean provenance backfill.'
: (error?.message || 'Failed to import the published page.');
showToast('error', message);
}
});
}
function openUserManagement() {
// Check if popup exists AND if the window is actually open
if (popup && popup.isOpen()) {
popup.focus(); // Focus existing window
return;
}
// Create new popup only if needed
popup = new PopupWindow();
popup.show({
title: 'User Login',
content: '/user_login',
isIframe: true,
sandboxTokens: ['allow-popups-to-escape-sandbox'],
onClose: (event) => {
console.log('User Login popup closed', event);
popup = null; // Clear reference when closed
}
});
}
// Paste Button Event Listener
document.getElementById('pasteCodeBtn').addEventListener('click', async function() {
try {
// Attempt to read text from the clipboard
const text = await navigator.clipboard.readText();
// Trim the text to remove leading/trailing whitespace
const trimmedText = text.trim();
// Define a regex to match a single URL
const urlRegex = /^(https?:\/\/[^\s]+)$/i;
if (urlRegex.test(trimmedText)) {
// If the pasted text is a single URL, use the shared function
const fromURL = trimmedText;
loadHtmlFromUrl(fromURL, 'load_from_paste_url');
} else {
// Otherwise, perform the normal paste functionality
if (currentProjectId) {
await saveVersion(
text, // HTML content
'paste', // lastAction: descriptive of the action
'', // lastPrompt: description or prompt related to the action
currentVersion, // referenceVersion: the version from which this change is derived
currentProjectId, // referenceProject: the current project ID
"" // referencePageID: any relevant page ID, if applicable
);
}
else {
await saveVersion(
text, // HTML content
'paste_as_new_project', // lastAction: descriptive of the action
'', // lastPrompt: description or prompt related to the action
currentVersion, // referenceVersion: the version from which this change is derived
currentProjectId, // referenceProject: the current project ID
"" // referencePageID: any relevant page ID, if applicable
);
}
// Refresh the iframe to display the new HTML content
refreshIframeWithHTMLContent(text);
// Notify the user of the successful paste and version creation
showToast('info', 'HTML content pasted from clipboard as a new version!');
}
}
catch (err) {
// Handle any errors that occur during clipboard access or fetch
console.error('Failed to paste HTML code:', err);
showToast('error', 'Failed to paste HTML code: ' + err);
}
});
var popup = null;
// Add message listener for iframe communication
window.addEventListener('message', async (event) => {
// Verify per-message against live iframe window and origin to avoid race with cached vars
let iframeWin = null; let iframeOrigin = '';
try {
const ifr = document.getElementById('responseIframe');
iframeWin = ifr && ifr.contentWindow;
try { iframeOrigin = new URL(ifr && ifr.src ? ifr.src : '', window.location.href).origin; } catch(_) { iframeOrigin = ''; }
} catch(_) {}
const fromAllowedOrigin = (event.origin === window.location.origin) || (iframeOrigin && event.origin === iframeOrigin);
const fromIframeWindow = !!(iframeWin && event.source === iframeWin);
if (!fromAllowedOrigin && !fromIframeWindow) return;
const { source, action, data, title, message, type, requestId } = event.data || {};
// Respond to resource overlay HTML requests
if (type === 'fuzzyResource:getHtml') {
try {
const html = projectVersionHistory?.[currentProjectId]?.[currentVersion]?.html || null;
if (event.source && typeof event.source.postMessage === 'function') {
event.source.postMessage({
type: 'fuzzyResource:html',
requestId,
html
}, event.origin || '*');
}
} catch (err) {
console.warn('[FC parent] failed to send html to overlay', err);
}
return;
}
// Handle direct level changes from level editor (AdapterSpec pattern)
if (type === 'applyLevelChanges') {
try {
const newHtml = event.data.html;
if (!newHtml) {
console.error('[Level Editor] No HTML in applyLevelChanges message');
return;
}
const currentVersionHtml = getCurrentVersionCode();
if (newHtml === currentVersionHtml) {
showToast('info', 'No level changes detected.');
console.log('[Level Editor] No changes detected, no new version created.');
return;
}
const referenceVersion = normalizeVersionNumber(currentVersion);
await saveVersion(
newHtml,
'level_editor_apply',
'',
Number.isFinite(referenceVersion) ? referenceVersion : 0,
currentProjectId,
''
);
refreshIframeWithHTMLContent(newHtml);
showToast('success', 'Level changes applied!');
console.log('[Level Editor] New version created:', currentVersion);
} catch (err) {
console.error('[Level Editor] Failed to apply level changes:', err);
showToast('error', 'Failed to apply level changes');
}
return;
}
// Auth bridge
if (source === 'fuzzycode-auth') {
switch (action) {
case 'payment-required':
if (authCheckoutInProgress) {
if (authCheckoutWindow && !authCheckoutWindow.closed) {
authCheckoutWindow.focus();
break;
}
if (!authCheckoutWindow) {
break;
}
if (authCheckoutWindow.closed) {
authCheckoutWindow = null;
authCheckoutInProgress = false;
}
}
authCheckoutInProgress = true;
// Close the popup
if (popup) popup.close();
// Open Stripe checkout in new window (for security)
const checkoutUrl = await createCheckoutSession(data.email);
if (checkoutUrl) {
// Open in new window for better Stripe experience
authCheckoutWindow = window.open(
checkoutUrl,
'fuzzycode-payment',
'width=800,height=600'
);
// Poll for payment completion
const checkInterval = setInterval(async () => {
// Check if window exists and is closed
if (authCheckoutWindow && authCheckoutWindow.closed) {
clearInterval(checkInterval);
// Check if payment was successful
const status = await checkSubscriptionStatus({ force: true });
if (status.has_active_subscription) {
console.log('Payment successful, refreshing broker session state...');
try {
const refreshed = await window.sessionUser?.refresh_session?.();
if (!refreshed) {
console.warn('Broker session refresh did not confirm updated access state');
} else {
console.log('Broker session refreshed successfully after payment');
}
} catch (refreshError) {
console.error('Exception during broker session refresh:', refreshError);
}
// Re-open user management to continue flow
openUserManagement();
}
authCheckoutInProgress = false;
authCheckoutWindow = null;
} else if (!authCheckoutWindow) {
// Window was never opened (redirect happened)
clearInterval(checkInterval);
console.log('Payment window not available - likely redirected to Stripe');
authCheckoutInProgress = false;
}
}, 1000);
} else {
authCheckoutInProgress = false;
}
break;
case 'login-success':
// Update UI after successful login
if (window.sessionUser && data?.userId) {
window.sessionUser._userId = data.userId;
}
if (window.sessionUser && data?.userName) {
window.sessionUser._userName = data.userName;
}
updateUsername();
// Don't auto-close - let user close manually or wait for explicit close message
break;
case 'logout':
if (window.sessionUser) {
window.sessionUser._userId = null;
window.sessionUser._userName = null;
}
updateUsername();
break;
case 'signup-pending-confirmation':
if (window.sessionUser) {
window.sessionUser._userId = null;
window.sessionUser._userName = null;
}
updateUsername();
break;
case 'close-popup':
// New explicit message for when popup should close
if (popup && popup.isOpen()) {
popup.close();
}
break;
}
return;
}
// UGC bridge
if (source === 'fuzzycode-ugc') {
// Only the current preview iframe may update preview title/error UI.
// A replaced temp-page iframe can still deliver a late same-origin
// postMessage after refreshIframeWithHTMLContent() clears errors; ignore
// those stale messages so a fixed version stays visually clean.
if (!fromIframeWindow) return;
if (action === 'title' && typeof title === 'string') {
document.title = `${title} - FuzzyCode - Learn in Fun`;
} else if (action === 'error' && typeof message === 'string') {
if (previewErrorReportingSuppressed) return;
try { updateFixErrorSection(message); } catch (_) {}
}
}
});
async function createCheckoutSession(email) {
try {
// Get the default price ID (yearly plan as recommended)
const defaultPriceId = 'yearly'; // This will be replaced with actual price ID from config
const response = await fetch('/api/create-checkout-session', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
price_id: defaultPriceId,
trial_days: 14,
success_url: window.location.origin + '/payment-success?session_id={CHECKOUT_SESSION_ID}',
cancel_url: window.location.origin + '/payment-cancelled'
})
});
if (!response.ok) {
throw new Error('Failed to create checkout session');
}
const data = await response.json();
return data.url;
} catch (error) {
console.error('Error creating checkout:', error);
showToast('error', 'Error creating checkout session');
return null;
}
}
async function checkSubscriptionStatus(options = {}) {
const force = options.force === true;
try {
const url = force
? '/api/subscription/status?force=1'
: '/api/subscription/status';
const response = await fetch(url, {
credentials: 'include'
});
if (!response.ok) {
return { has_active_subscription: false };
}
const data = await response.json();
return data;
} catch (error) {
console.error('Error checking subscription status:', error);
return { has_active_subscription: false };
}
}
function openLevelEditorWindow() {
// Check if 'popup' instance already exists; if not, create a new one
if (popup === null) popup = new PopupWindow();
// Get the HTML content from getCurrentVersionCode()
var htmlContent = getCurrentVersionCode();
const currentVersionObj = getVersionState(currentProjectId, currentVersion) || {};
const cleanMetadata = getVersionCleanMetadata(currentVersionObj);
const referencePageUrl = typeof currentVersionObj.referencePageID === 'string'
? currentVersionObj.referencePageID
: '';
const trustedPageSourceUrl = (
getHtmlCleanState(cleanMetadata) === HTML_CLEAN_STATE_VERIFIED &&
cleanMetadata?.html_attestation &&
extractPagesHashFromUrl(referencePageUrl)
) ? referencePageUrl : '';
// Show the popup with the Infinite Level Editor
popup.show({
title: 'Level Editor',
content: '/level_editor',
isIframe: true,
postData: {
type: 'initialize',
html: htmlContent,
source_url: trustedPageSourceUrl,
project_id: currentProjectId || '',
version_number: currentVersion,
clean_metadata: cleanMetadata
},
customIcons: [
{
class: 'fa-solid fa-floppy-disk',
action: 'save',
onClick: async () => {
try {
const iframeWindow = popup.window.querySelector('iframe').contentWindow;
if (iframeWindow && iframeWindow.levelEditorCanSave && iframeWindow.levelEditorCanSave()) {
// Toast is shown by applyLevelChanges message handler
await iframeWindow.levelEditorSaveChanges();
} else {
showToast('info', 'Level editor not initialized yet.');
}
} catch (err) {
console.error('Error saving level changes:', err);
showToast('error', 'Failed to save level changes');
}
}
}
],
onClose: async () => {
try {
const iframeWindow = popup.window.querySelector('iframe').contentWindow;
if (iframeWindow && iframeWindow.levelEditorHasChanges && iframeWindow.levelEditorHasChanges()) {
const result = await Swal.fire({
title: 'Save Changes?',
text: 'Do you want to save your level changes or discard them?',
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'Save',
cancelButtonText: 'Discard'
});
if (result.isConfirmed) {
// Toast is shown by applyLevelChanges message handler
await iframeWindow.levelEditorSaveChanges();
} else {
console.log('User chose to discard level changes.');
}
}
} catch (err) {
console.error('Error checking level editor changes:', err);
}
popup = null;
},
onMaximize: () => console.log('Level editor maximized'),
onRestore: () => console.log('Level editor restored')
});
}
async function applyMetadataChanges(event, close_or_save) {
try {
// Access the iframe's window and retrieve the new HTML content
const iframeWindow = popup.window.querySelector('iframe').contentWindow;
// Ensure the iframe and htmlContent are accessible
if (!iframeWindow || !iframeWindow.htmlContent) {
throw new Error('Unable to retrieve HTML content from metadata iframe.');
}
const newHtml = iframeWindow.htmlContent;
const currentVersionHtml = getCurrentVersionCode();
// Canonicalize the parent HTML using the iframe's toRoute (same normalization used by the child)
let parentCanonical = currentVersionHtml;
try {
if (typeof iframeWindow.toRoute === 'function') {
parentCanonical = iframeWindow.toRoute(currentVersionHtml);
}
} catch (_) {}
// Compare the current version with the new HTML content
if (newHtml === parentCanonical) {
showToast('info', 'No changes detected in metadata.');
console.log('No changes detected, no new version created.');
return;
}
// If close_or_save is "close", prompt the user with SweetAlert
if (close_or_save === "close") {
const result = await Swal.fire({
title: 'Save Changes?',
text: 'Do you want to save your changes or discard them?',
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'Save',
cancelButtonText: 'Discard'
});
if (!result.isConfirmed) {
// User chose to discard changes, so exit the function
console.log('User chose to discard changes.');
return;
}
// If result.isConfirmed is true, the function will continue to save the changes
}
// Persist canonical/origin URLs (images.fuzzycode.dev, sounds.fuzzycode.dev, etc.)
// while still allowing the metadata iframe to use route-form URLs internally.
let htmlForSave = newHtml;
try {
if (typeof iframeWindow.toOrigin === 'function') {
htmlForSave = iframeWindow.toOrigin(newHtml);
} else {
htmlForSave = newHtml
.replace(/fuzzycode\.dev\/@images/gi, 'images.fuzzycode.dev')
.replace(/fuzzycode\.dev\/@sounds/gi, 'sounds.fuzzycode.dev')
.replace(/fuzzycode\.dev\/@cdn/gi, 'cdn.fuzzycode.dev')
.replace(/fuzzycode\.dev\/@sprites/gi, 'sprites.fuzzycode.dev')
.replace(/fuzzycode\.dev\/@pages/gi, 'pages.fuzzycode.dev')
.replace(/fuzzycode\.dev\/@uploads/gi, 'uploads.fuzzycode.dev')
.replace(/fuzzycode\.dev\/@aws/gi, 'aws.fuzzycode.dev');
}
} catch (_) {}
// Create a new version with the updated HTML content
await saveVersion(
htmlForSave, // HTML content (canonical/origin URLs)
'update_metadata', // lastAction: descriptive of the action
'', // lastPrompt: description or prompt related to the action
currentVersion, // referenceVersion: the version from which this change is derived
currentProjectId, // referenceProject: the current project ID
"" // referencePageID: any relevant page ID, if applicable
);
// Refresh the iframe to display the new HTML content
refreshIframeWithHTMLContent(htmlForSave);
// Notify the user of the successful metadata update and version creation
showToast('info', 'Metadata updated and saved as a new version!');
console.log('Metadata window closed and new version created.', event);
} catch (error) {
console.error('Error updating metadata:', error);
showToast('error', 'Failed to update metadata: ' + error.message);
}
}
async function openMetaDataWindow() {
if (popup === null) popup = new PopupWindow();
popup.show({
title: 'Metadata',
content: '/metadata_page',
isIframe: true,
customIcons: [
{
class: 'fa-solid fa-floppy-disk',
action: 'save',
onClick: (event) => {applyMetadataChanges(event, "save");
var oldOnClose = popup.onClose;
popup.onClose = null;
popup.close();
popu.onClose = oldOnClose;
}}
],
onClose: (event) => {
applyMetadataChanges(event, "close");
popup = null; // Clear reference when closed
}
});
}
function openPseudocodeWindow() {
if (popup === null) popup = new PopupWindow();
popup.show({
title: 'Pseudocode',
content: '/show_pseudocode',
isIframe: true,
onClose: (event) => {
console.log('Pseudocode popup closed', event);
popup = null; // Clear reference when closed
}
});
}
document.getElementById('toggleSuggestionsBtn').addEventListener('click', function () {
const updateTab = document.getElementById('Update');
const toggleIcon = this.querySelector('i');
if (updateTab && updateTab.classList.contains('suggestion-mode')) {
// Exit suggestion mode - back to default
updateTab.classList.remove('suggestion-mode');
toggleIcon.classList.remove('fa-chevron-up');
toggleIcon.classList.add('fa-chevron-down');
// Clear active tabs
document.querySelectorAll('.suggest-tab').forEach(tab => tab.classList.remove('active'));
} else if (updateTab) {
// Enter suggestion mode (only if there's a cached category)
if (currentSuggestionCategory && suggestionsCache[currentSuggestionCategory]) {
updateTab.classList.add('suggestion-mode');
toggleIcon.classList.remove('fa-chevron-down');
toggleIcon.classList.add('fa-chevron-up');
updateSuggestionsScrollButtons();
}
}
});
// Back button to exit suggestion mode
document.getElementById('suggestBackBtn').addEventListener('click', function () {
const updateTab = document.getElementById('Update');
if (updateTab) {
updateTab.classList.remove('suggestion-mode');
// Clear active tabs
document.querySelectorAll('.suggest-tab').forEach(tab => tab.classList.remove('active'));
// Focus the textarea for immediate typing
const textarea = document.getElementById('customChanges');
if (textarea) textarea.focus();
}
});
document.getElementById('takeScreenshotBtn').addEventListener('click', function() {
captureAndProcessScreenshot({
iframeSelector: '#responseIframe',
onSuccess: function(imageDataUrl) {
var screenshotContainer = document.getElementById('screenshotContainer');
var screenshotImg = document.getElementById('screenshotImage');
screenshotImg.src = imageDataUrl;
screenshotContainer.style.display = 'block';
if (cropper) {
cropper.destroy();
}
cropper = new Cropper(screenshotImg, {
initialAspectRatio: 16 / 9,
viewMode: 1,
dragMode: 'move',
minCropBoxWidth: 150,
minCropBoxHeight: 100,
});
cropper.setDragMode("move");
},
onError: function(err) {
// Handle error (e.g. show a toast or log the error)
console.error('Error taking screenshot:', err);
// Example toast (assuming your caller has its own toast API):
showToast('error', 'Unable to take screenshot. You can take one on your own and then paste it in.');
}
});
});
var htmlDiffPopup = null;
// Function to show the HTML Diff Tool popup
function showHtmlDiffPopup() {
const diffButton = document.getElementById('diffButton');
if (currentVersion <= 0) {
return; // If the current version is not valid, do nothing
}
const previousHtml = projectVersionHistory[currentProjectId][currentVersion - 1]?.html || '';
const currentHtml = projectVersionHistory[currentProjectId][currentVersion]?.html || '';
if (htmlDiffPopup === null) {
htmlDiffPopup = new PopupWindow();
}
htmlDiffPopup.show({
title: 'HTML Diff Tool',
content: '/html_diff_tool',
isIframe: true,
postData: {
type: 'compareHtml'//,
//leftHtml: currentVersion === 1 ? currentHtml : previousHtml,
//rightHtml: currentHtml
},
onClose: () => console.log('Popup closed'),
onMaximize: () => console.log('Popup maximized'),
onRestore: () => console.log('Popup restored'),
customIcons: [
{
class: 'fas fa-info-circle',
action: 'info',
onClick: () => console.log('Info icon clicked')
}
]
});
}
var editorPopup = null;
function showEditorPopup() {
if (editorPopup === null) {
editorPopup = new PopupWindow();
}
editorPopup.show({
title: 'Code editor',
content: '/code_editor',
isIframe: true,
onClose: () => console.log('Popup closed'),
onMaximize: () => console.log('Popup maximized'),
onRestore: () => console.log('Popup restored'),
customIcons: [
{
class: 'fas fa-info-circle',
action: 'info',
onClick: () => console.log('Info icon clicked')
}
]
});
}
// Attach the event listener once during DOMContentLoaded
document.addEventListener('DOMContentLoaded', function() {
const diffButton = document.getElementById('diffButton');
diffButton.addEventListener('click', showHtmlDiffPopup);
const editorButton = document.getElementById('editorButton');
editorButton.addEventListener('click', showEditorPopup);
});
// Example usage: Assuming `projectVersionHistory` and `currentProjectId` are defined
// updateDiffButtonState(projectVersionHistory, currentProjectId);
function getCurrentVersionCode()
{
return projectVersionHistory[currentProjectId][currentVersion]["html"];
}
function getCurrentReferencedVersionNumber()
{
return projectVersionHistory[currentProjectId][currentVersion].referenceVersion;
}
function getCurrentVersionNumber()
{
return currentVersion;
}
// Function to populate the dropdown with available versions
function populateVersionDropdown() {
const versionDropdown = document.getElementById('versionDropdown');
const currentVersionDisplay = document.getElementById('currentVersionDisplay');
// Clear the dropdown first
versionDropdown.innerHTML = '';
// Get the version history path for the current version
const versionHistoryPath = getVersionHistoryPath(currentVersion);
const versionsInPath = Object.keys(versionHistoryPath).map(Number).sort((a, b) => a - b);
// Create a container for the timeline
const timelineContainer = document.createElement('div');
timelineContainer.style.position = 'relative';
timelineContainer.style.paddingLeft = '20px';
timelineContainer.style.borderLeft = '2px solid #ddd';
timelineContainer.style.margin = '10px 0';
// Retrieve the reference version for the current version
referenceVersion = null;
if (currentProjectId != null)
referenceVersion = projectVersionHistory[currentProjectId][currentVersion]?.referenceVersion || null;
// Populate the dropdown with the versions in the history path
for (let version = 1; version <= getMaxVersion(); version++) {
const versionLink = document.createElement('a');
versionLink.style.display = 'flex';
versionLink.style.justifyContent = 'space-between';
versionLink.style.alignItems = 'center';
versionLink.style.padding = '8px 12px';
const leftSection = document.createElement('div');
leftSection.style.display = 'flex';
leftSection.style.flexDirection = 'column';
leftSection.style.flex = '1';
const versionHeader = document.createElement('div');
versionHeader.style.display = 'flex';
versionHeader.style.alignItems = 'center';
versionHeader.style.justifyContent = 'space-between';
versionHeader.style.width = '100%';
const versionText = document.createElement('span');
versionText.textContent = `Version ${version}`;
versionText.className = 'version-number';
versionHeader.appendChild(versionText);
const timestamp = projectVersionHistory[currentProjectId][version]?.timestamp;
if (timestamp) {
const timeSpan = document.createElement('span');
timeSpan.textContent = moment.utc(timestamp).fromNow();
timeSpan.style.color = '#888';
timeSpan.style.fontSize = '0.8em';
timeSpan.style.marginLeft = '10px';
versionHeader.appendChild(timeSpan);
}
leftSection.appendChild(versionHeader);
// Add version summary if present
const versionSummary = projectVersionHistory[currentProjectId][version]?.versionSummary;
if (versionSummary) {
const summarySpan = document.createElement('span');
summarySpan.textContent = versionSummary;
summarySpan.className = 'version-summary';
summarySpan.style.color = '#666';
summarySpan.style.fontSize = '0.9em';
summarySpan.style.fontStyle = 'italic';
leftSection.appendChild(summarySpan);
}
// Add diff stats if available
const diff = projectVersionHistory[currentProjectId][version]?.diff;
if (diff) {
const statsSpan = document.createElement('span');
statsSpan.style.fontSize = '0.8em';
statsSpan.style.color = '#888';
// Calculate diff size in KB
const diffSize = (new Blob([diff]).size / 1024).toFixed(1);
// Count number of chunks by looking for @@ markers
const chunkCount = (diff.match(/@@/g) || []).length / 2;
statsSpan.innerHTML = ` ${diffSize}KB in ${chunkCount} location${chunkCount !== 1 ? 's' : ''}`;
statsSpan.style.marginTop = '2px';
leftSection.appendChild(statsSpan);
}
versionLink.appendChild(leftSection);
versionLink.dataset.version = version;
// Add screenshot if this version has a referencePageID
const referencePageID = projectVersionHistory[currentProjectId][version]?.referencePageID;
if (referencePageID && referencePageID.includes('fuzzycode') && referencePageID.includes('/page/')) {
const hashKey = referencePageID.split('/').pop();
const screenshotUrl = buildServiceUrl(ASSETS_ORIGIN, `uploaded/${hashKey}.webp`);
const screenshotDiv = document.createElement('div');
screenshotDiv.style.minWidth = '50px';
screenshotDiv.style.minHeight = '50px';
screenshotDiv.style.marginRight = '10px';
screenshotDiv.style.backgroundImage = `url(${screenshotUrl})`;
screenshotDiv.style.backgroundSize = 'contain';
screenshotDiv.style.backgroundPosition = 'center';
screenshotDiv.style.borderRadius = '5px';
screenshotDiv.style.border = '1px solid #ddd';
leftSection.insertBefore(screenshotDiv, leftSection.firstChild);
}
// Add timeline node
const timelineNode = document.createElement('div');
timelineNode.style.position = 'absolute';
timelineNode.style.left = '-6px';
timelineNode.style.width = '10px';
timelineNode.style.height = '10px';
timelineNode.style.borderRadius = '50%';
// Color scheme: first version (green), path versions (yellow), current version (blue)
if (version === Math.min(...versionsInPath)) {
timelineNode.style.backgroundColor = '#4CAF50'; // First version in green
} else if (version === currentVersion) {
timelineNode.style.backgroundColor = '#007bff'; // Current version in blue
} else if (versionsInPath.includes(version)) {
timelineNode.style.backgroundColor = '#FFA500'; // Path versions in yellow/orange
} else {
timelineNode.style.backgroundColor = '#ddd'; // Non-path versions in gray
}
timelineNode.style.border = '2px solid white';
timelineNode.style.marginTop = '15px';
// Highlight the current version
if (version === currentVersion) {
versionLink.classList.add('selected');
timelineNode.style.backgroundColor = '#007bff';
timelineNode.style.boxShadow = '0 0 0 3px rgba(0,123,255,0.3)';
}
// Style for reference version
if (version === referenceVersion) {
timelineNode.style.backgroundColor = '#FFA500';
}
timelineContainer.appendChild(timelineNode);
versionLink.addEventListener('click', function () {
// Update the current version display
currentVersionDisplay.textContent = version;
// Change the actual current version
updateCurrentVersion(version);
// Refresh iframe content
refreshIframeWithHTMLContent(projectVersionHistory[currentProjectId][version]["html"]);
// Hide the dropdown
versionDropdown.classList.remove('show');
// Update the version display
updateVersionDisplay();
});
timelineContainer.appendChild(versionLink);
}
versionDropdown.appendChild(timelineContainer);
// Always make it scrollable with a fixed height
versionDropdown.style.maxHeight = '400px';
versionDropdown.style.overflowY = 'auto';
versionDropdown.style.paddingRight = '10px';
// Ensure the current version is visible when dropdown opens
const selectedVersionLink = versionDropdown.querySelector('.selected');
if (selectedVersionLink) {
selectedVersionLink.scrollIntoView({ block: 'nearest' });
}
}
// Toggle the dropdown on button click
document.getElementById('historyBtn').addEventListener('click', function() {
if (currentProjectId) {
if (popup === null) popup = new PopupWindow();
popup.show({
title: 'Version History',
content: `/project_history/${currentProjectId}`,
isIframe: true,
onClose: (event) => {
console.log('Version History popup closed', event);
popup = null; // Clear reference when closed
}
});
}
});
document.getElementById('versionButton').addEventListener('click', function (event) {
event.stopPropagation(); // Prevent the click event from bubbling up
const versionDropdown = document.getElementById('versionDropdown');
versionDropdown.classList.toggle('show');
// Ensure the current version is in view when the dropdown is shown
const selectedVersionLink = versionDropdown.querySelector('.selected');
if (selectedVersionLink) {
selectedVersionLink.scrollIntoView({ block: 'nearest' });
}
});
// Close dropdown when clicking outside
window.addEventListener('click', function (e) {
if (!e.target.closest('.version-button')) {
const versionDropdown = document.getElementById('versionDropdown');
if (versionDropdown.classList.contains('show')) {
versionDropdown.classList.remove('show');
}
}
});
// Error fixing mode for the swipe button
let currentEditMode = 'swipe'; // Default mode
// Call this on page load to set initial state
document.addEventListener('DOMContentLoaded', function() {
// Add listener for enhanced errors toggle (Sentry)
document.getElementById('enhancedErrorsToggle').addEventListener('change', function() {
enhancedErrorsEnabled = this.checked;
sentryOn = enhancedErrorsEnabled;
// Re-render the preview via /temp_page so the child gets the updated helper block
reRenderIframeWithCurrentState(this.checked ? 'enable_sentry' : 'disable_sentry');
});
// Add listener for eruda toggle (Console Tools)
document.getElementById('erudaToggle').addEventListener('change', function() {
erudaOn = this.checked;
// Re-render the preview via /temp_page so the child gets the eruda scripts embedded
reRenderIframeWithCurrentState(this.checked ? 'enable_eruda' : 'disable_eruda');
});
});
//new swipe button
document.querySelector('swipe-button').addEventListener('selected', (e) => {
var action = e.detail.value;
makemychange(action);
});
async function makemychange(action) {
if (isSoundOn) audioPlayer_whoosh.play();
const prompt = document.getElementById('customChanges').value;
if (!prompt) {
showToast('error', 'Please enter something you would like to change first');
return;
}
// Check if the text is consistent with the intended "CHANGE" mode.
if (!(await confirmActionMode(prompt, "CHANGE"))) return;
gloalLastMakeChangePrompt = prompt;
const button = document.getElementById('change-swipe-button');
updateButtonProgress(button, 'loading');
if (action === 'make') {
// Main mode now auto-routes by project size without prompting.
const iframeContent = projectVersionHistory[currentProjectId][currentVersion]["html"];
const tokenCount = calculateTokenApproximation(iframeContent).approxTokens;
const autoFastMode = tokenCount > LARGE_PROJECT_FAST_MODE_THRESHOLD;
await applyAction(prompt, null, autoFastMode, null, false);
} else if (action === 'fast') {
// Fast mode should always use the fast endpoint without prompting.
await applyAction(prompt, null, true, null, false);
} else if (action === 'major' || action === 'meticulous') {
// Major/meticulous use normal mode with large-project prompt behavior.
await applyAction(prompt, null, false);
} else {
await applyAction(prompt, null, false);
}
updateButtonProgress(button, 'normal');
}
const TOUCHPAD_FAST_AGENT = 'BIG_FAST_REASONER';
async function runTouchpadFastChange() {
if (isSoundOn) audioPlayer_whoosh.play();
const prompt = (window.FUZZYCODE_TOUCHPAD_UPDATE_PROMPT || '').trim();
if (!prompt) {
showToast('error', 'Touchpad prompt unavailable. Please refresh and try again.');
return;
}
if (!(await confirmActionMode(prompt, "CHANGE"))) return;
const button = document.getElementById('touchpadFastChangeBtn');
if (!button) return;
updateButtonProgress(button, 'loading');
try {
await applyAction(prompt, null, true, TOUCHPAD_FAST_AGENT);
} finally {
updateButtonProgress(button, 'normal');
}
}
const touchpadFastChangeButton = document.getElementById('touchpadFastChangeBtn');
if (touchpadFastChangeButton) {
touchpadFastChangeButton.addEventListener('click', runTouchpadFastChange);
}
const publishVisibilitySelect = document.getElementById('publishVisibilitySelect');
if (publishVisibilitySelect) {
publishVisibilitySelect.addEventListener('change', syncPublishVisibilityUi);
}
const legacyVisibilityToggle = document.getElementById('visibilityToggle');
if (legacyVisibilityToggle) {
legacyVisibilityToggle.addEventListener('change', syncPublishVisibilityUi);
}
syncPublishVisibilityUi();
function getVersionHistoryPath(versionNumber) {
// Default to the current version if none is provided.
const currentVersion = getCurrentVersionNumber();
versionNumber =
typeof versionNumber !== 'undefined' && versionNumber !== null
? versionNumber
: currentVersion;
versionNumber = normalizeVersionNumber(versionNumber);
// Get the current project’s version history.
const projectId = currentProjectId; // assumed to be globally available
if (!projectId)
{
console.warn(`No project defined for version history`);
return {};
}
const fullHistory = projectVersionHistory[projectId];
if (!fullHistory) {
console.error(`No version history found for project: ${projectId}`);
return {};
}
// Build the chain by following the referenceVersion links.
const historyChain = {};
let current = versionNumber;
while (fullHistory.hasOwnProperty(current)) {
// Make a shallow copy of the version object so that if more fields are added in the future,
// they are automatically included.
const versionObj = { ...fullHistory[current] };
historyChain[current] = versionObj;
// If there is no valid referenceVersion (e.g. null, undefined, or 0 as a sentinel),
// then we have reached the start of the chain.
if (!versionObj.referenceVersion) {
break;
}
current = versionObj.referenceVersion;
}
return historyChain;
}
/**
* Returns the direct child versions for a given version that match a specific prompt.
*
* A "direct child" is any version whose `referenceVersion` field equals the supplied
* (or default) version. The function then further filters those children by comparing
* their `lastPrompt` field to a supplied prompt.
*
* If the version number is omitted, it defaults to the current version (as returned by getCurrentVersionNumber()).
* If the prompt is omitted, it defaults to the prompt of the current version.
*
* @param {number} [parentVersionNumber] - The version number to look for direct children of.
* @param {string} [prompt] - The prompt text to match. Defaults to the current version's prompt.
* @returns {Object} An object containing only the matching direct child versions.
*/
function getDirectReferencesMatchingPrompt(parentVersionNumber, prompt) {
// Get the current project ID and its version history.
const projectId = currentProjectId; // assumed to be globally available
const fullHistory = projectVersionHistory[projectId];
if (!fullHistory) {
console.error(`No version history found for project: ${projectId}`);
return {};
}
// Default the parent version to the current version if not supplied.
const currentVersion = getCurrentVersionNumber();
parentVersionNumber = (typeof parentVersionNumber !== 'undefined' && parentVersionNumber !== null)
? parentVersionNumber
: currentVersion;
// If no prompt is provided, use the prompt from the current version.
if (!prompt) {
if (fullHistory[currentVersion] && fullHistory[currentVersion].lastPrompt) {
prompt = fullHistory[currentVersion].lastPrompt;
} else {
console.error(`No prompt found in the current version (${currentVersion})`);
return {};
}
}
// Search the full history for versions that directly reference the target version
// and that have a matching prompt.
const result = {};
for (const ver in fullHistory) {
if (fullHistory.hasOwnProperty(ver)) {
const versionObj = fullHistory[ver];
// Ensure we are looking at direct references.
// (Convert to numbers if necessary.)
if (Number(versionObj.referenceVersion) === Number(parentVersionNumber)) {
// Check that the prompt matches.
if (versionObj.lastPrompt === prompt) {
// Include a shallow copy of this version so that all its fields are kept.
result[ver] = { ...versionObj };
}
}
}
}
return result;
}
var historyAttachmentsUI = null;
document.addEventListener("DOMContentLoaded", () => {
historyAttachmentsUI = document.querySelector("history-attachments-ui");
// Get version history from parent (assumed to exist)
const currentVersion = parent.getCurrentVersionNumber();
const versionHistory = parent.getVersionHistoryPath(currentVersion);
// Use the public reloadHistory method to initialize the component.
historyAttachmentsUI.reloadHistory(versionHistory, currentVersion);
historyAttachmentsUI.setSelectionCallback((selection) => {
console.log("Selection changed:", selection);
});
window.setHistoryContext = historyAttachmentsUI.setHistoryContext.bind(historyAttachmentsUI);
});
/* attached images div */
// Create an instance of the image importer, targeting the div with id 'screenshotImageContainer'
var screenshotter_image_importer = null;
document.addEventListener('DOMContentLoaded', function() {
screenshotter_image_importer = createImageImporter('screenshotImageContainer');
});
document.querySelector('.close').addEventListener('click', function() {
document.getElementById('screenshotModal').style.display = "none";
});
document.getElementById('screenshotBtn').onclick = function() {
captureAndProcessScreenshot({
iframeSelector: '#responseIframe',
onSuccess: function(imageDataUrl) {
// Replicates the original "modal" behavior:
screenshotter_image_importer.setImage(imageDataUrl);
if (document.getElementById('screenshotModal')) {
document.getElementById('screenshotModal').style.display = "block";
}
},
onError: function(err) {
console.error('Error taking screenshot:', err);
// Replicate toast error notification:
showToast('error', 'Unable to take screenshot. You can take one on your own and then paste it in.');
if (document.getElementById('removeScreenshotImage')) {
document.getElementById('removeScreenshotImage').style.display = 'block';
}
}
});
};
function downloadImage(dataUrl, filename) {
var a = document.createElement('a');
a.href = dataUrl;
a.setAttribute('download', filename);
a.click();
}
/* end attached images div */
// New helper function: confirmActionMode
// 'intendedMode' should be either "CREATE" (for sendButton) or "CHANGE" (for makemychange)
async function confirmActionMode(text, intendedMode) {
try {
// Use your existing classification function to classify the text.
const result = window.classify_CoU(text);
// If model is not ready, or an error occurs, skip the check.
if (!result || result.error) {
return true;
}
// If the classification doesn't match the intended mode, show the alert.
if (result.prediction !== intendedMode) {
// Map intendedMode to a friendly label and description.
const modeLabel = intendedMode === "CREATE" ? "Create" : "Update";
const modeDescription = intendedMode === "CREATE"
? "create something totally new"
: "update what you already have";
const alertText = `You are on the "${modeLabel}" mode, which will ${modeDescription}. Is that what you wanted to do?`;
const { isConfirmed } = await Swal.fire({
title: 'Confirm Action',
text: alertText,
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'Keep going',
cancelButtonText: 'Cancel'
});
return isConfirmed;
}
return true;
} catch (e) {
console.error("Error during action mode confirmation:", e);
// In case of any error, allow the action to proceed.
return true;
}
}
// Pages Promo Bar Animation
document.addEventListener('DOMContentLoaded', function() {
const promoBar = document.getElementById('pagesPromoBar');
const closeButton = document.getElementById('closePromoBar');
const promoButton = document.querySelector('.promo-button');
// Check if user has previously minimized the promo in this session
const promoBarState = sessionStorage.getItem('pagesPromoBarState');
if (!promoBarState) {
// Show promo bar after a short delay
setTimeout(() => {
promoBar.classList.add('show');
}, 1500);
} else if (promoBarState === 'minimized') {
promoBar.classList.add('show', 'minimized');
} else if (promoBarState === 'expanded') {
promoBar.classList.add('show');
}
closeButton.addEventListener('click', function() {
if (promoBar.classList.contains('minimized')) {
// If it's already minimized, expand it
promoBar.classList.remove('minimized');
sessionStorage.setItem('pagesPromoBarState', 'expanded');
} else {
// Minimize it
promoBar.classList.add('minimized');
sessionStorage.setItem('pagesPromoBarState', 'minimized');
}
});
// Allow clicking on the minimized bar to expand it again
promoBar.addEventListener('click', function(e) {
if (promoBar.classList.contains('minimized') && e.target !== closeButton && !closeButton.contains(e.target)) {
promoBar.classList.remove('minimized');
sessionStorage.setItem('pagesPromoBarState', 'expanded');
}
});
// Prevent the promo button from causing the bar to disappear
promoButton.addEventListener('click', function(e) {
// Minimize the bar instead of navigating away
if (!promoBar.classList.contains('minimized')) {
e.preventDefault();
promoBar.classList.add('minimized');
sessionStorage.setItem('pagesPromoBarState', 'minimized');
// Open the link in a new tab instead
window.open(this.href, '_blank');
}
});
// Automatically collapse the promo bar when scrolling down
let lastScrollTop = 0;
window.addEventListener('scroll', function() {
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
// If we're scrolling down and the bar is expanded, minimize it
if (scrollTop > lastScrollTop && scrollTop > 50) {
if (promoBar.classList.contains('show') && !promoBar.classList.contains('minimized')) {
promoBar.classList.add('minimized');
sessionStorage.setItem('pagesPromoBarState', 'minimized');
}
}
lastScrollTop = scrollTop;
}, { passive: true });
});
// Subscription Required Modal
function showSubscriptionRequiredModal(upgradeUrl = '/payment-required') {
// Check if modal already exists
let existingModal = document.querySelector('.subscription-modal');
if (existingModal) {
existingModal.remove();
}
const modal = document.createElement('div');
modal.className = 'subscription-modal';
modal.innerHTML = `
🚀 Subscription Required
To continue using AI features, you need an active subscription.
What you'll get:
✨ AI code generation
🎨 AI image & sound creation
📤 One-click publishing & sharing
🌐 Community gallery access
🕒 Version history with undo
🛡️ AI-powered moderation and safety
Parent/Guardian Consent Required
By purchasing, you certify that you are 18+ and the parent/guardian of any child using this service.
Checking payment status...
`;
document.body.appendChild(modal);
// Add styles for spinner if not already present
if (!document.querySelector('style[data-subscription-modal-styles]')) {
const style = document.createElement('style');
style.setAttribute('data-subscription-modal-styles', 'true');
style.textContent = `
.subscription-modal .spinner {
border: 2px solid #f3f3f3;
border-top: 2px solid #3498db;
border-radius: 50%;
width: 16px;
height: 16px;
animation: spin 1s linear infinite;
vertical-align: middle;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`;
document.head.appendChild(style);
}
// Store reference to the payment window
let paymentWindow = null;
let checkInterval = null;
let pollingStarted = false;
// Function to start polling for subscription status
function startPolling() {
if (pollingStarted) return;
pollingStarted = true;
// Show status message
const statusDiv = document.getElementById('modal-status');
if (statusDiv) {
statusDiv.style.display = 'block';
}
let checkCount = 0;
const maxChecks = 60; // 60 seconds timeout
checkInterval = setInterval(async () => {
checkCount++;
try {
// Check subscription status
const status = await checkSubscriptionStatus({ force: true });
if (status.has_active_subscription) {
// Stop polling
clearInterval(checkInterval);
// Update status message
if (statusDiv) {
statusDiv.innerHTML = '✓ Payment successful! Refreshing access...';
}
console.log('Payment successful, refreshing broker session state...');
try {
const refreshed = await window.sessionUser?.refresh_session?.();
if (!refreshed) {
console.warn('Broker session refresh did not confirm updated access state');
} else {
console.log('Broker session refreshed successfully');
}
} catch (refreshError) {
console.error('Exception during broker session refresh:', refreshError);
}
// Close payment window if still open
if (paymentWindow && !paymentWindow.closed) {
paymentWindow.close();
}
// Remove modal
modal.remove();
// Show success toast
showToast('success', 'Subscription activated! You can now use AI features.');
// Optional: Retry the last failed request if you track it
// For now, user will need to retry their action
} else if (checkCount >= maxChecks) {
// Timeout
clearInterval(checkInterval);
if (statusDiv) {
statusDiv.innerHTML = '⚠ Checking is taking longer than expected. Please refresh the page and try again.';
}
}
// Also check if payment window was closed
if (paymentWindow && paymentWindow.closed && !status.has_active_subscription) {
// Window closed without completing payment
clearInterval(checkInterval);
if (statusDiv) {
statusDiv.style.display = 'none';
}
pollingStarted = false;
}
} catch (error) {
console.error('Error during subscription check:', error);
}
}, 1000); // Check every second
}
// Handle upgrade button click
const upgradeBtn = modal.querySelector('#modal-upgrade-btn');
upgradeBtn.addEventListener('click', async function() {
// Try to create checkout session first
const checkoutUrl = await createCheckoutSession();
if (checkoutUrl) {
// Open payment window
paymentWindow = window.open(
checkoutUrl,
'fuzzycode-payment',
'width=800,height=600'
);
// Start polling immediately
startPolling();
} else {
// Fallback to the provided URL
paymentWindow = window.open(upgradeUrl, '_blank');
// Still start polling in case user completes payment
startPolling();
}
});
// Add click handler to overlay to close modal
modal.querySelector('.modal-overlay').addEventListener('click', function() {
if (checkInterval) {
clearInterval(checkInterval);
}
modal.remove();
});
// Add escape key handler
const escapeHandler = function(e) {
if (e.key === 'Escape') {
if (checkInterval) {
clearInterval(checkInterval);
}
modal.remove();
document.removeEventListener('keydown', escapeHandler);
}
};
document.addEventListener('keydown', escapeHandler);
}