Spaces:
Sleeping
Sleeping
| import { useState } from "react"; | |
| import { DownloadIcon, FileTextIcon, Loader2Icon } from "lucide-react"; | |
| 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); | |
| 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"); | |
| } | |
| }; | |
| if (loading) { | |
| return ( | |
| <div className="bg-white rounded-lg shadow-sm p-6 flex flex-col items-center justify-center"> | |
| <Loader2Icon className="w-10 h-10 text-blue-600 animate-spin mb-3" /> | |
| <p className="text-teal-700 font-medium">Analyzing image...</p> | |
| </div> | |
| ); | |
| } | |
| if (!result) { | |
| return ( | |
| <div className="bg-white rounded-lg shadow-sm p-6 text-center text-gray-500"> | |
| No analysis result available yet. | |
| </div> | |
| ); | |
| } | |
| const { | |
| model_used, | |
| detections, | |
| annotated_image_url, | |
| summary, | |
| // prediction (not used here) | |
| confidence, | |
| } = result; | |
| const handleDownload = () => { | |
| if (annotated_image_url) { | |
| const link = document.createElement("a"); | |
| link.href = annotated_image_url; | |
| link.download = "analysis_result.jpg"; | |
| link.click(); | |
| } | |
| }; | |
| return ( <div className="bg-white rounded-lg shadow-sm p-6"> | |
| {/* Header */} | |
| <div className="flex items-center justify-between mb-6"> | |
| <div> | |
| <h2 className="text-2xl font-bold text-gray-800"> | |
| {model_used || "Analysis Result"} | |
| </h2> | |
| <p className="text-sm text-gray-500">Automated Image Analysis</p> | |
| </div> | |
| <div className="flex items-center gap-3"> | |
| {annotated_image_url && ( | |
| <button | |
| onClick={handleDownload} | |
| className="flex items-center gap-2 bg-gradient-to-r from-teal-700 via-teal-600 to-teal-700 text-white px-4 py-2 rounded-lg hover:opacity-90 transition-all" | |
| > | |
| <DownloadIcon className="w-4 h-4" /> | |
| Download Image | |
| </button> | |
| )} | |
| <button | |
| onClick={() => setShowReportModal(true)} | |
| className="flex items-center gap-2 bg-gradient-to-r from-teal-700 via-teal-600 to-teal-700 text-white px-4 py-2 rounded-lg hover:opacity-90 transition-all" | |
| > | |
| <FileTextIcon className="w-4 h-4" /> | |
| Generate Report | |
| </button> | |
| </div> | |
| </div> | |
| {/* Image */} | |
| <div className="relative mb-6 rounded-lg overflow-hidden border border-gray-200"> | |
| <ImageWithFallback | |
| src={annotated_image_url || uploadedImage || "/ui.jpg"} | |
| alt="Analysis Result" | |
| className="w-full h-64 object-cover" | |
| /> | |
| </div> | |
| {/* Summary Section */} | |
| {summary && ( | |
| <div className="bg-gray-50 p-4 rounded-lg mb-6"> | |
| <h3 className="text-lg font-semibold text-gray-800 mb-2"> | |
| AI Summary | |
| </h3> | |
| <p className="text-gray-700 text-sm leading-relaxed"> | |
| <strong>Abnormal Cells:</strong> {summary.abnormal_cells} <br /> | |
| <strong>Normal Cells:</strong> {summary.normal_cells} <br /> | |
| <strong>Average Confidence:</strong> {summary.avg_confidence?.toFixed(2)}% <br /> | |
| </p> | |
| <div className="mt-3 text-gray-800 text-sm italic border-t pt-2"> | |
| {summary.ai_interpretation || "No AI interpretation available."} | |
| </div> | |
| </div> | |
| )} | |
| {/* Detection list */} | |
| {detections && detections.length > 0 && ( | |
| <div className="mb-6"> | |
| <h4 className="font-semibold text-gray-900 mb-3"> | |
| Detected Objects | |
| </h4> | |
| <ul className="text-sm text-gray-700 list-disc list-inside space-y-1"> | |
| {detections.map((det: any, i: number) => ( | |
| <li key={i}> | |
| {det.name || "Object"} – {(det.confidence * 100).toFixed(1)}% | |
| </li> | |
| ))} | |
| </ul> | |
| </div> | |
| )} | |
| {/* Probability / MWT visualization */} | |
| {confidence && ( | |
| <div className="mb-6"> | |
| <h4 className="font-semibold text-gray-900 mb-3">Confidence Levels</h4> | |
| {/* 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) ? ( | |
| <div> | |
| {/* Average confidence bar */} | |
| <div className="mb-3"> | |
| <div className="flex items-center justify-between mb-1"> | |
| <span className="text-sm font-medium text-gray-700">Average confidence</span> | |
| <span className="text-sm font-mono text-gray-600"> | |
| {summary?.avg_confidence ? `${summary.avg_confidence.toFixed(2)}%` : "-"} | |
| </span> | |
| </div> | |
| <div className="w-full bg-gray-200 rounded-full h-4 overflow-hidden"> | |
| <div | |
| className="h-4 bg-gradient-to-r from-amber-600 to-amber-400" | |
| style={{ width: `${summary?.avg_confidence ?? 0}%` }} | |
| /> | |
| </div> | |
| </div> | |
| {/* Per-class bars */} | |
| <div className="space-y-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 ( | |
| <div key={cls}> | |
| <div className="flex items-center justify-between text-sm mb-1"> | |
| <span className="text-gray-700">{cls}</span> | |
| <span className="text-gray-600">{pct.toFixed(2)}%</span> | |
| </div> | |
| <div className="w-full bg-gray-100 rounded-full h-3"> | |
| <div | |
| className={`h-3 rounded-full ${isNegative ? "bg-green-500" : "bg-red-500"}`} | |
| style={{ width: `${pct.toFixed(2)}%` }} | |
| /> | |
| </div> | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| {/* Mistral comment */} | |
| <div className="mt-4 bg-gray-50 p-3 rounded-lg text-sm italic text-gray-800 border-t"> | |
| {summary?.ai_interpretation || "No AI interpretation available."} | |
| </div> | |
| </div> | |
| ) : ( | |
| // Fallback display for non-MWT models | |
| <pre className="bg-gray-100 rounded-lg p-3 text-sm overflow-x-auto"> | |
| {JSON.stringify(confidence, null, 2)} | |
| </pre> | |
| )} | |
| </div> | |
| )} | |
| {/* Report Generation Modal */} | |
| <ReportModal | |
| isOpen={showReportModal} | |
| onClose={() => setShowReportModal(false)} | |
| onSubmit={handleGenerateReport} | |
| analysisId={annotated_image_url || ""} | |
| analysisSummaryJson={summary ? JSON.stringify({ ...summary, model_used, confidence }) : "{}"} | |
| /> | |
| </div> | |
| ); | |
| } | |