trading-platform-frontend-v2/src/modules/ml/components/ModelSelector.tsx
Adrian Flores Cortes f7a5ddcca8 [OQI-006] feat: Add 3 ML utility panel components
- ModelSelector: Model selection with dropdown/tabs/cards variants
- EnsemblePanel: Ensemble voting configuration with weight sliders
- ICTAnalysisPanel: ICT analysis parameters with collapsible sections

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 11:03:20 -06:00

275 lines
9.7 KiB
TypeScript

/**
* ModelSelector Component
* Dropdown/tabs interface to switch between ML prediction models
* OQI-006: Senales ML
*/
import React, { useMemo } from 'react';
import {
Brain,
ChevronDown,
Check,
Cpu,
LineChart,
Network,
Layers,
Sparkles,
TrendingUp,
AlertCircle,
} from 'lucide-react';
export interface MLModel {
id: string;
name: string;
type: 'lstm' | 'xgboost' | 'random_forest' | 'svm' | 'ensemble' | 'transformer';
accuracy: number;
lastUpdated: string;
status: 'active' | 'training' | 'inactive' | 'deprecated';
description?: string;
features?: string[];
}
export interface ModelSelectorProps {
models: MLModel[];
selectedModelId: string;
onModelChange: (modelId: string) => void;
variant?: 'dropdown' | 'tabs' | 'cards';
showMetrics?: boolean;
disabled?: boolean;
loading?: boolean;
}
const ModelSelector: React.FC<ModelSelectorProps> = ({
models,
selectedModelId,
onModelChange,
variant = 'dropdown',
showMetrics = true,
disabled = false,
loading = false,
}) => {
const [isOpen, setIsOpen] = React.useState(false);
const dropdownRef = React.useRef<HTMLDivElement>(null);
const selectedModel = useMemo(
() => models.find((m) => m.id === selectedModelId),
[models, selectedModelId]
);
// Close dropdown on outside click
React.useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
const getModelIcon = (type: MLModel['type']) => {
switch (type) {
case 'lstm':
return <Network className="w-4 h-4" />;
case 'xgboost':
return <TrendingUp className="w-4 h-4" />;
case 'random_forest':
return <Layers className="w-4 h-4" />;
case 'svm':
return <LineChart className="w-4 h-4" />;
case 'ensemble':
return <Sparkles className="w-4 h-4" />;
case 'transformer':
return <Cpu className="w-4 h-4" />;
default:
return <Brain className="w-4 h-4" />;
}
};
const getStatusColor = (status: MLModel['status']) => {
switch (status) {
case 'active':
return 'text-green-400 bg-green-500/20';
case 'training':
return 'text-yellow-400 bg-yellow-500/20';
case 'inactive':
return 'text-gray-400 bg-gray-500/20';
case 'deprecated':
return 'text-red-400 bg-red-500/20';
default:
return 'text-gray-400 bg-gray-500/20';
}
};
const getAccuracyColor = (accuracy: number) => {
if (accuracy >= 70) return 'text-green-400';
if (accuracy >= 50) return 'text-yellow-400';
return 'text-red-400';
};
// Dropdown variant
if (variant === 'dropdown') {
return (
<div ref={dropdownRef} className="relative">
<button
onClick={() => !disabled && !loading && setIsOpen(!isOpen)}
disabled={disabled || loading}
className={`w-full flex items-center justify-between gap-3 px-4 py-3 bg-gray-800/50 border border-gray-700 rounded-lg transition-colors ${
disabled ? 'opacity-50 cursor-not-allowed' : 'hover:bg-gray-800 hover:border-gray-600'
}`}
>
<div className="flex items-center gap-3">
{loading ? (
<div className="w-5 h-5 border-2 border-blue-400 border-t-transparent rounded-full animate-spin" />
) : selectedModel ? (
<>
<div className="p-1.5 bg-blue-500/20 text-blue-400 rounded">
{getModelIcon(selectedModel.type)}
</div>
<div className="text-left">
<div className="font-medium text-white">{selectedModel.name}</div>
{showMetrics && (
<div className="text-xs text-gray-400">
Accuracy: <span className={getAccuracyColor(selectedModel.accuracy)}>{selectedModel.accuracy}%</span>
</div>
)}
</div>
</>
) : (
<span className="text-gray-400">Select a model</span>
)}
</div>
<ChevronDown className={`w-5 h-5 text-gray-400 transition-transform ${isOpen ? 'rotate-180' : ''}`} />
</button>
{isOpen && (
<div className="absolute z-50 w-full mt-2 bg-gray-800 border border-gray-700 rounded-lg shadow-xl overflow-hidden">
<div className="max-h-80 overflow-y-auto">
{models.map((model) => (
<button
key={model.id}
onClick={() => {
onModelChange(model.id);
setIsOpen(false);
}}
disabled={model.status === 'deprecated'}
className={`w-full flex items-center gap-3 px-4 py-3 transition-colors ${
model.id === selectedModelId
? 'bg-blue-500/20 border-l-2 border-blue-500'
: 'hover:bg-gray-700/50'
} ${model.status === 'deprecated' ? 'opacity-50 cursor-not-allowed' : ''}`}
>
<div className={`p-1.5 rounded ${model.id === selectedModelId ? 'bg-blue-500/30 text-blue-400' : 'bg-gray-700 text-gray-400'}`}>
{getModelIcon(model.type)}
</div>
<div className="flex-1 text-left">
<div className="flex items-center gap-2">
<span className="font-medium text-white">{model.name}</span>
<span className={`text-xs px-1.5 py-0.5 rounded ${getStatusColor(model.status)}`}>
{model.status}
</span>
</div>
{showMetrics && (
<div className="text-xs text-gray-400 mt-0.5">
Accuracy: <span className={getAccuracyColor(model.accuracy)}>{model.accuracy}%</span>
{model.description && <span className="ml-2">- {model.description}</span>}
</div>
)}
</div>
{model.id === selectedModelId && <Check className="w-5 h-5 text-blue-400" />}
</button>
))}
</div>
</div>
)}
</div>
);
}
// Tabs variant
if (variant === 'tabs') {
return (
<div className="flex flex-wrap gap-2 p-1 bg-gray-800/50 rounded-lg">
{models.map((model) => (
<button
key={model.id}
onClick={() => onModelChange(model.id)}
disabled={disabled || model.status === 'deprecated'}
className={`flex items-center gap-2 px-4 py-2 rounded-md transition-all ${
model.id === selectedModelId
? 'bg-blue-500 text-white shadow-lg'
: 'text-gray-400 hover:text-white hover:bg-gray-700/50'
} ${model.status === 'deprecated' ? 'opacity-50 cursor-not-allowed' : ''}`}
>
{getModelIcon(model.type)}
<span className="font-medium">{model.name}</span>
{showMetrics && (
<span className={`text-xs ${model.id === selectedModelId ? 'text-blue-100' : getAccuracyColor(model.accuracy)}`}>
{model.accuracy}%
</span>
)}
</button>
))}
</div>
);
}
// Cards variant
return (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{models.map((model) => (
<button
key={model.id}
onClick={() => onModelChange(model.id)}
disabled={disabled || model.status === 'deprecated'}
className={`relative p-4 rounded-xl border transition-all text-left ${
model.id === selectedModelId
? 'bg-blue-500/20 border-blue-500 shadow-lg shadow-blue-500/20'
: 'bg-gray-800/50 border-gray-700 hover:border-gray-600 hover:bg-gray-800'
} ${model.status === 'deprecated' ? 'opacity-50 cursor-not-allowed' : ''}`}
>
{model.id === selectedModelId && (
<div className="absolute top-2 right-2">
<Check className="w-5 h-5 text-blue-400" />
</div>
)}
<div className="flex items-center gap-3 mb-3">
<div className={`p-2 rounded-lg ${model.id === selectedModelId ? 'bg-blue-500/30 text-blue-400' : 'bg-gray-700 text-gray-400'}`}>
{getModelIcon(model.type)}
</div>
<div>
<div className="font-semibold text-white">{model.name}</div>
<div className={`text-xs px-1.5 py-0.5 rounded inline-block ${getStatusColor(model.status)}`}>
{model.status}
</div>
</div>
</div>
{model.description && (
<p className="text-sm text-gray-400 mb-3 line-clamp-2">{model.description}</p>
)}
{showMetrics && (
<div className="flex items-center justify-between pt-3 border-t border-gray-700">
<div className="text-sm text-gray-400">Accuracy</div>
<div className={`text-lg font-bold ${getAccuracyColor(model.accuracy)}`}>
{model.accuracy}%
</div>
</div>
)}
{model.status === 'training' && (
<div className="mt-3 flex items-center gap-2 text-xs text-yellow-400">
<AlertCircle className="w-3 h-3" />
<span>Model is currently training</span>
</div>
)}
</button>
))}
</div>
);
};
export default ModelSelector;