// Конфигурация API const API_BASE_URL = '/api'; // Состояние приложения let deviceId = null; // Инициализация приложения document.addEventListener('DOMContentLoaded', function() { initializeApp(); }); // Инициализация function initializeApp() { deviceId = getOrCreateDeviceId(); bindEvents(); bindFieldErrorHandlers(); } // Генерация или получение уникального ID устройства function getOrCreateDeviceId() { let id = localStorage.getItem('deviceId'); if (!id) { id = 'device_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); localStorage.setItem('deviceId', id); } return id; } // Привязка событий function bindEvents() { const form = document.getElementById('scenario-form'); const newGenerationBtn = document.getElementById('new-generation-btn'); const retryBtn = document.getElementById('retry-btn'); if (form) { form.addEventListener('submit', handleFormSubmit); } if (newGenerationBtn) { newGenerationBtn.addEventListener('click', showFormScreen); } if (retryBtn) { retryBtn.addEventListener('click', showFormScreen); } } // Обработка отправки формы async function handleFormSubmit(event) { event.preventDefault(); // Очистить предыдущие ошибки clearFieldErrors(); const scenarioTopic = document.getElementById('scenario-topic').value.trim(); const niche = document.getElementById('niche').value.trim(); const scenarioGoal = document.getElementById('scenario-goal').value.trim(); let hasErrors = false; // Валидация полей if (!scenarioTopic) { showFieldError('scenario-topic', 'Пожалуйста, заполните тему сценария'); hasErrors = true; } else if (scenarioTopic.length < 3) { showFieldError('scenario-topic', 'Тема сценария должна содержать минимум 3 символа'); hasErrors = true; } if (!niche) { showFieldError('niche', 'Пожалуйста, заполните нишу'); hasErrors = true; } else if (niche.length < 2) { showFieldError('niche', 'Ниша должна содержать минимум 2 символа'); hasErrors = true; } if (!scenarioGoal) { showFieldError('scenario-goal', 'Пожалуйста, заполните цель сценария'); hasErrors = true; } else if (scenarioGoal.length < 5) { showFieldError('scenario-goal', 'Цель сценария должна содержать минимум 5 символов'); hasErrors = true; } // Если есть ошибки валидации, не отправляем форму if (hasErrors) { return; } // Показать экран загрузки showLoadingScreen(); // Попытка отправки с retry await attemptRequestWithRetry(scenarioTopic, niche, scenarioGoal, 3); } // Функция для повторных попыток запроса async function attemptRequestWithRetry(scenarioTopic, niche, scenarioGoal, maxRetries) { let lastError = null; for (let attempt = 1; attempt <= maxRetries; attempt++) { try { console.log(`Попытка ${attempt} из ${maxRetries}`); // Обновляем индикатор загрузки updateLoadingMessage('Пожалуйста, подождите...'); // Отправить запрос к API const response = await fetch(`${API_BASE_URL}/scenario/generate`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify({ scenario_topic: scenarioTopic, niche: niche, scenario_goal: scenarioGoal, device_id: deviceId }) }); const data = await response.json(); if (data.success) { // Показать результат showResult(data.data.scenario); return; // Успех, выходим из функции } else { // Ошибка от сервера - не retry showError(data.error || 'Произошла неизвестная ошибка'); return; } } catch (error) { console.error(`Попытка ${attempt} неудачна:`, error); lastError = error; // Если это не последняя попытка, ждем перед следующей if (attempt < maxRetries) { const delay = Math.pow(2, attempt) * 1000; // Экспоненциальная задержка: 2s, 4s, 8s console.log(`Ожидание ${delay}ms перед следующей попыткой...`); updateLoadingMessage('Улучшаем сценарий, пожалуйста, подождите...'); await new Promise(resolve => setTimeout(resolve, delay)); } } } // Все попытки исчерпаны console.error('Все попытки исчерпаны:', lastError); // Более детальная обработка ошибок let errorMessage = 'Произошла ошибка при генерации сценария'; if (lastError.name === 'TypeError' && lastError.message.includes('fetch')) { errorMessage = 'Ошибка подключения к серверу. Проверьте подключение к интернету.'; } else if (lastError.name === 'AbortError') { errorMessage = 'Запрос был отменен из-за превышения времени ожидания.'; } else if (lastError.message) { errorMessage = `Ошибка: ${lastError.message}`; } showError(errorMessage); } // Функция для обновления сообщения загрузки function updateLoadingMessage(message) { const loadingText = document.querySelector('#loading-screen .loading-text'); if (loadingText) { loadingText.textContent = message; } } // Показать экран формы function showFormScreen() { hideAllScreens(); document.getElementById('form-container').style.display = 'block'; // Очистить форму и ошибки document.getElementById('scenario-form').reset(); clearFieldErrors(); } // Показать экран загрузки function showLoadingScreen() { hideAllScreens(); document.getElementById('loading-screen').style.display = 'flex'; } // Показать результат function showResult(scenario) { hideAllScreens(); const resultContainer = document.getElementById('result-container'); const scenarioOutput = document.getElementById('scenario-output'); // Форматирование сценария scenarioOutput.innerHTML = formatScenario(scenario); resultContainer.style.display = 'block'; } // Показать ошибку function showError(message) { hideAllScreens(); const errorScreen = document.getElementById('error-screen'); const errorMessage = document.getElementById('error-message'); errorMessage.textContent = message; errorScreen.style.display = 'flex'; } // Скрыть все экраны function hideAllScreens() { document.getElementById('form-container').style.display = 'none'; document.getElementById('loading-screen').style.display = 'none'; document.getElementById('result-container').style.display = 'none'; document.getElementById('error-screen').style.display = 'none'; } // Форматирование сценария для красивого отображения function formatScenario(scenario) { // Если пришла строка (старый формат), используем старую логику if (typeof scenario === 'string') { let formatted = scenario .replace(/\n\n/g, '

