diff --git a/apps/web/src/components/diagnose-result-view.tsx b/apps/web/src/components/diagnose-result-view.tsx new file mode 100644 index 0000000..091e8ab --- /dev/null +++ b/apps/web/src/components/diagnose-result-view.tsx @@ -0,0 +1,205 @@ +import { useState } from "react"; +import { ChevronDown, ChevronUp, AlertTriangle, BookOpen, ShieldCheck, Pill, RefreshCcw } from "lucide-react"; + +// Spesifikasi data yang dibutuhkan oleh cetakan ini +type Props = { + imageUrl: string; + confidence: number; // contoh: 0.95 + diseaseName: string; + scientificName: string; + riskLevel: string; + description: string; + symptoms: string[]; + preventions: string[]; + medicines: string[]; + onRescan?: () => void; // Tombol opsional untuk Scan Ulang +}; + +export function DiagnosisResultView({ + imageUrl, + confidence, + diseaseName, + scientificName, + riskLevel, + description, + symptoms, + preventions, + medicines, + onRescan, +}: Props) { + // Ingatan untuk laci (accordion) sebelah kanan + const [openSection, setOpenSection] = useState("detail"); + + const toggleSection = (section: string) => { + setOpenSection(openSection === section ? "" : section); + }; + + const confidencePercent = Math.round(confidence * 100); + + return ( +
+ + {/* KIRI: Area Gambar & Hasil Deteksi ML */} +
+ {/* Gambar Daun */} +
+ Daun yang dianalisis +
+ 📷 + Gambar yang dianalisis +
+
+ + {/* Kartu Hasil Deteksi ML */} +
+
+ + HASIL DETEKSI ML +
+ +

{diseaseName}

+

{scientificName}

+ + {/* Bar Tingkat Keyakinan AI */} +
+
+ Tingkat Keyakinan AI + {confidencePercent}% +
+
+
+
+

+ Di atas ambang batas minimum (75%) +

+
+ +
+ Tingkat Keparahan: + + {riskLevel.toUpperCase()} + +
+
+
+ + {/* KANAN: Area Edukasi (Accordion) & Referensi */} +
+ + {/* Tombol Header (Jika ada fungsi onRescan) */} + {onRescan && ( +
+ +
+ )} + + {/* Laci 1: Detail Penyakit */} +
+
toggleSection("detail")} + className="flex items-center justify-between p-4 cursor-pointer hover:bg-slate-50" + > +
+
+ +
+ Detail Penyakit +
+ {openSection === "detail" ? : } +
+ {openSection === "detail" && ( +
+

{description}

+
Gejala Umum:
+
    + {symptoms.map((sym, i) =>
  • {sym}
  • )} +
+
+ )} +
+ + {/* Laci 2: Panduan Pencegahan */} +
+
toggleSection("cegah")} + className="flex items-center justify-between p-4 cursor-pointer hover:bg-slate-50" + > +
+
+ +
+ Panduan Pencegahan +
+ {openSection === "cegah" ? : } +
+ {openSection === "cegah" && ( +
+
    + {preventions.map((prev, i) => ( +
  • + {prev} +
  • + ))} +
+
+ )} +
+ + {/* Laci 3: Rekomendasi Obat */} +
+
toggleSection("obat")} + className="flex items-center justify-between p-4 cursor-pointer hover:bg-slate-50" + > +
+
+ +
+ Rekomendasi Obat +
+ {openSection === "obat" ? : } +
+ {openSection === "obat" && ( +
+
    + {medicines.map((med, i) => ( +
  • + ⚕️ {med} +
  • + ))} +
+
+ )} +
+ + {/* Kotak Disclaimer Penting */} +
+
+ Disclaimer Penting +
+

+ ZeaVis Edu adalah alat bantu edukasi awal, bukan pengganti diagnosis profesional. Hasil ini tidak dapat dijadikan dasar keputusan agronomis tanpa konfirmasi dari ahli pertanian atau Petugas Pengamat Organisme Pengganggu Tanaman (POPT) yang berwenang. +

