add collapse button for ai insight
This commit is contained in:
1 parent
6bad900cf0
commit
67df3eeeb6
2 files changed
+676
-593
No files matched your search
+298
-252
@@ -264,6 +264,7 @@ export const AiInsightCard: React.FC<AiInsightCardProps> = ({
|
||||
const [detailsOpen, setDetailsOpen] = useState(false);
|
||||
const [reportType, setReportType] = useState<InsightReportType>('daily');
|
||||
const [dayPeriod, setDayPeriod] = useState<string | null>(null);
|
||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||
const insightInFlightRef = useRef(false);
|
||||
|
||||
const effectiveKandangId = kandangId ?? selectedKandangId;
|
||||
@@ -781,13 +782,28 @@ export const AiInsightCard: React.FC<AiInsightCardProps> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white border border-gray-100 rounded-xl shadow-sm overflow-hidden">
|
||||
<div className="bg-white border border-gray-100 rounded-xl shadow-sm overflow-hidden transition-all">
|
||||
<div className="p-5 sm:p-6">
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between mb-4 gap-4">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<i className="fa-solid fa-brain text-red-600 text-xl" />
|
||||
<div
|
||||
className={`flex flex-col md:flex-row md:items-center justify-between gap-4 ${isCollapsed ? '' : 'mb-4'}`}
|
||||
>
|
||||
<div
|
||||
className="flex items-center gap-2 min-w-0 cursor-pointer select-none group"
|
||||
onClick={() => setIsCollapsed((prev) => !prev)}
|
||||
title={isCollapsed ? 'Klik untuk membuka AI Insight' : 'Klik untuk menutup AI Insight'}
|
||||
>
|
||||
<i className="fa-solid fa-brain text-red-600 text-xl shrink-0 group-hover:scale-110 transition-transform" />
|
||||
<div className="min-w-0">
|
||||
<h2 className="text-lg font-bold text-gray-900">AI Insight</h2>
|
||||
<div className="flex items-center gap-2">
|
||||
<h2 className="text-lg font-bold text-gray-900 group-hover:text-red-700 transition-colors">
|
||||
AI Insight
|
||||
</h2>
|
||||
{isCollapsed && hasInsight && (
|
||||
<span className="text-[11px] font-semibold text-emerald-700 bg-emerald-50 border border-emerald-200 px-2 py-0.5 rounded-full">
|
||||
Tersedia
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-gray-400 truncate">
|
||||
{selectedKandang?.kandang_name ?? `Kandang ${effectiveKandangId}`}
|
||||
{selectedSite?.site_name ? ` · ${selectedSite.site_name}` : ''}
|
||||
@@ -795,7 +811,7 @@ export const AiInsightCard: React.FC<AiInsightCardProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{(page === 'dashboard' || !page) && (
|
||||
{!isCollapsed && (page === 'dashboard' || !page) && (
|
||||
<>
|
||||
<select
|
||||
value={reportType}
|
||||
@@ -832,269 +848,299 @@ export const AiInsightCard: React.FC<AiInsightCardProps> = ({
|
||||
>
|
||||
<i className={`fa-solid fa-rotate-right text-gray-700 ${loading ? 'fa-spin' : ''}`} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsCollapsed((prev) => !prev)}
|
||||
className="p-2 rounded-lg transition-all shadow-sm border border-gray-200 bg-white hover:bg-gray-50 text-gray-600 hover:text-gray-900"
|
||||
title={isCollapsed ? 'Buka AI Insight' : 'Tutup AI Insight'}
|
||||
aria-label={isCollapsed ? 'Buka AI Insight' : 'Tutup AI Insight'}
|
||||
>
|
||||
<i
|
||||
className={`fa-solid ${isCollapsed ? 'fa-chevron-down' : 'fa-chevron-up'} text-sm`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{effectiveKandangId != null ? renderVisualBlock() : null}
|
||||
{!isCollapsed && (
|
||||
<>
|
||||
{effectiveKandangId != null ? renderVisualBlock() : null}
|
||||
|
||||
<div className="min-h-[120px]">
|
||||
{effectiveKandangId == null ? (
|
||||
<div className="bg-amber-50 border border-amber-100 rounded-lg p-4 text-sm text-amber-800">
|
||||
Pilih satu kandang untuk menghasilkan AI Insight. Generate untuk semua kandang tidak
|
||||
diizinkan.
|
||||
</div>
|
||||
) : loading && !insightText ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<div className="animate-spin rounded-full h-10 w-10 border-b-2 border-red-600" />
|
||||
<p className="text-sm text-gray-600 text-center max-w-sm">
|
||||
Menganalisis data peternakan… proses ini bisa memakan waktu beberapa menit.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : error && !insightText ? (
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
|
||||
<p className="text-red-700 text-sm font-semibold mb-2">Error</p>
|
||||
<p className="text-red-600 text-sm">{error}</p>
|
||||
<button
|
||||
onClick={() => generateInsight(true)}
|
||||
className="text-xs text-red-600 underline mt-2"
|
||||
>
|
||||
Coba lagi
|
||||
</button>
|
||||
</div>
|
||||
) : endCycleInsight ? (
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-xl border border-gray-100 bg-white p-5 shadow-sm">
|
||||
<h4 className="text-sm font-bold text-gray-800 mb-2 flex items-center gap-2">
|
||||
<i className="fa-solid fa-circle-check text-green-600" /> Kesimpulan
|
||||
</h4>
|
||||
<p className="text-sm text-gray-700 bg-gray-50 p-3 rounded-lg border border-gray-100">
|
||||
{endCycleInsight.kesimpulan}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-gray-100 bg-white p-5 shadow-sm">
|
||||
<h4 className="text-sm font-bold text-gray-800 mb-3 flex items-center gap-2">
|
||||
<i className="fa-solid fa-triangle-exclamation text-amber-600" /> Masalah
|
||||
</h4>
|
||||
{renderBulletList(
|
||||
endCycleInsight.masalah,
|
||||
'Tidak ada masalah kritis dari graded facts'
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-gray-100 bg-white p-5 shadow-sm">
|
||||
<h4 className="text-sm font-bold text-gray-800 mb-3 flex items-center gap-2">
|
||||
<i className="fa-solid fa-magnifying-glass-chart text-red-600" /> Akar penyebab
|
||||
</h4>
|
||||
{endCycleInsight.akar_penyebab.length === 0 ? (
|
||||
<p className="text-sm text-gray-500">
|
||||
Belum ada kandidat akar penyebab berbukti.
|
||||
</p>
|
||||
) : (
|
||||
<ul className="space-y-3">
|
||||
{endCycleInsight.akar_penyebab.map((item, index) => (
|
||||
<li
|
||||
key={`${item.id ?? item.hipotesis}-${index}`}
|
||||
className="rounded-lg border border-gray-100 bg-gray-50 p-3"
|
||||
>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<p className="text-sm font-semibold text-gray-800">{item.hipotesis}</p>
|
||||
{item.confidence ? (
|
||||
<span
|
||||
className={`rounded-full border px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide ${
|
||||
confidenceBadge[item.confidence] ?? confidenceBadge.low
|
||||
}`}
|
||||
>
|
||||
{item.confidence}
|
||||
</span>
|
||||
) : null}
|
||||
{item.fase ? (
|
||||
<span className="text-[10px] uppercase tracking-wide text-gray-500">
|
||||
{item.fase}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
{item.bukti.length > 0 ? (
|
||||
<ul className="mt-2 space-y-1">
|
||||
{item.bukti.map((bukti, buktiIdx) => (
|
||||
<li key={`${bukti}-${buktiIdx}`} className="text-xs text-gray-600">
|
||||
— {bukti}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : null}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-gray-100 bg-white p-5 shadow-sm">
|
||||
<h4 className="text-sm font-bold text-gray-800 mb-3 flex items-center gap-2">
|
||||
<i className="fa-solid fa-clipboard-check text-blue-600" /> Perbaikan siklus
|
||||
berikutnya
|
||||
</h4>
|
||||
{endCycleInsight.perbaikan_siklus_berikutnya.length === 0 ? (
|
||||
<p className="text-sm text-gray-500">Belum ada checklist perbaikan.</p>
|
||||
) : (
|
||||
<ul className="space-y-3">
|
||||
{endCycleInsight.perbaikan_siklus_berikutnya.map((item, index) => (
|
||||
<li
|
||||
key={`${item.id ?? item.aksi}-${index}`}
|
||||
className="rounded-lg border border-blue-100 bg-blue-50/40 p-3"
|
||||
>
|
||||
<p className="text-[10px] font-semibold uppercase tracking-wide text-blue-700">
|
||||
{item.fase}
|
||||
</p>
|
||||
<p className="mt-1 text-sm text-gray-800">{item.aksi}</p>
|
||||
{item.metrik_pantau ? (
|
||||
<p className="mt-1 text-xs text-gray-500">Pantau: {item.metrik_pantau}</p>
|
||||
) : null}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
{endCycleInsight.insight ? (
|
||||
<p className="mt-3 text-sm text-gray-600">{endCycleInsight.insight}</p>
|
||||
) : null}
|
||||
<CitationsBlock citations={citations} />
|
||||
</div>
|
||||
</div>
|
||||
) : structuredInsight && summary ? (
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-xl border border-gray-100 bg-white p-5 shadow-sm">
|
||||
<h4 className="text-sm font-bold text-gray-800 mb-2 flex items-center gap-2">
|
||||
<i className="fa-solid fa-circle-check text-green-600" /> Kesimpulan
|
||||
</h4>
|
||||
<p className="text-sm text-gray-700 bg-gray-50 p-3 rounded-lg border border-gray-100">
|
||||
{summary.headline}
|
||||
</p>
|
||||
<div className="mt-4">
|
||||
<h4 className="text-sm font-bold text-gray-800 mb-2 flex items-center gap-2">
|
||||
<i className="fa-solid fa-brain text-red-600" /> AI Insight
|
||||
</h4>
|
||||
<div className="text-sm text-gray-700 space-y-2 leading-relaxed">
|
||||
{renderBulletList([...summary.bullets, ...summary.risks, ...summary.actions])}
|
||||
<div className="min-h-[120px]">
|
||||
{effectiveKandangId == null ? (
|
||||
<div className="bg-amber-50 border border-amber-100 rounded-lg p-4 text-sm text-amber-800">
|
||||
Pilih satu kandang untuk menghasilkan AI Insight. Generate untuk semua kandang
|
||||
tidak diizinkan.
|
||||
</div>
|
||||
) : loading && !insightText ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<div className="animate-spin rounded-full h-10 w-10 border-b-2 border-red-600" />
|
||||
<p className="text-sm text-gray-600 text-center max-w-sm">
|
||||
Menganalisis data peternakan… proses ini bisa memakan waktu beberapa menit.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<CitationsBlock citations={citations} />
|
||||
</div>
|
||||
|
||||
{page === 'dashboard' && detailed && (
|
||||
<>
|
||||
) : error && !insightText ? (
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
|
||||
<p className="text-red-700 text-sm font-semibold mb-2">Error</p>
|
||||
<p className="text-red-600 text-sm">{error}</p>
|
||||
<button
|
||||
onClick={() => setDetailsOpen((v) => !v)}
|
||||
className="inline-flex items-center gap-2 rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm transition hover:bg-gray-50"
|
||||
onClick={() => generateInsight(true)}
|
||||
className="text-xs text-red-600 underline mt-2"
|
||||
>
|
||||
{detailsOpen ? 'Sembunyikan detail' : 'Lihat detail per metrik'}
|
||||
Coba lagi
|
||||
</button>
|
||||
{detailsOpen && (
|
||||
<div className="space-y-3">
|
||||
{detailed.hitung_ayam &&
|
||||
renderDetailSection(
|
||||
'Hitung Ayam',
|
||||
detailed.hitung_ayam,
|
||||
sectionBadgeClasses[detailed.hitung_ayam.status]
|
||||
)}
|
||||
{detailed.berat_ayam &&
|
||||
renderDetailSection(
|
||||
'Berat Ayam',
|
||||
detailed.berat_ayam,
|
||||
sectionBadgeClasses[detailed.berat_ayam.status]
|
||||
)}
|
||||
{detailed.fcr &&
|
||||
renderDetailSection(
|
||||
'FCR',
|
||||
detailed.fcr,
|
||||
sectionBadgeClasses[detailed.fcr.status]
|
||||
)}
|
||||
{detailed.eef &&
|
||||
renderDetailSection(
|
||||
'EEF',
|
||||
detailed.eef,
|
||||
sectionBadgeClasses[detailed.eef.status]
|
||||
)}
|
||||
{detailed.panel_iot &&
|
||||
renderDetailSection(
|
||||
'Panel IoT',
|
||||
detailed.panel_iot,
|
||||
sectionBadgeClasses[detailed.panel_iot.status]
|
||||
)}
|
||||
{detailed.data_quality &&
|
||||
renderDetailSection(
|
||||
'Kualitas Data',
|
||||
detailed.data_quality,
|
||||
sectionBadgeClasses[detailed.data_quality.status]
|
||||
)}
|
||||
</div>
|
||||
) : endCycleInsight ? (
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-xl border border-gray-100 bg-white p-5 shadow-sm">
|
||||
<h4 className="text-sm font-bold text-gray-800 mb-2 flex items-center gap-2">
|
||||
<i className="fa-solid fa-circle-check text-green-600" /> Kesimpulan
|
||||
</h4>
|
||||
<p className="text-sm text-gray-700 bg-gray-50 p-3 rounded-lg border border-gray-100">
|
||||
{endCycleInsight.kesimpulan}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-gray-100 bg-white p-5 shadow-sm">
|
||||
<h4 className="text-sm font-bold text-gray-800 mb-3 flex items-center gap-2">
|
||||
<i className="fa-solid fa-triangle-exclamation text-amber-600" /> Masalah
|
||||
</h4>
|
||||
{renderBulletList(
|
||||
endCycleInsight.masalah,
|
||||
'Tidak ada masalah kritis dari graded facts'
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-gray-100 bg-white p-5 shadow-sm">
|
||||
<h4 className="text-sm font-bold text-gray-800 mb-3 flex items-center gap-2">
|
||||
<i className="fa-solid fa-magnifying-glass-chart text-red-600" /> Akar
|
||||
penyebab
|
||||
</h4>
|
||||
{endCycleInsight.akar_penyebab.length === 0 ? (
|
||||
<p className="text-sm text-gray-500">
|
||||
Belum ada kandidat akar penyebab berbukti.
|
||||
</p>
|
||||
) : (
|
||||
<ul className="space-y-3">
|
||||
{endCycleInsight.akar_penyebab.map((item, index) => (
|
||||
<li
|
||||
key={`${item.id ?? item.hipotesis}-${index}`}
|
||||
className="rounded-lg border border-gray-100 bg-gray-50 p-3"
|
||||
>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<p className="text-sm font-semibold text-gray-800">
|
||||
{item.hipotesis}
|
||||
</p>
|
||||
{item.confidence ? (
|
||||
<span
|
||||
className={`rounded-full border px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide ${
|
||||
confidenceBadge[item.confidence] ?? confidenceBadge.low
|
||||
}`}
|
||||
>
|
||||
{item.confidence}
|
||||
</span>
|
||||
) : null}
|
||||
{item.fase ? (
|
||||
<span className="text-[10px] uppercase tracking-wide text-gray-500">
|
||||
{item.fase}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
{item.bukti.length > 0 ? (
|
||||
<ul className="mt-2 space-y-1">
|
||||
{item.bukti.map((bukti, buktiIdx) => (
|
||||
<li
|
||||
key={`${bukti}-${buktiIdx}`}
|
||||
className="text-xs text-gray-600"
|
||||
>
|
||||
— {bukti}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : null}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-gray-100 bg-white p-5 shadow-sm">
|
||||
<h4 className="text-sm font-bold text-gray-800 mb-3 flex items-center gap-2">
|
||||
<i className="fa-solid fa-clipboard-check text-blue-600" /> Perbaikan siklus
|
||||
berikutnya
|
||||
</h4>
|
||||
{endCycleInsight.perbaikan_siklus_berikutnya.length === 0 ? (
|
||||
<p className="text-sm text-gray-500">Belum ada checklist perbaikan.</p>
|
||||
) : (
|
||||
<ul className="space-y-3">
|
||||
{endCycleInsight.perbaikan_siklus_berikutnya.map((item, index) => (
|
||||
<li
|
||||
key={`${item.id ?? item.aksi}-${index}`}
|
||||
className="rounded-lg border border-blue-100 bg-blue-50/40 p-3"
|
||||
>
|
||||
<p className="text-[10px] font-semibold uppercase tracking-wide text-blue-700">
|
||||
{item.fase}
|
||||
</p>
|
||||
<p className="mt-1 text-sm text-gray-800">{item.aksi}</p>
|
||||
{item.metrik_pantau ? (
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
Pantau: {item.metrik_pantau}
|
||||
</p>
|
||||
) : null}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
{endCycleInsight.insight ? (
|
||||
<p className="mt-3 text-sm text-gray-600">{endCycleInsight.insight}</p>
|
||||
) : null}
|
||||
<CitationsBlock citations={citations} />
|
||||
</div>
|
||||
</div>
|
||||
) : structuredInsight && summary ? (
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-xl border border-gray-100 bg-white p-5 shadow-sm">
|
||||
<h4 className="text-sm font-bold text-gray-800 mb-2 flex items-center gap-2">
|
||||
<i className="fa-solid fa-circle-check text-green-600" /> Kesimpulan
|
||||
</h4>
|
||||
<p className="text-sm text-gray-700 bg-gray-50 p-3 rounded-lg border border-gray-100">
|
||||
{summary.headline}
|
||||
</p>
|
||||
<div className="mt-4">
|
||||
<h4 className="text-sm font-bold text-gray-800 mb-2 flex items-center gap-2">
|
||||
<i className="fa-solid fa-brain text-red-600" /> AI Insight
|
||||
</h4>
|
||||
<div className="text-sm text-gray-700 space-y-2 leading-relaxed">
|
||||
{renderBulletList([
|
||||
...summary.bullets,
|
||||
...summary.risks,
|
||||
...summary.actions,
|
||||
])}
|
||||
</div>
|
||||
</div>
|
||||
<CitationsBlock citations={citations} />
|
||||
</div>
|
||||
|
||||
{page === 'dashboard' && detailed && (
|
||||
<>
|
||||
<button
|
||||
onClick={() => setDetailsOpen((v) => !v)}
|
||||
className="inline-flex items-center gap-2 rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm transition hover:bg-gray-50"
|
||||
>
|
||||
{detailsOpen ? 'Sembunyikan detail' : 'Lihat detail per metrik'}
|
||||
</button>
|
||||
{detailsOpen && (
|
||||
<div className="space-y-3">
|
||||
{detailed.hitung_ayam &&
|
||||
renderDetailSection(
|
||||
'Hitung Ayam',
|
||||
detailed.hitung_ayam,
|
||||
sectionBadgeClasses[detailed.hitung_ayam.status]
|
||||
)}
|
||||
{detailed.berat_ayam &&
|
||||
renderDetailSection(
|
||||
'Berat Ayam',
|
||||
detailed.berat_ayam,
|
||||
sectionBadgeClasses[detailed.berat_ayam.status]
|
||||
)}
|
||||
{detailed.fcr &&
|
||||
renderDetailSection(
|
||||
'FCR',
|
||||
detailed.fcr,
|
||||
sectionBadgeClasses[detailed.fcr.status]
|
||||
)}
|
||||
{detailed.eef &&
|
||||
renderDetailSection(
|
||||
'EEF',
|
||||
detailed.eef,
|
||||
sectionBadgeClasses[detailed.eef.status]
|
||||
)}
|
||||
{detailed.panel_iot &&
|
||||
renderDetailSection(
|
||||
'Panel IoT',
|
||||
detailed.panel_iot,
|
||||
sectionBadgeClasses[detailed.panel_iot.status]
|
||||
)}
|
||||
{detailed.data_quality &&
|
||||
renderDetailSection(
|
||||
'Kualitas Data',
|
||||
detailed.data_quality,
|
||||
sectionBadgeClasses[detailed.data_quality.status]
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
) : dailyInsight ? (
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-xl border border-gray-100 bg-white p-5 shadow-sm">
|
||||
<h4 className="text-sm font-bold text-gray-800 mb-2 flex items-center gap-2">
|
||||
<i className="fa-solid fa-circle-check text-green-600" /> Kesimpulan
|
||||
</h4>
|
||||
<p className="text-sm text-gray-700 bg-gray-50 p-3 rounded-lg border border-gray-100">
|
||||
{dailyInsight.summary}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{dailyInsight.akar_masalah && (
|
||||
<div className="rounded-xl border border-amber-100/80 bg-amber-50/40 p-5 shadow-sm">
|
||||
<h4 className="text-sm font-bold text-gray-800 mb-2 flex items-center gap-2">
|
||||
<i className="fa-solid fa-magnifying-glass-chart text-amber-600" /> Akar
|
||||
Masalah
|
||||
</h4>
|
||||
<p className="text-sm text-gray-700 bg-white/80 p-3 rounded-lg border border-amber-100/80">
|
||||
{dailyInsight.akar_masalah}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
) : dailyInsight ? (
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-xl border border-gray-100 bg-white p-5 shadow-sm">
|
||||
<h4 className="text-sm font-bold text-gray-800 mb-2 flex items-center gap-2">
|
||||
<i className="fa-solid fa-circle-check text-green-600" /> Kesimpulan
|
||||
</h4>
|
||||
<p className="text-sm text-gray-700 bg-gray-50 p-3 rounded-lg border border-gray-100">
|
||||
{dailyInsight.summary}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{dailyInsight.akar_masalah && (
|
||||
<div className="rounded-xl border border-amber-100/80 bg-amber-50/40 p-5 shadow-sm">
|
||||
<h4 className="text-sm font-bold text-gray-800 mb-2 flex items-center gap-2">
|
||||
<i className="fa-solid fa-magnifying-glass-chart text-amber-600" /> Akar Masalah
|
||||
</h4>
|
||||
<p className="text-sm text-gray-700 bg-white/80 p-3 rounded-lg border border-amber-100/80">
|
||||
{dailyInsight.akar_masalah}
|
||||
<div className="rounded-xl border border-gray-100 bg-white p-5 shadow-sm">
|
||||
<h4 className="text-sm font-bold text-gray-800 mb-2 flex items-center gap-2">
|
||||
<i className="fa-solid fa-brain text-red-600" /> AI Insight & Rekomendasi
|
||||
</h4>
|
||||
<p className="text-sm text-gray-700 bg-gray-50 p-3 rounded-lg border border-gray-100 whitespace-pre-wrap">
|
||||
{dailyInsight.insight}
|
||||
</p>
|
||||
<CitationsBlock citations={citations} />
|
||||
</div>
|
||||
</div>
|
||||
) : insightText ? (
|
||||
<div className="text-sm text-gray-800 leading-relaxed whitespace-pre-wrap">
|
||||
{insightText.includes(END_CYCLE_JSON_MARKER)
|
||||
? insightText.split(END_CYCLE_JSON_MARKER)[0]?.trim()
|
||||
: insightText.includes('<!--daily_json-->')
|
||||
? insightText.split('<!--daily_json-->')[0]?.trim()
|
||||
: insightText}
|
||||
<CitationsBlock citations={citations} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center gap-2 py-8 text-center">
|
||||
<i className="fa-solid fa-brain text-gray-300 text-2xl" />
|
||||
<p className="text-sm text-gray-500">Belum ada AI Insight untuk periode ini.</p>
|
||||
<p className="text-xs text-gray-400">
|
||||
Klik tombol refresh untuk generate insight.
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => generateInsight(true)}
|
||||
className="mt-2 inline-flex items-center gap-2 rounded-lg bg-red-600 px-3 py-1.5 text-xs font-semibold text-white hover:bg-red-700"
|
||||
>
|
||||
<i className="fa-solid fa-wand-magic-sparkles" />
|
||||
Generate Insight
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-gray-100 bg-white p-5 shadow-sm">
|
||||
<h4 className="text-sm font-bold text-gray-800 mb-2 flex items-center gap-2">
|
||||
<i className="fa-solid fa-brain text-red-600" /> AI Insight & Rekomendasi
|
||||
</h4>
|
||||
<p className="text-sm text-gray-700 bg-gray-50 p-3 rounded-lg border border-gray-100 whitespace-pre-wrap">
|
||||
{dailyInsight.insight}
|
||||
</p>
|
||||
<CitationsBlock citations={citations} />
|
||||
{lastUpdated && (
|
||||
<div className="mt-4 pt-4 border-t border-gray-100">
|
||||
<p className="text-xs text-gray-500">Terakhir diperbarui: {formatLastUpdated()}</p>
|
||||
</div>
|
||||
</div>
|
||||
) : insightText ? (
|
||||
<div className="text-sm text-gray-800 leading-relaxed whitespace-pre-wrap">
|
||||
{insightText.includes(END_CYCLE_JSON_MARKER)
|
||||
? insightText.split(END_CYCLE_JSON_MARKER)[0]?.trim()
|
||||
: insightText.includes('<!--daily_json-->')
|
||||
? insightText.split('<!--daily_json-->')[0]?.trim()
|
||||
: insightText}
|
||||
<CitationsBlock citations={citations} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center gap-2 py-8 text-center">
|
||||
<i className="fa-solid fa-brain text-gray-300 text-2xl" />
|
||||
<p className="text-sm text-gray-500">Belum ada AI Insight untuk periode ini.</p>
|
||||
<p className="text-xs text-gray-400">Klik tombol refresh untuk generate insight.</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => generateInsight(true)}
|
||||
className="mt-2 inline-flex items-center gap-2 rounded-lg bg-red-600 px-3 py-1.5 text-xs font-semibold text-white hover:bg-red-700"
|
||||
>
|
||||
<i className="fa-solid fa-wand-magic-sparkles" />
|
||||
Generate Insight
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{lastUpdated && (
|
||||
<div className="mt-4 pt-4 border-t border-gray-100">
|
||||
<p className="text-xs text-gray-500">Terakhir diperbarui: {formatLastUpdated()}</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+378
-341
@@ -169,6 +169,7 @@ export const PageAiInsight: React.FC<PageAiInsightProps> = ({
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [stage, setStage] = useState<InsightJobStage | null>(null);
|
||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||
const supportsTimeframe = Boolean(scalarScope);
|
||||
const timeframe: 'harian' | 'mingguan' = 'harian';
|
||||
|
||||
@@ -673,14 +674,27 @@ export const PageAiInsight: React.FC<PageAiInsightProps> = ({
|
||||
}, [scopeKey]);
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-gray-100 bg-white shadow-sm overflow-hidden mb-6">
|
||||
<div className="rounded-xl border border-gray-100 bg-white shadow-sm overflow-hidden mb-6 transition-all">
|
||||
<div className="flex items-center justify-between px-5 py-3.5 border-b border-gray-100 bg-gray-50/80">
|
||||
<div className="flex items-center gap-2.5 min-w-0">
|
||||
<div className="p-1.5 bg-red-50 rounded-lg">
|
||||
<div
|
||||
className="flex items-center gap-2.5 min-w-0 cursor-pointer select-none group"
|
||||
onClick={() => setIsCollapsed((prev) => !prev)}
|
||||
title={isCollapsed ? 'Klik untuk membuka AI Insight' : 'Klik untuk menutup AI Insight'}
|
||||
>
|
||||
<div className="p-1.5 bg-red-50 rounded-lg group-hover:bg-red-100 transition-colors">
|
||||
<i className={`fa-solid ${meta.icon} text-red-600 text-sm`} />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<span className="text-sm font-bold text-gray-900">{meta.title}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-bold text-gray-900 group-hover:text-red-700 transition-colors">
|
||||
{meta.title}
|
||||
</span>
|
||||
{isCollapsed && result && (
|
||||
<span className="text-[10px] font-semibold text-emerald-700 bg-emerald-50 border border-emerald-200 px-2 py-0.5 rounded-full">
|
||||
Tersedia
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-gray-400 truncate">
|
||||
{selectedKandang?.kandang_name ?? 'Kandang'}
|
||||
{selectedSite?.site_name ? ` · ${selectedSite.site_name}` : ''}
|
||||
@@ -693,20 +707,23 @@ export const PageAiInsight: React.FC<PageAiInsightProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
{supportsTimeframe && timeframe === 'harian' && availableDays.length > 1 && (
|
||||
<select
|
||||
value={effectiveDay ?? availableDays[availableDays.length - 1]}
|
||||
onChange={(e) => setSelectedDay(parseInt(e.target.value, 10))}
|
||||
className="text-[11px] font-semibold border border-gray-200 bg-white text-gray-700 rounded-lg px-2 py-1 shadow-sm"
|
||||
title="Pilih hari siklus"
|
||||
>
|
||||
{availableDays.map((d) => (
|
||||
<option key={d} value={d}>
|
||||
Hari {d}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
{!isCollapsed &&
|
||||
supportsTimeframe &&
|
||||
timeframe === 'harian' &&
|
||||
availableDays.length > 1 && (
|
||||
<select
|
||||
value={effectiveDay ?? availableDays[availableDays.length - 1]}
|
||||
onChange={(e) => setSelectedDay(parseInt(e.target.value, 10))}
|
||||
className="text-[11px] font-semibold border border-gray-200 bg-white text-gray-700 rounded-lg px-2 py-1 shadow-sm"
|
||||
title="Pilih hari siklus"
|
||||
>
|
||||
{availableDays.map((d) => (
|
||||
<option key={d} value={d}>
|
||||
Hari {d}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
<button
|
||||
onClick={() => generate(true)}
|
||||
disabled={loading || selectedKandangId == null}
|
||||
@@ -717,357 +734,377 @@ export const PageAiInsight: React.FC<PageAiInsightProps> = ({
|
||||
className={`fa-solid fa-rotate-right text-gray-600 text-sm ${loading ? 'fa-spin' : ''}`}
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsCollapsed((prev) => !prev)}
|
||||
title={isCollapsed ? 'Buka AI Insight' : 'Tutup AI Insight'}
|
||||
aria-label={isCollapsed ? 'Buka AI Insight' : 'Tutup AI Insight'}
|
||||
className="p-1.5 rounded-lg border border-gray-200 bg-white hover:bg-gray-50 transition-all text-gray-600 hover:text-gray-900"
|
||||
>
|
||||
<i
|
||||
className={`fa-solid ${isCollapsed ? 'fa-chevron-down' : 'fa-chevron-up'} text-sm`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4">
|
||||
{selectedKandangId == null && (
|
||||
<div className="flex items-start gap-2 bg-amber-50 border border-amber-100 rounded-xl p-3">
|
||||
<i className="fa-solid fa-triangle-exclamation text-amber-500 mt-0.5" />
|
||||
<p className="text-xs text-amber-800">
|
||||
Pilih satu kandang untuk menghasilkan AI Insight. Generate untuk semua kandang tidak
|
||||
diizinkan.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{!isCollapsed && (
|
||||
<div className="p-4">
|
||||
{selectedKandangId == null && (
|
||||
<div className="flex items-start gap-2 bg-amber-50 border border-amber-100 rounded-xl p-3">
|
||||
<i className="fa-solid fa-triangle-exclamation text-amber-500 mt-0.5" />
|
||||
<p className="text-xs text-amber-800">
|
||||
Pilih satu kandang untuk menghasilkan AI Insight. Generate untuk semua kandang tidak
|
||||
diizinkan.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading && !result && (
|
||||
<div className="flex flex-col items-center gap-3 py-8 justify-center">
|
||||
<div className="w-8 h-8 rounded-full border-4 border-gray-100 border-t-red-600 animate-spin" />
|
||||
<InsightProgressSteps stage={stage} />
|
||||
<p className="text-sm text-gray-500 text-center max-w-sm">
|
||||
Menganalisis data dengan model AI… proses ini bisa memakan waktu beberapa menit.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{loading && !result && (
|
||||
<div className="flex flex-col items-center gap-3 py-8 justify-center">
|
||||
<div className="w-8 h-8 rounded-full border-4 border-gray-100 border-t-red-600 animate-spin" />
|
||||
<InsightProgressSteps stage={stage} />
|
||||
<p className="text-sm text-gray-500 text-center max-w-sm">
|
||||
Menganalisis data dengan model AI… proses ini bisa memakan waktu beberapa menit.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && !result && (
|
||||
<div className="flex items-start gap-2 bg-red-50 border border-red-100 rounded-xl p-3">
|
||||
<i className="fa-solid fa-triangle-exclamation text-red-400 shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<p className="text-xs font-semibold text-red-600">Gagal memuat insight</p>
|
||||
<p className="text-xs text-red-500 mt-0.5">{error}</p>
|
||||
{error && !result && (
|
||||
<div className="flex items-start gap-2 bg-red-50 border border-red-100 rounded-xl p-3">
|
||||
<i className="fa-solid fa-triangle-exclamation text-red-400 shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<p className="text-xs font-semibold text-red-600">Gagal memuat insight</p>
|
||||
<p className="text-xs text-red-500 mt-0.5">{error}</p>
|
||||
<button
|
||||
onClick={() => generate(true)}
|
||||
className="text-xs text-red-600 underline mt-1 hover:text-red-800"
|
||||
>
|
||||
Coba lagi
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && !error && !result && selectedKandangId != null && (
|
||||
<div className="flex flex-col items-center justify-center gap-2 py-8 text-center">
|
||||
<i className="fa-solid fa-brain text-gray-300 text-2xl" />
|
||||
<p className="text-sm text-gray-500">Belum ada AI Insight untuk periode ini.</p>
|
||||
<p className="text-xs text-gray-400">Klik tombol refresh untuk generate insight.</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => generate(true)}
|
||||
className="text-xs text-red-600 underline mt-1 hover:text-red-800"
|
||||
className="mt-2 inline-flex items-center gap-2 rounded-lg bg-red-600 px-3 py-1.5 text-xs font-semibold text-white hover:bg-red-700"
|
||||
>
|
||||
Coba lagi
|
||||
<i className="fa-solid fa-wand-magic-sparkles" />
|
||||
Generate Insight
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
|
||||
{!loading && !error && !result && selectedKandangId != null && (
|
||||
<div className="flex flex-col items-center justify-center gap-2 py-8 text-center">
|
||||
<i className="fa-solid fa-brain text-gray-300 text-2xl" />
|
||||
<p className="text-sm text-gray-500">Belum ada AI Insight untuk periode ini.</p>
|
||||
<p className="text-xs text-gray-400">Klik tombol refresh untuk generate insight.</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => generate(true)}
|
||||
className="mt-2 inline-flex items-center gap-2 rounded-lg bg-red-600 px-3 py-1.5 text-xs font-semibold text-white hover:bg-red-700"
|
||||
>
|
||||
<i className="fa-solid fa-wand-magic-sparkles" />
|
||||
Generate Insight
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{result && (
|
||||
<div className="space-y-4">
|
||||
{heroSpec && (
|
||||
<HeroFigure
|
||||
label={heroSpec.label}
|
||||
value={heroSpec.value}
|
||||
unit={heroSpec.unit}
|
||||
context={heroSpec.context}
|
||||
status={heroSpec.status}
|
||||
deltaLabel={heroSpec.deltaLabel}
|
||||
/>
|
||||
)}
|
||||
<div className="bg-white rounded-xl border border-gray-100 shadow-sm p-5">
|
||||
{periodeLabel && (
|
||||
<p className="mb-3 text-xs font-medium text-gray-500">
|
||||
Angka di bawah dihitung dari periode{' '}
|
||||
<span className="text-gray-700">{periodeLabel}</span>
|
||||
</p>
|
||||
{result && (
|
||||
<div className="space-y-4">
|
||||
{heroSpec && (
|
||||
<HeroFigure
|
||||
label={heroSpec.label}
|
||||
value={heroSpec.value}
|
||||
unit={heroSpec.unit}
|
||||
context={heroSpec.context}
|
||||
status={heroSpec.status}
|
||||
deltaLabel={heroSpec.deltaLabel}
|
||||
/>
|
||||
)}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-y-3 gap-x-6">
|
||||
{topic === 'fcr' && (
|
||||
<>
|
||||
<div className="flex justify-between items-center py-1 border-b border-gray-50">
|
||||
<span className="text-sm text-gray-600 font-medium">
|
||||
Konsumsi Pakan Kumulatif
|
||||
</span>
|
||||
<span className="text-sm font-bold text-gray-900">
|
||||
{renderValue(displayContext.pakan_karung_terakhir, 'Karung')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center py-1 border-b border-gray-50">
|
||||
<span className="text-sm text-gray-600 font-medium">Rata-rata Bobot</span>
|
||||
<span className="text-sm font-bold text-gray-900">
|
||||
{renderValue(displayContext.bobot_iot_gram_terakhir, 'g')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center py-1 border-b border-gray-50">
|
||||
<span className="text-sm text-gray-600 font-medium">Populasi Hidup</span>
|
||||
<span className="text-sm font-bold text-gray-900">
|
||||
{renderValue(displayContext.ayam_hidup_ekor, 'Ekor')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center py-1 border-b border-gray-50">
|
||||
<span className="text-sm text-gray-600 font-medium">Total Bobot Panen</span>
|
||||
<span className="text-sm font-bold text-gray-900">
|
||||
{renderValue(displayContext.beratPanen_kg, 'kg')}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
<div className="bg-white rounded-xl border border-gray-100 shadow-sm p-5">
|
||||
{periodeLabel && (
|
||||
<p className="mb-3 text-xs font-medium text-gray-500">
|
||||
Angka di bawah dihitung dari periode{' '}
|
||||
<span className="text-gray-700">{periodeLabel}</span>
|
||||
</p>
|
||||
)}
|
||||
|
||||
{topic === 'berat_ayam' && (
|
||||
<>
|
||||
<div className="flex justify-between items-center py-1 border-b border-gray-50">
|
||||
<span className="text-sm text-gray-600 font-medium">Target Bobot</span>
|
||||
<span className="text-sm font-bold text-gray-900">
|
||||
{renderValue(displayContext.targetWeight, 'g')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center py-1 border-b border-gray-50">
|
||||
<span className="text-sm text-gray-600 font-medium">Uniformity</span>
|
||||
<span className="text-sm font-bold text-gray-900">
|
||||
{renderValue(displayContext.uniformity, '%')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center py-1 border-b border-gray-50">
|
||||
<span className="text-sm text-gray-600 font-medium">
|
||||
Average Daily Gain (ADG)
|
||||
</span>
|
||||
<span className="text-sm font-bold text-gray-900">
|
||||
{renderValue(displayContext.adg, 'g/hari')}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{topic === 'eef' && (
|
||||
<>
|
||||
<div className="flex justify-between items-center py-1 border-b border-gray-50">
|
||||
<span className="text-sm text-gray-600 font-medium">FCR Aktual</span>
|
||||
<span className="text-sm font-bold text-gray-900">
|
||||
{typeof displayContext.fcr_terakhir === 'number'
|
||||
? formatRatio(displayContext.fcr_terakhir, 3)
|
||||
: renderValue(displayContext.fcr_terakhir)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center py-1 border-b border-gray-50">
|
||||
<span className="text-sm text-gray-600 font-medium">Persentase Hidup</span>
|
||||
<span className="text-sm font-bold text-gray-900">
|
||||
{typeof displayContext.persen_hidup === 'number'
|
||||
? `${displayContext.persen_hidup.toLocaleString('id-ID', {
|
||||
maximumFractionDigits: 2,
|
||||
})} %`
|
||||
: renderValue(displayContext.persen_hidup, '%')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center py-1 border-b border-gray-50">
|
||||
<span className="text-sm text-gray-600 font-medium">BW Rata-rata Panen</span>
|
||||
<span className="text-sm font-bold text-gray-900">
|
||||
{typeof displayContext.bw_panen_gram === 'number'
|
||||
? `${displayContext.bw_panen_gram.toLocaleString('id-ID', {
|
||||
maximumFractionDigits: 2,
|
||||
})} g`
|
||||
: renderValue(displayContext.bw_panen_gram, 'g')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center py-1 border-b border-gray-50">
|
||||
<span className="text-sm text-gray-600 font-medium">
|
||||
Umur Rata-rata Panen
|
||||
</span>
|
||||
<span className="text-sm font-bold text-gray-900">
|
||||
{typeof displayContext.umur_panen_rata_hari === 'number'
|
||||
? `${displayContext.umur_panen_rata_hari.toLocaleString('id-ID', {
|
||||
maximumFractionDigits: 2,
|
||||
})} hari`
|
||||
: renderValue(displayContext.umur_panen_rata_hari, 'hari')}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{topic === 'iot_panel' && (
|
||||
<>
|
||||
<div className="flex justify-between items-center py-1 border-b border-gray-50">
|
||||
<span className="text-sm text-gray-600 font-medium">Experience Suhu</span>
|
||||
<span className="text-sm font-bold text-gray-900">
|
||||
{renderDayRange(
|
||||
displayContext.experience_suhu_min_C ?? displayContext.experience_suhu_C,
|
||||
displayContext.experience_suhu_max_C ?? displayContext.experience_suhu_C,
|
||||
'°C'
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center py-1 border-b border-gray-50">
|
||||
<span className="text-sm text-gray-600 font-medium">Kelembapan</span>
|
||||
<span className="text-sm font-bold text-gray-900">
|
||||
{renderDayRange(
|
||||
displayContext.kelembapan_min_persen ?? displayContext.kelembapan_persen,
|
||||
displayContext.kelembapan_max_persen ?? displayContext.kelembapan_persen,
|
||||
'%'
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center py-1 border-b border-gray-50">
|
||||
<span className="text-sm text-gray-600 font-medium">Kecepatan Angin</span>
|
||||
<span className="text-sm font-bold text-gray-900">
|
||||
{renderDayRange(
|
||||
displayContext.kecepatan_angin_min_mPerDetik ??
|
||||
displayContext.kecepatan_angin_mPerDetik,
|
||||
displayContext.kecepatan_angin_max_mPerDetik ??
|
||||
displayContext.kecepatan_angin_mPerDetik,
|
||||
'm/s'
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{topic === 'hitung_karung' && (
|
||||
<>
|
||||
<div className="flex justify-between items-center py-1 border-b border-gray-50">
|
||||
<div>
|
||||
<span className="text-sm text-gray-600 font-medium block">
|
||||
Total Karung Masuk
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-y-3 gap-x-6">
|
||||
{topic === 'fcr' && (
|
||||
<>
|
||||
<div className="flex justify-between items-center py-1 border-b border-gray-50">
|
||||
<span className="text-sm text-gray-600 font-medium">
|
||||
Konsumsi Pakan Kumulatif
|
||||
</span>
|
||||
<span className="text-sm font-bold text-gray-900">
|
||||
{renderValue(displayContext.pakan_karung_terakhir, 'Karung')}
|
||||
</span>
|
||||
{(typeof displayContext.saldo_awal_karung === 'number' ||
|
||||
typeof displayContext.total_karung_masuk_iot === 'number') && (
|
||||
<span className="text-xs text-gray-400">
|
||||
{[
|
||||
typeof displayContext.saldo_awal_karung === 'number'
|
||||
? `Saldo Awal: ${displayContext.saldo_awal_karung.toLocaleString('id-ID')}`
|
||||
: null,
|
||||
typeof displayContext.total_karung_masuk_iot === 'number'
|
||||
? `Masuk IoT: ${displayContext.total_karung_masuk_iot.toLocaleString('id-ID')}`
|
||||
: null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' + ')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-sm font-bold text-gray-900">
|
||||
{renderValue(feedMasukTotal(displayContext), 'Karung')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center py-1 border-b border-gray-50">
|
||||
<span className="text-sm text-gray-600 font-medium">
|
||||
Total Karung Pakan Dituang
|
||||
</span>
|
||||
<span className="text-sm font-bold text-gray-900">
|
||||
{renderValue(displayContext.total_karung_dituang_iot, 'Karung')}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="flex justify-between items-center py-1 border-b border-gray-50">
|
||||
<span className="text-sm text-gray-600 font-medium">Rata-rata Bobot</span>
|
||||
<span className="text-sm font-bold text-gray-900">
|
||||
{renderValue(displayContext.bobot_iot_gram_terakhir, 'g')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center py-1 border-b border-gray-50">
|
||||
<span className="text-sm text-gray-600 font-medium">Populasi Hidup</span>
|
||||
<span className="text-sm font-bold text-gray-900">
|
||||
{renderValue(displayContext.ayam_hidup_ekor, 'Ekor')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center py-1 border-b border-gray-50">
|
||||
<span className="text-sm text-gray-600 font-medium">Total Bobot Panen</span>
|
||||
<span className="text-sm font-bold text-gray-900">
|
||||
{renderValue(displayContext.beratPanen_kg, 'kg')}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{topic === 'hitung_ayam' && (
|
||||
<>
|
||||
<div className="flex justify-between items-center py-1 border-b border-gray-50">
|
||||
<span className="text-sm text-gray-600 font-medium">Populasi Awal</span>
|
||||
<span className="text-sm font-bold text-gray-900">
|
||||
{renderValue(displayContext.populasi_awal_ekor, 'Ekor')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center py-1 border-b border-gray-50">
|
||||
<span className="text-sm text-gray-600 font-medium">Populasi Kini</span>
|
||||
<span className="text-sm font-bold text-gray-900">
|
||||
{renderValue(displayContext.populasi_kini_ekor, 'Ekor')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center py-1 border-b border-gray-50">
|
||||
<span className="text-sm text-gray-600 font-medium">
|
||||
Mortalitas Kumulatif
|
||||
</span>
|
||||
<span className="text-sm font-bold text-gray-900">
|
||||
{renderValue(displayContext.mortalitas_kumulatif_ekor, 'Ekor')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center py-1 border-b border-gray-50">
|
||||
<span className="text-sm text-gray-600 font-medium">Mortalitas</span>
|
||||
<span className="text-sm font-bold text-gray-900">
|
||||
{typeof displayContext.mortalitas_persen === 'number'
|
||||
? `${displayContext.mortalitas_persen.toLocaleString('id-ID', {
|
||||
maximumFractionDigits: 2,
|
||||
})} %`
|
||||
: renderValue(displayContext.mortalitas_persen, '%')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center py-1 border-b border-gray-50">
|
||||
<span className="text-sm text-gray-600 font-medium">Panen Kumulatif</span>
|
||||
<span className="text-sm font-bold text-gray-900">
|
||||
{renderValue(displayContext.panen_kumulatif_ekor, 'Ekor')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center py-1 border-b border-gray-50">
|
||||
<span className="text-sm text-gray-600 font-medium">Berat Panen</span>
|
||||
<span className="text-sm font-bold text-gray-900">
|
||||
{renderValue(displayContext.berat_panen_kumulatif_kg, 'kg')}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{topic === 'berat_ayam' && (
|
||||
<>
|
||||
<div className="flex justify-between items-center py-1 border-b border-gray-50">
|
||||
<span className="text-sm text-gray-600 font-medium">Target Bobot</span>
|
||||
<span className="text-sm font-bold text-gray-900">
|
||||
{renderValue(displayContext.targetWeight, 'g')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center py-1 border-b border-gray-50">
|
||||
<span className="text-sm text-gray-600 font-medium">Uniformity</span>
|
||||
<span className="text-sm font-bold text-gray-900">
|
||||
{renderValue(displayContext.uniformity, '%')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center py-1 border-b border-gray-50">
|
||||
<span className="text-sm text-gray-600 font-medium">
|
||||
Average Daily Gain (ADG)
|
||||
</span>
|
||||
<span className="text-sm font-bold text-gray-900">
|
||||
{renderValue(displayContext.adg, 'g/hari')}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{chartSpec && (
|
||||
<div className="mt-6 pt-4 border-t border-gray-100">
|
||||
{chartSpec.type === 'trend' ? (
|
||||
<InsightTrendChart
|
||||
rows={trendChartRows}
|
||||
series={chartSpec.series}
|
||||
unit={chartSpec.unit}
|
||||
title={chartSpec.title}
|
||||
highlight={chartHighlight}
|
||||
gap={chartSpec.gap ?? null}
|
||||
/>
|
||||
) : (
|
||||
<InsightConditionChart bars={chartSpec.bars} title={chartSpec.title} />
|
||||
{topic === 'eef' && (
|
||||
<>
|
||||
<div className="flex justify-between items-center py-1 border-b border-gray-50">
|
||||
<span className="text-sm text-gray-600 font-medium">FCR Aktual</span>
|
||||
<span className="text-sm font-bold text-gray-900">
|
||||
{typeof displayContext.fcr_terakhir === 'number'
|
||||
? formatRatio(displayContext.fcr_terakhir, 3)
|
||||
: renderValue(displayContext.fcr_terakhir)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center py-1 border-b border-gray-50">
|
||||
<span className="text-sm text-gray-600 font-medium">Persentase Hidup</span>
|
||||
<span className="text-sm font-bold text-gray-900">
|
||||
{typeof displayContext.persen_hidup === 'number'
|
||||
? `${displayContext.persen_hidup.toLocaleString('id-ID', {
|
||||
maximumFractionDigits: 2,
|
||||
})} %`
|
||||
: renderValue(displayContext.persen_hidup, '%')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center py-1 border-b border-gray-50">
|
||||
<span className="text-sm text-gray-600 font-medium">
|
||||
BW Rata-rata Panen
|
||||
</span>
|
||||
<span className="text-sm font-bold text-gray-900">
|
||||
{typeof displayContext.bw_panen_gram === 'number'
|
||||
? `${displayContext.bw_panen_gram.toLocaleString('id-ID', {
|
||||
maximumFractionDigits: 2,
|
||||
})} g`
|
||||
: renderValue(displayContext.bw_panen_gram, 'g')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center py-1 border-b border-gray-50">
|
||||
<span className="text-sm text-gray-600 font-medium">
|
||||
Umur Rata-rata Panen
|
||||
</span>
|
||||
<span className="text-sm font-bold text-gray-900">
|
||||
{typeof displayContext.umur_panen_rata_hari === 'number'
|
||||
? `${displayContext.umur_panen_rata_hari.toLocaleString('id-ID', {
|
||||
maximumFractionDigits: 2,
|
||||
})} hari`
|
||||
: renderValue(displayContext.umur_panen_rata_hari, 'hari')}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{topic === 'iot_panel' && (
|
||||
<>
|
||||
<div className="flex justify-between items-center py-1 border-b border-gray-50">
|
||||
<span className="text-sm text-gray-600 font-medium">Experience Suhu</span>
|
||||
<span className="text-sm font-bold text-gray-900">
|
||||
{renderDayRange(
|
||||
displayContext.experience_suhu_min_C ??
|
||||
displayContext.experience_suhu_C,
|
||||
displayContext.experience_suhu_max_C ??
|
||||
displayContext.experience_suhu_C,
|
||||
'°C'
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center py-1 border-b border-gray-50">
|
||||
<span className="text-sm text-gray-600 font-medium">Kelembapan</span>
|
||||
<span className="text-sm font-bold text-gray-900">
|
||||
{renderDayRange(
|
||||
displayContext.kelembapan_min_persen ??
|
||||
displayContext.kelembapan_persen,
|
||||
displayContext.kelembapan_max_persen ??
|
||||
displayContext.kelembapan_persen,
|
||||
'%'
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center py-1 border-b border-gray-50">
|
||||
<span className="text-sm text-gray-600 font-medium">Kecepatan Angin</span>
|
||||
<span className="text-sm font-bold text-gray-900">
|
||||
{renderDayRange(
|
||||
displayContext.kecepatan_angin_min_mPerDetik ??
|
||||
displayContext.kecepatan_angin_mPerDetik,
|
||||
displayContext.kecepatan_angin_max_mPerDetik ??
|
||||
displayContext.kecepatan_angin_mPerDetik,
|
||||
'm/s'
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{topic === 'hitung_karung' && (
|
||||
<>
|
||||
<div className="flex justify-between items-center py-1 border-b border-gray-50">
|
||||
<div>
|
||||
<span className="text-sm text-gray-600 font-medium block">
|
||||
Total Karung Masuk
|
||||
</span>
|
||||
{(typeof displayContext.saldo_awal_karung === 'number' ||
|
||||
typeof displayContext.total_karung_masuk_iot === 'number') && (
|
||||
<span className="text-xs text-gray-400">
|
||||
{[
|
||||
typeof displayContext.saldo_awal_karung === 'number'
|
||||
? `Saldo Awal: ${displayContext.saldo_awal_karung.toLocaleString('id-ID')}`
|
||||
: null,
|
||||
typeof displayContext.total_karung_masuk_iot === 'number'
|
||||
? `Masuk IoT: ${displayContext.total_karung_masuk_iot.toLocaleString('id-ID')}`
|
||||
: null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' + ')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-sm font-bold text-gray-900">
|
||||
{renderValue(feedMasukTotal(displayContext), 'Karung')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center py-1 border-b border-gray-50">
|
||||
<span className="text-sm text-gray-600 font-medium">
|
||||
Total Karung Pakan Dituang
|
||||
</span>
|
||||
<span className="text-sm font-bold text-gray-900">
|
||||
{renderValue(displayContext.total_karung_dituang_iot, 'Karung')}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{topic === 'hitung_ayam' && (
|
||||
<>
|
||||
<div className="flex justify-between items-center py-1 border-b border-gray-50">
|
||||
<span className="text-sm text-gray-600 font-medium">Populasi Awal</span>
|
||||
<span className="text-sm font-bold text-gray-900">
|
||||
{renderValue(displayContext.populasi_awal_ekor, 'Ekor')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center py-1 border-b border-gray-50">
|
||||
<span className="text-sm text-gray-600 font-medium">Populasi Kini</span>
|
||||
<span className="text-sm font-bold text-gray-900">
|
||||
{renderValue(displayContext.populasi_kini_ekor, 'Ekor')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center py-1 border-b border-gray-50">
|
||||
<span className="text-sm text-gray-600 font-medium">
|
||||
Mortalitas Kumulatif
|
||||
</span>
|
||||
<span className="text-sm font-bold text-gray-900">
|
||||
{renderValue(displayContext.mortalitas_kumulatif_ekor, 'Ekor')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center py-1 border-b border-gray-50">
|
||||
<span className="text-sm text-gray-600 font-medium">Mortalitas</span>
|
||||
<span className="text-sm font-bold text-gray-900">
|
||||
{typeof displayContext.mortalitas_persen === 'number'
|
||||
? `${displayContext.mortalitas_persen.toLocaleString('id-ID', {
|
||||
maximumFractionDigits: 2,
|
||||
})} %`
|
||||
: renderValue(displayContext.mortalitas_persen, '%')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center py-1 border-b border-gray-50">
|
||||
<span className="text-sm text-gray-600 font-medium">Panen Kumulatif</span>
|
||||
<span className="text-sm font-bold text-gray-900">
|
||||
{renderValue(displayContext.panen_kumulatif_ekor, 'Ekor')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center py-1 border-b border-gray-50">
|
||||
<span className="text-sm text-gray-600 font-medium">Berat Panen</span>
|
||||
<span className="text-sm font-bold text-gray-900">
|
||||
{renderValue(displayContext.berat_panen_kumulatif_kg, 'kg')}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-6 pt-4 border-t border-gray-100">
|
||||
<h4 className="text-sm font-bold text-gray-800 mb-2 flex items-center gap-2">
|
||||
<i className="fa-solid fa-circle-check text-green-600" /> Kesimpulan
|
||||
</h4>
|
||||
<p className="text-sm text-gray-700 bg-gray-50 p-3 rounded-lg border border-gray-100">
|
||||
{result.summary}
|
||||
</p>
|
||||
</div>
|
||||
{chartSpec && (
|
||||
<div className="mt-6 pt-4 border-t border-gray-100">
|
||||
{chartSpec.type === 'trend' ? (
|
||||
<InsightTrendChart
|
||||
rows={trendChartRows}
|
||||
series={chartSpec.series}
|
||||
unit={chartSpec.unit}
|
||||
title={chartSpec.title}
|
||||
highlight={chartHighlight}
|
||||
gap={chartSpec.gap ?? null}
|
||||
/>
|
||||
) : (
|
||||
<InsightConditionChart bars={chartSpec.bars} title={chartSpec.title} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{result.akar_masalah && (
|
||||
<div className="mt-4">
|
||||
<div className="mt-6 pt-4 border-t border-gray-100">
|
||||
<h4 className="text-sm font-bold text-gray-800 mb-2 flex items-center gap-2">
|
||||
<i className="fa-solid fa-magnifying-glass-chart text-amber-600" /> Akar Masalah
|
||||
<i className="fa-solid fa-circle-check text-green-600" /> Kesimpulan
|
||||
</h4>
|
||||
<p className="text-sm text-gray-700 bg-amber-50/50 p-3 rounded-lg border border-amber-100/80">
|
||||
{result.akar_masalah}
|
||||
<p className="text-sm text-gray-700 bg-gray-50 p-3 rounded-lg border border-gray-100">
|
||||
{result.summary}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-4">
|
||||
<h4 className="text-sm font-bold text-gray-800 mb-2 flex items-center gap-2">
|
||||
<i className="fa-solid fa-brain text-red-600" /> AI Insight & Rekomendasi
|
||||
</h4>
|
||||
<p className="text-sm text-gray-700 bg-gray-50 p-3 rounded-lg border border-gray-100 whitespace-pre-wrap">
|
||||
{result.insight}
|
||||
</p>
|
||||
{result.akar_masalah && (
|
||||
<div className="mt-4">
|
||||
<h4 className="text-sm font-bold text-gray-800 mb-2 flex items-center gap-2">
|
||||
<i className="fa-solid fa-magnifying-glass-chart text-amber-600" /> Akar
|
||||
Masalah
|
||||
</h4>
|
||||
<p className="text-sm text-gray-700 bg-amber-50/50 p-3 rounded-lg border border-amber-100/80">
|
||||
{result.akar_masalah}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-4">
|
||||
<h4 className="text-sm font-bold text-gray-800 mb-2 flex items-center gap-2">
|
||||
<i className="fa-solid fa-brain text-red-600" /> AI Insight & Rekomendasi
|
||||
</h4>
|
||||
<p className="text-sm text-gray-700 bg-gray-50 p-3 rounded-lg border border-gray-100 whitespace-pre-wrap">
|
||||
{result.insight}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<CitationsBlock citations={result.citations} />
|
||||
</div>
|
||||
|
||||
<CitationsBlock citations={result.citations} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user