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: 'اسمك' } }; let currentLang = 'en'; function detectLanguage() { 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; } 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')); } } 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')); }); }); } // 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); 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(); } // State Machine for Deployment const STATE = { FORM: 'form', PROGRESS: 'progress', SUCCESS: 'success', ERROR: 'error' }; let currentState = STATE.FORM; let deploymentId = null; let deploymentUrl = null; let eventSource = null; // DOM Elements const formState = document.getElementById('form-state'); const progressState = document.getElementById('progress-state'); const successState = document.getElementById('success-state'); const errorState = document.getElementById('error-state'); const deployForm = document.getElementById('deploy-form'); const stackNameInput = document.getElementById('stack-name'); const deployBtn = document.getElementById('deploy-btn'); const validationMessage = document.getElementById('validation-message'); const previewName = document.getElementById('preview-name'); const progressBar = document.getElementById('progress-fill'); const progressPercent = document.getElementById('progress-percent'); const deployingName = document.getElementById('deploying-name'); const deployingUrl = document.getElementById('deploying-url'); const progressLog = document.getElementById('progress-log'); const successName = document.getElementById('success-name'); const successUrl = document.getElementById('success-url'); const openStackBtn = document.getElementById('open-stack-btn'); const deployAnotherBtn = document.getElementById('deploy-another-btn'); const errorMessage = document.getElementById('error-message'); const tryAgainBtn = document.getElementById('try-again-btn'); // State Management function setState(newState) { currentState = newState; const states = [formState, progressState, successState, errorState]; states.forEach(state => { state.style.display = 'none'; state.classList.remove('fade-in'); }); 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') }; } const trimmedName = name.trim().toLowerCase(); 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 (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') }; } 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; previewName.textContent = value || t('yournamePlaceholder'); // Clear previous timeout clearTimeout(checkTimeout); // Validate format first const validation = validateName(value); 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 = 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); }); // Form Submission deployForm.addEventListener('submit', async (e) => { e.preventDefault(); 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'); } 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 = ` ${t('deployBtn')} `; } }); // SSE Progress Streaming function startProgressStream(deploymentId) { eventSource = new EventSource(`/api/status/${deploymentId}`); 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('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')); }; } // Update Progress UI function updateProgress(data) { // Update progress bar progressBar.style.width = `${data.progress}%`; progressPercent.textContent = `${data.progress}%`; // 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; } function showSuccess(data) { 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')); } } function showError(message) { 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%'; deployBtn.disabled = false; deployBtn.innerHTML = ` ${t('deployBtn')} `; setState(STATE.FORM); const targetSpan = document.getElementById('typewriter-target'); if(targetSpan) { typewriter(targetSpan, t('chooseStackName')); } } // Event Listeners 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'); });