|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>Property Visual Search - Status</title> |
|
<style> |
|
body { |
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; |
|
max-width: 800px; |
|
margin: 0 auto; |
|
padding: 20px; |
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
|
min-height: 100vh; |
|
color: white; |
|
} |
|
.container { |
|
background: rgba(255, 255, 255, 0.1); |
|
backdrop-filter: blur(10px); |
|
border-radius: 20px; |
|
padding: 30px; |
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); |
|
} |
|
h1 { |
|
text-align: center; |
|
margin-bottom: 30px; |
|
font-size: 2.5em; |
|
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); |
|
} |
|
.status-grid { |
|
display: grid; |
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); |
|
gap: 20px; |
|
margin-bottom: 30px; |
|
} |
|
.status-card { |
|
background: rgba(255, 255, 255, 0.2); |
|
border-radius: 15px; |
|
padding: 20px; |
|
text-align: center; |
|
transition: transform 0.3s ease; |
|
} |
|
.status-card:hover { |
|
transform: translateY(-5px); |
|
} |
|
.status-indicator { |
|
width: 20px; |
|
height: 20px; |
|
border-radius: 50%; |
|
display: inline-block; |
|
margin-right: 10px; |
|
} |
|
.status-ready { background-color: #4CAF50; } |
|
.status-loading { background-color: #FF9800; } |
|
.status-error { background-color: #f44336; } |
|
.progress-bar { |
|
width: 100%; |
|
height: 20px; |
|
background: rgba(255, 255, 255, 0.3); |
|
border-radius: 10px; |
|
overflow: hidden; |
|
margin: 10px 0; |
|
} |
|
.progress-fill { |
|
height: 100%; |
|
background: linear-gradient(90deg, #4CAF50, #45a049); |
|
transition: width 0.5s ease; |
|
border-radius: 10px; |
|
} |
|
.upload-section { |
|
background: rgba(255, 255, 255, 0.2); |
|
border-radius: 15px; |
|
padding: 30px; |
|
text-align: center; |
|
margin-top: 30px; |
|
} |
|
.upload-form { |
|
display: flex; |
|
flex-direction: column; |
|
align-items: center; |
|
gap: 20px; |
|
} |
|
.file-input { |
|
background: rgba(255, 255, 255, 0.2); |
|
border: 2px dashed rgba(255, 255, 255, 0.5); |
|
border-radius: 10px; |
|
padding: 20px; |
|
width: 100%; |
|
max-width: 400px; |
|
cursor: pointer; |
|
transition: all 0.3s ease; |
|
} |
|
.file-input:hover { |
|
border-color: rgba(255, 255, 255, 0.8); |
|
background: rgba(255, 255, 255, 0.3); |
|
} |
|
.submit-btn { |
|
background: linear-gradient(45deg, #4CAF50, #45a049); |
|
color: white; |
|
border: none; |
|
padding: 15px 30px; |
|
border-radius: 25px; |
|
font-size: 16px; |
|
cursor: pointer; |
|
transition: all 0.3s ease; |
|
} |
|
.submit-btn:hover { |
|
transform: translateY(-2px); |
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2); |
|
} |
|
.submit-btn:disabled { |
|
background: #ccc; |
|
cursor: not-allowed; |
|
transform: none; |
|
} |
|
.results { |
|
margin-top: 20px; |
|
padding: 20px; |
|
background: rgba(255, 255, 255, 0.1); |
|
border-radius: 10px; |
|
display: none; |
|
} |
|
.refresh-btn { |
|
background: linear-gradient(45deg, #2196F3, #1976D2); |
|
color: white; |
|
border: none; |
|
padding: 10px 20px; |
|
border-radius: 20px; |
|
cursor: pointer; |
|
margin-bottom: 20px; |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<div class="container"> |
|
<h1>π Property Visual Search</h1> |
|
|
|
<button class="refresh-btn" onclick="checkStatus()">π Refresh Status</button> |
|
|
|
<div class="status-grid"> |
|
<div class="status-card"> |
|
<h3>App Status</h3> |
|
<div id="app-status"> |
|
<span class="status-indicator status-loading"></span> |
|
Loading... |
|
</div> |
|
</div> |
|
|
|
<div class="status-card"> |
|
<h3>AI Model</h3> |
|
<div id="model-status"> |
|
<span class="status-indicator status-loading"></span> |
|
Loading... |
|
</div> |
|
</div> |
|
|
|
<div class="status-card"> |
|
<h3>Database</h3> |
|
<div id="db-status"> |
|
<span class="status-indicator status-loading"></span> |
|
Loading... |
|
</div> |
|
</div> |
|
|
|
<div class="status-card"> |
|
<h3>Property Classifier</h3> |
|
<div id="classifier-status"> |
|
<span class="status-indicator status-loading"></span> |
|
Loading... |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div class="status-card"> |
|
<h3>Initialization Progress</h3> |
|
<div id="progress-text">Loading...</div> |
|
<div class="progress-bar"> |
|
<div class="progress-fill" id="progress-fill" style="width: 0%"></div> |
|
</div> |
|
<div id="elapsed-time">Time elapsed: 0s</div> |
|
</div> |
|
|
|
<div class="upload-section"> |
|
<h3>π Search Properties</h3> |
|
<form class="upload-form" id="search-form"> |
|
<div class="file-input"> |
|
<input type="file" id="image-file" name="file" accept="image/*" required> |
|
<p>Select a property image to find similar properties</p> |
|
</div> |
|
<button type="submit" class="submit-btn" id="search-btn" disabled> |
|
π Search Properties |
|
</button> |
|
</form> |
|
<button id="refresh-status-btn" class="submit-btn" style="margin-top: 10px; background: linear-gradient(45deg, #2196F3, #1976D2);">π Refresh Status</button> |
|
|
|
<div class="results" id="results"> |
|
<h4>Search Results</h4> |
|
<div id="results-content"></div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<script> |
|
let statusCheckInterval; |
|
|
|
function checkStatus() { |
|
fetch('/status') |
|
.then(response => response.json()) |
|
.then(data => { |
|
|
|
const appStatus = document.getElementById('app-status'); |
|
appStatus.innerHTML = `<span class="status-indicator status-ready"></span>Running`; |
|
|
|
|
|
const modelStatus = document.getElementById('model-status'); |
|
if (data.model_loaded) { |
|
modelStatus.innerHTML = `<span class="status-indicator status-ready"></span>Loaded`; |
|
} else { |
|
modelStatus.innerHTML = `<span class="status-indicator status-loading"></span>Loading...`; |
|
} |
|
|
|
|
|
const dbStatus = document.getElementById('db-status'); |
|
if (data.collection_ready) { |
|
dbStatus.innerHTML = `<span class="status-indicator status-ready"></span>Ready (${data.total_images} images)`; |
|
} else { |
|
dbStatus.innerHTML = `<span class="status-indicator status-loading"></span>Loading...`; |
|
} |
|
|
|
|
|
const classifierStatus = document.getElementById('classifier-status'); |
|
if (data.property_classifier_loaded) { |
|
classifierStatus.innerHTML = `<span class="status-indicator status-ready"></span>Loaded`; |
|
} else { |
|
classifierStatus.innerHTML = `<span class="status-indicator status-loading"></span>Loading...`; |
|
} |
|
|
|
|
|
const progressText = document.getElementById('progress-text'); |
|
const progressFill = document.getElementById('progress-fill'); |
|
const elapsedTime = document.getElementById('elapsed-time'); |
|
|
|
progressText.textContent = data.initialization_status || 'Initializing...'; |
|
progressFill.style.width = `${data.initialization_progress || 0}%`; |
|
elapsedTime.textContent = `Time elapsed: ${data.elapsed_time_seconds || 0}s`; |
|
|
|
|
|
const searchBtn = document.getElementById('search-btn'); |
|
if (data.can_search) { |
|
searchBtn.disabled = false; |
|
searchBtn.textContent = 'π Search Properties'; |
|
|
|
|
|
if (statusCheckInterval) { |
|
clearInterval(statusCheckInterval); |
|
console.log('System ready - stopped status polling'); |
|
} |
|
} else { |
|
searchBtn.disabled = true; |
|
searchBtn.textContent = 'β³ System Initializing...'; |
|
} |
|
}) |
|
.catch(error => { |
|
console.error('Error checking status:', error); |
|
document.getElementById('app-status').innerHTML = |
|
`<span class="status-indicator status-error"></span>Error`; |
|
}); |
|
} |
|
|
|
|
|
statusCheckInterval = setInterval(checkStatus, 10000); |
|
|
|
|
|
checkStatus(); |
|
|
|
|
|
document.getElementById('refresh-status-btn').addEventListener('click', function() { |
|
checkStatus(); |
|
}); |
|
|
|
|
|
document.getElementById('search-form').addEventListener('submit', function(e) { |
|
e.preventDefault(); |
|
|
|
const fileInput = document.getElementById('image-file'); |
|
const file = fileInput.files[0]; |
|
|
|
if (!file) { |
|
alert('Please select an image file'); |
|
return; |
|
} |
|
|
|
const formData = new FormData(); |
|
formData.append('file', file); |
|
|
|
const searchBtn = document.getElementById('search-btn'); |
|
searchBtn.disabled = true; |
|
searchBtn.textContent = 'π Searching...'; |
|
|
|
fetch('/search', { |
|
method: 'POST', |
|
body: formData |
|
}) |
|
.then(response => response.json()) |
|
.then(data => { |
|
const results = document.getElementById('results'); |
|
const resultsContent = document.getElementById('results-content'); |
|
|
|
if (data.error) { |
|
resultsContent.innerHTML = ` |
|
<div style="color: #f44336; padding: 10px; background: rgba(244, 67, 54, 0.1); border-radius: 5px;"> |
|
<strong>Error:</strong> ${data.message || data.error} |
|
</div> |
|
`; |
|
} else if (data.results && data.results.length > 0) { |
|
let html = `<p>Found ${data.results.length} similar properties:</p>`; |
|
data.results.slice(0, 10).forEach((result, index) => { |
|
html += ` |
|
<div style="margin: 10px 0; padding: 10px; background: rgba(255, 255, 255, 0.1); border-radius: 5px;"> |
|
<strong>${index + 1}.</strong> Property ID: ${result.property_id}<br> |
|
Similarity: ${result.similarity_score}<br> |
|
${result.image_path ? `<img src="${result.image_path}" style="max-width: 200px; max-height: 150px; margin-top: 5px;">` : ''} |
|
</div> |
|
`; |
|
}); |
|
resultsContent.innerHTML = html; |
|
} else { |
|
resultsContent.innerHTML = '<p>No similar properties found.</p>'; |
|
} |
|
|
|
results.style.display = 'block'; |
|
searchBtn.disabled = false; |
|
searchBtn.textContent = 'π Search Properties'; |
|
}) |
|
.catch(error => { |
|
console.error('Error searching:', error); |
|
document.getElementById('results-content').innerHTML = |
|
'<div style="color: #f44336;">Error occurred during search. Please try again.</div>'; |
|
document.getElementById('results').style.display = 'block'; |
|
searchBtn.disabled = false; |
|
searchBtn.textContent = 'π Search Properties'; |
|
}); |
|
}); |
|
</script> |
|
</body> |
|
</html> |