import { useState } from "react"; /* Minimal inline SVG icon components to avoid requiring 'lucide-react' */ const DownloadIcon = ({ className }: { className?: string }) => ( ); const FileTextIcon = ({ className }: { className?: string }) => ( ); const Loader2Icon = ({ className }: { className?: string }) => ( ); import { ImageWithFallback } from "./ImageWithFallback"; import { ReportModal } from "./ReportModal"; import axios from "axios"; interface ResultsPanelProps { uploadedImage: string | null; result?: any; loading?: boolean; } export function ResultsPanel({ uploadedImage, result, loading }: ResultsPanelProps) { const [showReportModal, setShowReportModal] = useState(false); // Make loading detection robust: sometimes values arrive as the string "true" from deployed envs const isLoading = loading === true || String(loading) === "true"; // Helpful debug information when checking issues on deployed spaces (open browser devtools) // Keep as debug (console.debug) so it doesn't clutter normal logs. console.debug("ResultsPanel: props", { loading, isLoading, result }); const handleGenerateReport = async (formData: FormData) => { try { const baseURL = import.meta.env.MODE === "development" ? "http://127.0.0.1:7860" : window.location.origin; const response = await axios.post(`${baseURL}/reports/`, formData, { headers: { "Content-Type": "multipart/form-data" }, }); if (response.data.html_url) { // Open report in new tab window.open(`${baseURL}${response.data.html_url}`, "_blank"); } if (response.data.pdf_url) { // Open PDF in new tab when available window.open(`${baseURL}${response.data.pdf_url}`, "_blank"); } setShowReportModal(false); } catch (err: any) { console.error("Failed to generate report:", err); alert(err.response?.data?.error || "Failed to generate report"); } }; // Safely destructure result (keep undefined values when result is null) so we can render // a stable panel while loading and avoid early returns that change layout. const { model_used, detections, annotated_image_url, summary, // prediction (not used here) confidence, } = (result || {}) as any; const handleDownload = () => { if (annotated_image_url) { const link = document.createElement("a"); link.href = annotated_image_url; link.download = "analysis_result.jpg"; // For Firefox it is necessary to add the link to the DOM document.body.appendChild(link); link.click(); link.remove(); } }; // Precompute some helpers for rendering confidences const isCINModel = /cin/i.test(String(model_used || "")); // Determine predicted class from summary.prediction if available, otherwise pick the highest confidence const predictedClassFromConfidence = (conf: any) => { try { const entries = Object.entries(conf || {}); if (entries.length === 0) return ""; return entries.reduce((a: any, b: any) => (Number(a[1]) > Number(b[1]) ? a : b))[0]; } catch (e) { return ""; } }; // Prefer the class key present in the `confidence` object to ensure we use the exact key/casing // (this avoids showing all bars when `summary.prediction` has different casing/format). const predictedClass = predictedClassFromConfidence(confidence || {}) || ((summary && (summary.prediction || summary.result)) ? String(summary.prediction || summary.result) : ""); return (
{/* Header */}

{model_used || "Analysis Result"}

Automated Image Analysis

{/* header actions intentionally empty; Generate Report button moved below analysis details */}
{/* If we're loading, show a centered loader inside the panel */} {isLoading ? (

Analyzing image...

) : ( // Image
)} {/* Summary Section - model-specific rendering (colposcopy, cytology, histopathology) */} {summary && (() => { const model = (model_used || "").toString(); const isColpo = /colpo|colposcopy/i.test(model); const isCyto = /cyto|cytology/i.test(model); const isHistoLike = /mwt|cin|histopath/i.test(model); // ------helper values const abnormalCount = Number(summary.abnormal_cells) || 0; const pred = (summary.prediction || summary.result || "").toString().toLowerCase(); const isAbnormal = abnormalCount > 0 || /abnormal|positive|high-grade|malignant/.test(pred); // Colposcopy: show only Abnormal / Normal (based on abnormal_cells count or prediction) if (isColpo) { return (

AI Summary

Result: {isAbnormal ? "Abnormal" : "Normal"}

{summary.ai_interpretation || "No AI interpretation available."}
); } // Cytology: keep existing detailed summary (abnormal/normal counts + avg confidence + interpretation) if (isCyto) { return (

AI Summary

{typeof summary.abnormal_cells !== 'undefined' && ( <>Abnormal Cells: {summary.abnormal_cells}
)} {typeof summary.normal_cells !== 'undefined' && ( <>Normal Cells: {summary.normal_cells}
)} {/* average confidence removed */}

{summary.ai_interpretation || "No AI interpretation available."}
); } // Histopathology / CIN / MWT: show average confidence prominently + interpretation if (isHistoLike) { return (

AI Summary

{summary.ai_interpretation || "No AI interpretation available."}
); } // Fallback: render only the fields that exist to avoid empty labels return (

AI Summary

{typeof summary.abnormal_cells !== 'undefined' && ( <>Abnormal Cells: {summary.abnormal_cells}
)} {typeof summary.normal_cells !== 'undefined' && ( <>Normal Cells: {summary.normal_cells}
)}

{summary.ai_interpretation || "No AI interpretation available."}
); })()} {/* Detection list */} {detections && detections.length > 0 && (

Detected Objects

)} {/* Probability / MWT visualization */} {confidence && (

Confidence Levels

{/* If MWT, CIN, or Histopathology classifier, show a visual bar for average confidence and per-class bars */} {model_used && /mwt|cin|histopathology/i.test(model_used) ? (
{/* Average confidence bar */} {/* Average confidence removed from visualization */} {/* Per-class bars */}
{isCINModel ? ( // For CIN/colposcopy classifiers, show ONLY the predicted grade (no bars for other classes) (() => { const cls = String(predictedClass || ""); const val = (confidence && (confidence as any)[cls]) || 0; const num = Number(val) || 0; const pct = num * 100; const isNegative = cls.toLowerCase().includes("negative") || cls.toLowerCase().includes("benign") || cls.toLowerCase().includes("low-grade") || cls.toLowerCase().includes("cin1"); if (!cls) { return
Prediction not available.
; } return (
{cls} {pct.toFixed(2)}%
); })() ) : ( Object.entries(confidence).map(([cls, val]) => { const num = Number(val as any) || 0; const pct = (num * 100); // Color coding: Positive/Malignant/High-grade = red, Negative/Benign/Low-grade = green const isNegative = cls.toLowerCase().includes("negative") || cls.toLowerCase().includes("benign") || cls.toLowerCase().includes("low-grade"); return (
{cls} {pct.toFixed(2)}%
); }) )}
{/* Mistral comment */}
{summary?.ai_interpretation || "No AI interpretation available."}
) : ( // Fallback display for non-MWT models
            {JSON.stringify(confidence, null, 2)}
          
)}
)} {/* Report Generation Modal */} {/* Show only when not loading and we have at least a result (so Generate Report is available even if summary/confidence are missing) */} {!isLoading && result && (
{annotated_image_url && ( )}
)} setShowReportModal(false)} onSubmit={handleGenerateReport} analysisId={annotated_image_url || ""} // Include annotated_image_url in the analysis summary so the backend can embed it analysisSummaryJson={summary ? JSON.stringify({ ...summary, model_used, confidence, annotated_image_url }) : "{}"} />
); }