proba-le / index.html
farwew's picture
Add 3 files
b2ffd6d verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ML Prediction Probability Visualizer</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/PapaParse/5.3.0/papaparse.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
.dropzone {
border: 2px dashed #3b82f6;
transition: all 0.3s ease;
}
.dropzone.active {
border-color: #10b981;
background-color: #f0f9ff;
}
.probability-bar {
transition: width 0.5s ease;
}
.glow {
box-shadow: 0 0 15px rgba(59, 130, 246, 0.5);
}
.card-hover:hover {
transform: translateY(-5px);
transition: all 0.3s ease;
}
.fade-in {
animation: fadeIn 0.5s ease-in;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
</style>
</head>
<body class="bg-gray-50 min-h-screen">
<div class="container mx-auto px-4 py-8">
<!-- Header -->
<header class="text-center mb-12">
<h1 class="text-4xl font-bold text-blue-600 mb-2">ML Prediction Probability Visualizer</h1>
<p class="text-gray-600 max-w-2xl mx-auto">Upload your prediction results and visualize the probability distributions with interactive charts and insights.</p>
</header>
<!-- Main Content -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<!-- Upload Section -->
<div class="bg-white rounded-xl shadow-md p-6 lg:col-span-1 card-hover">
<h2 class="text-xl font-semibold text-gray-800 mb-4 flex items-center">
<i class="fas fa-upload mr-2 text-blue-500"></i> Upload Data
</h2>
<div id="dropzone" class="dropzone rounded-lg p-8 text-center cursor-pointer mb-4">
<i class="fas fa-file-csv text-4xl text-blue-400 mb-3"></i>
<p class="text-gray-600 mb-2">Drag & drop your CSV/JSON file here</p>
<p class="text-sm text-gray-500">or</p>
<input type="file" id="fileInput" class="hidden" accept=".csv,.json,.txt">
<button id="selectFileBtn" class="mt-3 bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-lg transition">
Select File
</button>
</div>
<div class="mt-4">
<label for="fileType" class="block text-sm font-medium text-gray-700 mb-1">File Type</label>
<select id="fileType" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="csv">CSV</option>
<option value="json">JSON</option>
</select>
</div>
<div class="mt-4">
<label for="probabilityColumn" class="block text-sm font-medium text-gray-700 mb-1">Probability Column (CSV)</label>
<input type="text" id="probabilityColumn" placeholder="e.g., coppaRisk" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<button id="visualizeBtn" class="mt-6 w-full bg-green-500 hover:bg-green-600 text-white px-4 py-3 rounded-lg font-medium transition flex items-center justify-center disabled:opacity-50" disabled>
<i class="fas fa-chart-bar mr-2"></i> Visualize Probabilities
</button>
</div>
<!-- Visualization Section -->
<div class="bg-white rounded-xl shadow-md p-6 lg:col-span-2 card-hover">
<div class="flex justify-between items-center mb-6">
<h2 class="text-xl font-semibold text-gray-800 flex items-center">
<i class="fas fa-chart-pie mr-2 text-blue-500"></i> Probability Distribution
</h2>
<div class="flex space-x-2">
<button id="barChartBtn" class="px-3 py-1 bg-blue-100 text-blue-600 rounded-md text-sm">
<i class="fas fa-chart-bar mr-1"></i> Bar
</button>
<button id="pieChartBtn" class="px-3 py-1 bg-gray-100 text-gray-600 rounded-md text-sm">
<i class="fas fa-chart-pie mr-1"></i> Pie
</button>
<button id="histogramBtn" class="px-3 py-1 bg-gray-100 text-gray-600 rounded-md text-sm">
<i class="fas fa-chart-line mr-1"></i> Histogram
</button>
</div>
</div>
<div id="chartContainer" class="h-80 flex items-center justify-center bg-gray-50 rounded-lg mb-6">
<p class="text-gray-400">Upload a file to visualize prediction probabilities</p>
</div>
<div id="statsContainer" class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
<!-- Stats will be populated here -->
</div>
<div id="probabilityBars" class="space-y-3">
<!-- Probability bars will be populated here -->
</div>
</div>
</div>
<!-- Data Table Section (hidden by default) -->
<div id="dataTableSection" class="mt-8 bg-white rounded-xl shadow-md p-6 hidden fade-in">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-semibold text-gray-800 flex items-center">
<i class="fas fa-table mr-2 text-blue-500"></i> Raw Data Preview
</h2>
<button id="toggleTableBtn" class="text-blue-500 hover:text-blue-700 text-sm">
<i class="fas fa-eye-slash mr-1"></i> Hide Table
</button>
</div>
<div class="overflow-x-auto">
<table id="dataTable" class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<!-- Table headers will be populated here -->
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<!-- Table rows will be populated here -->
</tbody>
</table>
</div>
</div>
<!-- Insights Section (hidden by default) -->
<div id="insightsSection" class="mt-8 bg-white rounded-xl shadow-md p-6 hidden fade-in">
<h2 class="text-xl font-semibold text-gray-800 mb-4 flex items-center">
<i class="fas fa-lightbulb mr-2 text-yellow-500"></i> Model Insights
</h2>
<div id="insightsContent" class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- Insights will be populated here -->
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// DOM Elements
const dropzone = document.getElementById('dropzone');
const fileInput = document.getElementById('fileInput');
const selectFileBtn = document.getElementById('selectFileBtn');
const visualizeBtn = document.getElementById('visualizeBtn');
const chartContainer = document.getElementById('chartContainer');
const statsContainer = document.getElementById('statsContainer');
const probabilityBars = document.getElementById('probabilityBars');
const dataTableSection = document.getElementById('dataTableSection');
const dataTable = document.getElementById('dataTable');
const insightsSection = document.getElementById('insightsSection');
const toggleTableBtn = document.getElementById('toggleTableBtn');
const barChartBtn = document.getElementById('barChartBtn');
const pieChartBtn = document.getElementById('pieChartBtn');
const histogramBtn = document.getElementById('histogramBtn');
// Variables
let uploadedData = null;
let chart = null;
let currentChartType = 'bar';
// Event Listeners
selectFileBtn.addEventListener('click', () => fileInput.click());
fileInput.addEventListener('change', handleFileSelect);
dropzone.addEventListener('dragover', (e) => {
e.preventDefault();
dropzone.classList.add('active');
});
dropzone.addEventListener('dragleave', () => {
dropzone.classList.remove('active');
});
dropzone.addEventListener('drop', (e) => {
e.preventDefault();
dropzone.classList.remove('active');
if (e.dataTransfer.files.length) {
fileInput.files = e.dataTransfer.files;
handleFileSelect({ target: fileInput });
}
});
visualizeBtn.addEventListener('click', visualizeData);
toggleTableBtn.addEventListener('click', () => {
dataTableSection.classList.toggle('hidden');
toggleTableBtn.innerHTML = dataTableSection.classList.contains('hidden') ?
'<i class="fas fa-eye mr-1"></i> Show Table' :
'<i class="fas fa-eye-slash mr-1"></i> Hide Table';
});
barChartBtn.addEventListener('click', () => switchChartType('bar'));
pieChartBtn.addEventListener('click', () => switchChartType('pie'));
histogramBtn.addEventListener('click', () => switchChartType('histogram'));
// Functions
function handleFileSelect(event) {
const file = event.target.files[0];
if (!file) return;
const fileType = document.getElementById('fileType').value;
const reader = new FileReader();
reader.onload = function(e) {
try {
if (fileType === 'csv') {
Papa.parse(e.target.result, {
header: true,
skipEmptyLines: true,
complete: function(results) {
uploadedData = results.data;
visualizeBtn.disabled = false;
showSuccess('File uploaded successfully!');
},
error: function(error) {
showError('Error parsing CSV file: ' + error.message);
}
});
} else if (fileType === 'json') {
uploadedData = JSON.parse(e.target.result);
visualizeBtn.disabled = false;
showSuccess('File uploaded successfully!');
}
} catch (error) {
showError('Error parsing file: ' + error.message);
}
};
reader.onerror = function() {
showError('Error reading file');
};
reader.readAsText(file);
}
function visualizeData() {
if (!uploadedData || uploadedData.length === 0) {
showError('No data to visualize');
return;
}
const probabilityColumn = document.getElementById('probabilityColumn').value;
// Process data based on file type
let probabilities = [];
let labels = [];
if (document.getElementById('fileType').value === 'csv') {
if (!probabilityColumn) {
showError('Please specify the probability column name for CSV files');
return;
}
// Process CSV data
uploadedData.forEach((row, index) => {
if (row[probabilityColumn]) {
probabilities.push(parseFloat(row[probabilityColumn]));
labels.push(`Prediction ${index + 1}`);
}
});
} else {
// Process JSON data (assuming array of numbers or array of objects with probability property)
if (Array.isArray(uploadedData)) {
if (typeof uploadedData[0] === 'number') {
probabilities = uploadedData;
labels = uploadedData.map((_, index) => `Prediction ${index + 1}`);
} else if (typeof uploadedData[0] === 'object') {
probabilities = uploadedData.map(item => item.probability || item.prediction || item.value || 0);
labels = uploadedData.map((item, index) => item.label || `Prediction ${index + 1}`);
}
}
}
if (probabilities.length === 0) {
showError('No valid probability data found');
return;
}
// Update UI
updateStats(probabilities);
updateProbabilityBars(probabilities, labels);
renderChart(probabilities, labels, currentChartType);
renderDataTable(uploadedData);
generateInsights(probabilities);
// Show sections
dataTableSection.classList.remove('hidden');
insightsSection.classList.remove('hidden');
}
function updateStats(probabilities) {
const mean = probabilities.reduce((a, b) => a + b, 0) / probabilities.length;
const sorted = [...probabilities].sort((a, b) => a - b);
const median = sorted[Math.floor(sorted.length / 2)];
const min = Math.min(...probabilities);
const max = Math.max(...probabilities);
const stdDev = Math.sqrt(probabilities.reduce((sq, n) => sq + Math.pow(n - mean, 2), 0) / probabilities.length);
statsContainer.innerHTML = `
<div class="bg-blue-50 p-4 rounded-lg">
<p class="text-sm text-blue-500 font-medium">Mean Probability</p>
<p class="text-2xl font-bold text-blue-600">${mean.toFixed(4)}</p>
</div>
<div class="bg-green-50 p-4 rounded-lg">
<p class="text-sm text-green-500 font-medium">Median Probability</p>
<p class="text-2xl font-bold text-green-600">${median.toFixed(4)}</p>
</div>
<div class="bg-purple-50 p-4 rounded-lg">
<p class="text-sm text-purple-500 font-medium">Standard Deviation</p>
<p class="text-2xl font-bold text-purple-600">${stdDev.toFixed(4)}</p>
</div>
<div class="bg-yellow-50 p-4 rounded-lg">
<p class="text-sm text-yellow-500 font-medium">Range</p>
<p class="text-2xl font-bold text-yellow-600">${min.toFixed(4)} - ${max.toFixed(4)}</p>
</div>
`;
}
function updateProbabilityBars(probabilities, labels) {
probabilityBars.innerHTML = '';
probabilities.forEach((prob, index) => {
const percentage = Math.round(prob * 100);
const barColor = prob > 0.7 ? 'bg-green-500' : prob > 0.3 ? 'bg-yellow-500' : 'bg-red-500';
const bar = document.createElement('div');
bar.className = 'probability-bar';
bar.innerHTML = `
<div class="flex justify-between mb-1">
<span class="text-sm font-medium text-gray-700">${labels[index] || `Prediction ${index + 1}`}</span>
<span class="text-sm font-medium ${prob > 0.7 ? 'text-green-600' : prob > 0.3 ? 'text-yellow-600' : 'text-red-600'}">${prob.toFixed(4)}</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2.5">
<div class="h-2.5 rounded-full ${barColor}" style="width: ${percentage}%"></div>
</div>
`;
probabilityBars.appendChild(bar);
// Trigger animation
setTimeout(() => {
bar.querySelector(`.${barColor}`).classList.add('glow');
}, index * 100);
});
}
function renderChart(probabilities, labels, type = 'bar') {
// Destroy previous chart if exists
if (chart) {
chart.destroy();
}
const ctx = document.createElement('canvas');
chartContainer.innerHTML = '';
chartContainer.appendChild(ctx);
const backgroundColors = probabilities.map(prob => {
const opacity = 0.7;
return prob > 0.7 ? `rgba(16, 185, 129, ${opacity})` :
prob > 0.3 ? `rgba(234, 179, 8, ${opacity})` :
`rgba(239, 68, 68, ${opacity})`;
});
const borderColors = probabilities.map(prob => {
return prob > 0.7 ? 'rgba(16, 185, 129, 1)' :
prob > 0.3 ? 'rgba(234, 179, 8, 1)' :
'rgba(239, 68, 68, 1)';
});
const chartData = {
labels: labels,
datasets: [{
label: 'Prediction Probability',
data: probabilities,
backgroundColor: backgroundColors,
borderColor: borderColors,
borderWidth: 1
}]
};
const chartOptions = {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
max: 1,
ticks: {
callback: function(value) {
return value.toFixed(2);
}
}
}
},
plugins: {
tooltip: {
callbacks: {
label: function(context) {
return `Probability: ${context.raw.toFixed(4)}`;
}
}
}
}
};
switch (type) {
case 'bar':
chart = new Chart(ctx, {
type: 'bar',
data: chartData,
options: chartOptions
});
barChartBtn.className = 'px-3 py-1 bg-blue-100 text-blue-600 rounded-md text-sm';
pieChartBtn.className = 'px-3 py-1 bg-gray-100 text-gray-600 rounded-md text-sm';
histogramBtn.className = 'px-3 py-1 bg-gray-100 text-gray-600 rounded-md text-sm';
break;
case 'pie':
chart = new Chart(ctx, {
type: 'pie',
data: chartData,
options: {
...chartOptions,
plugins: {
legend: {
position: 'right',
},
tooltip: {
callbacks: {
label: function(context) {
const label = context.label || '';
const value = context.raw || 0;
const percentage = Math.round(value * 100);
return `${label}: ${value.toFixed(4)} (${percentage}%)`;
}
}
}
}
}
});
barChartBtn.className = 'px-3 py-1 bg-gray-100 text-gray-600 rounded-md text-sm';
pieChartBtn.className = 'px-3 py-1 bg-blue-100 text-blue-600 rounded-md text-sm';
histogramBtn.className = 'px-3 py-1 bg-gray-100 text-gray-600 rounded-md text-sm';
break;
case 'histogram':
// For histogram, we'll bin the probabilities
const binCount = Math.min(10, Math.floor(probabilities.length / 5));
const histogramData = Array(binCount).fill(0);
const step = 1 / binCount;
probabilities.forEach(prob => {
const binIndex = Math.min(Math.floor(prob / step), binCount - 1);
histogramData[binIndex]++;
});
const histogramLabels = Array.from({length: binCount}, (_, i) => {
const start = (i * step).toFixed(2);
const end = ((i + 1) * step).toFixed(2);
return `${start}-${end}`;
});
chart = new Chart(ctx, {
type: 'bar',
data: {
labels: histogramLabels,
datasets: [{
label: 'Count of Predictions',
data: histogramData,
backgroundColor: 'rgba(59, 130, 246, 0.7)',
borderColor: 'rgba(59, 130, 246, 1)',
borderWidth: 1
}]
},
options: {
...chartOptions,
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: 'Count'
}
},
x: {
title: {
display: true,
text: 'Probability Range'
}
}
}
}
});
barChartBtn.className = 'px-3 py-1 bg-gray-100 text-gray-600 rounded-md text-sm';
pieChartBtn.className = 'px-3 py-1 bg-gray-100 text-gray-600 rounded-md text-sm';
histogramBtn.className = 'px-3 py-1 bg-blue-100 text-blue-600 rounded-md text-sm';
break;
}
currentChartType = type;
}
function renderDataTable(data) {
if (!data || data.length === 0) return;
// Clear previous table
dataTable.querySelector('thead tr').innerHTML = '';
dataTable.querySelector('tbody').innerHTML = '';
// Get headers from first object
const headers = Object.keys(data[0]);
// Add headers
headers.forEach(header => {
const th = document.createElement('th');
th.className = 'px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider';
th.textContent = header;
dataTable.querySelector('thead tr').appendChild(th);
});
// Add rows (limit to 10 for performance)
const rowsToShow = Math.min(10, data.length);
for (let i = 0; i < rowsToShow; i++) {
const row = document.createElement('tr');
row.className = i % 2 === 0 ? 'bg-white' : 'bg-gray-50';
headers.forEach(header => {
const td = document.createElement('td');
td.className = 'px-6 py-4 whitespace-nowrap text-sm text-gray-500';
td.textContent = data[i][header] !== undefined ? data[i][header] : '';
row.appendChild(td);
});
dataTable.querySelector('tbody').appendChild(row);
}
}
function generateInsights(probabilities) {
const highConfidence = probabilities.filter(p => p > 0.7).length;
const lowConfidence = probabilities.filter(p => p < 0.3).length;
const midConfidence = probabilities.length - highConfidence - lowConfidence;
const mean = probabilities.reduce((a, b) => a + b, 0) / probabilities.length;
const sorted = [...probabilities].sort((a, b) => a - b);
const median = sorted[Math.floor(sorted.length / 2)];
insightsSection.innerHTML = `
<div class="bg-white p-4 rounded-lg border border-gray-200">
<h3 class="font-medium text-gray-800 mb-2 flex items-center">
<i class="fas fa-chart-line mr-2 text-blue-500"></i> Confidence Distribution
</h3>
<div class="space-y-2">
<div class="flex justify-between">
<span class="text-sm text-green-600">High Confidence (>70%)</span>
<span class="text-sm font-medium">${highConfidence} (${Math.round(highConfidence/probabilities.length*100)}%)</span>
</div>
<div class="flex justify-between">
<span class="text-sm text-yellow-600">Medium Confidence (30-70%)</span>
<span class="text-sm font-medium">${midConfidence} (${Math.round(midConfidence/probabilities.length*100)}%)</span>
</div>
<div class="flex justify-between">
<span class="text-sm text-red-600">Low Confidence (<30%)</span>
<span class="text-sm font-medium">${lowConfidence} (${Math.round(lowConfidence/probabilities.length*100)}%)</span>
</div>
</div>
</div>
<div class="bg-white p-4 rounded-lg border border-gray-200">
<h3 class="font-medium text-gray-800 mb-2 flex items-center">
<i class="fas fa-balance-scale mr-2 text-purple-500"></i> Central Tendency
</h3>
<div class="space-y-2">
<div class="flex justify-between">
<span class="text-sm">Mean Probability</span>
<span class="text-sm font-medium">${mean.toFixed(4)}</span>
</div>
<div class="flex justify-between">
<span class="text-sm">Median Probability</span>
<span class="text-sm font-medium">${median.toFixed(4)}</span>
</div>
<div class="flex justify-between">
<span class="text-sm">Difference</span>
<span class="text-sm font-medium">${Math.abs(mean - median).toFixed(4)}</span>
</div>
</div>
</div>
<div class="bg-white p-4 rounded-lg border border-gray-200">
<h3 class="font-medium text-gray-800 mb-2 flex items-center">
<i class="fas fa-exclamation-triangle mr-2 text-red-500"></i> Potential Issues
</h3>
<div class="space-y-2 text-sm">
${lowConfidence > probabilities.length * 0.5 ?
'<p class="text-red-500"><i class="fas fa-exclamation-circle mr-1"></i> Over 50% of predictions have low confidence (<30%)</p>' :
'<p class="text-green-500"><i class="fas fa-check-circle mr-1"></i> Low confidence predictions are in acceptable range</p>'}
${Math.abs(mean - median) > 0.2 ?
'<p class="text-yellow-500"><i class="fas fa-exclamation-circle mr-1"></i> Significant difference between mean and median probabilities</p>' :
'<p class="fas fa-check-circle mr-1"></i> Mean and median probabilities are close</p>'}
</div>
</div>
<div class="bg-white p-4 rounded-lg border border-gray-200">
<h3 class="font-medium text-gray-800 mb-2 flex items-center">
<i class="fas fa-lightbulb mr-2 text-yellow-500"></i> Recommendations
</h3>
<div class="space-y-2 text-sm">
<p><i class="fas fa-check-circle text-blue-500 mr-1"></i> Review low confidence predictions for potential errors</p>
<p><i class="fas fa-check-circle text-blue-500 mr-1"></i> Consider model retraining if confidence is consistently low</p>
<p><i class="fas fa-check-circle text-blue-500 mr-1"></i> Examine features influencing high confidence predictions</p>
</div>
</div>
`;
}
function switchChartType(type) {
if (!uploadedData) return;
const probabilityColumn = document.getElementById('probabilityColumn').value;
let probabilities = [];
let labels = [];
if (document.getElementById('fileType').value === 'csv') {
uploadedData.forEach((row, index) => {
if (row[probabilityColumn]) {
probabilities.push(parseFloat(row[probabilityColumn]));
labels.push(`Prediction ${index + 1}`);
}
});
} else {
if (Array.isArray(uploadedData)) {
if (typeof uploadedData[0] === 'number') {
probabilities = uploadedData;
labels = uploadedData.map((_, index) => `Prediction ${index + 1}`);
} else if (typeof uploadedData[0] === 'object') {
probabilities = uploadedData.map(item => item.probability || item.prediction || item.value || 0);
labels = uploadedData.map((item, index) => item.label || `Prediction ${index + 1}`);
}
}
}
renderChart(probabilities, labels, type);
}
function showSuccess(message) {
const alert = document.createElement('div');
alert.className = 'fixed top-4 right-4 bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded shadow-lg z-50 flex items-center';
alert.innerHTML = `
<i class="fas fa-check-circle mr-2"></i>
<span>${message}</span>
`;
document.body.appendChild(alert);
setTimeout(() => {
alert.classList.add('opacity-0', 'transition-opacity', 'duration-500');
setTimeout(() => alert.remove(), 500);
}, 3000);
}
function showError(message) {
const alert = document.createElement('div');
alert.className = 'fixed top-4 right-4 bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded shadow-lg z-50 flex items-center';
alert.innerHTML = `
<i class="fas fa-exclamation-circle mr-2"></i>
<span>${message}</span>
`;
document.body.appendChild(alert);
setTimeout(() => {
alert.classList.add('opacity-0', 'transition-opacity', 'duration-500');
setTimeout(() => alert.remove(), 500);
}, 3000);
}
});
</script>
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=farwew/proba-le" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
</html>