// Backend API Base URL (使用相对路径,避免跨域问题) const API_BASE_URL = '/api'; // Tab switching functionality function switchTab(tabName) { // Remove active class from all tabs document.querySelectorAll('.tab-item').forEach(tab => { tab.classList.remove('active'); }); document.querySelectorAll('.tab-panel').forEach(panel => { panel.classList.remove('active'); }); // Add active class to current tab document.querySelector(`[data-tab="${tabName}"]`).classList.add('active'); document.querySelector(`#${tabName}-panel`).classList.add('active'); } // File status update functionality function updateFileStatus(input, statusElementId = 'file-status') { const fileStatus = statusElementId ? document.getElementById(statusElementId) : document.querySelector('.file-status'); if (input.files && input.files.length > 0) { const fileCount = input.files.length; if (fileCount === 1) { fileStatus.textContent = input.files[0].name; } else { fileStatus.textContent = `${fileCount} files selected`; } fileStatus.style.color = '#4A90A4'; // Validate email requirement const predictionType = input.id.replace('-file-input', ''); validateEmailRequirement(predictionType); // Show email-progress-section and email container if at least 1 file const emailProgressSection = document.getElementById(`${predictionType}-email-progress`); const emailContainer = document.getElementById(`${predictionType}-email-container`); if (emailProgressSection) { emailProgressSection.style.display = 'block'; } if (emailContainer) { emailContainer.style.display = 'block'; } // Clear previous results when selecting new file if (currentAnalysisId) { clearResultContent(); currentAnalysisId = null; if (progressInterval) { clearInterval(progressInterval); progressInterval = null; } const progressTab = document.getElementById(`${predictionType}-progress-tab`); if (progressTab) { progressTab.style.display = 'none'; } // We don't hide email section here anymore as long as files are selected } } else { fileStatus.textContent = 'No file selected'; fileStatus.style.color = '#666'; // Hide email-progress section when no files const predictionType = input.id.replace('-file-input', ''); const emailProgressSection = document.getElementById(`${predictionType}-email-progress`); const emailContainer = document.getElementById(`${predictionType}-email-container`); if (emailProgressSection) { emailProgressSection.style.display = 'none'; } if (emailContainer) { emailContainer.style.display = 'none'; } } } // Validate email requirement function validateEmailRequirement(predictionType) { const fileInput = document.getElementById(`${predictionType}-file-input`); const submitBtn = document.getElementById(`${predictionType}-submit-btn`); const emailInput = document.getElementById(`${predictionType}-email-input`); const emailError = document.getElementById(`${predictionType}-email-error`); if (!fileInput || !fileInput.files || fileInput.files.length === 0) { return true; // No files, validation not needed } const fileCount = fileInput.files.length; const emailValue = emailInput ? emailInput.value.trim() : ''; const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (fileCount >= 2) { // More than 2 files, email is required if (!emailValue || !emailRegex.test(emailValue)) { if (emailError) { emailError.textContent = 'Valid email is required for 2+ files'; emailError.style.display = 'block'; } submitBtn.disabled = true; return false; } else { if (emailError) { emailError.style.display = 'none'; } submitBtn.disabled = false; return true; } } else { // 1 file, email is optional but must be valid if entered if (emailValue && !emailRegex.test(emailValue)) { if (emailError) { emailError.textContent = 'Please enter a valid email address'; emailError.style.display = 'block'; } submitBtn.disabled = true; return false; } else { if (emailError) { emailError.style.display = 'none'; } submitBtn.disabled = false; return true; } } } // Global variables let currentAnalysisId = null; let progressInterval = null; let startTime = null; let allAnalysisIds = []; // Track all analysis IDs for "analysis all" mode let allAnalysisResults = {}; // Store results from all analyses // let currentJobId = null; // Deprecated, unified to currentAnalysisId // Map frontend prediction types to backend analysis types function mapAnalysisType(predictionType) { const typeMap = { 'nutrition': 'nutrition', 'ph': 'ph', 'temperature': 'temperature', 'oxygen': 'oxygen', 'growth': 'growth' }; return typeMap[predictionType] || 'nutrition'; } // Handle submit functionality async function handleSubmit(predictionType) { const currentTab = document.querySelector('.tab-panel.active'); console.log('predictionType:', predictionType); const fileInput = currentTab.querySelector('input[type="file"]'); const emailInput = document.getElementById(`${predictionType}-email-input`); const emailProgressSection = document.getElementById(`${predictionType}-email-progress`); const submitBtn = document.getElementById(`${predictionType}-submit-btn`); const analysisAllCheckbox = document.getElementById(`${predictionType}-analysis-all`); // Check if file is selected if (!fileInput.files || fileInput.files.length === 0) { alert('Please select genome file(s) first!'); return; } const fileCount = fileInput.files.length; const emailValue = emailInput ? emailInput.value.trim() : ''; const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; const isAnalysisAll = analysisAllCheckbox && analysisAllCheckbox.checked; // Validate email logic again before submission if (fileCount >= 2) { if (!emailValue || !emailRegex.test(emailValue)) { alert('Please enter a valid email address. Email is required when uploading 2+ files.'); if (emailInput) emailInput.focus(); return; } } else if (fileCount === 1 && emailValue) { // Optional email validation if (!emailRegex.test(emailValue)) { alert('Please enter a valid email address.'); if (emailInput) emailInput.focus(); return; } } // Clean up old state if (progressInterval) { clearInterval(progressInterval); progressInterval = null; } // Clear result content clearResultContent(); allAnalysisIds = []; allAnalysisResults = {}; // Disable submit button if (submitBtn) { submitBtn.disabled = true; } // Ensure email progress section is visible if (emailProgressSection) { emailProgressSection.style.display = 'block'; } const emailContainer = document.getElementById(`${predictionType}-email-container`); const progressTab = document.getElementById(`${predictionType}-progress-tab`); const progressFill = document.getElementById(`${predictionType}-progress-fill`); const progressText = document.getElementById(`${predictionType}-progress-text`); const progressDetails = document.getElementById(`${predictionType}-progress-details`); // Show progress tab if (progressTab) { progressTab.style.display = 'block'; } // Show stop button const stopBtn = document.getElementById(`${predictionType}-stop-btn`); if (stopBtn) { stopBtn.style.display = 'block'; } // Initialize progress if (progressFill) { progressFill.style.width = '0%'; } if (progressText) { progressText.textContent = isAnalysisAll ? 'Starting all analyses...' : 'Uploading files...'; } if (progressDetails) { progressDetails.textContent = ''; } try { // If analysis all is selected, submit single request with analysis_type='all' if (isAnalysisAll) { await submitAnalysisAll(fileInput, emailValue, predictionType, progressText, progressFill, progressDetails, submitBtn, progressTab); } else { // Single analysis submission await submitSingleAnalysis(predictionType, fileInput, emailValue, progressText, progressFill, submitBtn, progressTab); } } catch (error) { console.error('Upload error:', error); alert(`Upload failed: ${error.message}`); // Reset UI if (submitBtn) { submitBtn.disabled = false; } if (progressTab) { progressTab.style.display = 'none'; } if (progressText) { progressText.textContent = 'Upload failed'; } } } // Submit single analysis async function submitSingleAnalysis(predictionType, fileInput, emailValue, progressText, progressFill, submitBtn, progressTab) { // Create FormData for file upload const formData = new FormData(); for (let i = 0; i < fileInput.files.length; i++) { formData.append('files', fileInput.files[i]); } // Add analysis type and email const backendAnalysisType = mapAnalysisType(predictionType); formData.append('analysis_type', backendAnalysisType); console.log('analysis_type:', backendAnalysisType); if (emailValue) { formData.append('email', emailValue); } // Upload files const uploadResponse = await fetch(`${API_BASE_URL}/upload`, { method: 'POST', body: formData }); if (!uploadResponse.ok) { const errorData = await uploadResponse.json(); throw new Error(errorData.error || 'Upload failed'); } const uploadResult = await uploadResponse.json(); currentAnalysisId = uploadResult.analysis_id; startTime = Date.now(); // Update progress if (progressText) { progressText.textContent = 'Analysis started...'; } if (progressFill) { progressFill.style.width = '10%'; } // Get fileName for single file analysis const fileName = fileInput.files.length === 1 ? fileInput.files[0].name : null; // Start polling for status startProgressPolling(predictionType, currentAnalysisId, fileName); } // Submit analysis all mode - single request with analysis_type='all' async function submitAnalysisAll(fileInput, emailValue, currentPredictionType, progressText, progressFill, progressDetails, submitBtn, progressTab) { // Create FormData for file upload const formData = new FormData(); for (let i = 0; i < fileInput.files.length; i++) { formData.append('files', fileInput.files[i]); } // Add analysis type as 'all' and email formData.append('analysis_type', 'all'); console.log('analysis_type: all (all analyses)'); if (emailValue) { formData.append('email', emailValue); } // Upload files and start all analyses const uploadResponse = await fetch(`${API_BASE_URL}/upload`, { method: 'POST', body: formData }); if (!uploadResponse.ok) { const errorData = await uploadResponse.json(); throw new Error(errorData.error || 'Upload failed'); } const uploadResult = await uploadResponse.json(); currentAnalysisId = uploadResult.analysis_id; startTime = Date.now(); // Update progress if (progressText) { progressText.textContent = 'All analyses started...'; } if (progressFill) { progressFill.style.width = '10%'; } if (progressDetails) { progressDetails.textContent = 'Processing all analysis types...'; } // Get fileName for single file analysis const fileName = fileInput.files.length === 1 ? fileInput.files[0].name : null; // Show stop button for current prediction type (not 'all') const stopBtn = document.getElementById(`${currentPredictionType}-stop-btn`); if (stopBtn) { stopBtn.style.display = 'block'; } // Start polling for status - use 'all' as prediction type for display startProgressPolling('all', currentAnalysisId, fileName); } // Start polling for analysis progress function startProgressPolling(predictionType, analysisId, fileName = null) { if (progressInterval) { clearInterval(progressInterval); } // Get current active tab's prediction type for UI elements // If predictionType is 'all', use the active tab's type for UI elements const currentTab = document.querySelector('.tab-panel.active'); const activeTabType = currentTab ? currentTab.id.replace('-panel', '') : predictionType; const uiPredictionType = predictionType === 'all' ? activeTabType : predictionType; // Get fileName from current file input if not provided if (!fileName) { const fileInput = currentTab ? currentTab.querySelector('input[type="file"]') : null; if (fileInput && fileInput.files && fileInput.files.length === 1) { fileName = fileInput.files[0].name; } } progressInterval = setInterval(async () => { try { const response = await fetch(`${API_BASE_URL}/status/${analysisId}`); if (!response.ok) { throw new Error('Failed to get status'); } const statusData = await response.json(); const progressFill = document.getElementById(`${uiPredictionType}-progress-fill`); const progressText = document.getElementById(`${uiPredictionType}-progress-text`); const progressDetails = document.getElementById(`${uiPredictionType}-progress-details`); const submitBtn = document.getElementById(`${uiPredictionType}-submit-btn`); const stopBtn = document.getElementById(`${uiPredictionType}-stop-btn`); // Update progress bar if (progressFill) { progressFill.style.width = `${statusData.progress || 0}%`; } // Ensure stop button is visible during analysis if (stopBtn && (statusData.status === 'queued' || statusData.status === 'analyzing' || statusData.status === 'created')) { stopBtn.style.display = 'block'; } // Update status text if (statusData.status === 'completed') { if (progressText) { progressText.textContent = 'Analysis completed!'; } if (progressInterval) { clearInterval(progressInterval); progressInterval = null; } if (submitBtn) { submitBtn.disabled = false; } if (stopBtn) { stopBtn.style.display = 'none'; } // Display results - pass fileName for single file analysis if (statusData.result) { displayResults(statusData.result, predictionType, fileName); } } else if (statusData.status === 'failed') { if (progressText) { progressText.textContent = `Analysis failed: ${statusData.error || 'Unknown error'}`; } if (progressInterval) { clearInterval(progressInterval); progressInterval = null; } if (submitBtn) { submitBtn.disabled = false; } if (stopBtn) { stopBtn.style.display = 'none'; } } else { // Update progress details const elapsed = Math.floor((Date.now() - startTime) / 1000); const statusMessages = { 'queued': predictionType === 'all' ? 'Waiting in queue for all analyses...' : 'Waiting in queue...', 'analyzing': predictionType === 'all' ? 'Running all analyses...' : 'Analyzing...', 'created': 'Preparing...' }; if (progressText) { let statusMsg = statusMessages[statusData.status] || `Processing... (${statusData.progress || 0}%)`; if (predictionType === 'all' && statusData.status === 'analyzing') { statusMsg = `Running all analyses... (${statusData.progress || 0}%)`; } progressText.textContent = statusMsg; } if (progressDetails) { const detailMsg = predictionType === 'all' ? `All analyses in progress - Elapsed: ${elapsed}s` : `Elapsed time: ${elapsed}s`; progressDetails.textContent = detailMsg; } } } catch (error) { console.error('Status polling error:', error); } }, 2000); // Poll every 2 seconds } // Extract genome ID from filename function extractGenomeId(filename) { // Remove file extension const nameWithoutExt = filename.replace(/\.(fna|fasta|fa)$/i, ''); // Return the filename without extension as genome ID return nameWithoutExt; } // Display analysis results function displayResults(result, predictionType, fileName = null) { const resultContent = document.getElementById('result-content'); const downloadBtn = document.getElementById('download-btn'); if (!resultContent) return; // Show download button if (downloadBtn) { downloadBtn.style.display = 'block'; downloadBtn.onclick = () => { window.location.href = `${API_BASE_URL}/download/${currentAnalysisId}`; }; } // Update result content based on analysis type const speciesEl = document.getElementById('result-species'); const taxonomyEl = document.getElementById('result-taxonomy'); const phEl = document.getElementById('result-ph'); const tempEl = document.getElementById('result-temperature'); const respEl = document.getElementById('result-respiratory'); const mediumEl = document.getElementById('result-medium'); const growthEl = document.getElementById('result-growth'); // Set species name from filename if single file if (fileName && speciesEl) { const genomeId = extractGenomeId(fileName); speciesEl.textContent = genomeId; } // Display results based on analysis type - try to parse CSV data if available if (result && Object.keys(result).length > 2) { // More than just analysis_id and result_file // Handle 'all' analysis type - display all results if (predictionType === 'all') { // Display all analysis results from merged CSV const allFields = Object.keys(result).filter(k => k !== 'analysis_id' && k !== 'result_file' && k !== 'analysis_types' ); // pH results const phFields = allFields.filter(k => k.toLowerCase().includes('ph') && !k.toLowerCase().includes('predict') ); if (phFields.length > 0 && phEl) { phEl.textContent = result[phFields[0]] || 'See CSV file for details'; } else if (phEl) { phEl.textContent = 'See CSV file for details'; } // Temperature results const tempFields = allFields.filter(k => (k.toLowerCase().includes('temp') || k.toLowerCase().includes('temperature')) && !k.toLowerCase().includes('predict') ); if (tempFields.length > 0 && tempEl) { tempEl.textContent = result[tempFields[0]] || 'See CSV file for details'; } else if (tempEl) { tempEl.textContent = 'See CSV file for details'; } // Oxygen/respiratory results const respFields = allFields.filter(k => (k.toLowerCase().includes('oxygen') || k.toLowerCase().includes('respiratory') || k.toLowerCase().includes('o2')) && !k.toLowerCase().includes('predict') ); if (respFields.length > 0 && respEl) { respEl.textContent = result[respFields[0]] || 'See CSV file for details'; } else if (respEl) { respEl.textContent = 'See CSV file for details'; } // Nutrition/medium results const mediumFields = allFields.filter(k => (k.toLowerCase().includes('medium') || k.toLowerCase().includes('nutrition') || k.toLowerCase().includes('culture') || k.toLowerCase().includes('compound')) ); if (mediumFields.length > 0 && mediumEl) { const mediumValue = mediumFields.map(f => result[f]).filter(v => v && v !== '').join(', '); mediumEl.textContent = mediumValue || 'See CSV file for details'; } else if (mediumEl) { mediumEl.textContent = 'See CSV file for details'; } // Growth results const growthFields = allFields.filter(k => k.toLowerCase().includes('growth') && !k.toLowerCase().includes('predict') ); if (growthFields.length > 0 && growthEl) { growthEl.textContent = result[growthFields[0]] || 'See CSV file for details'; } else if (growthEl) { growthEl.textContent = 'See CSV file for details'; } } else if (predictionType === 'nutrition') { // Look for medium-related fields const mediumFields = Object.keys(result).filter(k => k.toLowerCase().includes('medium') || k.toLowerCase().includes('nutrition') || k.toLowerCase().includes('culture') ); if (mediumFields.length > 0 && mediumEl) { const mediumValue = mediumFields.map(f => result[f]).filter(v => v && v !== '').join(', '); mediumEl.textContent = mediumValue || 'See CSV file for details'; } else if (mediumEl) { mediumEl.textContent = 'See CSV file for details'; } } else if (predictionType === 'ph') { // Look for pH-related fields const phFields = Object.keys(result).filter(k => k.toLowerCase().includes('ph') || k.toLowerCase().includes('ph_') ); if (phFields.length > 0 && phEl) { // Only display the value, not the field name const phValue = result[phFields[0]] || ''; phEl.textContent = phValue || 'See CSV file for details'; } else if (phEl) { phEl.textContent = 'See CSV file for details'; } } else if (predictionType === 'temperature') { // Look for temperature-related fields const tempFields = Object.keys(result).filter(k => k.toLowerCase().includes('temp') || k.toLowerCase().includes('temperature') ); if (tempFields.length > 0 && tempEl) { // Only display the value, not the field name const tempValue = result[tempFields[0]] || ''; tempEl.textContent = tempValue || 'See CSV file for details'; } else if (tempEl) { tempEl.textContent = 'See CSV file for details'; } } else if (predictionType === 'oxygen') { // Look for oxygen/respiratory-related fields const respFields = Object.keys(result).filter(k => k.toLowerCase().includes('oxygen') || k.toLowerCase().includes('respiratory') || k.toLowerCase().includes('o2') ); if (respFields.length > 0 && respEl) { // Only display the value, not the field name const respValue = result[respFields[0]] || ''; respEl.textContent = respValue || 'See CSV file for details'; } else if (respEl) { respEl.textContent = 'See CSV file for details'; } } else if (predictionType === 'growth') { // Look for growth-related fields const growthFields = Object.keys(result).filter(k => k.toLowerCase().includes('growth') || k.toLowerCase().includes('rate') ); if (growthFields.length > 0 && growthEl) { // Only display the value, not the field name const growthValue = result[growthFields[0]] || ''; growthEl.textContent = growthValue || 'See CSV file for details'; } else if (growthEl) { growthEl.textContent = 'See CSV file for details'; } } } else { // Fallback to CSV file message if (predictionType === 'nutrition' && result.result_file) { if (mediumEl) { mediumEl.textContent = 'See CSV file for details'; } } else if (predictionType === 'ph' && result.result_file) { if (phEl) { phEl.textContent = 'See CSV file for details'; } } else if (predictionType === 'temperature' && result.result_file) { if (tempEl) { tempEl.textContent = 'See CSV file for details'; } } else if (predictionType === 'oxygen' && result.result_file) { if (respEl) { respEl.textContent = 'See CSV file for details'; } } else if (predictionType === 'growth' && result.result_file) { if (growthEl) { growthEl.textContent = 'See CSV file for details'; } } } } // Display all analysis results (for analysis all mode with single file) function displayAllResults(fileName) { const speciesEl = document.getElementById('result-species'); const taxonomyEl = document.getElementById('result-taxonomy'); const phEl = document.getElementById('result-ph'); const tempEl = document.getElementById('result-temperature'); const respEl = document.getElementById('result-respiratory'); const mediumEl = document.getElementById('result-medium'); const growthEl = document.getElementById('result-growth'); // Set species name from filename if (fileName && speciesEl) { const genomeId = extractGenomeId(fileName); speciesEl.textContent = genomeId; } // Display results from all analyses if (allAnalysisResults.ph && phEl) { const phResult = allAnalysisResults.ph; const phFields = Object.keys(phResult).filter(k => k.toLowerCase().includes('ph') && k !== 'analysis_id' && k !== 'result_file' ); if (phFields.length > 0) { phEl.textContent = phResult[phFields[0]] || 'See CSV file for details'; } else { phEl.textContent = 'See CSV file for details'; } } if (allAnalysisResults.temperature && tempEl) { const tempResult = allAnalysisResults.temperature; const tempFields = Object.keys(tempResult).filter(k => k.toLowerCase().includes('temp') && k !== 'analysis_id' && k !== 'result_file' ); if (tempFields.length > 0) { tempEl.textContent = tempResult[tempFields[0]] || 'See CSV file for details'; } else { tempEl.textContent = 'See CSV file for details'; } } if (allAnalysisResults.oxygen && respEl) { const oxygenResult = allAnalysisResults.oxygen; const respFields = Object.keys(oxygenResult).filter(k => (k.toLowerCase().includes('oxygen') || k.toLowerCase().includes('respiratory') || k.toLowerCase().includes('o2')) && k !== 'analysis_id' && k !== 'result_file' ); if (respFields.length > 0) { respEl.textContent = oxygenResult[respFields[0]] || 'See CSV file for details'; } else { respEl.textContent = 'See CSV file for details'; } } if (allAnalysisResults.nutrition && mediumEl) { const nutritionResult = allAnalysisResults.nutrition; const mediumFields = Object.keys(nutritionResult).filter(k => (k.toLowerCase().includes('medium') || k.toLowerCase().includes('nutrition')) && k !== 'analysis_id' && k !== 'result_file' ); if (mediumFields.length > 0) { const mediumValue = mediumFields.map(f => nutritionResult[f]).filter(v => v && v !== '').join(', '); mediumEl.textContent = mediumValue || 'See CSV file for details'; } else { mediumEl.textContent = 'See CSV file for details'; } } if (allAnalysisResults.growth && growthEl) { const growthResult = allAnalysisResults.growth; const growthFields = Object.keys(growthResult).filter(k => k.toLowerCase().includes('growth') && k !== 'analysis_id' && k !== 'result_file' ); if (growthFields.length > 0) { growthEl.textContent = growthResult[growthFields[0]] || 'See CSV file for details'; } else { growthEl.textContent = 'See CSV file for details'; } } // Show download button for the first analysis const downloadBtn = document.getElementById('download-btn'); if (downloadBtn && allAnalysisIds.length > 0) { downloadBtn.style.display = 'block'; downloadBtn.onclick = () => { // Download the first analysis result (or could be modified to download all) window.location.href = `${API_BASE_URL}/download/${allAnalysisIds[0].id}`; }; } } // 补充原代码中可能缺失的函数定义(根据上下文推测) function handleReset() { // 重置表单逻辑 const activeTab = document.querySelector('.tab-item.active').dataset.tab; const fileInput = document.getElementById(`${activeTab}-file-input`); const fileStatus = document.getElementById(`${activeTab}-file-status`); const emailInput = document.getElementById(`${activeTab}-email-input`); const emailError = document.getElementById(`${activeTab}-email-error`); const emailProgressSection = document.getElementById(`${activeTab}-email-progress`); const analysisAllCheckbox = document.getElementById(`${activeTab}-analysis-all`); const submitBtn = document.getElementById(`${activeTab}-submit-btn`); // 重置文件输入 if (fileInput) { fileInput.value = ''; } if (fileStatus) { fileStatus.textContent = 'No file selected'; fileStatus.style.color = '#666'; } // 重置邮箱输入 if (emailInput) { emailInput.value = ''; } if (emailError) { emailError.style.display = 'none'; } // 重置analysis all复选框 if (analysisAllCheckbox) { analysisAllCheckbox.checked = false; } // 重置提交按钮 if (submitBtn) { submitBtn.disabled = false; } // 隐藏进度和邮箱区域 if (emailProgressSection) { emailProgressSection.style.display = 'none'; } // 清除进度间隔 if (progressInterval) { clearInterval(progressInterval); progressInterval = null; } // 清除全局变量 currentAnalysisId = null; allAnalysisIds = []; allAnalysisResults = {}; // 清除结果 clearResultContent(); } function clearResultContent() { const resultContent = document.getElementById('result-content'); if (resultContent) { // 可以保留原始结构但清空内容,或恢复默认内容 resultContent.innerHTML = ` `; } // 隐藏下载按钮 const downloadBtn = document.getElementById('download-btn'); if (downloadBtn) { downloadBtn.style.display = 'none'; } } function downloadResult() { // 下载结果逻辑 alert('Download functionality will be implemented here.'); } // 初始化标签切换事件 document.querySelectorAll('.tab-item').forEach(tab => { tab.addEventListener('click', () => { const tabName = tab.dataset.tab; switchTab(tabName); }); }); // 页面加载时检查 URL 参数并切换到对应的 tab document.addEventListener('DOMContentLoaded', function() { const urlParams = new URLSearchParams(window.location.search); const tabParam = urlParams.get('tab'); if (tabParam) { // 验证 tab 参数是否有效 const validTabs = ['nutrition', 'ph', 'temperature', 'oxygen', 'growth']; if (validTabs.includes(tabParam)) { // 延迟执行以确保 DOM 完全加载 setTimeout(() => { switchTab(tabParam); // 滚动到 tab 导航区域 const tabNav = document.querySelector('.tab-nav'); if (tabNav) { tabNav.scrollIntoView({ behavior: 'smooth', block: 'start' }); } }, 100); } } }); // 初始化停止按钮事件 document.querySelectorAll('.stop-btn').forEach(btn => { btn.addEventListener('click', async function() { const predictionType = this.id.replace('-stop-btn', ''); if (!currentAnalysisId) { alert('No active analysis to stop'); return; } // 确认停止操作 if (!confirm('Are you sure you want to stop the analysis? This action cannot be undone.')) { return; } // 禁用stop按钮,防止重复点击 const stopBtn = this; stopBtn.disabled = true; stopBtn.textContent = 'Stopping...'; try { const response = await fetch(`${API_BASE_URL}/stop/${currentAnalysisId}`, { method: 'POST' }); if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new Error(errorData.error || 'Failed to stop analysis'); } const result = await response.json(); console.log('Stop response:', result); // Stop polling if (progressInterval) { clearInterval(progressInterval); progressInterval = null; } // 获取当前活动的tab类型(处理'all'类型的情况) const currentTab = document.querySelector('.tab-panel.active'); const activeTabType = currentTab ? currentTab.id.replace('-panel', '') : predictionType; // Update UI const progressTab = document.getElementById(`${activeTabType}-progress-tab`); const progressText = document.getElementById(`${activeTabType}-progress-text`); const progressDetails = document.getElementById(`${activeTabType}-progress-details`); const progressFill = document.getElementById(`${activeTabType}-progress-fill`); const submitBtn = document.getElementById(`${activeTabType}-submit-btn`); if (progressText) { progressText.textContent = 'Analysis stopped by user'; } if (progressDetails) { progressDetails.textContent = 'The analysis has been terminated.'; } if (progressFill) { progressFill.style.width = '0%'; } if (submitBtn) { submitBtn.disabled = false; } if (stopBtn) { stopBtn.style.display = 'none'; } // 清除结果 clearResultContent(); // 重置全局变量 currentAnalysisId = null; allAnalysisIds = []; allAnalysisResults = {}; // 显示成功消息 setTimeout(() => { if (progressTab) { progressTab.style.display = 'none'; } }, 3000); } catch (error) { console.error('Stop error:', error); alert(`Failed to stop analysis: ${error.message}`); // 恢复按钮状态 stopBtn.disabled = false; stopBtn.textContent = 'Stop Analysis'; } }); }); // 初始化邮箱输入验证事件 document.querySelectorAll('.email-input').forEach(input => { input.addEventListener('input', function() { const predictionType = this.id.replace('-email-input', ''); validateEmailRequirement(predictionType); }); });