| <!DOCTYPE html> |
| <html lang="en"> |
|
|
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Predictive Maintenance API - Test Interface</title> |
| <style> |
| * { |
| margin: 0; |
| padding: 0; |
| box-sizing: border-box; |
| } |
| |
| body { |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| min-height: 100vh; |
| padding: 20px; |
| } |
| |
| .container { |
| max-width: 1200px; |
| margin: 0 auto; |
| } |
| |
| .header { |
| text-align: center; |
| color: white; |
| margin-bottom: 30px; |
| } |
| |
| .header h1 { |
| font-size: 2.5em; |
| margin-bottom: 10px; |
| text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); |
| } |
| |
| .header p { |
| font-size: 1.1em; |
| opacity: 0.9; |
| } |
| |
| .tabs { |
| display: flex; |
| gap: 10px; |
| margin-bottom: 20px; |
| } |
| |
| .tab-button { |
| flex: 1; |
| padding: 15px; |
| background: white; |
| border: none; |
| border-radius: 10px 10px 0 0; |
| font-size: 1.1em; |
| font-weight: 600; |
| cursor: pointer; |
| transition: all 0.3s; |
| box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); |
| } |
| |
| .tab-button:hover { |
| transform: translateY(-2px); |
| box-shadow: 0 6px 8px rgba(0, 0, 0, 0.15); |
| } |
| |
| .tab-button.active { |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| color: white; |
| } |
| |
| .tab-content { |
| display: none; |
| background: white; |
| border-radius: 0 0 10px 10px; |
| padding: 30px; |
| box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2); |
| } |
| |
| .tab-content.active { |
| display: block; |
| animation: fadeIn 0.3s; |
| } |
| |
| @keyframes fadeIn { |
| from { |
| opacity: 0; |
| transform: translateY(10px); |
| } |
| |
| to { |
| opacity: 1; |
| transform: translateY(0); |
| } |
| } |
| |
| .form-grid { |
| display: grid; |
| grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); |
| gap: 20px; |
| margin-bottom: 20px; |
| } |
| |
| .form-group { |
| display: flex; |
| flex-direction: column; |
| } |
| |
| label { |
| font-weight: 600; |
| margin-bottom: 8px; |
| color: #333; |
| display: flex; |
| align-items: center; |
| gap: 5px; |
| } |
| |
| .info-icon { |
| font-size: 0.9em; |
| color: #667eea; |
| cursor: help; |
| } |
| |
| input, |
| select { |
| padding: 12px; |
| border: 2px solid #e0e0e0; |
| border-radius: 8px; |
| font-size: 1em; |
| transition: all 0.3s; |
| } |
| |
| input:focus, |
| select:focus { |
| outline: none; |
| border-color: #667eea; |
| box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); |
| } |
| |
| .button-group { |
| display: flex; |
| gap: 15px; |
| margin-top: 20px; |
| } |
| |
| button { |
| flex: 1; |
| padding: 15px 30px; |
| font-size: 1.1em; |
| font-weight: 600; |
| border: none; |
| border-radius: 8px; |
| cursor: pointer; |
| transition: all 0.3s; |
| box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); |
| } |
| |
| .btn-primary { |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| color: white; |
| } |
| |
| .btn-primary:hover { |
| transform: translateY(-2px); |
| box-shadow: 0 6px 12px rgba(102, 126, 234, 0.4); |
| } |
| |
| .btn-secondary { |
| background: #f0f0f0; |
| color: #333; |
| } |
| |
| .btn-secondary:hover { |
| background: #e0e0e0; |
| } |
| |
| .result-container { |
| margin-top: 30px; |
| padding: 25px; |
| border-radius: 10px; |
| display: none; |
| animation: slideIn 0.4s; |
| } |
| |
| @keyframes slideIn { |
| from { |
| opacity: 0; |
| transform: translateX(-20px); |
| } |
| |
| to { |
| opacity: 1; |
| transform: translateX(0); |
| } |
| } |
| |
| .result-container.success { |
| background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%); |
| border: 2px solid #667eea; |
| display: block; |
| } |
| |
| .result-container.error { |
| background: #fff0f0; |
| border: 2px solid #ff4444; |
| display: block; |
| } |
| |
| .prediction-header { |
| display: flex; |
| align-items: center; |
| gap: 15px; |
| margin-bottom: 20px; |
| padding-bottom: 15px; |
| border-bottom: 2px solid #e0e0e0; |
| } |
| |
| .prediction-badge { |
| padding: 10px 20px; |
| border-radius: 25px; |
| font-weight: 700; |
| font-size: 1.2em; |
| text-transform: uppercase; |
| } |
| |
| .badge-healthy { |
| background: #4caf50; |
| color: white; |
| } |
| |
| .badge-failure { |
| background: #ff4444; |
| color: white; |
| } |
| |
| .confidence-bar { |
| flex: 1; |
| height: 30px; |
| background: #e0e0e0; |
| border-radius: 15px; |
| overflow: hidden; |
| position: relative; |
| } |
| |
| .confidence-fill { |
| height: 100%; |
| background: linear-gradient(90deg, #667eea 0%, #764ba2 100%); |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| color: white; |
| font-weight: 600; |
| transition: width 0.5s ease; |
| } |
| |
| .diagnostics-grid { |
| display: grid; |
| gap: 15px; |
| } |
| |
| .diagnostic-item { |
| padding: 15px; |
| background: white; |
| border-radius: 8px; |
| border-left: 4px solid #667eea; |
| } |
| |
| .diagnostic-label { |
| font-weight: 600; |
| color: #667eea; |
| margin-bottom: 5px; |
| text-transform: uppercase; |
| font-size: 0.9em; |
| } |
| |
| .diagnostic-value { |
| color: #333; |
| line-height: 1.6; |
| } |
| |
| .severity-badge { |
| display: inline-block; |
| padding: 5px 15px; |
| border-radius: 15px; |
| font-weight: 600; |
| font-size: 0.9em; |
| } |
| |
| .severity-low { |
| background: #4caf50; |
| color: white; |
| } |
| |
| .severity-medium { |
| background: #ff9800; |
| color: white; |
| } |
| |
| .severity-high { |
| background: #ff5722; |
| color: white; |
| } |
| |
| .severity-critical { |
| background: #f44336; |
| color: white; |
| animation: pulse 1.5s infinite; |
| } |
| |
| @keyframes pulse { |
| |
| 0%, |
| 100% { |
| opacity: 1; |
| } |
| |
| 50% { |
| opacity: 0.7; |
| } |
| } |
| |
| .upload-area { |
| border: 3px dashed #667eea; |
| border-radius: 10px; |
| padding: 40px; |
| text-align: center; |
| cursor: pointer; |
| transition: all 0.3s; |
| background: #f8f9ff; |
| } |
| |
| .upload-area:hover { |
| background: #eef1ff; |
| border-color: #764ba2; |
| } |
| |
| .upload-area.dragging { |
| background: #e0e7ff; |
| border-color: #4f46e5; |
| } |
| |
| .upload-icon { |
| font-size: 3em; |
| color: #667eea; |
| margin-bottom: 15px; |
| } |
| |
| .file-name { |
| margin-top: 15px; |
| padding: 10px; |
| background: white; |
| border-radius: 8px; |
| font-weight: 600; |
| color: #667eea; |
| } |
| |
| .loading { |
| display: none; |
| text-align: center; |
| padding: 20px; |
| } |
| |
| .loading.active { |
| display: block; |
| } |
| |
| .spinner { |
| border: 4px solid #f3f3f3; |
| border-top: 4px solid #667eea; |
| border-radius: 50%; |
| width: 50px; |
| height: 50px; |
| animation: spin 1s linear infinite; |
| margin: 0 auto 15px; |
| } |
| |
| @keyframes spin { |
| 0% { |
| transform: rotate(0deg); |
| } |
| |
| 100% { |
| transform: rotate(360deg); |
| } |
| } |
| |
| .preset-buttons { |
| display: flex; |
| gap: 10px; |
| margin-bottom: 20px; |
| flex-wrap: wrap; |
| } |
| |
| .preset-btn { |
| padding: 8px 16px; |
| background: #f0f0f0; |
| border: 2px solid #667eea; |
| border-radius: 20px; |
| cursor: pointer; |
| transition: all 0.3s; |
| font-size: 0.9em; |
| font-weight: 600; |
| color: #667eea; |
| } |
| |
| .preset-btn:hover { |
| background: #667eea; |
| color: white; |
| } |
| |
| .api-status { |
| position: fixed; |
| top: 20px; |
| right: 20px; |
| padding: 10px 20px; |
| background: white; |
| border-radius: 25px; |
| box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); |
| display: flex; |
| align-items: center; |
| gap: 10px; |
| font-weight: 600; |
| } |
| |
| .status-dot { |
| width: 12px; |
| height: 12px; |
| border-radius: 50%; |
| animation: blink 2s infinite; |
| } |
| |
| .status-online { |
| background: #4caf50; |
| } |
| |
| .status-offline { |
| background: #ff4444; |
| } |
| |
| @keyframes blink { |
| |
| 0%, |
| 100% { |
| opacity: 1; |
| } |
| |
| 50% { |
| opacity: 0.5; |
| } |
| } |
| |
| @media (max-width: 768px) { |
| .form-grid { |
| grid-template-columns: 1fr; |
| } |
| |
| .button-group { |
| flex-direction: column; |
| } |
| |
| .header h1 { |
| font-size: 1.8em; |
| } |
| } |
| </style> |
| </head> |
|
|
| <body> |
| <div class="api-status" id="apiStatus"> |
| <div class="status-dot status-offline" id="statusDot"></div> |
| <span id="statusText">Checking...</span> |
| </div> |
|
|
| <div class="container"> |
| <div class="header"> |
| <h1>🔧 Predictive Maintenance</h1> |
| <p>ML-Powered Equipment Failure Prediction & Diagnostics</p> |
| </div> |
|
|
| <div class="tabs"> |
| <button class="tab-button active" onclick="switchTab('single')"> |
| 📊 Single Prediction |
| </button> |
| <button class="tab-button" onclick="switchTab('batch')"> |
| 📁 Batch Processing |
| </button> |
| </div> |
|
|
| |
| <div id="singleTab" class="tab-content active"> |
| <h2 style="margin-bottom: 20px; color: #333;">🎯 Single Machine Prediction</h2> |
|
|
| <div class="preset-buttons"> |
| <button class="preset-btn" onclick="loadPreset('healthy')">✅ Healthy Machine</button> |
| <button class="preset-btn" onclick="loadPreset('toolwear')">🔨 Tool Wear Critical</button> |
| <button class="preset-btn" onclick="loadPreset('overstrain')">⚡ Power Overstrain</button> |
| <button class="preset-btn" onclick="loadPreset('cooling')">❄️ Cooling Issue</button> |
| </div> |
|
|
| <form id="predictionForm" onsubmit="handleSinglePredict(event)"> |
| <div class="form-grid"> |
| <div class="form-group"> |
| <label for="machine_id"> |
| Machine ID <span class="info-icon" title="Optional identifier">ℹ️</span> |
| </label> |
| <input type="text" id="machine_id" placeholder="e.g., M_L_01"> |
| </div> |
|
|
| <div class="form-group"> |
| <label for="type"> |
| Machine Type <span class="info-icon" title="L=Low, M=Medium, H=High">ℹ️</span> |
| </label> |
| <select id="type" required> |
| <option value="">Select Type</option> |
| <option value="L">L - Low Quality</option> |
| <option value="M">M - Medium Quality</option> |
| <option value="H">H - High Quality</option> |
| </select> |
| </div> |
|
|
| <div class="form-group"> |
| <label for="air_temperature"> |
| Air Temperature (K) <span class="info-icon" title="Range: 250-350 K">ℹ️</span> |
| </label> |
| <input type="number" id="air_temperature" step="0.1" min="250" max="350" required |
| placeholder="e.g., 298.1"> |
| </div> |
|
|
| <div class="form-group"> |
| <label for="process_temperature"> |
| Process Temperature (K) <span class="info-icon" title="Range: 250-400 K">ℹ️</span> |
| </label> |
| <input type="number" id="process_temperature" step="0.1" min="250" max="400" required |
| placeholder="e.g., 308.6"> |
| </div> |
|
|
| <div class="form-group"> |
| <label for="rotational_speed"> |
| Rotational Speed (RPM) <span class="info-icon" title="Range: 0-3000 RPM">ℹ️</span> |
| </label> |
| <input type="number" id="rotational_speed" min="0" max="3000" required placeholder="e.g., 1551"> |
| </div> |
|
|
| <div class="form-group"> |
| <label for="torque"> |
| Torque (Nm) <span class="info-icon" title="Range: 0-100 Nm">ℹ️</span> |
| </label> |
| <input type="number" id="torque" step="0.1" min="0" max="100" required placeholder="e.g., 42.8"> |
| </div> |
|
|
| <div class="form-group"> |
| <label for="tool_wear"> |
| Tool Wear (min) <span class="info-icon" title="Range: 0-300 minutes">ℹ️</span> |
| </label> |
| <input type="number" id="tool_wear" min="0" max="300" required placeholder="e.g., 0"> |
| </div> |
| </div> |
|
|
| <div class="button-group"> |
| <button type="submit" class="btn-primary">🔍 Analyze & Predict</button> |
| <button type="button" class="btn-secondary" onclick="resetForm()">🔄 Reset Form</button> |
| </div> |
| </form> |
|
|
| <div class="loading" id="loading"> |
| <div class="spinner"></div> |
| <p>Analyzing machine data...</p> |
| </div> |
|
|
| <div id="result" class="result-container"></div> |
| </div> |
|
|
| |
| <div id="batchTab" class="tab-content"> |
| <h2 style="margin-bottom: 20px; color: #333;">📁 Batch Processing</h2> |
|
|
| <div class="upload-area" id="uploadArea" onclick="document.getElementById('csvFile').click()"> |
| <div class="upload-icon">📤</div> |
| <h3>Click or Drag & Drop CSV File</h3> |
| <p style="margin-top: 10px; color: #666;">Upload a CSV file with machine data for batch prediction</p> |
| <input type="file" id="csvFile" accept=".csv" style="display: none;" onchange="handleFileSelect(event)"> |
| <div id="fileName" class="file-name" style="display: none;"></div> |
| </div> |
|
|
| <div style="margin-top: 20px; padding: 20px; background: #f8f9ff; border-radius: 10px;"> |
| <h4 style="margin-bottom: 10px;">📋 Required CSV Columns:</h4> |
| <ul style="list-style: none; padding-left: 0;"> |
| <li>✓ <code>air_temperature</code> - Air temperature in Kelvin</li> |
| <li>✓ <code>process_temperature</code> - Process temperature in Kelvin</li> |
| <li>✓ <code>rotational_speed</code> - Rotational speed in RPM</li> |
| <li>✓ <code>torque</code> - Torque in Nm</li> |
| <li>✓ <code>tool_wear</code> - Tool wear in minutes</li> |
| <li>✓ <code>type</code> - Machine type (L, M, or H)</li> |
| <li>⭕ <code>machine_id</code> - Optional machine identifier</li> |
| </ul> |
| </div> |
|
|
| <div class="button-group" style="margin-top: 20px;"> |
| <button class="btn-primary" onclick="uploadBatch()" id="uploadBtn" disabled>📊 Process Batch</button> |
| <button class="btn-secondary" onclick="downloadTemplate()">⬇️ Download Template CSV</button> |
| </div> |
|
|
| <div class="loading" id="batchLoading"> |
| <div class="spinner"></div> |
| <p>Processing batch predictions...</p> |
| </div> |
|
|
| <div id="batchResult" class="result-container"></div> |
| </div> |
| </div> |
|
|
| <script> |
| const API_URL = 'http://localhost:8000'; |
| let selectedFile = null; |
| |
| |
| async function checkApiStatus() { |
| try { |
| const response = await fetch(`${API_URL}/health`); |
| const data = await response.json(); |
| document.getElementById('statusDot').className = 'status-dot status-online'; |
| document.getElementById('statusText').textContent = 'API Online'; |
| } catch (error) { |
| document.getElementById('statusDot').className = 'status-dot status-offline'; |
| document.getElementById('statusText').textContent = 'API Offline'; |
| } |
| } |
| |
| |
| function switchTab(tab) { |
| document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active')); |
| document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active')); |
| |
| if (tab === 'single') { |
| document.querySelector('.tab-button:nth-child(1)').classList.add('active'); |
| document.getElementById('singleTab').classList.add('active'); |
| } else { |
| document.querySelector('.tab-button:nth-child(2)').classList.add('active'); |
| document.getElementById('batchTab').classList.add('active'); |
| } |
| } |
| |
| |
| function loadPreset(type) { |
| const presets = { |
| healthy: { |
| machine_id: 'M_L_01', |
| type: 'M', |
| air_temperature: 298.1, |
| process_temperature: 308.6, |
| rotational_speed: 1551, |
| torque: 42.8, |
| tool_wear: 0 |
| }, |
| toolwear: { |
| machine_id: 'M_H_03', |
| type: 'H', |
| air_temperature: 297.5, |
| process_temperature: 312.8, |
| rotational_speed: 1800, |
| torque: 52.0, |
| tool_wear: 220 |
| }, |
| overstrain: { |
| machine_id: 'M_H_05', |
| type: 'H', |
| air_temperature: 298.5, |
| process_temperature: 316.0, |
| rotational_speed: 1900, |
| torque: 72.5, |
| tool_wear: 150 |
| }, |
| cooling: { |
| machine_id: 'M_M_02', |
| type: 'M', |
| air_temperature: 299.8, |
| process_temperature: 305.2, |
| rotational_speed: 1650, |
| torque: 45.0, |
| tool_wear: 100 |
| } |
| }; |
| |
| const preset = presets[type]; |
| Object.keys(preset).forEach(key => { |
| const element = document.getElementById(key); |
| if (element) element.value = preset[key]; |
| }); |
| } |
| |
| |
| async function handleSinglePredict(event) { |
| event.preventDefault(); |
| |
| const formData = { |
| machine_id: document.getElementById('machine_id').value || null, |
| type: document.getElementById('type').value, |
| air_temperature: parseFloat(document.getElementById('air_temperature').value), |
| process_temperature: parseFloat(document.getElementById('process_temperature').value), |
| rotational_speed: parseInt(document.getElementById('rotational_speed').value), |
| torque: parseFloat(document.getElementById('torque').value), |
| tool_wear: parseInt(document.getElementById('tool_wear').value) |
| }; |
| |
| document.getElementById('loading').classList.add('active'); |
| document.getElementById('result').style.display = 'none'; |
| |
| try { |
| console.log('Sending request to API:', formData); |
| |
| const response = await fetch(`${API_URL}/predict`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify(formData) |
| }); |
| |
| console.log('Response status:', response.status); |
| |
| const data = await response.json(); |
| console.log('Response data:', data); |
| |
| if (!response.ok) { |
| throw new Error(data.detail || 'Prediction failed'); |
| } |
| |
| displayResult(data); |
| } catch (error) { |
| console.error('Error during prediction:', error); |
| displayError(error.message); |
| } finally { |
| document.getElementById('loading').classList.remove('active'); |
| } |
| } |
| |
| |
| function displayResult(data) { |
| console.log('Displaying result:', data); |
| |
| const resultDiv = document.getElementById('result'); |
| const isFailure = data.prediction === 'FAILURE'; |
| const severityClass = `severity-${data.diagnostics.severity.toLowerCase()}`; |
| |
| |
| const probability = (data.confidence * 100).toFixed(1); |
| |
| resultDiv.className = 'result-container success'; |
| resultDiv.style.display = 'block'; |
| resultDiv.innerHTML = ` |
| <h3 style="margin-bottom: 15px;">🔍 Prediction Result</h3> |
| |
| <!-- Prediction Badge and Confidence Bar --> |
| <div class="prediction-header"> |
| <span class="prediction-badge ${isFailure ? 'badge-failure' : 'badge-healthy'}"> |
| ${data.prediction} |
| </span> |
| <div class="confidence-bar"> |
| <div class="confidence-fill" style="width: ${probability}%"> |
| ${probability}% Confidence |
| </div> |
| </div> |
| </div> |
| |
| <!-- Severity and Risk Level --> |
| <div style="display: flex; gap: 10px; margin: 15px 0;"> |
| <span class="severity-badge ${severityClass}" style="font-size: 14px; padding: 8px 16px;"> |
| ${data.diagnostics.severity} SEVERITY |
| </span> |
| </div> |
| |
| <!-- Main Diagnostics Grid --> |
| <div class="diagnostics-grid"> |
| <div class="diagnostic-item"> |
| <div class="diagnostic-label">🎯 Primary Cause</div> |
| <div class="diagnostic-value">${data.diagnostics.primary_cause}</div> |
| </div> |
| |
| <div class="diagnostic-item"> |
| <div class="diagnostic-label">📊 Sensor Alert</div> |
| <div class="diagnostic-value">${data.diagnostics.sensor_alert}</div> |
| </div> |
| |
| <div class="diagnostic-item"> |
| <div class="diagnostic-label">🔧 Recommended Action</div> |
| <div class="diagnostic-value">${data.diagnostics.recommended_action}</div> |
| </div> |
| |
| ${data.machine_id ? ` |
| <div class="diagnostic-item"> |
| <div class="diagnostic-label">🏷️ Machine ID</div> |
| <div class="diagnostic-value">${data.machine_id}</div> |
| </div> |
| ` : ''} |
| |
| <div class="diagnostic-item"> |
| <div class="diagnostic-label">⏰ Timestamp</div> |
| <div class="diagnostic-value">${new Date(data.timestamp).toLocaleString()}</div> |
| </div> |
| </div> |
| |
| <!-- Engineered Features Section --> |
| ${data.features && Object.keys(data.features).length > 0 ? ` |
| <div style="margin-top: 25px;"> |
| <h4 style="margin-bottom: 15px; color: #667eea;">📊 Engineered Features Analysis</h4> |
| <div class="feature-grid" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px;"> |
| ${Object.entries(data.features).map(([key, value]) => ` |
| <div class="feature-item" style="background: white; padding: 12px; border-radius: 8px; border-left: 3px solid #667eea;"> |
| <div class="feature-label" style="font-size: 11px; color: #6c757d; margin-bottom: 4px; text-transform: uppercase;"> |
| ${key.replace(/_/g, ' ')} |
| </div> |
| <div class="feature-value" style="font-size: 16px; font-weight: 600; color: #333;"> |
| ${typeof value === 'number' ? value.toFixed(2) : value} |
| </div> |
| </div> |
| `).join('')} |
| </div> |
| </div> |
| ` : ''} |
| |
| <!-- Anomaly Detection Section (if available) --> |
| ${data.anomalies && data.anomalies.length > 0 ? ` |
| <div style="margin-top: 25px; padding: 20px; background: #f8f9fa; border-radius: 10px; border-left: 5px solid #667eea;"> |
| <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;"> |
| <h4 style="margin: 0; color: #667eea;">🔧 Parameter Anomaly Analysis</h4> |
| <span style="color: #667eea; font-weight: 600; font-size: 14px;"> |
| ${data.anomalies.length} anomalies detected |
| </span> |
| </div> |
| <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 15px;"> |
| ${data.anomalies.map(anomaly => ` |
| <div style="background: white; padding: 15px; border-radius: 10px; border-left: 4px solid ${anomaly.status === 'CRITICAL' ? '#dc3545' : anomaly.status === 'WARNING' ? '#ffc107' : '#28a745'};"> |
| <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;"> |
| <strong style="font-size: 16px;">${anomaly.parameter}</strong> |
| <span style="font-size: 12px; padding: 4px 8px; border-radius: 10px; font-weight: 600; background: ${anomaly.status === 'CRITICAL' ? '#f8d7da' : anomaly.status === 'WARNING' ? '#fff3cd' : '#d4edda'}; color: ${anomaly.status === 'CRITICAL' ? '#721c24' : anomaly.status === 'WARNING' ? '#856404' : '#155724'};"> |
| ${anomaly.status} |
| </span> |
| </div> |
| <div style="font-size: 18px; font-weight: 600; color: #333; margin: 5px 0;"> |
| Current: ${anomaly.value} |
| </div> |
| <div style="font-size: 12px; color: #6c757d; margin: 5px 0;"> |
| Normal range: ${anomaly.normal_range} |
| </div> |
| <div style="font-size: 13px; color: #495057; margin-top: 10px; padding: 8px; background: #f1f3f4; border-radius: 6px;"> |
| ${anomaly.explanation} |
| </div> |
| </div> |
| `).join('')} |
| </div> |
| </div> |
| ` : ''} |
| |
| <!-- Overall Health Summary (if available) --> |
| ${data.overall_health ? ` |
| <div style="margin-top: 20px; padding: 15px; border-radius: 10px; font-weight: 600; text-align: center; background: ${data.overall_health.includes('EXCELLENT') ? '#d4edda' : data.overall_health.includes('GOOD') ? '#d1ecf1' : data.overall_health.includes('FAIR') ? '#fff3cd' : '#f8d7da'}; color: ${data.overall_health.includes('EXCELLENT') ? '#155724' : data.overall_health.includes('GOOD') ? '#0c5460' : data.overall_health.includes('FAIR') ? '#856404' : '#721c24'}; border: 2px solid ${data.overall_health.includes('EXCELLENT') ? '#28a745' : data.overall_health.includes('GOOD') ? '#17a2b8' : data.overall_health.includes('FAIR') ? '#ffc107' : '#dc3545'};"> |
| 🏥 ${data.overall_health} |
| </div> |
| ` : ''} |
| `; |
| } |
| |
| |
| function displayError(message) { |
| console.error('Displaying error:', message); |
| |
| const resultDiv = document.getElementById('result'); |
| resultDiv.className = 'result-container error'; |
| resultDiv.style.display = 'block'; |
| resultDiv.innerHTML = ` |
| <h3 style="color: #ff4444; margin-bottom: 10px;">❌ Error</h3> |
| <p>${message}</p> |
| `; |
| } |
| |
| |
| function resetForm() { |
| document.getElementById('predictionForm').reset(); |
| document.getElementById('result').style.display = 'none'; |
| } |
| |
| |
| const uploadArea = document.getElementById('uploadArea'); |
| |
| ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { |
| uploadArea.addEventListener(eventName, preventDefaults, false); |
| }); |
| |
| function preventDefaults(e) { |
| e.preventDefault(); |
| e.stopPropagation(); |
| } |
| |
| ['dragenter', 'dragover'].forEach(eventName => { |
| uploadArea.addEventListener(eventName, () => { |
| uploadArea.classList.add('dragging'); |
| }, false); |
| }); |
| |
| ['dragleave', 'drop'].forEach(eventName => { |
| uploadArea.addEventListener(eventName, () => { |
| uploadArea.classList.remove('dragging'); |
| }, false); |
| }); |
| |
| uploadArea.addEventListener('drop', (e) => { |
| const files = e.dataTransfer.files; |
| if (files.length > 0) { |
| handleFile(files[0]); |
| } |
| }, false); |
| |
| function handleFileSelect(event) { |
| const file = event.target.files[0]; |
| if (file) { |
| handleFile(file); |
| } |
| } |
| |
| function handleFile(file) { |
| if (!file.name.endsWith('.csv')) { |
| alert('Please upload a CSV file'); |
| return; |
| } |
| |
| selectedFile = file; |
| document.getElementById('fileName').style.display = 'block'; |
| document.getElementById('fileName').textContent = `📄 ${file.name}`; |
| document.getElementById('uploadBtn').disabled = false; |
| } |
| |
| |
| async function uploadBatch() { |
| if (!selectedFile) return; |
| |
| const formData = new FormData(); |
| formData.append('file', selectedFile); |
| |
| document.getElementById('batchLoading').classList.add('active'); |
| document.getElementById('batchResult').style.display = 'none'; |
| |
| try { |
| console.log('Uploading batch file:', selectedFile.name); |
| |
| const response = await fetch(`${API_URL}/predict/batch/json`, { |
| method: 'POST', |
| body: formData |
| }); |
| |
| if (!response.ok) { |
| const error = await response.json(); |
| throw new Error(error.detail || 'Batch processing failed'); |
| } |
| |
| const data = await response.json(); |
| console.log('Batch response:', data); |
| |
| displayBatchResults(data); |
| |
| } catch (error) { |
| console.error('Batch processing error:', error); |
| const resultDiv = document.getElementById('batchResult'); |
| resultDiv.className = 'result-container error'; |
| resultDiv.style.display = 'block'; |
| resultDiv.innerHTML = ` |
| <h3 style="color: #ff4444; margin-bottom: 10px;">❌ Batch Processing Error</h3> |
| <p>${error.message}</p> |
| <p style="margin-top: 10px; font-size: 0.9em; color: #666;"> |
| Make sure your CSV has the required columns: air_temperature, process_temperature, |
| rotational_speed, torque, tool_wear, type |
| </p> |
| `; |
| } finally { |
| document.getElementById('batchLoading').classList.remove('active'); |
| } |
| } |
| |
| |
| function displayBatchResults(data) { |
| const resultDiv = document.getElementById('batchResult'); |
| resultDiv.className = 'result-container success'; |
| resultDiv.style.display = 'block'; |
| |
| const summary = data.summary; |
| const results = data.results; |
| |
| let html = ` |
| <h3 style="margin-bottom: 20px;">📊 Batch Processing Results</h3> |
| |
| <!-- Summary Statistics --> |
| <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 15px; margin-bottom: 25px;"> |
| <div style="background: white; padding: 20px; border-radius: 10px; text-align: center; border-left: 4px solid #667eea;"> |
| <div style="font-size: 32px; font-weight: 700; color: #667eea;">${summary.total_records}</div> |
| <div style="font-size: 14px; color: #666; margin-top: 5px;">Total Records</div> |
| </div> |
| <div style="background: white; padding: 20px; border-radius: 10px; text-align: center; border-left: 4px solid #4caf50;"> |
| <div style="font-size: 32px; font-weight: 700; color: #4caf50;">${summary.predictions.healthy}</div> |
| <div style="font-size: 14px; color: #666; margin-top: 5px;">Healthy</div> |
| </div> |
| <div style="background: white; padding: 20px; border-radius: 10px; text-align: center; border-left: 4px solid #ff4444;"> |
| <div style="font-size: 32px; font-weight: 700; color: #ff4444;">${summary.predictions.failure}</div> |
| <div style="font-size: 14px; color: #666; margin-top: 5px;">Failures</div> |
| </div> |
| <div style="background: white; padding: 20px; border-radius: 10px; text-align: center; border-left: 4px solid #ff9800;"> |
| <div style="font-size: 32px; font-weight: 700; color: #ff9800;">${summary.failure_rate}%</div> |
| <div style="font-size: 14px; color: #666; margin-top: 5px;">Failure Rate</div> |
| </div> |
| </div> |
| |
| <!-- Individual Results --> |
| <h4 style="margin: 25px 0 15px 0; color: #333;">🔍 Detailed Analysis</h4> |
| <div style="display: flex; flex-direction: column; gap: 20px;"> |
| `; |
| |
| |
| results.forEach((result, index) => { |
| const isFailure = result.prediction === 'FAILURE'; |
| const severityClass = result.diagnostics ? `severity-${result.diagnostics.severity.toLowerCase()}` : ''; |
| const probability = (result.confidence * 100).toFixed(1); |
| |
| html += ` |
| <div style="background: white; padding: 20px; border-radius: 10px; border-left: 5px solid ${isFailure ? '#ff4444' : '#4caf50'};"> |
| <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; padding-bottom: 15px; border-bottom: 2px solid #e0e0e0;"> |
| <div style="display: flex; align-items: center; gap: 15px;"> |
| <span style="font-size: 14px; font-weight: 600; color: #666;">Row ${result.row_number}</span> |
| <span style="font-size: 14px; font-weight: 600; color: #667eea;">${result.machine_id}</span> |
| <span class="prediction-badge ${isFailure ? 'badge-failure' : 'badge-healthy'}" style="font-size: 14px; padding: 6px 14px;"> |
| ${result.prediction} |
| </span> |
| ${result.diagnostics ? ` |
| <span class="severity-badge ${severityClass}" style="font-size: 12px; padding: 4px 10px;"> |
| ${result.diagnostics.severity} |
| </span> |
| ` : ''} |
| </div> |
| <div style="background: #e0e0e0; height: 20px; width: 120px; border-radius: 10px; overflow: hidden;"> |
| <div style="height: 100%; background: linear-gradient(90deg, #667eea 0%, #764ba2 100%); width: ${probability}%; display: flex; align-items: center; justify-content: center; font-size: 11px; color: white; font-weight: 600;"> |
| ${probability}% |
| </div> |
| </div> |
| </div> |
| |
| ${result.diagnostics ? ` |
| <div style="display: grid; grid-template-columns: 1fr 2fr; gap: 15px; margin-bottom: 15px;"> |
| <div style="padding: 12px; background: #f8f9fa; border-radius: 8px;"> |
| <div style="font-size: 11px; color: #667eea; font-weight: 600; margin-bottom: 5px; text-transform: uppercase;">🎯 Primary Cause</div> |
| <div style="font-size: 13px; color: #333;">${result.diagnostics.primary_cause}</div> |
| </div> |
| <div style="padding: 12px; background: #f8f9fa; border-radius: 8px;"> |
| <div style="font-size: 11px; color: #667eea; font-weight: 600; margin-bottom: 5px; text-transform: uppercase;">🔧 Recommended Action</div> |
| <div style="font-size: 13px; color: #333;">${result.diagnostics.recommended_action}</div> |
| </div> |
| </div> |
| ` : ''} |
| |
| ${result.anomalies && result.anomalies.length > 0 ? ` |
| <div style="margin-top: 15px; padding: 15px; background: #f8f9fa; border-radius: 8px;"> |
| <div style="font-size: 12px; color: #667eea; font-weight: 600; margin-bottom: 10px;"> |
| ⚠️ ${result.anomalies.length} Anomalies Detected |
| </div> |
| <div style="display: flex; flex-wrap: wrap; gap: 10px;"> |
| ${result.anomalies.map(anomaly => ` |
| <span style="font-size: 11px; padding: 4px 8px; border-radius: 12px; background: ${anomaly.status === 'CRITICAL' ? '#f8d7da' : anomaly.status === 'WARNING' ? '#fff3cd' : '#d4edda'}; color: ${anomaly.status === 'CRITICAL' ? '#721c24' : anomaly.status === 'WARNING' ? '#856404' : '#155724'};"> |
| ${anomaly.parameter} |
| </span> |
| `).join('')} |
| </div> |
| </div> |
| ` : ''} |
| |
| ${result.overall_health ? ` |
| <div style="margin-top: 15px; padding: 10px; border-radius: 8px; text-align: center; font-size: 13px; font-weight: 600; background: ${result.overall_health.includes('EXCELLENT') ? '#d4edda' : result.overall_health.includes('GOOD') ? '#d1ecf1' : result.overall_health.includes('FAIR') ? '#fff3cd' : '#f8d7da'}; color: ${result.overall_health.includes('EXCELLENT') ? '#155724' : result.overall_health.includes('GOOD') ? '#0c5460' : result.overall_health.includes('FAIR') ? '#856404' : '#721c24'};"> |
| ${result.overall_health} |
| </div> |
| ` : ''} |
| |
| ${result.error ? ` |
| <div style="margin-top: 10px; padding: 10px; background: #fff0f0; border-radius: 8px; color: #ff4444; font-size: 13px;"> |
| <strong>Error:</strong> ${result.error} |
| </div> |
| ` : ''} |
| </div> |
| `; |
| }); |
| |
| html += ` |
| </div> |
| |
| <!-- Download CSV Button --> |
| <div style="margin-top: 25px; text-align: center;"> |
| <button onclick="downloadBatchCSV()" class="btn-secondary" style="padding: 12px 24px;"> |
| ⬇️ Download Results as CSV |
| </button> |
| </div> |
| `; |
| |
| resultDiv.innerHTML = html; |
| } |
| |
| |
| async function downloadBatchCSV() { |
| if (!selectedFile) return; |
| |
| try { |
| const formData = new FormData(); |
| formData.append('file', selectedFile); |
| |
| const response = await fetch(`${API_URL}/predict/batch`, { |
| method: 'POST', |
| body: formData |
| }); |
| |
| if (!response.ok) { |
| throw new Error('CSV download failed'); |
| } |
| |
| |
| const blob = await response.blob(); |
| const url = window.URL.createObjectURL(blob); |
| const a = document.createElement('a'); |
| a.href = url; |
| a.download = `predictions_${new Date().getTime()}.csv`; |
| document.body.appendChild(a); |
| a.click(); |
| window.URL.revokeObjectURL(url); |
| document.body.removeChild(a); |
| } catch (error) { |
| alert('Failed to download CSV: ' + error.message); |
| } |
| } |
| |
| |
| function downloadTemplate() { |
| const template = `machine_id,air_temperature,process_temperature,rotational_speed,torque,tool_wear,type |
| M_L_01,298.1,308.6,1551,42.8,0,M |
| M_L_02,299.2,310.1,1450,38.5,150,L |
| M_H_01,297.5,312.8,1800,65.2,220,H`; |
| |
| const blob = new Blob([template], { type: 'text/csv' }); |
| const url = window.URL.createObjectURL(blob); |
| const a = document.createElement('a'); |
| a.href = url; |
| a.download = 'template.csv'; |
| document.body.appendChild(a); |
| a.click(); |
| window.URL.revokeObjectURL(url); |
| document.body.removeChild(a); |
| } |
| |
| |
| checkApiStatus(); |
| setInterval(checkApiStatus, 10000); |
| </script> |
| </body> |
|
|
| </html> |