import React, { useState, useEffect, useRef } from 'react'; import ReactMarkdown from 'react-markdown'; import { motion } from 'motion/react'; import { User, UserPen, Camera, ClipboardCheck, Globe, CheckCircle2, WifiOff, Lightbulb, Upload, Activity, ShieldCheck, AlertTriangle, FileText, Save, Share2, Download, Search, Filter, ChevronRight, ChevronLeft, ZoomIn, X, PlusCircle, History, Loader2, ArrowRight, HeartPulse, Printer, BookOpen, Home, Trash2, Clock, Heart } from 'lucide-react'; import { SAMPLE_CASE_TEMPLATES, CaseRecord, PatientDetails, DiagnosticResult } from './types'; import { TRANSLATIONS, LanguageOption } from './locales'; import { TreatmentRecommendations} from './components/TreatmentRecommendations'; import { jsPDF } from 'jspdf'; import { clearProfile, ClinicianProfile, ClinicianSetup, loadProfile } from './components/ClinicianSetup'; export default function App() { // ── Clinician profile ────────────────────────────────────────────────────── const [clinician, setClinician] = useState(() => loadProfile()); const handleProfileComplete = (profile: ClinicianProfile) => { setClinician({ ...profile }); }; // ── Navigation ───────────────────────────────────────────────────────────── const [screen, setScreen] = useState<'home' | 'assessment-info' | 'assessment-capture' | 'assessment-review' | 'referral-note' | 'case-history'>('home'); const [lang, setLang] = useState('English'); const [langMenuOpen, setLangMenuOpen] = useState(false); // ── Cases ────────────────────────────────────────────────────────────────── const [cases, setCases] = useState([]); // ── Assessment state ─────────────────────────────────────────────────────── const [patient, setPatient] = useState({ name: '', age: '', sex: '', contactNumber: '', symptoms: '' }); const [capturedImage, setCapturedImage] = useState(null); const [customFileSelected, setCustomFileSelected] = useState(false); const [analysisLoading, setAnalysisLoading] = useState(false); const [loadingText, setLoadingText] = useState(''); const [activeAnalysisResult, setActiveAnalysisResult] = useState(null); const [activeCaseId, setActiveCaseId] = useState(''); const [showSuccessAnimation, setShowSuccessAnimation] = useState(false); const [referralNoteLoading, setReferralNoteLoading] = useState(false); // ── Prescribed Medication (Editable for Referral) ──────────────────────── const [prescribedMedication, setPrescribedMedication] = useState(''); const [prescribedRegimen, setPrescribedRegimen] = useState(''); const [pdfPreviewUrl, setPdfPreviewUrl] = useState(null); const [viewMode, setViewMode] = useState<'card' | 'pdf'>('card'); useEffect(() => { if (activeAnalysisResult) { if (activeAnalysisResult.therapyRegimen) { setPrescribedMedication(activeAnalysisResult.therapyRegimen.medication || ''); setPrescribedRegimen(activeAnalysisResult.therapyRegimen.regimen || ''); } else { setPrescribedMedication(''); setPrescribedRegimen(''); } } }, [activeAnalysisResult]); // ── Case history UI ──────────────────────────────────────────────────────── const [searchQuery, setSearchQuery] = useState(''); const [filterUrgency, setFilterUrgency] = useState<'All' | 'High' | 'Moderate' | 'Low'>('All'); const [selectedDetailsCase, setSelectedDetailsCase] = useState(null); const [zoomImage, setZoomImage] = useState(false); const [caseToDelete, setCaseToDelete] = useState(null); // ── FAQ state ────────────────────────────────────────────────────────────── const [faqOpenState, setFaqOpenState] = useState>({ 0: true }); // ── Signature canvas ─────────────────────────────────────────────────────── const signatureCanvasRef = useRef(null); const [isDrawingSig, setIsDrawingSig] = useState(false); const startDrawingSig = (e: React.MouseEvent | React.TouchEvent) => { setIsDrawingSig(true); const canvas = signatureCanvasRef.current; if (!canvas) return; const ctx = canvas.getContext('2d'); if (!ctx) return; ctx.strokeStyle = '#0A1628'; ctx.lineWidth = 2; ctx.lineCap = 'round'; ctx.lineJoin = 'round'; const rect = canvas.getBoundingClientRect(); const x = ('clientX' in e) ? e.clientX - rect.left : e.touches[0].clientX - rect.left; const y = ('clientY' in e) ? e.clientY - rect.top : e.touches[0].clientY - rect.top; ctx.beginPath(); ctx.moveTo(x, y); }; const drawSig = (e: React.MouseEvent | React.TouchEvent) => { if (!isDrawingSig) return; const canvas = signatureCanvasRef.current; if (!canvas) return; const ctx = canvas.getContext('2d'); if (!ctx) return; const rect = canvas.getBoundingClientRect(); const x = ('clientX' in e) ? e.clientX - rect.left : e.touches[0].clientX - rect.left; const y = ('clientY' in e) ? e.clientY - rect.top : e.touches[0].clientY - rect.top; ctx.lineTo(x, y); ctx.stroke(); e.preventDefault(); }; const stopDrawingSig = () => setIsDrawingSig(false); const clearSig = () => { const canvas = signatureCanvasRef.current; if (!canvas) return; const ctx = canvas.getContext('2d'); if (!ctx) return; ctx.clearRect(0, 0, canvas.width, canvas.height); }; // ── Sync / health ────────────────────────────────────────────────────────── const [dbStatus, setDbStatus] = useState<'online' | 'offline'>('online'); const [syncStatus, setSyncStatus] = useState<'synced' | 'pending' | 'syncing'>('synced'); const [lastSynced, setLastSynced] = useState(null); const [autoSyncEnabled] = useState(true); const triggerCloudSync = async (list: CaseRecord[]) => { if (list.length === 0) return; setSyncStatus('syncing'); try { await Promise.all(list.map(c => fetch('/api/cases', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(c) }))); setSyncStatus('synced'); setDbStatus('online'); const ts = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }); setLastSynced(ts); localStorage.setItem('dermadetect_last_synced', ts); } catch { setSyncStatus('pending'); setDbStatus('offline'); } }; const probeDatabaseHealth = async () => { try { const t0 = performance.now(); const r = await fetch('/api/health'); if (r.ok) { setDbStatus('online'); return true; } setDbStatus('offline'); return false; } catch { setDbStatus('offline'); return false; } }; useEffect(() => { const saved = localStorage.getItem('dermadetect_last_synced'); if (saved) setLastSynced(saved); }, []); useEffect(() => { const init = async () => { const online = await probeDatabaseHealth(); if (online && autoSyncEnabled && cases.length > 0) await triggerCloudSync(cases); }; if (cases.length > 0) init(); const id = setInterval(async () => { const online = await probeDatabaseHealth(); if (online && autoSyncEnabled && cases.length > 0) await triggerCloudSync(cases); }, 15000); return () => clearInterval(id); }, [cases, autoSyncEnabled]); // ── Camera ───────────────────────────────────────────────────────────────── const [isCameraActive, setIsCameraActive] = useState(false); const [cameraError, setCameraError] = useState(null); const videoRef = useRef(null); const streamRef = useRef(null); // ── Load cases ───────────────────────────────────────────────────────────── useEffect(() => { const load = async () => { try { const r = await fetch('/api/cases'); if (r.ok) { const d = await r.json(); if (d.cases?.length > 0) { setCases(d.cases); localStorage.setItem('dermadetect_cases', JSON.stringify(d.cases)); return; } } } catch { console.warn('Server unreachable, falling back to localStorage'); } const stored = localStorage.getItem('dermadetect_cases'); try { setCases(stored ? JSON.parse(stored) : []); } catch { setCases([]); } }; load(); }, []); const syncCasesToStorage = (updated: CaseRecord[]) => { setCases(updated); localStorage.setItem('dermadetect_cases', JSON.stringify(updated)); }; const t = TRANSLATIONS[lang]; const resetAndStartAssessment = () => { setPatient({ name: '', age: '', sex: '', contactNumber: '', symptoms: '' }); setCapturedImage(null); setCustomFileSelected(false); setActiveAnalysisResult(null); setActiveCaseId(''); setScreen('assessment-info'); stopWebcam(); }; const startWebcam = async () => { setIsCameraActive(true); setCameraError(null); try { const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment', width: 640, height: 480 } }); streamRef.current = stream; if (videoRef.current) videoRef.current.srcObject = stream; } catch { setCameraError("Webcam not accessible. Please upload from Gallery."); setIsCameraActive(false); } }; const stopWebcam = () => { streamRef.current?.getTracks().forEach(t => t.stop()); streamRef.current = null; setIsCameraActive(false); }; const captureFrame = () => { if (!videoRef.current) return; const canvas = document.createElement('canvas'); canvas.width = videoRef.current.videoWidth || 640; canvas.height = videoRef.current.videoHeight || 480; const ctx = canvas.getContext('2d'); if (ctx) { ctx.drawImage(videoRef.current, 0, 0, canvas.width, canvas.height); setCapturedImage(canvas.toDataURL('image/jpeg')); setCustomFileSelected(true); stopWebcam(); } }; const handleFileChange = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; const reader = new FileReader(); reader.onloadend = () => { setCapturedImage(reader.result as string); setCustomFileSelected(true); stopWebcam(); }; reader.readAsDataURL(file); }; // ── AI Analysis ──────────────────────────────────────────────────────────── const runAiAnalysis = async () => { if (!capturedImage) return; setAnalysisLoading(true); const messages = [ "Accessing secured local database sandbox...", "Encrypting transmission packet according to medical standard guidelines...", "Extracting skin lesion pigmentation & margins...", "Invoking DermaDefect diagnostic model...", "Conducting diagnostic taxonomy matrix parsing...", "Finalizing triage urgency confidence ratings...", ]; let i = 0; setLoadingText(messages[0]); const timer = setInterval(() => { i++; if (i < messages.length) setLoadingText(messages[i]); }, 700); try { const response = await fetch('/api/analyze-skin', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ image: capturedImage, symptoms: patient.symptoms, patientInfo: { name: patient.name, age: patient.age, sex: patient.sex, contactNumber: patient.contactNumber } }), }); if (!response.ok) { const e = await response.text(); throw new Error(`Server error ${response.status}: ${e}`); } const raw = await response.json(); const result: DiagnosticResult = { primaryFinding: raw.primaryFinding ?? 'Unknown', confidence: raw.confidence ?? 0, urgency: (['High','Moderate','Low'].includes(raw.urgency) ? raw.urgency : 'Moderate') as 'High'|'Moderate'|'Low', urgencyText: raw.urgencyText ?? '', treatmentNotes: Array.isArray(raw.treatmentNotes) ? raw.treatmentNotes : [], recommendedAction: raw.recommendedAction ?? '', referralNote: raw.referralNote ?? '', conditionCode: raw.conditionCode ?? '', heatmap_b64: raw.heatmap_b64 ?? undefined, therapyRegimen: raw.therapyRegimen ?? undefined, patientHandout: raw.patientHandout ?? undefined, }; await new Promise(r => setTimeout(r, 800)); clearInterval(timer); setActiveAnalysisResult(result); setActiveCaseId(`DD-${new Date().getFullYear()}-${Math.floor(1000 + Math.random() * 9000)}`); setScreen('assessment-review'); } catch (err) { clearInterval(timer); await new Promise(r => setTimeout(r, 800)); window.alert(`Analysis failed: ${err instanceof Error ? err.message : 'Unknown error'}`); } finally { setAnalysisLoading(false); } }; // ── Lazy referral note ───────────────────────────────────────────────────── const handleFormatReferral = async () => { if (!activeAnalysisResult) return; if (activeAnalysisResult.referralNote) { setScreen('referral-note'); return; } setReferralNoteLoading(true); try { const response = await fetch('/api/referral-note', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ predictions: activeAnalysisResult.allPredictions ?? [], patient_name: patient.name, patient_age: patient.age, patient_sex: patient.sex, symptoms: patient.symptoms, primary_finding: activeAnalysisResult.primaryFinding, urgency: activeAnalysisResult.urgency, urgency_text: activeAnalysisResult.urgencyText, clinician_name: clinician?.name ?? '', facility_name: clinician?.facilityName ?? '', district: clinician?.district ?? '', region: clinician?.region ?? '', }), }); const data = await response.json(); setActiveAnalysisResult(prev => prev ? { ...prev, referralNote: data.referralNote } : prev); setScreen('referral-note'); } catch { setScreen('referral-note'); } finally { setReferralNoteLoading(false); } }; // ── Save case ────────────────────────────────────────────────────────────── const saveCaseRecord = async () => { if (!activeAnalysisResult || !capturedImage) return; const updatedFinding = { ...activeAnalysisResult, therapyRegimen: { ...activeAnalysisResult.therapyRegimen, medication: prescribedMedication, regimen: prescribedRegimen, dosage: activeAnalysisResult.therapyRegimen?.dosage || '', contraindications: activeAnalysisResult.therapyRegimen?.contraindications || '', warningNote: activeAnalysisResult.therapyRegimen?.warningNote || '', } }; const newRecord: CaseRecord = { id: activeCaseId, patient: { ...patient }, date: new Date().toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }), finding: updatedFinding, image: capturedImage, healthWorker: clinician?.name ?? 'Unknown', saved: true, }; setActiveAnalysisResult(updatedFinding); syncCasesToStorage([newRecord, ...cases]); try { await fetch('/api/cases', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(newRecord) }); } catch { console.warn('Failed to persist case to server'); } setShowSuccessAnimation(true); setTimeout(() => { setShowSuccessAnimation(false); setScreen('case-history'); window.scrollTo({ top: 0, behavior: 'smooth' }); }, 1800); }; // ── Delete case ──────────────────────────────────────────────────────────── const deleteCaseRecord = async (id: string) => { syncCasesToStorage(cases.filter(c => c.id !== id)); try { await fetch(`/api/cases/${id}`, { method: 'DELETE' }); } catch {} if (selectedDetailsCase?.id === id) setSelectedDetailsCase(null); setCaseToDelete(null); }; // ── PDF helpers ──────────────────────────────────────────────────────────── const clinicianName = clinician?.name ?? 'Health Worker'; const clinicianRole = clinician?.role ?? 'Community Health Worker'; const clinicianFacility = clinician?.facilityName ?? 'Community Clinic'; const clinicianDistrict = clinician?.district ?? ''; const clinicianRegion = clinician?.region ?? ''; const clinicianContact = clinician?.contact ?? ''; const downloadReferralNotePdf = () => { if (!activeAnalysisResult) return; try { const doc = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4' }); const pageWidth = 210; const pageHeight = 297; const margin = 12; const contentWidth = 186; // Colors const navyDark = [10, 51, 105]; // #0a3369 — Header background const borderSlate = [226, 232, 240]; // #e2e8f0 const urgencyStr = activeAnalysisResult.urgency || 'Low'; const isHigh = urgencyStr === 'High'; const isMod = urgencyStr === 'Moderate'; const urgencyColor = isHigh ? [216, 90, 48] : isMod ? [239, 159, 39] : [8, 47, 73]; // #D85A30, #EF9F27, #082F49 // Clean non-Unicode alerts to prevent '%T' glitch const urgencyTextStr = isHigh ? 'URGENT — Immediate referral required. Do not delay.' : isMod ? 'MODERATE — Refer to clinic within 3 days for assessment and treatment.' : 'MILD — Can be managed locally. Refer if no improvement in 7 days.'; const cleanRefId = activeCaseId || `DD-${new Date().getFullYear()}-00847`; let y = margin; // 1. Header Band doc.setFillColor(navyDark[0], navyDark[1], navyDark[2]); doc.rect(margin, y, contentWidth, 18, 'F'); // Logo & Brand text on left doc.setFillColor(255, 255, 255, 0.2); doc.rect(margin + 5, y + 4, 10, 10, 'F'); doc.setTextColor(255, 255, 255); doc.setFont('helvetica', 'bold'); doc.setFontSize(10); doc.text('DD', margin + 7.5, y + 10.5); doc.setFontSize(11); doc.text('DermaDetect', margin + 18, y + 8); doc.setFont('helvetica', 'normal'); doc.setFontSize(7.5); doc.setTextColor(230, 240, 255); doc.text('AI-Powered Skin Assessment', margin + 18, y + 12); // Title on right doc.setTextColor(255, 255, 255); doc.setFont('helvetica', 'bold'); doc.setFontSize(10); const titleStr = 'CLINICAL REFERRAL NOTE'; doc.text(titleStr, margin + contentWidth - 5 - doc.getTextWidth(titleStr), y + 8); doc.setFont('helvetica', 'normal'); doc.setFontSize(7.5); doc.setTextColor(230, 240, 255); const refStr = `REF: ${cleanRefId}`; doc.text(refStr, margin + contentWidth - 5 - doc.getTextWidth(refStr), y + 12); y += 22; // 2. Alert Stripe (Zero glyph glitches, smaller text size to avoid clipping) doc.setFillColor(urgencyColor[0], urgencyColor[1], urgencyColor[2]); doc.rect(margin, y, contentWidth, 9, 'F'); doc.setTextColor(255, 255, 255); doc.setFont('helvetica', 'bold'); doc.setFontSize(7.5); // reduced to 7.5 to fit perfectly // Draw a tiny native white circle instead of Unicode bullet to prevent %T glitch doc.setFillColor(255, 255, 255); const alertTextX = margin + (contentWidth - doc.getTextWidth(urgencyTextStr)) / 2; doc.circle(alertTextX - 2.5, y + 4.2, 0.7, 'F'); doc.text(urgencyTextStr, alertTextX + 1.5, y + 6.2); y += 15; const col1Width = 100; const col2Width = contentWidth - col1Width - 8; // 78 const col2X = margin + col1Width + 8; let leftY = y; let rightY = y; // ── LEFT COLUMN ───────────────────────────────────────────────────────── // Patient Info doc.setTextColor(8, 47, 73); doc.setFont('helvetica', 'bold'); doc.setFontSize(8); doc.text('PATIENT INFORMATION', margin, leftY); leftY += 2; doc.setDrawColor(204, 251, 241); doc.setLineWidth(0.4); doc.line(margin, leftY, margin + col1Width, leftY); leftY += 4; const patientRows = [ ['Full Name', patient.name || '—'], ['Contact Number', patient.contactNumber || '—'], ['Age', patient.age ? `${patient.age} years` : '—'], ['Sex', patient.sex || '—'], ['Date of Visit', new Date().toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' })], ['Patient ID', cleanRefId], ]; doc.setFontSize(7.5); patientRows.forEach(([lbl, val]) => { doc.setTextColor(100, 116, 139); doc.setFont('helvetica', 'normal'); doc.text(lbl, margin, leftY); doc.setTextColor(10, 22, 40); doc.setFont('helvetica', 'bold'); doc.text(val, margin + 35, leftY); leftY += 4.5; }); leftY += 3; // Referring Health Worker doc.setTextColor(8, 47, 73); doc.setFont('helvetica', 'bold'); doc.setFontSize(8); doc.text('REFERRING HEALTH WORKER', margin, leftY); leftY += 2; doc.line(margin, leftY, margin + col1Width, leftY); leftY += 4; const workerRows = [ ['Name', clinicianName], ['Role', clinicianRole], ['Facility Name', clinicianFacility], ['District', clinicianDistrict || '—'], ['Region', clinicianRegion ? `${clinicianRegion} Region` : '—'], ['Contact', clinicianContact || '—'], ]; doc.setFontSize(7.5); workerRows.forEach(([lbl, val]) => { doc.setTextColor(100, 116, 139); doc.setFont('helvetica', 'normal'); doc.text(lbl, margin, leftY); doc.setTextColor(10, 22, 40); doc.setFont('helvetica', 'bold'); doc.text(val, margin + 35, leftY); leftY += 4.5; }); leftY += 3; // Refer To doc.setTextColor(8, 47, 73); doc.setFont('helvetica', 'bold'); doc.setFontSize(8); doc.text('REFER TO', margin, leftY); leftY += 2; doc.line(margin, leftY, margin + col1Width, leftY); leftY += 4; const referRows = [ ['Facility Type', 'District Hospital / Dermatology Clinic'], ['Department', 'Dermatology / General OPD'], ['Urgency', isHigh ? 'Immediate — Do Not Delay' : isMod ? 'Within 3 days' : 'Within 7 days'], ]; doc.setFontSize(7.5); referRows.forEach(([lbl, val]) => { doc.setTextColor(100, 116, 139); doc.setFont('helvetica', 'normal'); doc.text(lbl, margin, leftY); if (lbl === 'Urgency') { doc.setTextColor(urgencyColor[0], urgencyColor[1], urgencyColor[2]); doc.setFont('helvetica', 'bold'); } else { doc.setTextColor(10, 22, 40); doc.setFont('helvetica', 'bold'); } doc.text(val, margin + 35, leftY); leftY += 4.5; }); leftY += 3; // Health Worker's Notes doc.setTextColor(8, 47, 73); doc.setFont('helvetica', 'bold'); doc.setFontSize(8); doc.text("HEALTH WORKER'S NOTES", margin, leftY); leftY += 2; doc.line(margin, leftY, margin + col1Width, leftY); leftY += 3.5; doc.setFillColor(248, 250, 252); doc.setDrawColor(241, 245, 249); doc.setLineWidth(0.2); doc.rect(margin, leftY, col1Width, 14, 'DF'); doc.setTextColor(51, 65, 85); doc.setFont('helvetica', 'italic'); doc.setFontSize(7); const wrappedNotes = doc.splitTextToSize(patient.symptoms?.trim() ? `"${patient.symptoms.trim()}"` : 'No additional notes recorded.', col1Width - 6); doc.text(wrappedNotes, margin + 3, leftY + 5); leftY += 18; // Recommended Medications (Editable Card) doc.setTextColor(8, 47, 73); doc.setFont('helvetica', 'bold'); doc.setFontSize(8); doc.text("RECOMMENDED MEDICATIONS", margin, leftY); leftY += 2; doc.setDrawColor(187, 247, 208); doc.line(margin, leftY, margin + col1Width, leftY); leftY += 3.5; doc.setFillColor(240, 253, 244); doc.rect(margin, leftY, col1Width, 24, 'DF'); doc.setTextColor(21, 128, 61); doc.setFont('helvetica', 'bold'); doc.setFontSize(6.5); doc.text('PRESCRIBED MEDICATION', margin + 3, leftY + 4.5); doc.setTextColor(20, 83, 45); doc.setFont('helvetica', 'bold'); doc.setFontSize(7.5); doc.text(prescribedMedication || 'None Prescribed', margin + 3, leftY + 8.5); doc.setTextColor(21, 128, 61); doc.setFont('helvetica', 'bold'); doc.setFontSize(6.5); doc.text('DOSAGE REGIMEN / DIRECTIONS', margin + 3, leftY + 14); doc.setTextColor(20, 83, 45); doc.setFont('helvetica', 'normal'); doc.setFontSize(7); const wrappedRegimen = doc.splitTextToSize(prescribedRegimen || 'No directions specified.', col1Width - 6); doc.text(wrappedRegimen, margin + 3, leftY + 18); leftY += 28; // ── RIGHT COLUMN (Dynamic Height Tracking & Sizing Engine) ────────────────── const cardStartY = rightY; // 1. Prepare Text & Data to pre-calculate the container height dynamically const isHighText = activeAnalysisResult.urgency === 'High' ? 'Urgent Referral' : isMod ? 'Moderate Urgency' : 'Mild Urgency'; const descText = activeAnalysisResult.primaryFinding.toLowerCase().includes('ringworm') ? "Tinea corporis is a superficial fungal infection characterised by a ring-shaped, scaly, itchy rash. Highly treatable with topical antifungal agents." : activeAnalysisResult.primaryFinding.toLowerCase().includes('eczema') ? "Atopic dermatitis is a chronic pruritic inflammatory skin condition managed with hydration, trigger avoidance, and topical anti-inflammatories." : activeAnalysisResult.primaryFinding.toLowerCase().includes('impetigo') ? "Impetigo is a highly contagious superficial bacterial skin infection characterized by honey-colored crusts. Managed with antibiotic therapy." : activeAnalysisResult.primaryFinding.toLowerCase().includes('scabies') ? "Scabies is an intensely itchy skin infestation caused by the mite Sarcoptes scabiei. Highly contagious. Managed with permethrin or ivermectin." : "A potential clinical skin indication detected by the assistive triage scanner. Standard clinical diagnostic procedures are recommended before commencing definitive therapy."; const docDetails = activeAnalysisResult.primaryFinding.toLowerCase().includes('ringworm') ? ["Clotrimazole 1% cream — apply twice daily for 2–4 weeks", "Keep area clean and dry", "Avoid sharing towels or clothing"] : activeAnalysisResult.primaryFinding.toLowerCase().includes('eczema') ? ["Hydrocortisone 1% cream — apply twice daily for 7 days", "Apply thick emollient moisturizer frequently", "Avoid harsh scented soaps and hot baths"] : activeAnalysisResult.primaryFinding.toLowerCase().includes('impetigo') ? ["Mupirocin 2% topical ointment — apply 3 times daily", "Gently clean honey-colored crusts with warm soapy water", "Keep lesions covered to prevent auto-inoculation"] : activeAnalysisResult.primaryFinding.toLowerCase().includes('scabies') ? ["Permethrin 5% cream — apply from neck down, wash after 8-14 hours", "Treat all household contacts simultaneously", "Wash bedding and clothes in hot water"] : activeAnalysisResult.treatmentNotes?.length ? activeAnalysisResult.treatmentNotes : ["Monitor area daily for pigment or dimension shifts", "Keep the affected region clean, dry, and cool", "Refer to dermatology clinic if symptoms do not improve"]; // Pre-calculate heights doc.setFont('helvetica', 'normal'); doc.setFontSize(7); const wrappedDesc = doc.splitTextToSize(descText, col2Width - 8); const descHeight = wrappedDesc.length * 3.3; let tempY = cardStartY + 33 + descHeight + 5; // offset for suggested treatment header let treatBulletY = tempY + 4.5; doc.setFont('helvetica', 'normal'); doc.setFontSize(6.8); docDetails.slice(0, 3).forEach(item => { const wrappedItem = doc.splitTextToSize(item, col2Width - 10); treatBulletY += (wrappedItem.length * 3.4) + 1.2; }); const disclaimerText = 'This is an AI-generated suggestion. Final treatment decisions rest with the clinician.'; const cardHeight = treatBulletY - cardStartY + 6; // 2. Draw card background with exact pre-calculated height first doc.setFillColor(240, 253, 248); doc.setDrawColor(204, 251, 241); doc.setLineWidth(0.3); doc.rect(col2X, cardStartY, col2Width, cardHeight, 'DF'); doc.setDrawColor(8, 47, 73); doc.setLineWidth(0.8); doc.line(col2X, cardStartY, col2X, cardStartY + cardHeight); // 3. Write text on top of the background doc.setTextColor(8, 47, 73); doc.setFont('helvetica', 'bold'); doc.setFontSize(8); doc.text('AI ASSESSMENT', col2X + 4, cardStartY + 5.5); doc.setFillColor(240, 253, 244); doc.setDrawColor(187, 247, 208); doc.setLineWidth(0.2); doc.rect(col2X + col2Width - 22, cardStartY + 3, 18, 4, 'DF'); doc.setTextColor(8, 47, 73); doc.setFont('helvetica', 'bold'); doc.setFontSize(6); doc.text('ANALYSIS OK', col2X + col2Width - 19, cardStartY + 6); doc.setTextColor(10, 22, 40); doc.setFont('helvetica', 'bold'); doc.setFontSize(11); doc.text(activeAnalysisResult.primaryFinding, col2X + 4, cardStartY + 13.5); doc.setTextColor(100, 116, 139); doc.setFont('helvetica', 'normal'); doc.setFontSize(7.5); doc.text('Detection confidence', col2X + 4, cardStartY + 19); const barColor = isHigh ? [225, 29, 72] : isMod ? [245, 158, 11] : [16, 185, 129]; // Rose-600, Amber-500, Emerald-600 doc.setTextColor(barColor[0], barColor[1], barColor[2]); doc.setFont('helvetica', 'bold'); const confStr = `${activeAnalysisResult.confidence}%`; doc.text(confStr, col2X + col2Width - 4 - doc.getTextWidth(confStr), cardStartY + 19); // confidence visual bar doc.setFillColor(241, 245, 249); doc.rect(col2X + 4, cardStartY + 21.5, col2Width - 8, 1.2, 'F'); doc.setFillColor(barColor[0], barColor[1], barColor[2]); doc.rect(col2X + 4, cardStartY + 21.5, (col2Width - 8) * (activeAnalysisResult.confidence / 100), 1.2, 'F'); // Urgency badge with zero glyph glitches doc.setFillColor(urgencyColor[0], urgencyColor[1], urgencyColor[2]); doc.rect(col2X + 4, cardStartY + 25, doc.getTextWidth(isHighText) + 6, 4.5, 'F'); doc.setTextColor(255, 255, 255); doc.setFont('helvetica', 'bold'); doc.setFontSize(6.2); doc.circle(col2X + 7, cardStartY + 27.2, 0.6, 'F'); // Draw a native circle doc.text(isHighText, col2X + 9, cardStartY + 28.2); // Assessment description text doc.setTextColor(71, 85, 105); doc.setFont('helvetica', 'normal'); doc.setFontSize(7); doc.text(wrappedDesc, col2X + 4, cardStartY + 33.5); // Suggested treatment header doc.setTextColor(148, 163, 184); doc.setFont('helvetica', 'bold'); doc.setFontSize(6.5); doc.text('SUGGESTED TREATMENT', col2X + 4, tempY); // Bullet points doc.setTextColor(10, 22, 40); doc.setFont('helvetica', 'normal'); doc.setFontSize(6.8); let renderBulletY = tempY + 4.5; docDetails.slice(0, 3).forEach(item => { const wrappedItem = doc.splitTextToSize(item, col2Width - 10); // Draw standard high-fidelity native bullet point circle doc.setFillColor(8, 47, 73); doc.circle(col2X + 5, renderBulletY - 1, 0.6, 'F'); doc.text(wrappedItem, col2X + 8, renderBulletY); renderBulletY += (wrappedItem.length * 3.4) + 1.2; }); // Disclaimer doc.setTextColor(148, 163, 184); doc.setFont('helvetica', 'italic'); doc.setFontSize(5.8); doc.text(disclaimerText, col2X + 4, cardHeight + cardStartY - 3.5); // Update rightY dynamically using the calculated height + spacing! rightY = cardStartY + cardHeight + 8; // Photos Block doc.setTextColor(8, 47, 73); doc.setFont('helvetica', 'bold'); doc.setFontSize(8); doc.text('PHOTO TAKEN DURING ASSESSMENT', col2X, rightY); rightY += 2; doc.setDrawColor(186, 230, 253); doc.line(col2X, rightY, col2X + col2Width, rightY); rightY += 3.5; // Render the photos side by side in PDF const imgWidth = (col2Width - 4) / 2; const imgHeight = 32; let drawX = col2X; if (capturedImage) { try { doc.addImage(capturedImage, 'JPEG', drawX, rightY, imgWidth, imgHeight); } catch {} } else { doc.setFillColor(241, 245, 249); doc.rect(drawX, rightY, imgWidth, imgHeight, 'F'); doc.setTextColor(148, 163, 184); doc.setFontSize(6); doc.text('NO PHOTO CAPTURED', drawX + 5, rightY + 16); } // Draw high-fidelity overlay badge for Clinical Specimen doc.setFillColor(8, 47, 73); doc.rect(drawX + 1.5, rightY + 1.5, 20, 3.8, 'F'); doc.setTextColor(255, 255, 255); doc.setFont('helvetica', 'bold'); doc.setFontSize(4.5); doc.text('CLINICAL SPECIMEN', drawX + 2.5, rightY + 4.1); drawX += imgWidth + 4; if (activeAnalysisResult.heatmap_b64) { try { doc.addImage(`data:image/jpeg;base64,${activeAnalysisResult.heatmap_b64}`, 'JPEG', drawX, rightY, imgWidth, imgHeight); } catch {} } else { doc.setFillColor(241, 245, 249); doc.rect(drawX, rightY, imgWidth, imgHeight, 'F'); doc.setTextColor(148, 163, 184); doc.setFontSize(6); doc.text('NO HEATMAP AVAILABLE', drawX + 4, rightY + 16); } // Draw high-fidelity overlay badge for AI Saliency Map doc.setFillColor(8, 47, 73); doc.rect(drawX + 1.5, rightY + 1.5, 20, 3.8, 'F'); doc.setTextColor(255, 255, 255); doc.setFont('helvetica', 'bold'); doc.setFontSize(4.5); doc.text('AI SALIENCY MAP', drawX + 2.5, rightY + 4.1); rightY += imgHeight + 4; doc.setTextColor(148, 163, 184); doc.setFont('helvetica', 'normal'); doc.setFontSize(6.5); doc.text(`Photo captured: ${new Date().toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' })}`, col2X, rightY); rightY += 10; // Synchronize column heights for signature placement y = Math.max(leftY, rightY) + 4; // 3. Signature Block doc.setDrawColor(226, 232, 240); doc.setLineWidth(0.4); doc.line(margin, y, margin + contentWidth, y); y += 4; const sigWidth = contentWidth / 2 - 4; const sigHeight = 12; // Attending Clinician Signature doc.setTextColor(100, 116, 139); doc.setFont('helvetica', 'bold'); doc.setFontSize(7); doc.text('HEALTH WORKER SIGNATURE', margin, y); const sigCanvas = signatureCanvasRef.current; if (sigCanvas) { try { const sigImg = sigCanvas.toDataURL('image/png'); doc.addImage(sigImg, 'PNG', margin, y + 2, sigWidth, sigHeight); } catch {} } else { doc.setFillColor(248, 250, 252); doc.rect(margin, y + 2, sigWidth, sigHeight, 'F'); } doc.setTextColor(10, 22, 40); doc.setFont('helvetica', 'bold'); doc.setFontSize(8); doc.text(clinicianName, margin, y + sigHeight + 5); doc.setTextColor(100, 116, 139); doc.setFont('helvetica', 'normal'); doc.setFontSize(7); doc.text(`${clinicianRole} · ${clinicianFacility}`, margin, y + sigHeight + 8); doc.text(`Date: ${new Date().toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' })}`, margin, y + sigHeight + 11); // Receiving Clinician Signature Stamp Box const stampX = margin + contentWidth / 2 + 4; doc.setTextColor(100, 116, 139); doc.setFont('helvetica', 'bold'); doc.setFontSize(7); doc.text('RECEIVING CLINICIAN STAMP / SIGNATURE', stampX, y); doc.setFillColor(248, 250, 252); doc.setDrawColor(226, 232, 240); doc.setLineWidth(0.2); doc.rect(stampX, y + 2, sigWidth, sigHeight, 'DF'); doc.setTextColor(148, 163, 184); doc.setFont('helvetica', 'bold'); doc.setFontSize(6); doc.text('PLACE CLINICAL STAMP HERE', stampX + (sigWidth - doc.getTextWidth('PLACE CLINICAL STAMP HERE')) / 2, y + 2 + sigHeight / 2 + 1); doc.setTextColor(100, 116, 139); doc.setFont('helvetica', 'italic'); doc.setFontSize(6.5); const textToReceiving = '* To be completed at receiving facility'; doc.text(textToReceiving, margin + contentWidth - doc.getTextWidth(textToReceiving), y + sigHeight + 11); // 4. Footer const footerY = pageHeight - margin - 22; doc.setDrawColor(226, 232, 240); doc.setLineWidth(0.4); doc.line(margin, footerY, margin + contentWidth, footerY); doc.setFillColor(248, 250, 252); doc.rect(margin, footerY + 1, contentWidth, 21, 'F'); // Left brand in footer (zero glyph glitches) doc.setTextColor(71, 85, 105); doc.setFont('helvetica', 'bold'); doc.setFontSize(7.5); // Draw native bullet point circle instead of Unicode bullet to prevent %T glitch doc.circle(margin + 5, footerY + 4.8, 0.7, 'F'); doc.text('DermaDetect AI', margin + 7.5, footerY + 6); doc.setTextColor(148, 163, 184); doc.setFont('helvetica', 'normal'); doc.setFontSize(6.5); doc.text('Generated by DermaDetect — AI Skin Assessment Tool', margin + 4, footerY + 9.5); // Right timestamp refs doc.setTextColor(100, 116, 139); doc.setFont('helvetica', 'bold'); doc.setFontSize(6.8); const refsHeader = 'TIMESTAMP & REFERRAL REFS'; doc.text(refsHeader, margin + contentWidth - 4 - doc.getTextWidth(refsHeader), footerY + 6); doc.setTextColor(148, 163, 184); doc.setFont('helvetica', 'normal'); doc.setFontSize(6.5); const refsLine1 = `Ref: ${cleanRefId}`; const refsLine2 = `Generated: ${new Date().toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' })} at ${new Date().toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}`; doc.text(refsLine1, margin + contentWidth - 4 - doc.getTextWidth(refsLine1), footerY + 9.5); doc.text(refsLine2, margin + contentWidth - 4 - doc.getTextWidth(refsLine2), footerY + 13); // Centered disclaimer doc.setDrawColor(241, 245, 249); doc.setLineWidth(0.2); doc.line(margin + 4, footerY + 15, margin + contentWidth - 4, footerY + 15); doc.setTextColor(148, 163, 184); doc.setFont('helvetica', 'italic'); doc.setFontSize(6.2); const disclaimer = 'This referral note was generated with AI assistance. It is intended to support, not replace, clinical judgment.'; doc.text(disclaimer, margin + (contentWidth - doc.getTextWidth(disclaimer)) / 2, footerY + 19); // Save PDF doc.save(`Clinical_Referral_Note_${cleanRefId}_${patient.name.trim().replace(/\s+/g, '_')}.pdf`); } catch (err: any) { alert('PDF generation failed: ' + err.message); } }; const downloadPdfRecord = (record: CaseRecord) => { try { const doc = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4' }); const pageWidth = 210; const pageHeight = 297; const margin = 15; const contentWidth = 180; // Primary brand colors const primaryColor = [14, 116, 144]; // cyan-700 — main interactive blue const primaryDark = [8, 47, 73]; // sky-950 — deep navy for backgrounds/headers // Backgrounds & surfaces const paperBg = [240, 249, 255]; // sky-50 — near-white with a blue tint const borderSlate = [186, 230, 253]; // sky-200 — soft blue-tinted border // Text const textGray = [30, 58, 138]; // indigo-900 — deep blue-toned body text (replaces neutral gray) // Semantic accents (shifted cooler to match theme) const accentRed = [157, 23, 77]; // rose-800 — errors/danger (kept punchy, slightly cooler) const accentOrange = [161, 98, 7]; // amber-700 — warnings (muted gold instead of orange) const accentGreen = [6, 95, 70]; // emerald-800 — success (teal-leaning green) const urgencyStr = record.finding.urgency || 'Low'; let severityColor = accentGreen; if (urgencyStr === 'High') severityColor = accentRed; else if (urgencyStr === 'Moderate') severityColor = accentOrange; doc.setFillColor(primaryColor[0], primaryColor[1], primaryColor[2]); doc.rect(margin, 12, 180, 1.5, 'F'); let y = 24; doc.setFillColor(primaryColor[0], primaryColor[1], primaryColor[2]); doc.circle(margin + 4, y, 3, 'F'); doc.setDrawColor(255,255,255); doc.setLineWidth(0.6); doc.line(margin+4, y-1.5, margin+4, y+1.5); doc.line(margin+2.5, y, margin+5.5, y); doc.setTextColor(primaryColor[0], primaryColor[1], primaryColor[2]); doc.setFont('helvetica','bold'); doc.setFontSize(10); doc.text('DERMADETECT™ CLINICAL CASE DOSSIER', margin+10, y-1); doc.setTextColor(textGray[0], textGray[1], textGray[2]); doc.setFont('helvetica','normal'); doc.setFontSize(7.5); doc.text('FIELD OBSERVATION & DIAGNOSTIC RECORD', margin+10, y+2.5); doc.setTextColor(primaryDark[0], primaryDark[1], primaryDark[2]); doc.setFont('helvetica','bold'); doc.setFontSize(11); const docTitle = 'CLINICAL REFERRAL DOSSIER'; doc.text(docTitle, pageWidth-margin-doc.getTextWidth(docTitle), y+1); y += 8; doc.setDrawColor(borderSlate[0], borderSlate[1], borderSlate[2]); doc.setLineWidth(0.5); doc.line(margin, y, pageWidth-margin, y); y += 6; doc.setFillColor(paperBg[0], paperBg[1], paperBg[2]); doc.rect(margin, y, contentWidth, 36, 'F'); doc.setDrawColor(borderSlate[0], borderSlate[1], borderSlate[2]); doc.setLineWidth(0.3); doc.rect(margin, y, contentWidth, 36, 'D'); doc.setTextColor(primaryColor[0], primaryColor[1], primaryColor[2]); doc.setFont('helvetica','bold'); doc.setFontSize(8.5); doc.text('PATIENT ANTHROPOMETRIC RECORD', margin+6, y+6); doc.text('CLINICAL IDENTIFIER METADATA', margin+96, y+6); doc.line(margin+90, y+3, margin+90, y+33); doc.setTextColor(primaryDark[0], primaryDark[1], primaryDark[2]); doc.setFontSize(8); let rowY = y + 13; doc.setFont('helvetica','bold'); doc.text('Full Name:', margin+6, rowY); doc.setFont('helvetica','normal'); doc.text(record.patient.name, margin+28, rowY); doc.setFont('helvetica','bold'); doc.text('Case Reference:', margin+96, rowY); doc.setFont('helvetica','normal'); doc.text(record.id, margin+125, rowY); rowY += 5; doc.setFont('helvetica','bold'); doc.text('Age / Gender:', margin+6, rowY); doc.setFont('helvetica','normal'); doc.text(`${record.patient.age} Yrs / ${record.patient.sex}`, margin+28, rowY); doc.setFont('helvetica','bold'); doc.text('Assessment Date:', margin+96, rowY); doc.setFont('helvetica','normal'); doc.text(record.date || new Date().toLocaleDateString(), margin+125, rowY); rowY += 5; doc.setFont('helvetica','bold'); doc.text('Symptom Notes:', margin+6, rowY); doc.setFont('helvetica','italic'); const wrappedSx = doc.splitTextToSize(`"${record.patient.symptoms || 'None reported'}"`, 56); doc.text(wrappedSx, margin+28, rowY); doc.setFont('helvetica','bold'); doc.text('Triage Officer:', margin+96, rowY); doc.setFont('helvetica','normal'); doc.text(record.healthWorker || clinicianName, margin+125, rowY); y += 44; doc.setFillColor(paperBg[0], paperBg[1], paperBg[2]); doc.rect(margin, y, contentWidth, 24, 'F'); doc.setDrawColor(borderSlate[0], borderSlate[1], borderSlate[2]); doc.setLineWidth(0.3); doc.rect(margin, y, contentWidth, 24, 'D'); doc.setFillColor(severityColor[0], severityColor[1], severityColor[2]); doc.rect(margin, y, 4, 24, 'F'); doc.setTextColor(textGray[0], textGray[1], textGray[2]); doc.setFont('helvetica','bold'); doc.setFontSize(8); doc.text('PRIMARY TRIAGE CLASSIFICATION GUIDELINE TARGET', margin+8, y+6); doc.setTextColor(primaryDark[0], primaryDark[1], primaryDark[2]); doc.setFont('helvetica','bold'); doc.setFontSize(11); doc.text(record.finding.primaryFinding, margin+8, y+13); doc.setFontSize(8.5); doc.setTextColor(textGray[0], textGray[1], textGray[2]); doc.setFont('helvetica','normal'); doc.text('Confidence Score: ', margin+8, y+19); doc.setTextColor(primaryColor[0], primaryColor[1], primaryColor[2]); doc.setFont('helvetica','bold'); doc.text(`${record.finding.confidence}% match`, margin+35, y+19); doc.setTextColor(textGray[0], textGray[1], textGray[2]); doc.setFont('helvetica','normal'); doc.text(' | Triage Priority Level: ', margin+55, y+19); doc.setTextColor(severityColor[0], severityColor[1], severityColor[2]); doc.setFont('helvetica','bold'); doc.text(`${urgencyStr} Severity`, margin+90, y+19); y += 31; doc.setTextColor(primaryColor[0], primaryColor[1], primaryColor[2]); doc.setFont('helvetica','bold'); doc.setFontSize(9); doc.text('I. CLINICAL IMAGE SPECIMEN SNAPSHOT', margin, y); doc.text('II. SUPPORTIVE PRACTICAL OUTPATIENT DIRECTIVE', margin+82, y); y += 2.5; doc.setDrawColor(borderSlate[0], borderSlate[1], borderSlate[2]); doc.setLineWidth(0.4); doc.line(margin, y, margin+74, y); doc.line(margin+82, y, pageWidth-margin, y); y += 5; const imageBoxY = y; let imgOk = false; if (record.image) { try { doc.addImage(record.image, 'JPEG', margin, y, 74, 52); imgOk = true; } catch {} } if (!imgOk) { doc.setFillColor(242,245,248); doc.rect(margin, y, 74, 52, 'F'); doc.setDrawColor(borderSlate[0], borderSlate[1], borderSlate[2]); doc.rect(margin, y, 74, 52, 'D'); doc.setTextColor(textGray[0], textGray[1], textGray[2]); doc.setFont('helvetica','bold'); doc.setFontSize(7.5); doc.text('VISUAL DERMAL SPECIMEN RECORD', margin+14, y+24); } doc.setTextColor(primaryDark[0], primaryDark[1], primaryDark[2]); doc.setFont('helvetica','bold'); doc.setFontSize(8.5); const actionLines = doc.splitTextToSize(`Onward Action Plan: ${record.finding.recommendedAction}`, 94); doc.text(actionLines, margin+82, y); let notesY = y + actionLines.length * 4 + 2; doc.setTextColor(textGray[0], textGray[1], textGray[2]); doc.setFont('helvetica','normal'); doc.setFontSize(8); (record.finding.treatmentNotes || []).forEach(note => { const bl = doc.splitTextToSize(`• ${note}`, 94); if (notesY + bl.length * 4 < imageBoxY + 54) { doc.text(bl, margin+82, notesY); notesY += bl.length * 4 + 1; } }); y = Math.max(imageBoxY + 52, notesY) + 8; doc.setDrawColor(primaryColor[0], primaryColor[1], primaryColor[2]); doc.setFillColor(paperBg[0], paperBg[1], paperBg[2]); doc.rect(margin, y, contentWidth, 20, 'F'); doc.rect(margin, y, contentWidth, 20, 'D'); doc.setTextColor(primaryColor[0], primaryColor[1], primaryColor[2]); doc.setFont('helvetica','bold'); doc.setFontSize(7.5); doc.text('OFFICIAL MOH REFERRAL TRANSCRIPT VALIDATION SEAL', margin+5, y+5); doc.setTextColor(primaryDark[0], primaryDark[1], primaryDark[2]); doc.setFont('helvetica','normal'); doc.setFontSize(7); doc.text('1. Generative diagnostic handbooks conform to clinical protocol V8.46 guidelines.', margin+5, y+10); doc.text('2. Local sandboxed key integration verified. Regional medical handovers active.', margin+5, y+14); doc.setLineWidth(0.4); doc.setDrawColor(primaryColor[0], primaryColor[1], primaryColor[2]); doc.rect(pageWidth-margin-32, y+3, 27, 14, 'D'); doc.setTextColor(primaryColor[0], primaryColor[1], primaryColor[2]); doc.setFont('helvetica','bold'); doc.setFontSize(7); doc.text('VERIFIED DOSSIER', pageWidth-margin-30, y+8); doc.setFontSize(5); doc.text('MOH TRANSCRIPT SYSTEMS', pageWidth-margin-29, y+13); doc.setDrawColor(borderSlate[0], borderSlate[1], borderSlate[2]); doc.setLineWidth(0.3); doc.line(margin, pageHeight-14, pageWidth-margin, pageHeight-14); doc.setTextColor(textGray[0], textGray[1], textGray[2]); doc.setFont('helvetica','normal'); doc.setFontSize(6.5); doc.text('Dermatological clinical referral handout generated within browser environment.', margin, pageHeight-10); doc.text('Page 1 of 2', pageWidth-margin-doc.getTextWidth('Page 1 of 2'), pageHeight-10); // Page 2 doc.addPage(); y = 20; doc.setFillColor(primaryColor[0], primaryColor[1], primaryColor[2]); doc.rect(margin, 12, 180, 1.5, 'F'); doc.setTextColor(primaryColor[0], primaryColor[1], primaryColor[2]); doc.setFont('helvetica','bold'); doc.setFontSize(10); doc.text('SUPPORTIVE TREATMENT RECIPE & PHARMACOLOGICAL DOSES', margin, y+3); const rightH = 'SECURE PROTOCOL DISPENSING SCHEME'; doc.setTextColor(textGray[0], textGray[1], textGray[2]); doc.setFont('helvetica','normal'); doc.setFontSize(7.5); doc.text(rightH, pageWidth-margin-doc.getTextWidth(rightH), y+3); y += 10; doc.setDrawColor(borderSlate[0], borderSlate[1], borderSlate[2]); doc.setLineWidth(0.5); doc.line(margin, y, pageWidth-margin, y); y += 8; y = pageHeight - 34; doc.setDrawColor(borderSlate[0], borderSlate[1], borderSlate[2]); doc.setLineWidth(0.3); doc.line(margin, y, pageWidth-margin, y); doc.setTextColor(textGray[0], textGray[1], textGray[2]); doc.setFont('helvetica','normal'); doc.setFontSize(6.8); doc.text(`Digital Verification Signature: ${record.healthWorker || clinicianName}`, margin, y+5); doc.text(`System Unique Sign-key Hash: SHA256-${record.id.slice(0,12).toUpperCase()}...`, margin, y+9); doc.setFontSize(7); doc.line(pageWidth-margin-45, y+12, pageWidth-margin, y+12); doc.text('Attending Clinician Authenticated Stamp', pageWidth-margin-45, y+16); doc.setDrawColor(borderSlate[0], borderSlate[1], borderSlate[2]); doc.setLineWidth(0.3); doc.line(margin, pageHeight-14, pageWidth-margin, pageHeight-14); doc.setTextColor(textGray[0], textGray[1], textGray[2]); doc.setFont('helvetica','normal'); doc.setFontSize(6.5); doc.text('Approved by National Digital Health Authority.', margin, pageHeight-10); doc.text('Page 2 of 2', pageWidth-margin-doc.getTextWidth('Page 2 of 2'), pageHeight-10); doc.save(`Clinical_Referral_Note_${record.id}_${record.patient.name.trim().replace(/\s+/g,'_')}.pdf`); } catch (err: any) { alert('PDF generation failed: ' + err.message); } }; // ── Processed cases ──────────────────────────────────────────────────────── const processedCases = cases.filter(item => { const q = searchQuery.toLowerCase(); const matchSearch = item.patient.name.toLowerCase().includes(q) || item.finding.primaryFinding.toLowerCase().includes(q) || item.id.toLowerCase().includes(q); const matchUrgency = filterUrgency === 'All' || item.finding.urgency === filterUrgency; return matchSearch && matchUrgency; }); // ── RENDER ───────────────────────────────────────────────────────────────── return ( <> {!clinician ? ( ) : (
{/* ── HEADER ── */}
{ setScreen('home'); stopWebcam(); }} className="flex items-center gap-4 cursor-pointer group">
DD
DermaDefect Clinical Diagnostics
{/* Clinician badge */}
{clinician.name.split(' ').map(n => n[0]).join('').slice(0,2).toUpperCase()}

