Initial commit: Markov Economics Simulation App
This commit is contained in:
281
app/static/js/app.js
Normal file
281
app/static/js/app.js
Normal file
@@ -0,0 +1,281 @@
|
||||
/**
|
||||
* Main JavaScript Application for Markov Economics
|
||||
*
|
||||
* Provides utility functions and global application behavior
|
||||
*/
|
||||
|
||||
// Global application state
|
||||
window.MarkovEconomics = {
|
||||
socket: null,
|
||||
currentSimulationId: null,
|
||||
isConnected: false,
|
||||
|
||||
// Configuration
|
||||
config: {
|
||||
maxRetries: 3,
|
||||
retryDelay: 1000,
|
||||
updateInterval: 100
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize Socket.IO connection
|
||||
*/
|
||||
function initializeSocket() {
|
||||
if (typeof io !== 'undefined') {
|
||||
window.MarkovEconomics.socket = io();
|
||||
|
||||
window.MarkovEconomics.socket.on('connect', function() {
|
||||
console.log('Connected to server');
|
||||
window.MarkovEconomics.isConnected = true;
|
||||
updateConnectionStatus(true);
|
||||
});
|
||||
|
||||
window.MarkovEconomics.socket.on('disconnect', function() {
|
||||
console.log('Disconnected from server');
|
||||
window.MarkovEconomics.isConnected = false;
|
||||
updateConnectionStatus(false);
|
||||
});
|
||||
|
||||
window.MarkovEconomics.socket.on('connect_error', function(error) {
|
||||
console.error('Connection error:', error);
|
||||
updateConnectionStatus(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update connection status indicator
|
||||
*/
|
||||
function updateConnectionStatus(connected) {
|
||||
// You can add a connection status indicator here if needed
|
||||
if (connected) {
|
||||
console.log('✅ Real-time connection established');
|
||||
} else {
|
||||
console.log('❌ Real-time connection lost');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format numbers for display
|
||||
*/
|
||||
function formatNumber(num, decimals = 2) {
|
||||
if (num === null || num === undefined || isNaN(num)) {
|
||||
return '0';
|
||||
}
|
||||
|
||||
if (Math.abs(num) >= 1e9) {
|
||||
return (num / 1e9).toFixed(decimals) + 'B';
|
||||
} else if (Math.abs(num) >= 1e6) {
|
||||
return (num / 1e6).toFixed(decimals) + 'M';
|
||||
} else if (Math.abs(num) >= 1e3) {
|
||||
return (num / 1e3).toFixed(decimals) + 'K';
|
||||
} else {
|
||||
return num.toLocaleString(undefined, {
|
||||
minimumFractionDigits: decimals,
|
||||
maximumFractionDigits: decimals
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format currency values
|
||||
*/
|
||||
function formatCurrency(amount, decimals = 0) {
|
||||
if (amount === null || amount === undefined || isNaN(amount)) {
|
||||
return '$0';
|
||||
}
|
||||
|
||||
return '$' + formatNumber(amount, decimals);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format percentage values
|
||||
*/
|
||||
function formatPercentage(value, decimals = 1) {
|
||||
if (value === null || value === undefined || isNaN(value)) {
|
||||
return '0%';
|
||||
}
|
||||
|
||||
return (value * 100).toFixed(decimals) + '%';
|
||||
}
|
||||
|
||||
/**
|
||||
* Show notification message
|
||||
*/
|
||||
function showNotification(message, type = 'info', duration = 3000) {
|
||||
// Create notification element
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `alert alert-${type} alert-dismissible fade show position-fixed`;
|
||||
notification.style.cssText = `
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 9999;
|
||||
min-width: 300px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
`;
|
||||
|
||||
notification.innerHTML = `
|
||||
${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
`;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
// Auto-remove after duration
|
||||
setTimeout(() => {
|
||||
if (notification.parentNode) {
|
||||
notification.remove();
|
||||
}
|
||||
}, duration);
|
||||
|
||||
return notification;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show loading spinner
|
||||
*/
|
||||
function showLoading(element, text = 'Loading...') {
|
||||
if (typeof element === 'string') {
|
||||
element = document.getElementById(element);
|
||||
}
|
||||
|
||||
if (element) {
|
||||
element.innerHTML = `
|
||||
<div class="d-flex align-items-center justify-content-center">
|
||||
<div class="spinner me-2"></div>
|
||||
<span>${text}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide loading spinner
|
||||
*/
|
||||
function hideLoading(element, originalContent = '') {
|
||||
if (typeof element === 'string') {
|
||||
element = document.getElementById(element);
|
||||
}
|
||||
|
||||
if (element) {
|
||||
element.innerHTML = originalContent;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make API request with error handling
|
||||
*/
|
||||
async function apiRequest(url, options = {}) {
|
||||
const defaultOptions = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
};
|
||||
|
||||
const finalOptions = { ...defaultOptions, ...options };
|
||||
|
||||
try {
|
||||
const response = await fetch(url, finalOptions);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('API Request failed:', error);
|
||||
showNotification(`Error: ${error.message}`, 'danger');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate parameter ranges
|
||||
*/
|
||||
function validateParameters(params) {
|
||||
const errors = [];
|
||||
|
||||
if (params.r_rate < 0 || params.r_rate > 0.25) {
|
||||
errors.push('Capital rate must be between 0% and 25%');
|
||||
}
|
||||
|
||||
if (params.g_rate < 0 || params.g_rate > 0.20) {
|
||||
errors.push('Growth rate must be between 0% and 20%');
|
||||
}
|
||||
|
||||
if (params.num_agents < 10 || params.num_agents > 10000) {
|
||||
errors.push('Number of agents must be between 10 and 10,000');
|
||||
}
|
||||
|
||||
if (params.iterations < 100 || params.iterations > 100000) {
|
||||
errors.push('Iterations must be between 100 and 100,000');
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Debounce function for parameter changes
|
||||
*/
|
||||
function debounce(func, wait) {
|
||||
let timeout;
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout);
|
||||
func(...args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize application
|
||||
*/
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize Socket.IO if available
|
||||
initializeSocket();
|
||||
|
||||
// Initialize tooltips
|
||||
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
||||
tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||||
return new bootstrap.Tooltip(tooltipTriggerEl);
|
||||
});
|
||||
|
||||
// Add fade-in animation to cards
|
||||
const cards = document.querySelectorAll('.card');
|
||||
cards.forEach((card, index) => {
|
||||
card.style.animationDelay = `${index * 0.1}s`;
|
||||
card.classList.add('fade-in');
|
||||
});
|
||||
|
||||
console.log('🚀 Markov Economics application initialized');
|
||||
});
|
||||
|
||||
/**
|
||||
* Handle page visibility changes
|
||||
*/
|
||||
document.addEventListener('visibilitychange', function() {
|
||||
if (document.hidden) {
|
||||
console.log('Page hidden - pausing updates');
|
||||
} else {
|
||||
console.log('Page visible - resuming updates');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Export functions to global scope
|
||||
*/
|
||||
window.MarkovEconomics.utils = {
|
||||
formatNumber,
|
||||
formatCurrency,
|
||||
formatPercentage,
|
||||
showNotification,
|
||||
showLoading,
|
||||
hideLoading,
|
||||
apiRequest,
|
||||
validateParameters,
|
||||
debounce
|
||||
};
|
Reference in New Issue
Block a user