2025-12-21 14:34:18 +07:00
<!DOCTYPE html>
< html lang = "vi" >
< head >
< meta charset = "UTF-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1.0" >
< title > Land Classification Training Interface</ title >
<!-- Leaflet CSS -->
< link rel = "stylesheet" href = "https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
< link rel = "stylesheet" href = "https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.css" />
< style >
* {
margin : 0 ;
padding : 0 ;
box-sizing : border-box ;
}
body {
font-family : 'Segoe UI' , Tahoma , Geneva , Verdana , sans-serif ;
background : linear-gradient ( 135 deg , #667eea 0 % , #764ba2 100 % );
padding : 20 px ;
min-height : 100 vh ;
}
. container {
max-width : 1400 px ;
margin : 0 auto ;
background : white ;
border-radius : 20 px ;
box-shadow : 0 20 px 60 px rgba ( 0 , 0 , 0 , 0.3 );
overflow : hidden ;
}
. header {
background : linear-gradient ( 135 deg , #667eea 0 % , #764ba2 100 % );
color : white ;
padding : 30 px ;
text-align : center ;
}
. header h1 {
font-size : 2.5 em ;
margin-bottom : 10 px ;
}
. header p {
opacity : 0.9 ;
font-size : 1.1 em ;
}
. content {
padding : 30 px ;
display : grid ;
grid-template-columns : 1 fr 1 fr ;
gap : 30 px ;
}
# map {
height : 500 px ;
border-radius : 10 px ;
box-shadow : 0 4 px 15 px rgba ( 0 , 0 , 0 , 0.1 );
}
. map-container {
grid-column : 1 / -1 ;
}
. map-instructions {
background : #e3f2fd ;
padding : 15 px ;
border-radius : 10 px ;
margin-bottom : 15 px ;
border-left : 4 px solid #2196f3 ;
}
. map-instructions h3 {
color : #1976d2 ;
margin-bottom : 8 px ;
}
. map-instructions p {
color : #555 ;
margin : 5 px 0 ;
}
. section {
margin-bottom : 30 px ;
padding : 20 px ;
background : #f8f9fa ;
border-radius : 10 px ;
}
. section h2 {
color : #667eea ;
margin-bottom : 15 px ;
font-size : 1.5 em ;
}
. form-group {
margin-bottom : 15 px ;
}
. form-group label {
display : block ;
margin-bottom : 5 px ;
color : #333 ;
font-weight : 600 ;
}
. form-group input , . form-group select {
width : 100 % ;
padding : 10 px ;
border : 2 px solid #e0e0e0 ;
border-radius : 5 px ;
font-size : 1 em ;
transition : border-color 0.3 s ;
}
. form-group input : focus , . form-group select : focus {
outline : none ;
border-color : #667eea ;
}
. form-row {
display : grid ;
grid-template-columns : repeat ( auto - fit , minmax ( 200 px , 1 fr ));
gap : 15 px ;
}
. btn {
padding : 12 px 30 px ;
border : none ;
border-radius : 5 px ;
font-size : 1 em ;
font-weight : 600 ;
cursor : pointer ;
transition : all 0.3 s ;
margin-right : 10 px ;
}
. btn-primary {
background : linear-gradient ( 135 deg , #667eea 0 % , #764ba2 100 % );
color : white ;
}
. btn-primary : hover {
transform : translateY ( -2 px );
box-shadow : 0 5 px 15 px rgba ( 102 , 126 , 234 , 0.4 );
}
. btn-secondary {
background : #6c757d ;
color : white ;
}
. btn-danger {
background : #dc3545 ;
color : white ;
}
. btn : disabled {
opacity : 0.5 ;
cursor : not-allowed ;
}
. status-box {
padding : 20 px ;
background : white ;
border-radius : 10 px ;
border-left : 5 px solid #667eea ;
margin-bottom : 20 px ;
}
. status-box . success {
border-left-color : #28a745 ;
background : #d4edda ;
}
. status-box . error {
border-left-color : #dc3545 ;
background : #f8d7da ;
}
. status-box . training {
border-left-color : #ffc107 ;
background : #fff3cd ;
}
. progress {
height : 30 px ;
background : #e0e0e0 ;
border-radius : 15 px ;
overflow : hidden ;
margin : 10 px 0 ;
}
. progress-bar {
height : 100 % ;
background : linear-gradient ( 90 deg , #667eea 0 % , #764ba2 100 % );
transition : width 0.3 s ;
display : flex ;
align-items : center ;
justify-content : center ;
color : white ;
font-weight : 600 ;
}
. preset-buttons {
display : flex ;
gap : 10 px ;
flex-wrap : wrap ;
margin-bottom : 15 px ;
}
. preset-btn {
padding : 8 px 15 px ;
background : white ;
border : 2 px solid #667eea ;
color : #667eea ;
border-radius : 20 px ;
cursor : pointer ;
transition : all 0.3 s ;
}
. preset-btn : hover {
background : #667eea ;
color : white ;
}
. info {
background : #e3f2fd ;
padding : 15 px ;
border-radius : 5 px ;
margin-bottom : 15 px ;
border-left : 4 px solid #2196f3 ;
}
. model-list {
max-height : 400 px ;
overflow-y : auto ;
}
. model-item {
background : white ;
padding : 15 px ;
margin-bottom : 10 px ;
border-radius : 5 px ;
border : 1 px solid #e0e0e0 ;
}
. model-item h3 {
color : #667eea ;
margin-bottom : 5 px ;
}
. model-item p {
margin : 5 px 0 ;
color : #666 ;
}
/* Animations for notifications */
@ keyframes slideIn {
from {
transform : translateX ( 400 px );
opacity : 0 ;
}
to {
transform : translateX ( 0 );
opacity : 1 ;
}
}
@ keyframes slideOut {
from {
transform : translateX ( 0 );
opacity : 1 ;
}
to {
transform : translateX ( 400 px );
opacity : 0 ;
}
}
</ style >
</ head >
< body >
< div class = "container" >
< div class = "header" >
2025-12-21 17:31:51 +07:00
< h1 > Training Interface</ h1 >
2025-12-21 14:34:18 +07:00
< p > Giao diện training model phân loại đất từ ảnh vệ tinh</ p >
</ div >
< div class = "content" >
<!-- Status Section -->
< div class = "section" >
< h2 > 📊 Trạng Thái Training</ h2 >
< div id = "statusBox" class = "status-box" >
< p >< strong > Trạng thái:</ strong > < span id = "statusText" > Chưa bắt đầu</ span ></ p >
< p >< strong > Tiến độ:</ strong > < span id = "progressText" > -</ span ></ p >
< div id = "progressBar" class = "progress" style = "display: none;" >
< div class = "progress-bar" style = "width: 0%;" > 0%</ div >
</ div >
</ div >
<!-- System Info -->
< div style = "margin-top: 20px;" >
< h3 style = "color: #667eea; margin-bottom: 10px;" > 💻 Thông Tin Hệ Thống</ h3 >
< div class = "info" >
< p >< strong > 🖥️ GPU:</ strong > < span id = "gpuInfo" > Đang tải...</ span ></ p >
< p >< strong > 💾 RAM:</ strong > < span id = "ramInfo" > Đang tải...</ span ></ p >
< p >< strong > 📁 Models:</ strong > < span id = "modelCount" > 0</ span > models đã train</ p >
</ div >
</ div >
<!-- Training History -->
< div style = "margin-top: 20px;" >
< h3 style = "color: #667eea; margin-bottom: 10px;" > 📜 Lịch Sử Training</ h3 >
< div id = "trainingHistory" style = "max-height: 300px; overflow-y: auto; background: #f8f9fa; padding: 15px; border-radius: 8px; font-size: 13px;" >
< p style = "color: #999;" > Chưa có lịch sử training</ p >
</ div >
</ div >
<!-- Quick Stats -->
< div style = "margin-top: 20px;" >
< h3 style = "color: #667eea; margin-bottom: 10px;" > 📈 Thống Kê Nhanh</ h3 >
< div class = "info" style = "background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%);" >
< p >< strong > 🎯 Tổng số lần train:</ strong > < span id = "totalTrainings" > 0</ span ></ p >
< p >< strong > ✅ Thành công:</ strong > < span id = "successTrainings" > 0</ span ></ p >
< p >< strong > ❌ Thất bại:</ strong > < span id = "failedTrainings" > 0</ span ></ p >
< p >< strong > ⏱️ Thời gian TB:</ strong > < span id = "avgTime" > -</ span ></ p >
</ div >
</ div >
<!-- Quick Guide -->
< div style = "margin-top: 20px;" >
< h3 style = "color: #667eea; margin-bottom: 10px;" > 📖 Hướng Dẫn Nhanh</ h3 >
< div class = "info" style = "background: #e7f3ff; border-left: 4px solid #2196F3;" >
< ol style = "margin: 0; padding-left: 20px; font-size: 13px;" >
< li > Chọn preset hoặc vẽ khu vực trên bản đồ</ li >
< li > Chọn thời gian và cấu hình vệ tinh</ li >
< li > Điều chỉnh tham số model (nếu cần)</ li >
< li > Nhấn "Bắt Đầu Training"</ li >
< li > Theo dõi tiến độ ở phần này</ li >
</ ol >
</ div >
</ div >
</ div >
<!-- Configuration Section -->
< div class = "section" >
< h2 > ⚙️ Cấu Hình Training</ h2 >
< div class = "info" >
< strong > 💡 Lựa chọn nhanh:</ strong > Chọn một trong các preset bên dưới hoặc tùy chỉnh thủ công
</ div >
< div class = "preset-buttons" id = "presetButtons" >
<!-- Preset buttons will be inserted here -->
</ div >
< form id = "trainingForm" >
< h3 style = "margin-bottom: 15px; color: #667eea;" > 📍 Khu Vực Training</ h3 >
< div class = "map-container" >
< div class = "map-instructions" >
< strong > 💡 Hướng dẫn:</ strong > Sử dụng công cụ vẽ hình chữ nhật
< span style = "display: inline-block; width: 24px; height: 24px; background: white; border: 2px solid #333; vertical-align: middle; margin: 0 5px;" > □</ span >
ở góc trên bên trái của bản đồ để chọn khu vực training
</ div >
< div id = "map" ></ div >
< div style = "margin-top: 10px; font-size: 13px; color: #666;" >
< strong > Khu vực đã chọn:</ strong >
< span id = "bboxDisplay" > Chưa chọn khu vực</ span >
</ div >
</ div >
<!-- Hidden inputs to store bbox values -->
< input type = "hidden" id = "minLon" value = "105.6" required >
< input type = "hidden" id = "minLat" value = "9.3" required >
< input type = "hidden" id = "maxLon" value = "106.2" required >
< input type = "hidden" id = "maxLat" value = "9.8" required >
< h3 style = "margin: 20px 0 15px; color: #667eea;" > 📅 Thời Gian</ h3 >
< div class = "form-row" >
< div class = "form-group" >
< label > Ngày bắt đầu:</ label >
< input type = "date" id = "startDate" value = "2023-03-01" required >
</ div >
< div class = "form-group" >
< label > Ngày kết thúc:</ label >
< input type = "date" id = "endDate" value = "2023-05-31" required >
</ div >
</ div >
< h3 style = "margin: 20px 0 15px; color: #667eea;" > � Dataset Cache Preset</ h3 >
< div class = "form-group" >
< label > Chọn Dataset đã cache:</ label >
< select id = "cachePreset" style = "font-size: 14px;" >
< option value = "" > -- Không dùng cache preset --</ option >
</ select >
< div style = "font-size: 12px; color: #666; margin-top: 5px;" >
💡 Chọn dataset cache có sẵn để tự động điền các thông số tương ứng
</ div >
</ div >
< h3 style = "margin: 20px 0 15px; color: #667eea;" > � 🛰️ Dữ Liệu Vệ Tinh</ h3 >
< div class = "form-row" >
< div class = "form-group" >
< label > Số scenes tối đa:</ label >
< input type = "number" id = "maxScenes" value = "12" min = "1" max = "100" required >
</ div >
< div class = "form-group" >
< label > Cloud cover (%):</ label >
< input type = "number" id = "cloudCover" value = "30" min = "0" max = "100" required >
</ div >
< div class = "form-group" >
< label > Độ phân giải (m):</ label >
< select id = "resolution" required >
< option value = "10" > 10m (Chính xác cao)</ option >
< option value = "20" selected > 20m (Cân bằng)</ option >
< option value = "30" > 30m (Nhanh)</ option >
</ select >
</ div >
</ div >
< h3 style = "margin: 20px 0 15px; color: #667eea;" > 🤖 Model Parameters</ h3 >
<!-- Model Type Selection -->
< div class = "form-group" >
< label >< strong > 🧠 Loại Model:</ strong ></ label >
< select id = "modelType" required style = "font-weight: 600; font-size: 14px;" >
< option value = "xgboost" selected > 🚀 XGBoost (Nhanh, Chính xác cao, Hỗ trợ GPU)</ option >
< option value = "random_forest" > 🌲 Random Forest (Ổn định, Không cần GPU)</ option >
< option value = "decision_tree" > 🌳 Decision Tree (Đơn giản, Nhanh nhất)</ option >
< option value = "svm" > 🎯 SVM (Chính xác, Chậm với dữ liệu lớn)</ option >
< option value = "cnn" > 🧠 CNN - Deep Learning (PyTorch, Tốt với ảnh vệ tinh, Hỗ trợ GPU)</ option >
</ select >
< div style = "margin-top: 8px; padding: 10px; background: #e7f3ff; border-radius: 5px; font-size: 12px;" >
< span id = "modelTypeDesc" style = "color: #1976d2;" >
✓ XGBoost: Tốt nhất cho dữ liệu satellite, hỗ trợ GPU, training nhanh
</ span >
</ div >
</ div >
<!-- Model-specific parameters -->
< div class = "form-row" >
< div class = "form-group" id = "nEstimatorsGroup" >
< label > N Estimators:</ label >
< input type = "number" id = "nEstimators" value = "100" min = "10" max = "1000" required >
</ div >
< div class = "form-group" >
< label > Max Depth:</ label >
< input type = "number" id = "maxDepth" value = "20" min = "1" max = "50" required >
</ div >
< div class = "form-group" id = "learningRateGroup" >
< label > Learning Rate:</ label >
< input type = "number" step = "0.01" id = "learningRate" value = "0.1" min = "0.01" max = "1" required >
</ div >
< div class = "form-group" id = "testSizeGroup" >
< label > Tỷ lệ dữ liệu test (0-1):</ label >
< input type = "number" step = "0.05" id = "testSize" value = "0.2" min = "0.05" max = "0.5" required >
</ div >
< div class = "form-group" id = "useGpuGroup" >
< label > Use GPU:</ label >
< select id = "useGpu" required >
< option value = "true" selected > Có (RTX 4060)</ option >
< option value = "false" > Không (CPU)</ option >
</ select >
</ div >
</ div >
<!-- Cache Option -->
< div class = "form-group" style = "margin-top: 20px; padding: 15px; background: #fff8dc; border-radius: 8px; border: 1px solid #ffc107;" >
< label style = "display: flex; align-items: center; cursor: pointer; margin: 0;" >
< input type = "checkbox" id = "useCache" checked style = "width: 18px; height: 18px; margin-right: 10px;" >
< span style = "font-weight: 600; color: #856404;" > 💾 Sử dụng Cache Dataset</ span >
</ label >
< div style = "font-size: 12px; color: #856404; margin-top: 8px; margin-left: 28px;" >
✅ < strong > Khuyến nghị:</ strong > Bật để test nhanh hơn. Lần đầu load dữ liệu sẽ chậm, nhưng các lần sau rất nhanh (không cần download lại từ satellite).< br >
📊 < span id = "cacheInfo" > Đang kiểm tra cache...</ span >
</ div >
< div style = "margin-top: 10px; margin-left: 28px;" >
< button type = "button" class = "btn btn-secondary" onclick = "clearCache()" style = "font-size: 12px; padding: 5px 10px; background: #dc3545;" >
🗑️ Xóa Cache
</ button >
< button type = "button" class = "btn btn-secondary" onclick = "loadCacheInfo()" style = "font-size: 12px; padding: 5px 10px; background: #17a2b8; margin-left: 5px;" >
🔄 Refresh
</ button >
</ div >
</ div >
< div style = "margin-top: 30px; text-align: center;" >
< button type = "submit" class = "btn btn-primary" id = "startBtn" >
🚀 Bắt Đầu Training
</ button >
< button type = "button" class = "btn btn-danger" id = "stopBtn" style = "display: none;" >
⏹️ Dừng Training
</ button >
</ div >
</ form >
</ div >
<!-- Models List Section -->
< div class = "section" >
< h2 > 📦 Danh Sách Models Đã Train</ h2 >
< button class = "btn btn-secondary" onclick = "loadModels()" > 🔄 Refresh</ button >
< div id = "modelsList" class = "model-list" style = "margin-top: 15px;" >
< p > Đang tải...</ p >
</ div >
2025-12-21 17:31:51 +07:00
< div style = "margin-top: 15px; visibility: hidden;" >
< label for = "selectedModel" style = "font-weight:600; color:#667eea;" > Chọn model để dự đoán:</ label >
< select id = "selectedModel" style = "width:100%; padding:10px; border-radius:5px; font-size:1em; margin-top:5px;" ></ select >
</ div >
2025-12-21 14:34:18 +07:00
</ div >
<!-- Reports Section -->
< div class = "section" >
< h2 > 📝 Báo Cáo (Auto Reports)</ h2 >
< button class = "btn btn-secondary" onclick = "loadReports()" > 🔄 Refresh</ button >
< div id = "reportsList" class = "model-list" style = "margin-top: 15px;" >
< p > Đang tải...</ p >
</ div >
</ div >
2025-12-21 17:31:51 +07:00
2025-12-21 14:34:18 +07:00
</ div >
</ div >
<!-- Leaflet JavaScript -->
< script src = "https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" ></ script >
< script src = "https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.js" ></ script >
< script >
const API_BASE = 'http://localhost:8000/api' ;
let statusInterval = null ;
let trainingHistory = [];
let trainingStats = {
total : 0 ,
success : 0 ,
failed : 0 ,
times : []
};
2025-12-21 17:31:51 +07:00
// Load presets and models only after DOM is ready
document . addEventListener ( 'DOMContentLoaded' , async () => {
2025-12-21 14:34:18 +07:00
await loadPresets ();
await loadModels ();
await loadReports ();
await loadSystemInfo ();
checkStatus ();
loadTrainingHistory ();
2025-12-21 17:31:51 +07:00
});
2025-12-21 14:34:18 +07:00
// Load preset configurations
async function loadPresets () {
try {
const response = await fetch ( ` ${ API_BASE } /config/presets` );
const data = await response . json ();
const container = document . getElementById ( 'presetButtons' );
container . innerHTML = '' ;
data . presets . forEach ( preset => {
const btn = document . createElement ( 'button' );
btn . className = 'preset-btn' ;
btn . textContent = preset . name ;
btn . onclick = () => applyPreset ( preset . config );
container . appendChild ( btn );
});
} catch ( error ) {
console . error ( 'Error loading presets:' , error );
}
}
// Apply preset configuration
function applyPreset ( config ) {
document . getElementById ( 'minLon' ). value = config . min_lon ;
document . getElementById ( 'minLat' ). value = config . min_lat ;
document . getElementById ( 'maxLon' ). value = config . max_lon ;
document . getElementById ( 'maxLat' ). value = config . max_lat ;
document . getElementById ( 'startDate' ). value = config . start_date ;
document . getElementById ( 'endDate' ). value = config . end_date ;
document . getElementById ( 'maxScenes' ). value = config . max_scenes ;
document . getElementById ( 'cloudCover' ). value = config . cloud_cover ;
document . getElementById ( 'resolution' ). value = config . resolution ;
if ( config . test_size !== undefined ) {
document . getElementById ( 'testSize' ). value = config . test_size ;
}
// Update map with new bounds
if ( currentRectangle ) {
drawnItems . removeLayer ( currentRectangle );
}
const bounds = [[ config . min_lat , config . min_lon ], [ config . max_lat , config . max_lon ]];
const rectangle = L . rectangle ( bounds , {
color : '#667eea' ,
weight : 3 ,
fillOpacity : 0.2
});
drawnItems . addLayer ( rectangle );
currentRectangle = rectangle ;
map . fitBounds ( bounds );
updateBboxFromMap ( L . latLngBounds ( bounds ));
}
// Handle form submission
document . getElementById ( 'trainingForm' ). onsubmit = async ( e ) => {
e . preventDefault ();
const config = {
min_lon : parseFloat ( document . getElementById ( 'minLon' ). value ),
min_lat : parseFloat ( document . getElementById ( 'minLat' ). value ),
max_lon : parseFloat ( document . getElementById ( 'maxLon' ). value ),
max_lat : parseFloat ( document . getElementById ( 'maxLat' ). value ),
start_date : document . getElementById ( 'startDate' ). value ,
end_date : document . getElementById ( 'endDate' ). value ,
max_scenes : parseInt ( document . getElementById ( 'maxScenes' ). value ),
cloud_cover : parseInt ( document . getElementById ( 'cloudCover' ). value ),
resolution : parseInt ( document . getElementById ( 'resolution' ). value ),
model_type : document . getElementById ( 'modelType' ). value ,
n_estimators : parseInt ( document . getElementById ( 'nEstimators' ). value ),
max_depth : parseInt ( document . getElementById ( 'maxDepth' ). value ),
learning_rate : parseFloat ( document . getElementById ( 'learningRate' ). value ),
test_size : parseFloat ( document . getElementById ( 'testSize' ). value ),
use_gpu : document . getElementById ( 'useGpu' ). value === 'true' ,
use_cache : document . getElementById ( 'useCache' ). checked
};
try {
const response = await fetch ( ` ${ API_BASE } /training/start` , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' },
body : JSON . stringify ( config )
});
if ( ! response . ok ) {
const error = await response . json ();
alert ( 'Lỗi: ' + error . detail );
return ;
}
const result = await response . json ();
alert ( result . message );
// Start monitoring status
if ( statusInterval ) clearInterval ( statusInterval );
statusInterval = setInterval ( checkStatus , 2000 );
document . getElementById ( 'startBtn' ). style . display = 'none' ;
document . getElementById ( 'stopBtn' ). style . display = 'inline-block' ;
} catch ( error ) {
alert ( 'Lỗi kết nối: ' + error . message );
}
};
// Stop training
document . getElementById ( 'stopBtn' ). onclick = async () => {
try {
const response = await fetch ( ` ${ API_BASE } /training/stop` , { method : 'POST' });
const result = await response . json ();
alert ( result . message );
checkStatus ();
} catch ( error ) {
alert ( 'Lỗi: ' + error . message );
}
};
// Check training status
async function checkStatus () {
try {
const response = await fetch ( ` ${ API_BASE } /training/status` );
const status = await response . json ();
const statusBox = document . getElementById ( 'statusBox' );
const statusText = document . getElementById ( 'statusText' );
const progressText = document . getElementById ( 'progressText' );
statusText . textContent = status . is_training ? 'Đang training...' :
( status . error ? 'Lỗi' : ( status . result ? 'Hoàn thành' : 'Chờ' ));
progressText . textContent = status . progress || '-' ;
// Update status box styling
statusBox . className = 'status-box' ;
if ( status . is_training ) {
statusBox . classList . add ( 'training' );
} else if ( status . error ) {
statusBox . classList . add ( 'error' );
} else if ( status . result ) {
statusBox . classList . add ( 'success' );
}
// Show/hide buttons
if ( status . is_training ) {
document . getElementById ( 'startBtn' ). style . display = 'none' ;
document . getElementById ( 'stopBtn' ). style . display = 'inline-block' ;
} else {
document . getElementById ( 'startBtn' ). style . display = 'inline-block' ;
document . getElementById ( 'stopBtn' ). style . display = 'none' ;
if ( statusInterval ) {
clearInterval ( statusInterval );
statusInterval = null ;
// Add to history when training completes
if ( status . result || status . error ) {
const record = {
timestamp : new Date (). toLocaleString ( 'vi-VN' ),
success : !! status . result ,
accuracy : status . result ? . test_accuracy ,
error : status . error ,
duration : status . start_time && status . end_time ?
( new Date ( status . end_time ) - new Date ( status . start_time )) / 1000 : null
};
addTrainingRecord ( record );
}
}
if ( status . result ) {
await loadModels ();
await loadReports ();
}
}
} catch ( error ) {
console . error ( 'Error checking status:' , error );
}
}
// Load models list
async function loadModels () {
try {
const response = await fetch ( ` ${ API_BASE } /models/list` );
const data = await response . json ();
const container = document . getElementById ( 'modelsList' );
const modelSelect = document . getElementById ( 'selectedModel' );
if ( data . models . length === 0 ) {
container . innerHTML = '<p>Chưa có model nào</p>' ;
modelSelect . innerHTML = '<option value="">-- Chưa có model --</option>' ;
return ;
}
container . innerHTML = '' ;
modelSelect . innerHTML = '<option value="">-- Chọn model --</option>' ;
data . models . forEach ( model => {
// Add to models list display
const item = document . createElement ( 'div' );
item . className = 'model-item' ;
item . innerHTML = `
<h3>📦 ${ model . filename } </h3>
<p><strong>Tạo lúc:</strong> ${ new Date ( model . created ). toLocaleString ( 'vi-VN' ) } </p>
<p><strong>Kích thước:</strong> ${ model . size_mb } MB</p>
${ model . info . train_accuracy ? `<p><strong>Train Accuracy:</strong> ${ ( model . info . train_accuracy * 100 ). toFixed ( 2 ) } %</p>` : '' }
${ model . info . test_accuracy ? `<p><strong>Test Accuracy:</strong> ${ ( model . info . test_accuracy * 100 ). toFixed ( 2 ) } %</p>` : '' }
` ;
container . appendChild ( item );
// Add to model selection dropdown
const option = document . createElement ( 'option' );
option . value = model . filename ;
option . textContent = ` ${ model . filename } ( ${ model . size_mb } MB)` ;
modelSelect . appendChild ( option );
});
// Update model count
document . getElementById ( 'modelCount' ). textContent = data . models . length ;
} catch ( error ) {
console . error ( 'Error loading models:' , error );
document . getElementById ( 'modelsList' ). innerHTML = '<p>Lỗi khi tải danh sách models</p>' ;
}
}
// Load reports list
async function loadReports () {
try {
const response = await fetch ( ` ${ API_BASE } /reports/list` );
const data = await response . json ();
const container = document . getElementById ( 'reportsList' );
if ( data . reports . length === 0 ) {
container . innerHTML = '<p style="color: #666;">Chưa có báo cáo nào. Báo cáo sẽ tự động tạo sau mỗi lần training/prediction.</p>' ;
return ;
}
container . innerHTML = '' ;
data . reports . forEach ( report => {
const item = document . createElement ( 'div' );
item . className = 'model-item' ;
const typeIcon = report . type === 'training' ? '📊' : '🗺️' ;
const typeLabel = report . type === 'training' ? 'Training Report' : 'Prediction Report' ;
const typeBg = report . type === 'training' ? '#667eea' : '#ff6b6b' ;
item . innerHTML = `
<div style="display: flex; justify-content: space-between; align-items: center;">
<div>
<h3 style="margin-bottom: 8px;">
${ typeIcon } ${ report . filename }
</h3>
<p style="margin: 0;">
<span style="background: ${ typeBg } ; color: white; padding: 3px 10px; border-radius: 12px; font-size: 11px;"> ${ typeLabel } </span>
</p>
<p style="margin-top: 8px; font-size: 13px; color: #666;">
<strong>Tạo lúc:</strong> ${ new Date ( report . created ). toLocaleString ( 'vi-VN' ) } |
<strong>Kích thước:</strong> ${ report . size_kb } KB
</p>
</div>
<div style="display: flex; gap: 8px;">
<button onclick="viewReport(' ${ report . filename } ')" class="btn btn-primary" style="padding: 8px 15px; font-size: 13px;">
👁️ Xem
</button>
<button onclick="downloadReport(' ${ report . filename } ')" class="btn btn-secondary" style="padding: 8px 15px; font-size: 13px;">
📥 Tải
</button>
<button onclick="deleteReport(' ${ report . filename } ')" class="btn btn-danger" style="padding: 8px 15px; font-size: 13px;">
🗑️
</button>
</div>
</div>
` ;
container . appendChild ( item );
});
} catch ( error ) {
console . error ( 'Error loading reports:' , error );
document . getElementById ( 'reportsList' ). innerHTML = '<p style="color: red;">Lỗi khi tải danh sách báo cáo</p>' ;
}
}
// View report in new tab
function viewReport ( filename ) {
window . open ( ` ${ API_BASE } /reports/view/ ${ filename } ` , '_blank' );
}
// Download report
function downloadReport ( filename ) {
window . location . href = ` ${ API_BASE } /reports/download/ ${ filename } ` ;
}
// Delete report
async function deleteReport ( filename ) {
if ( ! confirm ( `Bạn có chắc muốn xóa báo cáo: ${ filename } ?` )) {
return ;
}
try {
const response = await fetch ( ` ${ API_BASE } /reports/delete/ ${ filename } ` , {
method : 'DELETE'
});
const result = await response . json ();
if ( result . success ) {
showNotification ( 'success' , 'Đã xóa báo cáo thành công!' );
loadReports ();
} else {
showNotification ( 'error' , 'Không thể xóa báo cáo!' );
}
} catch ( error ) {
console . error ( 'Error deleting report:' , error );
showNotification ( 'error' , 'Lỗi khi xóa báo cáo!' );
}
}
// Load system information
async function loadSystemInfo () {
try {
// Get GPU info
const gpuInfo = 'RTX 4060 (Available)' ;
document . getElementById ( 'gpuInfo' ). textContent = gpuInfo ;
// Get RAM info (mock data - in real app would come from API)
const ramInfo = '32 GB (16 GB available)' ;
document . getElementById ( 'ramInfo' ). textContent = ramInfo ;
} catch ( error ) {
console . error ( 'Error loading system info:' , error );
document . getElementById ( 'gpuInfo' ). textContent = 'N/A' ;
document . getElementById ( 'ramInfo' ). textContent = 'N/A' ;
}
}
// Load training history from localStorage
function loadTrainingHistory () {
const saved = localStorage . getItem ( 'trainingHistory' );
if ( saved ) {
trainingHistory = JSON . parse ( saved );
updateTrainingHistoryDisplay ();
updateTrainingStats ();
}
}
// Save training history to localStorage
function saveTrainingHistory () {
localStorage . setItem ( 'trainingHistory' , JSON . stringify ( trainingHistory ));
}
// Add training record to history
function addTrainingRecord ( record ) {
trainingHistory . unshift ( record );
if ( trainingHistory . length > 20 ) {
trainingHistory = trainingHistory . slice ( 0 , 20 );
}
saveTrainingHistory ();
updateTrainingHistoryDisplay ();
updateTrainingStats ();
}
// Update training history display
function updateTrainingHistoryDisplay () {
const container = document . getElementById ( 'trainingHistory' );
if ( trainingHistory . length === 0 ) {
container . innerHTML = '<p style="color: #999;">Chưa có lịch sử training</p>' ;
return ;
}
container . innerHTML = trainingHistory . map ( record => {
const statusIcon = record . success ? '✅' : '❌' ;
const statusColor = record . success ? '#4caf50' : '#f44336' ;
const duration = record . duration ? ` ( ${ Math . round ( record . duration / 60 ) } m)` : '' ;
return `
<div style="padding: 8px; margin-bottom: 8px; background: white; border-radius: 4px; border-left: 3px solid ${ statusColor } ;">
<div style="display: flex; justify-content: space-between; align-items: center;">
<span style="font-weight: 600;"> ${ statusIcon } ${ record . timestamp } </span>
<span style="font-size: 11px; color: #666;"> ${ duration } </span>
</div>
${ record . accuracy ? `<div style="font-size: 12px; color: #666; margin-top: 4px;">Accuracy: ${ ( record . accuracy * 100 ). toFixed ( 1 ) } %</div>` : '' }
${ record . error ? `<div style="font-size: 11px; color: #f44336; margin-top: 4px;"> ${ record . error } </div>` : '' }
</div>
` ;
}). join ( '' );
}
// Update training statistics
function updateTrainingStats () {
const total = trainingHistory . length ;
const success = trainingHistory . filter ( r => r . success ). length ;
const failed = total - success ;
document . getElementById ( 'totalTrainings' ). textContent = total ;
document . getElementById ( 'successTrainings' ). textContent = success ;
document . getElementById ( 'failedTrainings' ). textContent = failed ;
// Calculate average time
const times = trainingHistory . filter ( r => r . duration ). map ( r => r . duration );
if ( times . length > 0 ) {
const avgSeconds = times . reduce (( a , b ) => a + b , 0 ) / times . length ;
const avgMinutes = Math . round ( avgSeconds / 60 );
document . getElementById ( 'avgTime' ). textContent = ` ${ avgMinutes } phút` ;
} else {
document . getElementById ( 'avgTime' ). textContent = '-' ;
}
}
// Initialize Leaflet Map
let map , drawnItems , currentRectangle ;
function initMap () {
// Initialize map centered on Vietnam Mekong Delta
map = L . map ( 'map' ). setView ([ 9.55 , 105.9 ], 9 );
// Add OpenStreetMap tile layer
L . tileLayer ( 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png' , {
attribution : '© OpenStreetMap contributors' ,
maxZoom : 18
}). addTo ( map );
// Initialize feature group for drawn items
drawnItems = new L . FeatureGroup ();
map . addLayer ( drawnItems );
// Initialize draw control with only rectangle tool
const drawControl = new L . Control . Draw ({
draw : {
polyline : false ,
polygon : false ,
circle : false ,
marker : false ,
circlemarker : false ,
rectangle : {
shapeOptions : {
color : '#667eea' ,
weight : 3 ,
fillOpacity : 0.2
}
}
},
edit : {
featureGroup : drawnItems ,
remove : true
}
});
map . addControl ( drawControl );
// Handle rectangle creation
map . on ( L . Draw . Event . CREATED , function ( event ) {
const layer = event . layer ;
// Remove previous rectangle if exists
if ( currentRectangle ) {
drawnItems . removeLayer ( currentRectangle );
}
// Add new rectangle
drawnItems . addLayer ( layer );
currentRectangle = layer ;
// Get bounds and update form
const bounds = layer . getBounds ();
updateBboxFromMap ( bounds );
});
// Handle rectangle edit
map . on ( L . Draw . Event . EDITED , function ( event ) {
const layers = event . layers ;
layers . eachLayer ( function ( layer ) {
const bounds = layer . getBounds ();
updateBboxFromMap ( bounds );
});
});
// Handle rectangle deletion
map . on ( L . Draw . Event . DELETED , function () {
currentRectangle = null ;
document . getElementById ( 'bboxDisplay' ). textContent = 'Chưa chọn khu vực' ;
// Reset to default values
document . getElementById ( 'minLon' ). value = '' ;
document . getElementById ( 'minLat' ). value = '' ;
document . getElementById ( 'maxLon' ). value = '' ;
document . getElementById ( 'maxLat' ). value = '' ;
});
// Draw initial rectangle based on default values
drawInitialRectangle ();
}
function updateBboxFromMap ( bounds ) {
const south = bounds . getSouth (). toFixed ( 6 );
const west = bounds . getWest (). toFixed ( 6 );
const north = bounds . getNorth (). toFixed ( 6 );
const east = bounds . getEast (). toFixed ( 6 );
document . getElementById ( 'minLat' ). value = south ;
document . getElementById ( 'minLon' ). value = west ;
document . getElementById ( 'maxLat' ). value = north ;
document . getElementById ( 'maxLon' ). value = east ;
document . getElementById ( 'bboxDisplay' ). textContent =
`Lon: ${ west } → ${ east } , Lat: ${ south } → ${ north } ` ;
}
function drawInitialRectangle () {
const minLon = parseFloat ( document . getElementById ( 'minLon' ). value );
const minLat = parseFloat ( document . getElementById ( 'minLat' ). value );
const maxLon = parseFloat ( document . getElementById ( 'maxLon' ). value );
const maxLat = parseFloat ( document . getElementById ( 'maxLat' ). value );
if ( minLon && minLat && maxLon && maxLat ) {
const bounds = [[ minLat , minLon ], [ maxLat , maxLon ]];
const rectangle = L . rectangle ( bounds , {
color : '#667eea' ,
weight : 3 ,
fillOpacity : 0.2
});
drawnItems . addLayer ( rectangle );
currentRectangle = rectangle ;
map . fitBounds ( bounds );
updateBboxFromMap ( L . latLngBounds ( bounds ));
}
}
// Model type change handler
function updateModelTypeUI () {
const modelType = document . getElementById ( 'modelType' ). value ;
const desc = document . getElementById ( 'modelTypeDesc' );
const nEstimatorsGroup = document . getElementById ( 'nEstimatorsGroup' );
const learningRateGroup = document . getElementById ( 'learningRateGroup' );
const useGpuGroup = document . getElementById ( 'useGpuGroup' );
const descriptions = {
'xgboost' : '✓ XGBoost: Tốt nhất cho dữ liệu satellite, hỗ trợ GPU, training nhanh' ,
'random_forest' : '✓ Random Forest: Ổn định, không overfitting, phù hợp mọi kích thước dữ liệu' ,
'decision_tree' : '✓ Decision Tree: Đơn giản nhất, nhanh nhất, dễ hiểu, phù hợp để test nhanh' ,
'svm' : '✓ SVM: Chính xác cao với dữ liệu nhỏ, chậm với dữ liệu lớn' ,
'cnn' : '✓ CNN PyTorch: Mạnh nhất với ảnh vệ tinh, tự học features, tương thích GPU tốt, cần pip install torch'
};
desc . textContent = descriptions [ modelType ];
// Show/hide parameters based on model type
if ( modelType === 'xgboost' ) {
nEstimatorsGroup . style . display = '' ;
learningRateGroup . style . display = '' ;
useGpuGroup . style . display = '' ;
} else if ( modelType === 'random_forest' ) {
nEstimatorsGroup . style . display = '' ;
learningRateGroup . style . display = 'none' ;
useGpuGroup . style . display = 'none' ;
} else if ( modelType === 'decision_tree' ) {
nEstimatorsGroup . style . display = 'none' ;
learningRateGroup . style . display = 'none' ;
useGpuGroup . style . display = 'none' ;
} else if ( modelType === 'svm' ) {
nEstimatorsGroup . style . display = 'none' ;
learningRateGroup . style . display = 'none' ;
useGpuGroup . style . display = 'none' ;
} else if ( modelType === 'cnn' ) {
// CNN uses n_estimators as epochs and supports GPU
nEstimatorsGroup . style . display = '' ;
document . querySelector ( '#nEstimatorsGroup label' ). textContent = 'Epochs (số lần training):' ;
document . getElementById ( 'nEstimators' ). value = 50 ;
learningRateGroup . style . display = 'none' ;
useGpuGroup . style . display = '' ; // Show GPU option for CNN
}
// Reset n_estimators label for non-CNN
if ( modelType !== 'cnn' && modelType !== 'decision_tree' && modelType !== 'svm' ) {
document . querySelector ( '#nEstimatorsGroup label' ). textContent = 'N Estimators:' ;
}
}
// ============== CACHE MANAGEMENT ==============
let cacheFiles = []; // Store cache files data
// Load cache info and populate preset dropdown
async function loadCacheInfo () {
try {
const response = await fetch ( ` ${ API_BASE } /cache/info` );
const data = await response . json ();
// Update cache info display
const cacheInfoSpan = document . getElementById ( 'cacheInfo' );
if ( data . exists && data . count > 0 ) {
cacheInfoSpan . innerHTML = `✅ Có <strong> ${ data . count } </strong> file cache ( ${ data . total_size_mb } MB)` ;
cacheInfoSpan . style . color = '#28a745' ;
} else {
cacheInfoSpan . innerHTML = '❌ Chưa có cache nào' ;
cacheInfoSpan . style . color = '#6c757d' ;
}
// Populate cache preset dropdown
const cachePreset = document . getElementById ( 'cachePreset' );
cachePreset . innerHTML = '<option value="">-- Không dùng cache preset --</option>' ;
if ( data . exists && data . files && data . files . length > 0 ) {
cacheFiles = data . files ;
data . files . forEach (( file , index ) => {
const meta = file . metadata ;
const option = document . createElement ( 'option' );
option . value = index ;
// Format display text with metadata
let displayText = `Cache # ${ index + 1 } ` ;
if ( meta && meta . n_samples ) {
displayText += ` ( ${ meta . n_samples } mẫu` ;
if ( meta . start_date && meta . end_date ) {
displayText += `, ${ meta . start_date } → ${ meta . end_date } ` ;
}
if ( meta . resolution ) {
displayText += `, ${ meta . resolution } m` ;
}
displayText += ')' ;
} else {
displayText += ` ( ${ file . size_mb } MB, ${ new Date ( file . modified ). toLocaleString ( 'vi-VN' ) } )` ;
}
option . textContent = displayText ;
cachePreset . appendChild ( option );
});
}
} catch ( error ) {
console . error ( 'Error loading cache info:' , error );
document . getElementById ( 'cacheInfo' ). innerHTML = '⚠️ Lỗi kiểm tra cache' ;
}
}
// Apply cache preset to form inputs
function applyCachePreset () {
const selectIndex = document . getElementById ( 'cachePreset' ). value ;
if ( selectIndex === '' ) {
// Clear preset - no auto-fill
return ;
}
const fileData = cacheFiles [ parseInt ( selectIndex )];
if ( ! fileData || ! fileData . metadata ) {
alert ( 'Không có metadata cho cache này' );
return ;
}
const meta = fileData . metadata ;
// Fill bbox inputs
if ( meta . min_lon !== undefined ) {
document . getElementById ( 'minLon' ). value = meta . min_lon ;
document . getElementById ( 'maxLon' ). value = meta . max_lon ;
document . getElementById ( 'minLat' ). value = meta . min_lat ;
document . getElementById ( 'maxLat' ). value = meta . max_lat ;
// Update map rectangle
if ( rectangle ) {
drawnItems . removeLayer ( rectangle );
}
const bounds = L . latLngBounds (
[ meta . min_lat , meta . min_lon ],
[ meta . max_lat , meta . max_lon ]
);
rectangle = L . rectangle ( bounds , { color : '#3388ff' , weight : 3 , fillOpacity : 0.2 });
drawnItems . addLayer ( rectangle );
map . fitBounds ( bounds );
}
// Fill time range inputs
if ( meta . start_date ) {
document . getElementById ( 'startDate' ). value = meta . start_date ;
}
if ( meta . end_date ) {
document . getElementById ( 'endDate' ). value = meta . end_date ;
}
// Fill resolution if available
if ( meta . resolution ) {
document . getElementById ( 'resolution' ). value = meta . resolution ;
}
// Note to user
const presetNote = `📌 Đã áp dụng cache preset: ${ meta . n_samples || '?' } mẫu, ${ meta . start_date || '?' } → ${ meta . end_date || '?' } ` ;
console . log ( presetNote );
// Show notification
const notification = document . createElement ( 'div' );
notification . style . cssText = 'position:fixed;top:20px;right:20px;background:#28a745;color:white;padding:15px 20px;border-radius:8px;box-shadow:0 4px 6px rgba(0,0,0,0.1);z-index:10000;animation:slideIn 0.3s ease-out;' ;
notification . innerHTML = `<strong>✅ Cache Preset Applied</strong><br> ${ presetNote } ` ;
document . body . appendChild ( notification );
setTimeout (() => {
notification . style . animation = 'slideOut 0.3s ease-out' ;
setTimeout (() => notification . remove (), 300 );
}, 3000 );
}
// Clear cache
async function clearCache () {
if ( ! confirm ( 'Bạn có chắc muốn xóa toàn bộ cache?\n\nCache giúp test nhanh hơn bằng cách lưu lại dữ liệu đã download.' )) {
return ;
}
try {
const response = await fetch ( ` ${ API_BASE } /cache/clear` , {
method : 'POST'
});
const result = await response . json ();
alert ( result . message );
loadCacheInfo (); // Refresh info
} catch ( error ) {
alert ( 'Lỗi xóa cache: ' + error . message );
}
}
// Initialize map when page loads
document . addEventListener ( 'DOMContentLoaded' , function () {
initMap ();
initPredictionMap ();
loadPredictionsList (); // Load predictions list on page load
loadCacheInfo (); // Load cache info
// Add model type change listener
document . getElementById ( 'modelType' ). addEventListener ( 'change' , updateModelTypeUI );
updateModelTypeUI (); // Initial update
// Add cache preset change listener
document . getElementById ( 'cachePreset' ). addEventListener ( 'change' , applyCachePreset );
});
</ script >
</ body >
</ html >