') .replace(/\n/g, '
') .replace(/\*\*(.*?)\*\*/g, '$1') .replace(/\*(.*?)\*/g, '$1'); if (!formatted.startsWith('

')) { formatted = '

' + formatted + '

'; } return formatted; } // Если пришел объект (новый формат), форматируем структурированно if (typeof scenario === 'object' && scenario !== null) { return formatStructuredScenario(scenario); } // Fallback для неожиданных форматов return '

Ошибка форматирования сценария

'; } // Форматирование структурированного сценария function formatStructuredScenario(data) { let html = ''; // Мета-информация if (data.meta) { html += `

${escapeHtml(data.meta.title || 'Сценарий')}

`; if (data.meta.targetAudience) { html += `
Целевая аудитория:${escapeHtml(data.meta.targetAudience)}
`; } if (data.meta.goal) { html += `
Цель:${escapeHtml(data.meta.goal)}
`; } if (data.meta.durationSeconds) { html += `
Длительность:${data.meta.durationSeconds} сек
`; } html += '
'; } // Сцены if (data.scenes && Array.isArray(data.scenes)) { html += '
'; html += '

Сцены

'; data.scenes.forEach((scene, index) => { html += `
Сцена ${index + 1}${escapeHtml(scene.timing || '')}${escapeHtml(translateSceneType(scene.type || ''))}
`; if (scene.visual) { html += `
Визуал:${escapeHtml(scene.visual)}
`; } if (scene.text) { html += `
Текст:${escapeHtml(scene.text)}
`; } html += '
'; }); html += '
'; } // Заметки по продакшену if (data.productionNotes) { html += '
'; html += '

Заметки по продакшену

'; if (data.productionNotes.music) { html += `
Музыка:${escapeHtml(data.productionNotes.music)}
`; } if (data.productionNotes.sfx) { html += `
Звуковые эффекты:${escapeHtml(data.productionNotes.sfx)}
`; } if (data.productionNotes.editingStyle) { html += `
Стиль монтажа:${escapeHtml(data.productionNotes.editingStyle)}
`; } html += '
'; } // Анализ виральности if (data.viralityAnalysis) { html += '
'; html += '

Анализ виральности

'; if (data.viralityAnalysis.hookEffectiveness) { html += `
Эффективность хука:${escapeHtml(data.viralityAnalysis.hookEffectiveness)}
`; } if (data.viralityAnalysis.retentionStrategy) { html += `
Стратегия удержания:${escapeHtml(data.viralityAnalysis.retentionStrategy)}
`; } if (data.viralityAnalysis.valueAndCTA) { html += `
Ценность и призыв к действию:${escapeHtml(data.viralityAnalysis.valueAndCTA)}
`; } html += '
'; } return html; } // Функция для экранирования HTML function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // Функция для перевода типов сцен на русский function translateSceneType(type) { const translations = { 'Hook': 'Хук', 'Body': 'Основная часть', 'CTA': 'Призыв к действию', 'Introduction': 'Введение', 'Conclusion': 'Заключение', 'Transition': 'Переход', 'Call to Action': 'Призыв к действию', 'Opening': 'Открытие', 'Closing': 'Закрытие' }; return translations[type] || type; } // Показать ошибку валидации для конкретного поля function showFieldError(fieldId, message) { const field = document.getElementById(fieldId + '-field'); const errorElement = document.getElementById(fieldId + '-error'); if (field && errorElement) { field.classList.add('form-field--error'); errorElement.textContent = message; errorElement.classList.add('form-error--visible'); } } // Очистить ошибку для конкретного поля function clearFieldError(fieldId) { const field = document.getElementById(fieldId + '-field'); const errorElement = document.getElementById(fieldId + '-error'); if (field && errorElement) { field.classList.remove('form-field--error'); errorElement.textContent = ''; errorElement.classList.remove('form-error--visible'); } } // Очистить все ошибки полей function clearFieldErrors() { const fieldIds = ['scenario-topic', 'niche', 'scenario-goal']; fieldIds.forEach(fieldId => { clearFieldError(fieldId); }); } // Добавить обработчики для очистки ошибок при вводе текста function bindFieldErrorHandlers() { const fieldIds = ['scenario-topic', 'niche', 'scenario-goal']; fieldIds.forEach(fieldId => { const input = document.getElementById(fieldId); if (input) { input.addEventListener('input', function() { // Очистить ошибку при начале ввода clearFieldError(fieldId); }); } }); } // Утилита для отладки function debugLog(message, data = null) { if (console && console.log) { console.log(`[Scenario App] ${message}`, data || ''); } }