{clinician.name}

{clinician.role}

{/* ── MAIN ── */}
{/* HOME */} {screen === 'home' && (
{/* Hero */}
Dermatology Support for General Practice

Diagnostic support,
built for clinicians.

DermaDefect provides physicians with instant, evidence-backed reference mapping and diagnostic cross-examinations.

Read Clinical Protocol
{/* Stats section */}
The Clinical Disparity

Africa has fewer than 1 dermatologist
per million people.

Skin diseases are among the most common reasons for primary clinic visits — yet most go misdiagnosed or untreated at the community level.

Field Support Optimization

Empowering frontlines in local community clinics.

{[ { icon: , stat: '1 in 3', label: 'Affected Annually', desc: 'Ghanaians affected by common, preventable dermatological conditions every year.' }, { icon: , stat: '72 Hours', label: 'Average Rural Wait', desc: 'Mean commute and wait times to consult specialists in metropolitan centers.' }, { icon: , stat: '60%', label: 'Initial Misdiagnosis', desc: 'Cases misdiagnosed at community care outposts without assistive workflows.', red: true }, ].map(({ icon, stat, label, desc, red }) => (
{icon}

{stat}

{label}

{desc}

))}
{/* How it works */}
The Protocol

Three steps. Two seconds. One life changed.

{[ { step: '01', title: 'Capture Image', desc: 'The clinician captures a clear frame of the skin anomaly directly within the device browser, or uploads from local device storage.' }, { step: '02', title: 'Reference Map', desc: 'The internal software engine evaluates key visual structural parameters locally, formatting confirmed criteria markers in under two seconds.' }, { step: '03', title: 'Action Results', desc: 'Review structural indices, confirm urgency tiers, and copy instantly generated case summaries formatted for local referral networks.' }, ].map(({ step, title, desc }) => (
Step {step}

{title}

{step}

{desc}

))}
{/* CTA */}

Ready to bring AI-powered skin care to your clinic?

DermaDetect is free to use, works offline, and takes less than 2 minutes to learn.

🔒 Patient data stays on your device 📶 Works without internet 🌍 Built for African healthcare
{/* FAQ */}
FAQ

Privacy, Scope & Safety FAQ

{[ { q: "What skin conditions can DermaDetect help with?", a: "The system assists in recognizing conditions including Tinea Corporis, Eczema, Impetigo, Scabies, Melanoma, Basal Cell Carcinoma, and more. It aids clinical triaging but does not replace a doctor's examination." }, { q: "How is patient data kept private and secure?", a: "All image evaluation occurs locally on your browser. No patient records or photographs are sent to external servers without your explicit action." }, { q: "Can I generate and download clinical reports offline?", a: "Yes. Once loaded, report compilation operates fully offline. You can generate, preview, and download patient summaries or referral notes directly to your local device." }, ].map((faq, idx) => { const isOpen = !!faqOpenState[idx]; return (
{isOpen &&
{faq.a}
}
); })}
{/* Footer */}
DD
DermaDetect

DermaDetect is an open-source assistive screening tool designed to expand primary care diagnostic support in community, rural, and outreach settings.

*This is a clinical decision support tool and does not replace professional medical advice or formal diagnosis.

Clinical Tools

Device Privacy

All processing occurs locally within your browser. No sensitive patient data is transmitted to remote servers.

© {new Date().getFullYear()} DermaDetect Telehealth Systems.
)} {/* ASSESSMENT FLOW */} {(screen === 'assessment-info' || screen === 'assessment-capture' || screen === 'assessment-review' || screen === 'referral-note') && (
{/* Sidebar */}
{/* Step 1: Patient Info */} {screen === 'assessment-info' && (

Patient Details

Record basic patient identifiers, demographics, and symptom definitions.

{ e.preventDefault(); if (patient.name) setScreen('assessment-capture'); }}>
setPatient({...patient, name: e.target.value})} placeholder="Full legal name" className="w-full h-11 px-3.5 border border-[#bccac1] rounded-lg focus:outline-none focus:ring-2 focus:ring-[#0077b6] bg-white text-sm transition-all" />
setPatient({...patient, age: e.target.value})} placeholder="e.g. 34" className="w-full h-11 px-3.5 border border-[#bccac1] rounded-lg focus:outline-none focus:ring-2 focus:ring-[#0077b6] bg-white text-sm transition-all" />
{['Male','Female'].map((s, i) => ( {i > 0 &&
} ))}
setPatient({...patient, contactNumber: e.target.value})} placeholder="0200000000" max={10} className="w-full h-11 px-3.5 border border-[#bccac1] rounded-lg focus:outline-none focus:ring-2 focus:ring-[#0077b6] bg-white text-sm transition-all" />