mi23's picture
add polish version
5849fc4 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Fungus Among Us Classifier</title>
<link rel="icon" type="image/x-icon" href="/static/favicon.ico">
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/feather-icons"></script>
<script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
<style>
.dropzone {
border: 2px dashed #9CA3AF;
transition: all 0.3s ease;
}
.dropzone-active {
border-color: #10B981;
background-color: rgba(16, 185, 129, 0.05);
}
.dropzone-reject {
border-color: #EF4444;
background-color: rgba(239, 68, 68, 0.05);
}
.progress-bar {
transition: width 0.3s ease;
}
.result-card {
transition: all 0.3s ease;
}
.result-card:hover {
transform: translateY(-2px);
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
</style>
</head>
<body class="bg-gray-50 min-h-screen">
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<!-- Header -->
<header class="text-center mb-8">
<div class="flex justify-between items-center mb-4">
<div class="flex items-center">
<i data-feather="moon" class="w-8 h-8 text-emerald-600 mr-2"></i>
<h1 id="titleText" class="text-2xl font-bold text-gray-800">Fungus Among Us</h1>
</div>
<div class="relative">
<select id="languageSelect" class="appearance-none bg-white border border-gray-300 rounded-md px-3 py-1 text-sm focus:outline-none focus:ring-1 focus:ring-emerald-500">
<option value="en">English</option>
<option value="es">Español</option>
<option value="fr">Français</option>
<option value="de">Deutsch</option>
<option value="pl">Polski</option>
</select>
<i data-feather="chevron-down" class="absolute right-2 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-500 pointer-events-none"></i>
</div>
</div>
<p id="subtitleText" class="text-gray-600 max-w-2xl mx-auto">
Upload mushroom images to identify species with our AI classifier.
Supports common edible and poisonous varieties.
</p>
</header>
<!-- Main Content -->
<main>
<!-- Upload Section -->
<section class="mb-12">
<div id="dropzone" class="dropzone rounded-xl p-8 md:p-12 text-center cursor-pointer bg-white shadow-sm">
<div class="max-w-md mx-auto">
<i data-feather="upload-cloud" class="w-12 h-12 text-emerald-500 mx-auto mb-4"></i>
<h2 id="uploadTitle" class="text-xl font-semibold text-gray-800 mb-2">Upload Mushroom Image</h2>
<p id="uploadSubtitle" class="text-gray-500 mb-6">Drag & drop or click to browse (JPG, PNG up to 5MB)</p>
<input type="file" id="fileInput" class="hidden" accept="image/jpeg,image/png" />
<button id="uploadBtn" class="px-6 py-2 bg-emerald-500 text-white rounded-lg hover:bg-emerald-600 transition">
<span id="selectFileText">Select File</span>
</button>
<p id="privacyText" class="text-xs text-gray-400 mt-4">We don't store your images after processing</p>
</div>
</div>
</section>
<!-- Status Section -->
<section id="statusSection" class="hidden mb-12">
<div class="bg-white rounded-xl p-6 shadow-sm">
<h2 id="processingText" class="text-xl font-semibold text-gray-800 mb-4">Processing</h2>
<div class="flex items-center mb-4">
<div class="flex-1 bg-gray-200 rounded-full h-2.5">
<div id="progressBar" class="progress-bar bg-emerald-500 h-2.5 rounded-full" style="width: 0%"></div>
</div>
<span id="progressText" class="ml-4 text-sm font-medium text-gray-700">0%</span>
</div>
<div class="flex items-center">
<div class="animate-spin rounded-full h-5 w-5 border-b-2 border-emerald-500 mr-3"></div>
<p id="statusMessage" class="text-gray-600"><span id="uploadingText">Uploading image</span>...</p>
</div>
</div>
</section>
<!-- Results Section -->
<section id="resultsSection" class="hidden">
<h2 id="resultsTitle" class="text-2xl font-semibold text-gray-800 mb-6">Classification Results</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Image Preview -->
<div class="result-card bg-white rounded-xl p-4 shadow-sm">
<h3 id="yourImageText" class="text-lg font-medium text-gray-800 mb-3">Your Image</h3>
<div class="rounded-lg overflow-hidden bg-gray-100">
<img id="previewImage" src="" alt="Uploaded mushroom" class="w-full h-64 object-contain" />
</div>
</div>
<!-- Classification Results -->
<div class="result-card bg-white rounded-xl p-4 shadow-sm">
<h3 id="predictionText" class="text-lg font-medium text-gray-800 mb-3">Prediction</h3>
<div id="resultsContainer" class="space-y-4">
<!-- Results will be populated here -->
</div>
</div>
</div>
<div class="mt-6 text-center">
<button id="newUploadBtn" class="px-6 py-2 border border-emerald-500 text-emerald-500 rounded-lg hover:bg-emerald-50 transition">
<i data-feather="plus" class="w-4 h-4 inline mr-2"></i> <span id="uploadAnotherText">Upload Another</span>
</button>
</div>
</section>
<!-- Error Section -->
<section id="errorSection" class="hidden">
<div class="bg-red-50 border-l-4 border-red-500 p-4 rounded-lg">
<div class="flex">
<div class="flex-shrink-0">
<i data-feather="alert-triangle" class="h-5 w-5 text-red-500"></i>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-red-800" id="errorTitle">Error</h3>
<div class="mt-2 text-sm text-red-700" id="errorMessage">
<!-- Error message will be inserted here -->
</div>
<div class="mt-4">
<button id="tryAgainBtn" class="px-4 py-1 text-sm font-medium text-red-800 hover:text-red-700">
Try Again
</button>
</div>
</div>
</div>
</div>
</section>
</main>
<!-- Footer -->
<footer class="mt-16 text-center text-gray-500 text-sm">
<p id="copyrightText">© 2023 Fungus Among Us Classifier. Not responsible for foraging decisions.</p>
<p id="disclaimerText" class="mt-1">Always consult an expert before consuming wild mushrooms.</p>
</footer>
</div>
<script>
// Initialize
feather.replace();
// Translations
const translations = {
en: {
title: "Fungus Among Us",
subtitle: "Upload mushroom images to identify species with our AI classifier. Supports common edible and poisonous varieties.",
uploadTitle: "Upload Mushroom Image",
uploadSubtitle: "Drag & drop or click to browse (JPG, PNG up to 5MB)",
selectFile: "Select File",
privacy: "We don't store your images after processing",
processing: "Processing",
uploading: "Uploading image",
resultsTitle: "Classification Results",
yourImage: "Your Image",
prediction: "Prediction",
uploadAnother: "Upload Another",
copyright: "© 2023 Fungus Among Us Classifier. Not responsible for foraging decisions.",
disclaimer: "Always consult an expert before consuming wild mushrooms."
},
es: {
title: "Hongo Entre Nosotros",
subtitle: "Sube imágenes de hongos para identificar especies con nuestro clasificador de IA. Soporta variedades comestibles y venenosas comunes.",
uploadTitle: "Subir Imagen de Hongo",
uploadSubtitle: "Arrastra y suelta o haz clic para buscar (JPG, PNG hasta 5MB)",
selectFile: "Seleccionar Archivo",
privacy: "No almacenamos tus imágenes después del procesamiento",
processing: "Procesando",
uploading: "Subiendo imagen",
resultsTitle: "Resultados de Clasificación",
yourImage: "Tu Imagen",
prediction: "Predicción",
uploadAnother: "Subir Otro",
copyright: "© 2023 Clasificador de Hongos Entre Nosotros. No responsable de decisiones de recolección.",
disclaimer: "Siempre consulta a un experto antes de consumir hongos silvestres."
},
fr: {
title: "Champignon Parmi Nous",
subtitle: "Téléchargez des images de champignons pour identifier les espèces avec notre classificateur IA. Prend en charge les variétés comestibles et vénéneuses courantes.",
uploadTitle: "Télécharger une Image de Champignon",
uploadSubtitle: "Glisser-déposer ou cliquer pour parcourir (JPG, PNG jusqu'à 5MB)",
selectFile: "Sélectionner un Fichier",
privacy: "Nous ne stockons pas vos images après traitement",
processing: "Traitement en cours",
uploading: "Téléchargement de l'image",
resultsTitle: "Résultats de Classification",
yourImage: "Votre Image",
prediction: "Prédiction",
uploadAnother: "Télécharger un Autre",
copyright: "© 2023 Classificateur de Champignons Parmi Nous. Non responsable des décisions de cueillette.",
disclaimer: "Consultez toujours un expert avant de consommer des champignons sauvages."
},
de: {
title: "Pilz Unter Uns",
subtitle: "Laden Sie Pilzbilder hoch, um Arten mit unserem KI-Klassifikator zu identifizieren. Unterstützt häufige essbare und giftige Sorten.",
uploadTitle: "Pilzbild Hochladen",
uploadSubtitle: "Ziehen und ablegen oder klicken, um zu durchsuchen (JPG, PNG bis zu 5MB)",
selectFile: "Datei Auswählen",
privacy: "Wir speichern Ihre Bilder nicht nach der Verarbeitung",
processing: "Wird Verarbeitet",
uploading: "Bild wird Hochgeladen",
resultsTitle: "Klassifikationsergebnisse",
yourImage: "Ihr Bild",
prediction: "Vorhersage",
uploadAnother: "Weiteres Hochladen",
copyright: "© 2023 Pilz Unter Uns Klassifikator. Nicht verantwortlich für Sammelentscheidungen.",
disclaimer: "Konsultieren Sie immer einen Experten, bevor Sie Wildpilze verzehren."
},
pl: {
title: "Grzyb Wśród Nas",
subtitle: "Prześlij zdjęcia grzybów, aby zidentyfikować gatunki za pomocą naszego klasyfikatora AI. Obsługuje popularne jadalne i trujące odmiany.",
uploadTitle: "Prześlij Zdjęcie Grzyba",
uploadSubtitle: "Przeciągnij i upuść lub kliknij, aby przeglądać (JPG, PNG do 5MB)",
selectFile: "Wybierz Plik",
privacy: "Nie przechowujemy Twoich zdjęć po przetworzeniu",
processing: "Przetwarzanie",
uploading: "Przesyłanie obrazu",
resultsTitle: "Wyniki Klasyfikacji",
yourImage: "Twoje Zdjęcie",
prediction: "Przewidywanie",
uploadAnother: "Prześlij Kolejne",
copyright: "© 2023 Klasyfikator Grzyb Wśród Nas. Nie ponosimy odpowiedzialności za decyzje dotyczące zbieractwa.",
disclaimer: "Zawsze skonsultuj się z ekspertem przed spożyciem dzikich grzybów."
}
};
// Language selector
document.getElementById('languageSelect').addEventListener('change', function() {
const lang = this.value;
updateLanguage(lang);
});
function updateLanguage(lang) {
const t = translations[lang] || translations.en;
// Update all text elements
document.getElementById('titleText').textContent = t.title;
document.getElementById('subtitleText').textContent = t.subtitle;
document.getElementById('uploadTitle').textContent = t.uploadTitle;
document.getElementById('uploadSubtitle').textContent = t.uploadSubtitle;
document.getElementById('selectFileText').textContent = t.selectFile;
document.getElementById('privacyText').textContent = t.privacy;
document.getElementById('processingText').textContent = t.processing;
document.getElementById('uploadingText').textContent = t.uploading;
document.getElementById('resultsTitle').textContent = t.resultsTitle;
document.getElementById('yourImageText').textContent = t.yourImage;
document.getElementById('predictionText').textContent = t.prediction;
document.getElementById('uploadAnotherText').textContent = t.uploadAnother;
document.getElementById('copyrightText').textContent = t.copyright;
document.getElementById('disclaimerText').textContent = t.disclaimer;
// Update selected option in dropdown
const select = document.getElementById('languageSelect');
for (let i = 0; i < select.options.length; i++) {
if (select.options[i].value === lang) {
select.selectedIndex = i;
break;
}
}
// Update feather icons (they might contain text)
feather.replace();
}
// Initialize with default language
updateLanguage('en');
// DOM Elements
const dropzone = document.getElementById('dropzone');
const fileInput = document.getElementById('fileInput');
const uploadBtn = document.getElementById('uploadBtn');
const statusSection = document.getElementById('statusSection');
const resultsSection = document.getElementById('resultsSection');
const errorSection = document.getElementById('errorSection');
const progressBar = document.getElementById('progressBar');
const progressText = document.getElementById('progressText');
const statusMessage = document.getElementById('statusMessage');
const previewImage = document.getElementById('previewImage');
const resultsContainer = document.getElementById('resultsContainer');
const newUploadBtn = document.getElementById('newUploadBtn');
const tryAgainBtn = document.getElementById('tryAgainBtn');
const errorMessage = document.getElementById('errorMessage');
const errorTitle = document.getElementById('errorTitle');
// State
let currentTaskId = null;
let statusCheckInterval = null;
// Event Listeners
uploadBtn.addEventListener('click', () => fileInput.click());
fileInput.addEventListener('change', handleFileSelect);
newUploadBtn.addEventListener('click', resetUpload);
tryAgainBtn.addEventListener('click', resetUpload);
// Drag and Drop Events
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
dropzone.addEventListener(eventName, preventDefaults, false);
});
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
['dragenter', 'dragover'].forEach(eventName => {
dropzone.addEventListener(eventName, highlight, false);
});
['dragleave', 'drop'].forEach(eventName => {
dropzone.addEventListener(eventName, unhighlight, false);
});
function highlight() {
dropzone.classList.add('dropzone-active');
}
function unhighlight() {
dropzone.classList.remove('dropzone-active');
}
dropzone.addEventListener('drop', handleDrop, false);
function handleDrop(e) {
const dt = e.dataTransfer;
const files = dt.files;
if (files.length) {
fileInput.files = files;
handleFileSelect({ target: fileInput });
}
}
// File Handling
function handleFileSelect(e) {
const file = e.target.files[0];
if (!file) return;
// Validate file type
if (!file.type.match('image.*')) {
showError('Invalid File Type', 'Please upload an image file (JPEG or PNG)');
return;
}
// Validate file size (5MB max)
if (file.size > 5 * 1024 * 1024) {
showError('File Too Large', 'Maximum file size is 5MB');
return;
}
// Show preview
const reader = new FileReader();
reader.onload = function(e) {
previewImage.src = e.target.result;
};
reader.readAsDataURL(file);
// Start upload process
startUpload(file);
}
// Upload Process
async function startUpload(file) {
try {
// Show status section
statusSection.classList.remove('hidden');
dropzone.classList.add('hidden');
updateStatus('Uploading image...', 0);
const formData = new FormData();
formData.append('file', file);
const response = await fetch('/upload', {
method: 'POST',
body: formData
});
if (!response.ok) {
throw new Error('Upload failed');
}
const data = await response.json();
currentTaskId = data.task_id;
// Start checking status
checkStatus();
} catch (error) {
showError('Upload Failed', error.message);
}
}
// Status Checking
async function checkStatus() {
updateStatus('Analyzing mushroom...', 30);
statusCheckInterval = setInterval(async () => {
try {
const response = await fetch(`/status/${currentTaskId}`);
const data = await response.json();
if (data.status === 'completed') {
clearInterval(statusCheckInterval);
updateStatus('Finalizing results...', 100);
await new Promise(resolve => setTimeout(resolve, 500));
showResults(data);
} else if (data.status === 'processing') {
// Update progress - simple linear progression for demo
const currentProgress = parseInt(progressBar.style.width) || 30;
const newProgress = Math.min(currentProgress + 10, 90);
updateStatus(data.message || 'Analyzing mushroom...', newProgress);
}
} catch (error) {
clearInterval(statusCheckInterval);
showError('Processing Error', error.message);
}
}, 2000);
}
// Update Status UI
function updateStatus(message, percent) {
statusMessage.textContent = message;
progressBar.style.width = `${percent}%`;
progressText.textContent = `${percent}%`;
}
// Show Results
async function showResults(data) {
try {
const predictions = data.predictions || [];
const topPrediction = data.top_prediction || predictions[0];
// Populate results
resultsContainer.innerHTML = '';
// Add Wikipedia image and description for top prediction
if (topPrediction) {
const wikiCard = document.createElement('div');
wikiCard.className = 'mb-6 p-4 bg-white rounded-xl shadow-sm border border-gray-100';
wikiCard.innerHTML = `
<h3 class="text-lg font-medium text-gray-800 mb-3">${topPrediction.class}</h3>
<div class="flex flex-col md:flex-row gap-4">
<div class="w-full md:w-1/3">
<img src="https://en.wikipedia.org/w/api.php?action=query&titles=${encodeURIComponent(topPrediction.class)}&prop=pageimages&format=json&pithumbsize=300"
alt="${topPrediction.class}"
class="w-full h-48 object-cover rounded-lg"
onerror="this.src='http://static.photos/nature/320x240'">
</div>
<div class="w-full md:w-2/3">
<p class="text-gray-700">Loading description from Wikipedia...</p>
</div>
</div>
`;
resultsContainer.appendChild(wikiCard);
// Fetch Wikipedia description
fetchWikipediaDescription(topPrediction.class);
}
// Add all predictions
predictions.forEach(result => {
const resultItem = document.createElement('div');
resultItem.className = 'p-3 rounded-lg border';
resultItem.style.borderColor = result.edible ? '#D1FAE5' : '#FEE2E2';
resultItem.style.backgroundColor = result.edible ? '#ECFDF5' : '#FEF2F2';
const confidencePercentage = result.probability.toFixed(2);
const isEdible = !result.class.toLowerCase().includes('amanita'); // Simple edible check for demo
resultItem.innerHTML = `
<div class="flex justify-between items-start mb-1">
<h4 class="font-medium ${isEdible ? 'text-emerald-800' : 'text-red-800'}">
${result.class}
</h4>
<span class="text-xs px-2 py-1 rounded-full ${result.edible ? 'bg-emerald-100 text-emerald-800' : 'bg-red-100 text-red-800'}">
${confidencePercentage}% confidence
</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-1.5 mb-2">
<div class="h-1.5 rounded-full ${result.edible ? 'bg-emerald-500' : 'bg-red-500'}" style="width: ${confidencePercentage}%"></div>
</div>
<p class="text-sm ${isEdible ? 'text-emerald-700' : 'text-red-700'}">
<i data-feather="${isEdible ? 'check-circle' : 'alert-circle'}" class="w-4 h-4 inline mr-1"></i>
${isEdible ? 'Likely Edible' : 'Likely Poisonous'}
</p>
`;
resultsContainer.appendChild(resultItem);
});
// Show results section
statusSection.classList.add('hidden');
resultsSection.classList.remove('hidden');
// Update feather icons in new elements
feather.replace();
} catch (error) {
showError('Results Error', error.message);
}
}
// Fetch Wikipedia Description
async function fetchWikipediaDescription(species) {
try {
// This would need to be proxied through your backend due to CORS
const response = await fetch(`/wiki?species=${encodeURIComponent(species)}`);
const data = await response.json();
if (data.description) {
const descElement = document.querySelector(`[data-species="${species}"] .wiki-description`);
if (descElement) {
descElement.innerHTML = data.description;
}
}
} catch (error) {
console.error('Failed to fetch Wikipedia description:', error);
}
}
// Error Handling
function showError(title, message) {
clearInterval(statusCheckInterval);
errorTitle.textContent = title;
errorMessage.textContent = message;
statusSection.classList.add('hidden');
resultsSection.classList.add('hidden');
errorSection.classList.remove('hidden');
}
// Reset Upload
function resetUpload() {
fileInput.value = '';
currentTaskId = null;
clearInterval(statusCheckInterval);
statusSection.classList.add('hidden');
resultsSection.classList.add('hidden');
errorSection.classList.add('hidden');
dropzone.classList.remove('hidden');
progressBar.style.width = '0%';
progressText.textContent = '0%';
}
</script>
</body>
</html>