|
<!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 - AI-Powered Image Recognition</title>
|
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: 'Inter', sans-serif;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
min-height: 100vh;
|
|
color: #333;
|
|
}
|
|
|
|
.container {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
}
|
|
|
|
.header {
|
|
text-align: center;
|
|
margin-bottom: 40px;
|
|
color: white;
|
|
}
|
|
|
|
.header h1 {
|
|
font-size: 2.5rem;
|
|
font-weight: 700;
|
|
margin-bottom: 10px;
|
|
text-shadow: 0 2px 4px rgba(0,0,0,0.3);
|
|
}
|
|
|
|
.header p {
|
|
font-size: 1.1rem;
|
|
opacity: 0.9;
|
|
font-weight: 300;
|
|
}
|
|
|
|
.main-content {
|
|
background: white;
|
|
border-radius: 20px;
|
|
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
|
|
overflow: hidden;
|
|
margin-bottom: 30px;
|
|
}
|
|
|
|
.upload-section {
|
|
padding: 40px;
|
|
text-align: center;
|
|
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
|
}
|
|
|
|
.upload-area {
|
|
border: 3px dashed #667eea;
|
|
border-radius: 15px;
|
|
padding: 60px 20px;
|
|
background: white;
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
margin-bottom: 30px;
|
|
}
|
|
|
|
.upload-area:hover {
|
|
border-color: #764ba2;
|
|
background: #f8f9ff;
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.upload-area.dragover {
|
|
border-color: #764ba2;
|
|
background: #f0f2ff;
|
|
transform: scale(1.02);
|
|
}
|
|
|
|
.upload-icon {
|
|
font-size: 4rem;
|
|
color: #667eea;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.upload-text {
|
|
font-size: 1.2rem;
|
|
color: #666;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.upload-subtext {
|
|
font-size: 0.9rem;
|
|
color: #999;
|
|
}
|
|
|
|
.file-input {
|
|
display: none;
|
|
}
|
|
|
|
.upload-btn {
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
color: white;
|
|
border: none;
|
|
padding: 15px 30px;
|
|
border-radius: 50px;
|
|
font-size: 1rem;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
margin-top: 20px;
|
|
}
|
|
|
|
.upload-btn:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
|
|
}
|
|
|
|
.upload-btn:disabled {
|
|
opacity: 0.6;
|
|
cursor: not-allowed;
|
|
transform: none;
|
|
}
|
|
|
|
.status-section {
|
|
padding: 20px 40px;
|
|
background: #f8f9fa;
|
|
border-top: 1px solid #e9ecef;
|
|
}
|
|
|
|
.status-indicator {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.status-dot {
|
|
width: 10px;
|
|
height: 10px;
|
|
border-radius: 50%;
|
|
background: #dc3545;
|
|
}
|
|
|
|
.status-dot.ready {
|
|
background: #28a745;
|
|
}
|
|
|
|
.status-dot.loading {
|
|
background: #ffc107;
|
|
animation: pulse 1.5s infinite;
|
|
}
|
|
|
|
@keyframes pulse {
|
|
0%, 100% { opacity: 1; }
|
|
50% { opacity: 0.5; }
|
|
}
|
|
|
|
.results-section {
|
|
padding: 40px;
|
|
display: none;
|
|
}
|
|
|
|
.results-header {
|
|
text-align: center;
|
|
margin-bottom: 30px;
|
|
}
|
|
|
|
.results-header h2 {
|
|
color: #333;
|
|
font-size: 1.8rem;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.results-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
gap: 20px;
|
|
margin-top: 30px;
|
|
}
|
|
|
|
.result-card {
|
|
background: white;
|
|
border-radius: 15px;
|
|
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
|
|
overflow: hidden;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.result-card:hover {
|
|
transform: translateY(-5px);
|
|
box-shadow: 0 15px 30px rgba(0,0,0,0.15);
|
|
}
|
|
|
|
.result-image {
|
|
width: 100%;
|
|
height: 200px;
|
|
background: #f8f9fa;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: #999;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.result-image img {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
border-radius: 0;
|
|
}
|
|
|
|
.result-card img {
|
|
width: 100%;
|
|
height: 200px;
|
|
object-fit: cover;
|
|
border-radius: 0;
|
|
}
|
|
|
|
.result-info {
|
|
padding: 20px;
|
|
}
|
|
|
|
.result-title {
|
|
font-weight: 600;
|
|
color: #333;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.result-details {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.similarity-score {
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
color: white;
|
|
padding: 5px 12px;
|
|
border-radius: 20px;
|
|
font-size: 0.8rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.property-id {
|
|
color: #666;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.loading-spinner {
|
|
display: none;
|
|
text-align: center;
|
|
padding: 40px;
|
|
}
|
|
|
|
.spinner {
|
|
border: 4px solid #f3f3f3;
|
|
border-top: 4px solid #667eea;
|
|
border-radius: 50%;
|
|
width: 40px;
|
|
height: 40px;
|
|
animation: spin 1s linear infinite;
|
|
margin: 0 auto 20px;
|
|
}
|
|
|
|
@keyframes spin {
|
|
0% { transform: rotate(0deg); }
|
|
100% { transform: rotate(360deg); }
|
|
}
|
|
|
|
.error-message {
|
|
background: #f8d7da;
|
|
color: #721c24;
|
|
padding: 15px;
|
|
border-radius: 10px;
|
|
margin: 20px 0;
|
|
display: none;
|
|
}
|
|
|
|
.success-message {
|
|
background: #d4edda;
|
|
color: #155724;
|
|
padding: 15px;
|
|
border-radius: 10px;
|
|
margin: 20px 0;
|
|
display: none;
|
|
}
|
|
|
|
.preview-image {
|
|
max-width: 100%;
|
|
max-height: 300px;
|
|
border-radius: 10px;
|
|
margin: 20px 0;
|
|
display: none;
|
|
}
|
|
|
|
.features {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
gap: 20px;
|
|
margin-top: 30px;
|
|
}
|
|
|
|
.feature-card {
|
|
background: white;
|
|
padding: 30px;
|
|
border-radius: 15px;
|
|
text-align: center;
|
|
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.feature-icon {
|
|
font-size: 2.5rem;
|
|
color: #667eea;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.feature-title {
|
|
font-size: 1.2rem;
|
|
font-weight: 600;
|
|
margin-bottom: 10px;
|
|
color: #333;
|
|
}
|
|
|
|
.feature-description {
|
|
color: #666;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.header h1 {
|
|
font-size: 2rem;
|
|
}
|
|
|
|
.upload-section {
|
|
padding: 20px;
|
|
}
|
|
|
|
.upload-area {
|
|
padding: 40px 20px;
|
|
}
|
|
|
|
.results-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="header">
|
|
<h1><i class="fas fa-search"></i> Property Visual Search</h1>
|
|
<p>AI-Powered Image Recognition for Real Estate</p>
|
|
</div>
|
|
|
|
<div class="main-content">
|
|
<div class="upload-section">
|
|
<div class="upload-area" id="uploadArea">
|
|
<div class="upload-icon">
|
|
<i class="fas fa-cloud-upload-alt"></i>
|
|
</div>
|
|
<div class="upload-text">Drag & Drop your property image here</div>
|
|
<div class="upload-subtext">or click to browse files (JPG, PNG, GIF)</div>
|
|
<input type="file" id="fileInput" class="file-input" accept="image/*">
|
|
<button class="upload-btn" id="uploadBtn" onclick="document.getElementById('fileInput').click()">
|
|
<i class="fas fa-upload"></i> Choose Image
|
|
</button>
|
|
</div>
|
|
|
|
<img id="previewImage" class="preview-image" alt="Preview">
|
|
|
|
<div class="error-message" id="errorMessage"></div>
|
|
<div class="success-message" id="successMessage"></div>
|
|
</div>
|
|
|
|
<div class="status-section">
|
|
<div class="status-indicator">
|
|
<div class="status-dot" id="statusDot"></div>
|
|
<span id="statusText">Initializing visual search system...</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="loading-spinner" id="loadingSpinner">
|
|
<div class="spinner"></div>
|
|
<p>Analyzing your image and finding similar properties...</p>
|
|
</div>
|
|
|
|
<div class="results-section" id="resultsSection">
|
|
<div class="results-header">
|
|
<h2>Similar Properties Found</h2>
|
|
<p>Based on visual similarity analysis</p>
|
|
</div>
|
|
<div class="results-grid" id="resultsGrid"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="features">
|
|
<div class="feature-card">
|
|
<div class="feature-icon">
|
|
<i class="fas fa-brain"></i>
|
|
</div>
|
|
<div class="feature-title">AI-Powered</div>
|
|
<div class="feature-description">
|
|
Advanced machine learning algorithms analyze visual patterns and architectural features
|
|
</div>
|
|
</div>
|
|
|
|
<div class="feature-card">
|
|
<div class="feature-icon">
|
|
<i class="fas fa-bolt"></i>
|
|
</div>
|
|
<div class="feature-title">Lightning Fast</div>
|
|
<div class="feature-description">
|
|
Get instant results with our optimized image processing and similarity matching
|
|
</div>
|
|
</div>
|
|
|
|
<div class="feature-card">
|
|
<div class="feature-icon">
|
|
<i class="fas fa-shield-alt"></i>
|
|
</div>
|
|
<div class="feature-title">Secure & Private</div>
|
|
<div class="feature-description">
|
|
Your uploaded images are processed securely and never stored permanently
|
|
</div>
|
|
</div>
|
|
|
|
<div class="feature-card">
|
|
<div class="feature-icon">
|
|
<i class="fas fa-chart-line"></i>
|
|
</div>
|
|
<div class="feature-title">Smart Matching</div>
|
|
<div class="feature-description">
|
|
Find properties with similar architectural styles, layouts, and visual characteristics
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
let isSystemReady = false;
|
|
|
|
|
|
checkSystemStatus();
|
|
|
|
|
|
const uploadArea = document.getElementById('uploadArea');
|
|
const fileInput = document.getElementById('fileInput');
|
|
const previewImage = document.getElementById('previewImage');
|
|
const uploadBtn = document.getElementById('uploadBtn');
|
|
const loadingSpinner = document.getElementById('loadingSpinner');
|
|
const resultsSection = document.getElementById('resultsSection');
|
|
const resultsGrid = document.getElementById('resultsGrid');
|
|
const errorMessage = document.getElementById('errorMessage');
|
|
const successMessage = document.getElementById('successMessage');
|
|
|
|
|
|
uploadArea.addEventListener('dragover', (e) => {
|
|
e.preventDefault();
|
|
uploadArea.classList.add('dragover');
|
|
});
|
|
|
|
uploadArea.addEventListener('dragleave', () => {
|
|
uploadArea.classList.remove('dragover');
|
|
});
|
|
|
|
uploadArea.addEventListener('drop', (e) => {
|
|
e.preventDefault();
|
|
uploadArea.classList.remove('dragover');
|
|
const files = e.dataTransfer.files;
|
|
if (files.length > 0) {
|
|
handleFile(files[0]);
|
|
}
|
|
});
|
|
|
|
fileInput.addEventListener('change', (e) => {
|
|
if (e.target.files.length > 0) {
|
|
handleFile(e.target.files[0]);
|
|
}
|
|
});
|
|
|
|
function handleFile(file) {
|
|
if (!isSystemReady) {
|
|
showError('Visual search system is still initializing. Please wait a moment and try again.');
|
|
return;
|
|
}
|
|
|
|
if (!file.type.startsWith('image/')) {
|
|
showError('Please select a valid image file.');
|
|
return;
|
|
}
|
|
|
|
|
|
const reader = new FileReader();
|
|
reader.onload = (e) => {
|
|
previewImage.src = e.target.result;
|
|
previewImage.style.display = 'block';
|
|
};
|
|
reader.readAsDataURL(file);
|
|
|
|
|
|
uploadAndSearch(file);
|
|
}
|
|
|
|
function uploadAndSearch(file) {
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
|
|
|
|
loadingSpinner.style.display = 'block';
|
|
resultsSection.style.display = 'none';
|
|
hideMessages();
|
|
|
|
fetch('/search', {
|
|
method: 'POST',
|
|
body: formData
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
loadingSpinner.style.display = 'none';
|
|
|
|
if (data.error) {
|
|
showError(data.error);
|
|
} else if (data.message) {
|
|
showSuccess(data.message);
|
|
} else if (data.results) {
|
|
displayResults(data.results);
|
|
}
|
|
})
|
|
.catch(error => {
|
|
loadingSpinner.style.display = 'none';
|
|
showError('An error occurred while processing your image. Please try again.');
|
|
console.error('Error:', error);
|
|
});
|
|
}
|
|
|
|
function displayResults(results) {
|
|
resultsSection.style.display = 'block';
|
|
resultsGrid.innerHTML = '';
|
|
|
|
results.forEach((result, index) => {
|
|
const resultCard = document.createElement('div');
|
|
resultCard.className = 'result-card';
|
|
|
|
|
|
let imageHtml = '';
|
|
if (result.image_path) {
|
|
imageHtml = `<img src="${result.image_path}" alt="Property ${index + 1}" style="width: 100%; height: 200px; object-fit: cover;" onerror="this.parentElement.innerHTML='<div class=\\'result-image\\'><i class=\\'fas fa-home\\' style=\\'font-size: 3rem; color: #ddd;\\'></i></div>'">`;
|
|
} else {
|
|
imageHtml = `<div class="result-image">
|
|
<i class="fas fa-home" style="font-size: 3rem; color: #ddd;"></i>
|
|
</div>`;
|
|
}
|
|
|
|
resultCard.innerHTML = `
|
|
${imageHtml}
|
|
<div class="result-info">
|
|
<div class="result-title">Property ${index + 1}</div>
|
|
<div class="result-details">
|
|
<span class="similarity-score">${result.similarity_score}</span>
|
|
<span class="property-id">ID: ${result.property_id}</span>
|
|
</div>
|
|
<div style="font-size: 0.8rem; color: #999;">
|
|
Distance: ${result.distance}
|
|
</div>
|
|
</div>
|
|
`;
|
|
resultsGrid.appendChild(resultCard);
|
|
});
|
|
|
|
showSuccess(`Found ${results.length} similar properties!`);
|
|
}
|
|
|
|
function checkSystemStatus() {
|
|
fetch('/status')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
const statusDot = document.getElementById('statusDot');
|
|
const statusText = document.getElementById('statusText');
|
|
|
|
if (data.model_loaded && data.collection_ready) {
|
|
statusDot.className = 'status-dot ready';
|
|
statusText.textContent = `System ready! ${data.total_images} properties indexed`;
|
|
isSystemReady = true;
|
|
} else {
|
|
statusDot.className = 'status-dot loading';
|
|
statusText.textContent = 'Initializing system... Please wait';
|
|
isSystemReady = false;
|
|
|
|
|
|
setTimeout(checkSystemStatus, 5000);
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error checking status:', error);
|
|
setTimeout(checkSystemStatus, 5000);
|
|
});
|
|
}
|
|
|
|
function showError(message) {
|
|
errorMessage.textContent = message;
|
|
errorMessage.style.display = 'block';
|
|
successMessage.style.display = 'none';
|
|
}
|
|
|
|
function showSuccess(message) {
|
|
successMessage.textContent = message;
|
|
successMessage.style.display = 'block';
|
|
errorMessage.style.display = 'none';
|
|
}
|
|
|
|
function hideMessages() {
|
|
errorMessage.style.display = 'none';
|
|
successMessage.style.display = 'none';
|
|
}
|
|
</script>
|
|
</body>
|
|
</html> |