update ai insight migration for all page

This commit is contained in:
Alberto-Audrix committed 2026-09-17 10:11:14 +07:00
1 parent 07d36081ce
commit 18790ab8f6
69 files changed
+9457 -399

No files matched your search

+61 -54
View File
@@ -9,10 +9,12 @@ import {
YAxis,
} from 'recharts';
import { api, errorMessage } from '../services/apiClient.ts';
import type { AIInsight, KPI } from '../types/api.ts';
import type { KPI } from '../types/api.ts';
import { useFarm } from '../context/FarmContext.tsx';
import { Banner, EmptyFarm, PageHeader, Spinner } from './ui/Feedback.tsx';
import { formatNumber, formatRatio } from '../utils/format.ts';
import { buildEefInsightContext } from '../utils/insightContextBuilders.ts';
import { PageAiInsight } from './shared/PageAiInsight.tsx';
type EefRow = {
day: number;
@@ -164,9 +166,8 @@ const SupportingMetricChart: React.FC<{
);
const EefDashboardPage: React.FC<{ onSettings: () => void }> = ({ onSettings }) => {
const { selectedCycle } = useFarm();
const { selectedCycle, selectedKandang } = useFarm();
const [rows, setRows] = useState<KPI[]>([]);
const [insights, setInsights] = useState<AIInsight[]>([]);
const [loading, setLoading] = useState(false);
const [refreshing, setRefreshing] = useState(false);
const [error, setError] = useState<string | null>(null);
@@ -176,12 +177,8 @@ const EefDashboardPage: React.FC<{ onSettings: () => void }> = ({ onSettings })
if (!selectedCycle) return;
setError(null);
try {
const [kpiRows, insightRows] = await Promise.all([
api.kpis.list({ cycle_id: selectedCycle.id }),
api.insights.list({ cycle_id: selectedCycle.id }),
]);
const kpiRows = await api.kpis.list({ cycle_id: selectedCycle.id });
setRows(kpiRows);
setInsights(insightRows);
setLastUpdated(new Date());
} catch (err) {
setError(errorMessage(err));
@@ -191,7 +188,6 @@ const EefDashboardPage: React.FC<{ onSettings: () => void }> = ({ onSettings })
useEffect(() => {
if (!selectedCycle) {
setRows([]);
setInsights([]);
setLastUpdated(null);
return;
}
@@ -262,21 +258,16 @@ const EefDashboardPage: React.FC<{ onSettings: () => void }> = ({ onSettings })
const latestEef = getLatestValid('eef');
const eefInsights = useMemo(() => {
const matched = insights.filter((item) => {
const text =
`${item.section ?? ''} ${item.alert ?? ''} ${item.insight_text ?? ''}`.toLowerCase();
return (
text.includes('eef') ||
text.includes('fcr') ||
text.includes('hidup') ||
text.includes('live') ||
text.includes('panen') ||
text.includes('bw')
);
const insightContext = useMemo(() => {
if (!selectedKandang) return null;
return buildEefInsightContext({
kandangId: selectedKandang.id,
kandangName: selectedKandang.kandang_name,
cycleId: selectedCycle?.id,
totalDays: selectedCycle?.total_days ?? null,
eefRows,
});
return (matched.length > 0 ? matched : insights).slice(0, 4);
}, [insights]);
}, [selectedKandang, selectedCycle, eefRows]);
if (!selectedCycle) return <EmptyFarm onSettings={onSettings} />;
@@ -304,37 +295,53 @@ const EefDashboardPage: React.FC<{ onSettings: () => void }> = ({ onSettings })
</div>
)}
<section>
<h2 className="text-lg font-bold text-gray-800 mb-3">
<i className="fa-solid fa-brain text-red-600 mr-2" />
AI Insight EEF
</h2>
{loading ? (
<Spinner />
) : eefInsights.length === 0 ? (
<Banner>Belum ada insight untuk siklus ini.</Banner>
) : (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{eefInsights.map((item) => (
<article
key={item.id}
className="bg-white rounded-xl shadow-sm p-5 border border-gray-100"
>
<div className="flex items-start justify-between gap-3">
<div>
<p className="text-xs uppercase tracking-wide text-gray-400">{item.section}</p>
<h3 className="font-semibold text-gray-800 mt-1">{item.alert || 'Insight'}</h3>
</div>
<span className="text-xs text-gray-400">{item.session}</span>
</div>
<p className="text-sm text-gray-600 mt-3 whitespace-pre-wrap">
{item.insight_text}
</p>
</article>
))}
</div>
)}
</section>
{selectedKandang && insightContext ? (
<PageAiInsight
topic="eef"
cacheKey={`eef-k${selectedKandang.id}-c${selectedCycle.id}-rows${eefRows.length}-eef${String(latestEef ?? 'null').replace('.', '_')}`}
contextData={insightContext}
chartSpec={{
type: 'trend',
historyKey: 'tren_eef_harian',
title: 'Tren EEF',
series: [{ field: 'eef', label: 'EEF', color: '#dc2626' }],
}}
scalarScope={{
historyKey: 'tren_eef_harian',
derive: {
// Match EEF page StatCards: latest valid value, never a window mean.
eefTerakhir_indeksTanpaSatuan: {
field: 'eef',
agg: 'lastValid',
replaces: 'eef_terakhir',
},
fcrTerakhir_rasio: {
field: 'fcr',
agg: 'lastValid',
replaces: 'fcr_terakhir',
},
persenHidupTerakhir_persen: {
field: 'persen_hidup',
agg: 'lastValid',
replaces: 'persen_hidup',
},
bobotPanenTerakhir_gram: {
field: 'bw_panen',
agg: 'lastValid',
replaces: 'bw_panen_gram',
},
},
}}
/>
) : (
<section>
<h2 className="text-lg font-bold text-gray-800 mb-3">
<i className="fa-solid fa-brain text-red-600 mr-2" />
AI Insight EEF
</h2>
<Banner>Pilih kandang untuk melihat dan generate AI Insight.</Banner>
</section>
)}
{loading ? (
<Spinner />