import { useState, useMemo } from "react"; import { BarChart, Bar, XAxis, YAxis, Tooltip, Legend, ResponsiveContainer, CartesianGrid, RadarChart, Radar, PolarGrid, PolarAngleAxis, PolarRadiusAxis, Cell, ReferenceLine, } from "recharts"; import { Zap, Target, TrendingUp, TrendingDown, Thermometer, Activity, Trophy, Award, Medal, BarChart2, GitCompare, RefreshCcw, ChevronDown, ChevronUp, Info, AlertTriangle, CheckCircle2, Sliders, } from "lucide-react"; import { fetchRecommendations, RecommendationResponse } from "../api"; const CHART_COLORS = [ "#22c55e", "#3b82f6", "#f59e0b", "#ef4444", "#8b5cf6", "#06b6d4", "#ec4899", "#84cc16", "#f97316", "#6366f1", ]; const TOOLTIP_STYLE = { backgroundColor: "#111827", border: "1px solid #374151", borderRadius: "8px", fontSize: 12 }; function RankIcon({ rank }: { rank: number }) { if (rank === 1) return ; if (rank === 2) return ; if (rank === 3) return ; return #{rank}; } function SliderInput({ label, value, min, max, step, unit, onChange }: { label: string; value: number; min: number; max: number; step: number; unit: string; onChange: (v: number) => void; }) { return (
{value}{unit}
onChange(+e.target.value)} className="w-full accent-green-500 h-1.5" />
{min}{unit}{max}{unit}
); } export default function RecommendationPanel() { const [batteryId, setBatteryId] = useState("B0005"); const [currentCycle, setCurrentCycle] = useState(100); const [currentSoh, setCurrentSoh] = useState(85); const [ambientTemp, setAmbientTemp] = useState(24); const [topK, setTopK] = useState(5); const [result, setResult] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [expandedRow, setExpandedRow] = useState(null); const [chartTab, setChartTab] = useState<"rul" | "params" | "radar">("rul"); const handleSubmit = async () => { setLoading(true); setError(null); try { const res = await fetchRecommendations({ battery_id: batteryId, current_cycle: currentCycle, current_soh: currentSoh, ambient_temperature: ambientTemp, top_k: topK, }); setResult(res); } catch (e: any) { setError(e.response?.data?.detail || e.message); } finally { setLoading(false); } }; // Derived chart data const rulBarData = useMemo(() => { if (!result?.recommendations) return []; return result.recommendations.map((r) => ({ rank: `#${r.rank}`, RUL: Math.round(r.predicted_rul), Improvement: Math.round(r.rul_improvement), fill: CHART_COLORS[(r.rank - 1) % CHART_COLORS.length], })); }, [result]); const paramData = useMemo(() => { if (!result?.recommendations) return []; return result.recommendations.map((r) => ({ rank: `#${r.rank}`, "Temp (°C)": r.ambient_temperature, "Current (A)": r.discharge_current, "Cutoff (V)": r.cutoff_voltage * 10, })); }, [result]); const radarData = useMemo(() => { if (!result?.recommendations || result.recommendations.length < 2) return []; const top3 = result.recommendations.slice(0, 3); const maxRul = Math.max(...top3.map((r) => r.predicted_rul)); const maxImp = Math.max(...top3.map((r) => Math.abs(r.rul_improvement)), 1); return [ { metric: "RUL", ...Object.fromEntries(top3.map((r) => [`#${r.rank}`, +((r.predicted_rul / maxRul) * 100).toFixed(1)])) }, { metric: "Improvement", ...Object.fromEntries(top3.map((r) => [`#${r.rank}`, +(Math.max(0, r.rul_improvement) / maxImp * 100).toFixed(1)])) }, { metric: "Low Temp", ...Object.fromEntries(top3.map((r) => [`#${r.rank}`, +(Math.max(0, 45 - r.ambient_temperature) / 45 * 100).toFixed(1)])) }, { metric: "Low Current", ...Object.fromEntries(top3.map((r) => [`#${r.rank}`, +(Math.max(0, 3 - r.discharge_current) / 3 * 100).toFixed(1)])) }, ]; }, [result]); const baseline = result?.recommendations[0]; const bestOnly = result?.recommendations.find((r) => r.rank === 1); return (
{/* Input panel */}

Operating Condition Optimizer

Find optimal temperature, discharge current and cutoff voltage to maximize Remaining Useful Life.

{/* Text inputs */}
setBatteryId(e.target.value)} className="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-white" />
setTopK(+e.target.value)} className="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-white" />
setCurrentCycle(+e.target.value)} className="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-white" />
{/* Sliders */}
{result && ( )}
{error && (
{error}
)}
{/* Results */} {result && (
{/* Summary cards */}
Battery
{result.battery_id}
Current SOH: {result.current_soh}%
Best RUL
{bestOnly?.predicted_rul.toFixed(0)} cyc
Top recommendation
Best Improvement
{bestOnly && bestOnly.rul_improvement > 0 ? "+" : ""}{bestOnly?.rul_improvement.toFixed(0)} cyc
{bestOnly?.rul_improvement_pct}% gain
Recommendations
{result.recommendations.length}
configurations
{/* Chart tabs */}

Visual Analysis

{(["rul", "params", "radar"] as const).map((t) => ( ))}
{chartTab === "rul" && ( [`${v} cycles`, name]} /> {rulBarData.map((d, i) => )} )} {chartTab === "params" && ( )} {chartTab === "radar" && radarData.length > 0 && ( {result.recommendations.slice(0, 3).map((r, i) => ( ))} )}
{/* Recommendations table */}
Recommendations for {result.battery_id} — SOH: {result.current_soh}%
{result.recommendations.length} configs
{result.recommendations.map((rec) => { const expanded = expandedRow === rec.rank; const impPositive = rec.rul_improvement > 0; return ( <> setExpandedRow(expanded ? null : rec.rank)} > {expanded && ( )} ); })}
# Temp (°C) Current (A) Cutoff (V) Pred. RUL Improvement % Gain
{rec.ambient_temperature} {rec.discharge_current}A {rec.cutoff_voltage}V {rec.predicted_rul.toFixed(0)} {impPositive ? "+" : ""}{rec.rul_improvement.toFixed(0)} {impPositive ? "+" : ""}{rec.rul_improvement_pct}% {expanded ? : }

{rec.explanation}

Temp: {rec.ambient_temperature}°C
Current: {rec.discharge_current}A
Cutoff: {rec.cutoff_voltage}V
)}
); }