`;
newItem.appendChild(buttonDiv);
// Remove buttons from previous Item container
lastItem.querySelector('.item-buttons-row').remove();
// Add new Item to container
itemsContainer.appendChild(newItem);
// Add event listeners for new buttons
newItem.querySelector('.add-component-btn').addEventListener('click', addComponentToAllItems);
newItem.querySelector('.delete-component-btn').addEventListener('click', deleteComponentFromAllItems);
newItem.querySelector('.add-item-btn').addEventListener('click', addNewItem);
newItem.querySelector('.delete-item-btn').addEventListener('click', function () {
deleteItem(newItem.id);
});
}
// Delete Item
function deleteItem(itemId) {
const itemToDelete = document.getElementById(itemId);
const itemContainers = document.querySelectorAll('.item-container');
// If there's only one Item, don't perform delete operation
if (itemContainers.length <= 1) {
return;
}
// Get position of Item to be deleted
let itemIndex = -1;
for (let i = 0; i < itemContainers.length; i++) {
if (itemContainers[i].id === itemId) {
itemIndex = i;
break;
}
}
// If it's the last Item, need to add buttons to previous Item
if (itemIndex === itemContainers.length - 1) {
const previousItem = itemContainers[itemIndex - 1];
const buttonDiv = document.createElement('div');
buttonDiv.className = 'item-buttons-row';
// If it's Item 1, no Delete item button needed
if (previousItem.id === 'item-1') {
buttonDiv.innerHTML = `
`;
} else {
buttonDiv.innerHTML = `
`;
}
previousItem.appendChild(buttonDiv);
// Add event listeners for new buttons
previousItem.querySelector('.add-component-btn').addEventListener('click', addComponentToAllItems);
previousItem.querySelector('.delete-component-btn').addEventListener('click', deleteComponentFromAllItems);
previousItem.querySelector('.add-item-btn').addEventListener('click', addNewItem);
const deleteItemBtn = previousItem.querySelector('.delete-item-btn');
if (deleteItemBtn) {
deleteItemBtn.addEventListener('click', function () {
deleteItem(previousItem.id);
});
}
}
// Delete Item
itemToDelete.remove();
}
// Collect data from all Item tables
function collectItemsData() {
const stimuli = [];
const itemContainers = document.querySelectorAll('.item-container');
itemContainers.forEach(itemContainer => {
const tbody = itemContainer.querySelector('.stimuli-table tbody');
const rows = tbody.getElementsByTagName('tr');
const itemData = {};
for (let i = 0; i < rows.length; i++) {
const typeCell = rows[i].querySelector('.type-column input');
const contentCell = rows[i].querySelector('.content-column input');
if (typeCell && contentCell) {
const type = typeCell.value.trim();
const content = contentCell.value.trim();
if (type && content) {
itemData[type] = content;
}
}
}
// Only add to stimuli array when itemData is not empty
if (Object.keys(itemData).length > 0) {
stimuli.push(itemData);
}
});
return stimuli;
}
// This event listener was removed since we now have individual delete buttons for each row
// Show generating status text
function showGeneratingStatus() {
generationStatus.textContent = "Generating...";
generationStatus.className = "generation-status generating";
generationStatus.style.display = "inline-block";
}
// Show checking status text
function showCheckingStatus() {
generationStatus.textContent = "Scoring & Checking status & Creating file...";
generationStatus.className = "generation-status checking";
generationStatus.style.display = "inline-block";
}
// Hide status text
function hideGenerationStatus() {
generationStatus.style.display = "none";
generationStatus.textContent = "";
}
// Update progress bar
function updateProgress(progress) {
console.log(`Updating progress bar to ${progress}%`);
// Ensure progress value is a number
if (typeof progress !== 'number') {
try {
progress = parseFloat(progress);
} catch (e) {
console.error("Invalid progress value:", progress);
return;
}
}
// Progress value should be between 0-100
progress = Math.max(0, Math.min(100, progress));
// Round to integer
const roundedProgress = Math.round(progress);
// Get current progress
let currentProgressText = progressBar.style.width || '0%';
let currentProgress = parseInt(currentProgressText.replace('%', ''), 10) || 0;
// For special cases:
// 1. If current progress is already 100%, no updates are accepted (unless explicitly reset to 0%)
if (currentProgress >= 100 && roundedProgress > 0) {
console.log(`Progress already at 100%, ignoring update to ${roundedProgress}%`);
return;
}
// 2. If the received progress is less than the current progress and the difference is more than 10%, it may be a new generation process
// At this time, we accept the smaller progress value
const progressDifference = currentProgress - roundedProgress;
if (progressDifference > 10 && roundedProgress <= 20) {
console.log(`Accepting progress ${roundedProgress}% as start of new generation`);
currentProgress = 0; // Reset progress
}
// 3. Otherwise, if the received progress is less than the current progress, ignore it (unless it's 0% to indicate reset)
else if (roundedProgress < currentProgress && roundedProgress > 0) {
console.log(`Ignoring backward progress update: ${roundedProgress}% < ${currentProgress}%`);
return;
}
// Update progress display
progressBar.style.width = `${roundedProgress}%`;
document.getElementById('progress_percentage').textContent = `${roundedProgress}%`;
console.log(`Progress bar updated to ${roundedProgress}%`);
// When progress reaches 100%, switch status text
if (roundedProgress >= 100) {
showCheckingStatus();
}
// Enable/disable buttons
if (roundedProgress > 0 && roundedProgress < 100) {
// In progress
generateButton.disabled = true;
stopButton.disabled = false;
} else if (roundedProgress >= 100) {
// Completed
generateButton.disabled = false;
stopButton.disabled = true;
}
}
function resetUI() {
// Hide generating status text
hideGenerationStatus();
// Enable all buttons
generateButton.disabled = false;
clearButton.disabled = false;
stopButton.disabled = true; // Stop button remains disabled
// Enable all table-related buttons
addAgent2Button.disabled = false;
addAgent3Button.disabled = false;
// Enable Add/Delete buttons in all tables
document.querySelectorAll('.add-component-btn, .delete-component-btn, .add-item-btn, .delete-item-btn').forEach(btn => {
btn.disabled = false;
});
// Enable model selection dropdown
document.getElementById('model_choice').disabled = false;
// Determine whether API Key input box is available based on the currently selected model
const selectedModel = document.getElementById('model_choice').value;
if (selectedModel === 'GPT-4o') {
document.getElementById('api_key').disabled = false;
} else {
document.getElementById('api_key').disabled = true;
}
// Enable all other input boxes and text areas
document.getElementById('iteration').disabled = false;
document.getElementById('experiment_design').disabled = false;
// Enable all input boxes in all tables
document.querySelectorAll('input[type="text"], input[type="number"], textarea').forEach(input => {
if (input.id !== 'api_key') { // API Key input box is excluded
input.disabled = false;
}
});
// Reset progress bar
updateProgress(0);
}
function validateInputs() {
const selectedModel = modelChoice.value;
const apiKey = document.getElementById('api_key').value.trim();
const iteration = document.getElementById('iteration').value.trim();
const experimentDesign = document.getElementById('experiment_design').value.trim();
// Check if a model is selected
if (!selectedModel) {
alert('Please choose a model!');
return false;
}
// If GPT-4o is selected, check API Key
if (selectedModel === 'GPT-4o' && !apiKey) {
alert('OpenAI API Key cannot be empty when using GPT-4o!');
return false;
}
// If custom model is selected, check API Key and URL
if (selectedModel === 'custom') {
const apiUrl = document.getElementById('custom_api_url').value.trim(); // Fixed field ID
const modelName = document.getElementById('custom_model_name').value.trim(); // Add model name validation
if (!apiKey) {
alert('API Key cannot be empty when using custom model!');
return false;
}
if (!modelName) { // Add model name validation
alert('Model Name cannot be empty when using custom model!');
return false;
}
if (!apiUrl) {
alert('API URL cannot be empty when using custom model!');
return false;
}
// Validate custom parameters if provided
const customParams = document.getElementById('custom_params').value.trim();
if (customParams) {
try {
JSON.parse(customParams);
} catch (e) {
alert('Invalid JSON in custom parameters!');
return false;
}
}
}
// Check inputs in Item tables
const itemContainers = document.querySelectorAll('.item-container');
for (let i = 0; i < itemContainers.length; i++) {
const tbody = itemContainers[i].querySelector('.stimuli-table tbody');
const rows = tbody.getElementsByTagName('tr');
for (let j = 0; j < rows.length; j++) {
const typeCell = rows[j].querySelector('.type-column input');
const contentCell = rows[j].querySelector('.content-column input');
if (typeCell && contentCell) {
const typeValue = typeCell.value.trim();
const contentValue = contentCell.value.trim();
if (!typeValue || !contentValue) {
alert(`All Components and Content fields in the "Item ${i + 1}" table must be filled!`);
return false;
}
}
}
}
// Check Experiment Design
if (!experimentDesign) {
alert('Stimulus design cannot be empty!');
return false;
}
const agent2Rows = document.getElementById('agent2PropertiesTable').getElementsByTagName('tr');
for (let i = 1; i < agent2Rows.length; i++) { // Skip header
const cells = agent2Rows[i].getElementsByTagName('input');
if (cells.length === 2) {
const propertyValue = cells[0].value.trim();
const descriptionValue = cells[1].value.trim();
if (!propertyValue || !descriptionValue) {
alert('All fields in the "Validator" table must be filled!');
return false;
}
}
}
// Check agent3PropertiesTable
const agent3Rows = document.getElementById('agent3PropertiesTable').getElementsByTagName('tr');
for (let i = 1; i < agent3Rows.length; i++) { // Skip header
const cells = agent3Rows[i].getElementsByTagName('input');
if (cells.length === 4) {
const propertyValue = cells[0].value.trim();
const descriptionValue = cells[1].value.trim();
const minValue = cells[2].value.trim();
const maxValue = cells[3].value.trim();
if (!propertyValue || !descriptionValue || !minValue || !maxValue) {
alert('All fields in the "Scorer" table must be filled!');
return false;
}
// Ensure Minimum and Maximum are integers
if (!/^\d+$/.test(minValue) || !/^\d+$/.test(maxValue)) {
alert('Min and Max scores must be non-negative integers (e.g., 0, 1, 2, 3...)');
return false;
}
const minInt = parseInt(minValue, 10);
const maxInt = parseInt(maxValue, 10);
if (maxInt <= minInt) {
alert('Max score must be greater than Min score!');
return false;
}
}
}
// Check if Iteration is a positive integer
if (!/^\d+$/.test(iteration) || parseInt(iteration, 10) <= 0) {
alert("'The number of items' must be a positive integer!");
return false;
}
return true; // If all checks pass, return true
}
// Modify generateButton click handler
generateButton.addEventListener('click', () => {
if (!validateInputs()) {
return; // If frontend input checks fail, terminate "Generate Stimulus" related operations
}
// Ensure WebSocket connection
ensureSocketConnection(startGeneration);
});
// Extract generation process as a standalone function
function startGeneration() {
updateProgress(0);
// Show "Generating..." status text
showGeneratingStatus();
// Clear log display area
clearLogs();
// Ensure WebSocket connection status is normal before generation starts
checkSocketConnection();
// Disable all buttons, except Stop button
generateButton.disabled = true;
clearButton.disabled = true;
stopButton.disabled = false;
// Disable all table-related buttons
addAgent2Button.disabled = true;
addAgent3Button.disabled = true;
// Disable Add/Delete buttons in all tables
document.querySelectorAll('.add-component-btn, .delete-component-btn, .add-item-btn, .delete-item-btn').forEach(btn => {
btn.disabled = true;
});
// Disable model selection dropdown
document.getElementById('model_choice').disabled = true;
// Disable all input boxes and text areas
document.getElementById('api_key').disabled = true;
document.getElementById('iteration').disabled = true;
document.getElementById('experiment_design').disabled = true;
// Disable all input boxes in all tables
document.querySelectorAll('input[type="text"], input[type="number"], textarea').forEach(input => {
input.disabled = true;
});
// Collect table data
const stimuli = collectItemsData();
const previousStimuli = JSON.stringify(stimuli); // Convert to JSON string
// Modify the logic to get agent1Properties
// Get data from Components column of the first Item table
const agent1Properties = {};
// Find the first Item table
const firstItemTable = document.querySelector('#item-1 .stimuli-table');
if (firstItemTable) {
// Get all rows (skip header)
const componentRows = Array.from(firstItemTable.querySelectorAll('tbody tr'));
componentRows.forEach(row => {
// Get input value from Components column
const componentInput = row.querySelector('td.type-column input');
if (componentInput && componentInput.value.trim()) {
agent1Properties[componentInput.value.trim()] = { type: 'string' };
}
});
}
// Get agent2_properties
const agent2Rows = Array.from(agent2Table.rows).slice(1);
let agent2Properties = {};
agent2Rows.forEach(row => {
const propertyName = row.cells[0].querySelector("input").value.trim();
const propertyDesc = row.cells[1].querySelector("input").value.trim();
if (propertyName && propertyDesc) {
agent2Properties[propertyName] = {
"type": "boolean",
"description": propertyDesc
};
}
});
// Get agent3_properties
const agent3PropertiesTable = document.getElementById('agent3PropertiesTable');
const agent3Rows = Array.from(agent3PropertiesTable.rows).slice(1); // Skip header
let agent3Properties = {};
agent3Rows.forEach(row => {
let property = row.cells[0].querySelector('input').value.trim();
let description = row.cells[1].querySelector('input').value.trim();
let min = row.cells[2].querySelector('input').value.trim();
let max = row.cells[3].querySelector('input').value.trim();
if (property && description && min && max) {
agent3Properties[property] = {
"type": "integer",
"description": description,
"minimum": parseInt(min, 10),
"maximum": parseInt(max, 10)
};
}
});
const settings = {
agent1Properties: JSON.stringify(agent1Properties),
agent2Properties: JSON.stringify(agent2Properties),
agent3Properties: JSON.stringify(agent3Properties),
apiKey: document.getElementById('api_key').value,
modelChoice: document.getElementById('model_choice').value,
experimentDesign: document.getElementById('experiment_design').value,
previousStimuli: previousStimuli,
iteration: parseInt(document.getElementById('iteration').value),
agent2IndividualValidation: document.getElementById('agent2_individual_validation').checked,
agent3IndividualScoring: document.getElementById('agent3_individual_scoring').checked
};
// Add custom model parameters if custom model is selected
if (settings.modelChoice === 'custom') {
settings.apiUrl = document.getElementById('custom_api_url').value.trim();
settings.modelName = document.getElementById('custom_model_name').value.trim(); // Fixed field ID
const customParams = document.getElementById('custom_params').value.trim();
if (customParams) {
try {
settings.params = JSON.parse(customParams);
} catch (e) {
alert('Invalid JSON in custom parameters!');
resetUI();
return;
}
}
}
// Add request timeout control
const fetchWithTimeout = (url, options, timeout = 10000) => {
return Promise.race([
fetch(url, options),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Request timed out')), timeout)
)
]);
};
// Add retry counter
let fetchRetryCount = 0;
const maxFetchRetries = 2;
// Function to handle fetch requests
function attemptFetch() {
fetchRetryCount++;
console.log(`Fetch attempt ${fetchRetryCount}/${maxFetchRetries + 1}...`);
fetchWithTimeout(getApiUrl('generate_stimulus'), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(settings)
}, 15000)
.then(response => {
if (!response.ok) {
throw new Error(`Failed to start stimulus generation: ${response.status} ${response.statusText.trim()}.`);
}
return response.json();
})
.then(data => {
console.log('Stimulus generation started, server response:', data);
alert('Stimulus generation started!');
// Start checking generation status
checkGenerationStatus();
})
.catch(error => {
console.error('Error starting stimulus generation:', error);
// If there are retries left, continue trying
if (fetchRetryCount <= maxFetchRetries) {
console.log(`Request failed, trying again...`);
setTimeout(attemptFetch, 2000); // Wait 2 seconds before retrying
} else {
resetUI();
alert(`Failed to start stimulus generation: ${error.message}. Please try again later.`);
}
});
}
// Start first request attempt
attemptFetch();
}
function checkGenerationStatus() {
// Ensure WebSocket connection is active, if not, try to reconnect
if (!socket || !socket.connected) {
console.log("Detected WebSocket connection disconnected, trying to reconnect");
initializeSocket();
}
fetch(getApiUrl('generation_status'))
.then(response => {
if (!response.ok) {
if (response.status === 400) {
// Maybe session expired, wait 2 seconds before retrying
console.log("Session may be temporarily unavailable, retrying...");
window.statusCheckTimer = setTimeout(function () {
checkGenerationStatus();
}, 2000);
return null; // Don't continue processing current response
}
throw new Error(`Status error: ${response.status}`);
}
return response.json();
})
.then(data => {
if (!data) return; // If null (due to session issue), return immediately
console.log("Generation status:", data);
if (data.status === 'error') {
// If session invalid error, try to restart generation process
if (data.error_message === 'Invalid session') {
console.log("Session expired, refreshing page...");
alert("Your session has expired. The page will refresh.");
window.location.reload();
return;
}
alert('Error: ' + data.error_message);
resetUI();
return; // Immediately stop polling
}
if (data.status === 'completed') {
// Confirm again that there is a file before displaying completion message
if (data.file) {
// Save current file name for future check to avoid duplicates
const currentFile = data.file;
// Ensure file contains current session ID to prevent downloading incorrect file
if (!currentFile.includes(sessionId)) {
console.error("File does not match current session:", currentFile, sessionId);
alert('Error: Generated file does not match current session. Please try again.');
resetUI();
return;
}
// Use timestamp to ensure file name is unique, avoid browser cache
const timestamp = Date.now();
const downloadUrl = `${getApiUrl(`download/${currentFile}`)}?t=${timestamp}`;
console.log(`Preparing to download from: ${downloadUrl}`);
// Clear previous polling timer
if (window.statusCheckTimer) {
clearTimeout(window.statusCheckTimer);
window.statusCheckTimer = null;
}
// Create download link element
const link = document.createElement('a');
hideGenerationStatus(); // Hide status text
// Use timestamp URL to avoid browser cache
link.href = downloadUrl;
link.download = currentFile;
document.body.appendChild(link); // Add to DOM
// Show success message based on whether it's partial or complete
if (data.partial) {
alert('Generation stopped. Partial data saved and will be downloaded.');
} else {
alert('Stimulus generation complete!');
}
// Clear waiting flag
window.waitingForStopData = false;
// Download file
setTimeout(() => {
link.click();
// Remove link from DOM after completion
setTimeout(() => {
document.body.removeChild(link);
}, 100);
}, 500);
// Reset UI state
resetUI();
// Reset polling counter
window.retryCount = 0;
} else {
// If there is no file but status is completed, this is an error
console.error("Status reported as completed but no file was returned");
alert('Generation completed but no file was produced. Please try again.');
resetUI();
}
} else if (data.status === 'stopped') {
// Generation stopped with no partial data available
console.log("Generation has been stopped by user with no data to save");
window.waitingForStopData = false;
alert('Generation stopped. No data was generated.');
resetUI();
} else if (data.status === 'running') {
updateProgress(data.progress);
// Check if we're in stopping state (waiting for partial data to be saved)
if (data.stopping) {
console.log("Stopping... waiting for partial data to be saved");
// Show stopping status
const statusTextElement = document.getElementById('generation-status-text');
if (statusTextElement) {
statusTextElement.textContent = 'Stopping... Saving partial data...';
statusTextElement.style.display = 'block';
}
// Poll more frequently while stopping
window.statusCheckTimer = setTimeout(function () {
checkGenerationStatus();
}, 500);
return;
}
// Add extra check - if progress is 100 but status is still running
if (data.progress >= 100) {
console.log("Progress is 100% but status is still running, waiting for file...");
// Set to "Checking status & Creating file..."
showCheckingStatus();
// Give server more time to complete file generation
window.statusCheckTimer = setTimeout(function () {
checkGenerationStatus();
}, 2000); // Extend to 2 seconds to wait for file generation
} else {
// Ensure "Generating..." text is displayed
if (data.progress > 0 && data.progress < 100) {
showGeneratingStatus();
}
// Adjust polling frequency based on progress, the higher the progress, the more frequent the check
let pollInterval = 1000; // Default 1 second
if (data.progress > 0) {
// When progress is greater than 0, check more frequently
pollInterval = 500; // Change to 0.5 seconds
}
window.statusCheckTimer = setTimeout(function () {
checkGenerationStatus();
}, pollInterval);
}
} else {
// Handle unknown status
console.error("Unknown status received:", data.status);
alert("Received unexpected status from server. Generation may have failed. Please try again later.");
resetUI();
}
})
.catch(error => {
console.error('Error checking generation status:', error);
// If there is an error, wait 3 seconds before retrying, but only up to 3 times
if (!window.retryCount) window.retryCount = 0;
window.retryCount++;
if (window.retryCount <= 3) {
console.log(`Retrying status check (${window.retryCount}/3)...`);
window.statusCheckTimer = setTimeout(function () {
checkGenerationStatus();
}, 3000);
} else {
window.retryCount = 0; // Reset counter
alert('Error checking generation status. Please try again.');
resetUI();
}
});
}
stopButton.addEventListener('click', () => {
// Add confirmation dialogue box
const confirmStop = confirm('Are you sure you want to stop? Partial data will be saved if available.');
// Only execute stop operation when user clicks "Yes"
if (confirmStop) {
fetch(getApiUrl('stop_generation'), { method: 'POST' })
.then(response => response.json())
.then(data => {
console.log('Stop response:', data.message);
// Don't immediately stop polling - continue to wait for partial data
// The status check will handle the download when partial data is ready
// Set a flag to indicate we're waiting for stop to complete
window.waitingForStopData = true;
// Continue polling for a bit to get partial data
if (!window.statusCheckTimer) {
checkGenerationStatus();
}
})
.catch(error => {
console.error('Error stopping generation:', error);
alert('Failed to stop generation. Please try again in a few seconds.');
});
}
// If user clicks "No", do nothing
});
clearButton.addEventListener('click', () => {
// Reset model selection
document.getElementById('model_choice').value = '';
// Disable API key input box
document.getElementById('api_key').disabled = true;
const textAreas = [
'agent1_properties',
'agent2_properties',
'agent3_properties',
'api_key',
'iteration',
'experiment_design'
];
textAreas.forEach(id => {
const element = document.getElementById(id);
if (element) {
element.value = ''; // Clear text box and text area values
}
});
// Clear custom model configuration fields
const customModelFields = [
'custom_model_name',
'custom_api_url',
'custom_params'
];
customModelFields.forEach(id => {
const element = document.getElementById(id);
if (element) {
element.value = '';
}
});
// Reset checkboxes to default state
const agent2IndividualValidation = document.getElementById('agent2_individual_validation');
if (agent2IndividualValidation) {
agent2IndividualValidation.checked = false; // Set to false as per requirement #4
}
const agent3IndividualScoring = document.getElementById('agent3_individual_scoring');
if (agent3IndividualScoring) {
agent3IndividualScoring.checked = false;
}
// Hide custom model configuration section
const customModelConfig = document.getElementById('custom_model_config');
if (customModelConfig) {
customModelConfig.style.display = 'none';
}
// Clear all Items, only keep one initial Item
while (itemsContainer.firstChild) {
itemsContainer.removeChild(itemsContainer.firstChild);
}
// Create new Item 1
const newItem = document.createElement('div');
newItem.className = 'item-container';
newItem.id = 'item-1';
newItem.innerHTML = `
Item 1
Components
Content
`;
itemsContainer.appendChild(newItem);
// Add event listener to new buttons
newItem.querySelector('.add-component-btn').addEventListener('click', addComponentToAllItems);
newItem.querySelector('.delete-component-btn').addEventListener('click', deleteComponentFromAllItems);
newItem.querySelector('.add-item-btn').addEventListener('click', addNewItem);
// Add Agent 2 table clear functionality
const agent2Rows = Array.from(agent2Table.rows).slice(1); // Skip header
// First delete extra rows
while (agent2Table.rows.length > 2) {
agent2Table.deleteRow(2); // Always delete second row and all following rows
}
// Clear input in first row of two columns
agent2Table.rows[1].cells[0].querySelector('input').value = '';
agent2Table.rows[1].cells[1].querySelector('input').value = '';
// Add Agent 3 table clear functionality
const agent3Rows = Array.from(agent3PropertiesTable.rows).slice(1); // Skip header
// First delete extra rows
while (agent3PropertiesTable.rows.length > 2) {
agent3PropertiesTable.deleteRow(2); // Always delete second row and all following rows
}
// Clear input in first row of four columns
agent3PropertiesTable.rows[1].cells[0].querySelector('input').value = '';
agent3PropertiesTable.rows[1].cells[1].querySelector('input').value = '';
agent3PropertiesTable.rows[1].cells[2].querySelector('input').value = '';
agent3PropertiesTable.rows[1].cells[3].querySelector('input').value = '';
// Reset progress bar
updateProgress(0);
// Clear log area
clearLogs();
});
// Function to periodically check WebSocket status
function periodicSocketCheck() {
if (!socket || !socket.connected) {
if (socketInitialized) {
console.log("Detected WebSocket connection disconnected, resetting connection flag");
socketInitialized = false;
}
// Only try to reconnect if there is no connection attempt in progress, and the time since last attempt is reasonable
if (!socketConnectionInProgress && (!socketReconnectTimer || socketReconnectAttempts > 10)) {
console.log("Trying to reconnect WebSocket");
socketReconnectAttempts = 0; // Reset attempt counter
socketBackoffDelay = 1000; // Reset backoff delay
initializeSocket();
}
}
// Check connection status every 30 seconds
setTimeout(periodicSocketCheck, 30000);
}
// Disable all interactive elements on the page
function disableAllElements() {
// Start safety timeout
startSafetyTimeout();
// Disable all dropdowns, text boxes, and text areas
document.querySelectorAll('input, textarea, select').forEach(element => {
element.disabled = true;
});
// Disable all buttons (except the AutoGenerate button)
document.querySelectorAll('button').forEach(button => {
if (button.id !== 'auto_generate_button') {
button.disabled = true;
}
});
// Add disabled style to tables
document.querySelectorAll('table').forEach(table => {
table.classList.add('disabled-table');
});
// Add semi-transparent overlay to prevent user interaction
const overlay = document.createElement('div');
overlay.id = 'page-overlay';
overlay.className = 'page-overlay';
// Add loading animation
const spinner = document.createElement('div');
spinner.className = 'loading-spinner';
// Add processing text
const loadingText = document.createElement('div');
loadingText.className = 'loading-text';
loadingText.textContent = 'Generating properties...';
// Add animation and text to overlay
overlay.appendChild(spinner);
overlay.appendChild(loadingText);
document.body.appendChild(overlay);
}
// Enable all interactive elements on the page
function enableAllElements() {
// Clear safety timeout
clearSafetyTimeout();
// Enable all dropdowns, text boxes, and text areas
document.querySelectorAll('input, textarea, select').forEach(element => {
// Check if element is API key input box, and model selection requires API key
if (element.id === 'api_key' && modelChoice.value !== 'GPT-4o' && modelChoice.value !== 'custom') {
element.disabled = true; // Keep API key input box disabled
} else {
element.disabled = false;
}
});
// Enable all buttons
document.querySelectorAll('button').forEach(button => {
button.disabled = false;
});
// Restore Stop button state (should be disabled by default)
if (stopButton) {
stopButton.disabled = true;
}
// Remove table disabled style
document.querySelectorAll('table').forEach(table => {
table.classList.remove('disabled-table');
});
// Remove page overlay
const overlay = document.getElementById('page-overlay');
if (overlay) {
document.body.removeChild(overlay);
}
}
// Modify AutoGenerate button click event
autoGenerateButton.addEventListener('click', function () {
// First validate inputs
if (!validateAutoGenerateInputs()) {
return;
}
// Show loading status
autoGenerateButton.disabled = true;
autoGenerateButton.innerText = "Generating...";
// Disable all other elements on the page
disableAllElements();
// Collect example stimuli from tables
const exampleStimuli = collectExampleStimuli();
// Get experimental design
const experimentDesign = document.getElementById('experiment_design').value.trim();
// Build prompt, replace placeholders
let prompt = autoGeneratePromptTemplate
.replace('{Experimental design}', experimentDesign)
.replace('{Example stimuli}', JSON.stringify(exampleStimuli, null, 2));
// Record used model and built prompt
const selectedModel = modelChoice.value;
console.log("Model used:", selectedModel);
console.log("Experimental Design:", experimentDesign);
console.log("Example Stimuli:", exampleStimuli);
console.log("Complete Prompt:", prompt);
if (selectedModel === 'GPT-4o') {
// Use OpenAI API
callOpenAIAPI(prompt);
} else if (selectedModel === 'custom') {
// Use custom model API
callcustomAPI(prompt);
}
});
// Modify OpenAI API call function, enable page elements when successful or failed
function callOpenAIAPI(prompt) {
try {
const apiKey = document.getElementById('api_key').value.trim();
// Prepare request body
const requestBody = {
model: "gpt-4o",
messages: [
{
role: "user",
content: prompt
}
],
max_tokens: 2000
};
// Send API request
fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`
},
body: JSON.stringify(requestBody)
})
.then(response => {
// Check response status
if (!response.ok) {
throw new Error(`API request failed: ${response.status} ${response.statusText.trim()}`);
}
return response.json();
})
.then(data => {
// Clear safety timeout when API call succeeds
clearSafetyTimeout();
// Process API response
const content = data.choices[0].message.content;
// Wrap processing in try-catch
try {
processAPIResponse(content);
} catch (processingError) {
console.error('Error processing response:', processingError);
// Ensure page is not locked
enableAllElements();
alert(`Error processing response: ${processingError.message}`);
}
})
.catch(error => {
console.error('OpenAI API call error:', error);
// Clear safety timeout when API call fails
clearSafetyTimeout();
// Ensure page is not locked
enableAllElements();
alert(`OpenAI API call failed: ${error.message}. Please check your input is correct and try again later.`);
})
.finally(() => {
// Restore button state
autoGenerateButton.disabled = false;
autoGenerateButton.innerText = "AutoGenerate properties";
});
} catch (error) {
// Catch any errors that occur before setting up or executing API call
console.error('Error setting up API call:', error);
clearSafetyTimeout();
enableAllElements();
autoGenerateButton.disabled = false;
autoGenerateButton.innerText = "AutoGenerate properties";
alert(`Error setting up API call: ${error.message}`);
}
}
// Add custom model API call function (routed through backend to avoid CORS)
function callcustomAPI(prompt) {
const apiUrl = document.getElementById('custom_api_url').value.trim();
const apiKey = document.getElementById('api_key').value.trim();
const modelName = document.getElementById('custom_model_name').value.trim();
// Route through backend proxy to avoid CORS issues
const requestBody = {
session_id: sessionId,
prompt: prompt,
model: modelName,
api_url: apiUrl,
api_key: apiKey
};
fetch('/api/custom_model_inference', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestBody)
})
.then(response => {
if (!response.ok) {
return response.json().then(err => {
throw new Error(err.error || `API request failed: ${response.status}`);
});
}
return response.json();
})
.then(data => {
// Clear safety timeout when API call succeeds
clearSafetyTimeout();
if (data.error) {
throw new Error(data.error);
}
const content = data.response;
processAPIResponse(content);
})
.catch(error => {
console.error('Custom model API call error:', error);
clearSafetyTimeout();
enableAllElements();
alert(`Custom model API call failed: ${error.message}. Please check your input is correct and try again later.`);
})
.finally(() => {
autoGenerateButton.disabled = false;
autoGenerateButton.innerText = "AutoGenerate properties";
});
}
// Process API response
function processAPIResponse(response) {
try {
console.log("API response:", response);
// Clear safety timeout when starting to process response
clearSafetyTimeout();
// Extract Requirements dictionary
const requirementsMatch = response.match(/Requirements:\s*\{([^}]+)\}/s);
let requirements = {};
if (requirementsMatch && requirementsMatch[1]) {
try {
// Try to parse complete JSON
requirements = JSON.parse(`{${requirementsMatch[1]}}`);
} catch (e) {
console.error("Failed to parse Requirements JSON:", e);
// Use regular expression to parse key-value pairs
const keyValuePairs = requirementsMatch[1].match(/"([^"]+)":\s*"([^"]+)"/g);
if (keyValuePairs) {
keyValuePairs.forEach(pair => {
const matches = pair.match(/"([^"]+)":\s*"([^"]+)"/);
if (matches && matches.length === 3) {
requirements[matches[1]] = matches[2];
}
});
}
}
}
// Extract Scoring Dimensions dictionary
const scoringMatch = response.match(/Scoring Dimensions:\s*\{([^}]+)\}/s);
let scoringDimensions = {};
if (scoringMatch && scoringMatch[1]) {
try {
// Try to parse complete JSON
scoringDimensions = JSON.parse(`{${scoringMatch[1]}}`);
} catch (e) {
console.error("Failed to parse Scoring Dimensions JSON:", e);
// Use regular expression to parse key-value pairs
const keyValuePairs = scoringMatch[1].match(/"([^"]+)":\s*"([^"]+)"/g);
if (keyValuePairs) {
keyValuePairs.forEach(pair => {
const matches = pair.match(/"([^"]+)":\s*"([^"]+)"/);
if (matches && matches.length === 3) {
scoringDimensions[matches[1]] = matches[2];
}
});
}
}
}
// If no content is found above, try to find curly braces
if (Object.keys(requirements).length === 0 && Object.keys(scoringDimensions).length === 0) {
const jsonMatches = response.match(/\{([^{}]*)\}/g);
if (jsonMatches && jsonMatches.length >= 2) {
try {
requirements = JSON.parse(jsonMatches[0]);
scoringDimensions = JSON.parse(jsonMatches[1]);
} catch (e) {
console.error("Failed to parse JSON objects:", e);
}
}
}
// Confirm content has been extracted
if (Object.keys(requirements).length === 0 || Object.keys(scoringDimensions).length === 0) {
// First enable all elements, then display error message
enableAllElements();
throw new Error("Could not extract valid Requirements or Scoring Dimensions from API response.");
}
// Fill validator table
fillValidatorTable(requirements);
// Fill scorer table
fillScorerTable(scoringDimensions);
// Ensure all elements are enabled before displaying completion message
enableAllElements();
alert("Auto-generation complete! Please carefully review the content in the Validator and Scorer tables to ensure they meet your experimental design requirements.");
} catch (error) {
console.error("Error processing API response:", error);
// Clear safety timeout even on error
clearSafetyTimeout();
// Ensure all elements are enabled regardless
enableAllElements();
alert(`Failed to process API response: ${error.message}. Please try again later.`);
}
}
// Fill validator table
function fillValidatorTable(requirements) {
// Clear current table content
const tbody = agent2Table.querySelector('tbody');
const currentRowCount = tbody.querySelectorAll('tr').length;
const requirementsCount = Object.keys(requirements).length;
console.log(`Validator table: current rows = ${currentRowCount}, required rows = ${requirementsCount}`);
// Calculate how many rows need to be added or deleted
if (requirementsCount > currentRowCount) {
// Need to add rows
const rowsToAdd = requirementsCount - currentRowCount;
console.log(`Adding ${rowsToAdd} rows to Validator table`);
// Add rows directly using DOM operations, not through clicking buttons
for (let i = 0; i < rowsToAdd; i++) {
// Create new row
const newRow = document.createElement('tr');
// Create and add first cell (property name)
const propertyCell = document.createElement('td');
propertyCell.className = "agent_2_properties-column";
const propertyInput = document.createElement('input');
propertyInput.type = "text";
propertyInput.placeholder = "Enter new property";
propertyCell.appendChild(propertyInput);
// Create and add second cell (description)
const descriptionCell = document.createElement('td');
descriptionCell.className = "agent_2_description-column";
const descriptionInput = document.createElement('input');
descriptionInput.type = "text";
descriptionInput.placeholder = "Enter property's description";
descriptionCell.appendChild(descriptionInput);
// New: Add Delete button
const actionCell = document.createElement('td');
const deleteBtn = document.createElement('button');
deleteBtn.className = 'delete-row-btn delete-btn';
deleteBtn.textContent = 'Delete';
actionCell.appendChild(deleteBtn);
newRow.appendChild(propertyCell);
newRow.appendChild(descriptionCell);
newRow.appendChild(actionCell);
// Add row to table
tbody.appendChild(newRow);
}
} else if (requirementsCount < currentRowCount) {
// Need to delete rows
const rowsToDelete = currentRowCount - requirementsCount;
console.log(`Deleting ${rowsToDelete} rows from Validator table`);
// Delete extra rows, starting from the last row
for (let i = 0; i < rowsToDelete; i++) {
tbody.removeChild(tbody.lastElementChild);
}
}
// Get updated rows and fill in content
const updatedRows = agent2Table.querySelectorAll('tbody tr');
let index = 0;
for (const [key, value] of Object.entries(requirements)) {
if (index < updatedRows.length) {
const cells = updatedRows[index].querySelectorAll('input');
if (cells.length >= 2) {
cells[0].value = key;
cells[1].value = value;
}
index++;
}
}
}
// Fill scorer table
function fillScorerTable(scoringDimensions) {
// Clear current table content
const tbody = agent3PropertiesTable.querySelector('tbody');
const currentRowCount = tbody.querySelectorAll('tr').length;
const dimensionsCount = Object.keys(scoringDimensions).length;
console.log(`Scorer table: current rows = ${currentRowCount}, required rows = ${dimensionsCount}`);
// Calculate how many rows need to be added or deleted
if (dimensionsCount > currentRowCount) {
// Need to add rows
const rowsToAdd = dimensionsCount - currentRowCount;
console.log(`Adding ${rowsToAdd} rows to Scorer table`);
// Add rows directly using DOM operations, not through clicking buttons
for (let i = 0; i < rowsToAdd; i++) {
// Create new row
const newRow = document.createElement('tr');
// Create and add first cell (aspect name)
const aspectCell = document.createElement('td');
aspectCell.className = "agent_3_properties-column";
const aspectInput = document.createElement('input');
aspectInput.type = "text";
aspectInput.placeholder = "Enter new aspect";
aspectCell.appendChild(aspectInput);
// Create and add second cell (description)
const descriptionCell = document.createElement('td');
descriptionCell.className = "agent_3_description-column";
const descriptionInput = document.createElement('input');
descriptionInput.type = "text";
descriptionInput.placeholder = "Enter aspect's description";
descriptionCell.appendChild(descriptionInput);
// Create and add third cell (minimum value)
const minCell = document.createElement('td');
minCell.className = "agent_3_minimum-column";
const minInput = document.createElement('input');
minInput.type = "number";
minInput.min = "0";
minInput.placeholder = "e.g. 0";
minCell.appendChild(minInput);
// Create and add fourth cell (maximum value)
const maxCell = document.createElement('td');
maxCell.className = "agent_3_maximum-column";
const maxInput = document.createElement('input');
maxInput.type = "number";
maxInput.min = "0";
maxInput.placeholder = "e.g. 10";
maxCell.appendChild(maxInput);
// New: Add Delete button
const actionCell = document.createElement('td');
const deleteBtn = document.createElement('button');
deleteBtn.className = 'delete-row-btn delete-btn';
deleteBtn.textContent = 'Delete';
actionCell.appendChild(deleteBtn);
newRow.appendChild(aspectCell);
newRow.appendChild(descriptionCell);
newRow.appendChild(minCell);
newRow.appendChild(maxCell);
newRow.appendChild(actionCell);
// Add row to table
tbody.appendChild(newRow);
}
} else if (dimensionsCount < currentRowCount) {
// Need to delete rows
const rowsToDelete = currentRowCount - dimensionsCount;
console.log(`Deleting ${rowsToDelete} rows from Scorer table`);
// Delete extra rows, starting from the last row
for (let i = 0; i < rowsToDelete; i++) {
tbody.removeChild(tbody.lastElementChild);
}
}
// Get updated rows and fill in content
const updatedRows = agent3PropertiesTable.querySelectorAll('tbody tr');
let index = 0;
for (const [key, value] of Object.entries(scoringDimensions)) {
if (index < updatedRows.length) {
const cells = updatedRows[index].querySelectorAll('input');
if (cells.length >= 4) {
cells[0].value = key;
cells[1].value = value;
cells[2].value = '0'; // Default minimum value
cells[3].value = '10'; // Default maximum value
}
index++;
}
}
}
// Add global error handler, ensure page is not locked
window.addEventListener('error', function (event) {
console.error('Global error:', event.error || event.message);
// If page overlay exists and has been active for more than 30 seconds, remove it automatically
const overlay = document.getElementById('page-overlay');
if (overlay && document.body.contains(overlay)) {
console.warn('Detected global error, removing possibly frozen page overlay');
clearSafetyTimeout();
document.body.removeChild(overlay);
// Restore normal page interaction
enableUI();
}
});
// Add safety timeout, ensure page is unlocked after 90 seconds
function startSafetyTimeout() {
console.log('Starting safety timeout');
window.safetyTimeoutId = setTimeout(function () {
console.log('Safety timeout triggered');
if (document.getElementById('page-overlay')) {
console.log('Removing overlay due to safety timeout');
enableAllElements();
alert("Operation timed out. The page has been unlocked. Please try again.");
autoGenerateButton.disabled = false;
autoGenerateButton.innerText = "AutoGenerate properties";
}
}, 90000); // 90 second timeout
}
function clearSafetyTimeout() {
if (window.safetyTimeoutId) {
console.log('Clearing safety timeout');
clearTimeout(window.safetyTimeoutId);
window.safetyTimeoutId = null;
}
}
// Add UI safety timeout protection, ensure interface is not locked for more than 30 seconds
function setupUIProtection() {
// Global timeout protection: ensure page is not locked for more than 30 seconds
window.uiProtectionTimeout = null;
}
// Modify disableUI function to add timeout protection
function disableUI() {
// Disable main buttons and input elements
generateButton.disabled = true;
clearButton.disabled = true;
addAgent2Button.disabled = true;
addAgent3Button.disabled = true;
modelSelect.disabled = true;
apiKeyInput.disabled = true;
// Enable Stop button
stopButton.disabled = false;
// Add element to display "Generating..." status
updateGenerationStatus('generating');
// Create and display page overlay
createPageOverlay("Generating stimuli, please wait...");
// Set UI protection timeout, ensure interface is not locked indefinitely
if (window.uiProtectionTimeout) {
clearTimeout(window.uiProtectionTimeout);
}
window.uiProtectionTimeout = setTimeout(() => {
console.warn("UI protection timeout triggered - interface will automatically restore after 90 seconds");
enableUI();
// Remove overlay (if it exists)
const overlay = document.getElementById('page-overlay');
if (overlay && document.body.contains(overlay)) {
document.body.removeChild(overlay);
}
// Display warning message
alert("Operation timed out. If you are waiting for the generation result, please try again later.");
}, 90000); // 90 second timeout
}
// Modify enableUI function to clear timeout protection
function enableUI() {
// Enable main buttons and input elements
generateButton.disabled = false;
clearButton.disabled = false;
addAgent2Button.disabled = false;
addAgent3Button.disabled = false;
modelSelect.disabled = false;
// Determine if API key input box is available based on current model selection
handleModelChange();
// Disable Stop button
stopButton.disabled = true;
// Clear "Generating..." status display
updateGenerationStatus('idle');
// Remove overlay (if it exists)
const overlay = document.getElementById('page-overlay');
if (overlay && document.body.contains(overlay)) {
document.body.removeChild(overlay);
}
// Clear UI protection timeout
if (window.uiProtectionTimeout) {
clearTimeout(window.uiProtectionTimeout);
window.uiProtectionTimeout = null;
}
}
// Modify createPageOverlay function to add click event handling
function createPageOverlay(message) {
// If overlay already exists, remove it first
const existingOverlay = document.getElementById('page-overlay');
if (existingOverlay) {
document.body.removeChild(existingOverlay);
}
// Create overlay element
const overlay = document.createElement('div');
overlay.id = 'page-overlay';
overlay.className = 'page-overlay';
// Create loading animation
const spinner = document.createElement('div');
spinner.className = 'loading-spinner';
overlay.appendChild(spinner);
// Create loading text
const loadingText = document.createElement('div');
loadingText.className = 'loading-text';
loadingText.textContent = message || 'Loading...';
overlay.appendChild(loadingText);
// Add to page
document.body.appendChild(overlay);
return overlay;
}
// Add event delegation for row deletion in Validator table
agent2Table.addEventListener('click', function (e) {
if (e.target && e.target.classList.contains('delete-row-btn')) {
const row = e.target.closest('tr');
if (row.parentNode.rows.length > 1) {
row.remove();
}
}
});
// Add event delegation for row deletion in Scorer table
agent3PropertiesTable.addEventListener('click', function (e) {
if (e.target && e.target.classList.contains('delete-row-btn')) {
const row = e.target.closest('tr');
if (row.parentNode.rows.length > 1) {
row.remove();
}
}
});
// ===== RESTART COUNTDOWN FUNCTIONALITY =====
/**
* Initialize the restart countdown system
* This calculates when to show the countdown (20 minutes before restart)
* and sets up the timer to start the countdown at the right time
*/
function initializeRestartCountdown() {
try {
// Calculate when the app will restart (RESTART_INTERVAL seconds from now)
const appStartTime = Date.now();
const restartTime = appStartTime + (RESTART_INTERVAL * 1000);
// Calculate when to start the countdown (20 minutes before restart)
const countdownStartTime = restartTime - (COUNTDOWN_DURATION * 1000);
const timeUntilCountdownStart = countdownStartTime - Date.now();
console.log(`App started at: ${new Date(appStartTime).toLocaleString()}`);
console.log(`Restart scheduled for: ${new Date(restartTime).toLocaleString()}`);
console.log(`Countdown will start at: ${new Date(countdownStartTime).toLocaleString()}`);
console.log(`Time until countdown starts: ${Math.round(timeUntilCountdownStart / 1000)} seconds`);
// If countdown should start immediately (for testing or if app was restarted recently)
if (timeUntilCountdownStart <= 0) {
const remainingTime = Math.max(0, Math.round((restartTime - Date.now()) / 1000));
if (remainingTime > 0) {
console.log(`Starting countdown immediately with ${remainingTime} seconds remaining`);
startRestartCountdown(remainingTime);
}
} else {
// Set timer to start countdown at the right time
countdownStartTimer = setTimeout(() => {
startRestartCountdown(COUNTDOWN_DURATION);
}, timeUntilCountdownStart);
console.log(`Countdown timer set to start in ${Math.round(timeUntilCountdownStart / 1000)} seconds`);
}
} catch (error) {
console.error('Error initializing restart countdown:', error);
}
}
/**
* Start the visual countdown timer
* @param {number} initialSeconds - Number of seconds to count down from
*/
function startRestartCountdown(initialSeconds) {
try {
let remainingSeconds = initialSeconds;
// Show the countdown element
const countdownElement = document.getElementById('restart-countdown');
const countdownTimeElement = document.getElementById('countdown-time');
if (!countdownElement || !countdownTimeElement) {
console.error('Countdown elements not found in DOM');
return;
}
countdownElement.style.display = 'block';
// Update countdown display immediately
updateCountdownDisplay(remainingSeconds);
console.log(`Starting restart countdown: ${remainingSeconds} seconds`);
// Start the countdown timer (update every second)
restartCountdownTimer = setInterval(() => {
remainingSeconds--;
if (remainingSeconds <= 0) {
// Countdown finished
stopRestartCountdown();
console.log('Restart countdown completed - app should restart now');
} else {
updateCountdownDisplay(remainingSeconds);
}
}, 1000);
} catch (error) {
console.error('Error starting restart countdown:', error);
}
}
/**
* Update the countdown display with current time
* @param {number} seconds - Remaining seconds
*/
function updateCountdownDisplay(seconds) {
try {
const minutes = Math.floor(seconds / 60);
const secs = seconds % 60;
const timeString = `${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
const countdownTimeElement = document.getElementById('countdown-time');
if (countdownTimeElement) {
countdownTimeElement.textContent = timeString;
}
// Add visual urgency as time runs out
const countdownElement = document.getElementById('restart-countdown');
if (countdownElement) {
if (seconds <= 60) {
// Last minute - add urgent animation
countdownElement.style.animationDuration = '0.5s';
} else if (seconds <= 300) {
// Last 5 minutes - speed up animation
countdownElement.style.animationDuration = '1s';
}
}
} catch (error) {
console.error('Error updating countdown display:', error);
}
}
/**
* Stop and hide the countdown timer
*/
function stopRestartCountdown() {
try {
// Clear the countdown timer
if (restartCountdownTimer) {
clearInterval(restartCountdownTimer);
restartCountdownTimer = null;
}
// Hide the countdown element
const countdownElement = document.getElementById('restart-countdown');
if (countdownElement) {
countdownElement.style.display = 'none';
}
console.log('Restart countdown stopped and hidden');
} catch (error) {
console.error('Error stopping restart countdown:', error);
}
}
/**
* Cleanup countdown timers (called when page unloads or app shuts down)
*/
function cleanupCountdownTimers() {
try {
if (countdownStartTimer) {
clearTimeout(countdownStartTimer);
countdownStartTimer = null;
}
if (restartCountdownTimer) {
clearInterval(restartCountdownTimer);
restartCountdownTimer = null;
}
console.log('Countdown timers cleaned up');
} catch (error) {
console.error('Error cleaning up countdown timers:', error);
}
}
// Modify addAgent2Button click handler to add Delete button in new row
addAgent2Button.addEventListener('click', function () {
const tbody = agent2Table.querySelector('tbody');
const newRow = document.createElement('tr');
newRow.innerHTML = `
`;
tbody.appendChild(newRow);
});
// Modify addAgent3Button click handler to add Delete button in new row
addAgent3Button.addEventListener('click', function () {
const tbody = agent3PropertiesTable.querySelector('tbody');
const newRow = document.createElement('tr');
newRow.innerHTML = `