update applying current ai insight from dashboard-cpsp

This commit is contained in:
Alberto-Audrix committed 2026-10-05 14:25:22 +07:00
1 parent 31272035e9
commit b87f20e3b3
27 files changed
+5300 -343

No files matched your search

+83 -56
View File
@@ -9,13 +9,20 @@ import {
YAxis,
} from 'recharts';
import { api, errorMessage } from '../services/apiClient.ts';
import type { AIInsight, Flock, PanelIoT } from '../types/api.ts';
import type { Flock, PanelIoT } from '../types/api.ts';
import { useFarm } from '../context/FarmContext.tsx';
import { Banner, EmptyFarm, PageHeader, Spinner } from './ui/Feedback.tsx';
import { cycleDayForDate, formatDateId, formatDateRange, formatNumber } from '../utils/format.ts';
import {
getTempStandardByDay,
getWaterStandardByDay,
HUMIDITY_RANGE_PCT,
} from '../utils/cp707EnvStandards.ts';
import { buildIotInsightContext } from '../utils/insightContextBuilders.ts';
import { computeExperienceCalc } from './iotPanel/chillFactor.ts';
import IotPanelChillFactorCard from './iotPanel/IotPanelChillFactorCard.tsx';
import IotPanelExperienceTempCard from './iotPanel/IotPanelExperienceTempCard.tsx';
import { PageAiInsight } from './shared/PageAiInsight.tsx';
const cardClass = 'rounded-xl border border-slate-200 bg-white p-4 shadow-sm';
@@ -57,7 +64,6 @@ const IotPanelPage: React.FC<{ onSettings: () => void }> = ({ onSettings }) => {
const [dates, setDates] = useState<string[]>([]);
const [selectedDate, setSelectedDate] = useState('');
const [dayRows, setDayRows] = useState<PanelIoT[]>([]);
const [insights, setInsights] = useState<AIInsight[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [selectedChartKey, setSelectedChartKey] = useState<'humidity' | 'wind' | 'water' | null>(
@@ -80,7 +86,6 @@ const IotPanelPage: React.FC<{ onSettings: () => void }> = ({ onSettings }) => {
setDates([]);
setSelectedDate('');
setDayRows([]);
setInsights([]);
return;
}
let cancelled = false;
@@ -107,7 +112,6 @@ const IotPanelPage: React.FC<{ onSettings: () => void }> = ({ onSettings }) => {
setDates([]);
setSelectedDate('');
setDayRows([]);
setInsights([]);
return;
}
let cancelled = false;
@@ -115,14 +119,13 @@ const IotPanelPage: React.FC<{ onSettings: () => void }> = ({ onSettings }) => {
setLoading(true);
setError(null);
try {
const [dateList, insightRows] = await Promise.all([
api.panelIot.dates({ cycle_id: selectedCycle.id, flock_id: selectedFlockId }),
api.insights.list({ cycle_id: selectedCycle.id }),
]);
const dateList = await api.panelIot.dates({
cycle_id: selectedCycle.id,
flock_id: selectedFlockId,
});
if (cancelled) return;
const sortedDates = [...dateList].sort();
setDates(sortedDates);
setInsights(insightRows);
setSelectedDate((previous) =>
previous && sortedDates.includes(previous)
? previous
@@ -222,21 +225,26 @@ const IotPanelPage: React.FC<{ onSettings: () => void }> = ({ onSettings }) => {
[summary, dayAge]
);
const iotInsights = useMemo(() => {
const matched = insights.filter((item) => {
const text =
`${item.section ?? ''} ${item.alert ?? ''} ${item.insight_text ?? ''}`.toLowerCase();
return (
text.includes('iot') ||
text.includes('suhu') ||
text.includes('kelembapan') ||
text.includes('humidity') ||
text.includes('temperature') ||
text.includes('lingkungan')
);
const insightContext = useMemo(() => {
if (!selectedKandang) return null;
return buildIotInsightContext({
kandangId: selectedKandang.id,
kandangName: selectedKandang.kandang_name,
cycleId: selectedCycle?.id,
hari_ke: dayAge,
totalDays: selectedCycle?.total_days ?? null,
suhu_rata_rata_C: summary?.temp ?? null,
experience_suhu_C: summary?.experienceTemp ?? null,
kelembapan_persen: summary?.humidity ?? null,
kecepatan_angin_mPerDetik: summary?.windSpeed ?? null,
konsumsi_air_L: summary?.waterTotal ?? null,
setpoint_C: getTempStandardByDay(dayAge),
suhu_min_C: summary?.minTemp ?? null,
suhu_max_C: summary?.maxTemp ?? null,
experience_suhu_min_C: summary?.minExp ?? null,
experience_suhu_max_C: summary?.maxExp ?? null,
});
return (matched.length > 0 ? matched : insights).slice(0, 4);
}, [insights]);
}, [selectedKandang, selectedCycle, dayAge, summary]);
if (!selectedCycle) return <EmptyFarm onSettings={onSettings} />;
@@ -266,43 +274,62 @@ const IotPanelPage: React.FC<{ onSettings: () => void }> = ({ onSettings }) => {
/>
{error && <Banner kind="error">{error}</Banner>}
{selectedKandang && insightContext ? (
<PageAiInsight
topic="iot_panel"
cacheKey={`iot-k${selectedKandang.id}-c${selectedCycle.id}-d${selectedDate}-t${summary?.temp ?? 'na'}`}
contextData={insightContext}
chartSpec={{
type: 'condition',
title: 'Kondisi kandang vs standar CP 707',
bars: [
{
label: 'Suhu rata-rata',
actual: summary?.temp ?? null,
standard: getTempStandardByDay(dayAge),
unit: '°C',
note:
getTempStandardByDay(dayAge) === null
? 'Umur tidak diketahui, standar suhu tidak dapat ditentukan.'
: undefined,
},
{
label: 'Kelembapan',
actual: summary?.humidity ?? null,
standard: HUMIDITY_RANGE_PCT.max,
unit: '%',
note: `Rentang ideal buku ${HUMIDITY_RANGE_PCT.min}–${HUMIDITY_RANGE_PCT.max}%; batang biru menandai batas atas.`,
},
{
label: 'Experience Suhu',
actual: summary?.experienceTemp ?? null,
standard: getTempStandardByDay(dayAge),
unit: '°C',
},
{
label: 'Konsumsi Air',
actual: summary?.waterTotal ?? null,
standard: getWaterStandardByDay(dayAge)?.max ?? null,
unit: 'L',
},
],
}}
/>
) : (
<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 Panel IoT
</h2>
<Banner>Pilih kandang untuk melihat AI Insight.</Banner>
</section>
)}
{loading && dayRows.length === 0 ? (
<Spinner />
) : summary ? (
<>
<section>
<h2 className="mb-3 text-lg font-bold text-gray-800">
<i className="fa-solid fa-brain mr-2 text-red-600" />
AI Insight Panel IoT
</h2>
{iotInsights.length === 0 ? (
<Banner>Belum ada insight untuk siklus ini.</Banner>
) : (
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
{iotInsights.map((item) => (
<article
key={item.id}
className="rounded-xl border border-gray-100 bg-white p-5 shadow-sm"
>
<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="mt-1 font-semibold text-gray-800">
{item.alert || 'Insight'}
</h3>
</div>
<span className="text-xs text-gray-400">{item.session}</span>
</div>
<p className="mt-3 whitespace-pre-wrap text-sm text-gray-600">
{item.insight_text}
</p>
</article>
))}
</div>
)}
</section>
<div className="flex flex-wrap items-center justify-between gap-2 rounded-xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm text-slate-600">
<span className="inline-flex items-center gap-2">