+
+ +
+
+ ); +} + +// Ikon bantuan untuk persentase +function CheckCircle2(props: any) { + return +} \ No newline at end of file diff --git a/apps/web/src/pages/diagnosis-detail-page.tsx b/apps/web/src/pages/diagnosis-detail-page.tsx index c2c56ff..fe23342 100644 --- a/apps/web/src/pages/diagnosis-detail-page.tsx +++ b/apps/web/src/pages/diagnosis-detail-page.tsx @@ -1,102 +1,139 @@ -import { Link, useParams } from 'react-router-dom'; -import { useQuery } from '@tanstack/react-query'; -import { Button } from '@/components/ui/button'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; -import { DiagnosisStatusBadge } from '@/components/diagnosis-status-badge'; -import { RiskBadge } from '@/components/risk-badge'; -import { apiClient } from '@/lib/api-client'; +import { Link, useParams } from "react-router-dom"; +import { useQuery } from "@tanstack/react-query"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { apiClient } from "@/lib/api-client"; +import { DiagnosisResultView } from "../components/diagnose-result-view"; export function DiagnosisDetailPage() { const { id } = useParams(); const query = useQuery({ - queryKey: ['diagnosis', id], + queryKey: ["diagnosis", id], queryFn: () => apiClient.getDiagnosis(id!), enabled: Boolean(id), }); - if (query.isLoading) return
Memuat diagnosis...
; - if (query.error || !query.data) return
Diagnosis tidak ditemukan
; + if (query.isLoading) + return ( +
+ Memuat diagnosis... +
+ ); + if (query.error || !query.data) + return ( +
+ Diagnosis tidak ditemukan +
+ ); const diagnosis = query.data; + const finalImageUrl = + diagnosis.imageUrl || "/src/assets/images/placeholder-image.png"; return (
+ {/* Header Page */}
-
-

Detail Diagnosis

-

{diagnosis.disease?.commonName ?? 'Diagnosis gagal'}

-
- +
+

+ Analisis & Modul Edukasi +

+

+ Hasil deteksi AI dan panduan edukasi berdasarkan gambar yang + diunggah +

+ +
-
- - - Daun jagung - - + {/* Warning if expert review is needed */} + {diagnosis.status === "needs_review" && ( +
+ Catatan: Hasil ini masih sementara karena tingkat + keyakinan (confidence) di bawah standar minimum dan sedang menunggu + ulasan pakar. +
+ )} - - -
- Hasil AI - -
- - {diagnosis.confidence === null ? 'Model gagal memproses gambar.' : `Confidence ${(diagnosis.confidence * 100).toFixed(1)}%`} - -
- - {diagnosis.status === 'needs_review' && ( -
- Hasil ini masih sementara karena confidence di bawah 70% dan sedang menunggu review pakar. + {/* Message if AI failed to process */} + {diagnosis.failureReason && ( +
+ Gagal Memproses: {diagnosis.failureReason} +
+ )} + + {/* Diagnosis Result View */} + + + {/* List of Alternative Predictions */} + {diagnosis.predictions && diagnosis.predictions.length > 0 && ( +
+

+ Semua Prediksi Model +

+

+ Tebakan alternatif lain dari sistem AI ZeaVis +

+
+ {diagnosis.predictions + .sort((a, b) => a.rank - b.rank) + .map((pred) => ( +
+ + {pred.modelLabel} + + + {(pred.confidence * 100).toFixed(1)}% +
- )} - {diagnosis.failureReason &&

{diagnosis.failureReason}

} - {diagnosis.disease && ( -
-
-

{diagnosis.disease.label}

- -
-

{diagnosis.disease.summary}

-
-

Rekomendasi

-
    - {diagnosis.disease.recommendations.map((item) =>
  • {item}
  • )} -
-
-
- )} - - -
+ ))} +
+ + )} - + {/* Expert Notes (Only shown if already reviewed by human) */} + {diagnosis.latestReview && ( + - Top Predictions + + Catatan Pakar Agronomi + + + Ditinjau oleh: {diagnosis.latestReview.expert.name} + - - {diagnosis.predictions.map((prediction) => ( -
- {prediction.rank}. {prediction.modelLabel} - {(prediction.confidence * 100).toFixed(1)}% -
- ))} + +

+ "{diagnosis.latestReview.notes}" +

- - {diagnosis.latestReview && ( - - - Catatan Pakar - {diagnosis.latestReview.expert.name} - - -

{diagnosis.latestReview.notes}

-
-
- )} - - ); - } + )} + + ); +} diff --git a/apps/web/src/pages/scan-page.tsx b/apps/web/src/pages/scan-page.tsx index 19187c2..7ffabe4 100644 --- a/apps/web/src/pages/scan-page.tsx +++ b/apps/web/src/pages/scan-page.tsx @@ -15,10 +15,10 @@ import { import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { Modal } from "@/components/ui/modal"; -import { DiagnosisStatusBadge } from "@/components/diagnosis-status-badge"; import type { DiagnosisRecord } from "@zeavis/shared"; import { apiClient } from "@/lib/api-client"; import { trackScan, trackDiagnosisResult } from "@/lib/telemetry"; +import { DiagnosisResultView } from "../components/diagnose-result-view"; export function ScanPage() { const [fileName, setFileName] = useState(null); @@ -36,8 +36,6 @@ export function ScanPage() { queryClient.invalidateQueries({ queryKey: ["diagnoses"] }); setDiagnosisPreview(diagnosis); setPreviewOpen(true); - setFileName(null); - setPreviewUrl(null); if (inputRef.current) inputRef.current.value = ""; }, onError: () => { @@ -55,7 +53,6 @@ export function ScanPage() { const url = URL.createObjectURL(f); setPreviewUrl(url); - // Detect image dimensions const img = new Image(); img.onload = () => { setImageDimensions({ width: img.width, height: img.height }); @@ -71,8 +68,7 @@ export function ScanPage() { }; const [previewOpen, setPreviewOpen] = useState(false); - const [diagnosisPreview, setDiagnosisPreview] = - useState(null); + const [diagnosisPreview, setDiagnosisPreview] = useState(null); const diagnosesQuery = useQuery({ queryKey: ["diagnoses"], @@ -82,12 +78,12 @@ export function ScanPage() { return (
+ {/* Main Header */}

Scan Tanaman

- Unggah foto daun jagung untuk dianalisis oleh sistem AI kami secara - real-time. + Unggah foto daun jagung untuk dianalisis oleh sistem AI kami secara real-time.

+ {/* Main Content */}
+ {/* Left Column: Upload Area */}
- {/* Left Sidebar */}
Area Unggah Gambar
- {/* Upload area */} {!previewUrl ? (
inputRef.current?.click()} > -

- Seret & Lepas Foto Daun -

-

- atau klik untuk memilih file berkas dari perangkat Anda -

+

Seret & Lepas Foto Daun

+

atau klik untuk memilih file berkas dari perangkat Anda

- - PNG, JPG, JPEG + PNG, JPG, JPEG, WEBP
- - Maks. 5 MB + Maks. 5 MB
-
) : ( @@ -150,19 +138,12 @@ export function ScanPage() { className="w-full max-h-80 object-contain" onLoad={(e) => { const img = e.currentTarget; - setImageDimensions({ - width: img.naturalWidth, - height: img.naturalHeight, - }); + setImageDimensions({ width: img.naturalWidth, height: img.naturalHeight }); }} /> - {/* Scanner area detection overlay */} {imageDimensions && (
- {/* Outer dark overlay */}
- - {/* Scan area frame - responsive to image */}
- {/* Corner markers */}
- - {/* Center text */}
AREA SCAN @@ -186,7 +164,6 @@ export function ScanPage() {
)} -
- {/* Right Sidebar */} + {/* Right Column: Guidelines */}
- {/* Result Modal */} + {/* Modal Preview Diagnosis */} { setPreviewOpen(false); setDiagnosisPreview(null); }} - title={diagnosisPreview?.disease?.commonName ?? "Hasil Diagnosis"} - size="md" + title="Hasil Analisis AI" + size="lg" footer={ diagnosisPreview && ( -
+
) } > {diagnosisPreview ? ( -
-
- - {diagnosisPreview.confidence !== null && ( - - Confidence: {(diagnosisPreview.confidence * 100).toFixed(1)}% - - )} -
- - {diagnosisPreview.disease && ( -
-

- {diagnosisPreview.disease.commonName} -

-

- {diagnosisPreview.disease.summary} -

+
+ + {/* Warning if expert review is needed */} + {diagnosisPreview.status === "needs_review" && ( +
+ Catatan: Hasil ini masih sementara karena tingkat keyakinan (confidence) di bawah standar minimum.
)} + {/* Message if AI fails to process */} {diagnosisPreview.failureReason && ( -
- {diagnosisPreview.failureReason} +
+ Gagal Memproses: {diagnosisPreview.failureReason}
)} - {diagnosisPreview.predictions.length > 0 && ( -
-

Semua Prediksi

-
+ + + {/* All Model Predictions */} + {diagnosisPreview.predictions && diagnosisPreview.predictions.length > 0 && ( +
+

Semua Prediksi Model

+

Tebakan alternatif lain dari sistem AI ZeaVis

+
{diagnosisPreview.predictions .sort((a, b) => a.rank - b.rank) .map((pred) => (
- {pred.modelLabel} - + {pred.modelLabel} + {(pred.confidence * 100).toFixed(1)}%
@@ -443,52 +368,9 @@ export function ScanPage() {
)} - -
-

- Riwayat Diagnosis Terbaru -

- {diagnosesQuery.isLoading && ( -
- Memuat riwayat... -
- )} - {!diagnosesQuery.isLoading && !diagnosesQuery.isError && ( -
- {(diagnosesQuery.data ?? []).slice(0, 5).map((d) => ( -
-
- thumb -
-
- {d.disease?.commonName ?? "Unknown"} -
-
- {new Date(d.createdAt).toLocaleString("id-ID")} -
-
-
- - Lihat - -
- ))} -
- )} -
) : null}
); -} +} \ No newline at end of file