// ============================================================ // 状态管理 // ============================================================ const state = { file: null, taskId: null, status: 'idle', pollInterval: null, currentStatusName: '', repoURL: '', dotCount: 0, dotInterval: null, progressSimInterval: null, }; // ============================================================ // DOM 引用 // ============================================================ const uploadZone = document.getElementById('uploadZone'); const fileInput = document.getElementById('fileInput'); const fileInfo = document.getElementById('fileInfo'); const fileName = document.getElementById('fileName'); const fileSize = document.getElementById('fileSize'); const removeFile = document.getElementById('removeFile'); const sdkName = document.getElementById('sdkName'); const llmModel = document.getElementById('llmModel'); const apiKey = document.getElementById('apiKey'); const baseUrl = document.getElementById('baseUrl'); const generateBtn = document.getElementById('generateBtn'); const btnText = document.querySelector('.btn-text'); const btnSpinner = document.querySelector('.btn-spinner'); const progressSection = document.getElementById('progressSection'); const progressFill = document.getElementById('progressFill'); const progressStatus = document.getElementById('progressStatus'); const progressLogs = document.getElementById('progressLogs'); const resultSection = document.getElementById('resultSection'); const resultFiles = document.getElementById('resultFiles'); const resultError = document.getElementById('resultError'); const downloadBtn = document.getElementById('downloadBtn'); const viewRepoBtn = document.getElementById('viewRepoBtn'); // ============================================================ // 动态效果函数 // ============================================================ // 1. 进度条闪烁动画 function addProgressGlow() { const existingStyle = document.getElementById('progressGlowStyle'); if (existingStyle) return; const style = document.createElement('style'); style.id = 'progressGlowStyle'; style.textContent = ` .progress-fill.glow { background: linear-gradient(90deg, #6366f1, #8b5cf6, #6366f1); background-size: 200% 100%; animation: glowMove 1.5s ease-in-out infinite; position: relative; } .progress-fill.glow::after { content: ''; position: absolute; right: -4px; top: -4px; width: 14px; height: 14px; background: #8b5cf6; border-radius: 50%; box-shadow: 0 0 20px 8px rgba(139, 92, 246, 0.6); animation: pulse 1s ease-in-out infinite; } @keyframes glowMove { 0% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } 100% { background-position: 0% 50%; } } @keyframes pulse { 0%, 100% { transform: scale(1); opacity: 1; } 50% { transform: scale(1.3); opacity: 0.6; } } `; document.head.appendChild(style); } // 2. 状态文字动态点点点 function startDotAnimation() { if (state.dotInterval) { clearInterval(state.dotInterval); } let dotCount = 0; state.dotInterval = setInterval(() => { dotCount = (dotCount + 1) % 4; const dots = '.'.repeat(dotCount); if (state.status === 'generating' || state.status === 'pending') { const currentText = progressStatus.textContent.replace(/\.\.\.$/, '').trim(); if (!currentText.includes('✅') && !currentText.includes('❌')) { progressStatus.textContent = currentText + dots; } } }, 500); } function stopDotAnimation() { if (state.dotInterval) { clearInterval(state.dotInterval); state.dotInterval = null; } } // 3. 进度条模拟前进 function startProgressSimulation() { if (state.progressSimInterval) { clearInterval(state.progressSimInterval); } let lastPercent = parseInt(progressFill.style.width) || 0; state.progressSimInterval = setInterval(() => { if (state.status === 'generating' && lastPercent < 95) { const increment = 0.5 + Math.random() * 1.5; lastPercent = Math.min(lastPercent + increment, 95); progressFill.style.width = lastPercent + '%'; } }, 2000); } function stopProgressSimulation() { if (state.progressSimInterval) { clearInterval(state.progressSimInterval); state.progressSimInterval = null; } } // ============================================================ // 日志管理(只让最新的一条闪烁) // ============================================================ let logCounter = 0; function addLog(type, message) { const line = document.createElement('div'); line.className = `log-line ${type}`; line.textContent = message; progressLogs.appendChild(line); progressLogs.scrollTop = progressLogs.scrollHeight; // 移除之前所有日志的闪烁效果 document.querySelectorAll('.log-line .log-dot-blink').forEach(el => { el.classList.remove('log-dot-blink'); }); // 为最新日志添加闪烁点(在行首添加) const dotSpan = document.createElement('span'); dotSpan.className = 'log-dot-blink'; dotSpan.textContent = '● '; dotSpan.style.cssText = ` color: #60a5fa; animation: logBlink 1s ease-in-out infinite; `; line.prepend(dotSpan); // 确保动画样式存在 const styleId = 'logBlinkStyle'; if (!document.getElementById(styleId)) { const style = document.createElement('style'); style.id = styleId; style.textContent = ` @keyframes logBlink { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.2; transform: scale(0.8); } } `; document.head.appendChild(style); } } function clearLogs() { progressLogs.innerHTML = ''; logCounter = 0; } // ============================================================ // 上传区域事件 // ============================================================ uploadZone.addEventListener('click', () => fileInput.click()); uploadZone.addEventListener('dragover', (e) => { e.preventDefault(); uploadZone.classList.add('dragover'); }); uploadZone.addEventListener('dragleave', () => { uploadZone.classList.remove('dragover'); }); uploadZone.addEventListener('drop', (e) => { e.preventDefault(); uploadZone.classList.remove('dragover'); if (e.dataTransfer.files.length > 0) { handleFile(e.dataTransfer.files[0]); } }); fileInput.addEventListener('change', (e) => { if (e.target.files.length > 0) { handleFile(e.target.files[0]); } }); removeFile.addEventListener('click', () => { state.file = null; fileInfo.style.display = 'none'; uploadZone.style.display = 'block'; generateBtn.disabled = true; fileInput.value = ''; }); // ============================================================ // 文件处理 // ============================================================ function handleFile(file) { const validTypes = ['.md', '.txt', '.doc', '.docx', '.pdf']; const ext = '.' + file.name.split('.').pop().toLowerCase(); if (!validTypes.includes(ext)) { alert('不支持的文件类型,请上传 .md .txt .doc .docx .pdf'); return; } if (file.size > 50 * 1024 * 1024) { alert('文件大小超过 50MB 限制'); return; } state.file = file; fileName.textContent = file.name; fileSize.textContent = formatFileSize(file.size); fileInfo.style.display = 'flex'; uploadZone.style.display = 'none'; generateBtn.disabled = false; } function formatFileSize(bytes) { if (bytes < 1024) return bytes + ' B'; if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'; return (bytes / (1024 * 1024)).toFixed(1) + ' MB'; } // ============================================================ // 生成按钮 // ============================================================ generateBtn.addEventListener('click', generateSDK); async function generateSDK() { if (!state.file) return; const sdkNameVal = sdkName.value.trim(); const modelVal = llmModel.value.trim(); const apiKeyVal = apiKey.value.trim(); const baseUrlVal = baseUrl.value.trim(); if (!sdkNameVal) { alert('请输入 SDK 名称'); sdkName.focus(); return; } if (!modelVal) { alert('请输入大模型名称'); llmModel.focus(); return; } if (!apiKeyVal) { alert('请输入 API Key'); apiKey.focus(); return; } if (!baseUrlVal) { alert('请输入 Base URL'); baseUrl.focus(); return; } // 重置状态 state.status = 'generating'; state.taskId = null; state.currentStatusName = ''; state.repoURL = ''; resultSection.style.display = 'none'; resultError.style.display = 'none'; resultFiles.innerHTML = ''; progressSection.style.display = 'block'; progressFill.style.width = '0%'; progressStatus.textContent = '准备中...'; clearLogs(); generateBtn.disabled = true; btnText.textContent = '生成中'; btnSpinner.style.display = 'inline-block'; viewRepoBtn.style.display = 'none'; // 启动动态效果 addProgressGlow(); progressFill.classList.add('glow'); startDotAnimation(); startProgressSimulation(); const formData = new FormData(); formData.append('file', state.file); formData.append('sdk_name', sdkNameVal); formData.append('llm_model', modelVal); formData.append('llm_api_key', apiKeyVal); formData.append('llm_base_url', baseUrlVal); try { const response = await fetch('/api/v1/generate', { method: 'POST', body: formData, }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || '生成失败'); } state.taskId = data.task_id; addLog('info', '📤 任务已提交: ' + data.task_id); pollTaskStatus(); } catch (err) { showError(err.message); } } // ============================================================ // 轮询任务状态 // ============================================================ function pollTaskStatus() { if (state.pollInterval) { clearInterval(state.pollInterval); } state.pollInterval = setInterval(async () => { try { const response = await fetch(`/api/v1/tasks/${state.taskId}`); const data = await response.json(); if (!response.ok) { throw new Error(data.error || '查询状态失败'); } updateProgress(data); if (data.status === 'generateComplete' || data.status === 'completed') { clearInterval(state.pollInterval); state.pollInterval = null; onComplete(data); } else if (data.status === 'failed') { clearInterval(state.pollInterval); state.pollInterval = null; showError(data.error || '生成失败'); } } catch (err) { clearInterval(state.pollInterval); state.pollInterval = null; showError(err.message); } }, 2000); } function updateProgress(data) { const percent = data.percent || 0; progressFill.style.width = percent + '%'; const statusName = data.status_name || ''; if (statusName && statusName !== state.currentStatusName) { state.currentStatusName = statusName; addLog('info', '🔄 ' + statusName); } // ✅ 更新状态文字(带闪烁前缀) const statusTextMap = { 'pending': '⏳ 等待中', 'generateCode': '🔄 生成代码', 'validateCode': '🔍 验证代码', 'generateComplete': '✅ 完成!', 'completed': '✅ 完成!', 'failed': '❌ 失败', }; const baseText = statusTextMap[data.status] || (statusName || data.status); // ✅ 只有进行中的状态才让文字闪烁 const isProcessing = data.status !== 'generateComplete' && data.status !== 'completed' && data.status !== 'failed'; if (isProcessing) { progressStatus.innerHTML = ` ${baseText}`; } else { progressStatus.textContent = baseText; stopDotAnimation(); } // ✅ 确保闪烁样式存在 const styleId = 'statusBlinkStyle'; if (!document.getElementById(styleId) && isProcessing) { const style = document.createElement('style'); style.id = styleId; style.textContent = ` .status-blink { color: #60a5fa; display: inline-block; animation: statusBlink 1s ease-in-out infinite; margin-right: 6px; } @keyframes statusBlink { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.2; transform: scale(0.8); } } `; document.head.appendChild(style); } } // ============================================================ // 完成处理 // ============================================================ function onComplete(data) { stopDotAnimation(); stopProgressSimulation(); progressFill.classList.remove('glow'); generateBtn.disabled = false; btnText.textContent = '生成 SDK'; btnSpinner.style.display = 'none'; resultSection.style.display = 'block'; resultFiles.innerHTML = ''; const viewBtn = document.getElementById('viewBtn'); if (viewBtn) { viewBtn.style.display = 'none'; } downloadBtn.onclick = () => { window.location.href = `/api/v1/tasks/${state.taskId}/download`; }; const repoURL = data.RepoURL || data.repo_url || ''; if (repoURL) { viewRepoBtn.style.display = 'inline-block'; viewRepoBtn.onclick = () => { window.open(repoURL, '_blank'); }; } else { viewRepoBtn.style.display = 'none'; } // ✅ 完成状态不带闪烁 progressStatus.textContent = '✅ 生成完成!'; progressFill.style.width = '100%'; addLog('success', '🎉 SDK 生成完成!'); } // ============================================================ // 错误处理 // ============================================================ function showError(message) { stopDotAnimation(); stopProgressSimulation(); progressFill.classList.remove('glow'); state.status = 'error'; generateBtn.disabled = false; btnText.textContent = '生成 SDK'; btnSpinner.style.display = 'none'; resultSection.style.display = 'block'; resultError.style.display = 'block'; resultError.textContent = '❌ ' + message; viewRepoBtn.style.display = 'none'; // ✅ 错误状态不带闪烁 progressStatus.textContent = '❌ 失败'; progressFill.style.width = '100%'; progressFill.style.background = '#f87171'; if (state.pollInterval) { clearInterval(state.pollInterval); state.pollInterval = null; } }