update ai insight card to include status
This commit is contained in:
1 parent
3e3eb88751
commit
a9a2acb705
3 files changed
+416
-240
No files matched your search
+244
-200
@@ -27,7 +27,10 @@ import { parseAiResult, type AiResult } from '../utils/insightParse.ts';
|
|||||||
import { formatRatio } from '../utils/format.ts';
|
import { formatRatio } from '../utils/format.ts';
|
||||||
import {
|
import {
|
||||||
HeroFigure,
|
HeroFigure,
|
||||||
|
InsightStatusBadge,
|
||||||
|
INSIGHT_STATUS_META,
|
||||||
MutedStatRow,
|
MutedStatRow,
|
||||||
|
normalizeInsightStatus,
|
||||||
StatTile,
|
StatTile,
|
||||||
type InsightStatus,
|
type InsightStatus,
|
||||||
} from './shared/InsightStatTiles.tsx';
|
} from './shared/InsightStatTiles.tsx';
|
||||||
@@ -36,13 +39,6 @@ import InsightTrendChart from './shared/InsightTrendChart.tsx';
|
|||||||
const CLIENT_TIMEOUT_MS = 1_200_000; // 20 minutes
|
const CLIENT_TIMEOUT_MS = 1_200_000; // 20 minutes
|
||||||
const END_CYCLE_JSON_MARKER = '<!--end_cycle_json-->';
|
const END_CYCLE_JSON_MARKER = '<!--end_cycle_json-->';
|
||||||
|
|
||||||
const sectionBadgeClasses: Record<InsightStatusLabel, string> = {
|
|
||||||
ok: 'border-emerald-200 bg-emerald-50 text-emerald-700',
|
|
||||||
warning: 'border-amber-200 bg-amber-50 text-amber-700',
|
|
||||||
critical: 'border-red-200 bg-red-50 text-red-700',
|
|
||||||
unknown: 'border-slate-200 bg-slate-50 text-slate-600',
|
|
||||||
};
|
|
||||||
|
|
||||||
const confidenceBadge: Record<string, string> = {
|
const confidenceBadge: Record<string, string> = {
|
||||||
high: 'border-red-200 bg-red-50 text-red-700',
|
high: 'border-red-200 bg-red-50 text-red-700',
|
||||||
med: 'border-amber-200 bg-amber-50 text-amber-700',
|
med: 'border-amber-200 bg-amber-50 text-amber-700',
|
||||||
@@ -261,6 +257,7 @@ export const AiInsightCard: React.FC<AiInsightCardProps> = ({
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [lastUpdated, setLastUpdated] = useState<Date | null>(null);
|
const [lastUpdated, setLastUpdated] = useState<Date | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [insightAlert, setInsightAlert] = useState<string | null>(null);
|
||||||
const [detailsOpen, setDetailsOpen] = useState(false);
|
const [detailsOpen, setDetailsOpen] = useState(false);
|
||||||
const [reportType, setReportType] = useState<InsightReportType>('daily');
|
const [reportType, setReportType] = useState<InsightReportType>('daily');
|
||||||
const [dayPeriod, setDayPeriod] = useState<string | null>(null);
|
const [dayPeriod, setDayPeriod] = useState<string | null>(null);
|
||||||
@@ -297,10 +294,12 @@ export const AiInsightCard: React.FC<AiInsightCardProps> = ({
|
|||||||
structuredEndCycle?: EndCycleStructuredInsight | null,
|
structuredEndCycle?: EndCycleStructuredInsight | null,
|
||||||
summary?: string | null,
|
summary?: string | null,
|
||||||
akarMasalah?: string | null,
|
akarMasalah?: string | null,
|
||||||
insightBody?: string | null
|
insightBody?: string | null,
|
||||||
|
alert?: string | null
|
||||||
) => {
|
) => {
|
||||||
const citationsArg = Array.isArray(nextCitations) ? nextCitations : [];
|
const citationsArg = Array.isArray(nextCitations) ? nextCitations : [];
|
||||||
setInsightText(text);
|
setInsightText(text);
|
||||||
|
setInsightAlert(alert ?? null);
|
||||||
const endCycle =
|
const endCycle =
|
||||||
structuredEndCycle ??
|
structuredEndCycle ??
|
||||||
parseEndCycleFromText(text, summary) ??
|
parseEndCycleFromText(text, summary) ??
|
||||||
@@ -473,7 +472,8 @@ export const AiInsightCard: React.FC<AiInsightCardProps> = ({
|
|||||||
apiResult.structured_end_cycle ?? null,
|
apiResult.structured_end_cycle ?? null,
|
||||||
apiResult.summary,
|
apiResult.summary,
|
||||||
apiResult.akar_masalah,
|
apiResult.akar_masalah,
|
||||||
apiResult.insight
|
apiResult.insight,
|
||||||
|
(apiResult as Record<string, unknown> | null)?.alert as string | undefined
|
||||||
);
|
);
|
||||||
setLastUpdated(new Date());
|
setLastUpdated(new Date());
|
||||||
setDetailsOpen(false);
|
setDetailsOpen(false);
|
||||||
@@ -541,19 +541,11 @@ export const AiInsightCard: React.FC<AiInsightCardProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderDetailSection = (
|
const renderDetailSection = (title: string, section: InsightStructuredSection) => (
|
||||||
title: string,
|
|
||||||
section: InsightStructuredSection,
|
|
||||||
accentClass: string
|
|
||||||
) => (
|
|
||||||
<section className="rounded-xl border border-gray-100 bg-white p-4 shadow-sm">
|
<section className="rounded-xl border border-gray-100 bg-white p-4 shadow-sm">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.16em] text-gray-500">{title}</p>
|
<p className="text-xs font-semibold uppercase tracking-[0.16em] text-gray-500">{title}</p>
|
||||||
<span
|
<InsightStatusBadge status={section.status} size="xs" />
|
||||||
className={`rounded-full border px-2.5 py-1 text-xs font-semibold uppercase tracking-wide ${accentClass}`}
|
|
||||||
>
|
|
||||||
{section.status}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4 grid gap-4 md:grid-cols-3">
|
<div className="mt-4 grid gap-4 md:grid-cols-3">
|
||||||
<div>
|
<div>
|
||||||
@@ -634,6 +626,42 @@ export const AiInsightCard: React.FC<AiInsightCardProps> = ({
|
|||||||
const hasInsight =
|
const hasInsight =
|
||||||
Boolean(insightText.trim()) || structuredInsight != null || endCycleInsight != null;
|
Boolean(insightText.trim()) || structuredInsight != null || endCycleInsight != null;
|
||||||
|
|
||||||
|
const overallStatus = useMemo((): InsightStatus => {
|
||||||
|
if (insightAlert) {
|
||||||
|
const norm = normalizeInsightStatus(insightAlert);
|
||||||
|
if (norm !== 'unknown') return norm;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (structuredInsight?.detailed) {
|
||||||
|
const sections = Object.values(structuredInsight.detailed).filter(Boolean);
|
||||||
|
if (sections.some((s) => s.status === 'critical')) return 'critical';
|
||||||
|
if (sections.some((s) => s.status === 'warning')) return 'warning';
|
||||||
|
if (sections.some((s) => s.status === 'ok')) return 'ok';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endCycleInsight) {
|
||||||
|
if (endCycleInsight.masalah && endCycleInsight.masalah.length > 0) {
|
||||||
|
return 'warning';
|
||||||
|
}
|
||||||
|
return 'ok';
|
||||||
|
}
|
||||||
|
|
||||||
|
const tileStatuses: InsightStatus[] = [fcrStatus, eefStatus, mortalityStatus, weightStatus];
|
||||||
|
if (tileStatuses.includes('critical')) return 'critical';
|
||||||
|
if (tileStatuses.includes('warning')) return 'warning';
|
||||||
|
if (tileStatuses.includes('ok')) return 'ok';
|
||||||
|
|
||||||
|
return 'unknown';
|
||||||
|
}, [
|
||||||
|
insightAlert,
|
||||||
|
structuredInsight,
|
||||||
|
endCycleInsight,
|
||||||
|
fcrStatus,
|
||||||
|
eefStatus,
|
||||||
|
mortalityStatus,
|
||||||
|
weightStatus,
|
||||||
|
]);
|
||||||
|
|
||||||
const renderVisualBlock = () => {
|
const renderVisualBlock = () => {
|
||||||
if (!hasInsight || !scopedMetrics || page !== 'dashboard') return null;
|
if (!hasInsight || !scopedMetrics || page !== 'dashboard') return null;
|
||||||
|
|
||||||
@@ -802,10 +830,15 @@ export const AiInsightCard: React.FC<AiInsightCardProps> = ({
|
|||||||
<h2 className="text-lg font-bold text-gray-900 group-hover:text-red-700 transition-colors">
|
<h2 className="text-lg font-bold text-gray-900 group-hover:text-red-700 transition-colors">
|
||||||
AI Insight
|
AI Insight
|
||||||
</h2>
|
</h2>
|
||||||
{isCollapsed && hasInsight && (
|
{hasInsight && (
|
||||||
<span className="text-[11px] font-semibold text-emerald-700 bg-emerald-50 border border-emerald-200 px-2 py-0.5 rounded-full">
|
<InsightStatusBadge
|
||||||
Tersedia
|
status={overallStatus}
|
||||||
</span>
|
customLabel={
|
||||||
|
isCollapsed
|
||||||
|
? `Tersedia (${INSIGHT_STATUS_META[overallStatus].label})`
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-gray-400 truncate">
|
<p className="text-xs text-gray-400 truncate">
|
||||||
@@ -898,125 +931,143 @@ export const AiInsightCard: React.FC<AiInsightCardProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
) : endCycleInsight ? (
|
) : endCycleInsight ? (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="rounded-xl border border-gray-100 bg-white p-5 shadow-sm">
|
<div
|
||||||
<h4 className="text-sm font-bold text-gray-800 mb-2 flex items-center gap-2">
|
className={`rounded-xl border p-5 space-y-4 shadow-xs ${INSIGHT_STATUS_META[overallStatus].containerCls}`}
|
||||||
<i className="fa-solid fa-circle-check text-green-600" /> Kesimpulan
|
>
|
||||||
</h4>
|
<div>
|
||||||
<p className="text-sm text-gray-700 bg-gray-50 p-3 rounded-lg border border-gray-100">
|
<div className="flex items-center justify-between mb-2">
|
||||||
{endCycleInsight.kesimpulan}
|
<h4 className="text-sm font-bold text-gray-800 flex items-center gap-2">
|
||||||
</p>
|
<i className="fa-solid fa-file-lines text-blue-600" /> Kesimpulan
|
||||||
</div>
|
</h4>
|
||||||
|
<InsightStatusBadge status={overallStatus} />
|
||||||
<div className="rounded-xl border border-gray-100 bg-white p-5 shadow-sm">
|
</div>
|
||||||
<h4 className="text-sm font-bold text-gray-800 mb-3 flex items-center gap-2">
|
<p className="text-sm text-gray-700 bg-white/85 p-3.5 rounded-lg border border-gray-200/70 shadow-xs leading-relaxed">
|
||||||
<i className="fa-solid fa-triangle-exclamation text-amber-600" /> Masalah
|
{endCycleInsight.kesimpulan}
|
||||||
</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>
|
</p>
|
||||||
) : (
|
</div>
|
||||||
<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">
|
<div>
|
||||||
<h4 className="text-sm font-bold text-gray-800 mb-3 flex items-center gap-2">
|
<h4 className="text-sm font-bold text-gray-800 mb-2 flex items-center gap-2">
|
||||||
<i className="fa-solid fa-clipboard-check text-blue-600" /> Perbaikan siklus
|
<i className="fa-solid fa-triangle-exclamation text-amber-600" /> Masalah
|
||||||
berikutnya
|
</h4>
|
||||||
</h4>
|
<div className="bg-white/85 p-3.5 rounded-lg border border-gray-200/70 shadow-xs">
|
||||||
{endCycleInsight.perbaikan_siklus_berikutnya.length === 0 ? (
|
{renderBulletList(
|
||||||
<p className="text-sm text-gray-500">Belum ada checklist perbaikan.</p>
|
endCycleInsight.masalah,
|
||||||
) : (
|
'Tidak ada masalah kritis dari graded facts'
|
||||||
<ul className="space-y-3">
|
)}
|
||||||
{endCycleInsight.perbaikan_siklus_berikutnya.map((item, index) => (
|
</div>
|
||||||
<li
|
</div>
|
||||||
key={`${item.id ?? item.aksi}-${index}`}
|
|
||||||
className="rounded-lg border border-blue-100 bg-blue-50/40 p-3"
|
<div>
|
||||||
>
|
<h4 className="text-sm font-bold text-gray-800 mb-2 flex items-center gap-2">
|
||||||
<p className="text-[10px] font-semibold uppercase tracking-wide text-blue-700">
|
<i className="fa-solid fa-magnifying-glass-chart text-red-600" /> Akar
|
||||||
{item.fase}
|
penyebab
|
||||||
</p>
|
</h4>
|
||||||
<p className="mt-1 text-sm text-gray-800">{item.aksi}</p>
|
{endCycleInsight.akar_penyebab.length === 0 ? (
|
||||||
{item.metrik_pantau ? (
|
<p className="text-sm text-gray-500 bg-white/85 p-3.5 rounded-lg border border-gray-200/70 shadow-xs">
|
||||||
<p className="mt-1 text-xs text-gray-500">
|
Belum ada kandidat akar penyebab berbukti.
|
||||||
Pantau: {item.metrik_pantau}
|
</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-200/70 bg-white/85 p-3.5 shadow-xs"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
<h4 className="text-sm font-bold text-gray-800 mb-2 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 bg-white/85 p-3.5 rounded-lg border border-gray-200/70 shadow-xs">
|
||||||
|
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-200/70 bg-white/90 p-3.5 shadow-xs"
|
||||||
|
>
|
||||||
|
<p className="text-[10px] font-semibold uppercase tracking-wide text-blue-700">
|
||||||
|
{item.fase}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
<p className="mt-1 text-sm text-gray-800">{item.aksi}</p>
|
||||||
</li>
|
{item.metrik_pantau ? (
|
||||||
))}
|
<p className="mt-1 text-xs text-gray-500">
|
||||||
</ul>
|
Pantau: {item.metrik_pantau}
|
||||||
)}
|
</p>
|
||||||
{endCycleInsight.insight ? (
|
) : null}
|
||||||
<p className="mt-3 text-sm text-gray-600">{endCycleInsight.insight}</p>
|
</li>
|
||||||
) : null}
|
))}
|
||||||
<CitationsBlock citations={citations} />
|
</ul>
|
||||||
|
)}
|
||||||
|
{endCycleInsight.insight ? (
|
||||||
|
<p className="mt-3 text-sm text-gray-600 bg-white/85 p-3.5 rounded-lg border border-gray-200/70 shadow-xs">
|
||||||
|
{endCycleInsight.insight}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<CitationsBlock citations={citations} />
|
||||||
</div>
|
</div>
|
||||||
) : structuredInsight && summary ? (
|
) : structuredInsight && summary ? (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="rounded-xl border border-gray-100 bg-white p-5 shadow-sm">
|
<div
|
||||||
<h4 className="text-sm font-bold text-gray-800 mb-2 flex items-center gap-2">
|
className={`rounded-xl border p-5 shadow-xs ${INSIGHT_STATUS_META[overallStatus].containerCls}`}
|
||||||
<i className="fa-solid fa-circle-check text-green-600" /> Kesimpulan
|
>
|
||||||
</h4>
|
<div className="flex items-center justify-between mb-2">
|
||||||
<p className="text-sm text-gray-700 bg-gray-50 p-3 rounded-lg border border-gray-100">
|
<h4 className="text-sm font-bold text-gray-800 flex items-center gap-2">
|
||||||
|
<i className="fa-solid fa-file-lines text-blue-600" /> Kesimpulan
|
||||||
|
</h4>
|
||||||
|
<InsightStatusBadge status={overallStatus} />
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-gray-700 bg-white/85 p-3.5 rounded-lg border border-gray-200/70 shadow-xs leading-relaxed">
|
||||||
{summary.headline}
|
{summary.headline}
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<h4 className="text-sm font-bold text-gray-800 mb-2 flex items-center gap-2">
|
<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
|
<i className="fa-solid fa-brain text-red-600" /> AI Insight
|
||||||
</h4>
|
</h4>
|
||||||
<div className="text-sm text-gray-700 space-y-2 leading-relaxed">
|
<div className="text-sm text-gray-700 bg-white/85 p-3.5 rounded-lg border border-gray-200/70 shadow-xs space-y-2 leading-relaxed">
|
||||||
{renderBulletList([
|
{renderBulletList([
|
||||||
...summary.bullets,
|
...summary.bullets,
|
||||||
...summary.risks,
|
...summary.risks,
|
||||||
@@ -1024,8 +1075,8 @@ export const AiInsightCard: React.FC<AiInsightCardProps> = ({
|
|||||||
])}
|
])}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<CitationsBlock citations={citations} />
|
|
||||||
</div>
|
</div>
|
||||||
|
<CitationsBlock citations={citations} />
|
||||||
|
|
||||||
{page === 'dashboard' && detailed && (
|
{page === 'dashboard' && detailed && (
|
||||||
<>
|
<>
|
||||||
@@ -1038,41 +1089,15 @@ export const AiInsightCard: React.FC<AiInsightCardProps> = ({
|
|||||||
{detailsOpen && (
|
{detailsOpen && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{detailed.hitung_ayam &&
|
{detailed.hitung_ayam &&
|
||||||
renderDetailSection(
|
renderDetailSection('Hitung Ayam', detailed.hitung_ayam)}
|
||||||
'Hitung Ayam',
|
|
||||||
detailed.hitung_ayam,
|
|
||||||
sectionBadgeClasses[detailed.hitung_ayam.status]
|
|
||||||
)}
|
|
||||||
{detailed.berat_ayam &&
|
{detailed.berat_ayam &&
|
||||||
renderDetailSection(
|
renderDetailSection('Berat Ayam', detailed.berat_ayam)}
|
||||||
'Berat Ayam',
|
{detailed.fcr && renderDetailSection('FCR', detailed.fcr)}
|
||||||
detailed.berat_ayam,
|
{detailed.eef && renderDetailSection('EEF', detailed.eef)}
|
||||||
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 &&
|
{detailed.panel_iot &&
|
||||||
renderDetailSection(
|
renderDetailSection('Panel IoT', detailed.panel_iot)}
|
||||||
'Panel IoT',
|
|
||||||
detailed.panel_iot,
|
|
||||||
sectionBadgeClasses[detailed.panel_iot.status]
|
|
||||||
)}
|
|
||||||
{detailed.data_quality &&
|
{detailed.data_quality &&
|
||||||
renderDetailSection(
|
renderDetailSection('Kualitas Data', detailed.data_quality)}
|
||||||
'Kualitas Data',
|
|
||||||
detailed.data_quality,
|
|
||||||
sectionBadgeClasses[detailed.data_quality.status]
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
@@ -1080,44 +1105,57 @@ export const AiInsightCard: React.FC<AiInsightCardProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
) : dailyInsight ? (
|
) : dailyInsight ? (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="rounded-xl border border-gray-100 bg-white p-5 shadow-sm">
|
<div
|
||||||
<h4 className="text-sm font-bold text-gray-800 mb-2 flex items-center gap-2">
|
className={`rounded-xl border p-5 space-y-4 shadow-xs ${INSIGHT_STATUS_META[overallStatus].containerCls}`}
|
||||||
<i className="fa-solid fa-circle-check text-green-600" /> Kesimpulan
|
>
|
||||||
</h4>
|
<div>
|
||||||
<p className="text-sm text-gray-700 bg-gray-50 p-3 rounded-lg border border-gray-100">
|
<div className="flex items-center justify-between mb-2">
|
||||||
{dailyInsight.summary}
|
<h4 className="text-sm font-bold text-gray-800 flex items-center gap-2">
|
||||||
</p>
|
<i className="fa-solid fa-file-lines text-blue-600" /> Kesimpulan
|
||||||
</div>
|
</h4>
|
||||||
|
<InsightStatusBadge status={overallStatus} />
|
||||||
{dailyInsight.akar_masalah && (
|
</div>
|
||||||
<div className="rounded-xl border border-amber-100/80 bg-amber-50/40 p-5 shadow-sm">
|
<p className="text-sm text-gray-700 bg-white/85 p-3.5 rounded-lg border border-gray-200/70 shadow-xs leading-relaxed">
|
||||||
<h4 className="text-sm font-bold text-gray-800 mb-2 flex items-center gap-2">
|
{dailyInsight.summary}
|
||||||
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="rounded-xl border border-gray-100 bg-white p-5 shadow-sm">
|
{dailyInsight.akar_masalah && (
|
||||||
<h4 className="text-sm font-bold text-gray-800 mb-2 flex items-center gap-2">
|
<div>
|
||||||
<i className="fa-solid fa-brain text-red-600" /> AI Insight & Rekomendasi
|
<h4 className="text-sm font-bold text-gray-800 mb-2 flex items-center gap-2">
|
||||||
</h4>
|
<i className="fa-solid fa-magnifying-glass-chart text-amber-600" /> Akar
|
||||||
<p className="text-sm text-gray-700 bg-gray-50 p-3 rounded-lg border border-gray-100 whitespace-pre-wrap">
|
Masalah
|
||||||
{dailyInsight.insight}
|
</h4>
|
||||||
</p>
|
<p className="text-sm text-gray-700 bg-white/85 p-3.5 rounded-lg border border-amber-200/70 shadow-xs leading-relaxed">
|
||||||
<CitationsBlock citations={citations} />
|
{dailyInsight.akar_masalah}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<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-white/85 p-3.5 rounded-lg border border-gray-200/70 shadow-xs whitespace-pre-wrap leading-relaxed">
|
||||||
|
{dailyInsight.insight}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<CitationsBlock citations={citations} />
|
||||||
</div>
|
</div>
|
||||||
) : insightText ? (
|
) : insightText ? (
|
||||||
<div className="text-sm text-gray-800 leading-relaxed whitespace-pre-wrap">
|
<div className="space-y-4">
|
||||||
{insightText.includes(END_CYCLE_JSON_MARKER)
|
<div
|
||||||
? insightText.split(END_CYCLE_JSON_MARKER)[0]?.trim()
|
className={`rounded-xl border p-5 shadow-xs ${INSIGHT_STATUS_META[overallStatus].containerCls}`}
|
||||||
: insightText.includes('<!--daily_json-->')
|
>
|
||||||
? insightText.split('<!--daily_json-->')[0]?.trim()
|
<div className="text-sm text-gray-800 leading-relaxed whitespace-pre-wrap bg-white/85 p-3.5 rounded-lg border border-gray-200/70 shadow-xs">
|
||||||
: insightText}
|
{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}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<CitationsBlock citations={citations} />
|
<CitationsBlock citations={citations} />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -1139,7 +1177,15 @@ export const AiInsightCard: React.FC<AiInsightCardProps> = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-6 pt-4 border-t border-gray-100 flex flex-col items-center justify-center gap-1.5">
|
<div className="mt-6 pt-4 border-t border-gray-100 flex items-center justify-between">
|
||||||
|
<div className="text-xs text-gray-400">
|
||||||
|
{hasInsight && (
|
||||||
|
<span className="inline-flex items-center gap-1.5">
|
||||||
|
<span>Status AI Insight:</span>
|
||||||
|
<InsightStatusBadge status={overallStatus} size="xs" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -1152,11 +1198,9 @@ export const AiInsightCard: React.FC<AiInsightCardProps> = ({
|
|||||||
>
|
>
|
||||||
<i className="fa-solid fa-chevron-up text-xs transition-transform group-hover:-translate-y-0.5" />
|
<i className="fa-solid fa-chevron-up text-xs transition-transform group-hover:-translate-y-0.5" />
|
||||||
</button>
|
</button>
|
||||||
{lastUpdated && (
|
<div className="text-right text-[11px] text-gray-400 min-w-[80px]">
|
||||||
<p className="text-[11px] text-gray-400">
|
{lastUpdated && <span>Terakhir: {formatLastUpdated()}</span>}
|
||||||
Terakhir diperbarui: {formatLastUpdated()}
|
</div>
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -7,11 +7,85 @@ import React from 'react';
|
|||||||
|
|
||||||
export type InsightStatus = 'ok' | 'warning' | 'critical' | 'unknown';
|
export type InsightStatus = 'ok' | 'warning' | 'critical' | 'unknown';
|
||||||
|
|
||||||
const STATUS_CHIP: Record<InsightStatus, { cls: string; glyph: string }> = {
|
export const INSIGHT_STATUS_META: Record<
|
||||||
ok: { cls: 'bg-green-50 text-green-800 border-green-200', glyph: '✓' },
|
InsightStatus,
|
||||||
warning: { cls: 'bg-amber-50 text-amber-800 border-amber-200', glyph: '!' },
|
{ cls: string; glyph: string; label: string; bg: string; text: string; containerCls: string }
|
||||||
critical: { cls: 'bg-red-50 text-red-800 border-red-200', glyph: '⚠' },
|
> = {
|
||||||
unknown: { cls: 'bg-gray-50 text-gray-500 border-gray-200', glyph: '–' },
|
ok: {
|
||||||
|
cls: 'bg-green-50 text-green-800 border-green-200',
|
||||||
|
glyph: '✓',
|
||||||
|
label: 'Normal',
|
||||||
|
bg: 'bg-green-50',
|
||||||
|
text: 'text-green-800',
|
||||||
|
containerCls: 'bg-green-50/40 border-green-200',
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
cls: 'bg-amber-50 text-amber-800 border-amber-200',
|
||||||
|
glyph: '!',
|
||||||
|
label: 'Perhatian',
|
||||||
|
bg: 'bg-amber-50',
|
||||||
|
text: 'text-amber-800',
|
||||||
|
containerCls: 'bg-amber-50/40 border-amber-200',
|
||||||
|
},
|
||||||
|
critical: {
|
||||||
|
cls: 'bg-red-50 text-red-800 border-red-200',
|
||||||
|
glyph: '⚠',
|
||||||
|
label: 'Kritis',
|
||||||
|
bg: 'bg-red-50',
|
||||||
|
text: 'text-red-800',
|
||||||
|
containerCls: 'bg-red-50/40 border-red-200',
|
||||||
|
},
|
||||||
|
unknown: {
|
||||||
|
cls: 'bg-gray-50 text-gray-600 border-gray-200',
|
||||||
|
glyph: '–',
|
||||||
|
label: 'Belum Dinilai',
|
||||||
|
bg: 'bg-gray-50',
|
||||||
|
text: 'text-gray-600',
|
||||||
|
containerCls: 'bg-gray-50/60 border-gray-200',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const normalizeInsightStatus = (status: unknown): InsightStatus => {
|
||||||
|
if (!status || typeof status !== 'string') return 'unknown';
|
||||||
|
const lower = status.toLowerCase();
|
||||||
|
if (lower === 'ok' || lower === 'healthy' || lower === 'normal' || lower === 'aman') return 'ok';
|
||||||
|
if (lower === 'warning' || lower === 'perhatian' || lower === 'waspada') return 'warning';
|
||||||
|
if (lower === 'critical' || lower === 'kritis' || lower === 'bahaya') return 'critical';
|
||||||
|
return 'unknown';
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface InsightStatusBadgeProps {
|
||||||
|
status: InsightStatus | string;
|
||||||
|
customLabel?: string;
|
||||||
|
className?: string;
|
||||||
|
size?: 'xs' | 'sm' | 'md';
|
||||||
|
}
|
||||||
|
|
||||||
|
export const InsightStatusBadge: React.FC<InsightStatusBadgeProps> = ({
|
||||||
|
status,
|
||||||
|
customLabel,
|
||||||
|
className = '',
|
||||||
|
size = 'sm',
|
||||||
|
}) => {
|
||||||
|
const norm = normalizeInsightStatus(status);
|
||||||
|
const meta = INSIGHT_STATUS_META[norm];
|
||||||
|
const sizeClasses =
|
||||||
|
size === 'xs'
|
||||||
|
? 'px-2 py-0.5 text-[10px]'
|
||||||
|
: size === 'md'
|
||||||
|
? 'px-3 py-1 text-xs'
|
||||||
|
: 'px-2.5 py-0.5 text-[11px]';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={`inline-flex items-center gap-1.5 rounded-full border font-semibold tracking-wide ${meta.cls} ${sizeClasses} ${className}`}
|
||||||
|
>
|
||||||
|
<span aria-hidden="true" className="font-bold">
|
||||||
|
{meta.glyph}
|
||||||
|
</span>
|
||||||
|
<span>{customLabel || meta.label}</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
interface DeltaChipProps {
|
interface DeltaChipProps {
|
||||||
@@ -20,7 +94,7 @@ interface DeltaChipProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const DeltaChip: React.FC<DeltaChipProps> = ({ status, label }) => {
|
export const DeltaChip: React.FC<DeltaChipProps> = ({ status, label }) => {
|
||||||
const chip = STATUS_CHIP[status];
|
const chip = INSIGHT_STATUS_META[status] ?? INSIGHT_STATUS_META.unknown;
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={`inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-[11px] font-semibold ${chip.cls}`}
|
className={`inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-[11px] font-semibold ${chip.cls}`}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
* the DB row is the source of truth.
|
* the DB row is the source of truth.
|
||||||
* Distinct from `AiInsightCard` (Dashboard report modes).
|
* Distinct from `AiInsightCard` (Dashboard report modes).
|
||||||
*/
|
*/
|
||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef, useMemo } from 'react';
|
||||||
import { useFarm } from '../../context/FarmContext.tsx';
|
import { useFarm } from '../../context/FarmContext.tsx';
|
||||||
import { api, errorMessage } from '../../services/apiClient.ts';
|
import { api, errorMessage } from '../../services/apiClient.ts';
|
||||||
import type {
|
import type {
|
||||||
@@ -29,7 +29,13 @@ import {
|
|||||||
} from '../../utils/insightScope.ts';
|
} from '../../utils/insightScope.ts';
|
||||||
import InsightTrendChart, { type InsightSeries } from './InsightTrendChart.tsx';
|
import InsightTrendChart, { type InsightSeries } from './InsightTrendChart.tsx';
|
||||||
import InsightConditionChart, { type ConditionBar } from './InsightConditionChart.tsx';
|
import InsightConditionChart, { type ConditionBar } from './InsightConditionChart.tsx';
|
||||||
import { HeroFigure, type InsightStatus } from './InsightStatTiles.tsx';
|
import {
|
||||||
|
HeroFigure,
|
||||||
|
InsightStatusBadge,
|
||||||
|
INSIGHT_STATUS_META,
|
||||||
|
normalizeInsightStatus,
|
||||||
|
type InsightStatus,
|
||||||
|
} from './InsightStatTiles.tsx';
|
||||||
import InsightProgressSteps from './InsightProgressSteps.tsx';
|
import InsightProgressSteps from './InsightProgressSteps.tsx';
|
||||||
|
|
||||||
const CLIENT_TIMEOUT_MS = 1_200_000; // 20 minutes — NUC LLM can take several minutes
|
const CLIENT_TIMEOUT_MS = 1_200_000; // 20 minutes — NUC LLM can take several minutes
|
||||||
@@ -57,6 +63,8 @@ async function pollInsightJob(
|
|||||||
|
|
||||||
type PageInsightResult = AiResult & {
|
type PageInsightResult = AiResult & {
|
||||||
citations?: InsightCitation[];
|
citations?: InsightCitation[];
|
||||||
|
alert?: string;
|
||||||
|
status?: InsightStatus;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Dedupes concurrent soft DB loads for the same scope (not a content cache). */
|
/** Dedupes concurrent soft DB loads for the same scope (not a content cache). */
|
||||||
@@ -426,13 +434,29 @@ export const PageAiInsight: React.FC<PageAiInsightProps> = ({
|
|||||||
manual === null ? null : `Manual dituang: ${manual.toLocaleString('id-ID')} karung`,
|
manual === null ? null : `Manual dituang: ${manual.toLocaleString('id-ID')} karung`,
|
||||||
perDay === null ? null : `${perDay.toLocaleString('id-ID')} karung/hari`,
|
perDay === null ? null : `${perDay.toLocaleString('id-ID')} karung/hari`,
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
|
|
||||||
|
const saldoKarung = masukTotal != null && actual != null ? masukTotal - actual : null;
|
||||||
|
let karungStatus: InsightStatus = 'ok';
|
||||||
|
if (saldoKarung !== null && saldoKarung < 0) {
|
||||||
|
karungStatus = 'critical';
|
||||||
|
} else if (manual !== null && manual > 0 && Math.abs(actual - manual) / manual > 0.1) {
|
||||||
|
karungStatus = 'warning';
|
||||||
|
} else {
|
||||||
|
karungStatus = 'ok';
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
label: 'Pakan Dituang (IOT)',
|
label: 'Pakan Dituang (IOT)',
|
||||||
value: actual.toLocaleString('id-ID'),
|
value: actual.toLocaleString('id-ID'),
|
||||||
unit: 'karung',
|
unit: 'karung',
|
||||||
context: parts.length > 0 ? parts.join(' · ') : null,
|
context: parts.length > 0 ? parts.join(' · ') : null,
|
||||||
status: 'unknown' as InsightStatus,
|
status: karungStatus,
|
||||||
deltaLabel: null,
|
deltaLabel:
|
||||||
|
saldoKarung !== null
|
||||||
|
? `Saldo: ${saldoKarung.toLocaleString('id-ID')} karung`
|
||||||
|
: manual !== null && manual > 0
|
||||||
|
? `${Math.round(((actual - manual) / manual) * 100)}% vs manual`
|
||||||
|
: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -620,11 +644,14 @@ export const PageAiInsight: React.FC<PageAiInsightProps> = ({
|
|||||||
throw new Error('Respons AI tidak dapat diproses.');
|
throw new Error('Respons AI tidak dapat diproses.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const alertRaw = (apiResult as Record<string, unknown> | null)?.alert as string | undefined;
|
||||||
const wrapped: PageInsightResult = {
|
const wrapped: PageInsightResult = {
|
||||||
summary: parsed.summary,
|
summary: parsed.summary,
|
||||||
akar_masalah: parsed.akar_masalah,
|
akar_masalah: parsed.akar_masalah,
|
||||||
insight: parsed.insight,
|
insight: parsed.insight,
|
||||||
citations: apiResult.citations,
|
citations: apiResult.citations,
|
||||||
|
alert: alertRaw,
|
||||||
|
status: normalizeInsightStatus(alertRaw),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!isCurrent()) return wrapped;
|
if (!isCurrent()) return wrapped;
|
||||||
@@ -662,6 +689,16 @@ export const PageAiInsight: React.FC<PageAiInsightProps> = ({
|
|||||||
generateRef.current = generate;
|
generateRef.current = generate;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const effectiveStatus: InsightStatus = useMemo(() => {
|
||||||
|
if (result?.status && result.status !== 'unknown') return result.status;
|
||||||
|
if (result?.alert) {
|
||||||
|
const norm = normalizeInsightStatus(result.alert);
|
||||||
|
if (norm !== 'unknown') return norm;
|
||||||
|
}
|
||||||
|
if (heroSpec?.status && heroSpec.status !== 'unknown') return heroSpec.status;
|
||||||
|
return 'unknown';
|
||||||
|
}, [result, heroSpec]);
|
||||||
|
|
||||||
// Day / scope change: clear UI and soft-load from DB (no generate).
|
// Day / scope change: clear UI and soft-load from DB (no generate).
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (lastCacheKeyRef.current === scopeKey) return;
|
if (lastCacheKeyRef.current === scopeKey) return;
|
||||||
@@ -693,10 +730,15 @@ export const PageAiInsight: React.FC<PageAiInsightProps> = ({
|
|||||||
<span className="text-sm font-bold text-gray-900 group-hover:text-red-700 transition-colors">
|
<span className="text-sm font-bold text-gray-900 group-hover:text-red-700 transition-colors">
|
||||||
{meta.title}
|
{meta.title}
|
||||||
</span>
|
</span>
|
||||||
{isCollapsed && result && (
|
{result && (
|
||||||
<span className="text-[10px] font-semibold text-emerald-700 bg-emerald-50 border border-emerald-200 px-2 py-0.5 rounded-full">
|
<InsightStatusBadge
|
||||||
Tersedia
|
status={effectiveStatus}
|
||||||
</span>
|
customLabel={
|
||||||
|
isCollapsed
|
||||||
|
? `Tersedia (${INSIGHT_STATUS_META[effectiveStatus].label})`
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-gray-400 truncate">
|
<p className="text-xs text-gray-400 truncate">
|
||||||
@@ -1073,34 +1115,41 @@ export const PageAiInsight: React.FC<PageAiInsightProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="mt-6 pt-4 border-t border-gray-100">
|
<div
|
||||||
<h4 className="text-sm font-bold text-gray-800 mb-2 flex items-center gap-2">
|
className={`mt-6 rounded-xl border p-4 sm:p-5 space-y-4 shadow-xs ${INSIGHT_STATUS_META[effectiveStatus].containerCls}`}
|
||||||
<i className="fa-solid fa-circle-check text-green-600" /> Kesimpulan
|
>
|
||||||
</h4>
|
<div>
|
||||||
<p className="text-sm text-gray-700 bg-gray-50 p-3 rounded-lg border border-gray-100">
|
<div className="flex items-center justify-between mb-2">
|
||||||
{result.summary}
|
<h4 className="text-sm font-bold text-gray-800 flex items-center gap-2">
|
||||||
</p>
|
<i className="fa-solid fa-file-lines text-blue-600" /> Kesimpulan
|
||||||
</div>
|
</h4>
|
||||||
|
<InsightStatusBadge status={effectiveStatus} />
|
||||||
{result.akar_masalah && (
|
</div>
|
||||||
<div className="mt-4">
|
<p className="text-sm text-gray-700 bg-white/85 p-3.5 rounded-lg border border-gray-200/70 shadow-xs leading-relaxed">
|
||||||
<h4 className="text-sm font-bold text-gray-800 mb-2 flex items-center gap-2">
|
{result.summary}
|
||||||
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="mt-4">
|
{result.akar_masalah && (
|
||||||
<h4 className="text-sm font-bold text-gray-800 mb-2 flex items-center gap-2">
|
<div>
|
||||||
<i className="fa-solid fa-brain text-red-600" /> AI Insight & Rekomendasi
|
<h4 className="text-sm font-bold text-gray-800 mb-2 flex items-center gap-2">
|
||||||
</h4>
|
<i className="fa-solid fa-magnifying-glass-chart text-amber-600" /> Akar
|
||||||
<p className="text-sm text-gray-700 bg-gray-50 p-3 rounded-lg border border-gray-100 whitespace-pre-wrap">
|
Masalah
|
||||||
{result.insight}
|
</h4>
|
||||||
</p>
|
<p className="text-sm text-gray-700 bg-white/85 p-3.5 rounded-lg border border-amber-200/70 shadow-xs leading-relaxed">
|
||||||
|
{result.akar_masalah}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<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-white/85 p-3.5 rounded-lg border border-gray-200/70 shadow-xs whitespace-pre-wrap leading-relaxed">
|
||||||
|
{result.insight}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CitationsBlock citations={result.citations} />
|
<CitationsBlock citations={result.citations} />
|
||||||
@@ -1108,7 +1157,15 @@ export const PageAiInsight: React.FC<PageAiInsightProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="mt-5 pt-3 border-t border-gray-100 flex justify-center">
|
<div className="mt-5 pt-3 border-t border-gray-100 flex items-center justify-between">
|
||||||
|
<div className="text-xs text-gray-400">
|
||||||
|
{result && (
|
||||||
|
<span className="inline-flex items-center gap-1.5">
|
||||||
|
<span>Status AI Insight:</span>
|
||||||
|
<InsightStatusBadge status={effectiveStatus} size="xs" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -1121,6 +1178,7 @@ export const PageAiInsight: React.FC<PageAiInsightProps> = ({
|
|||||||
>
|
>
|
||||||
<i className="fa-solid fa-chevron-up text-xs transition-transform group-hover:-translate-y-0.5" />
|
<i className="fa-solid fa-chevron-up text-xs transition-transform group-hover:-translate-y-0.5" />
|
||||||
</button>
|
</button>
|
||||||
|
<div className="w-12" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in new issue
Block a user