fix(frontend): update ModelInfoResponse types to match backend structure

- Update TypeScript types to match flat backend response structure
- Remove nested model_info and metrics objects
- Remove label_config, use labels array and per_class_metrics array
- Update all component references to use new structure
- Generate default colors for prediction labels in CandleChart
- Fix TypeScript type errors for nullable model_version
- Remove accuracy/F1 metrics display (not in new response)
This commit is contained in:
Marko Djordjevic 2026-02-15 21:39:38 +01:00
parent aa81d4f3d0
commit 5a7c901980
95 changed files with 326 additions and 63 deletions

View file

@ -365,7 +365,7 @@ export default function Home() {
setPredictionState((prev) => ({
...prev,
modelInfo: data,
selectedLabels: new Set(data.label_config.map((l) => l.name)),
selectedLabels: new Set(data.labels),
error: null,
}));
return data;
@ -382,9 +382,9 @@ export default function Home() {
}, []);
// Generate cache key from chart, timerange, and model version
const generateCacheKey = useCallback((chartId: number | null, modelVersion?: string) => {
const generateCacheKey = useCallback((chartId: number | null, modelVersion?: string | null) => {
if (!chartId) return null;
const version = modelVersion || predictionState.modelInfo?.model_info.model_version || 'unknown';
const version = modelVersion || predictionState.modelInfo?.model_version || 'unknown';
return `${chartId}_${version}`;
}, [predictionState.modelInfo]);
@ -392,12 +392,12 @@ export default function Home() {
const fetchPredictions = useCallback(async (candles: any[]) => {
if (!activeChartId || candles.length === 0) return;
const cacheKey = generateCacheKey(activeChartId, predictionState.modelInfo?.model_info.model_version);
const cacheKey = generateCacheKey(activeChartId, predictionState.modelInfo?.model_version);
// Check cache first
if (cacheKey && predictionCacheRef.current.has(cacheKey)) {
const cached = predictionCacheRef.current.get(cacheKey)!;
if (cached.modelVersion === predictionState.modelInfo?.model_info.model_version) {
if (cached.modelVersion === predictionState.modelInfo?.model_version) {
setPredictionState((prev) => ({
...prev,
spans: cached.spans,
@ -562,7 +562,7 @@ export default function Home() {
// Clear prediction cache when model version changes
useEffect(() => {
if (predictionState.modelInfo) {
const currentVersion = predictionState.modelInfo.model_info.model_version;
const currentVersion = predictionState.modelInfo.model_version;
// Clear cache entries with different model versions
const newCache = new Map();
for (const [key, value] of predictionCacheRef.current.entries()) {
@ -572,7 +572,7 @@ export default function Home() {
}
predictionCacheRef.current = newCache;
}
}, [predictionState.modelInfo?.model_info.model_version]);
}, [predictionState.modelInfo?.model_version]);
// Health polling - check model status every 30 seconds when offline
useEffect(() => {

View file

@ -374,10 +374,21 @@ const CandleChart = forwardRef<CandleChartHandle, CandleChartProps>(
}
// Build a label-to-color map from modelInfo
// Generate colors for labels since backend no longer provides them
const labelColorMap: Record<string, string> = {};
if (modelInfo?.label_config) {
modelInfo.label_config.forEach((lc) => {
labelColorMap[lc.name] = lc.color;
if (modelInfo?.labels) {
const predefinedColors = [
'#3b82f6', // blue
'#ef4444', // red
'#10b981', // green
'#f59e0b', // amber
'#8b5cf6', // violet
'#ec4899', // pink
'#06b6d4', // cyan
'#f97316', // orange
];
modelInfo.labels.forEach((label, index) => {
labelColorMap[label] = predefinedColors[index % predefinedColors.length];
});
}

View file

@ -9,7 +9,7 @@ interface PredictionPanelProps {
onFetchBatchPredictions: () => void;
onConfidenceChange: (threshold: number) => void;
onToggleLabelSelection: (label: string) => void;
predictionSummary?: PredictionSummary;
predictionSummary: PredictionSummary | null;
isModelOnline: boolean;
showOnlyDisagreements?: boolean;
onToggleShowOnlyDisagreements?: () => void;
@ -77,23 +77,15 @@ export default function PredictionPanel({
<div className="mb-3 p-2 bg-muted/50 rounded text-xs">
<div className="flex justify-between">
<span className="text-muted-foreground">Model:</span>
<span className="font-mono text-foreground">{modelInfo.model_info.model_name}</span>
<span className="font-mono text-foreground">{modelInfo.model_name}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Version:</span>
<span className="font-mono text-foreground">{modelInfo.model_info.model_version}</span>
<span className="font-mono text-foreground">{modelInfo.model_version || 'N/A'}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Type:</span>
<span className="text-foreground">{modelInfo.model_info.model_type}</span>
</div>
<div className="flex justify-between mt-1 pt-1 border-t border-border">
<span className="text-muted-foreground">Accuracy:</span>
<span className="text-foreground">{(modelInfo.metrics.accuracy * 100).toFixed(1)}%</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">F1 (macro):</span>
<span className="text-foreground">{(modelInfo.metrics.f1_macro * 100).toFixed(1)}%</span>
<span className="text-foreground">{modelInfo.model_type}</span>
</div>
</div>
)}
@ -144,26 +136,22 @@ export default function PredictionPanel({
<div className="mb-3">
<label className="text-xs text-muted-foreground mb-2 block">Filter by Label</label>
<div className="space-y-1 max-h-32 overflow-y-auto">
{modelInfo.label_config.map((labelConfig) => {
const metrics = modelInfo.metrics.per_class[labelConfig.name];
const isSelected = selectedLabels.has(labelConfig.name);
{modelInfo.labels.map((label) => {
const metrics = modelInfo.per_class_metrics.find((m) => m.label === label);
const isSelected = selectedLabels.has(label);
return (
<label
key={labelConfig.name}
key={label}
className="flex items-center gap-2 p-1 rounded hover:bg-muted/50 cursor-pointer"
>
<input
type="checkbox"
checked={isSelected}
onChange={() => onToggleLabelSelection(labelConfig.name)}
onChange={() => onToggleLabelSelection(label)}
className="w-3 h-3"
/>
<div
className="w-3 h-3 rounded"
style={{ backgroundColor: labelConfig.color }}
/>
<span className="text-xs text-foreground flex-1">{labelConfig.name}</span>
<span className="text-xs text-foreground flex-1">{label}</span>
{metrics && (
<span className="text-xs text-muted-foreground font-mono">
F1: {(metrics.f1_score * 100).toFixed(0)}%

View file

@ -16,40 +16,23 @@ export interface PerCandlePrediction {
confidence: number;
}
export interface ModelInfo {
model_name: string;
model_version: string;
model_type: string;
experiment_name: string;
run_id: string;
trained_at: string;
feature_count: number;
label_names: string[];
}
export interface PerClassMetrics {
[label: string]: {
precision: number;
recall: number;
f1_score: number;
support: number;
};
}
export interface ModelMetrics {
accuracy: number;
f1_macro: number;
f1_weighted: number;
per_class: PerClassMetrics;
label: string;
precision: number;
recall: number;
f1_score: number;
support: number;
}
export interface ModelInfoResponse {
model_info: ModelInfo;
metrics: ModelMetrics;
label_config: {
name: string;
color: string;
}[];
model_name: string;
model_version: string | null;
model_type: string;
trained_at: string | null;
dataset_version: string | null;
feature_engineering_enabled: boolean;
labels: string[];
per_class_metrics: PerClassMetrics[];
}
export interface PredictRequest {