658 lines
26 KiB
HTML
658 lines
26 KiB
HTML
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<title>MIRA - Training</title>
|
||
<link href="https://fonts.googleapis.com/css2?family=Dangrek&family=Poppins:wght@400;600;700&display=swap" rel="stylesheet">
|
||
<style>
|
||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
|
||
body {
|
||
font-family: 'Poppins', sans-serif;
|
||
background: #6D60B4;
|
||
min-height: 100vh;
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 20px;
|
||
}
|
||
|
||
.wrapper {
|
||
display: flex;
|
||
width: 100%;
|
||
min-height: calc(100vh - 40px);
|
||
}
|
||
|
||
.sidebar {
|
||
width: 170px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
padding: 32px 14px 24px;
|
||
gap: 40px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.brand { text-align: center; color: white; }
|
||
.brand h2 {
|
||
font-family: 'Poppins', sans-serif;
|
||
font-size: 26px; font-weight: 700; line-height: 1; letter-spacing: 2px;
|
||
}
|
||
.brand p {
|
||
font-size: 10px; line-height: 1.5;
|
||
margin-top: 7px; opacity: 0.9;
|
||
}
|
||
|
||
.nav { display: flex; flex-direction: column; gap: 10px; width: 100%; }
|
||
|
||
.nav-item {
|
||
display: flex; align-items: center; gap: 10px;
|
||
padding: 10px 10px 10px 16px;
|
||
color: rgba(255,255,255,0.6);
|
||
font-size: 14px; font-weight: 600;
|
||
text-decoration: none; position: relative;
|
||
}
|
||
.nav-item.active { color: white; }
|
||
.nav-item.active::before {
|
||
content: ''; position: absolute; left: 0;
|
||
top: 50%; transform: translateY(-50%);
|
||
width: 4px; height: 28px;
|
||
background: white; border-radius: 0 3px 3px 0;
|
||
}
|
||
|
||
.icon-grid {
|
||
width: 22px; height: 22px; display: inline-block; flex-shrink: 0;
|
||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='white'%3E%3Crect x='3' y='3' width='8' height='8' rx='1'/%3E%3Crect x='13' y='3' width='8' height='8' rx='1'/%3E%3Crect x='3' y='13' width='8' height='8' rx='1'/%3E%3Crect x='13' y='13' width='8' height='8' rx='1'/%3E%3C/svg%3E");
|
||
background-size: contain; background-repeat: no-repeat;
|
||
}
|
||
.icon-money {
|
||
width: 22px; height: 22px; display: inline-block; flex-shrink: 0;
|
||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='white'%3E%3Cpath d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z'/%3E%3C/svg%3E");
|
||
background-size: contain; background-repeat: no-repeat;
|
||
}
|
||
.icon-train {
|
||
width: 22px; height: 22px; display: inline-block; flex-shrink: 0;
|
||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='white'%3E%3Cpath d='M19 3H5c-1.1 0-2 .9-2 2v14l4-4h12c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-5 9l-3-2.25V12H7V6h4v2.25L14 6l5 3-5 3z'/%3E%3C/svg%3E");
|
||
background-size: contain; background-repeat: no-repeat;
|
||
}
|
||
|
||
.main-wrap { flex: 1; display: flex; }
|
||
|
||
.content-area {
|
||
flex: 1; background: #FFFFFF;
|
||
border-radius: 24px;
|
||
padding: 32px 36px 36px;
|
||
display: flex; flex-direction: column;
|
||
overflow-y: auto;
|
||
max-height: calc(100vh - 40px);
|
||
}
|
||
|
||
.page-title {
|
||
font-size: 24px; font-weight: 700;
|
||
color: #6D60B4; margin-bottom: 28px;
|
||
}
|
||
|
||
.step-card {
|
||
background: #F4F2FC;
|
||
border-radius: 18px;
|
||
border: 1.5px solid #D4CFEE;
|
||
padding: 22px 26px;
|
||
margin-bottom: 18px;
|
||
}
|
||
|
||
.step-header {
|
||
display: flex; align-items: center; gap: 12px;
|
||
margin-bottom: 14px;
|
||
}
|
||
|
||
.step-badge {
|
||
background: #6D60B4; color: white;
|
||
font-size: 12px; font-weight: 700;
|
||
width: 28px; height: 28px; border-radius: 50%;
|
||
display: flex; align-items: center; justify-content: center;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.step-title { font-size: 15px; font-weight: 700; color: #4a3a9a; }
|
||
.step-desc { font-size: 12px; color: #7a7a9a; margin-top: 2px; }
|
||
|
||
.step-status {
|
||
margin-left: auto;
|
||
font-size: 11px; font-weight: 600;
|
||
padding: 4px 12px; border-radius: 20px;
|
||
}
|
||
.status-wait { background: #e8e6f6; color: #8880c4; }
|
||
.status-ok { background: #d4f4e2; color: #2e7d52; }
|
||
.status-err { background: #fde8e8; color: #b94040; }
|
||
.status-run { background: #fff3cd; color: #8a6400; }
|
||
|
||
.form-row {
|
||
display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
|
||
}
|
||
|
||
.btn {
|
||
background: #6D60B4; color: white;
|
||
border: none; border-radius: 9px;
|
||
padding: 9px 22px;
|
||
font-family: 'Poppins', sans-serif;
|
||
font-size: 13px; font-weight: 600;
|
||
cursor: pointer; white-space: nowrap;
|
||
}
|
||
.btn:hover { background: #5c51a0; }
|
||
.btn:disabled { background: #b0aad4; cursor: not-allowed; }
|
||
.btn-green { background: #3a7d52; }
|
||
.btn-green:hover { background: #2e6442; }
|
||
|
||
.file-label {
|
||
background: #6D60B4; color: white;
|
||
font-size: 12px; padding: 8px 18px;
|
||
border-radius: 9px; cursor: pointer; font-weight: 600;
|
||
}
|
||
.file-label:hover { background: #5c51a0; }
|
||
input[type="file"] { display: none; }
|
||
|
||
.file-name { font-size: 12px; color: #4a3a9a; }
|
||
|
||
.input-small {
|
||
font-family: 'Poppins', sans-serif;
|
||
border: 1.5px solid #C4BFDF;
|
||
border-radius: 8px;
|
||
padding: 7px 12px;
|
||
font-size: 13px;
|
||
color: #4a3a9a;
|
||
width: 90px;
|
||
background: white;
|
||
}
|
||
|
||
.input-label { font-size: 12px; color: #6a6a9a; font-weight: 600; }
|
||
|
||
.log-box {
|
||
background: #1e1e2e;
|
||
color: #c8c0ff;
|
||
font-family: 'Courier New', monospace;
|
||
font-size: 12px;
|
||
border-radius: 12px;
|
||
padding: 16px;
|
||
height: 280px;
|
||
overflow-y: auto;
|
||
white-space: pre-wrap;
|
||
word-break: break-all;
|
||
line-height: 1.6;
|
||
display: none;
|
||
margin-top: 14px;
|
||
}
|
||
.log-box.visible { display: block; }
|
||
|
||
.result-info {
|
||
font-size: 13px; color: #4a3a9a;
|
||
background: #eceaf6; border-radius: 10px;
|
||
padding: 10px 16px; margin-top: 10px;
|
||
display: none;
|
||
}
|
||
.result-info.visible { display: block; }
|
||
|
||
.model-note {
|
||
font-size: 11px; color: #9a9abf;
|
||
margin-top: 8px; font-style: italic;
|
||
}
|
||
|
||
/* === AUTOCOMPLETE === */
|
||
.model-input-wrap { position: relative; }
|
||
.model-input-wrap input[type="text"] {
|
||
font-family: 'Poppins', sans-serif;
|
||
border: 1.5px solid #C4BFDF;
|
||
border-radius: 8px;
|
||
padding: 7px 12px;
|
||
font-size: 13px;
|
||
color: #4a3a9a;
|
||
width: 210px;
|
||
background: white;
|
||
outline: none;
|
||
}
|
||
.model-input-wrap input[type="text"]:focus { border-color: #6D60B4; }
|
||
|
||
.suggestion-box {
|
||
display: none;
|
||
position: absolute;
|
||
top: calc(100% + 4px); left: 0;
|
||
background: white;
|
||
border: 1.5px solid #C4BFDF;
|
||
border-radius: 12px;
|
||
width: 290px;
|
||
max-height: 260px;
|
||
overflow-y: auto;
|
||
z-index: 999;
|
||
box-shadow: 0 6px 20px rgba(109,96,180,0.15);
|
||
padding: 6px 0;
|
||
}
|
||
.sg-group {
|
||
font-size: 10px; font-weight: 700;
|
||
color: #9a90d4; padding: 8px 14px 4px;
|
||
letter-spacing: 0.5px;
|
||
}
|
||
.sg-item {
|
||
font-size: 12px; font-weight: 600;
|
||
color: #4a3a9a; padding: 8px 16px;
|
||
cursor: pointer;
|
||
display: flex; justify-content: space-between;
|
||
align-items: center; gap: 8px;
|
||
}
|
||
.sg-item:hover { background: #eceaf6; }
|
||
.sg-item span { font-weight: 400; color: #9a90c0; font-size: 11px; white-space: nowrap; }
|
||
.sg-item.sg-hidden { display: none; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="wrapper">
|
||
|
||
<div class="sidebar">
|
||
<div class="brand">
|
||
<h2>MIRA</h2>
|
||
<p>Money Identification and<br>Recognition Assistant</p>
|
||
</div>
|
||
<nav class="nav">
|
||
<a href="/monitoring" class="nav-item">
|
||
<span class="icon-grid"></span> Monitoring
|
||
</a>
|
||
<a href="/testing" class="nav-item">
|
||
<span class="icon-money"></span> Testing
|
||
</a>
|
||
<a href="/training" class="nav-item active">
|
||
<span class="icon-train"></span> Training
|
||
</a>
|
||
</nav>
|
||
</div>
|
||
|
||
<div class="main-wrap">
|
||
<div class="content-area">
|
||
<div class="page-title">Training</div>
|
||
|
||
<!-- STEP 1: Install Ultralytics -->
|
||
<div class="step-card">
|
||
<div class="step-header">
|
||
<div class="step-badge">1</div>
|
||
<div>
|
||
<div class="step-title">Install Ultralytics</div>
|
||
<div class="step-desc">Install library ultralytics yang dibutuhkan untuk proses training YOLO</div>
|
||
</div>
|
||
<span class="step-status status-wait" id="status0">Menunggu</span>
|
||
</div>
|
||
<div class="form-row">
|
||
<button class="btn" id="btnInstall" onclick="installUltralytics()">Install Ultralytics</button>
|
||
</div>
|
||
<div class="log-box" id="logBox0"></div>
|
||
</div>
|
||
|
||
<!-- STEP 2: Upload Dataset -->
|
||
<div class="step-card">
|
||
<div class="step-header">
|
||
<div class="step-badge">2</div>
|
||
<div>
|
||
<div class="step-title">Upload Dataset</div>
|
||
<div class="step-desc">Upload file dataset ".zip"</div>
|
||
</div>
|
||
<span class="step-status status-wait" id="status1">Menunggu</span>
|
||
</div>
|
||
<div class="form-row">
|
||
<label class="file-label" for="datasetFile">Pilih File .zip</label>
|
||
<input type="file" id="datasetFile" accept=".zip" onchange="updateFileName(this, 'fname1')">
|
||
<span class="file-name" id="fname1">Belum ada file</span>
|
||
<button class="btn" onclick="uploadDataset()">Upload & Ekstrak</button>
|
||
</div>
|
||
<div class="result-info" id="info1"></div>
|
||
</div>
|
||
|
||
<!-- STEP 3: Split Dataset -->
|
||
<div class="step-card">
|
||
<div class="step-header">
|
||
<div class="step-badge">3</div>
|
||
<div>
|
||
<div class="step-title">Bagi Dataset (Train / Validasi)</div>
|
||
<div class="step-desc">Membagi dataset menjadi data training dan validasi</div>
|
||
</div>
|
||
<span class="step-status status-wait" id="status2">Menunggu</span>
|
||
</div>
|
||
<div class="form-row">
|
||
<span class="input-label">Persentase Training:</span>
|
||
<input type="number" class="input-small" id="trainPct" value="90" min="50" max="95"> %
|
||
<button class="btn" onclick="splitDataset()">Bagi Dataset</button>
|
||
</div>
|
||
<div class="result-info" id="info2"></div>
|
||
</div>
|
||
|
||
<!-- STEP 4: Buat data.yaml -->
|
||
<div class="step-card">
|
||
<div class="step-header">
|
||
<div class="step-badge">4</div>
|
||
<div>
|
||
<div class="step-title">Konfigurasi Training</div>
|
||
<div class="step-desc">Membaca classes.txt dan membuat file konfigurasi training</div>
|
||
</div>
|
||
<span class="step-status status-wait" id="status3">Menunggu</span>
|
||
</div>
|
||
<div class="form-row">
|
||
<button class="btn" onclick="createYaml()">Baca data</button>
|
||
</div>
|
||
<div class="result-info" id="info3"></div>
|
||
</div>
|
||
|
||
<!-- STEP 5: Training -->
|
||
<div class="step-card">
|
||
<div class="step-header">
|
||
<div class="step-badge">5</div>
|
||
<div>
|
||
<div class="step-title">Mulai Training YOLO</div>
|
||
<div class="step-desc">Proses training model — log akan muncul secara real-time di bawah</div>
|
||
</div>
|
||
<span class="step-status status-wait" id="status4">Menunggu</span>
|
||
</div>
|
||
<div class="form-row">
|
||
<span class="input-label">Model:</span>
|
||
<div class="model-input-wrap">
|
||
<input
|
||
type="text"
|
||
id="modelSize"
|
||
value="yolov8n.pt"
|
||
placeholder="cth: yolov8n.pt"
|
||
autocomplete="off"
|
||
oninput="filterSuggestions(this.value)"
|
||
onfocus="showSuggestions()"
|
||
onblur="setTimeout(hideSuggestions, 200)"
|
||
>
|
||
<div class="suggestion-box" id="suggestionBox">
|
||
<div class="sg-item" onclick="selectModel('yolov8n.pt')">yolov8n.pt <span>Nano — Tercepat, CPU ok</span></div>
|
||
</div>
|
||
</div>
|
||
<span class="input-label">Epochs:</span>
|
||
<input type="number" class="input-small" id="epochs" value="60" min="1" max="500">
|
||
<span class="input-label">Imgsz:</span>
|
||
<input type="number" class="input-small" id="imgsz" value="640" min="320" max="1280" step="32">
|
||
<button class="btn" id="btnTrain" onclick="startTraining()">▶ Mulai Training</button>
|
||
</div>
|
||
<p class="model-note">* Untuk CPU: gunakan Nano atau Small dengan epochs 30–60.</p>
|
||
<p class="model-note">* Hasil training: <strong>best.pt</strong> (akurasi terbaik) dan <strong>last.pt</strong> (epoch terakhir) akan tersimpan otomatis.</p>
|
||
<div class="log-box" id="logBox"></div>
|
||
</div>
|
||
|
||
<!-- STEP 6: Kelola Model -->
|
||
<div class="step-card">
|
||
<div class="step-header">
|
||
<div class="step-badge">6</div>
|
||
<div>
|
||
<div class="step-title">Kelola Model</div>
|
||
<div class="step-desc">Lihat semua model tersimpan, terapkan, atau download model yang diinginkan</div>
|
||
</div>
|
||
<button onclick="loadModels()" style="margin-left:auto; background:white; color:#6D60B4; border:1.5px solid #6D60B4; border-radius:9px; padding:7px 16px; font-family:'Poppins',sans-serif; font-size:12px; font-weight:600; cursor:pointer;">🔄 Refresh</button>
|
||
</div>
|
||
<div id="modelLibrary">
|
||
<div style="font-size:12px; color:#9a9abf; font-style:italic;">Klik Refresh untuk memuat daftar model.</div>
|
||
</div>
|
||
<div class="result-info" id="info5"></div>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
function updateFileName(input, targetId) {
|
||
document.getElementById(targetId).textContent = input.files[0] ? input.files[0].name : "Belum ada file";
|
||
}
|
||
|
||
function setStatus(stepNum, type, text) {
|
||
const el = document.getElementById('status' + stepNum);
|
||
el.className = 'step-status status-' + type;
|
||
el.textContent = text;
|
||
}
|
||
|
||
function showInfo(stepNum, text, isError) {
|
||
const el = document.getElementById('info' + stepNum);
|
||
el.textContent = text;
|
||
el.style.background = isError ? '#fde8e8' : '#eceaf6';
|
||
el.style.color = isError ? '#b94040' : '#4a3a9a';
|
||
el.classList.add('visible');
|
||
}
|
||
|
||
function appendLog(boxId, text) {
|
||
const box = document.getElementById(boxId);
|
||
box.classList.add('visible');
|
||
box.textContent += text + '\n';
|
||
box.scrollTop = box.scrollHeight;
|
||
}
|
||
|
||
// === AUTOCOMPLETE ===
|
||
function showSuggestions() {
|
||
filterSuggestions(document.getElementById('modelSize').value);
|
||
document.getElementById('suggestionBox').style.display = 'block';
|
||
}
|
||
|
||
function hideSuggestions() {
|
||
document.getElementById('suggestionBox').style.display = 'none';
|
||
}
|
||
|
||
function selectModel(val) {
|
||
document.getElementById('modelSize').value = val;
|
||
hideSuggestions();
|
||
}
|
||
|
||
function filterSuggestions(query) {
|
||
const items = document.querySelectorAll('.sg-item');
|
||
const q = query.toLowerCase().trim();
|
||
items.forEach(item => {
|
||
const text = item.textContent.toLowerCase();
|
||
item.classList.toggle('sg-hidden', q.length > 0 && !text.includes(q));
|
||
});
|
||
document.getElementById('suggestionBox').style.display = 'block';
|
||
}
|
||
|
||
// STEP 0 - Install Ultralytics
|
||
function installUltralytics() {
|
||
const logBox = document.getElementById('logBox0');
|
||
const btn = document.getElementById('btnInstall');
|
||
logBox.textContent = '';
|
||
logBox.classList.add('visible');
|
||
btn.disabled = true;
|
||
setStatus(0, 'run', 'Installing...');
|
||
|
||
fetch('/install_ultralytics', { method: 'POST' })
|
||
.then(res => res.json())
|
||
.then(() => {
|
||
const evtSource = new EventSource('/training_log');
|
||
evtSource.onmessage = function(e) {
|
||
if (e.data === '__DONE__') {
|
||
evtSource.close();
|
||
btn.disabled = false;
|
||
if (logBox.textContent.includes('❌')) {
|
||
setStatus(0, 'err', 'Gagal');
|
||
} else {
|
||
setStatus(0, 'ok', 'Selesai ✓');
|
||
}
|
||
return;
|
||
}
|
||
if (e.data.trim()) appendLog('logBox0', e.data);
|
||
};
|
||
evtSource.onerror = function() {
|
||
evtSource.close();
|
||
btn.disabled = false;
|
||
setStatus(0, 'err', 'Koneksi terputus');
|
||
};
|
||
}).catch(e => {
|
||
btn.disabled = false;
|
||
setStatus(0, 'err', 'Error');
|
||
appendLog('logBox0', '❌ ' + e);
|
||
});
|
||
}
|
||
|
||
// STEP 1 - Upload Dataset
|
||
async function uploadDataset() {
|
||
const file = document.getElementById('datasetFile').files[0];
|
||
if (!file) { alert('Pilih file .zip terlebih dahulu!'); return; }
|
||
|
||
setStatus(1, 'run', 'Mengupload...');
|
||
const formData = new FormData();
|
||
formData.append('dataset', file);
|
||
|
||
try {
|
||
const res = await fetch('/upload_dataset', { method: 'POST', body: formData });
|
||
const data = await res.json();
|
||
if (data.error) { setStatus(1, 'err', 'Gagal'); showInfo(1, '❌ ' + data.error, true); }
|
||
else { setStatus(1, 'ok', 'Selesai ✓'); showInfo(1, '✅ ' + data.message, false); }
|
||
} catch(e) {
|
||
setStatus(1, 'err', 'Error'); showInfo(1, '❌ Gagal upload: ' + e, true);
|
||
}
|
||
}
|
||
|
||
// STEP 2 - Split Dataset
|
||
async function splitDataset() {
|
||
const pct = parseFloat(document.getElementById('trainPct').value) / 100;
|
||
setStatus(2, 'run', 'Memproses...');
|
||
|
||
try {
|
||
const res = await fetch('/split_dataset', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({ train_pct: pct })
|
||
});
|
||
const data = await res.json();
|
||
if (data.error) { setStatus(2, 'err', 'Gagal'); showInfo(2, '❌ ' + data.error, true); }
|
||
else { setStatus(2, 'ok', 'Selesai ✓'); showInfo(2, '✅ ' + data.message, false); }
|
||
} catch(e) {
|
||
setStatus(2, 'err', 'Error'); showInfo(2, '❌ ' + e, true);
|
||
}
|
||
}
|
||
|
||
// STEP 3 - Buat data.yaml
|
||
async function createYaml() {
|
||
setStatus(3, 'run', 'Memproses...');
|
||
|
||
try {
|
||
const res = await fetch('/create_yaml', { method: 'POST' });
|
||
const data = await res.json();
|
||
if (data.error) { setStatus(3, 'err', 'Gagal'); showInfo(3, '❌ ' + data.error, true); }
|
||
else { setStatus(3, 'ok', 'Selesai ✓'); showInfo(3, '✅ ' + data.message, false); }
|
||
} catch(e) {
|
||
setStatus(3, 'err', 'Error'); showInfo(3, '❌ ' + e, true);
|
||
}
|
||
}
|
||
|
||
// STEP 4 - Training
|
||
function startTraining() {
|
||
const epochs = document.getElementById('epochs').value;
|
||
const imgsz = document.getElementById('imgsz').value;
|
||
const modelSize = document.getElementById('modelSize').value.trim();
|
||
const logBox = document.getElementById('logBox');
|
||
const btn = document.getElementById('btnTrain');
|
||
|
||
if (!modelSize) { alert('Masukkan nama model terlebih dahulu!'); return; }
|
||
|
||
logBox.textContent = '';
|
||
logBox.classList.add('visible');
|
||
btn.disabled = true;
|
||
setStatus(4, 'run', 'Training...');
|
||
|
||
fetch('/start_training', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({ epochs, imgsz, model: modelSize })
|
||
})
|
||
.then(res => res.json())
|
||
.then(data => {
|
||
if (data.error) {
|
||
setStatus(4, 'err', 'Gagal');
|
||
appendLog('logBox', '❌ ' + data.error);
|
||
btn.disabled = false;
|
||
return;
|
||
}
|
||
|
||
const evtSource = new EventSource('/training_log');
|
||
evtSource.onmessage = function(e) {
|
||
if (e.data === '__DONE__') {
|
||
evtSource.close();
|
||
btn.disabled = false;
|
||
if (logBox.textContent.includes('❌')) {
|
||
setStatus(4, 'err', 'Gagal');
|
||
} else {
|
||
setStatus(4, 'ok', 'Selesai ✓');
|
||
loadModels();
|
||
}
|
||
return;
|
||
}
|
||
if (e.data.trim()) appendLog('logBox', e.data);
|
||
};
|
||
evtSource.onerror = function() {
|
||
evtSource.close();
|
||
setStatus(4, 'err', 'Koneksi terputus');
|
||
btn.disabled = false;
|
||
};
|
||
})
|
||
.catch(e => {
|
||
setStatus(4, 'err', 'Error');
|
||
appendLog('logBox', '❌ ' + e);
|
||
btn.disabled = false;
|
||
});
|
||
}
|
||
|
||
// STEP 6 - MODEL LIBRARY
|
||
async function loadModels() {
|
||
const container = document.getElementById('modelLibrary');
|
||
container.innerHTML = '<div style="font-size:12px;color:#9a9abf;font-style:italic;">Memuat...</div>';
|
||
try {
|
||
const res = await fetch('/list_models');
|
||
const data = await res.json();
|
||
const models = data.models;
|
||
|
||
if (models.length === 0) {
|
||
container.innerHTML = '<div style="font-size:12px;color:#9a9abf;font-style:italic;">Belum ada model tersimpan. Jalankan training terlebih dahulu.</div>';
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = '';
|
||
models.forEach(m => {
|
||
const card = document.createElement('div');
|
||
card.style.cssText = `
|
||
background:${m.active ? '#f0eef9' : 'white'};
|
||
border:1.5px solid ${m.active ? '#6D60B4' : '#D4CFEE'};
|
||
border-radius:12px; padding:12px 16px; margin-bottom:10px;
|
||
display:flex; align-items:center; gap:12px; flex-wrap:wrap;
|
||
`;
|
||
card.innerHTML = `
|
||
<div style="flex:1; min-width:0;">
|
||
<div style="font-size:13px; font-weight:700; color:#4a3a9a; display:flex; align-items:center; gap:8px;">
|
||
${m.name}
|
||
${m.active ? '<span style="background:#6D60B4;color:white;font-size:9px;font-weight:700;padding:2px 8px;border-radius:20px;">AKTIF</span>' : ''}
|
||
</div>
|
||
<div style="font-size:11px; color:#9a90c0; margin-top:3px;">
|
||
Base: ${m.base_model} | Epochs: ${m.epochs} | Imgsz: ${m.imgsz} | ${m.created}
|
||
</div>
|
||
</div>
|
||
<div style="display:flex; gap:8px; flex-shrink:0;">
|
||
${!m.active ? `<button class="btn" style="padding:7px 16px;font-size:12px;" onclick="applyModel('${m.name}')">✅ Terapkan</button>` : ''}
|
||
<a href="/download_model/${m.name}">
|
||
<button style="background:white;color:#6D60B4;border:1.5px solid #6D60B4;border-radius:9px;padding:7px 16px;font-family:'Poppins',sans-serif;font-size:12px;font-weight:600;cursor:pointer;">⬇ Download</button>
|
||
</a>
|
||
</div>
|
||
`;
|
||
container.appendChild(card);
|
||
});
|
||
} catch(e) {
|
||
container.innerHTML = `<div style="font-size:12px;color:#b94040;">❌ Gagal memuat: ${e}</div>`;
|
||
}
|
||
}
|
||
|
||
async function applyModel(name) {
|
||
if (!confirm(`Terapkan model "${name}"?\nModel ini akan langsung digunakan di Monitoring & Testing.`)) return;
|
||
try {
|
||
const res = await fetch('/apply_model', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({name}) });
|
||
const data = await res.json();
|
||
if (data.error) { alert('❌ ' + data.error); return; }
|
||
const el = document.getElementById('info5');
|
||
el.textContent = '✅ ' + data.message;
|
||
el.style.background = '#eceaf6'; el.style.color = '#4a3a9a';
|
||
el.classList.add('visible');
|
||
loadModels();
|
||
} catch(e) { alert('❌ Gagal: ' + e); }
|
||
}
|
||
|
||
loadModels();
|
||
</script>
|
||
</body>
|
||
</html>
|