988 lines
39 KiB
JavaScript
988 lines
39 KiB
JavaScript
// 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 = `
|
||
<ul>
|
||
<li>
|
||
<p class="field-name">species name</p>
|
||
<p class="field-value" id="result-species"></p>
|
||
</li>
|
||
<li>
|
||
<p class="field-name">taxonomy</p>
|
||
<p class="field-value" id="result-taxonomy"></p>
|
||
</li>
|
||
<li>
|
||
<p class="field-name">pH</p>
|
||
<p class="field-value" id="result-ph"></p>
|
||
</li>
|
||
<li>
|
||
<p class="field-name">temperature</p>
|
||
<p class="field-value" id="result-temperature"></p>
|
||
</li>
|
||
<li>
|
||
<p class="field-name">respiratory type</p>
|
||
<p class="field-value" id="result-respiratory"></p>
|
||
</li>
|
||
<li>
|
||
<p class="field-name">culture medium</p>
|
||
<p class="field-value" id="result-medium"></p>
|
||
</li>
|
||
<li>
|
||
<p class="field-name">Max growth rate</p>
|
||
<p class="field-value" id="result-growth"></p>
|
||
</li>
|
||
</ul>
|
||
`;
|
||
}
|
||
|
||
// 隐藏下载按钮
|
||
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);
|
||
});
|
||
}); |