|
|
<!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> |