import { useState, useRef, useEffect } from 'react'; // Menggunakan library lucide-react untuk ikon modern dan profesional import { Camera, Upload, Utensils, Flame, RefreshCcw, HeartHandshake, CircleEllipsis, Wheat, Droplets, Egg, Cherry, Leaf, CheckCircle, XCircle, Info, X } from 'lucide-react'; const App = () => { // State untuk mengelola mode aktif: 'camera' atau 'upload' const [mode, setMode] = useState('camera'); // State untuk menyimpan stream video dari kamera const [stream, setStream] = useState(null); // State untuk menyimpan URL data gambar yang diambil/diunggah const [imageSrc, setImageSrc] = useState(null); // State untuk menyimpan hasil analisis dari AI const [result, setResult] = useState(null); // State untuk melacak status loading saat panggilan API const [isLoading, setIsLoading] = useState(false); // State untuk melacak apakah kamera sedang aktif const [isCameraOn, setIsCameraOn] = useState(false); // State untuk menyimpan deskripsi yang diisi pengguna const [userDescription, setUserDescription] = useState(''); // State untuk mengontrol tampilan bagian "Tentang" const [showAbout, setShowAbout] = useState(false); // Referensi untuk elemen video dan canvas const videoRef = useRef(null); const canvasRef = useRef(null); // Referensi untuk elemen input file untuk memicu klik secara programatik const fileInputRef = useRef(null); // Konfigurasi API untuk model Gemini const apiKey = ""; const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-05-20:generateContent?key=${apiKey}`; // Efek untuk mematikan stream kamera saat komponen dilepas atau mode berubah useEffect(() => { return () => { if (stream) { stream.getTracks().forEach(track => track.stop()); } }; }, [stream]); // Fungsi untuk memulai kamera const startCamera = async () => { // Hentikan stream yang ada terlebih dahulu if (stream) { stream.getTracks().forEach(track => track.stop()); } setImageSrc(null); setResult(null); setMode('camera'); setUserDescription(''); setShowAbout(false); // Tutup bagian "Tentang" saat memulai kamera try { // Dapatkan stream media dari kamera pengguna const cameraStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: false }); setStream(cameraStream); setIsCameraOn(true); if (videoRef.current) { videoRef.current.srcObject = cameraStream; } } catch (err) { console.error("Error accessing camera: ", err); setResult({ error: "Gagal mengakses kamera. Mohon berikan izin kamera." }); } }; // Fungsi untuk menghentikan kamera const stopCamera = () => { if (stream) { stream.getTracks().forEach(track => track.stop()); setStream(null); setIsCameraOn(false); } }; // Fungsi untuk mengambil gambar dari stream video const captureImage = () => { if (videoRef.current && canvasRef.current) { const video = videoRef.current; const canvas = canvasRef.current; const context = canvas.getContext('2d'); // Atur dimensi canvas agar sesuai dengan dimensi video canvas.width = video.videoWidth; canvas.height = video.videoHeight; // Gambar frame video saat ini ke canvas context.drawImage(video, 0, 0, canvas.width, canvas.height); // Dapatkan URL data gambar const imageData = canvas.toDataURL('image/jpeg'); setImageSrc(imageData); // Hentikan kamera setelah mengambil gambar stopCamera(); // Analisis gambar yang diambil analyzeImage(imageData, ''); } }; // Fungsi untuk menangani unggahan file const handleFileUpload = (event) => { const file = event.target.files[0]; if (file) { stopCamera(); setMode('upload'); const reader = new FileReader(); reader.onloadend = () => { setImageSrc(reader.result); setResult(null); // Reset hasil saat gambar baru diunggah }; reader.readAsDataURL(file); } }; // Fungsi untuk menganalisis gambar menggunakan Gemini API const analyzeImage = async (imageData, userDesc) => { setIsLoading(true); setResult(null); setShowAbout(false); // Tutup bagian "Tentang" saat memulai analisis // Konversi URL data ke string base64 const base64Image = imageData.split(',')[1]; try { // Prompt diperbarui untuk meminta respons JSON yang lebih detail const prompt = ` Tugas Anda adalah menganalisis gambar makanan dan memberikan laporan nutrisi yang sangat detail sesuai format berikut. 1. Identifikasi makanan dan berikan deskripsi porsi yang spesifik (contoh: "2 tahu goreng + 2 bakwan goreng"). 2. Berikan perkiraan total kalori untuk satu porsi (contoh: "± 450 Kcal"). 3. Jelaskan makronutrien (protein, lemak, karbohidrat) secara terperinci (contoh: "15 g"). 4. Berikan perkiraan gula (contoh: "2-3 g (perkiraan dari adonan tepung & sayur)"). 5. Berikan perkiraan serat (contoh: "1-2 g (dari sayur bakwan, minimal karena digoreng)"). 6. Berikan daftar saran kesehatan yang ringkas dan jelas, dengan status "aman" atau "bahaya" untuk setiap poin. 7. Jika tidak ada makanan yang terdeteksi, berikan objek JSON dengan kunci 'error' dan nilai 'Sepertinya tidak ada makanan yang terlihat'. Informasi tambahan dari pengguna: "${userDesc}" Hanya berikan respons dalam format JSON dengan struktur berikut: { "porsi": "2 tahu goreng + 2 bakwan goreng", "kalori": "± 450 Kcal", "protein": "15 g", "lemak": "30 g", "karbohidrat": "30 g", "gula": "2-3 g (perkiraan dari adonan tepung & sayur)", "serat": "1-2 g (dari sayur bakwan, minimal karena digoreng)", "saran_kesehatan": [ {"teks": "Hindari konsumsi berlebihan karena tinggi lemak jenuh.", "status": "bahaya"}, {"teks": "Pilih metode masak lain seperti dikukus atau direbus untuk mengurangi kalori.", "status": "aman"}, {"teks": "Imbangi dengan sumber serat dan protein lain.", "status": "aman"} ] } Atau jika tidak ada makanan: { "error": "Sepertinya tidak ada makanan yang terlihat" } `; const payload = { contents: [ { role: "user", parts: [ { text: prompt }, { inlineData: { mimeType: "image/jpeg", data: base64Image } } ] } ] }; // Lakukan panggilan API dengan backoff eksponensial const callApiWithRetry = async (retries = 5, delay = 1000) => { try { const response = await fetch(apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (!response.ok) { if (response.status === 429 && retries > 0) { console.warn(`API call failed with status 429. Retrying in ${delay}ms...`); await new Promise(res => setTimeout(res, delay)); return callApiWithRetry(retries - 1, delay * 2); } throw new Error(`HTTP error! status: ${response.status}`); } return await response.json(); } catch (error) { if (retries > 0) { console.warn(`API call failed. Retrying in ${delay}ms...`); await new Promise(res => setTimeout(res, delay)); return callApiWithRetry(retries - 1, delay * 2); } throw error; } }; const apiResponse = await callApiWithRetry(); if (apiResponse.candidates && apiResponse.candidates.length > 0 && apiResponse.candidates[0].content && apiResponse.candidates[0].content.parts && apiResponse.candidates[0].content.parts.length > 0) { const jsonText = apiResponse.candidates[0].content.parts[0].text; // API mungkin mengembalikan markdown, jadi kita perlu membersihkannya sebelum parsing const cleanedJsonText = jsonText.replace(/```json\n|\n```/g, ''); const parsedJson = JSON.parse(cleanedJsonText); setResult(parsedJson); } else { setResult({ error: "Gagal mendeteksi makanan. Mohon coba lagi." }); } } catch (error) { console.error("Error during API call: ", error); setResult({ error: "Terjadi kesalahan saat menganalisis. Mohon coba lagi." }); } finally { setIsLoading(false); } }; // Fungsi untuk mereset status aplikasi const resetApp = () => { stopCamera(); setImageSrc(null); setResult(null); setMode('camera'); setUserDescription(''); setShowAbout(false); // FIX: Mengatur ulang nilai input file agar dapat digunakan kembali if (fileInputRef.current) { fileInputRef.current.value = null; } }; // Fungsi untuk memulai analisis setelah deskripsi diisi (untuk mode unggah) const startAnalysisFromUpload = () => { if (imageSrc) { analyzeImage(imageSrc, userDescription); } }; return (
{/* Header dengan tombol "Tentang" */}

CzFoodCal AI

Ambil foto atau unggah gambar makanan Anda untuk mendapatkan analisis kalori dan nutrisi yang mendalam.

{/* Section untuk tampilan kamera dan foto */}
{!isCameraOn && !imageSrc && (

Pilih mode di bawah untuk memulai.

)} {isCameraOn && (
{/* Section untuk tombol aksi */}
{!imageSrc && !showAbout && (
)} {isCameraOn && mode === 'camera' && ( )} {imageSrc && !result && mode === 'upload' && !showAbout && (