Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
> **AI system for malaria detection from blood smear images**
> Implemented in Rust (Burn) with an Axum inference API and a Yew web UI.

![](demo.png)
## Overview

This model predicts:
Expand All @@ -31,7 +32,7 @@ Stage labels are weak (image-level presence inferred from filename tokens) and a
## Requirements

- Rust toolchain
- Optional (UI): `trunk` + target `wasm32-unknown-unknown`
- UI: `trunk` + target `wasm32-unknown-unknown`

## Data Preparation (Crops + Manifest)

Expand Down
1 change: 1 addition & 0 deletions inference-ui/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions inference-ui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ web-sys = { version = "0.3", features = [
js-sys = "0.3"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde-wasm-bindgen = "0.6"
log = "0.4"
console_error_panic_hook = "0.1"
console_log = { version = "1", features = ["color"] }
Expand Down
272 changes: 220 additions & 52 deletions inference-ui/index.html
Original file line number Diff line number Diff line change
@@ -1,57 +1,225 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Malaria Inference UI (Yew)</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;600;700;800&display=swap" rel="stylesheet">
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
fontFamily: { sans: ['Poppins', 'ui-sans-serif', 'system-ui'] },
colors: {
primary: { DEFAULT: '#1E40AF', foreground: '#FFFFFF' },
'dgrv-blue': '#1E40AF',
'dgrv-green': '#10B981',
'dgrv-light-blue': '#EFF6FF',
'bright-blue': '#00BFFF',
},
borderRadius: {
lg: '0.5rem',
md: '0.375rem',
sm: '0.25rem',
},
keyframes: {
'fade-in': { '0%': { opacity: 0, transform: 'translateY(10px)' }, '100%': { opacity: 1, transform: 'translateY(0)' } },
'scale-in': { '0%': { transform: 'scale(0.95)', opacity: 0 }, '100%': { transform: 'scale(1)', opacity: 1 } },
glow: { '0%, 100%': { boxShadow: '0 0 5px rgba(30, 64, 175, 0.5)' }, '50%': { boxShadow: '0 0 20px rgba(30, 64, 175, 0.8)' } },
},
animation: {
'fade-in': 'fade-in 0.3s ease-out',
'scale-in': 'scale-in 0.2s ease-out',
glow: 'glow 2s ease-in-out infinite',
}

<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Giemsa AI - Malaria Analysis</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;600;700;800&display=swap" rel="stylesheet">
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
fontFamily: { sans: ['Poppins', 'ui-sans-serif', 'system-ui'] },
colors: {
primary: { DEFAULT: '#1E40AF', foreground: '#FFFFFF' },
'dgrv-blue': '#1E40AF',
'dgrv-green': '#10B981',
'dgrv-light-blue': '#EFF6FF',
'bright-blue': '#00BFFF',
},
borderRadius: {
lg: '0.5rem',
md: '0.375rem',
sm: '0.25rem',
},
keyframes: {
'fade-in': { '0%': { opacity: 0, transform: 'translateY(10px)' }, '100%': { opacity: 1, transform: 'translateY(0)' } },
'scale-in': { '0%': { transform: 'scale(0.95)', opacity: 0 }, '100%': { transform: 'scale(1)', opacity: 1 } },
glow: { '0%, 100%': { boxShadow: '0 0 5px rgba(30, 64, 175, 0.5)' }, '50%': { boxShadow: '0 0 20px rgba(30, 64, 175, 0.8)' } },
},
animation: {
'fade-in': 'fade-in 0.3s ease-out',
'scale-in': 'scale-in 0.2s ease-out',
glow: 'glow 2s ease-in-out infinite',
}
}
}
</script>
<style>
:root { color-scheme: dark; }
body { margin: 0; }
</style>
<script>
// Optional: set window.VITE_API_BASE = "http://localhost:8080"; at runtime to override
</script>
</head>
<body>
<div id="root"></div>
<!-- Trunk will build and inject the WASM/JS for this Rust crate -->
<link data-trunk rel="copy-dir" href="static/" />
<link data-trunk rel="rust" />
</body>
</html>
}
</script>
<style>
:root {
color-scheme: dark;
}

