add collapse button for ai insight

This commit is contained in:
Alberto-Audrix committed 2026-10-08 10:24:08 +07:00
1 parent 6bad900cf0
commit 67df3eeeb6
2 files changed
+676 -593

No files matched your search

+298 -252
View File
@@ -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>