From f2cb76b65ddf9cd8f2cab823b4d57c63f5012ae0 Mon Sep 17 00:00:00 2001 From: Oussama Douhou Date: Sat, 10 Jan 2026 13:28:14 +0100 Subject: [PATCH] your-mom --- src/frontend/app.js | 752 ++++++++++++++++++++++---------------------- 1 file changed, 376 insertions(+), 376 deletions(-) diff --git a/src/frontend/app.js b/src/frontend/app.js index 5898942..c710252 100644 --- a/src/frontend/app.js +++ b/src/frontend/app.js @@ -1,213 +1,213 @@ const translations = { - en: { - title: 'AI Stack Deployer', - subtitle: 'Deploy your personal OpenCode AI coding assistant in seconds', - chooseStackName: 'Choose Your Stack Name', - availableAt: 'Your AI assistant will be available at', - stackName: 'Stack Name', - placeholder: 'e.g., john-dev', - inputHint: '3-20 characters, lowercase letters, numbers, and hyphens only', - deployBtn: 'Deploy My AI Stack', - deploying: 'Deploying Your Stack', - stack: 'Stack', - initializing: 'Initializing deployment...', - successMessage: 'Your AI coding assistant is ready to use', - stackNameLabel: 'Stack Name:', - openStack: 'Open My AI Stack', - deployAnother: 'Deploy Another Stack', - tryAgain: 'Try Again', - poweredBy: 'Powered by', - deploymentComplete: 'Deployment Complete', - deploymentFailed: 'Deployment Failed', - nameRequired: 'Name is required', - nameLengthError: 'Name must be between 3 and 20 characters', - nameCharsError: 'Only lowercase letters, numbers, and hyphens allowed', - nameHyphenError: 'Cannot start or end with a hyphen', - nameReserved: 'This name is reserved', - checkingAvailability: 'Checking availability...', - nameAvailable: '✓ Name is available!', - nameNotAvailable: 'Name is not available', - checkFailed: 'Failed to check availability', - connectionLost: 'Connection lost. Please refresh and try again.', - deployingText: 'Deploying...', - yournamePlaceholder: 'yourname' - }, - nl: { - title: 'AI Stack Deployer', - subtitle: 'Implementeer je persoonlijke OpenCode AI programmeerassistent in seconden', - chooseStackName: 'Kies Je Stack Naam', - availableAt: 'Je AI-assistent is beschikbaar op', - stackName: 'Stack Naam', - placeholder: 'bijv., jan-dev', - inputHint: '3-20 tekens, kleine letters, cijfers en koppeltekens', - deployBtn: 'Implementeer Mijn AI Stack', - deploying: 'Stack Wordt Geïmplementeerd', - stack: 'Stack', - initializing: 'Implementatie initialiseren...', - successMessage: 'Je AI programmeerassistent is klaar voor gebruik', - stackNameLabel: 'Stack Naam:', - openStack: 'Open Mijn AI Stack', - deployAnother: 'Implementeer Nog Een Stack', - tryAgain: 'Probeer Opnieuw', - poweredBy: 'Mogelijk gemaakt door', - deploymentComplete: 'Implementatie Voltooid', - deploymentFailed: 'Implementatie Mislukt', - nameRequired: 'Naam is verplicht', - nameLengthError: 'Naam moet tussen 3 en 20 tekens zijn', - nameCharsError: 'Alleen kleine letters, cijfers en koppeltekens toegestaan', - nameHyphenError: 'Kan niet beginnen of eindigen met een koppelteken', - nameReserved: 'Deze naam is gereserveerd', - checkingAvailability: 'Beschikbaarheid controleren...', - nameAvailable: '✓ Naam is beschikbaar!', - nameNotAvailable: 'Naam is niet beschikbaar', - checkFailed: 'Controle mislukt', - connectionLost: 'Verbinding verbroken. Ververs de pagina en probeer opnieuw.', - deployingText: 'Implementeren...', - yournamePlaceholder: 'jouwnaam' - }, - ar: { - title: 'AI Stack Deployer', - subtitle: 'انشر مساعد البرمجة الذكي الخاص بك في ثوانٍ', - chooseStackName: 'اختر اسم المشروع', - availableAt: 'سيكون مساعدك الذكي متاحًا على', - stackName: 'اسم المشروع', - placeholder: 'مثال: أحمد-dev', - inputHint: '3-20 حرف، أحرف صغيرة وأرقام وشرطات فقط', - deployBtn: 'انشر مشروعي', - deploying: 'جاري النشر', - stack: 'المشروع', - initializing: 'جاري التهيئة...', - successMessage: 'مساعد البرمجة الذكي جاهز للاستخدام', - stackNameLabel: 'اسم المشروع:', - openStack: 'افتح مشروعي', - deployAnother: 'انشر مشروع آخر', - tryAgain: 'حاول مرة أخرى', - poweredBy: 'مدعوم من', - deploymentComplete: 'تم النشر بنجاح', - deploymentFailed: 'فشل النشر', - nameRequired: 'الاسم مطلوب', - nameLengthError: 'يجب أن يكون الاسم بين 3 و 20 حرفًا', - nameCharsError: 'يُسمح فقط بالأحرف الصغيرة والأرقام والشرطات', - nameHyphenError: 'لا يمكن أن يبدأ أو ينتهي بشرطة', - nameReserved: 'هذا الاسم محجوز', - checkingAvailability: 'جاري التحقق...', - nameAvailable: '✓ الاسم متاح!', - nameNotAvailable: 'الاسم غير متاح', - checkFailed: 'فشل التحقق', - connectionLost: 'انقطع الاتصال. يرجى تحديث الصفحة والمحاولة مرة أخرى.', - deployingText: 'جاري النشر...', - yournamePlaceholder: 'اسمك' - } + en: { + title: 'AI Stack Deployer', + subtitle: 'Deploy your personal OpenCode AI coding assistant in seconds', + chooseStackName: 'Choose Your Stack Name', + availableAt: 'Your AI assistant will be available at', + stackName: 'Stack Name', + placeholder: 'e.g., your-mom-is-fat-dev', + inputHint: '3-20 characters, lowercase letters, numbers, and hyphens only', + deployBtn: 'Deploy My AI Stack', + deploying: 'Deploying Your Stack', + stack: 'Stack', + initializing: 'Initializing deployment...', + successMessage: 'Your AI coding assistant is ready to use', + stackNameLabel: 'Stack Name:', + openStack: 'Open My AI Stack', + deployAnother: 'Deploy Another Stack', + tryAgain: 'Try Again', + poweredBy: 'Powered by', + deploymentComplete: 'Deployment Complete', + deploymentFailed: 'Deployment Failed', + nameRequired: 'Name is required', + nameLengthError: 'Name must be between 3 and 20 characters', + nameCharsError: 'Only lowercase letters, numbers, and hyphens allowed', + nameHyphenError: 'Cannot start or end with a hyphen', + nameReserved: 'This name is reserved', + checkingAvailability: 'Checking availability...', + nameAvailable: '✓ Name is available!', + nameNotAvailable: 'Name is not available', + checkFailed: 'Failed to check availability', + connectionLost: 'Connection lost. Please refresh and try again.', + deployingText: 'Deploying...', + yournamePlaceholder: 'yourname' + }, + nl: { + title: 'AI Stack Deployer', + subtitle: 'Implementeer je persoonlijke OpenCode AI programmeerassistent in seconden', + chooseStackName: 'Kies Je Stack Naam', + availableAt: 'Je AI-assistent is beschikbaar op', + stackName: 'Stack Naam', + placeholder: 'bijv., je-moeder-is-dik-dev', + inputHint: '3-20 tekens, kleine letters, cijfers en koppeltekens', + deployBtn: 'Implementeer Mijn AI Stack', + deploying: 'Stack Wordt Geïmplementeerd', + stack: 'Stack', + initializing: 'Implementatie initialiseren...', + successMessage: 'Je AI programmeerassistent is klaar voor gebruik', + stackNameLabel: 'Stack Naam:', + openStack: 'Open Mijn AI Stack', + deployAnother: 'Implementeer Nog Een Stack', + tryAgain: 'Probeer Opnieuw', + poweredBy: 'Mogelijk gemaakt door', + deploymentComplete: 'Implementatie Voltooid', + deploymentFailed: 'Implementatie Mislukt', + nameRequired: 'Naam is verplicht', + nameLengthError: 'Naam moet tussen 3 en 20 tekens zijn', + nameCharsError: 'Alleen kleine letters, cijfers en koppeltekens toegestaan', + nameHyphenError: 'Kan niet beginnen of eindigen met een koppelteken', + nameReserved: 'Deze naam is gereserveerd', + checkingAvailability: 'Beschikbaarheid controleren...', + nameAvailable: '✓ Naam is beschikbaar!', + nameNotAvailable: 'Naam is niet beschikbaar', + checkFailed: 'Controle mislukt', + connectionLost: 'Verbinding verbroken. Ververs de pagina en probeer opnieuw.', + deployingText: 'Implementeren...', + yournamePlaceholder: 'jouwnaam' + }, + ar: { + title: 'AI Stack Deployer', + subtitle: 'انشر مساعد البرمجة الذكي الخاص بك في ثوانٍ', + chooseStackName: 'اختر اسم المشروع', + availableAt: 'سيكون مساعدك الذكي متاحًا على', + stackName: 'اسم المشروع', + placeholder: 'مثال: أحمد-dev', + inputHint: '3-20 حرف، أحرف صغيرة وأرقام وشرطات فقط', + deployBtn: 'انشر مشروعي', + deploying: 'جاري النشر', + stack: 'المشروع', + initializing: 'جاري التهيئة...', + successMessage: 'مساعد البرمجة الذكي جاهز للاستخدام', + stackNameLabel: 'اسم المشروع:', + openStack: 'افتح مشروعي', + deployAnother: 'انشر مشروع آخر', + tryAgain: 'حاول مرة أخرى', + poweredBy: 'مدعوم من', + deploymentComplete: 'تم النشر بنجاح', + deploymentFailed: 'فشل النشر', + nameRequired: 'الاسم مطلوب', + nameLengthError: 'يجب أن يكون الاسم بين 3 و 20 حرفًا', + nameCharsError: 'يُسمح فقط بالأحرف الصغيرة والأرقام والشرطات', + nameHyphenError: 'لا يمكن أن يبدأ أو ينتهي بشرطة', + nameReserved: 'هذا الاسم محجوز', + checkingAvailability: 'جاري التحقق...', + nameAvailable: '✓ الاسم متاح!', + nameNotAvailable: 'الاسم غير متاح', + checkFailed: 'فشل التحقق', + connectionLost: 'انقطع الاتصال. يرجى تحديث الصفحة والمحاولة مرة أخرى.', + deployingText: 'جاري النشر...', + yournamePlaceholder: 'اسمك' + } }; let currentLang = 'en'; function detectLanguage() { - const browserLang = navigator.language || navigator.userLanguage; - const lang = browserLang.split('-')[0].toLowerCase(); - - if (translations[lang]) { - return lang; - } - return 'en'; + const browserLang = navigator.language || navigator.userLanguage; + const lang = browserLang.split('-')[0].toLowerCase(); + + if (translations[lang]) { + return lang; + } + return 'en'; } function t(key) { - return translations[currentLang][key] || translations['en'][key] || key; + return translations[currentLang][key] || translations['en'][key] || key; } function setLanguage(lang) { - if (!translations[lang]) return; - - currentLang = lang; - localStorage.setItem('preferredLanguage', lang); - - document.documentElement.lang = lang; - document.documentElement.dir = lang === 'ar' ? 'rtl' : 'ltr'; - - document.querySelectorAll('[data-i18n]').forEach(el => { - const key = el.getAttribute('data-i18n'); - el.textContent = t(key); - }); - - document.querySelectorAll('[data-i18n-placeholder]').forEach(el => { - const key = el.getAttribute('data-i18n-placeholder'); - el.placeholder = t(key); - }); - - document.querySelectorAll('.lang-btn').forEach(btn => { - btn.classList.remove('active'); - if (btn.getAttribute('data-lang') === lang) { - btn.classList.add('active'); - } - }); - - const previewNameEl = document.getElementById('preview-name'); - if (previewNameEl && !stackNameInput?.value) { - previewNameEl.textContent = t('yournamePlaceholder'); - } - - const typewriterTarget = document.getElementById('typewriter-target'); - if (typewriterTarget && currentState === STATE.FORM) { - typewriter(typewriterTarget, t('chooseStackName')); + if (!translations[lang]) return; + + currentLang = lang; + localStorage.setItem('preferredLanguage', lang); + + document.documentElement.lang = lang; + document.documentElement.dir = lang === 'ar' ? 'rtl' : 'ltr'; + + document.querySelectorAll('[data-i18n]').forEach(el => { + const key = el.getAttribute('data-i18n'); + el.textContent = t(key); + }); + + document.querySelectorAll('[data-i18n-placeholder]').forEach(el => { + const key = el.getAttribute('data-i18n-placeholder'); + el.placeholder = t(key); + }); + + document.querySelectorAll('.lang-btn').forEach(btn => { + btn.classList.remove('active'); + if (btn.getAttribute('data-lang') === lang) { + btn.classList.add('active'); } + }); + + const previewNameEl = document.getElementById('preview-name'); + if (previewNameEl && !stackNameInput?.value) { + previewNameEl.textContent = t('yournamePlaceholder'); + } + + const typewriterTarget = document.getElementById('typewriter-target'); + if (typewriterTarget && currentState === STATE.FORM) { + typewriter(typewriterTarget, t('chooseStackName')); + } } function initLanguage() { - const saved = localStorage.getItem('preferredLanguage'); - const lang = saved || detectLanguage(); - setLanguage(lang); - - document.querySelectorAll('.lang-btn').forEach(btn => { - btn.addEventListener('click', () => { - setLanguage(btn.getAttribute('data-lang')); - }); + const saved = localStorage.getItem('preferredLanguage'); + const lang = saved || detectLanguage(); + setLanguage(lang); + + document.querySelectorAll('.lang-btn').forEach(btn => { + btn.addEventListener('click', () => { + setLanguage(btn.getAttribute('data-lang')); }); + }); } // Track active typewriter instances to prevent race conditions let activeTypewriters = new Map(); function typewriter(element, text, speed = 50) { - // Cancel any existing typewriter on this element - const elementId = element.id || 'default'; - if (activeTypewriters.has(elementId)) { - clearTimeout(activeTypewriters.get(elementId)); - activeTypewriters.delete(elementId); - } - - let i = 0; - element.innerHTML = ''; - - const cursor = document.createElement('span'); - cursor.className = 'typing-cursor'; - - const existingCursor = element.parentNode.querySelector('.typing-cursor'); - if (existingCursor) { - existingCursor.remove(); - } - - element.parentNode.insertBefore(cursor, element.nextSibling); + // Cancel any existing typewriter on this element + const elementId = element.id || 'default'; + if (activeTypewriters.has(elementId)) { + clearTimeout(activeTypewriters.get(elementId)); + activeTypewriters.delete(elementId); + } - function type() { - if (i < text.length) { - element.textContent += text.charAt(i); - i++; - const timeoutId = setTimeout(type, speed); - activeTypewriters.set(elementId, timeoutId); - } else { - activeTypewriters.delete(elementId); - } + let i = 0; + element.innerHTML = ''; + + const cursor = document.createElement('span'); + cursor.className = 'typing-cursor'; + + const existingCursor = element.parentNode.querySelector('.typing-cursor'); + if (existingCursor) { + existingCursor.remove(); + } + + element.parentNode.insertBefore(cursor, element.nextSibling); + + function type() { + if (i < text.length) { + element.textContent += text.charAt(i); + i++; + const timeoutId = setTimeout(type, speed); + activeTypewriters.set(elementId, timeoutId); + } else { + activeTypewriters.delete(elementId); } - type(); + } + type(); } // State Machine for Deployment const STATE = { - FORM: 'form', - PROGRESS: 'progress', - SUCCESS: 'success', - ERROR: 'error' + FORM: 'form', + PROGRESS: 'progress', + SUCCESS: 'success', + ERROR: 'error' }; let currentState = STATE.FORM; @@ -243,271 +243,271 @@ const tryAgainBtn = document.getElementById('try-again-btn'); // State Management function setState(newState) { - currentState = newState; + currentState = newState; - const states = [formState, progressState, successState, errorState]; - - states.forEach(state => { - state.style.display = 'none'; - state.classList.remove('fade-in'); - }); + const states = [formState, progressState, successState, errorState]; - let activeState; - switch (newState) { - case STATE.FORM: - activeState = formState; - break; - case STATE.PROGRESS: - activeState = progressState; - break; - case STATE.SUCCESS: - activeState = successState; - break; - case STATE.ERROR: - activeState = errorState; - break; - } + states.forEach(state => { + state.style.display = 'none'; + state.classList.remove('fade-in'); + }); - if (activeState) { - activeState.style.display = 'block'; - // Add a slight delay to ensure the display property has taken effect before adding the class - setTimeout(() => { - activeState.classList.add('fade-in'); - }, 10); - } + let activeState; + switch (newState) { + case STATE.FORM: + activeState = formState; + break; + case STATE.PROGRESS: + activeState = progressState; + break; + case STATE.SUCCESS: + activeState = successState; + break; + case STATE.ERROR: + activeState = errorState; + break; + } + + if (activeState) { + activeState.style.display = 'block'; + // Add a slight delay to ensure the display property has taken effect before adding the class + setTimeout(() => { + activeState.classList.add('fade-in'); + }, 10); + } } function validateName(name) { - if (!name) { - return { valid: false, error: t('nameRequired') }; - } + if (!name) { + return { valid: false, error: t('nameRequired') }; + } - const trimmedName = name.trim().toLowerCase(); + const trimmedName = name.trim().toLowerCase(); - if (trimmedName.length < 3 || trimmedName.length > 20) { - return { valid: false, error: t('nameLengthError') }; - } + if (trimmedName.length < 3 || trimmedName.length > 20) { + return { valid: false, error: t('nameLengthError') }; + } - if (!/^[a-z0-9-]+$/.test(trimmedName)) { - return { valid: false, error: t('nameCharsError') }; - } + if (!/^[a-z0-9-]+$/.test(trimmedName)) { + return { valid: false, error: t('nameCharsError') }; + } - if (trimmedName.startsWith('-') || trimmedName.endsWith('-')) { - return { valid: false, error: t('nameHyphenError') }; - } + if (trimmedName.startsWith('-') || trimmedName.endsWith('-')) { + return { valid: false, error: t('nameHyphenError') }; + } - const reservedNames = ['admin', 'api', 'www', 'root', 'system', 'test', 'demo', 'portal']; - if (reservedNames.includes(trimmedName)) { - return { valid: false, error: t('nameReserved') }; - } + const reservedNames = ['admin', 'api', 'www', 'root', 'system', 'test', 'demo', 'portal']; + if (reservedNames.includes(trimmedName)) { + return { valid: false, error: t('nameReserved') }; + } - return { valid: true, name: trimmedName }; + return { valid: true, name: trimmedName }; } // Real-time Name Validation let checkTimeout; stackNameInput.addEventListener('input', (e) => { - const value = e.target.value.toLowerCase(); - e.target.value = value; + const value = e.target.value.toLowerCase(); + e.target.value = value; - previewName.textContent = value || t('yournamePlaceholder'); + previewName.textContent = value || t('yournamePlaceholder'); - // Clear previous timeout - clearTimeout(checkTimeout); + // Clear previous timeout + clearTimeout(checkTimeout); - // Validate format first - const validation = validateName(value); + // Validate format first + const validation = validateName(value); - if (!validation.valid) { - stackNameInput.classList.remove('success'); + if (!validation.valid) { + stackNameInput.classList.remove('success'); + stackNameInput.classList.add('error'); + validationMessage.textContent = validation.error; + validationMessage.className = 'validation-message error'; + deployBtn.disabled = true; + return; + } + + stackNameInput.classList.remove('error', 'success'); + validationMessage.textContent = t('checkingAvailability'); + validationMessage.className = 'validation-message'; + + checkTimeout = setTimeout(async () => { + try { + const response = await fetch(`/api/check/${validation.name}`); + const data = await response.json(); + + if (data.available && data.valid) { + stackNameInput.classList.add('success'); + validationMessage.textContent = t('nameAvailable'); + validationMessage.className = 'validation-message success'; + deployBtn.disabled = false; + } else { stackNameInput.classList.add('error'); - validationMessage.textContent = validation.error; + validationMessage.textContent = data.error || t('nameNotAvailable'); validationMessage.className = 'validation-message error'; deployBtn.disabled = true; - return; + } + } catch (error) { + console.error('Failed to check name availability:', error); + validationMessage.textContent = t('checkFailed'); + validationMessage.className = 'validation-message error'; + deployBtn.disabled = true; } - - stackNameInput.classList.remove('error', 'success'); - validationMessage.textContent = t('checkingAvailability'); - validationMessage.className = 'validation-message'; - - checkTimeout = setTimeout(async () => { - try { - const response = await fetch(`/api/check/${validation.name}`); - const data = await response.json(); - - if (data.available && data.valid) { - stackNameInput.classList.add('success'); - validationMessage.textContent = t('nameAvailable'); - validationMessage.className = 'validation-message success'; - deployBtn.disabled = false; - } else { - stackNameInput.classList.add('error'); - validationMessage.textContent = data.error || t('nameNotAvailable'); - validationMessage.className = 'validation-message error'; - deployBtn.disabled = true; - } - } catch (error) { - console.error('Failed to check name availability:', error); - validationMessage.textContent = t('checkFailed'); - validationMessage.className = 'validation-message error'; - deployBtn.disabled = true; - } - }, 500); + }, 500); }); // Form Submission deployForm.addEventListener('submit', async (e) => { - e.preventDefault(); + e.preventDefault(); - const validation = validateName(stackNameInput.value); - if (!validation.valid) { - return; + const validation = validateName(stackNameInput.value); + if (!validation.valid) { + return; + } + + deployBtn.disabled = true; + deployBtn.innerHTML = `${t('deployingText')}`; + + try { + const response = await fetch('/api/deploy', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + name: validation.name + }) + }); + + const data = await response.json(); + + if (!response.ok || !data.success) { + throw new Error(data.error || 'Deployment failed'); } - deployBtn.disabled = true; - deployBtn.innerHTML = `${t('deployingText')}`; + deploymentId = data.deploymentId; + deploymentUrl = data.url; - try { - const response = await fetch('/api/deploy', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - name: validation.name - }) - }); + // Update progress UI + deployingName.textContent = validation.name; + deployingUrl.textContent = deploymentUrl; - const data = await response.json(); + // Switch to progress state + setState(STATE.PROGRESS); - if (!response.ok || !data.success) { - throw new Error(data.error || 'Deployment failed'); - } + // Start SSE connection + startProgressStream(deploymentId); - deploymentId = data.deploymentId; - deploymentUrl = data.url; - - // Update progress UI - deployingName.textContent = validation.name; - deployingUrl.textContent = deploymentUrl; - - // Switch to progress state - setState(STATE.PROGRESS); - - // Start SSE connection - startProgressStream(deploymentId); - - } catch (error) { - console.error('Deployment error:', error); - showError(error.message); - deployBtn.disabled = false; - deployBtn.innerHTML = ` + } catch (error) { + console.error('Deployment error:', error); + showError(error.message); + deployBtn.disabled = false; + deployBtn.innerHTML = ` ${t('deployBtn')} `; - } + } }); // SSE Progress Streaming function startProgressStream(deploymentId) { - eventSource = new EventSource(`/api/status/${deploymentId}`); + eventSource = new EventSource(`/api/status/${deploymentId}`); - eventSource.addEventListener('progress', (event) => { - const data = JSON.parse(event.data); - updateProgress(data); - }); + eventSource.addEventListener('progress', (event) => { + const data = JSON.parse(event.data); + updateProgress(data); + }); - eventSource.addEventListener('complete', (event) => { - const data = JSON.parse(event.data); - eventSource.close(); - showSuccess(data); - }); + eventSource.addEventListener('complete', (event) => { + const data = JSON.parse(event.data); + eventSource.close(); + showSuccess(data); + }); - eventSource.addEventListener('error', (event) => { - const data = event.data ? JSON.parse(event.data) : { message: 'Unknown error' }; - eventSource.close(); - showError(data.message); - }); + eventSource.addEventListener('error', (event) => { + const data = event.data ? JSON.parse(event.data) : { message: 'Unknown error' }; + eventSource.close(); + showError(data.message); + }); - eventSource.onerror = () => { - eventSource.close(); - showError(t('connectionLost')); - }; + eventSource.onerror = () => { + eventSource.close(); + showError(t('connectionLost')); + }; } // Update Progress UI function updateProgress(data) { - // Update progress bar - progressBar.style.width = `${data.progress}%`; - progressPercent.textContent = `${data.progress}%`; + // Update progress bar + progressBar.style.width = `${data.progress}%`; + progressPercent.textContent = `${data.progress}%`; - // Update current step - const stepContainer = document.querySelector('.progress-steps'); - stepContainer.innerHTML = ` + // Update current step + const stepContainer = document.querySelector('.progress-steps'); + stepContainer.innerHTML = `
⚙️
${data.currentStep}
`; - // Add to log - const logEntry = document.createElement('div'); - logEntry.className = 'log-entry'; - logEntry.textContent = `[${new Date().toLocaleTimeString()}] ${data.currentStep}`; - progressLog.appendChild(logEntry); - progressLog.scrollTop = progressLog.scrollHeight; + // Add to log + const logEntry = document.createElement('div'); + logEntry.className = 'log-entry'; + logEntry.textContent = `[${new Date().toLocaleTimeString()}] ${data.currentStep}`; + progressLog.appendChild(logEntry); + progressLog.scrollTop = progressLog.scrollHeight; } function showSuccess(data) { - successName.textContent = deployingName.textContent; - successUrl.textContent = deploymentUrl; - successUrl.href = deploymentUrl; - openStackBtn.href = deploymentUrl; + successName.textContent = deployingName.textContent; + successUrl.textContent = deploymentUrl; + successUrl.href = deploymentUrl; + openStackBtn.href = deploymentUrl; - setState(STATE.SUCCESS); - const targetSpan = document.getElementById('success-title'); - if(targetSpan) { - typewriter(targetSpan, t('deploymentComplete')); - } + setState(STATE.SUCCESS); + const targetSpan = document.getElementById('success-title'); + if (targetSpan) { + typewriter(targetSpan, t('deploymentComplete')); + } } function showError(message) { - errorMessage.textContent = message; - setState(STATE.ERROR); - const targetSpan = document.getElementById('error-title'); - if(targetSpan) { - typewriter(targetSpan, t('deploymentFailed'), 30); - } + errorMessage.textContent = message; + setState(STATE.ERROR); + const targetSpan = document.getElementById('error-title'); + if (targetSpan) { + typewriter(targetSpan, t('deploymentFailed'), 30); + } } function resetToForm() { - deploymentId = null; - deploymentUrl = null; - stackNameInput.value = ''; - previewName.textContent = t('yournamePlaceholder'); - validationMessage.textContent = ''; - validationMessage.className = 'validation-message'; - stackNameInput.classList.remove('error', 'success'); - progressLog.innerHTML = ''; - progressBar.style.width = '0%'; - progressPercent.textContent = '0%'; + deploymentId = null; + deploymentUrl = null; + stackNameInput.value = ''; + previewName.textContent = t('yournamePlaceholder'); + validationMessage.textContent = ''; + validationMessage.className = 'validation-message'; + stackNameInput.classList.remove('error', 'success'); + progressLog.innerHTML = ''; + progressBar.style.width = '0%'; + progressPercent.textContent = '0%'; - deployBtn.disabled = false; - deployBtn.innerHTML = ` + deployBtn.disabled = false; + deployBtn.innerHTML = ` ${t('deployBtn')} `; - setState(STATE.FORM); - const targetSpan = document.getElementById('typewriter-target'); - if(targetSpan) { - typewriter(targetSpan, t('chooseStackName')); - } + setState(STATE.FORM); + const targetSpan = document.getElementById('typewriter-target'); + if (targetSpan) { + typewriter(targetSpan, t('chooseStackName')); + } } // Event Listeners @@ -515,11 +515,11 @@ deployAnotherBtn.addEventListener('click', resetToForm); tryAgainBtn.addEventListener('click', resetToForm); document.addEventListener('DOMContentLoaded', () => { - initLanguage(); - setState(STATE.FORM); - const targetSpan = document.getElementById('typewriter-target'); - if(targetSpan) { - typewriter(targetSpan, t('chooseStackName')); - } - console.log('AI Stack Deployer initialized'); + initLanguage(); + setState(STATE.FORM); + const targetSpan = document.getElementById('typewriter-target'); + if (targetSpan) { + typewriter(targetSpan, t('chooseStackName')); + } + console.log('AI Stack Deployer initialized'); });