body {
margin: 0;
}
</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
<script>
window.generateMalariaReport = function (raw_data) {
console.log("Generating Malaria Report with data:", raw_data);
if (!window.jspdf) {
console.error("jsPDF library not loaded.");
alert("Error: Report generation library not loaded.");
return;
}

// Handle Map vs Object (serde-wasm-bindgen can return Maps)
let data = raw_data;
if (raw_data instanceof Map) {
data = Object.fromEntries(raw_data);
}

const { jsPDF } = window.jspdf;
const doc = new jsPDF();
const { infected, species, speciesProb, stage, stageProb, date, imageUrl } = data;

// Brand Colors
const primary = infected ? [153, 27, 27] : [6, 95, 70]; // Red-800 or Emerald-800
const accent = [16, 185, 129]; // Emerald-500

// Header
doc.setFillColor(primary[0], primary[1], primary[2]);
doc.rect(0, 0, 210, 40, 'F');
doc.setTextColor(255, 255, 255);
doc.setFontSize(22);
doc.setFont("helvetica", "bold");
doc.text("Giemsa AI Diagnostics", 20, 25);

doc.setFontSize(10);
doc.setFont("helvetica", "normal");
doc.text(`Generated on: ${date}`, 140, 25);

// Main Status
doc.setTextColor(0, 0, 0);
doc.setFontSize(16);
doc.setFont("helvetica", "bold");
doc.text("Diagnostic Summary", 20, 55);

doc.setDrawColor(primary[0], primary[1], primary[2]);
doc.setLineWidth(0.5);
doc.line(20, 58, 190, 58);

// Status Box
doc.setFillColor(243, 244, 246);
doc.roundedRect(20, 65, 170, 30, 3, 3, 'F');

doc.setFontSize(12);
doc.text("RESULT:", 30, 80);
doc.setFontSize(14);
doc.setTextColor(primary[0], primary[1], primary[2]);
doc.text(infected ? "MALARIA DETECTED" : "NO MALARIA DETECTED", 60, 80);

// Details
doc.setTextColor(0, 0, 0);
doc.setFontSize(12);
doc.setFont("helvetica", "bold");
doc.text("Analysis Details", 20, 110);

doc.setFontSize(10);
doc.setFont("helvetica", "normal");
let y = 120;

const info = [
["Status", infected ? "Positive" : "Negative"],
["Primary Species", species],
["Species Confidence", `${(speciesProb * 100).toFixed(1)}%`],
];

if (infected) {
info.push(["Parasite Stage", stage]);
info.push(["Stage Confidence", `${(stageProb * 100).toFixed(1)}%`]);
}

info.forEach(([label, value]) => {
doc.setFont("helvetica", "bold");
doc.text(`${label}:`, 20, y);
doc.setFont("helvetica", "normal");
doc.text(value, 60, y);
y += 10;
});

// Footer (re-defined later)

// Image Handling
if (imageUrl) {
const logoImg = new Image();
logoImg.src = "/static/logo.png";

const img = new Image();
img.src = imageUrl;

// Wait for both images (logo + evidence)
let loaded = 0;
const total = 2; // logo + evidence

const checkDone = () => {
loaded++;
if (loaded >= total) {
finalizeAndSave(logoImg, img);
}
};

// Fallback if images fail
logoImg.onerror = checkDone;
img.onerror = checkDone;

logoImg.onload = checkDone;
img.onload = checkDone;

} else {
// Just logo needed
const logoImg = new Image();
logoImg.src = "/static/logo.png";
logoImg.onload = function () { finalizeAndSave(logoImg, null); };
logoImg.onerror = function () { finalizeAndSave(null, null); };
}

function finalizeAndSave(logoImg, evidenceImg) {
// Add Evidence Image if loaded
if (evidenceImg && evidenceImg.width > 0) {
const canvas = document.createElement("canvas");
canvas.width = evidenceImg.width;
canvas.height = evidenceImg.height;
const ctx = canvas.getContext("2d");
ctx.drawImage(evidenceImg, 0, 0);
const imgData = canvas.toDataURL("image/jpeg");

doc.setDrawColor(200, 200, 200);
doc.rect(120, 115, 70, 70, 'S');
doc.addImage(imgData, 'JPEG', 121, 116, 68, 68);
doc.setFontSize(8);
doc.text("Analysis Input Image Evidence", 125, 190);
}

doc.setFontSize(8);
doc.setTextColor(150, 150, 150);
doc.text("Note: This is an AI-generated research report and should be verified by a certified medical professional.", 20, 280);
doc.text("Giemsa AI - Advanced Malaria Detection Platform", 20, 285);

// Add Logo at bottom right if loaded
if (logoImg && logoImg.width > 0) {
const canvas = document.createElement("canvas");
canvas.width = logoImg.width;
canvas.height = logoImg.height;
const ctx = canvas.getContext("2d");
ctx.drawImage(logoImg, 0, 0);
const logoData = canvas.toDataURL("image/png");
// Bottom right corner (Page height ~297mm)
doc.addImage(logoData, 'PNG', 170, 260, 25, 25);
}

doc.save(`giemsa_report_${date.replace(/[/:\s]/g, '_')}.pdf`);
}
};
</script>
<script>
// Optional: set window.VITE_API_BASE = "http://localhost:8080"; at runtime to override
</script>

