import React, { useState, useCallback, useEffect, useRef } from 'react'; import { Sparkles, RefreshCw, TrendingDown, AlertTriangle, CheckCircle, Info } from 'lucide-react'; import { apiClient } from '../../services/apiService.ts'; interface KandangInsight { name: string; docIn: number; currentPopulation: number; mortalityTotal: number; mortalityRate: number; } interface PopulationPoint { date: string; population: number; } export interface ChickenCountingInsightProps { cycleId: string; cycleStartDate: string; currentDay: number; totalDays: number; totalInitialPopulation: number; populationTrend: PopulationPoint[]; kandangInsights: KandangInsight[]; } interface AiResult { headline: string; status: 'ok' | 'warning' | 'critical'; summary: string[]; risks: string[]; actions: string[]; perKandang: Array<{ name: string; status: 'ok' | 'warning' | 'critical'; insight: string; action: string; }>; } const STATUS_CONFIG = { ok: { bg: 'bg-emerald-50', border: 'border-emerald-200', text: 'text-emerald-700', badge: 'bg-emerald-100 text-emerald-700', icon: CheckCircle, label: 'Normal', }, warning: { bg: 'bg-amber-50', border: 'border-amber-200', text: 'text-amber-700', badge: 'bg-amber-100 text-amber-700', icon: AlertTriangle, label: 'Perhatian', }, critical: { bg: 'bg-red-50', border: 'border-red-200', text: 'text-red-700', badge: 'bg-red-100 text-red-700', icon: TrendingDown, label: 'Kritis', }, }; const CC_FOCUSES = [ 'Tren mortalitas harian dan dampaknya terhadap populasi akhir', 'Distribusi ayam di berbagai area/kamera (penumpukan)', 'Perbandingan populasi aktual dengan target DOC In', 'Deteksi dini potensi wabah penyakit dari lonjakan mortalitas', 'Evaluasi efektivitas afkir selektif vs kematian alami', 'Proyeksi sisa populasi layak panen', 'Evaluasi tingkat kepadatan (density) vs kapasitas kandang', ]; const buildPrompt = (props: ChickenCountingInsightProps): string => { const { currentDay, totalDays, totalInitialPopulation, populationTrend, kandangInsights } = props; const selectedFocus = CC_FOCUSES[Math.floor(Math.random() * CC_FOCUSES.length)]; const latestPop = populationTrend.length > 0 ? populationTrend[populationTrend.length - 1].population : totalInitialPopulation; const totalMortality = Math.max(0, totalInitialPopulation - latestPop); const overallMortalityRate = totalInitialPopulation > 0 ? ((totalMortality / totalInitialPopulation) * 100).toFixed(2) : '0'; const recentPops = populationTrend.slice(-7).map((p) => p.population); const trendStr = recentPops.length > 0 ? recentPops.join(' → ') : 'belum ada data trend'; const kandangStr = kandangInsights.length > 0 ? kandangInsights .map( (k) => `- ${k.name}: DOC=${k.docIn.toLocaleString()}, populasi_kini=${k.currentPopulation.toLocaleString()}, mati+afkir=${k.mortalityTotal.toLocaleString()}, mortalitas=${k.mortalityRate.toFixed(2)}%` ) .join('\n') : '- Data per kandang belum tersedia (backend offline). Gunakan data total untuk analisis.'; return `Anda adalah dokter hewan dan manajer peternakan ayam broiler berpengalaman. Analisis data hitung ayam berikut dan buat AI Insight yang MUDAH DIPAHAMI, PROFESIONAL, dan ACTIONABLE bagi peternak. Jika ada data yang belum tersedia, tetap berikan insight dan rekomendasi berdasarkan data yang ada. DATA SIKLUS: - Hari ke: ${currentDay} dari total ${totalDays} hari - Populasi awal (DOC in): ${totalInitialPopulation.toLocaleString()} ekor - Populasi estimasi saat ini: ${latestPop.toLocaleString()} ekor - Total kematian+afkir: ${totalMortality.toLocaleString()} ekor - Tingkat mortalitas keseluruhan: ${overallMortalityRate}% - Trend populasi (7 hari terakhir): ${trendStr} DATA PER KANDANG: ${kandangStr} PEDOMAN MORTALITAS: - Mortalitas <3%: NORMAL (ok) - Mortalitas 3-5%: PERLU PERHATIAN (warning) - Mortalitas >5%: KRITIS, perlu tindakan segera (critical) Konteks & Aturan Variasi: Fokus utama analisis Anda hari ini adalah: "${selectedFocus}". Jadikan ini sebagai sudut pandang utama analisis. Jangan sebutkan nama fokus secara eksplisit di hasil akhir. Dilarang keras berhalusinasi atau menambahkan angka fiktif. INSTRUKSI OUTPUT: Kembalikan HANYA satu objek JSON valid tanpa markdown, tanpa komentar, tanpa teks tambahan di luar JSON. Format JSON yang harus dikembalikan: { "headline": "satu kalimat ringkas tentang kondisi populasi saat ini yang mencerminkan fokus terpilih", "status": "ok", "summary": ["poin ringkasan 1", "poin ringkasan 2", "poin ringkasan 3"], "risks": ["risiko 1", "risiko 2"], "actions": ["tindakan konkret 1 yang bisa langsung dipraktikkan peternak hari ini", "tindakan 2", "tindakan 3"], "perKandang": [ { "name": "nama kandang", "status": "ok", "insight": "satu kalimat insight tentang kandang ini", "action": "satu tindakan konkret spesifik untuk kandang ini" } ] }`; }; const parseResult = (text: string): AiResult | null => { try { const first = text.indexOf('{'); const last = text.lastIndexOf('}'); if (first === -1 || last === -1) return null; const parsed = JSON.parse(text.slice(first, last + 1)); if (!parsed.headline) return null; return { headline: String(parsed.headline ?? ''), status: ['ok', 'warning', 'critical'].includes(parsed.status) ? parsed.status : 'ok', summary: Array.isArray(parsed.summary) ? parsed.summary.map(String) : [], risks: Array.isArray(parsed.risks) ? parsed.risks.map(String) : [], actions: Array.isArray(parsed.actions) ? parsed.actions.map(String) : [], perKandang: Array.isArray(parsed.perKandang) ? parsed.perKandang.map((k: any) => ({ name: String(k.name ?? ''), status: ['ok', 'warning', 'critical'].includes(k.status) ? k.status : 'ok', insight: String(k.insight ?? ''), action: String(k.action ?? ''), })) : [], }; } catch { return null; } }; export const ChickenCountingAiInsight: React.FC = (props) => { const [result, setResult] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [showPerKandang, setShowPerKandang] = useState(false); const inFlightRef = useRef(false); const hasGeneratedRef = useRef(false); const generate = useCallback( async (force = false) => { if (inFlightRef.current) return; // Minimal data check if (!props.cycleId) return; const cacheKey = `cc-insight-${props.cycleId}-${props.currentDay}`; if (!force) { try { const cached = sessionStorage.getItem(cacheKey); if (cached) { const parsed = parseResult(cached); if (parsed) { setResult(parsed); hasGeneratedRef.current = true; return; } } } catch { // sessionStorage might be unavailable } } inFlightRef.current = true; setLoading(true); setError(null); try { const prompt = buildPrompt(props); const system_prompt = 'Anda adalah manajer peternakan ayam broiler CP 707. Analisis HANYA berdasarkan standar buku CP 707. Kembalikan HANYA objek JSON valid. Dilarang memberikan teks di luar JSON.'; // Race antara LM Studio dan timeout 300 detik const fetchCall = apiClient.proxyLmStudio(prompt, system_prompt, 0.2); const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error('LM Studio timeout setelah 300 detik.')), 300000) ); const json = await Promise.race([fetchCall, timeout]); const text = json.response ?? ''; const parsed = parseResult(text); if (!parsed) throw new Error('Format respons LM Studio tidak valid. Coba lagi.'); if (result && JSON.stringify(parsed) === JSON.stringify(result)) { throw new Error( 'Data belum diperbarui. Hasil analisis AI masih sama dengan versi sebelumnya.' ); } setResult(parsed); hasGeneratedRef.current = true; try { sessionStorage.setItem(cacheKey, text); } catch { // ignore } } catch (err) { const msg = err instanceof Error ? err.message : 'Terjadi kesalahan tidak diketahui.'; setError(msg); } finally { setLoading(false); inFlightRef.current = false; } }, // eslint-disable-next-line react-hooks/exhaustive-deps [ props.cycleId, props.currentDay, props.totalInitialPopulation, props.kandangInsights.length, props.populationTrend.length, ] ); // Auto-generate on mount and when cycle changes useEffect(() => { if (!props.cycleId) return; if (hasGeneratedRef.current) return; const timer = setTimeout(() => { generate(false); }, 600); return () => clearTimeout(timer); // eslint-disable-next-line react-hooks/exhaustive-deps }, [props.cycleId]); // Re-generate when data becomes available (population data loaded from API) useEffect(() => { if (!hasGeneratedRef.current) return; if (props.populationTrend.length === 0) return; const timer = setTimeout(() => { generate(true); }, 1000); return () => clearTimeout(timer); // eslint-disable-next-line react-hooks/exhaustive-deps }, [props.populationTrend.length, props.kandangInsights.length]); const cfg = result ? STATUS_CONFIG[result.status] : null; const StatusIcon = cfg?.icon ?? Info; return (
{/* Header */}

AI Insight — Hitung Ayam

Analisis populasi & mortalitas berbasis kecerdasan buatan

{/* Body */}
{/* Loading */} {loading && !result && (

Menganalisis data populasi ayam...

)} {/* Error */} {error && !result && (

Gagal memuat insight

{error}

)} {/* No data / waiting */} {!loading && !error && !result && (
Memuat insight AI...
)} {/* Result */} {result && cfg && (
{/* Headline banner */}
{cfg.label} {loading && ( Memperbarui... )}

{result.headline}

{/* 3-column grid */}
{/* Ringkasan */}

📋 Ringkasan

    {result.summary.map((item, i) => (
  • {item}
  • ))}
{/* Risiko */}

⚠️ Risiko

{result.risks.length > 0 ? (
    {result.risks.map((item, i) => (
  • {item}
  • ))}
) : (

Tidak ada risiko signifikan

)}
{/* Tindakan */}

✅ Tindakan

    {result.actions.map((item, i) => (
  1. {i + 1} {item}
  2. ))}
{/* Per-Kandang */} {result.perKandang.length > 0 && (
{showPerKandang && (
{result.perKandang.map((k, i) => { const kCfg = STATUS_CONFIG[k.status] ?? STATUS_CONFIG.ok; const KIcon = kCfg.icon; return (

{k.name}

{kCfg.label}

{k.insight}

{k.action && (

Tindakan:

{k.action}

)}
); })}
)}
)}
)}
); }; export default ChickenCountingAiInsight;