diff --git a/apps/web/src/assets/images/scan-guide-bg.png b/apps/web/src/assets/images/scan-guide-bg.png new file mode 100644 index 0000000..7a3d888 Binary files /dev/null and b/apps/web/src/assets/images/scan-guide-bg.png differ diff --git a/apps/web/src/lib/api-client.ts b/apps/web/src/lib/api-client.ts index cd93ede..e24c6c7 100644 --- a/apps/web/src/lib/api-client.ts +++ b/apps/web/src/lib/api-client.ts @@ -15,6 +15,11 @@ import type { const apiBaseUrl = import.meta.env.VITE_API_BASE_URL ?? ''; +export interface ApiError extends Error { + status: number; + source?: 'uploader' | 'model-service' | 'unknown'; +} + async function fetchApi(endpoint: string, options?: RequestInit): Promise { const url = `${apiBaseUrl}${endpoint}`; const response = await fetch(url, { @@ -25,14 +30,21 @@ async function fetchApi(endpoint: string, options?: RequestInit): Promise if (!response.ok) { let errorMessage = `HTTP ${response.status}`; + let source: 'uploader' | 'model-service' | 'unknown' | undefined; try { const errorData = await response.json(); if (errorData.error) { errorMessage = errorData.error; } + if (errorData.source) { + source = errorData.source; + } } catch { } - throw new Error(errorMessage); + const error = new Error(errorMessage) as ApiError; + error.status = response.status; + error.source = source; + throw error; } return response.json(); diff --git a/apps/web/src/pages/scan-page.tsx b/apps/web/src/pages/scan-page.tsx index 82e5dae..7b8c817 100644 --- a/apps/web/src/pages/scan-page.tsx +++ b/apps/web/src/pages/scan-page.tsx @@ -4,6 +4,15 @@ import { useMutation, useQueryClient, useQuery } from "@tanstack/react-query"; import { apiClient } from "@/lib/api-client"; import { Modal } from "@/components/ui/modal"; import type { DiagnosisRecord } from "@zeavis/shared"; +import { + AlignCenter, + Camera, + Check, + CircleAlert, + Sun, + Upload, + ZoomIn, +} from "lucide-react"; export function ScanPage() { const [fileName, setFileName] = useState(null); @@ -37,24 +46,110 @@ export function ScanPage() { enabled: previewOpen, }); + const getUploadErrorMessage = (error: unknown): string => { + let raw = ""; + let source: string | undefined; + + if (error instanceof Error) { + raw = error.message; + // Check if error has source property from our ApiError + source = (error as any).source; + } else if (typeof error === "string") { + raw = error; + } + + // Distinguish between uploader service down vs model service down + if (raw.includes("HTTP 502")) { + if (source === "uploader") { + return "Layanan penyimpanan gambar sedang down. Hubungi administrator."; + } + if (source === "model-service") { + return "Layanan AI model sedang down. Hubungi administrator."; + } + return "Layanan backend sedang bermasalah (502). Silakan coba lagi."; + } + + if (raw.includes("Failed to fetch")) { + return "Koneksi ke server gagal. Pastikan API dan koneksi jaringan aktif."; + } + + if (raw.includes("Upload service error")) { + return "Layanan penyimpanan gambar sedang bermasalah. Coba beberapa saat lagi."; + } + + if (raw.includes("Model service error")) { + return "Layanan AI model sedang bermasalah. Coba beberapa saat lagi."; + } + + if (raw.includes("File type is not allowed")) { + return "Format file tidak didukung. Gunakan PNG, JPG, atau JPEG."; + } + + if (raw.includes("File must be smaller")) { + return "Ukuran file terlalu besar. Maksimal 5 MB."; + } + + if (raw.includes("File is empty")) { + return "File kosong. Silakan pilih file gambar yang valid."; + } + + return raw || "Upload gagal. Silakan coba lagi."; + }; + return ( -
-

Scan Tanaman

-
-
+
+ {/* Header */} +
+

Scan Tanaman

+

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

+
+ + {/* Main Content */} +
+ {/* Left Sidebar */} +
+
+ + Area Unggah Gambar +
+ + handleFile(e.target.files?.[0])} /> -
+ +
{mutation.isPending && ( -
Mengunggah...
+
+ Mengunggah... +
)} {mutation.isError && ( -
- {mutation.error instanceof Error - ? mutation.error.message - : String(mutation.error) || "Upload gagal"} +
+ {getUploadErrorMessage(mutation.error)}
)}
- + + {/* Right Sidebar */} +
+ {/* Card 1: Panduan Pengambilan Foto */} +
+
+
+ +
+

+ Panduan Pengambilan Foto +

+
+ +
    +
  • +
    + +
    +
    + Jarak 15–30 cm dari daun +
    +
  • +
  • +
    + +
    +
    + Pencahayaan cukup merata +
    +
  • +
  • +
    + +
    +
    + Posisi daun mengisi bingkai dengan jelas +
    +
  • +
  • +
    + +
    +
    + Hindari bayangan atau pantulan pada daun & blur pada gambar +
    +
  • +
+ +
+
+
+
+
+
+
+ + + + + + AREA SCAN + +
+
+ Posisi ideal: daun memenuhi bingkai +
+
+
+
+
+
+ + {/* Card 2: Persyaratan Berkas */} +
+

+ Persyaratan Berkas +

+
+ {[ + { label: "Format", value: "PNG, JPG, JPEG" }, + { label: "Ukuran Maks.", value: "5 MB" }, + { label: "Resolusi Min.", value: "512 × 512 px" }, + { label: "Objek Foto", value: "Daun jagung tunggal" }, + ].map((r, i) => ( +
+ + {r.label} + + + {r.value} + +
+ ))} +
+
+
+ + {/* Modal Section */} {