</head>

<body>
<div id="root"></div>
<!-- Trunk will build and inject the WASM/JS for this Rust crate -->
<link data-trunk rel="copy-dir" href="static/" />
<link data-trunk rel="rust" />
</body>

</html>
18 changes: 10 additions & 8 deletions inference-ui/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ pub enum Route {
Demo,
#[at("/analyze")]
Analyze,
#[at("/features")]
Features,
#[not_found]
#[at("/404")]
NotFound,
Expand All @@ -42,6 +44,7 @@ fn switch(route: Route) -> Html {
Route::Home => html! { <pages::home::HomePage /> },
Route::Demo => html! { <pages::demo::DemoPage /> },
Route::Analyze => html! { <pages::analyze::AnalyzePage /> },
Route::Features => html! { <pages::features::FeaturesPage /> },
Route::NotFound => html! {
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-10">
<div class="bg-white/5 border border-white/10 rounded-xl p-6">
Expand All @@ -67,13 +70,12 @@ fn layout(props: &LayoutProps) -> Html {

<header class="fixed top-0 inset-x-0 z-50 backdrop-blur supports-[backdrop-filter]:bg-black/30 bg-black/40">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-16 grid grid-cols-3 items-center">
<div class="flex items-center">
{ html!{
<Link<Route> to={Route::Home} classes="text-lg font-bold tracking-tight text-bright-blue">
{ if get_locale() == "fr" { "Détection du paludisme" } else { "Malaria Detection" } }
<div class="flex items-center gap-3">
<Link<Route> to={Route::Home} classes="flex items-center gap-2 text-lg font-bold tracking-tight text-white hover:opacity-90">
<img src="/static/logo.png" class="h-8 w-8 rounded-full" alt="Giemsa AI Logo" />
<span>{"Giemsa AI"}</span>
</Link<Route>>
} }
</div>
</div>
<nav class="flex items-center justify-center gap-6 text-sm text-bright-blue opacity-90">
{ html!{
<>
Expand All @@ -83,9 +85,9 @@ fn layout(props: &LayoutProps) -> Html {
<Link<Route> to={Route::Analyze} classes="hover:opacity-100">
{ if get_locale() == "fr" { "Analyser" } else { "Analyze" } }
</Link<Route>>
<a href="#features" class="hover:opacity-100">
<Link<Route> to={Route::Features} classes="hover:opacity-100">
{ if get_locale() == "fr" { "Fonctionnalités" } else { "Features" } }
</a>
</Link<Route>>
<a href="#contact" class="hover:opacity-100">
{"Contact"}
</a>
Expand Down
Loading