- 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>
275 lines
9.7 KiB
TypeScript
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;
|