Loading forecast_app/app.py +31 −0 Original line number Diff line number Diff line Loading @@ -39,6 +39,10 @@ TEST_DATA_PATH = os.path.join(DATA_DIR, 'sim2_supply_chain_data_test.json') # TiRex model (loaded lazily on first request) _tirex_model = None # AI activation state (disabled by default) _ai_enabled = False AI_PASSWORD = os.environ.get('AI_PASSWORD', 'HorloML-AI') def get_tirex_model(): """Load TiRex model lazily (first time only)""" Loading Loading @@ -590,9 +594,36 @@ def results(): test_data=test_data) @app.route('/api/ai_status', methods=['GET']) def ai_status(): """Get current AI activation status""" global _ai_enabled return jsonify({'enabled': _ai_enabled}) @app.route('/api/toggle_ai', methods=['POST']) def toggle_ai(): """Toggle AI activation with password verification""" global _ai_enabled data = request.get_json() password = data.get('password', '') if password != AI_PASSWORD: return jsonify({'success': False, 'error': 'Invalid password'}), 401 _ai_enabled = not _ai_enabled return jsonify({'success': True, 'enabled': _ai_enabled}) @app.route('/api/tirex_forecast', methods=['GET']) def tirex_forecast(): """Generate AI predictions using TiRex model""" # Check if AI is enabled global _ai_enabled if not _ai_enabled: return jsonify({'error': 'AI features are currently disabled. Please ask your teacher to enable AI.'}), 403 if not TIREX_AVAILABLE: return jsonify({'error': 'TiRex library not available. Please install: pip install tirex-ts torch'}), 500 Loading forecast_app/docker-compose.yml +3 −1 Original line number Diff line number Diff line Loading @@ -4,3 +4,5 @@ services: ports: - "52999:5001" restart: unless-stopped environment: - AI_PASSWORD=HorloML-AI No newline at end of file forecast_app/templates/base.html +131 −0 Original line number Diff line number Diff line Loading @@ -72,6 +72,12 @@ </div> <div class="navbar-end"> <div class="navbar-item"> <button class="button is-danger" id="aiToggleButton" onclick="toggleAI()" style="min-width: 150px;"> <span class="icon"><i class="fas fa-brain"></i></span> <span id="aiToggleText">Activate AI</span> </button> </div> <div class="navbar-item"> <a class="button is-light" href="{{ url_for('reset') }}"> <span class="icon"><i class="fas fa-redo"></i></span> Loading @@ -92,5 +98,130 @@ </p> </div> </footer> <!-- Password Modal --> <div class="modal" id="passwordModal"> <div class="modal-background" onclick="closePasswordModal()"></div> <div class="modal-card"> <header class="modal-card-head"> <p class="modal-card-title">Teacher Authentication Required</p> <button class="delete" aria-label="close" onclick="closePasswordModal()"></button> </header> <section class="modal-card-body"> <div class="field"> <label class="label">Enter Password</label> <div class="control has-icons-left"> <input class="input" type="password" id="aiPassword" placeholder="Enter teacher password"> <span class="icon is-small is-left"> <i class="fas fa-lock"></i> </span> </div> <p class="help is-danger" id="passwordError" style="display: none;">Invalid password. Please try again.</p> </div> </section> <footer class="modal-card-foot"> <button class="button is-success" onclick="submitPassword()">Submit</button> <button class="button" onclick="closePasswordModal()">Cancel</button> </footer> </div> </div> <script> // AI Toggle functionality let currentAIStatus = false; // Check AI status on page load async function checkAIStatus() { try { const response = await fetch('/api/ai_status'); const data = await response.json(); currentAIStatus = data.enabled; updateAIButton(currentAIStatus); } catch (error) { console.error('Failed to check AI status:', error); } } // Update AI button appearance function updateAIButton(enabled) { const button = document.getElementById('aiToggleButton'); const text = document.getElementById('aiToggleText'); if (enabled) { button.classList.remove('is-danger'); button.classList.add('is-success'); text.textContent = 'Deactivate AI'; } else { button.classList.remove('is-success'); button.classList.add('is-danger'); text.textContent = 'Activate AI'; } } // Open password modal function toggleAI() { document.getElementById('passwordModal').classList.add('is-active'); document.getElementById('aiPassword').value = ''; document.getElementById('passwordError').style.display = 'none'; // Focus on password input setTimeout(() => { document.getElementById('aiPassword').focus(); }, 100); } // Close password modal function closePasswordModal() { document.getElementById('passwordModal').classList.remove('is-active'); } // Submit password async function submitPassword() { const password = document.getElementById('aiPassword').value; const errorElement = document.getElementById('passwordError'); try { const response = await fetch('/api/toggle_ai', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ password: password }) }); const data = await response.json(); if (response.ok && data.success) { currentAIStatus = data.enabled; updateAIButton(currentAIStatus); closePasswordModal(); // Reload page to update AI button state on predict page if (typeof updateAIPredictButton === 'function') { updateAIPredictButton(currentAIStatus); } } else { errorElement.style.display = 'block'; } } catch (error) { console.error('Failed to toggle AI:', error); errorElement.textContent = 'Network error. Please try again.'; errorElement.style.display = 'block'; } } // Allow Enter key to submit password document.addEventListener('DOMContentLoaded', function() { checkAIStatus(); const passwordInput = document.getElementById('aiPassword'); if (passwordInput) { passwordInput.addEventListener('keypress', function(e) { if (e.key === 'Enter') { submitPassword(); } }); } }); </script> </body> </html> forecast_app/templates/predict.html +34 −1 Original line number Diff line number Diff line Loading @@ -85,7 +85,7 @@ </button> </div> <div class="control"> <button type="button" class="button is-primary" onclick="fillWithAI()" id="aiButton"> <button type="button" class="button is-primary" onclick="fillWithAI()" id="aiButton" disabled> <span class="icon"><i class="fas fa-brain"></i></span> <span>Fill with AI Model</span> </button> Loading Loading @@ -168,6 +168,39 @@ const watches = {{ watches | tojson }}; const lastYear = {{ last_year | tojson }}; // Update AI button state based on AI activation function updateAIPredictButton(enabled) { const button = document.getElementById('aiButton'); if (button) { button.disabled = !enabled; if (enabled) { button.classList.remove('is-light'); button.classList.add('is-primary'); button.style.opacity = '1'; button.style.cursor = 'pointer'; } else { button.classList.remove('is-primary'); button.classList.add('is-light'); button.style.opacity = '0.5'; button.style.cursor = 'not-allowed'; } } } // Check AI status on page load async function checkAIStatusForPredict() { try { const response = await fetch('/api/ai_status'); const data = await response.json(); updateAIPredictButton(data.enabled); } catch (error) { console.error('Failed to check AI status:', error); } } // Call on page load document.addEventListener('DOMContentLoaded', checkAIStatusForPredict); function getInputs(watchId) { const inputs = []; for (let month = 1; month <= 12; month++) { Loading Loading
forecast_app/app.py +31 −0 Original line number Diff line number Diff line Loading @@ -39,6 +39,10 @@ TEST_DATA_PATH = os.path.join(DATA_DIR, 'sim2_supply_chain_data_test.json') # TiRex model (loaded lazily on first request) _tirex_model = None # AI activation state (disabled by default) _ai_enabled = False AI_PASSWORD = os.environ.get('AI_PASSWORD', 'HorloML-AI') def get_tirex_model(): """Load TiRex model lazily (first time only)""" Loading Loading @@ -590,9 +594,36 @@ def results(): test_data=test_data) @app.route('/api/ai_status', methods=['GET']) def ai_status(): """Get current AI activation status""" global _ai_enabled return jsonify({'enabled': _ai_enabled}) @app.route('/api/toggle_ai', methods=['POST']) def toggle_ai(): """Toggle AI activation with password verification""" global _ai_enabled data = request.get_json() password = data.get('password', '') if password != AI_PASSWORD: return jsonify({'success': False, 'error': 'Invalid password'}), 401 _ai_enabled = not _ai_enabled return jsonify({'success': True, 'enabled': _ai_enabled}) @app.route('/api/tirex_forecast', methods=['GET']) def tirex_forecast(): """Generate AI predictions using TiRex model""" # Check if AI is enabled global _ai_enabled if not _ai_enabled: return jsonify({'error': 'AI features are currently disabled. Please ask your teacher to enable AI.'}), 403 if not TIREX_AVAILABLE: return jsonify({'error': 'TiRex library not available. Please install: pip install tirex-ts torch'}), 500 Loading
forecast_app/docker-compose.yml +3 −1 Original line number Diff line number Diff line Loading @@ -4,3 +4,5 @@ services: ports: - "52999:5001" restart: unless-stopped environment: - AI_PASSWORD=HorloML-AI No newline at end of file
forecast_app/templates/base.html +131 −0 Original line number Diff line number Diff line Loading @@ -72,6 +72,12 @@ </div> <div class="navbar-end"> <div class="navbar-item"> <button class="button is-danger" id="aiToggleButton" onclick="toggleAI()" style="min-width: 150px;"> <span class="icon"><i class="fas fa-brain"></i></span> <span id="aiToggleText">Activate AI</span> </button> </div> <div class="navbar-item"> <a class="button is-light" href="{{ url_for('reset') }}"> <span class="icon"><i class="fas fa-redo"></i></span> Loading @@ -92,5 +98,130 @@ </p> </div> </footer> <!-- Password Modal --> <div class="modal" id="passwordModal"> <div class="modal-background" onclick="closePasswordModal()"></div> <div class="modal-card"> <header class="modal-card-head"> <p class="modal-card-title">Teacher Authentication Required</p> <button class="delete" aria-label="close" onclick="closePasswordModal()"></button> </header> <section class="modal-card-body"> <div class="field"> <label class="label">Enter Password</label> <div class="control has-icons-left"> <input class="input" type="password" id="aiPassword" placeholder="Enter teacher password"> <span class="icon is-small is-left"> <i class="fas fa-lock"></i> </span> </div> <p class="help is-danger" id="passwordError" style="display: none;">Invalid password. Please try again.</p> </div> </section> <footer class="modal-card-foot"> <button class="button is-success" onclick="submitPassword()">Submit</button> <button class="button" onclick="closePasswordModal()">Cancel</button> </footer> </div> </div> <script> // AI Toggle functionality let currentAIStatus = false; // Check AI status on page load async function checkAIStatus() { try { const response = await fetch('/api/ai_status'); const data = await response.json(); currentAIStatus = data.enabled; updateAIButton(currentAIStatus); } catch (error) { console.error('Failed to check AI status:', error); } } // Update AI button appearance function updateAIButton(enabled) { const button = document.getElementById('aiToggleButton'); const text = document.getElementById('aiToggleText'); if (enabled) { button.classList.remove('is-danger'); button.classList.add('is-success'); text.textContent = 'Deactivate AI'; } else { button.classList.remove('is-success'); button.classList.add('is-danger'); text.textContent = 'Activate AI'; } } // Open password modal function toggleAI() { document.getElementById('passwordModal').classList.add('is-active'); document.getElementById('aiPassword').value = ''; document.getElementById('passwordError').style.display = 'none'; // Focus on password input setTimeout(() => { document.getElementById('aiPassword').focus(); }, 100); } // Close password modal function closePasswordModal() { document.getElementById('passwordModal').classList.remove('is-active'); } // Submit password async function submitPassword() { const password = document.getElementById('aiPassword').value; const errorElement = document.getElementById('passwordError'); try { const response = await fetch('/api/toggle_ai', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ password: password }) }); const data = await response.json(); if (response.ok && data.success) { currentAIStatus = data.enabled; updateAIButton(currentAIStatus); closePasswordModal(); // Reload page to update AI button state on predict page if (typeof updateAIPredictButton === 'function') { updateAIPredictButton(currentAIStatus); } } else { errorElement.style.display = 'block'; } } catch (error) { console.error('Failed to toggle AI:', error); errorElement.textContent = 'Network error. Please try again.'; errorElement.style.display = 'block'; } } // Allow Enter key to submit password document.addEventListener('DOMContentLoaded', function() { checkAIStatus(); const passwordInput = document.getElementById('aiPassword'); if (passwordInput) { passwordInput.addEventListener('keypress', function(e) { if (e.key === 'Enter') { submitPassword(); } }); } }); </script> </body> </html>
forecast_app/templates/predict.html +34 −1 Original line number Diff line number Diff line Loading @@ -85,7 +85,7 @@ </button> </div> <div class="control"> <button type="button" class="button is-primary" onclick="fillWithAI()" id="aiButton"> <button type="button" class="button is-primary" onclick="fillWithAI()" id="aiButton" disabled> <span class="icon"><i class="fas fa-brain"></i></span> <span>Fill with AI Model</span> </button> Loading Loading @@ -168,6 +168,39 @@ const watches = {{ watches | tojson }}; const lastYear = {{ last_year | tojson }}; // Update AI button state based on AI activation function updateAIPredictButton(enabled) { const button = document.getElementById('aiButton'); if (button) { button.disabled = !enabled; if (enabled) { button.classList.remove('is-light'); button.classList.add('is-primary'); button.style.opacity = '1'; button.style.cursor = 'pointer'; } else { button.classList.remove('is-primary'); button.classList.add('is-light'); button.style.opacity = '0.5'; button.style.cursor = 'not-allowed'; } } } // Check AI status on page load async function checkAIStatusForPredict() { try { const response = await fetch('/api/ai_status'); const data = await response.json(); updateAIPredictButton(data.enabled); } catch (error) { console.error('Failed to check AI status:', error); } } // Call on page load document.addEventListener('DOMContentLoaded', checkAIStatusForPredict); function getInputs(watchId) { const inputs = []; for (let month = 1; month <= 12; month++) { Loading