Abs6187's picture
Update templates/index.html
ef65871 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Clarity AI | Medical Analysis</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<style>
* {
font-family: 'Inter', sans-serif;
}
body {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
height: 100vh;
overflow: hidden;
}
.glass-card {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.3);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
transition: all 0.2s ease;
}
.glass-card:hover {
transform: translateY(-2px);
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
transition: all 0.2s ease;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
}
.btn-primary:hover {
transform: translateY(-1px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
}
.user-message {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.bot-message {
background: rgba(248, 250, 252, 0.95);
color: #334155;
border: 1px solid rgba(226, 232, 240, 0.5);
}
.upload-zone {
transition: all 0.2s ease;
border: 2px dashed rgba(255, 255, 255, 0.4);
background: rgba(255, 255, 255, 0.1);
}
.upload-zone:hover {
border-color: rgba(255, 255, 255, 0.7);
background: rgba(255, 255, 255, 0.15);
transform: scale(1.01);
}
.upload-zone.dragover {
border-color: #ffd700;
background: rgba(255, 215, 0, 0.1);
}
.chat-container {
height: calc(100vh - 120px);
display: flex;
flex-direction: column;
}
.chat-messages {
flex: 1;
overflow-y: auto;
min-height: 0;
}
.main-container {
height: calc(100vh - 80px);
overflow-y: auto;
}
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.1);
border-radius: 10px;
}
::-webkit-scrollbar-thumb {
background: rgba(102, 126, 234, 0.6);
border-radius: 10px;
}
.fade-in {
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.glassmorphism-header {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(20px);
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
}
.progress-bar {
background: linear-gradient(90deg, #667eea, #764ba2);
transition: width 1s ease;
}
.typing-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: #667eea;
margin: 0 1px;
animation: typing 1s infinite ease-in-out;
}
.typing-dot:nth-child(1) { animation-delay: 0s; }
.typing-dot:nth-child(2) { animation-delay: 0.2s; }
.typing-dot:nth-child(3) { animation-delay: 0.4s; }
@keyframes typing {
0%, 60%, 100% { transform: scale(0.8); opacity: 0.5; }
30% { transform: scale(1); opacity: 1; }
}
</style>
</head>
<body class="antialiased text-gray-800">
<!-- Header -->
<header class="glassmorphism-header h-20 flex-shrink-0 z-50">
<nav class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-full flex justify-between items-center">
<div class="flex items-center space-x-3">
<div class="bg-gradient-to-r from-white to-gray-100 p-2 rounded-xl shadow-lg">
<svg class="w-6 h-6 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
</svg>
</div>
<div>
<h1 class="text-xl font-bold text-white">Clarity AI</h1>
<p class="text-xs text-white/80">Medical Analysis</p>
</div>
</div>
<div class="flex items-center space-x-4">
<!-- Public app: no auth controls -->
</div>
</nav>
</header>
<!-- Main Container -->
<main class="main-container">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<!-- Upload Section -->
<section class="glass-card p-6 mb-6 fade-in">
<div class="flex items-center justify-between mb-6">
<h2 class="text-2xl font-bold text-gray-800">Medical Image Analysis</h2>
<div class="hidden sm:block text-sm text-gray-500 bg-gray-100 px-3 py-1 rounded-full">Step 1</div>
</div>
<form id="upload-form" class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Model Selection -->
<div>
<label for="model-select" class="block text-lg font-semibold text-gray-700 mb-3">Choose Model</label>
<select id="model-select" name="model" class="w-full p-3 text-lg border-2 border-gray-200 focus:ring-2 focus:ring-purple-500 focus:border-purple-500 rounded-xl transition-all">
<option value="Brain Tumor">🧠 Brain Tumor</option>
<option value="Kvasir">🔬 Gastrointestinal</option>
<option value="Pneumonia">🫁 Pneumonia</option>
<option value="Skin Cancer">🩺 Skin Cancer</option>
<option value="Tuberculosis">🦠 Tuberculosis</option>
</select>
</div>
<!-- Image Upload -->
<div>
<label class="block text-lg font-semibold text-gray-700 mb-3">Upload Image</label>
<div id="upload-container">
<!-- Image Preview -->
<div id="image-preview-container" class="hidden mb-4">
<div class="relative rounded-xl overflow-hidden shadow-lg">
<img id="image-preview" src="#" alt="Preview" class="w-full h-40 object-cover">
<button type="button" id="remove-image" class="absolute top-2 right-2 bg-red-500 text-white rounded-full w-8 h-8 flex items-center justify-center hover:bg-red-600 transition-colors">×</button>
</div>
</div>
<!-- Upload Zone -->
<div id="upload-zone" class="upload-zone rounded-xl p-8 text-center cursor-pointer">
<svg class="mx-auto h-12 w-12 text-white/80 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"></path>
</svg>
<p class="text-lg font-medium text-white mb-2">Drop image here or click</p>
<p class="text-white/70 text-sm">JPG, PNG, GIF up to 10MB</p>
</div>
<input id="file-upload-hidden" name="file" type="file" accept="image/*" class="hidden"/>
</div>
<!-- Examples Gallery -->
<div class="mt-6">
<div class="flex items-center justify-between mb-3">
<h3 class="text-lg font-semibold text-gray-700">Or pick an example</h3>
<button type="button" id="refresh-examples" class="text-sm text-indigo-700 hover:underline">Refresh</button>
</div>
<div id="examples-container" class="grid grid-cols-2 sm:grid-cols-3 gap-3"></div>
<p id="examples-empty" class="text-sm text-gray-500 hidden">No examples found.</p>
</div>
</div>
<!-- Submit Button -->
<div class="lg:col-span-2">
<button type="submit" class="w-full btn-primary font-bold py-4 px-6 rounded-xl text-lg flex items-center justify-center">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
</svg>
Analyze Image
</button>
</div>
</form>
</section>
<!-- Results Section -->
<section id="results-section" class="hidden">
<div class="grid grid-cols-1 xl:grid-cols-3 gap-6">
<!-- Results Panel -->
<div class="xl:col-span-2 glass-card p-6 fade-in">
<h2 class="text-xl font-bold text-gray-800 mb-4 flex items-center">
<svg class="w-5 h-5 mr-2 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 11-6 0 3 3 0 016 0z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
</svg>
Analysis Results
</h2>
<div id="results-content" class="space-y-6">
<div id="prediction-output"></div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<h3 class="font-semibold mb-3 text-gray-700 text-center">Original Image</h3>
<img id="original-image" src="" class="w-full h-auto rounded-lg shadow-md">
</div>
<div>
<h3 class="font-semibold mb-3 text-gray-700 text-center">AI Focus Map</h3>
<div id="gradcam-container">
<img id="gradcam-image" src="" class="w-full h-auto rounded-lg shadow-md hidden">
<div id="gradcam-placeholder" class="flex items-center justify-center h-32 bg-gray-100 rounded-lg text-gray-500">
<div class="text-center">
<svg class="w-8 h-8 mx-auto mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<p class="text-sm">Not Available</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Chat Panel -->
<div id="chat-card" class="glass-card opacity-50 pointer-events-none transition-all duration-300 fade-in">
<div class="chat-container p-6">
<div class="flex items-center justify-between mb-4">
<h2 class="text-xl font-bold text-gray-800 flex items-center">
<svg class="w-5 h-5 mr-2 text-purple-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"></path>
</svg>
AI Assistant
</h2>
<div id="chat-status" class="bg-yellow-100 text-yellow-800 px-2 py-1 rounded-full text-xs">Waiting</div>
</div>
<div class="chat-messages mb-4 p-3 bg-gray-50 rounded-xl border space-y-3">
<div id="chat-messages">
<div class="bot-message p-3 rounded-xl max-w-[90%]">
<div class="flex items-center mb-2">
<div class="w-2 h-2 bg-yellow-400 rounded-full mr-2"></div>
<span class="text-xs text-gray-500">SYSTEM</span>
</div>
Run an analysis to activate the AI assistant.
</div>
</div>
</div>
<div id="predefined-prompts-container" class="flex flex-wrap gap-2 mb-4 empty:hidden"></div>
<form id="chat-form" class="flex bg-white rounded-xl shadow-sm overflow-hidden">
<input type="text" id="chat-input" class="flex-1 p-3 focus:outline-none" placeholder="Ask about results..." disabled>
<button type="submit" id="chat-submit" class="btn-primary px-4 flex items-center rounded-none" disabled>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"></path>
</svg>
</button>
</form>
</div>
</div>
</div>
</section>
</div>
</main>
<script>
// Element references
const uploadForm = document.getElementById('upload-form');
const fileUploadHidden = document.getElementById('file-upload-hidden');
const uploadZone = document.getElementById('upload-zone');
const imagePreviewContainer = document.getElementById('image-preview-container');
const imagePreview = document.getElementById('image-preview');
const removeImageBtn = document.getElementById('remove-image');
const resultsSection = document.getElementById('results-section');
const predictionOutput = document.getElementById('prediction-output');
const originalImage = document.getElementById('original-image');
const gradcamImage = document.getElementById('gradcam-image');
const gradcamPlaceholder = document.getElementById('gradcam-placeholder');
const chatCard = document.getElementById('chat-card');
const chatStatus = document.getElementById('chat-status');
const chatMessages = document.getElementById('chat-messages');
const chatInput = document.getElementById('chat-input');
const chatSubmit = document.getElementById('chat-submit');
const chatForm = document.getElementById('chat-form');
const promptsContainer = document.getElementById('predefined-prompts-container');
const modelSelect = document.getElementById('model-select');
const examplesContainer = document.getElementById('examples-container');
const examplesEmpty = document.getElementById('examples-empty');
const refreshExamplesBtn = document.getElementById('refresh-examples');
let predictionContext = null;
let chatHistory = [];
let cachedExamples = [];
// File upload handling
uploadZone.addEventListener('click', () => fileUploadHidden.click());
uploadZone.addEventListener('dragover', (e) => {
e.preventDefault();
uploadZone.classList.add('dragover');
});
uploadZone.addEventListener('dragleave', () => {
uploadZone.classList.remove('dragover');
});
uploadZone.addEventListener('drop', (e) => {
e.preventDefault();
uploadZone.classList.remove('dragover');
const files = e.dataTransfer.files;
if (files.length > 0) {
fileUploadHidden.files = files;
handleFileSelect(files[0]);
}
});
fileUploadHidden.addEventListener('change', function() {
if (this.files[0]) {
handleFileSelect(this.files[0]);
}
});
removeImageBtn.addEventListener('click', () => {
imagePreviewContainer.classList.add('hidden');
uploadZone.style.display = 'block';
fileUploadHidden.value = '';
});
function handleFileSelect(file) {
if (!file.type.startsWith('image/')) {
alert('Please select an image file.');
return;
}
if (file.size > 10 * 1024 * 1024) {
alert('File size must be less than 10MB.');
return;
}
const reader = new FileReader();
reader.onload = (e) => {
imagePreview.src = e.target.result;
imagePreviewContainer.classList.remove('hidden');
uploadZone.style.display = 'none';
};
reader.readAsDataURL(file);
}
uploadForm.addEventListener('submit', async (e) => {
e.preventDefault();
if (!fileUploadHidden.files[0]) {
alert("Please select an image file first.");
return;
}
const submitButton = uploadForm.querySelector('button[type="submit"]');
const originalText = submitButton.innerHTML;
submitButton.disabled = true;
submitButton.innerHTML = `
<div class="flex items-center">
<div class="flex space-x-1 mr-2">
<div class="typing-dot"></div>
<div class="typing-dot"></div>
<div class="typing-dot"></div>
</div>
Analyzing...
</div>
`;
// Reset states
resultsSection.classList.add('hidden');
chatCard.classList.add('opacity-50', 'pointer-events-none');
chatStatus.textContent = 'Analyzing';
chatStatus.className = 'bg-blue-100 text-blue-800 px-2 py-1 rounded-full text-xs';
chatInput.disabled = true;
chatSubmit.disabled = true;
chatMessages.innerHTML = `
<div class="bot-message p-3 rounded-xl max-w-[90%]">
<div class="flex items-center mb-2">
<div class="flex space-x-1 mr-2">
<div class="typing-dot"></div>
<div class="typing-dot"></div>
<div class="typing-dot"></div>
</div>
<span class="text-xs text-gray-500">ANALYZING</span>
</div>
Processing your medical image...
</div>
`;
predictionContext = null;
chatHistory = [];
promptsContainer.innerHTML = '';
try {
const formData = new FormData(uploadForm);
const response = await fetch('/predict', { method: 'POST', body: formData });
const data = await response.json();
if (data.error) {
alert(`Error: ${data.error}`);
return;
}
predictionContext = data;
resultsSection.classList.remove('hidden');
predictionOutput.innerHTML = `
<div class="bg-gradient-to-r from-indigo-50 to-purple-50 p-6 rounded-xl border border-indigo-200">
<div class="flex items-center justify-between mb-4">
<span class="text-sm font-semibold text-indigo-600 bg-indigo-100 px-3 py-1 rounded-full">${data.model_used}</span>
<span class="text-2xl font-bold text-gray-900">${(data.confidence * 100).toFixed(1)}%</span>
</div>
<h3 class="text-2xl font-bold text-gray-900 mb-3">${data.prediction}</h3>
<div class="bg-gray-200 rounded-full h-3 overflow-hidden">
<div class="progress-bar h-3 rounded-full" style="width: ${(data.confidence * 100).toFixed(0)}%"></div>
</div>
</div>
`;
originalImage.src = `${data.original_image}?t=${new Date().getTime()}`;
if (data.gradcam_image) {
gradcamImage.src = `${data.gradcam_image}?t=${new Date().getTime()}`;
gradcamImage.classList.remove('hidden');
gradcamPlaceholder.classList.add('hidden');
} else {
gradcamImage.classList.add('hidden');
gradcamPlaceholder.classList.remove('hidden');
}
// Activate chat
chatCard.classList.remove('opacity-50', 'pointer-events-none');
chatStatus.textContent = 'Active';
chatStatus.className = 'bg-green-100 text-green-800 px-2 py-1 rounded-full text-xs';
chatMessages.innerHTML = '';
addMessage("Analysis complete! Ask me anything about the results.", 'bot');
chatInput.disabled = false;
chatSubmit.disabled = false;
if (data.predefined_prompts) {
data.predefined_prompts.forEach(prompt => {
const btn = document.createElement('button');
btn.textContent = prompt;
btn.className = 'bg-indigo-100 text-indigo-700 text-sm px-3 py-1 rounded-full hover:bg-indigo-200 transition-all';
btn.onclick = () => {
chatInput.value = prompt;
chatForm.requestSubmit();
};
promptsContainer.appendChild(btn);
});
}
} catch (error) {
alert('An unexpected error occurred.');
console.error("Prediction Error:", error);
} finally {
submitButton.disabled = false;
submitButton.innerHTML = originalText;
}
});
chatForm.addEventListener('submit', async (e) => {
e.preventDefault();
const message = chatInput.value.trim();
if (!message || !predictionContext) return;
addMessage(message, 'user');
chatHistory.push({ role: 'user', parts: [message] });
chatInput.value = '';
promptsContainer.innerHTML = '';
const thinkingMsg = addMessage('', 'bot');
thinkingMsg.innerHTML = `
<div class="flex items-center">
<div class="flex space-x-1 mr-2">
<div class="typing-dot"></div>
<div class="typing-dot"></div>
<div class="typing-dot"></div>
</div>
<span class="text-sm text-gray-500">Thinking...</span>
</div>
`;
try {
const response = await fetch('/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message, context: predictionContext, history: chatHistory })
});
const data = await response.json();
thinkingMsg.remove();
if (data.error) {
addMessage(`Error: ${data.error}`, 'bot');
chatHistory.pop();
} else {
addMessage(data.response, 'bot');
chatHistory.push({ role: 'model', parts: [data.response] });
}
} catch (error) {
thinkingMsg.remove();
addMessage('Connection error. Please try again.', 'bot');
chatHistory.pop();
console.error("Chat Error:", error);
}
});
function addMessage(text, type) {
const msgDiv = document.createElement('div');
msgDiv.className = `p-3 rounded-xl max-w-[90%] ${type}-message mb-3`;
if (type === 'user') {
msgDiv.classList.add('ml-auto', 'text-right');
msgDiv.textContent = text;
} else {
msgDiv.classList.add('mr-auto');
if (text) {
msgDiv.innerHTML = marked.parse(text);
}
}
chatMessages.appendChild(msgDiv);
chatMessages.scrollTop = chatMessages.scrollHeight;
return msgDiv;
}
// Load example images when model changes
async function loadExamples() {
try {
const res = await fetch('/example_images');
const data = await res.json();
cachedExamples = Array.isArray(data.images) ? data.images : [];
renderExamples();
} catch (e) {
console.error('Failed to load examples', e);
cachedExamples = [];
renderExamples();
}
}
function renderExamples() {
examplesContainer.innerHTML = '';
if (!cachedExamples.length) {
examplesEmpty.classList.remove('hidden');
return;
}
examplesEmpty.classList.add('hidden');
// Show up to 9 examples
cachedExamples.slice(0, 9).forEach((url) => {
const card = document.createElement('button');
card.type = 'button';
card.className = 'relative group rounded-xl overflow-hidden shadow hover:shadow-lg transition focus:outline-none focus:ring-2 focus:ring-indigo-500 text-left';
card.title = 'Use this example';
card.onclick = () => selectExample(url);
const name = decodeURIComponent(url.split('/').pop() || 'example');
card.innerHTML = `
<img src="${url}" alt="${name}" class="w-full h-24 object-cover">
<div class="px-2 py-1 text-xs bg-white text-gray-700 truncate">${name}</div>
`;
examplesContainer.appendChild(card);
});
}
async function selectExample(url) {
try {
const resp = await fetch(url);
const blob = await resp.blob();
const inferredName = url.split('/').pop() || 'example.png';
const file = new File([blob], inferredName, { type: blob.type || 'image/png' });
// Put into the hidden file input
const dt = new DataTransfer();
dt.items.add(file);
fileUploadHidden.files = dt.files;
// Reuse existing preview logic
handleFileSelect(file);
} catch (e) {
console.error('Failed to select example', e);
alert('Unable to load example image.');
}
}
modelSelect.addEventListener('change', loadExamples);
refreshExamplesBtn.addEventListener('click', loadExamples);
// Initial load for default selection
loadExamples();
// Smooth scrolling
document.documentElement.style.scrollBehavior = 'smooth';
</script>
</body>
</html>