Spaces:
Running
Running
| /** | |
| * 🤖 ML Predictions - Complete Training & Results Page | |
| * | |
| * SAME AS DATAHUB: | |
| * - Select from existing files uploaded in DataHub | |
| * - Smart target column detection with auto-select | |
| * - Fast Mode (7 algorithms, 30-60s) vs Ultra Mode (20+ algorithms, 2-10min) | |
| * - Same training overlay with animated spinning icon | |
| * - Full dark/light mode support | |
| */ | |
| import React, { useState, useEffect } from 'react'; | |
| import { useNavigate, useLocation } from 'react-router-dom'; | |
| import { motion } from 'framer-motion'; | |
| import { WebIDE } from '../components/WebIDE'; | |
| import { api } from '../services/api'; | |
| import { useLiveStore } from '../store/liveStore'; | |
| // Force Vite HMR reload | |
| // Force Vite HMR reload | |
| import { | |
| Brain, | |
| TrendingUp, | |
| BarChart3, | |
| AlertTriangle, | |
| CheckCircle, | |
| Target, | |
| Zap, | |
| RefreshCw, | |
| Sparkles, | |
| Award, | |
| Layers, | |
| PieChart, | |
| Activity, | |
| Play, | |
| Square, | |
| History, | |
| Database, | |
| Download, | |
| FileText, | |
| XCircle, | |
| HeartPulse, | |
| Sliders, | |
| HelpCircle, | |
| Boxes, | |
| GitBranch, | |
| Code2, | |
| Shield, | |
| ShieldCheck, | |
| Copy, | |
| Check, | |
| Presentation, | |
| ImageDown, | |
| BarChart2, | |
| Rocket | |
| } from 'lucide-react'; | |
| import PptxGenJS from 'pptxgenjs'; | |
| import ModelHistory from '@/components/automl/ModelHistory'; | |
| import DataHealthCard from '@/components/automl/DataHealthCard'; | |
| import PlaygroundTab from '@/components/automl/PlaygroundTab'; | |
| import ExplainModal from '@/components/automl/ExplainModal'; | |
| import DeployModal from '@/components/automl/DeployModal'; | |
| import apiService from '@/services/api'; | |
| import { useUserStore } from '@/store/userStore'; | |
| import { useToast } from '@/contexts/ToastContext'; | |
| import { getUserIdSync, getAuthHeadersSync } from '@/utils/userId'; | |
| interface FeatureMetadata { | |
| name: string; | |
| type: 'numeric' | 'categorical' | 'text' | 'datetime' | 'date'; | |
| min?: number | string; // number for numeric, ISO string for datetime | |
| max?: number | string; // number for numeric, ISO string for datetime | |
| mean?: number; | |
| options?: string[]; | |
| placeholder?: string; // For text/datetime inputs | |
| format?: string; // e.g. 'YYYY-MM-DD' for date columns | |
| } | |
| interface MLResult { | |
| success: boolean; | |
| task_type: string; | |
| target_column: string; | |
| best_model: { | |
| name: string; | |
| metrics: Record<string, number>; | |
| reliability?: number; // 0-100 reliability score (ALL MODES) | |
| }; | |
| all_models: Array<{ | |
| name: string; | |
| metrics: Record<string, number>; | |
| reliability_score?: number; // Production Intelligence: per-model reliability | |
| warning?: string; // Production Intelligence: model warnings | |
| }>; | |
| feature_importance: Array<{ | |
| feature: string; | |
| importance: number; | |
| rank: number; | |
| }>; | |
| feature_metadata?: FeatureMetadata[]; | |
| bias_reports: Array<{ | |
| type: string; | |
| description: string; | |
| severity: string; | |
| corrected: boolean; | |
| }>; | |
| insights: string[]; | |
| recommendations: string[]; | |
| charts: Record<string, string>; | |
| data_summary: { | |
| rows: number; | |
| columns: number; | |
| features_engineered: number; | |
| features_used?: number; | |
| }; | |
| processing_time_seconds: number; | |
| is_nlp_task?: boolean; | |
| primary_text_col?: string; | |
| feature_columns?: string[]; | |
| cleaned_file?: string; | |
| mode?: 'traditional' | 'nlp' | 'deep_learning'; | |
| modes_trained?: string[]; | |
| modes_requested?: string[]; | |
| results_per_mode?: Record<string, { | |
| success: boolean; | |
| best_model?: string; | |
| algorithm?: string; | |
| architecture?: string; | |
| metrics?: Record<string, number>; | |
| error?: string; | |
| }>; | |
| combined_metrics?: Record<string, any>; | |
| leaderboard?: Array<{ | |
| mode: string; | |
| model: string; | |
| score: number; | |
| metrics?: Record<string, number>; | |
| reliability_score?: number; // Production Intelligence | |
| }>; | |
| best_overall?: { | |
| mode: string; | |
| name: string; | |
| metrics?: Record<string, number>; | |
| }; | |
| pipeline?: string; | |
| was_stopped?: boolean; | |
| // PRODUCTION INTELLIGENCE - Built into ALL training modes | |
| reliability_score?: number; // Overall model reliability 0-100 | |
| validation_warnings?: string[]; // Any warnings from production validation | |
| leakage_report?: { | |
| has_leakage: boolean; | |
| severity: string; | |
| columns_removed: string[]; | |
| details: string[]; | |
| }; | |
| dataset_profile?: { | |
| size_category: string; | |
| is_imbalanced: boolean; | |
| imbalance_ratio?: number; | |
| noise_level?: number; | |
| missing_ratio?: number; | |
| }; | |
| preprocessing_steps?: string[]; | |
| warnings?: string[]; | |
| } | |
| interface FileItem { | |
| id: string; | |
| name: string; | |
| size: number; | |
| type: string; | |
| uploadedAt: string; | |
| status: 'processing' | 'completed' | 'failed'; | |
| } | |
| const MLPredictions: React.FC = () => { | |
| const { isDark } = useUserStore(); | |
| const { mlCache, setMlCache } = useLiveStore(); | |
| const toast = useToast(); | |
| const navigate = useNavigate(); | |
| const location = useLocation(); | |
| // Results state | |
| const [result, setResult] = useState<MLResult | null>(() => { | |
| // Hydrate from zustand cache if available | |
| const cached = mlCache['latest_ml_result']; | |
| if (cached) return cached; | |
| return null; | |
| }); | |
| // Automatically save to cache whenever result changes | |
| useEffect(() => { | |
| if (result) { | |
| setMlCache('latest_ml_result', result); | |
| } | |
| }, [result, setMlCache]); | |
| const [loading, setLoading] = useState(!result); | |
| const [activeTab, setActiveTab] = useState<'overview' | 'charts' | 'features' | 'predict' | 'playground' | 'history' | 'data' | 'clustering' | 'experiments'>('overview'); | |
| const [showExplainModal, setShowExplainModal] = useState(false); | |
| const [showDeployModal, setShowDeployModal] = useState(false); | |
| const [explainInputValues, setExplainInputValues] = useState<Record<string, any>>({}); | |
| const [predictionInput, setPredictionInput] = useState<Record<string, string>>({}); | |
| const [predictionResult, setPredictionResult] = useState<any>(null); | |
| const [chartsLoading, setChartsLoading] = useState(false); | |
| const [copiedChart, setCopiedChart] = useState<string | null>(null); | |
| const [exportingPPT, setExportingPPT] = useState(false); | |
| // File & Training state - SAME AS DATAHUB | |
| const [existingFiles, setExistingFiles] = useState<FileItem[]>([]); | |
| const [selectedFiles, setSelectedFiles] = useState<FileItem[]>([]); | |
| const [training, setTraining] = useState(false); | |
| const [modelAStatus, setModelAStatus] = useState('Active'); | |
| const [modelBStatus, setModelBStatus] = useState('Testing'); | |
| const promoteModelB = () => { | |
| setModelAStatus('Archived'); | |
| setModelBStatus('Active (Champion)'); | |
| }; | |
| const toggleModelA = () => { | |
| setModelAStatus(prev => prev === 'Active' ? 'Inactive' : 'Active'); | |
| }; | |
| const [ultraMode, setUltraMode] = useState(true); // Ultra AutoML (maximum accuracy) is default | |
| // Note: Production Intelligence (leakage detection, reliability scoring) is now built into ALL modes | |
| const [targetColumn, setTargetColumn] = useState(''); | |
| const [availableColumns, setAvailableColumns] = useState<string[]>([]); | |
| const [progressMessage, setProgressMessage] = useState('Initializing...'); | |
| const [abortController, setAbortController] = useState<AbortController | null>(null); | |
| // Learning Type: supervised (classification/regression) or unsupervised (clustering) | |
| const [learningType, setLearningType] = useState<'supervised' | 'unsupervised'>('supervised'); | |
| // Multi-mode ML selection - users can select multiple modes simultaneously | |
| const [selectedModes, setSelectedModes] = useState<Set<'traditional' | 'nlp' | 'deep_learning'>>(new Set(['traditional'])); | |
| // Algorithm selection per ML type (multi-select within each mode) | |
| const [selectedAlgorithms, setSelectedAlgorithms] = useState<{ | |
| traditional: string[]; | |
| nlp: string[]; | |
| deep_learning: string[]; | |
| }>({ | |
| traditional: ['auto'], | |
| nlp: ['auto'], | |
| deep_learning: ['auto'], | |
| }); | |
| // For backward compatibility with existing code | |
| const mlType = Array.from(selectedModes)[0] || 'traditional'; | |
| const selectedAlgorithm = selectedAlgorithms[mlType]?.[0] || 'auto'; | |
| const setMlType = (type: 'traditional' | 'nlp' | 'deep_learning') => { | |
| setSelectedModes(new Set([type])); | |
| }; | |
| // Toggle mode selection (multi-select enabled) | |
| const toggleMode = (mode: 'traditional' | 'nlp' | 'deep_learning') => { | |
| const newModes = new Set(selectedModes); | |
| if (newModes.has(mode)) { | |
| if (newModes.size > 1) { // Keep at least one mode selected | |
| newModes.delete(mode); | |
| } | |
| } else { | |
| newModes.add(mode); | |
| } | |
| setSelectedModes(newModes); | |
| }; | |
| // Toggle algorithm selection within a mode | |
| const toggleAlgorithm = (mode: 'traditional' | 'nlp' | 'deep_learning', algo: string) => { | |
| setSelectedAlgorithms(prev => { | |
| const current = prev[mode]; | |
| if (algo === 'auto') { | |
| return { ...prev, [mode]: ['auto'] }; | |
| } | |
| const withoutAuto = current.filter(a => a !== 'auto'); | |
| if (current.includes(algo)) { | |
| const filtered = withoutAuto.filter(a => a !== algo); | |
| return { ...prev, [mode]: filtered.length > 0 ? filtered : ['auto'] }; | |
| } else { | |
| return { ...prev, [mode]: [...withoutAuto, algo] }; | |
| } | |
| }); | |
| }; | |
| // Select all algorithms in a mode | |
| const selectAllAlgorithms = (mode: 'traditional' | 'nlp' | 'deep_learning') => { | |
| const allAlgos = algorithmOptions[mode].filter(a => a.value !== 'auto').map(a => a.value); | |
| setSelectedAlgorithms(prev => ({ ...prev, [mode]: allAlgos })); | |
| }; | |
| // COMPREHENSIVE Algorithm Options - ALL algorithms for each mode | |
| const algorithmOptions = { | |
| traditional: [ | |
| { value: 'auto', label: '🚀 Auto (Best Model)', description: 'Smart selection from all algorithms', category: 'auto' }, | |
| // Tree-based | |
| { value: 'random_forest', label: 'Random Forest', description: 'Ensemble of decision trees', category: 'Tree' }, | |
| { value: 'xgboost', label: 'XGBoost', description: 'Competition winner', category: 'Tree' }, | |
| { value: 'lightgbm', label: 'LightGBM', description: 'Fast gradient boosting', category: 'Tree' }, | |
| { value: 'catboost', label: 'CatBoost', description: 'Great for categories', category: 'Tree' }, | |
| { value: 'decision_tree', label: 'Decision Tree', description: 'Simple, interpretable', category: 'Tree' }, | |
| { value: 'extra_trees', label: 'Extra Trees', description: 'More random than RF', category: 'Tree' }, | |
| { value: 'gradient_boosting', label: 'Gradient Boosting', description: 'Classic boosting', category: 'Tree' }, | |
| { value: 'hist_gradient_boosting', label: 'Histogram GB', description: 'Fast for large data', category: 'Tree' }, | |
| // Linear | |
| { value: 'logistic_regression', label: 'Logistic Regression', description: 'Fast baseline', category: 'Linear' }, | |
| { value: 'ridge', label: 'Ridge Regression', description: 'L2 regularization', category: 'Linear' }, | |
| { value: 'lasso', label: 'Lasso Regression', description: 'L1 feature selection', category: 'Linear' }, | |
| { value: 'elastic_net', label: 'Elastic Net', description: 'L1+L2 combined', category: 'Linear' }, | |
| { value: 'sgd', label: 'SGD Classifier', description: 'Online learning', category: 'Linear' }, | |
| // SVM | |
| { value: 'svm_linear', label: 'SVM (Linear)', description: 'Linear kernel', category: 'SVM' }, | |
| { value: 'svm_rbf', label: 'SVM (RBF)', description: 'Non-linear kernel', category: 'SVM' }, | |
| { value: 'svm_poly', label: 'SVM (Polynomial)', description: 'Polynomial kernel', category: 'SVM' }, | |
| // K-Nearest Neighbors | |
| { value: 'knn_3', label: 'KNN (k=3)', description: 'Fast, small k', category: 'KNN' }, | |
| { value: 'knn_5', label: 'KNN (k=5)', description: 'Balanced k', category: 'KNN' }, | |
| { value: 'knn_7', label: 'KNN (k=7)', description: 'Smoother', category: 'KNN' }, | |
| { value: 'knn_weighted', label: 'KNN (Weighted)', description: 'Distance weighted', category: 'KNN' }, | |
| // Naive Bayes | |
| { value: 'gaussian_nb', label: 'Gaussian Naive Bayes', description: 'Continuous features', category: 'Naive Bayes' }, | |
| { value: 'multinomial_nb', label: 'Multinomial NB', description: 'Count data', category: 'Naive Bayes' }, | |
| { value: 'bernoulli_nb', label: 'Bernoulli NB', description: 'Binary features', category: 'Naive Bayes' }, | |
| { value: 'complement_nb', label: 'Complement NB', description: 'Imbalanced data', category: 'Naive Bayes' }, | |
| // Ensemble | |
| { value: 'adaboost', label: 'AdaBoost', description: 'Adaptive boosting', category: 'Ensemble' }, | |
| { value: 'bagging', label: 'Bagging', description: 'Bootstrap aggregating', category: 'Ensemble' }, | |
| { value: 'voting', label: 'Voting Ensemble', description: 'Multiple models vote', category: 'Ensemble' }, | |
| { value: 'stacking', label: 'Stacking', description: 'Meta-learner', category: 'Ensemble' }, | |
| // Other | |
| { value: 'lda', label: 'LDA', description: 'Linear Discriminant', category: 'Other' }, | |
| { value: 'qda', label: 'QDA', description: 'Quadratic Discriminant', category: 'Other' }, | |
| ], | |
| nlp: [ | |
| { value: 'auto', label: '🚀 Auto (Best NLP)', description: 'Smart NLP selection', category: 'auto' }, | |
| // Text Vectorization | |
| { value: 'tfidf', label: 'TF-IDF', description: 'Term frequency-inverse doc', category: 'Vectorization' }, | |
| { value: 'bow', label: 'Bag of Words', description: 'Word count vectors', category: 'Vectorization' }, | |
| { value: 'count_vectorizer', label: 'Count Vectorizer', description: 'Raw word counts', category: 'Vectorization' }, | |
| { value: 'hashing', label: 'Hashing Vectorizer', description: 'Memory efficient', category: 'Vectorization' }, | |
| // N-grams | |
| { value: 'unigram', label: 'Unigram', description: 'Single words', category: 'N-gram' }, | |
| { value: 'bigram', label: 'Bigram', description: 'Word pairs', category: 'N-gram' }, | |
| { value: 'trigram', label: 'Trigram', description: 'Three-word sequences', category: 'N-gram' }, | |
| { value: 'char_ngram', label: 'Character N-gram', description: 'Spelling-robust', category: 'N-gram' }, | |
| // Word Embeddings | |
| { value: 'word2vec_cbow', label: 'Word2Vec (CBOW)', description: 'Context prediction', category: 'Embeddings' }, | |
| { value: 'word2vec_skipgram', label: 'Word2Vec (Skip-gram)', description: 'Word prediction', category: 'Embeddings' }, | |
| { value: 'glove', label: 'GloVe', description: 'Global vectors', category: 'Embeddings' }, | |
| { value: 'fasttext', label: 'FastText', description: 'Subword embeddings', category: 'Embeddings' }, | |
| { value: 'doc2vec', label: 'Doc2Vec', description: 'Document vectors', category: 'Embeddings' }, | |
| // Topic Modeling | |
| { value: 'lda', label: 'LDA', description: 'Topic discovery', category: 'Topic Model' }, | |
| { value: 'lsa', label: 'LSA/LSI', description: 'Latent semantics', category: 'Topic Model' }, | |
| { value: 'nmf', label: 'NMF', description: 'Non-negative matrix', category: 'Topic Model' }, | |
| // Transformers | |
| { value: 'bert', label: 'BERT', description: 'Bidirectional encoder', category: 'Transformer' }, | |
| { value: 'distilbert', label: 'DistilBERT', description: 'Lightweight BERT', category: 'Transformer' }, | |
| { value: 'roberta', label: 'RoBERTa', description: 'Robust BERT', category: 'Transformer' }, | |
| { value: 'albert', label: 'ALBERT', description: 'Efficient BERT', category: 'Transformer' }, | |
| { value: 'xlnet', label: 'XLNet', description: 'Permutation LM', category: 'Transformer' }, | |
| { value: 'electra', label: 'ELECTRA', description: 'Efficient pretraining', category: 'Transformer' }, | |
| { value: 'gpt2', label: 'GPT-2', description: 'Generative model', category: 'Transformer' }, | |
| // Sentiment Analysis | |
| { value: 'vader', label: 'VADER', description: 'Rule-based sentiment', category: 'Sentiment' }, | |
| { value: 'textblob', label: 'TextBlob', description: 'Simple NLP', category: 'Sentiment' }, | |
| // Ensembles | |
| { value: 'voting_ensemble', label: 'Voting Ensemble', description: 'Multiple classifiers', category: 'Ensemble' }, | |
| { value: 'stacking_ensemble', label: 'Stacking Ensemble', description: 'Meta-learner', category: 'Ensemble' }, | |
| { value: 'blending_ensemble', label: 'Blending Ensemble', description: 'Holdout blend', category: 'Ensemble' }, | |
| { value: 'weighted_ensemble', label: 'Weighted Ensemble', description: 'Weighted voting', category: 'Ensemble' }, | |
| ], | |
| deep_learning: [ | |
| { value: 'auto', label: '🚀 Auto (Best NN)', description: 'Smart architecture', category: 'auto' }, | |
| // ANN - Artificial Neural Network | |
| { value: 'ann_shallow', label: 'ANN Shallow (32)', description: 'Single hidden layer', category: 'ANN' }, | |
| { value: 'ann_medium', label: 'ANN Medium (64-32)', description: 'Two layers', category: 'ANN' }, | |
| { value: 'ann_deep', label: 'ANN Deep (128-64-32)', description: 'Three layers', category: 'ANN' }, | |
| { value: 'ann_wide', label: 'ANN Wide (256-128)', description: 'Wide network', category: 'ANN' }, | |
| // MLP - Multi-Layer Perceptron | |
| { value: 'mlp_small', label: 'MLP Small (64-32)', description: 'Fast, small data', category: 'MLP' }, | |
| { value: 'mlp_medium', label: 'MLP Medium (128-64-32)', description: 'Balanced', category: 'MLP' }, | |
| { value: 'mlp_large', label: 'MLP Large (256-128-64)', description: 'Complex patterns', category: 'MLP' }, | |
| { value: 'mlp_xl', label: 'MLP XL (512-256-128)', description: 'Large scale', category: 'MLP' }, | |
| // RNN - Recurrent Neural Network | |
| { value: 'rnn_simple', label: 'RNN Simple', description: 'Basic recurrent', category: 'RNN' }, | |
| { value: 'rnn_deep', label: 'RNN Deep', description: 'Stacked RNN', category: 'RNN' }, | |
| { value: 'rnn_bidirectional', label: 'Bidirectional RNN', description: 'Both directions', category: 'RNN' }, | |
| // LSTM - Long Short-Term Memory | |
| { value: 'lstm_simple', label: 'LSTM', description: 'Long-term memory', category: 'LSTM' }, | |
| { value: 'lstm_stacked', label: 'Stacked LSTM', description: 'Multi-layer LSTM', category: 'LSTM' }, | |
| { value: 'lstm_deep', label: 'Deep LSTM', description: '3+ layer LSTM', category: 'LSTM' }, | |
| { value: 'bilstm', label: 'BiLSTM', description: 'Bidirectional LSTM', category: 'LSTM' }, | |
| { value: 'lstm_attention', label: 'LSTM + Attention', description: 'Attention mechanism', category: 'LSTM' }, | |
| // GRU - Gated Recurrent Unit | |
| { value: 'gru_simple', label: 'GRU', description: 'Simpler than LSTM', category: 'GRU' }, | |
| { value: 'gru_stacked', label: 'Stacked GRU', description: 'Multi-layer GRU', category: 'GRU' }, | |
| { value: 'bigru', label: 'BiGRU', description: 'Bidirectional GRU', category: 'GRU' }, | |
| // CNN - Convolutional Networks | |
| { value: 'cnn_1d', label: 'CNN 1D', description: '1D convolutions', category: 'CNN' }, | |
| { value: 'textcnn', label: 'TextCNN', description: 'Text classification', category: 'CNN' }, | |
| { value: 'cnn_multichannel', label: 'Multi-channel CNN', description: 'Multiple filters', category: 'CNN' }, | |
| // Transformer | |
| { value: 'transformer_encoder', label: 'Transformer Encoder', description: 'Self-attention', category: 'Transformer' }, | |
| { value: 'transformer_decoder', label: 'Transformer Decoder', description: 'Causal attention', category: 'Transformer' }, | |
| { value: 'self_attention', label: 'Self-Attention', description: 'Attention layer', category: 'Transformer' }, | |
| { value: 'multihead_attention', label: 'Multi-Head Attention', description: 'Multiple heads', category: 'Transformer' }, | |
| // Autoencoder | |
| { value: 'autoencoder', label: 'Autoencoder', description: 'Compression', category: 'Autoencoder' }, | |
| { value: 'vae', label: 'VAE', description: 'Variational', category: 'Autoencoder' }, | |
| { value: 'sparse_autoencoder', label: 'Sparse Autoencoder', description: 'Sparse features', category: 'Autoencoder' }, | |
| { value: 'denoising_autoencoder', label: 'Denoising AE', description: 'Noise robust', category: 'Autoencoder' }, | |
| // Regularization Variants | |
| { value: 'dropout_nn', label: 'Dropout NN', description: 'Dropout regularization', category: 'Regularized' }, | |
| { value: 'batchnorm_nn', label: 'BatchNorm NN', description: 'Batch normalization', category: 'Regularized' }, | |
| { value: 'layernorm_nn', label: 'LayerNorm NN', description: 'Layer normalization', category: 'Regularized' }, | |
| { value: 'l1_regularized', label: 'L1 Regularized', description: 'Lasso penalty', category: 'Regularized' }, | |
| { value: 'l2_regularized', label: 'L2 Regularized', description: 'Ridge penalty', category: 'Regularized' }, | |
| { value: 'elastic_net_nn', label: 'Elastic Net NN', description: 'L1+L2 penalty', category: 'Regularized' }, | |
| // Activation Variants | |
| { value: 'relu_nn', label: 'ReLU Network', description: 'ReLU activation', category: 'Activation' }, | |
| { value: 'leaky_relu_nn', label: 'Leaky ReLU Network', description: 'Leaky ReLU', category: 'Activation' }, | |
| { value: 'elu_nn', label: 'ELU Network', description: 'Exponential LU', category: 'Activation' }, | |
| { value: 'selu_nn', label: 'SELU Network', description: 'Self-normalizing', category: 'Activation' }, | |
| { value: 'gelu_nn', label: 'GELU Network', description: 'Gaussian Error LU', category: 'Activation' }, | |
| { value: 'swish_nn', label: 'Swish Network', description: 'Swish activation', category: 'Activation' }, | |
| { value: 'mish_nn', label: 'Mish Network', description: 'Mish activation', category: 'Activation' }, | |
| { value: 'tanh_nn', label: 'Tanh Network', description: 'Tanh activation', category: 'Activation' }, | |
| { value: 'sigmoid_nn', label: 'Sigmoid Network', description: 'Sigmoid activation', category: 'Activation' }, | |
| // Ensemble Neural Networks | |
| { value: 'bagging_nn', label: 'Bagging NN', description: 'Bootstrap aggregating', category: 'Ensemble' }, | |
| { value: 'boosting_nn', label: 'Boosting NN', description: 'Sequential learning', category: 'Ensemble' }, | |
| { value: 'snapshot_ensemble', label: 'Snapshot Ensemble', description: 'Single training', category: 'Ensemble' }, | |
| { value: 'stacked_nn', label: 'Stacked NN', description: 'Meta-learning', category: 'Ensemble' }, | |
| // Residual Networks | |
| { value: 'resnet_mlp', label: 'ResNet-MLP', description: 'Skip connections', category: 'Residual' }, | |
| { value: 'densenet_mlp', label: 'DenseNet-MLP', description: 'Dense connections', category: 'Residual' }, | |
| { value: 'highway_network', label: 'Highway Network', description: 'Gated connections', category: 'Residual' }, | |
| { value: 'mlp_bagging', label: 'MLP Bagging', description: 'Bootstrap MLPs', category: 'Ensemble' }, | |
| ], | |
| }; | |
| const [clusteringAlgorithm, setClusteringAlgorithm] = useState<string>('auto'); | |
| const [clusterCount, setClusterCount] = useState<number | null>(null); // null = auto-detect | |
| const [clusteringResult, setClusteringResult] = useState<any>(null); | |
| // Clustering UI state | |
| const [clusterActiveTab, setClusterActiveTab] = useState<'overview' | 'charts' | 'profiles' | 'predict' | 'download'>('overview'); | |
| const [clusterPredictionInput, setClusterPredictionInput] = useState<Record<string, string>>({}); | |
| const [clusterPredictionResult, setClusterPredictionResult] = useState<any>(null); | |
| const [clusterPredicting, setClusterPredicting] = useState(false); | |
| // WebIDE state | |
| const [showIde, setShowIde] = useState(false); | |
| const [ideFiles, setIdeFiles] = useState<Record<string, string>>({}); | |
| // Smart target column detection - SAME AS DATAHUB | |
| const detectTargetColumn = (columns: string[]): string => { | |
| const targetPatterns = [ | |
| 'target', 'label', 'class', 'y', 'output', 'result', 'prediction', | |
| 'price_range', 'price', 'category', 'status', 'type', 'outcome', | |
| 'fraud', 'churn', 'default', 'survived', 'approved', 'purchased' | |
| ]; | |
| // Check for exact/partial matches | |
| for (const pattern of targetPatterns) { | |
| for (const col of columns) { | |
| if (col.toLowerCase().includes(pattern)) { | |
| return col; | |
| } | |
| } | |
| } | |
| // Default: last column (common ML convention) | |
| return columns[columns.length - 1]; | |
| }; | |
| // Load existing files from DataHub | |
| const loadExistingFiles = async () => { | |
| try { | |
| const response = await apiService.listFiles(); | |
| const dataFiles = (response.data.files || []).filter((f: FileItem) => | |
| f.name.endsWith('.csv') || f.name.endsWith('.xlsx') || f.name.endsWith('.xls') | |
| ); | |
| setExistingFiles(dataFiles); | |
| } catch (error) { | |
| console.error('Failed to load files:', error); | |
| } | |
| }; | |
| // Fetch charts from API | |
| const fetchChartsFromAPI = async (userId: string): Promise<Record<string, string>> => { | |
| try { | |
| const response = await fetch(`/api/v2/automl/charts/${userId}`, { | |
| headers: getAuthHeadersSync() | |
| }); | |
| const data = await response.json(); | |
| if (data.success && data.charts && Object.keys(data.charts).length > 0) { | |
| try { | |
| sessionStorage.setItem(`mlCharts_${userId}`, JSON.stringify(data.charts)); | |
| } catch (e) { | |
| console.warn('[MLPredictions] Failed to cache charts'); | |
| } | |
| return data.charts; | |
| } | |
| return {}; | |
| } catch (error) { | |
| console.warn('[MLPredictions] Failed to fetch charts:', error); | |
| return {}; | |
| } | |
| }; | |
| // Load results on mount - with proper cleanup for memory leak prevention | |
| useEffect(() => { | |
| let isMounted = true; // Prevent state updates after unmount | |
| const abortCtrl = new AbortController(); | |
| const loadResults = async () => { | |
| const userId = getUserIdSync(); // CONSISTENT user ID handling | |
| const addChartsFromStorage = async (result: MLResult): Promise<MLResult> => { | |
| if (!result.charts || Object.keys(result.charts).length === 0) { | |
| const savedCharts = sessionStorage.getItem(`mlCharts_${userId}`); | |
| if (savedCharts) { | |
| try { | |
| result.charts = JSON.parse(savedCharts); | |
| return result; | |
| } catch (e) { } | |
| } | |
| setChartsLoading(true); | |
| const apiCharts = await fetchChartsFromAPI(userId); | |
| setChartsLoading(false); | |
| if (Object.keys(apiCharts).length > 0) { | |
| result.charts = apiCharts; | |
| } | |
| } | |
| return result; | |
| }; | |
| // 🔄 Sync result with active model from backend to ensure consistency | |
| const syncWithActiveModel = async (result: MLResult): Promise<MLResult> => { | |
| try { | |
| // First sync basic model info | |
| const response = await fetch(`/api/v2/autonomous/models/${userId}`, { | |
| headers: getAuthHeadersSync() | |
| }); | |
| const data = await response.json(); | |
| if (data.success && data.models && data.models.length > 0) { | |
| const activeModel = data.models.find((m: any) => m.is_active); | |
| if (activeModel) { | |
| // Only update best_model if NO multimode result with best_overall exists | |
| // (multimode training already has the correct best model across all modes) | |
| const hasMultimodeResult = result.best_overall || (result as any).modes_trained?.length > 0; | |
| if (!hasMultimodeResult) { | |
| result.best_model = { | |
| name: activeModel.model_name, | |
| metrics: activeModel.metrics || {} | |
| }; | |
| result.mode = activeModel.mode || 'traditional'; | |
| } | |
| result.target_column = activeModel.target_column; | |
| result.task_type = activeModel.task_type; | |
| result.feature_columns = activeModel.feature_columns || result.feature_columns; | |
| console.log('[MLPredictions] Synced with active model:', activeModel.model_name, 'hasMultimode:', hasMultimodeResult); | |
| } | |
| } | |
| // CRITICAL: Fetch feature_metadata AND charts from saved model | |
| // Use mode=auto to let backend auto-detect the best mode for multi-mode training | |
| const savedResultResponse = await fetch(`/api/v1/automl/saved-result?user_id=${userId}&mode=auto`, { | |
| headers: getAuthHeadersSync() | |
| }); | |
| const savedResult = await savedResultResponse.json(); | |
| if (savedResult.success) { | |
| // Update feature_metadata if missing | |
| if ((!result.feature_metadata || result.feature_metadata.length === 0) && savedResult.feature_metadata?.length > 0) { | |
| result.feature_metadata = savedResult.feature_metadata; | |
| console.log('[MLPredictions] Loaded feature_metadata from saved model:', savedResult.feature_metadata.length, 'features'); | |
| } | |
| // Update charts if missing or empty | |
| if ((!result.charts || Object.keys(result.charts).length === 0) && savedResult.charts && Object.keys(savedResult.charts).length > 0) { | |
| result.charts = savedResult.charts; | |
| console.log('[MLPredictions] Loaded charts from saved model:', Object.keys(savedResult.charts).length, 'charts'); | |
| // Also save to sessionStorage for faster future access | |
| try { | |
| sessionStorage.setItem(`mlCharts_${userId}`, JSON.stringify(savedResult.charts)); | |
| } catch (e) { } | |
| } | |
| // Update mode if detected | |
| if (savedResult.mode) { | |
| result.mode = savedResult.mode as 'traditional' | 'nlp' | 'deep_learning'; | |
| } | |
| // Update multi-mode specific fields | |
| if (savedResult.modes_trained) { | |
| (result as any).modes_trained = savedResult.modes_trained; | |
| } | |
| if (savedResult.best_overall) { | |
| (result as any).best_overall = savedResult.best_overall; | |
| // CRITICAL: Update best_model from best_overall (correct mode winner) | |
| result.best_model = { | |
| name: savedResult.best_overall.name || savedResult.best_overall.model || result.best_model?.name || 'Unknown', | |
| metrics: savedResult.best_overall.metrics || result.best_model?.metrics || {} | |
| }; | |
| console.log('[MLPredictions] Updated best_model from best_overall:', result.best_model.name, 'mode:', savedResult.mode); | |
| } | |
| if (savedResult.results_per_mode) { | |
| (result as any).results_per_mode = savedResult.results_per_mode; | |
| } | |
| if (savedResult.primary_text_col) { | |
| (result as any).primary_text_col = savedResult.primary_text_col; | |
| } | |
| // CRITICAL: Update data_summary if missing (for rows, columns, features display) | |
| if ((!result.data_summary || result.data_summary.rows === 0) && savedResult.data_summary) { | |
| result.data_summary = savedResult.data_summary; | |
| console.log('[MLPredictions] Loaded data_summary from saved model:', savedResult.data_summary); | |
| } | |
| // Update all_models if missing (for Models Trained count) | |
| if ((!result.all_models || result.all_models.length === 0) && savedResult.all_models?.length > 0) { | |
| result.all_models = savedResult.all_models; | |
| console.log('[MLPredictions] Loaded all_models from saved model:', savedResult.all_models.length, 'models'); | |
| } | |
| // Update leaderboard if missing | |
| if ((!result.leaderboard || result.leaderboard?.length === 0) && savedResult.leaderboard?.length > 0) { | |
| (result as any).leaderboard = savedResult.leaderboard; | |
| } | |
| // Update insights if missing (for AI Insights section) | |
| if ((!result.insights || result.insights.length === 0) && savedResult.insights?.length > 0) { | |
| result.insights = savedResult.insights; | |
| console.log('[MLPredictions] Loaded insights from saved model:', savedResult.insights.length, 'insights'); | |
| } | |
| } | |
| } catch (err) { | |
| console.warn('[MLPredictions] Could not sync with active model:', err); | |
| } | |
| return result; | |
| }; | |
| // Priority 1: Location state (fresh training - skip sync) | |
| if (location.state?.automlResult) { | |
| const navResult = location.state.automlResult; | |
| const resultWithCharts = await addChartsFromStorage(navResult); | |
| if (!isMounted) return; // Prevent state updates if unmounted | |
| setResult(resultWithCharts); | |
| setLearningType('supervised'); | |
| setActiveTab('overview'); // Reset to overview when loading results | |
| try { | |
| // Save to BOTH localStorage and sessionStorage | |
| localStorage.setItem(`mlResults_${userId}`, JSON.stringify(navResult)); | |
| localStorage.setItem(`hasMLResults_${userId}`, 'true'); | |
| // SessionStorage for current browser session (persists during navigation) | |
| sessionStorage.setItem(`mlResultsSession_${userId}`, JSON.stringify(navResult)); | |
| } catch (storageErr) { | |
| const { charts, ...lightResult } = navResult; | |
| localStorage.setItem(`mlResults_${userId}`, JSON.stringify(lightResult)); | |
| localStorage.setItem(`hasMLResults_${userId}`, 'true'); | |
| sessionStorage.setItem(`mlResultsSession_${userId}`, JSON.stringify(lightResult)); | |
| } | |
| if (resultWithCharts.charts && Object.keys(resultWithCharts.charts).length > 0) { | |
| try { | |
| sessionStorage.setItem(`mlCharts_${userId}`, JSON.stringify(resultWithCharts.charts)); | |
| } catch (e) { } | |
| } | |
| if (isMounted) setLoading(false); | |
| return; | |
| } | |
| // Priority 1.5: Check if clustering result exists in localStorage | |
| // This MUST come before backend API (Priority 2) because the backend | |
| // may return OLD supervised results that would override recent clustering | |
| const savedClustering = localStorage.getItem(`clusteringResult_${userId}`); | |
| if (savedClustering) { | |
| try { | |
| const parsedClustering = JSON.parse(savedClustering); | |
| if (parsedClustering && parsedClustering.success) { | |
| console.log('[MLPredictions] Restored clustering result from localStorage'); | |
| if (isMounted) { | |
| setClusteringResult(parsedClustering); | |
| setLearningType('unsupervised'); | |
| setClusterActiveTab('overview'); | |
| } | |
| if (isMounted) setLoading(false); | |
| return; | |
| } | |
| } catch (e) { } | |
| } | |
| // Also check sessionStorage for clustering (current session) | |
| const sessionClustering = sessionStorage.getItem(`clusteringResult_${userId}`); | |
| if (sessionClustering) { | |
| try { | |
| const parsedClustering = JSON.parse(sessionClustering); | |
| if (parsedClustering && parsedClustering.success) { | |
| console.log('[MLPredictions] Restored clustering result from sessionStorage'); | |
| if (isMounted) { | |
| setClusteringResult(parsedClustering); | |
| setLearningType('unsupervised'); | |
| setClusterActiveTab('overview'); | |
| } | |
| if (isMounted) setLoading(false); | |
| return; | |
| } | |
| } catch (e) { } | |
| } | |
| // Priority 2: Try loading from backend FIRST (persisted across logout/refresh) | |
| // This ensures data survives logout and browser refresh | |
| try { | |
| const backendResponse = await fetch(`/api/v1/automl/saved-result?user_id=${userId}&mode=auto`, { | |
| signal: abortCtrl.signal, | |
| headers: getAuthHeadersSync() | |
| }); | |
| const backendResult = await backendResponse.json(); | |
| if (backendResult.success && backendResult.best_model) { | |
| console.log('[MLPredictions] Loaded from backend API (persisted data)'); | |
| const resultWithCharts = await addChartsFromStorage(backendResult); | |
| if (!isMounted) return; | |
| setResult(resultWithCharts); | |
| setLearningType('supervised'); | |
| setActiveTab('overview'); // Reset to overview when loading results | |
| // Cache to localStorage and sessionStorage | |
| try { | |
| localStorage.setItem(`mlResults_${userId}`, JSON.stringify(backendResult)); | |
| localStorage.setItem(`hasMLResults_${userId}`, 'true'); | |
| sessionStorage.setItem(`mlResultsSession_${userId}`, JSON.stringify(backendResult)); | |
| } catch (e) {} | |
| if (isMounted) setLoading(false); | |
| return; | |
| } | |
| } catch (err) { | |
| if ((err as Error).name === 'AbortError') return; // Ignore abort errors | |
| console.warn('[MLPredictions] Backend API not available, trying localStorage:', err); | |
| } | |
| // Priority 3: SessionStorage (current browser session - navigation within app) | |
| const sessionSaved = sessionStorage.getItem(`mlResultsSession_${userId}`); | |
| if (sessionSaved) { | |
| try { | |
| let parsedSession = JSON.parse(sessionSaved); | |
| // Don't sync - use session results as-is (they're from current session) | |
| const resultWithCharts = await addChartsFromStorage(parsedSession); | |
| if (!isMounted) return; | |
| setResult(resultWithCharts); | |
| setLearningType('supervised'); | |
| setActiveTab('overview'); // Reset to overview when loading results | |
| setLoading(false); | |
| return; | |
| } catch (e) { } | |
| } | |
| // Priority 4: User-specific localStorage (sync with active model) | |
| const saved = localStorage.getItem(`mlResults_${userId}`); | |
| if (saved) { | |
| try { | |
| let parsedSaved = JSON.parse(saved); | |
| parsedSaved = await syncWithActiveModel(parsedSaved); | |
| const resultWithCharts = await addChartsFromStorage(parsedSaved); | |
| if (!isMounted) return; | |
| setResult(resultWithCharts); | |
| setLearningType('supervised'); | |
| setActiveTab('overview'); // Reset to overview when loading results | |
| setLoading(false); | |
| return; | |
| } catch (e) { } | |
| } | |
| // Priority 5: Legacy migration (sync with active model) | |
| const legacySaved = localStorage.getItem('mlResults'); | |
| if (legacySaved) { | |
| try { | |
| let parsed = JSON.parse(legacySaved); | |
| parsed = await syncWithActiveModel(parsed); | |
| const resultWithCharts = await addChartsFromStorage(parsed); | |
| if (!isMounted) return; | |
| setResult(resultWithCharts); | |
| setLearningType('supervised'); | |
| setActiveTab('overview'); // Reset to overview when loading results | |
| localStorage.setItem(`mlResults_${userId}`, legacySaved); | |
| localStorage.setItem(`hasMLResults_${userId}`, 'true'); | |
| setLoading(false); | |
| return; | |
| } catch (e) { } | |
| } | |
| // No results - load existing files | |
| await loadExistingFiles(); | |
| if (isMounted) setLoading(false); | |
| }; | |
| loadResults(); | |
| // Only override activeTab if explicitly passed in location state | |
| // Otherwise, keep the 'overview' that was set during loadResults | |
| if (location.state?.activeTab) { | |
| setActiveTab(location.state.activeTab); | |
| } else { | |
| // Reset to overview when navigating to page without explicit tab | |
| setActiveTab('overview'); | |
| } | |
| // Cleanup function to prevent memory leaks | |
| return () => { | |
| isMounted = false; | |
| abortCtrl.abort(); | |
| }; | |
| }, [location.state, location.key]); | |
| // 🔄 Refresh overview when model changes (rollback/delete) | |
| const handleModelChange = async () => { | |
| const userId = getUserIdSync(); | |
| try { | |
| const response = await fetch(`/api/v2/autonomous/models/${userId}`, { | |
| headers: getAuthHeadersSync() | |
| }); | |
| const data = await response.json(); | |
| if (data.success && data.models && data.models.length > 0) { | |
| const activeModel = data.models.find((m: any) => m.is_active); | |
| if (activeModel && result) { | |
| setResult({ | |
| ...result, | |
| best_model: { | |
| name: activeModel.model_name, | |
| metrics: activeModel.metrics || {} | |
| }, | |
| target_column: activeModel.target_column, | |
| task_type: activeModel.task_type, | |
| feature_columns: activeModel.feature_columns || result.feature_columns | |
| }); | |
| } | |
| } | |
| } catch (err) { | |
| console.warn('[MLPredictions] Could not refresh after model change:', err); | |
| } | |
| }; | |
| const getMetricColor = (value: number) => { | |
| if (value >= 0.9) return '#10b981'; | |
| if (value >= 0.7) return '#f59e0b'; | |
| return '#ef4444'; | |
| }; | |
| // Fetch columns from existing file - SAME AS DATAHUB | |
| const fetchColumnsFromFile = async (fileName: string) => { | |
| try { | |
| const userId = getUserIdSync(); | |
| // Fetch parsed columns safely using pandas on the backend (handles .csv and .xlsx) | |
| const fileResponse = await fetch(`/api/v1/files/${userId}/${fileName}/columns`, { | |
| headers: getAuthHeadersSync() | |
| }); | |
| if (!fileResponse.ok) return; | |
| const resData = await fileResponse.json(); | |
| if (resData.success && resData.columns) { | |
| const columns = resData.columns; | |
| setAvailableColumns(columns); | |
| const detected = detectTargetColumn(columns); | |
| setTargetColumn(detected); | |
| console.log(`📊 Detected columns: ${columns.length}, Target: ${detected}`); | |
| } else { | |
| console.warn('Failed to parse columns from backend:', resData.error); | |
| } | |
| } catch (error) { | |
| console.error('Failed to fetch columns:', error); | |
| } | |
| }; | |
| // Select existing file(s) | |
| const handleSelectFile = async (file: FileItem) => { | |
| let newSelected: FileItem[]; | |
| if (selectedFiles.some(f => f.id === file.id)) { | |
| // Deselect | |
| newSelected = selectedFiles.filter(f => f.id !== file.id); | |
| } else { | |
| // Select (max 5) | |
| if (selectedFiles.length >= 5) { | |
| toast.error('You can only select up to 5 datasets.'); | |
| return; | |
| } | |
| newSelected = [...selectedFiles, file]; | |
| } | |
| setSelectedFiles(newSelected); | |
| if (newSelected.length > 0) { | |
| await fetchColumnsFromFile(newSelected[0].name); | |
| } else { | |
| setAvailableColumns([]); | |
| setTargetColumn(''); | |
| } | |
| }; | |
| // Training handler - Updated for ML Type support | |
| const handleRunAutoML = async () => { | |
| if (selectedFiles.length === 0) { | |
| toast.error('Please select at least one data file first.'); | |
| return; | |
| } | |
| setTraining(true); | |
| // Create abort controller for "Stop" functionality | |
| const controller = new AbortController(); | |
| setAbortController(controller); | |
| try { | |
| // Get the files from server and send to AutoML - SAME AS DATAHUB | |
| const userId = getUserIdSync(); | |
| const formData = new FormData(); | |
| for (const f of selectedFiles) { | |
| const fileResponse = await fetch(`/api/v1/files/${userId}/${f.name}/download`, { | |
| signal: controller.signal, | |
| headers: getAuthHeadersSync() | |
| }); | |
| if (!fileResponse.ok) throw new Error(`Failed to get file ${f.name}`); | |
| const fileBlob = await fileResponse.blob(); | |
| formData.append('files', fileBlob, f.name); | |
| } | |
| formData.append('user_id', userId); | |
| // Add target column if selected/detected | |
| if (targetColumn) { | |
| formData.append('target_column', targetColumn); | |
| } | |
| // Multi-mode training support | |
| const modes = Array.from(selectedModes); | |
| formData.append('modes', JSON.stringify(modes)); | |
| formData.append('algorithms', JSON.stringify(selectedAlgorithms)); | |
| // ️ PRODUCTION INTELLIGENCE - Built into ALL modes (leakage detection, reliability scoring) | |
| let endpoint: string; | |
| // Always use multi_mode/train endpoint - production intelligence is built in | |
| endpoint = '/api/v2/automl/multi_mode/train'; | |
| // Pass selected algorithms for each mode | |
| formData.append('selected_traditional', JSON.stringify(selectedAlgorithms.traditional)); | |
| formData.append('selected_nlp', JSON.stringify(selectedAlgorithms.nlp)); | |
| formData.append('selected_deep_learning', JSON.stringify(selectedAlgorithms.deep_learning)); | |
| // Ultra mode is OPTIONAL - only if user explicitly clicked Ultra button | |
| formData.append('ultra_mode', String(ultraMode && modes.includes('traditional'))); | |
| console.log('🚀 Training with Production Intelligence:', { | |
| modes, | |
| algorithms: selectedAlgorithms, | |
| ultraMode: ultraMode && modes.includes('traditional'), | |
| productionIntelligence: 'ENABLED (leakage detection, reliability scoring)' | |
| }); | |
| const automlResponse = await fetch(endpoint, { | |
| method: 'POST', | |
| body: formData, | |
| signal: controller.signal, | |
| headers: { | |
| 'X-User-ID': userId | |
| } | |
| }); | |
| const automlResult = await automlResponse.json(); | |
| if (automlResult.success) { | |
| // Save to localStorage with USER-SPECIFIC key for data isolation | |
| try { | |
| localStorage.setItem(`mlResults_${userId}`, JSON.stringify(automlResult)); | |
| localStorage.setItem(`hasMLResults_${userId}`, 'true'); | |
| // Persist supervised learning type (so navigation back restores correctly) | |
| localStorage.setItem(`learningType_${userId}`, 'supervised'); | |
| // Clear any stale clustering result when supervised training completes | |
| localStorage.removeItem(`clusteringResult_${userId}`); | |
| sessionStorage.removeItem(`clusteringResult_${userId}`); | |
| if (automlResult.charts) { | |
| try { | |
| sessionStorage.setItem(`mlCharts_${userId}`, JSON.stringify(automlResult.charts)); | |
| } catch (chartErr) { | |
| console.warn("Charts too large for sessionStorage"); | |
| } | |
| } | |
| } catch (e) { | |
| console.warn("Storage quota full, saving result without charts"); | |
| const { charts, ...lightResult } = automlResult; | |
| localStorage.setItem(`mlResults_${userId}`, JSON.stringify(lightResult)); | |
| localStorage.setItem(`hasMLResults_${userId}`, 'true'); | |
| } | |
| window.dispatchEvent(new CustomEvent('filesUpdated')); | |
| setResult(automlResult); | |
| } else { | |
| toast.error(`AutoML failed: ${automlResult.detail || automlResult.error || 'Unknown error'}`); | |
| } | |
| } catch (error: any) { | |
| // Handle User Stop | |
| if (error.name === 'AbortError') { | |
| return; // Silent exit on stop | |
| } | |
| console.error('AutoML error:', error); | |
| toast.error(`AutoML error: ${error.message}`); | |
| } finally { | |
| setTraining(false); | |
| setAbortController(null); | |
| } | |
| }; | |
| // Clustering handler - For UNSUPERVISED learning (no target column) | |
| const handleRunClustering = async () => { | |
| if (selectedFiles.length === 0) { | |
| toast.error('Please select a data file first.'); | |
| return; | |
| } | |
| setTraining(true); | |
| setProgressMessage('🎯 Starting Clustering Analysis...'); | |
| try { | |
| const userId = getUserIdSync(); | |
| const token = localStorage.getItem('access_token') || sessionStorage.getItem('access_token'); | |
| const fileId = selectedFiles.length > 0 ? (selectedFiles[0].id || selectedFiles[0].name) : ''; | |
| const response = await fetch('/api/v1/ml/clustering', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'X-User-ID': userId, | |
| ...(token && { 'Authorization': `Bearer ${token}` }) | |
| }, | |
| body: JSON.stringify({ | |
| file_id: fileId, | |
| user_id: userId, | |
| algorithm: clusteringAlgorithm, | |
| n_clusters: clusterCount, | |
| }) | |
| }); | |
| const clusterResult = await response.json(); | |
| if (!response.ok) { | |
| // Handle HTTP errors (4xx, 5xx) | |
| const errorMsg = clusterResult.detail || clusterResult.error || clusterResult.message || 'Server error'; | |
| toast.error(`Clustering failed: ${errorMsg}`); | |
| return; | |
| } | |
| if (clusterResult.success) { | |
| setClusteringResult(clusterResult); | |
| setClusterActiveTab('overview'); // Start with overview tab | |
| // Initialize prediction input with default values | |
| if (clusterResult.feature_columns && clusterResult.feature_stats) { | |
| const defaultInputs: Record<string, string> = {}; | |
| clusterResult.feature_columns.forEach((col: string) => { | |
| const stats = clusterResult.feature_stats[col]; | |
| if (stats) { | |
| defaultInputs[col] = stats.mean?.toFixed(2) || '0'; | |
| } | |
| }); | |
| setClusterPredictionInput(defaultInputs); | |
| } | |
| // Store clustering result WITHOUT charts to avoid localStorage quota exceeded | |
| try { | |
| const { charts, ...resultWithoutCharts } = clusterResult; | |
| localStorage.setItem(`clusteringResult_${userId}`, JSON.stringify(resultWithoutCharts)); | |
| // Persist the learning type so navigation back restores correctly | |
| localStorage.setItem(`learningType_${userId}`, 'unsupervised'); | |
| // Also save to sessionStorage for faster in-session navigation restore | |
| sessionStorage.setItem(`clusteringResult_${userId}`, JSON.stringify(resultWithoutCharts)); | |
| // Clear supervised session data so it doesn't override clustering on navigation | |
| sessionStorage.removeItem(`mlResultsSession_${userId}`); | |
| } catch (storageErr) { | |
| console.warn('Could not cache clustering result:', storageErr); | |
| } | |
| toast.success(`Found ${clusterResult.n_clusters} clusters with ${(clusterResult.silhouette_score * 100).toFixed(1)}% separation score!`); | |
| } else { | |
| toast.error(`Clustering failed: ${clusterResult.error || clusterResult.detail || 'Unknown error'}`); | |
| } | |
| } catch (error: any) { | |
| console.error('Clustering error:', error); | |
| toast.error(`Clustering error: ${error.message}`); | |
| } finally { | |
| setTraining(false); | |
| } | |
| }; | |
| // Predict which cluster a new data point belongs to | |
| const handlePredictCluster = async () => { | |
| if (!clusteringResult?.model_id) { | |
| toast.error('No clustering model available. Please run clustering first.'); | |
| return; | |
| } | |
| setClusterPredicting(true); | |
| setClusterPredictionResult(null); | |
| try { | |
| const userId = getUserIdSync(); | |
| const token = localStorage.getItem('access_token') || sessionStorage.getItem('access_token'); | |
| // Convert string inputs to numbers | |
| const features: Record<string, number> = {}; | |
| for (const [key, value] of Object.entries(clusterPredictionInput)) { | |
| features[key] = parseFloat(value) || 0; | |
| } | |
| const response = await fetch('/api/v1/ml/clustering/predict', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'X-User-ID': userId, | |
| ...(token && { 'Authorization': `Bearer ${token}` }) | |
| }, | |
| body: JSON.stringify({ | |
| user_id: userId, | |
| model_id: clusteringResult.model_id, | |
| features: features | |
| }) | |
| }); | |
| const result = await response.json(); | |
| if (result.success) { | |
| setClusterPredictionResult(result); | |
| toast.success(`Predicted: ${result.cluster_name}`); | |
| } else { | |
| toast.error(`Prediction failed: ${result.error || 'Unknown error'}`); | |
| } | |
| } catch (error: any) { | |
| console.error('Cluster prediction error:', error); | |
| toast.error(`Prediction error: ${error.message}`); | |
| } finally { | |
| setClusterPredicting(false); | |
| } | |
| }; | |
| // Combined training handler based on learning type | |
| const handleStartTraining = () => { | |
| if (learningType === 'supervised') { | |
| handleRunAutoML(); | |
| } else { | |
| handleRunClustering(); | |
| } | |
| }; | |
| // Stop Training - SAME AS DATAHUB | |
| const handleStopTraining = async () => { | |
| if (abortController) abortController.abort(); | |
| setTraining(false); | |
| // Signal Backend to Stop Permanently | |
| try { | |
| const userId = getUserIdSync(); | |
| const formData = new FormData(); | |
| formData.append('user_id', userId); | |
| await fetch('/api/v2/automl/stop_training', { | |
| method: 'POST', | |
| body: formData, | |
| headers: { | |
| 'X-User-ID': userId | |
| } | |
| }); | |
| } catch (e) { | |
| console.error("Failed to signal stop to backend", e); | |
| } | |
| }; | |
| // Animation loop for training messages - Mode-aware | |
| useEffect(() => { | |
| if (!training) return; | |
| // Get mode-specific messages | |
| const modes = Array.from(selectedModes); | |
| const isMultiMode = modes.length > 1; | |
| let messages: string[]; | |
| if (isMultiMode) { | |
| messages = [ | |
| '🚀 Starting Multi-Mode Training...', | |
| '🌲 Training Traditional ML algorithms...', | |
| '📝 Running NLP text classification...', | |
| '🧠 Building Deep Learning models...', | |
| '📊 Generating mode-specific charts...', | |
| '⚖️ Comparing cross-mode performance...', | |
| '🏆 Selecting best overall model...', | |
| ]; | |
| } else if (modes.includes('nlp')) { | |
| messages = [ | |
| '📝 Initializing NLP Pipeline...', | |
| '🧹 Preprocessing text data...', | |
| '📊 Building TF-IDF/BOW features...', | |
| '🔤 Training text classifiers...', | |
| '📈 Evaluating NLP models...', | |
| '☁️ Generating word clouds...', | |
| '📊 Creating NLP charts...', | |
| ]; | |
| } else if (modes.includes('deep_learning')) { | |
| messages = [ | |
| '🧠 Initializing Deep Learning...', | |
| '📊 Preprocessing features...', | |
| '🔧 Building neural network architectures...', | |
| '⚡ Training ANN/MLP models...', | |
| '🔄 Testing LSTM/GRU/RNN patterns...', | |
| '📈 Evaluating network performance...', | |
| '📊 Generating architecture diagrams...', | |
| ]; | |
| } else if (ultraMode) { | |
| messages = [ | |
| '🎼 Initializing Ultra AutoML...', | |
| '📊 Analyzing Dataset Profile...', | |
| '🎯 Meta-Learning Recommendations...', | |
| '🔬 Synthesizing 50+ Features...', | |
| '🤖 Training Classical Models...', | |
| '🧠 Training Neural Networks...', | |
| '📈 Optimizing Hyperparameters...', | |
| '⚖️ Building Ultra Ensemble...', | |
| '🔮 Generating Explainability...', | |
| ]; | |
| } else { | |
| messages = [ | |
| '🧹 Cleaning Data (Phase 1/4)...', | |
| '🛠️ Engineering Features (Phase 2/4)...', | |
| '🤖 Training 15+ Models (Phase 3/4)...', | |
| '📈 Optimizing Hyperparameters...', | |
| '⚖️ Building Ensembles...', | |
| '📊 Generating High-Res Charts...' | |
| ]; | |
| } | |
| let i = 0; | |
| const interval = setInterval(() => { | |
| setProgressMessage(messages[i % messages.length]); | |
| i++; | |
| }, 3500); | |
| return () => clearInterval(interval); | |
| }, [training, ultraMode, selectedModes]); | |
| // Check if data files exist | |
| const hasDataFiles = existingFiles.length > 0; | |
| // Show loading spinner | |
| if (loading) { | |
| return ( | |
| <div className="flex flex-col items-center justify-center min-h-[60vh]"> | |
| <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="text-center"> | |
| <RefreshCw className="w-16 h-16 mx-auto mb-4 animate-spin" style={{ color: isDark ? '#4ade80' : '#16a34a' }} /> | |
| <h2 className="text-xl font-semibold" style={{ color: 'var(--text-primary)' }}>Loading ML Results...</h2> | |
| </motion.div> | |
| </div> | |
| ); | |
| } | |
| // ======================================================================== | |
| // NO RESULTS - SHOW TRAINING INTERFACE (SAME AS DATAHUB) | |
| // ======================================================================== | |
| if (!result && !clusteringResult) { | |
| return ( | |
| <div className="space-y-6"> | |
| {/* TRAINING OVERLAY - Mode-Aware for Traditional ML, NLP, Deep Learning, Combined */} | |
| {training && (() => { | |
| // Determine training mode display config | |
| const modes = Array.from(selectedModes); | |
| const isMultiMode = modes.length > 1; | |
| const isNlpOnly = modes.length === 1 && modes[0] === 'nlp'; | |
| const isDlOnly = modes.length === 1 && modes[0] === 'deep_learning'; | |
| const isTraditionalOnly = modes.length === 1 && modes[0] === 'traditional'; | |
| const isAutoMode = selectedAlgorithms.traditional.includes('auto'); | |
| // Get mode-specific colors and labels | |
| let gradientClass = 'bg-gradient-to-r from-emerald-400 via-green-400 to-cyan-400'; | |
| let bgGlow = 'bg-green-500/20'; | |
| let borderOuter = 'border-t-green-400 border-r-green-400/50 border-b-green-400/20 border-l-green-400/50'; | |
| let borderInner = 'border-b-blue-400 border-l-blue-400/50 border-t-blue-400/20 border-r-blue-400/50'; | |
| let badgeBg = 'bg-green-500/20 text-green-300'; | |
| let borderCard = 'border-green-500/30'; | |
| let title = '🚀 Fast ML Training'; | |
| let timeLabel = '⏱️ Fast mode: 1-3 minutes for quick results.'; | |
| let badges = [{ text: '10 Core Algorithms' }, { text: 'Quick Training' }]; | |
| // PRIORITY ORDER: Check specific modes FIRST, then Fast/Ultra for traditional auto | |
| if (isMultiMode) { | |
| // Combined multi-mode training | |
| gradientClass = 'bg-gradient-to-r from-indigo-400 via-purple-400 to-pink-400'; | |
| bgGlow = 'bg-indigo-500/20'; | |
| borderOuter = 'border-t-indigo-400 border-r-indigo-400/50 border-b-indigo-400/20 border-l-indigo-400/50'; | |
| borderInner = 'border-b-pink-400 border-l-pink-400/50 border-t-pink-400/20 border-r-pink-400/50'; | |
| badgeBg = 'bg-indigo-500/20 text-indigo-300'; | |
| borderCard = 'border-indigo-500/30'; | |
| title = '🔀 Combined ML Training'; | |
| timeLabel = '⏱️ Multi-mode: 5-15 minutes for comprehensive analysis.'; | |
| badges = modes.map(m => ({ | |
| text: m === 'traditional' ? 'Traditional ML' : m === 'nlp' ? 'NLP' : 'Deep Learning' | |
| })); | |
| } else if (isNlpOnly) { | |
| // NLP only mode | |
| gradientClass = 'bg-gradient-to-r from-blue-400 via-cyan-400 to-teal-400'; | |
| bgGlow = 'bg-blue-500/20'; | |
| borderOuter = 'border-t-blue-400 border-r-blue-400/50 border-b-blue-400/20 border-l-blue-400/50'; | |
| borderInner = 'border-b-cyan-400 border-l-cyan-400/50 border-t-cyan-400/20 border-r-cyan-400/50'; | |
| badgeBg = 'bg-blue-500/20 text-blue-300'; | |
| borderCard = 'border-blue-500/30'; | |
| title = '📝 NLP Training'; | |
| timeLabel = '⏱️ NLP mode: 2-5 minutes for text classification.'; | |
| badges = [{ text: 'TF-IDF/BOW' }, { text: 'Word Embeddings' }, { text: 'Text Classification' }]; | |
| } else if (isDlOnly) { | |
| // Deep Learning only mode | |
| gradientClass = 'bg-gradient-to-r from-red-400 via-orange-400 to-amber-400'; | |
| bgGlow = 'bg-red-500/20'; | |
| borderOuter = 'border-t-red-400 border-r-red-400/50 border-b-red-400/20 border-l-red-400/50'; | |
| borderInner = 'border-b-orange-400 border-l-orange-400/50 border-t-orange-400/20 border-r-orange-400/50'; | |
| badgeBg = 'bg-red-500/20 text-red-300'; | |
| borderCard = 'border-red-500/30'; | |
| title = '🧠 Deep Learning Training'; | |
| timeLabel = '⏱️ Deep Learning: 3-10 minutes for neural networks.'; | |
| badges = [{ text: 'Neural Networks' }, { text: 'MLP/ANN' }, { text: 'Auto Architecture' }]; | |
| } else if (isTraditionalOnly && !isAutoMode) { | |
| // User selected SPECIFIC algorithms (not auto) - Traditional ML with custom selection | |
| gradientClass = 'bg-gradient-to-r from-amber-400 via-yellow-400 to-lime-400'; | |
| bgGlow = 'bg-amber-500/20'; | |
| borderOuter = 'border-t-amber-400 border-r-amber-400/50 border-b-amber-400/20 border-l-amber-400/50'; | |
| borderInner = 'border-b-lime-400 border-l-lime-400/50 border-t-lime-400/20 border-r-lime-400/50'; | |
| badgeBg = 'bg-amber-500/20 text-amber-300'; | |
| borderCard = 'border-amber-500/30'; | |
| title = '🌲 Traditional ML Training'; | |
| timeLabel = `⏱️ Training ${selectedAlgorithms.traditional.length} selected algorithm(s).`; | |
| badges = [{ text: `${selectedAlgorithms.traditional.length} Algorithm(s)` }, { text: 'Custom Selection' }]; | |
| } else if (isTraditionalOnly && isAutoMode && ultraMode) { | |
| // Traditional with AUTO and ULTRA mode explicitly selected | |
| gradientClass = 'bg-gradient-to-r from-purple-400 via-pink-400 to-rose-400'; | |
| bgGlow = 'bg-purple-500/20'; | |
| borderOuter = 'border-t-purple-400 border-r-purple-400/50 border-b-purple-400/20 border-l-purple-400/50'; | |
| borderInner = 'border-b-pink-400 border-l-pink-400/50 border-t-pink-400/20 border-r-pink-400/50'; | |
| badgeBg = 'bg-purple-500/20 text-purple-300'; | |
| borderCard = 'border-purple-500/30'; | |
| title = '🎼 Ultra AutoML Training'; | |
| timeLabel = '⏱️ Ultra mode: 3-8 minutes for maximum accuracy.'; | |
| badges = [{ text: '15+ Algorithms' }, { text: 'Ensembles' }, { text: 'Auto-Tuning' }]; | |
| } | |
| // else: Default is Fast ML Training (already set above) | |
| return ( | |
| <div | |
| className="fixed inset-0 z-[100] flex items-center justify-center transition-all duration-500" | |
| style={{ backgroundColor: 'var(--glass-overlay)' }} | |
| > | |
| <motion.div | |
| initial={{ scale: 0.9, opacity: 0, y: 20 }} | |
| animate={{ scale: 1, opacity: 1, y: 0 }} | |
| className="flex flex-col items-center max-w-lg w-full p-8 text-center" | |
| > | |
| {/* Big Animated Icon - Mode Aware Colors */} | |
| <div className="relative w-40 h-40 mb-8 flex items-center justify-center"> | |
| <div className={`absolute inset-0 blur-2xl rounded-full animate-pulse ${bgGlow}`}></div> | |
| <div className={`absolute inset-0 border-4 rounded-full animate-spin ${borderOuter}`}></div> | |
| <div className={`absolute inset-4 border-4 rounded-full animate-spin ${borderInner}`} style={{ animationDirection: 'reverse', animationDuration: '3s' }}></div> | |
| <Brain className="w-16 h-16 relative z-10 animate-pulse" style={{ color: 'var(--text-primary)' }} /> | |
| </div> | |
| {/* Title - Mode Aware */} | |
| <h2 className={`text-4xl font-bold bg-clip-text text-transparent mb-4 ${gradientClass}`}> | |
| {title} | |
| </h2> | |
| {/* Mode Details */} | |
| <div className={`flex flex-wrap items-center justify-center gap-2 mb-4 px-4 py-2 rounded-full ${badgeBg}`}> | |
| {badges.map((badge, idx) => ( | |
| <React.Fragment key={idx}> | |
| {idx > 0 && <span className="opacity-50">•</span>} | |
| <span className="text-sm font-medium">{badge.text}</span> | |
| </React.Fragment> | |
| ))} | |
| </div> | |
| {/* Progress Card */} | |
| <div | |
| className={`backdrop-blur border rounded-2xl p-6 w-full mb-8 shadow-2xl ${borderCard}`} | |
| style={{ backgroundColor: 'var(--bg-card)' }} | |
| > | |
| <p className="text-xl font-medium mb-2" style={{ color: 'var(--text-primary)' }}> | |
| {progressMessage} | |
| </p> | |
| <p className="text-sm" style={{ color: 'var(--text-muted)' }}> | |
| {timeLabel} | |
| </p> | |
| </div> | |
| <button | |
| onClick={handleStopTraining} | |
| className="group px-8 py-4 bg-red-500/10 border border-red-500/30 rounded-2xl text-red-400 font-bold hover:bg-red-500/20 hover:border-red-500/50 transition-all flex items-center gap-3" | |
| > | |
| <XCircle className="w-6 h-6 group-hover:scale-110 transition-transform" /> | |
| STOP TRAINING | |
| </button> | |
| </motion.div> | |
| </div> | |
| ); | |
| })()} | |
| {/* Header - SAME STYLE AS DATAHUB */} | |
| <motion.div initial={{ opacity: 0, y: -20 }} animate={{ opacity: 1, y: 0 }} className="flex flex-col md:flex-row md:items-center justify-between gap-4"> | |
| <div> | |
| <h1 className="text-2xl font-bold" style={{ color: 'var(--text-primary)' }}>ML Predictions</h1> | |
| <p className="text-sm" style={{ color: 'var(--text-muted)' }}>Train ML models on your data files</p> | |
| </div> | |
| {/* 🤖 ML Train Button - Shows when data files exist - SAME AS DATAHUB */} | |
| {hasDataFiles && selectedFiles.length > 0 && ( | |
| <div className="flex items-center gap-2 w-full md:w-auto"> | |
| {/* For Supervised: Show Fast/Ultra toggle ONLY when 'auto' is selected */} | |
| {/* Hide when user has manually selected specific algorithms */} | |
| {/* Production Intelligence (leakage detection, reliability scoring) is built into ALL modes */} | |
| {learningType === 'supervised' && mlType === 'traditional' && | |
| selectedAlgorithms.traditional.includes('auto') && ( | |
| <div className="inline-flex p-1 rounded-xl border" style={{ backgroundColor: 'var(--bg-secondary)', borderColor: 'var(--border-color)' }}> | |
| <button | |
| onClick={() => setUltraMode(false)} | |
| disabled={training} | |
| className={`px-4 py-2 rounded-lg font-medium text-sm transition-all flex items-center gap-2 ${!ultraMode | |
| ? 'bg-gradient-to-r from-amber-500 to-orange-500 text-white shadow-lg' | |
| : '' | |
| }`} | |
| style={ultraMode ? { color: 'var(--text-muted)' } : undefined} | |
| title="Fast Mode: 10 models with Production Intelligence" | |
| > | |
| <Zap className="w-4 h-4" /> | |
| Fast | |
| </button> | |
| <button | |
| onClick={() => setUltraMode(true)} | |
| disabled={training} | |
| className={`px-4 py-2 rounded-lg font-medium text-sm transition-all flex items-center gap-2 ${ultraMode | |
| ? 'bg-gradient-to-r from-purple-500 to-pink-500 text-white shadow-lg' | |
| : '' | |
| }`} | |
| style={!ultraMode ? { color: 'var(--text-muted)' } : undefined} | |
| title="Ultra Mode: 25+ models with Production Intelligence" | |
| > | |
| <Sparkles className="w-4 h-4" /> | |
| Ultra | |
| </button> | |
| {/* Production Intelligence indicator */} | |
| <div className="flex items-center gap-1 px-3 py-2 text-xs font-medium text-green-400" title="Production Intelligence: Leakage Detection, Reliability Scoring, Overfitting Prevention"> | |
| <ShieldCheck className="w-4 h-4" /> | |
| <span className="hidden sm:inline">Protected</span> | |
| </div> | |
| </div> | |
| )} | |
| {/* Train Button */} | |
| <button | |
| onClick={handleStartTraining} | |
| disabled={training} | |
| className={`btn-primary flex-1 md:flex-none rounded-full flex items-center justify-center gap-2 px-6 py-2.5 ${learningType === 'unsupervised' | |
| ? 'bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-500 hover:to-pink-500' | |
| : mlType === 'nlp' | |
| ? 'bg-gradient-to-r from-blue-600 to-cyan-600 hover:from-blue-500 hover:to-cyan-500' | |
| : mlType === 'deep_learning' | |
| ? 'bg-gradient-to-r from-red-600 to-orange-600 hover:from-red-500 hover:to-orange-500' | |
| : ultraMode | |
| ? 'bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-500 hover:to-pink-500' | |
| : 'bg-gradient-to-r from-emerald-600 to-green-600 hover:from-emerald-500 hover:to-green-500' | |
| } text-white font-medium ${training ? 'opacity-60 cursor-wait' : ''}`} | |
| > | |
| {training ? ( | |
| <> | |
| <div className="loading-spinner" /> | |
| <span className="whitespace-nowrap"> | |
| {learningType === 'unsupervised' ? 'Clustering...' : | |
| mlType === 'nlp' ? 'NLP Training...' : | |
| mlType === 'deep_learning' ? 'Deep Learning...' : | |
| ultraMode ? 'Ultra Training...' : 'Training...'} | |
| </span> | |
| </> | |
| ) : ( | |
| <> | |
| {learningType === 'unsupervised' ? ( | |
| <> | |
| <Boxes className="w-5 h-5" /> | |
| <span className="whitespace-nowrap">🎯 Cluster</span> | |
| </> | |
| ) : mlType === 'nlp' ? ( | |
| <> | |
| <FileText className="w-5 h-5" /> | |
| <span className="whitespace-nowrap">📝 NLP Train</span> | |
| </> | |
| ) : mlType === 'deep_learning' ? ( | |
| <> | |
| <Brain className="w-5 h-5" /> | |
| <span className="whitespace-nowrap">🧠 Deep Learning</span> | |
| </> | |
| ) : ( | |
| <> | |
| <Brain className="w-5 h-5" /> | |
| <span className="whitespace-nowrap">{ultraMode ? '🧠 Ultra ML' : '🚀 Fast ML'}</span> | |
| </> | |
| )} | |
| </> | |
| )} | |
| </button> | |
| </div> | |
| )} | |
| </motion.div> | |
| {/* File Selection Card */} | |
| <motion.div | |
| initial={{ opacity: 0, y: 20 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| transition={{ delay: 0.1 }} | |
| className="p-6 rounded-2xl border" | |
| style={{ backgroundColor: 'var(--bg-card)', borderColor: 'var(--border-color)' }} | |
| > | |
| <div className="flex items-center gap-3 mb-4"> | |
| <div className="p-2 rounded-xl bg-blue-500/20"> | |
| <Database className="w-5 h-5 text-blue-400" /> | |
| </div> | |
| <div> | |
| <h2 className="font-semibold" style={{ color: 'var(--text-primary)' }}>Your Data Files</h2> | |
| <p className="text-sm" style={{ color: 'var(--text-muted)' }}> | |
| {existingFiles.length > 0 | |
| ? `${existingFiles.length} file(s) available from DataHub` | |
| : 'No files found - Upload files in DataHub first'} | |
| </p> | |
| </div> | |
| </div> | |
| {existingFiles.length > 0 ? ( | |
| <div className="grid grid-cols-1 md:grid-cols-2 gap-3"> | |
| {existingFiles.map((file) => ( | |
| <button | |
| key={file.id} | |
| onClick={() => handleSelectFile(file)} | |
| disabled={training} | |
| className="p-4 rounded-xl border text-left transition-all flex items-center gap-3 hover:border-emerald-500/50" | |
| style={{ | |
| backgroundColor: selectedFiles.some(f => f.id === file.id) | |
| ? (isDark ? 'rgba(16, 185, 129, 0.15)' : 'rgba(16, 185, 129, 0.1)') | |
| : (isDark ? 'rgba(255,255,255,0.03)' : 'rgba(0,0,0,0.02)'), | |
| borderColor: selectedFiles.some(f => f.id === file.id) ? '#10b981' : 'var(--border-color)' | |
| }} | |
| > | |
| <FileText className="w-6 h-6 flex-shrink-0" style={{ color: selectedFiles.some(f => f.id === file.id) ? '#10b981' : 'var(--text-muted)' }} /> | |
| <div className="flex-1 min-w-0"> | |
| <p className="font-medium truncate" style={{ color: 'var(--text-primary)' }}>{file.name}</p> | |
| <p className="text-xs" style={{ color: 'var(--text-muted)' }}>{(file.size / 1024).toFixed(1)} KB</p> | |
| </div> | |
| {selectedFiles.some(f => f.id === file.id) && <CheckCircle className="w-5 h-5 flex-shrink-0" style={{ color: '#10b981' }} />} | |
| </button> | |
| ))} | |
| </div> | |
| ) : ( | |
| <div className="text-center py-8"> | |
| <Database className="w-12 h-12 mx-auto mb-3 opacity-50" style={{ color: 'var(--text-muted)' }} /> | |
| <p style={{ color: 'var(--text-muted)' }}>No data files found</p> | |
| <button | |
| onClick={() => navigate('/data-hub')} | |
| className="mt-4 px-4 py-2 bg-primary-500 text-white rounded-lg hover:bg-primary-600 transition-colors" | |
| > | |
| Go to DataHub to upload files | |
| </button> | |
| </div> | |
| )} | |
| </motion.div> | |
| {/* Learning Type Selection - Supervised vs Unsupervised */} | |
| {availableColumns.length > 0 && ( | |
| <motion.div | |
| initial={{ opacity: 0, y: 20 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| transition={{ delay: 0.12 }} | |
| className="p-4 rounded-2xl border" | |
| style={{ backgroundColor: 'var(--bg-card)', borderColor: 'var(--border-color)' }} | |
| > | |
| <div className="flex items-center gap-3 mb-4"> | |
| <div className="p-2 rounded-xl bg-indigo-500/20"> | |
| <GitBranch className="w-5 h-5 text-indigo-400" /> | |
| </div> | |
| <div> | |
| <p className="font-semibold" style={{ color: 'var(--text-primary)' }}> | |
| Learning Type | |
| </p> | |
| <p className="text-sm" style={{ color: 'var(--text-muted)' }}> | |
| Choose how the AI should learn from your data | |
| </p> | |
| </div> | |
| </div> | |
| <div className="grid grid-cols-1 md:grid-cols-2 gap-3"> | |
| {/* Supervised Learning */} | |
| <button | |
| onClick={() => setLearningType('supervised')} | |
| disabled={training} | |
| className="p-4 rounded-xl border text-left transition-all" | |
| style={{ | |
| backgroundColor: learningType === 'supervised' | |
| ? (isDark ? 'rgba(34, 197, 94, 0.15)' : 'rgba(34, 197, 94, 0.1)') | |
| : (isDark ? 'rgba(255,255,255,0.03)' : 'rgba(0,0,0,0.02)'), | |
| borderColor: learningType === 'supervised' ? '#22c55e' : 'var(--border-color)' | |
| }} | |
| > | |
| <div className="flex items-center gap-3 mb-2"> | |
| <Target className="w-6 h-6" style={{ color: learningType === 'supervised' ? '#22c55e' : 'var(--text-muted)' }} /> | |
| <span className="font-semibold" style={{ color: 'var(--text-primary)' }}> | |
| Supervised Learning | |
| </span> | |
| {learningType === 'supervised' && <CheckCircle className="w-5 h-5 ml-auto" style={{ color: '#22c55e' }} />} | |
| </div> | |
| <p className="text-sm" style={{ color: 'var(--text-muted)' }}> | |
| <strong>Predict a target column</strong> - Train AI to predict outcomes like price, category, churn, fraud. | |
| </p> | |
| <div className="flex flex-wrap gap-1 mt-2"> | |
| <span className="text-xs px-2 py-0.5 rounded-full bg-green-500/20 text-green-400">Classification</span> | |
| <span className="text-xs px-2 py-0.5 rounded-full bg-blue-500/20 text-blue-400">Regression</span> | |
| </div> | |
| </button> | |
| {/* Unsupervised Learning */} | |
| <button | |
| onClick={() => setLearningType('unsupervised')} | |
| disabled={training} | |
| className="p-4 rounded-xl border text-left transition-all" | |
| style={{ | |
| backgroundColor: learningType === 'unsupervised' | |
| ? (isDark ? 'rgba(168, 85, 247, 0.15)' : 'rgba(168, 85, 247, 0.1)') | |
| : (isDark ? 'rgba(255,255,255,0.03)' : 'rgba(0,0,0,0.02)'), | |
| borderColor: learningType === 'unsupervised' ? '#a855f7' : 'var(--border-color)' | |
| }} | |
| > | |
| <div className="flex items-center gap-3 mb-2"> | |
| <Boxes className="w-6 h-6" style={{ color: learningType === 'unsupervised' ? '#a855f7' : 'var(--text-muted)' }} /> | |
| <span className="font-semibold" style={{ color: 'var(--text-primary)' }}> | |
| Unsupervised Learning | |
| </span> | |
| {learningType === 'unsupervised' && <CheckCircle className="w-5 h-5 ml-auto" style={{ color: '#a855f7' }} />} | |
| </div> | |
| <p className="text-sm" style={{ color: 'var(--text-muted)' }}> | |
| <strong>Find patterns & groups</strong> - Discover customer segments, product clusters, anomalies automatically. | |
| </p> | |
| <div className="flex flex-wrap gap-1 mt-2"> | |
| <span className="text-xs px-2 py-0.5 rounded-full bg-purple-500/20 text-purple-400">Clustering</span> | |
| <span className="text-xs px-2 py-0.5 rounded-full bg-pink-500/20 text-pink-400">Segmentation</span> | |
| </div> | |
| </button> | |
| </div> | |
| </motion.div> | |
| )} | |
| {/* Supervised: ML Type Selector (Traditional/NLP/Deep Learning) */} | |
| {learningType === 'supervised' && availableColumns.length > 0 && ( | |
| <motion.div | |
| initial={{ opacity: 0, y: 20 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| transition={{ delay: 0.12 }} | |
| className="p-4 rounded-2xl border" | |
| style={{ backgroundColor: 'var(--bg-card)', borderColor: 'var(--border-color)' }} | |
| > | |
| <div className="flex items-center justify-between mb-4"> | |
| <div className="flex items-center gap-3"> | |
| <div className="p-2 rounded-xl bg-gradient-to-br from-green-500/20 to-emerald-500/20"> | |
| <Brain className="w-5 h-5 text-green-400" /> | |
| </div> | |
| <div> | |
| <p className="font-semibold" style={{ color: 'var(--text-primary)' }}> | |
| 🤖 Select ML Modes (Multi-Select) | |
| </p> | |
| <p className="text-sm" style={{ color: 'var(--text-muted)' }}> | |
| Combine modes for hybrid predictions • Click to toggle | |
| </p> | |
| </div> | |
| </div> | |
| <div className="text-xs px-2 py-1 rounded-full bg-green-500/20 text-green-400"> | |
| {selectedModes.size} mode{selectedModes.size > 1 ? 's' : ''} selected | |
| </div> | |
| </div> | |
| {/* ML Mode Cards - Multi-Select */} | |
| <div className="grid grid-cols-1 md:grid-cols-3 gap-3 mb-4"> | |
| {/* Traditional ML */} | |
| <button | |
| onClick={() => toggleMode('traditional')} | |
| disabled={training} | |
| className="p-4 rounded-xl border text-left transition-all relative" | |
| style={{ | |
| backgroundColor: selectedModes.has('traditional') | |
| ? (isDark ? 'rgba(34, 197, 94, 0.15)' : 'rgba(34, 197, 94, 0.1)') | |
| : (isDark ? 'rgba(255,255,255,0.03)' : 'rgba(0,0,0,0.02)'), | |
| borderColor: selectedModes.has('traditional') ? '#22c55e' : 'var(--border-color)' | |
| }} | |
| > | |
| {selectedModes.has('traditional') && ( | |
| <div className="absolute top-2 right-2"> | |
| <CheckCircle className="w-5 h-5 text-green-500" /> | |
| </div> | |
| )} | |
| <div className="flex items-center gap-2 mb-2"> | |
| <BarChart3 className="w-5 h-5" style={{ color: selectedModes.has('traditional') ? '#22c55e' : 'var(--text-muted)' }} /> | |
| <span className="font-semibold" style={{ color: 'var(--text-primary)' }}>Traditional ML</span> | |
| </div> | |
| <p className="text-xs mb-2" style={{ color: 'var(--text-muted)' }}> | |
| {algorithmOptions.traditional.length - 1} algorithms: RF, XGBoost, SVM... | |
| </p> | |
| <div className="text-xs px-2 py-0.5 rounded bg-green-500/10 text-green-400 inline-block"> | |
| {selectedAlgorithms.traditional.includes('auto') ? 'Auto' : `${selectedAlgorithms.traditional.length} selected`} | |
| </div> | |
| </button> | |
| {/* NLP */} | |
| <button | |
| onClick={() => toggleMode('nlp')} | |
| disabled={training} | |
| className="p-4 rounded-xl border text-left transition-all relative" | |
| style={{ | |
| backgroundColor: selectedModes.has('nlp') | |
| ? (isDark ? 'rgba(59, 130, 246, 0.15)' : 'rgba(59, 130, 246, 0.1)') | |
| : (isDark ? 'rgba(255,255,255,0.03)' : 'rgba(0,0,0,0.02)'), | |
| borderColor: selectedModes.has('nlp') ? '#3b82f6' : 'var(--border-color)' | |
| }} | |
| > | |
| {selectedModes.has('nlp') && ( | |
| <div className="absolute top-2 right-2"> | |
| <CheckCircle className="w-5 h-5 text-blue-500" /> | |
| </div> | |
| )} | |
| <div className="flex items-center gap-2 mb-2"> | |
| <FileText className="w-5 h-5" style={{ color: selectedModes.has('nlp') ? '#3b82f6' : 'var(--text-muted)' }} /> | |
| <span className="font-semibold" style={{ color: 'var(--text-primary)' }}>NLP</span> | |
| </div> | |
| <p className="text-xs mb-2" style={{ color: 'var(--text-muted)' }}> | |
| {algorithmOptions.nlp.length - 1} techniques: TF-IDF, Word2Vec... | |
| </p> | |
| <div className="text-xs px-2 py-0.5 rounded bg-blue-500/10 text-blue-400 inline-block"> | |
| {selectedAlgorithms.nlp.includes('auto') ? 'Auto' : `${selectedAlgorithms.nlp.length} selected`} | |
| </div> | |
| </button> | |
| {/* Deep Learning */} | |
| <button | |
| onClick={() => toggleMode('deep_learning')} | |
| disabled={training} | |
| className="p-4 rounded-xl border text-left transition-all relative" | |
| style={{ | |
| backgroundColor: selectedModes.has('deep_learning') | |
| ? (isDark ? 'rgba(239, 68, 68, 0.15)' : 'rgba(239, 68, 68, 0.1)') | |
| : (isDark ? 'rgba(255,255,255,0.03)' : 'rgba(0,0,0,0.02)'), | |
| borderColor: selectedModes.has('deep_learning') ? '#ef4444' : 'var(--border-color)' | |
| }} | |
| > | |
| {selectedModes.has('deep_learning') && ( | |
| <div className="absolute top-2 right-2"> | |
| <CheckCircle className="w-5 h-5 text-red-500" /> | |
| </div> | |
| )} | |
| <div className="flex items-center gap-2 mb-2"> | |
| <Brain className="w-5 h-5" style={{ color: selectedModes.has('deep_learning') ? '#ef4444' : 'var(--text-muted)' }} /> | |
| <span className="font-semibold" style={{ color: 'var(--text-primary)' }}>Deep Learning</span> | |
| </div> | |
| <p className="text-xs mb-2" style={{ color: 'var(--text-muted)' }}> | |
| {algorithmOptions.deep_learning.length - 1} architectures: MLP, Neural Nets... | |
| </p> | |
| <div className="text-xs px-2 py-0.5 rounded bg-red-500/10 text-red-400 inline-block"> | |
| {selectedAlgorithms.deep_learning.includes('auto') ? 'Auto' : `${selectedAlgorithms.deep_learning.length} selected`} | |
| </div> | |
| </button> | |
| </div> | |
| {/* Algorithm Selection for Selected Modes */} | |
| {Array.from(selectedModes).map((mode) => ( | |
| <div key={mode} className="mb-4 p-3 rounded-xl border" style={{ borderColor: 'var(--border-color)', backgroundColor: isDark ? 'rgba(0,0,0,0.2)' : 'rgba(0,0,0,0.02)' }}> | |
| <div className="flex items-center justify-between mb-3"> | |
| <span className="font-medium text-sm" style={{ color: 'var(--text-primary)' }}> | |
| {mode === 'traditional' ? '🌲 Traditional ML Algorithms' : | |
| mode === 'nlp' ? '📝 NLP Techniques' : '🧠 Deep Learning Architectures'} | |
| </span> | |
| <div className="flex gap-2"> | |
| <button | |
| onClick={() => setSelectedAlgorithms(prev => ({ ...prev, [mode]: ['auto'] }))} | |
| className={`text-xs px-2 py-1 rounded ${selectedAlgorithms[mode].includes('auto') ? 'bg-green-500 text-white' : 'bg-gray-500/20'}`} | |
| style={{ color: selectedAlgorithms[mode].includes('auto') ? '#fff' : 'var(--text-muted)' }} | |
| > | |
| 🚀 Auto | |
| </button> | |
| <button | |
| onClick={() => selectAllAlgorithms(mode)} | |
| className="text-xs px-2 py-1 rounded bg-gray-500/20" | |
| style={{ color: 'var(--text-muted)' }} | |
| > | |
| Select All | |
| </button> | |
| </div> | |
| </div> | |
| {/* Algorithm Grid */} | |
| <div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-5 gap-2 max-h-48 overflow-y-auto"> | |
| {algorithmOptions[mode].filter(a => a.value !== 'auto').map((algo) => { | |
| const isSelected = selectedAlgorithms[mode].includes(algo.value); | |
| return ( | |
| <button | |
| key={algo.value} | |
| onClick={() => toggleAlgorithm(mode, algo.value)} | |
| disabled={training} | |
| className={`p-2 rounded-lg border text-left transition-all text-xs ${isSelected ? 'border-green-500' : ''}`} | |
| style={{ | |
| backgroundColor: isSelected | |
| ? (isDark ? 'rgba(34, 197, 94, 0.15)' : 'rgba(34, 197, 94, 0.1)') | |
| : (isDark ? 'rgba(255,255,255,0.03)' : 'rgba(0,0,0,0.02)'), | |
| borderColor: isSelected ? '#22c55e' : 'var(--border-color)' | |
| }} | |
| title={algo.description} | |
| > | |
| <div className="flex items-center gap-1"> | |
| {isSelected && <CheckCircle className="w-3 h-3 text-green-500 flex-shrink-0" />} | |
| <span className="truncate" style={{ color: 'var(--text-primary)' }}>{algo.label}</span> | |
| </div> | |
| <span className="text-[10px] block mt-0.5 opacity-60" style={{ color: 'var(--text-muted)' }}> | |
| {algo.category} | |
| </span> | |
| </button> | |
| ); | |
| })} | |
| </div> | |
| </div> | |
| ))} | |
| {/* Training Summary + Start Button */} | |
| <div className="p-4 rounded-xl border bg-gradient-to-r from-green-500/10 to-emerald-500/10" style={{ borderColor: '#22c55e' }}> | |
| <div className="flex flex-col md:flex-row md:items-center justify-between gap-4"> | |
| <div> | |
| <p className="text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>📊 Training Summary</p> | |
| <div className="text-xs space-y-1" style={{ color: 'var(--text-muted)' }}> | |
| <p>• <strong>Modes:</strong> {Array.from(selectedModes).map(m => | |
| m === 'traditional' ? '🌲 Traditional ML' : | |
| m === 'nlp' ? '📝 NLP' : '🧠 Deep Learning' | |
| ).join(', ')}</p> | |
| <p>• <strong>Target:</strong> {targetColumn}</p> | |
| {Array.from(selectedModes).map(mode => ( | |
| <p key={mode}>• <strong>{mode}:</strong> { | |
| selectedAlgorithms[mode].includes('auto') | |
| ? '🚀 Auto (best algorithms)' | |
| : `${selectedAlgorithms[mode].length} algorithm${selectedAlgorithms[mode].length > 1 ? 's' : ''} selected` | |
| }</p> | |
| ))} | |
| </div> | |
| </div> | |
| {/* START TRAINING BUTTON - Prominent */} | |
| <div className="flex flex-col gap-2"> | |
| <button | |
| onClick={handleStartTraining} | |
| disabled={training || !targetColumn} | |
| className={`px-8 py-3 rounded-xl font-bold text-white shadow-lg transition-all transform hover:scale-105 flex items-center justify-center gap-2 ${training ? 'opacity-60 cursor-wait' : 'hover:shadow-xl' | |
| } ${selectedModes.size > 1 | |
| ? 'bg-gradient-to-r from-purple-600 via-blue-600 to-green-600' | |
| : selectedModes.has('nlp') | |
| ? 'bg-gradient-to-r from-blue-600 to-cyan-600' | |
| : selectedModes.has('deep_learning') | |
| ? 'bg-gradient-to-r from-red-600 to-orange-600' | |
| : 'bg-gradient-to-r from-green-600 to-emerald-600' | |
| }`} | |
| > | |
| {training ? ( | |
| <> | |
| <div className="loading-spinner" /> | |
| <span>Training {selectedModes.size} Mode{selectedModes.size > 1 ? 's' : ''}...</span> | |
| </> | |
| ) : ( | |
| <> | |
| <Play className="w-5 h-5" /> | |
| <span>🚀 Start Training</span> | |
| </> | |
| )} | |
| </button> | |
| {training && ( | |
| <button | |
| onClick={handleStopTraining} | |
| className="px-4 py-2 rounded-lg bg-red-500/20 text-red-400 hover:bg-red-500/30 transition-all flex items-center justify-center gap-2" | |
| > | |
| <Square className="w-4 h-4" /> | |
| Stop Training | |
| </button> | |
| )} | |
| {!targetColumn && ( | |
| <p className="text-xs text-amber-400">⚠️ Select a target column first</p> | |
| )} | |
| </div> | |
| </div> | |
| {/* Training Progress */} | |
| {training && ( | |
| <div className="mt-4 p-3 rounded-lg bg-black/20"> | |
| <div className="flex items-center gap-2 mb-2"> | |
| <div className="loading-spinner" /> | |
| <span className="text-sm font-medium" style={{ color: 'var(--text-primary)' }}>{progressMessage}</span> | |
| </div> | |
| <div className="w-full bg-gray-700 rounded-full h-2"> | |
| <div className="bg-gradient-to-r from-green-500 to-emerald-500 h-2 rounded-full animate-pulse" style={{ width: '60%' }}></div> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| </motion.div> | |
| )} | |
| {/* Unsupervised: Clustering Configuration */} | |
| {learningType === 'unsupervised' && availableColumns.length > 0 && ( | |
| <motion.div | |
| initial={{ opacity: 0, y: 20 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| transition={{ delay: 0.15 }} | |
| className="p-4 rounded-2xl border" | |
| style={{ backgroundColor: 'var(--bg-card)', borderColor: 'var(--border-color)' }} | |
| > | |
| <div className="flex items-center gap-3 mb-4"> | |
| <div className="p-2 rounded-xl bg-purple-500/20"> | |
| <Boxes className="w-5 h-5 text-purple-400" /> | |
| </div> | |
| <div> | |
| <p className="font-semibold" style={{ color: 'var(--text-primary)' }}> | |
| 🎯 Clustering Configuration | |
| </p> | |
| <p className="text-sm" style={{ color: 'var(--text-muted)' }}> | |
| No target column needed - AI will discover groups automatically | |
| </p> | |
| </div> | |
| </div> | |
| <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| {/* Algorithm Selection */} | |
| <div> | |
| <label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}> | |
| Algorithm | |
| </label> | |
| <select | |
| value={clusteringAlgorithm} | |
| onChange={(e) => setClusteringAlgorithm(e.target.value)} | |
| disabled={training} | |
| className="w-full p-3 rounded-xl border bg-transparent outline-none focus:border-purple-500 transition-all" | |
| style={{ borderColor: 'var(--border-color)', color: 'var(--text-primary)' }} | |
| > | |
| <option value="auto" style={{ backgroundColor: isDark ? '#1f2937' : '#fff', color: isDark ? '#fff' : '#000' }}>🚀 Auto (Smart Selection)</option> | |
| <option value="kmeans" style={{ backgroundColor: isDark ? '#1f2937' : '#fff', color: isDark ? '#fff' : '#000' }}>K-Means (Fast, Spherical clusters)</option> | |
| <option value="dbscan" style={{ backgroundColor: isDark ? '#1f2937' : '#fff', color: isDark ? '#fff' : '#000' }}>DBSCAN (Finds outliers)</option> | |
| <option value="hierarchical" style={{ backgroundColor: isDark ? '#1f2937' : '#fff', color: isDark ? '#fff' : '#000' }}>Hierarchical (Cluster hierarchy)</option> | |
| <option value="gmm" style={{ backgroundColor: isDark ? '#1f2937' : '#fff', color: isDark ? '#fff' : '#000' }}>Gaussian Mixture (Soft clustering)</option> | |
| <option value="spectral" style={{ backgroundColor: isDark ? '#1f2937' : '#fff', color: isDark ? '#fff' : '#000' }}>Spectral (Complex shapes)</option> | |
| </select> | |
| </div> | |
| {/* Cluster Count */} | |
| <div> | |
| <label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}> | |
| Number of Clusters | |
| </label> | |
| <div className="flex items-center gap-2"> | |
| <input | |
| type="number" | |
| value={clusterCount || ''} | |
| onChange={(e) => setClusterCount(e.target.value ? parseInt(e.target.value) : null)} | |
| placeholder="Auto-detect" | |
| min={2} | |
| max={20} | |
| disabled={training} | |
| className="flex-1 p-3 rounded-xl border bg-transparent outline-none focus:border-purple-500 transition-all" | |
| style={{ borderColor: 'var(--border-color)', color: 'var(--text-primary)' }} | |
| /> | |
| </div> | |
| <p className="text-xs mt-1" style={{ color: 'var(--text-muted)' }}> | |
| Leave empty to auto-detect optimal clusters | |
| </p> | |
| </div> | |
| </div> | |
| {/* Train Clustering Button */} | |
| <button | |
| onClick={handleStartTraining} | |
| disabled={training || selectedFiles.length === 0} | |
| className="mt-4 w-full px-6 py-3 rounded-xl font-medium text-white flex items-center justify-center gap-2 | |
| bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-500 hover:to-pink-500 | |
| disabled:opacity-50 disabled:cursor-not-allowed transition-all" | |
| > | |
| {training ? ( | |
| <> | |
| <RefreshCw className="w-5 h-5 animate-spin" /> | |
| Analyzing Clusters... | |
| </> | |
| ) : ( | |
| <> | |
| <Boxes className="w-5 h-5" /> | |
| Run Clustering Analysis | |
| </> | |
| )} | |
| </button> | |
| </motion.div> | |
| )} | |
| {/* 🎯 Target Column Selection - ONLY FOR SUPERVISED */} | |
| {learningType === 'supervised' && availableColumns.length > 0 && ( | |
| <motion.div | |
| initial={{ opacity: 0, y: 20 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| transition={{ delay: 0.15 }} | |
| className="p-4 rounded-2xl border" | |
| style={{ backgroundColor: 'var(--bg-card)', borderColor: 'var(--border-color)' }} | |
| > | |
| <div className="flex items-center gap-3 mb-3"> | |
| <div className="p-2 rounded-xl bg-purple-500/20"> | |
| <Sparkles className="w-5 h-5 text-purple-400" /> | |
| </div> | |
| <div> | |
| <p className="font-semibold" style={{ color: 'var(--text-primary)' }}> | |
| 🎯 Target Column (What to predict) | |
| </p> | |
| <p className="text-sm" style={{ color: 'var(--text-muted)' }}> | |
| Auto-detected: <span className="text-purple-400">{targetColumn}</span> • Change if needed | |
| </p> | |
| </div> | |
| </div> | |
| <select | |
| value={targetColumn} | |
| onChange={(e) => setTargetColumn(e.target.value)} | |
| disabled={training} | |
| className="w-full p-3 rounded-xl border bg-transparent outline-none focus:border-purple-500 transition-all" | |
| style={{ borderColor: 'var(--border-color)', color: 'var(--text-primary)' }} | |
| > | |
| {availableColumns.map((col) => ( | |
| <option key={col} value={col} style={{ backgroundColor: isDark ? '#1f2937' : '#ffffff', color: isDark ? '#ffffff' : '#000000' }}> | |
| {col} | |
| </option> | |
| ))} | |
| </select> | |
| <p className="text-xs mt-2" style={{ color: 'var(--text-muted)' }}> | |
| 💡 Tip: Select the column you want the model to predict (e.g., price, category, fraud) | |
| </p> | |
| </motion.div> | |
| )} | |
| {/* 🏥 Data Health Card - Shows before training */} | |
| {selectedFiles.length > 0 && ( | |
| <motion.div | |
| initial={{ opacity: 0, y: 20 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| transition={{ delay: 0.2 }} | |
| > | |
| <DataHealthCard | |
| fileName={selectedFiles[0].name} | |
| targetColumn={targetColumn} | |
| /> | |
| </motion.div> | |
| )} | |
| {/* Instructions when no file selected */} | |
| {selectedFiles.length === 0 && existingFiles.length > 0 && ( | |
| <motion.div | |
| initial={{ opacity: 0 }} | |
| animate={{ opacity: 1 }} | |
| className="text-center py-6" | |
| > | |
| <p style={{ color: 'var(--text-muted)' }}> | |
| 👆 Select a file above to start training | |
| </p> | |
| </motion.div> | |
| )} | |
| </div> | |
| ); | |
| } | |
| // ======================================================================== | |
| // RESULTS VIEW - Showing trained model results | |
| // ======================================================================== | |
| // If only clustering result exists (no supervised result), show clustering-focused view | |
| if (!result && clusteringResult) { | |
| return ( | |
| <div className="space-y-6"> | |
| {/* Clustering Header */} | |
| <motion.div | |
| initial={{ opacity: 0, y: -20 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| className="flex flex-col md:flex-row md:items-center justify-between gap-4" | |
| > | |
| <div className="flex items-center gap-4 w-full md:w-auto"> | |
| <div className="min-w-0"> | |
| <h1 className="text-2xl font-bold flex items-center gap-3 truncate" style={{ color: 'var(--text-primary)' }}> | |
| <Boxes className="w-8 h-8 flex-shrink-0 text-purple-400" /> | |
| <span className="truncate">Clustering Results</span> | |
| </h1> | |
| <p className="text-sm truncate" style={{ color: 'var(--text-muted)' }}> | |
| Algorithm: <span className="font-medium text-purple-400">{clusteringResult.algorithm?.toUpperCase()}</span> | |
| {' • '} | |
| Found: <span className="text-blue-400 font-medium">{clusteringResult.n_clusters} clusters</span> | |
| {' • '} | |
| Silhouette: <span className="text-amber-400 font-medium">{(clusteringResult.silhouette_score * 100).toFixed(1)}%</span> | |
| </p> | |
| </div> | |
| </div> | |
| <button | |
| onClick={() => { | |
| setClusteringResult(null); | |
| setLearningType('supervised'); | |
| const userId = getUserIdSync(); | |
| localStorage.removeItem(`clusteringResult_${userId}`); | |
| localStorage.removeItem(`learningType_${userId}`); | |
| sessionStorage.removeItem(`clusteringResult_${userId}`); | |
| loadExistingFiles(); | |
| }} | |
| className="w-full md:w-auto px-4 py-2 rounded-xl border transition-colors flex items-center justify-center gap-2" | |
| style={{ borderColor: 'var(--border-color)', color: 'var(--text-muted)' }} | |
| > | |
| <RefreshCw className="w-4 h-4" /> | |
| <span>New Analysis</span> | |
| </button> | |
| </motion.div> | |
| {/* Clustering Metrics */} | |
| <div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4"> | |
| <motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.1 }} | |
| className="p-4 rounded-2xl border" style={{ backgroundColor: 'var(--bg-card)', borderColor: 'var(--border-color)' }}> | |
| <div className="flex items-center gap-2 mb-2"> | |
| <div className="p-1.5 rounded-lg bg-purple-500/20"><Boxes className="w-4 h-4 text-purple-400" /></div> | |
| <span className="text-xs font-medium" style={{ color: 'var(--text-muted)' }}>Clusters</span> | |
| </div> | |
| <p className="text-2xl font-bold text-purple-400">{clusteringResult.n_clusters}</p> | |
| </motion.div> | |
| <motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.12 }} | |
| className="p-4 rounded-2xl border" style={{ backgroundColor: 'var(--bg-card)', borderColor: 'var(--border-color)' }}> | |
| <div className="flex items-center gap-2 mb-2"> | |
| <div className="p-1.5 rounded-lg bg-blue-500/20"><TrendingUp className="w-4 h-4 text-blue-400" /></div> | |
| <span className="text-xs font-medium" style={{ color: 'var(--text-muted)' }}>Silhouette Score</span> | |
| </div> | |
| <p className="text-2xl font-bold" style={{ color: clusteringResult.silhouette_score >= 0.5 ? '#22c55e' : clusteringResult.silhouette_score >= 0.25 ? '#f59e0b' : '#ef4444' }}> | |
| {(clusteringResult.silhouette_score * 100).toFixed(1)}% | |
| </p> | |
| <p className="text-xs mt-1" style={{ color: 'var(--text-muted)' }}> | |
| {clusteringResult.silhouette_score >= 0.5 ? 'Good' : clusteringResult.silhouette_score >= 0.25 ? 'Moderate' : 'Weak'} | |
| </p> | |
| </motion.div> | |
| <motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.14 }} | |
| className="p-4 rounded-2xl border" style={{ backgroundColor: 'var(--bg-card)', borderColor: 'var(--border-color)' }}> | |
| <div className="flex items-center gap-2 mb-2"> | |
| <div className="p-1.5 rounded-lg bg-emerald-500/20"><Sparkles className="w-4 h-4 text-emerald-400" /></div> | |
| <span className="text-xs font-medium" style={{ color: 'var(--text-muted)' }}>Algorithm</span> | |
| </div> | |
| <p className="text-lg font-bold text-emerald-400">{clusteringResult.algorithm?.toUpperCase()}</p> | |
| </motion.div> | |
| <motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.16 }} | |
| className="p-4 rounded-2xl border" style={{ backgroundColor: 'var(--bg-card)', borderColor: 'var(--border-color)' }}> | |
| <div className="flex items-center gap-2 mb-2"> | |
| <div className="p-1.5 rounded-lg bg-amber-500/20"><Database className="w-4 h-4 text-amber-400" /></div> | |
| <span className="text-xs font-medium" style={{ color: 'var(--text-muted)' }}>Samples</span> | |
| </div> | |
| <p className="text-2xl font-bold text-amber-400">{clusteringResult.n_samples?.toLocaleString() || 'N/A'}</p> | |
| </motion.div> | |
| <motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.18 }} | |
| className="p-4 rounded-2xl border" style={{ backgroundColor: 'var(--bg-card)', borderColor: 'var(--border-color)' }}> | |
| <div className="flex items-center gap-2 mb-2"> | |
| <div className="p-1.5 rounded-lg bg-cyan-500/20"><Activity className="w-4 h-4 text-cyan-400" /></div> | |
| <span className="text-xs font-medium" style={{ color: 'var(--text-muted)' }}>Features</span> | |
| </div> | |
| <p className="text-2xl font-bold text-cyan-400">{clusteringResult.n_features || clusteringResult.feature_columns?.length || 'N/A'}</p> | |
| </motion.div> | |
| {clusteringResult.calinski_harabasz_score && ( | |
| <motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.2 }} | |
| className="p-4 rounded-2xl border" style={{ backgroundColor: 'var(--bg-card)', borderColor: 'var(--border-color)' }}> | |
| <div className="flex items-center gap-2 mb-2"> | |
| <div className="p-1.5 rounded-lg bg-rose-500/20"><Target className="w-4 h-4 text-rose-400" /></div> | |
| <span className="text-xs font-medium" style={{ color: 'var(--text-muted)' }}>Calinski-Harabasz</span> | |
| </div> | |
| <p className="text-xl font-bold text-rose-400">{clusteringResult.calinski_harabasz_score.toFixed(1)}</p> | |
| <p className="text-xs mt-1" style={{ color: 'var(--text-muted)' }}>Higher = better</p> | |
| </motion.div> | |
| )} | |
| </div> | |
| {/* TABS - Like Supervised */} | |
| <div className="overflow-x-auto no-scrollbar pb-2 md:pb-0"> | |
| <div className="flex gap-2 p-1.5 rounded-xl min-w-max md:min-w-0 border" style={{ backgroundColor: 'var(--bg-card)', borderColor: 'var(--border-color)' }}> | |
| {[ | |
| { id: 'overview', label: 'Overview', icon: PieChart }, | |
| { id: 'charts', label: 'Visualization', icon: BarChart3 }, | |
| { id: 'profiles', label: 'Cluster Profiles', icon: Activity }, | |
| { id: 'predict', label: 'Predict Cluster', icon: Play }, | |
| { id: 'download', label: 'Download', icon: Download }, | |
| ].map((tab) => ( | |
| <button | |
| key={tab.id} | |
| onClick={() => setClusterActiveTab(tab.id as any)} | |
| className={`flex-1 flex flex-shrink-0 items-center justify-center gap-2 px-4 py-3 rounded-lg font-medium transition-all whitespace-nowrap ${clusterActiveTab === tab.id | |
| ? 'bg-gradient-to-r from-purple-500 to-pink-500 text-white shadow-lg' | |
| : ''}`} | |
| style={clusterActiveTab !== tab.id ? { color: 'var(--text-muted)' } : undefined} | |
| > | |
| <tab.icon className="w-4 h-4" /> | |
| {tab.label} | |
| </button> | |
| ))} | |
| </div> | |
| </div> | |
| {/* Tab Content */} | |
| <motion.div key={clusterActiveTab} initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.3 }}> | |
| {/* OVERVIEW TAB */} | |
| {clusterActiveTab === 'overview' && ( | |
| <div className="space-y-6"> | |
| {/* Cluster Distribution */} | |
| <div className="p-6 rounded-2xl border" style={{ backgroundColor: 'var(--bg-card)', borderColor: 'var(--border-color)' }}> | |
| <h3 className="text-lg font-semibold mb-4 flex items-center gap-2" style={{ color: 'var(--text-primary)' }}> | |
| <BarChart3 className="w-5 h-5 text-purple-400" /> Cluster Distribution | |
| </h3> | |
| <div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-5 gap-4"> | |
| {clusteringResult.cluster_distribution && Object.entries(clusteringResult.cluster_distribution).map(([cluster, count], i) => { | |
| const total = Object.values(clusteringResult.cluster_distribution as Record<string, number>).reduce((a: number, b: number) => a + b, 0); | |
| const percentage = ((count as number) / total * 100).toFixed(1); | |
| return ( | |
| <div key={cluster} className="p-4 rounded-xl text-center border" style={{ borderColor: 'var(--border-color)', backgroundColor: isDark ? 'rgba(168, 85, 247, 0.1)' : 'rgba(168, 85, 247, 0.05)' }}> | |
| <p className="text-2xl font-bold text-purple-400">{count as number}</p> | |
| <p className="text-sm font-medium" style={{ color: 'var(--text-muted)' }}>{cluster}</p> | |
| <div className="w-full bg-gray-700/30 rounded-full h-2 mt-2"> | |
| <div className="h-2 rounded-full bg-gradient-to-r from-purple-500 to-pink-500" style={{ width: `${percentage}%` }} /> | |
| </div> | |
| <p className="text-xs mt-1 text-purple-400">{percentage}%</p> | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| </div> | |
| {/* Insights */} | |
| {clusteringResult.insights && clusteringResult.insights.length > 0 && ( | |
| <div className="p-6 rounded-2xl border" style={{ backgroundColor: 'var(--bg-card)', borderColor: 'var(--border-color)' }}> | |
| <h3 className="text-lg font-semibold mb-4 flex items-center gap-2" style={{ color: 'var(--text-primary)' }}> | |
| <Sparkles className="w-5 h-5 text-amber-400" /> AI Insights | |
| </h3> | |
| <div className="grid grid-cols-1 md:grid-cols-2 gap-3"> | |
| {clusteringResult.insights.map((insight: string, i: number) => ( | |
| <div key={i} className="flex items-start gap-3 p-3 rounded-lg" style={{ backgroundColor: isDark ? 'rgba(255,255,255,0.03)' : 'rgba(0,0,0,0.02)' }}> | |
| <CheckCircle className="w-5 h-5 text-emerald-400 flex-shrink-0 mt-0.5" /> | |
| <span style={{ color: 'var(--text-muted)' }}>{insight}</span> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| )} | |
| {/* CHARTS TAB - Full Clustering Visualizations */} | |
| {clusterActiveTab === 'charts' && (() => { | |
| const clusterChartEntries = clusteringResult.charts ? Object.entries(clusteringResult.charts) : []; | |
| const copyClusterChart = async (base64: string, name: string) => { | |
| try { | |
| const res = await fetch(base64); | |
| const blob = await res.blob(); | |
| await navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })]) | |
| setCopiedChart(name); | |
| setTimeout(() => setCopiedChart(null), 2000); | |
| } catch { | |
| const w = window.open(); | |
| if (w) { w.document.write(`<img src="${base64}" />`); } | |
| } | |
| }; | |
| const exportClusterChartsToPPT = async () => { | |
| if (clusterChartEntries.length === 0) return; | |
| setExportingPPT(true); | |
| try { | |
| const pptx = new PptxGenJS(); | |
| pptx.title = 'Clustering Charts'; | |
| pptx.author = 'DataVision AI'; | |
| pptx.layout = 'LAYOUT_WIDE'; | |
| for (const [name, b64] of clusterChartEntries) { | |
| const title = name.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase()); | |
| const slide = pptx.addSlide(); | |
| slide.addText(title, { x: 0.5, y: 0.2, w: '90%', h: 0.6, fontSize: 22, bold: true, color: '333333', fontFace: 'Segoe UI' }); | |
| const imgData = (b64 as string).replace(/^data:image\/\w+;base64,/, ''); | |
| slide.addImage({ data: `image/png;base64,${imgData}`, x: 0.8, y: 1.0, w: 11.5, h: 5.8, sizing: { type: 'contain', w: 11.5, h: 5.8 } }); | |
| } | |
| await pptx.writeFile({ fileName: 'Clustering_Charts.pptx' }); | |
| } catch (e) { console.error('PPT export failed:', e); } | |
| finally { setExportingPPT(false); } | |
| }; | |
| const ClusterChartCard = ({ name, label, icon, base64, description, colSpan }: { name: string; label: string; icon: React.ReactNode; base64: string; description?: string; colSpan?: boolean }) => ( | |
| <div className={`rounded-2xl border overflow-hidden transition-all hover:shadow-lg ${colSpan ? 'lg:col-span-2' : ''}`} style={{ backgroundColor: 'var(--bg-card)', borderColor: 'var(--border-color)' }}> | |
| <div className="flex items-center gap-2 px-4 py-3 border-b" style={{ borderColor: 'var(--border-color)' }}> | |
| {icon} | |
| <h3 className="flex-1 text-sm font-semibold" style={{ color: 'var(--text-primary)' }}>{label}</h3> | |
| <button onClick={() => copyClusterChart(base64, name)} className="p-1.5 rounded-lg transition-all hover:scale-110" style={{ backgroundColor: copiedChart === name ? (isDark ? 'rgba(34,197,94,0.2)' : 'rgba(34,197,94,0.1)') : (isDark ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.05)') }} title="Copy chart"> | |
| {copiedChart === name ? <Check className="w-3.5 h-3.5 text-green-500" /> : <Copy className="w-3.5 h-3.5" style={{ color: 'var(--text-muted)' }} />} | |
| </button> | |
| </div> | |
| <div className="flex flex-col items-center justify-center p-2" style={{ backgroundColor: isDark ? '#0f172a' : '#f8fafc' }}> | |
| <img src={base64} alt={name} className="rounded-lg" style={{ width: '100%', height: 'auto', display: 'block' }} /> | |
| {description && <p className="text-center mt-1 text-xs" style={{ color: 'var(--text-muted)' }}>{description}</p>} | |
| </div> | |
| </div> | |
| ); | |
| return ( | |
| <div className="space-y-3"> | |
| {clusterChartEntries.length > 0 && ( | |
| <div className="flex items-center justify-between px-1"> | |
| <p className="text-sm font-medium" style={{ color: 'var(--text-muted)' }}>{clusterChartEntries.length} chart{clusterChartEntries.length !== 1 ? 's' : ''} generated</p> | |
| <button onClick={exportClusterChartsToPPT} disabled={exportingPPT} className="flex items-center gap-2 px-4 py-2 rounded-xl text-sm font-medium transition-all hover:scale-105" style={{ backgroundColor: isDark ? 'rgba(99,102,241,0.2)' : 'rgba(99,102,241,0.1)', color: isDark ? '#a5b4fc' : '#4f46e5', border: `1px solid ${isDark ? 'rgba(99,102,241,0.3)' : 'rgba(99,102,241,0.2)'}` }}> | |
| {exportingPPT ? <><RefreshCw className="w-4 h-4 animate-spin" /> Exporting...</> : <><Presentation className="w-4 h-4" /> Export All to PPT</>} | |
| </button> | |
| </div> | |
| )} | |
| {/* Charts Grid */} | |
| {clusteringResult.charts && Object.keys(clusteringResult.charts).length > 0 ? ( | |
| <div className="grid grid-cols-1 lg:grid-cols-2 gap-3"> | |
| {clusteringResult.charts.cluster_scatter && ( | |
| <ClusterChartCard name="cluster_scatter" label="Cluster Scatter Plot (PCA 2D)" icon={<Activity className="w-4 h-4 text-blue-400" />} base64={clusteringResult.charts.cluster_scatter} description={clusteringResult.pca_variance_explained ? `PCA captures ${(clusteringResult.pca_variance_explained * 100).toFixed(1)}% of data variance` : undefined} /> | |
| )} | |
| {clusteringResult.charts.elbow_method && ( | |
| <ClusterChartCard name="elbow_method" label="Elbow Method" icon={<TrendingUp className="w-4 h-4 text-amber-400" />} base64={clusteringResult.charts.elbow_method} description="Find optimal k where the curve bends (elbow point)" /> | |
| )} | |
| {clusteringResult.charts.silhouette_comparison && ( | |
| <ClusterChartCard name="silhouette_comparison" label="Silhouette Score Comparison" icon={<BarChart3 className="w-4 h-4 text-emerald-400" />} base64={clusteringResult.charts.silhouette_comparison} description="Higher silhouette = better cluster separation" /> | |
| )} | |
| {clusteringResult.charts.cluster_distribution && ( | |
| <ClusterChartCard name="cluster_distribution" label="Cluster Distribution" icon={<PieChart className="w-4 h-4 text-purple-400" />} base64={clusteringResult.charts.cluster_distribution} /> | |
| )} | |
| {clusteringResult.charts.silhouette_plot && ( | |
| <ClusterChartCard name="silhouette_plot" label="Silhouette Plot" icon={<Activity className="w-4 h-4 text-cyan-400" />} base64={clusteringResult.charts.silhouette_plot} description="Per-sample silhouette coefficients by cluster" /> | |
| )} | |
| {clusteringResult.charts.cluster_heatmap && ( | |
| <ClusterChartCard name="cluster_heatmap" label="Cluster Centers Heatmap" icon={<Layers className="w-4 h-4 text-red-400" />} base64={clusteringResult.charts.cluster_heatmap} description="Feature values at each cluster center" /> | |
| )} | |
| {clusteringResult.charts.pca_variance && ( | |
| <ClusterChartCard name="pca_variance" label="PCA Variance Explained" icon={<BarChart2 className="w-4 h-4 text-indigo-400" />} base64={clusteringResult.charts.pca_variance} description="Cumulative variance captured by principal components" /> | |
| )} | |
| {clusteringResult.charts.pairplot && ( | |
| <ClusterChartCard name="pairplot" label="Feature Pairplot" icon={<Activity className="w-4 h-4 text-teal-400" />} base64={clusteringResult.charts.pairplot} description="Pairwise scatter plots of top features" colSpan /> | |
| )} | |
| {clusteringResult.charts.cluster_3d && ( | |
| <ClusterChartCard name="cluster_3d" label="3D Cluster Visualization" icon={<Boxes className="w-4 h-4 text-violet-400" />} base64={clusteringResult.charts.cluster_3d} description="PCA 3D scatter plot of clusters" /> | |
| )} | |
| {clusteringResult.charts.dendrogram && ( | |
| <ClusterChartCard name="dendrogram" label="Dendrogram (Hierarchical Tree)" icon={<GitBranch className="w-4 h-4 text-rose-400" />} base64={clusteringResult.charts.dendrogram} description="Hierarchical relationships between clusters" colSpan /> | |
| )} | |
| {clusteringResult.charts.tsne && ( | |
| <ClusterChartCard name="tsne" label="t-SNE Visualization" icon={<Sparkles className="w-4 h-4 text-pink-400" />} base64={clusteringResult.charts.tsne} description="Non-linear dimensionality reduction for cluster visualization" /> | |
| )} | |
| {clusteringResult.charts.umap && ( | |
| <ClusterChartCard name="umap" label="UMAP Projection" icon={<Activity className="w-4 h-4 text-sky-400" />} base64={clusteringResult.charts.umap} description="Uniform Manifold Approximation for cluster topology" /> | |
| )} | |
| {clusteringResult.charts.boxplots && ( | |
| <ClusterChartCard name="boxplots" label="Feature Box Plots" icon={<BarChart3 className="w-4 h-4 text-orange-400" />} base64={clusteringResult.charts.boxplots} description="Distribution of features across clusters" colSpan /> | |
| )} | |
| {clusteringResult.charts.violin_plots && ( | |
| <ClusterChartCard name="violin_plots" label="Violin Plots" icon={<Activity className="w-4 h-4 text-fuchsia-400" />} base64={clusteringResult.charts.violin_plots} description="Feature density distributions by cluster" colSpan /> | |
| )} | |
| {clusteringResult.charts.correlation_heatmap && ( | |
| <ClusterChartCard name="correlation_heatmap" label="Correlation Heatmap" icon={<Layers className="w-4 h-4 text-amber-400" />} base64={clusteringResult.charts.correlation_heatmap} description="Feature correlation matrix" /> | |
| )} | |
| {clusteringResult.charts.radar_chart && ( | |
| <ClusterChartCard name="radar_chart" label="Radar Chart" icon={<Target className="w-4 h-4 text-lime-400" />} base64={clusteringResult.charts.radar_chart} description="Cluster profiles across normalized features" /> | |
| )} | |
| {clusteringResult.charts.feature_importance && ( | |
| <ClusterChartCard name="feature_importance" label="Feature Importance" icon={<Sparkles className="w-4 h-4 text-yellow-400" />} base64={clusteringResult.charts.feature_importance} description="Features most important for cluster separation" /> | |
| )} | |
| {clusteringResult.charts.gmm_bic_aic && ( | |
| <ClusterChartCard name="gmm_bic_aic" label="GMM BIC/AIC Scores" icon={<TrendingUp className="w-4 h-4 text-emerald-400" />} base64={clusteringResult.charts.gmm_bic_aic} description="Model selection criteria for GMM clustering" /> | |
| )} | |
| {clusteringResult.charts.dbscan_kdist && ( | |
| <ClusterChartCard name="dbscan_kdist" label="DBSCAN k-Distance" icon={<TrendingUp className="w-4 h-4 text-red-400" />} base64={clusteringResult.charts.dbscan_kdist} description="k-distance graph for DBSCAN eps parameter selection" /> | |
| )} | |
| {clusteringResult.charts.spectral_affinity && ( | |
| <ClusterChartCard name="spectral_affinity" label="Spectral Affinity Matrix" icon={<Layers className="w-4 h-4 text-blue-400" />} base64={clusteringResult.charts.spectral_affinity} description="Pairwise similarity matrix for spectral clustering" /> | |
| )} | |
| </div> | |
| ) : ( | |
| <div className="p-12 text-center rounded-2xl border" style={{ backgroundColor: 'var(--bg-card)', borderColor: 'var(--border-color)' }}> | |
| <BarChart3 className="w-16 h-16 mx-auto mb-4 opacity-50" style={{ color: 'var(--text-muted)' }} /> | |
| <p style={{ color: 'var(--text-muted)' }}>No charts available. Re-run clustering to generate visualizations.</p> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| })()} | |
| {/* PROFILES TAB */} | |
| {clusterActiveTab === 'profiles' && ( | |
| <div className="p-6 rounded-2xl border" style={{ backgroundColor: 'var(--bg-card)', borderColor: 'var(--border-color)' }}> | |
| <h3 className="text-lg font-semibold mb-4 flex items-center gap-2" style={{ color: 'var(--text-primary)' }}> | |
| <Activity className="w-5 h-5 text-emerald-400" /> Cluster Profiles | |
| </h3> | |
| {clusteringResult.cluster_profiles ? ( | |
| <div className="space-y-4"> | |
| {Object.entries(clusteringResult.cluster_profiles).map(([clusterName, profile]: [string, any]) => ( | |
| <div key={clusterName} className="p-4 rounded-xl border" style={{ borderColor: 'var(--border-color)', backgroundColor: isDark ? 'rgba(255,255,255,0.02)' : 'rgba(0,0,0,0.01)' }}> | |
| <div className="flex items-center justify-between mb-3"> | |
| <h4 className="font-semibold text-purple-400">{clusterName}</h4> | |
| <span className="text-sm px-2 py-1 rounded-full bg-purple-500/20 text-purple-400"> | |
| {profile.size} samples ({profile.percentage?.toFixed(1)}%) | |
| </span> | |
| </div> | |
| <div className="grid grid-cols-2 md:grid-cols-4 gap-2"> | |
| {profile.characteristics && Object.entries(profile.characteristics).slice(0, 8).map(([feature, stats]: [string, any]) => ( | |
| <div key={feature} className="p-2 rounded-lg text-center" style={{ backgroundColor: isDark ? 'rgba(255,255,255,0.03)' : 'rgba(0,0,0,0.02)' }}> | |
| <p className="text-xs truncate" style={{ color: 'var(--text-muted)' }}>{feature}</p> | |
| <p className="font-semibold text-sm" style={{ color: 'var(--text-primary)' }}>μ = {stats.mean?.toFixed(2)}</p> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| ) : ( | |
| <p style={{ color: 'var(--text-muted)' }}>Cluster profiles not available. Re-run clustering to generate profiles.</p> | |
| )} | |
| </div> | |
| )} | |
| {/* PREDICT TAB */} | |
| {/* DOWNLOAD TAB */} | |
| {clusterActiveTab === 'download' && (() => { | |
| const handleClusteringDownload = async (type: 'model' | 'data' | 'code', filename: string) => { | |
| try { | |
| const userId = getUserIdSync(); | |
| const urlMap = { | |
| model: `/api/v1/ml/clustering/download-model/${userId}`, | |
| data: `/api/v1/ml/clustering/download-data/${userId}`, | |
| code: `/api/v1/ml/clustering/download-code/${userId}`, | |
| }; | |
| const response = await fetch(urlMap[type], { | |
| method: 'GET', | |
| headers: { ...getAuthHeadersSync() }, | |
| }); | |
| if (!response.ok) throw new Error(`Download failed: ${response.status} ${response.statusText}`); | |
| const blob = await response.blob(); | |
| const url = window.URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| // Use filename from Content-Disposition if available | |
| const disposition = response.headers.get('Content-Disposition'); | |
| const serverFilename = disposition?.match(/filename=(.+)/)?.[1]?.replace(/"/g, ''); | |
| a.download = serverFilename || filename; | |
| document.body.appendChild(a); | |
| a.click(); | |
| window.URL.revokeObjectURL(url); | |
| document.body.removeChild(a); | |
| } catch (err) { | |
| console.error(`Clustering ${type} download error:`, err); | |
| alert(`Download failed: ${err instanceof Error ? err.message : 'Unknown error'}`); | |
| } | |
| }; | |
| return ( | |
| <div className="p-8 rounded-2xl border border-dashed" style={{ backgroundColor: 'var(--bg-card)', borderColor: 'var(--border-color)' }}> | |
| <motion.div initial={{ scale: 0.9, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} className="max-w-4xl mx-auto"> | |
| <div className="text-center mb-8"> | |
| <div className="w-20 h-20 mx-auto bg-purple-500/20 rounded-full flex items-center justify-center mb-6"> | |
| <Boxes className="w-10 h-10 text-purple-400" /> | |
| </div> | |
| <h2 className="text-2xl font-bold mb-3" style={{ color: 'var(--text-primary)' }}> | |
| Clustering Assets & Code Export | |
| </h2> | |
| <p className="leading-relaxed max-w-2xl mx-auto" style={{ color: 'var(--text-muted)' }}> | |
| Download trained clustering model, clustered dataset, and complete unsupervised ML code. | |
| </p> | |
| </div> | |
| <div className="grid grid-cols-1 md:grid-cols-3 gap-5"> | |
| {/* Clustering Model Download */} | |
| <div className="p-5 rounded-xl border text-center" style={{ borderColor: 'var(--border-color)', backgroundColor: isDark ? 'rgba(255,255,255,0.02)' : 'rgba(0,0,0,0.02)' }}> | |
| <div className="w-14 h-14 mx-auto bg-purple-500/20 rounded-full flex items-center justify-center mb-4"> | |
| <Brain className="w-7 h-7 text-purple-500" /> | |
| </div> | |
| <h3 className="text-lg font-semibold mb-2" style={{ color: 'var(--text-primary)' }}> | |
| Clustering Model | |
| </h3> | |
| <p className="text-sm mb-4" style={{ color: 'var(--text-muted)' }}> | |
| Download the trained clustering model (.pkl) with scaler & centroids. | |
| </p> | |
| <button | |
| onClick={() => handleClusteringDownload('model', 'clustering_model.pkl')} | |
| className="inline-flex items-center gap-2 px-5 py-2.5 bg-purple-500 hover:bg-purple-600 text-white rounded-xl font-semibold transition-all hover:scale-105" | |
| > | |
| <Download className="w-4 h-4" /> | |
| Download .pkl | |
| </button> | |
| <p className="text-xs mt-3 flex items-center justify-center gap-2" style={{ color: 'var(--text-muted)' }}> | |
| <CheckCircle className="w-3 h-3 text-purple-400" /> | |
| {clusteringResult.algorithm?.toUpperCase()} ({clusteringResult.n_clusters} clusters) | |
| </p> | |
| </div> | |
| {/* Clustered Dataset Download */} | |
| <div className="p-5 rounded-xl border text-center" style={{ borderColor: 'var(--border-color)', backgroundColor: isDark ? 'rgba(255,255,255,0.02)' : 'rgba(0,0,0,0.02)' }}> | |
| <div className="w-14 h-14 mx-auto bg-emerald-500/20 rounded-full flex items-center justify-center mb-4"> | |
| <Database className="w-7 h-7 text-emerald-500" /> | |
| </div> | |
| <h3 className="text-lg font-semibold mb-2" style={{ color: 'var(--text-primary)' }}> | |
| Clustered Dataset | |
| </h3> | |
| <p className="text-sm mb-4" style={{ color: 'var(--text-muted)' }}> | |
| Data with cluster assignments, PCA components & labels. | |
| </p> | |
| {clusteringResult.cleaned_file || clusteringResult.labels ? ( | |
| <button | |
| onClick={() => handleClusteringDownload('data', 'clustered_data.csv')} | |
| className="inline-flex items-center gap-2 px-5 py-2.5 bg-emerald-500 hover:bg-emerald-600 text-white rounded-xl font-semibold transition-all hover:scale-105" | |
| > | |
| <Download className="w-4 h-4" /> | |
| Download CSV | |
| </button> | |
| ) : ( | |
| <button disabled className="inline-flex items-center gap-2 px-5 py-2.5 bg-gray-500 text-white rounded-xl font-semibold opacity-50 cursor-not-allowed"> | |
| <Download className="w-4 h-4" /> | |
| Not Available | |
| </button> | |
| )} | |
| <p className="text-xs mt-3 flex items-center justify-center gap-2" style={{ color: 'var(--text-muted)' }}> | |
| <CheckCircle className="w-3 h-3 text-emerald-400" /> | |
| With Cluster Labels | |
| </p> | |
| </div> | |
| {/* Complete Code ZIP Download */} | |
| <div className="p-5 rounded-xl border text-center relative overflow-hidden" style={{ borderColor: isDark ? '#f59e0b' : '#d97706', backgroundColor: isDark ? 'rgba(245,158,11,0.05)' : 'rgba(245,158,11,0.03)' }}> | |
| <div className="absolute top-0 right-0 px-2 py-0.5 text-[10px] font-bold rounded-bl-lg" style={{ backgroundColor: '#f59e0b', color: '#000' }}> | |
| NEW | |
| </div> | |
| <div className="w-14 h-14 mx-auto bg-amber-500/20 rounded-full flex items-center justify-center mb-4"> | |
| <Code2 className="w-7 h-7 text-amber-500" /> | |
| </div> | |
| <h3 className="text-lg font-semibold mb-2" style={{ color: 'var(--text-primary)' }}> | |
| Complete Code | |
| </h3> | |
| <p className="text-sm mb-4" style={{ color: 'var(--text-muted)' }}> | |
| Full clustering project with train, predict, visualize & API. | |
| </p> | |
| <button | |
| onClick={() => handleClusteringDownload('code', 'clustering_project.zip')} | |
| className="inline-flex items-center gap-2 px-5 py-2.5 bg-gradient-to-r from-amber-500 to-orange-500 hover:from-amber-600 hover:to-orange-600 text-white rounded-xl font-semibold transition-all hover:scale-105 shadow-lg shadow-amber-500/20" | |
| > | |
| <Download className="w-4 h-4" /> | |
| Download ZIP | |
| </button> | |
| <p className="text-xs mt-3 flex items-center justify-center gap-2" style={{ color: 'var(--text-muted)' }}> | |
| <CheckCircle className="w-3 h-3 text-amber-400" /> | |
| Train + Predict + Charts + API | |
| </p> | |
| </div> | |
| </div> | |
| {/* What's inside the ZIP */} | |
| <div className="mt-8 p-5 rounded-xl border" style={{ borderColor: 'var(--border-color)', backgroundColor: isDark ? 'rgba(255,255,255,0.02)' : 'rgba(0,0,0,0.02)' }}> | |
| <h4 className="font-semibold mb-4 flex items-center gap-2 text-base" style={{ color: 'var(--text-primary)' }}> | |
| <Code2 className="w-5 h-5 text-amber-500" /> | |
| What's Inside the ZIP | |
| </h4> | |
| <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3"> | |
| {[ | |
| { file: 'clustering_model.pkl', desc: 'Trained clustering model', color: '#a855f7' }, | |
| { file: 'clustered_data.csv', desc: 'Data with cluster labels', color: '#22c55e' }, | |
| { file: 'predict_cluster.py', desc: 'Predict cluster for new data', color: '#3b82f6' }, | |
| { file: 'train_clustering.py', desc: 'Re-train clustering model', color: '#ef4444' }, | |
| { file: 'visualize_clusters.py', desc: 'Generate all clustering charts', color: '#ec4899' }, | |
| { file: 'api_server.py', desc: 'Flask REST API server', color: '#6366f1' }, | |
| { file: 'charts/', desc: 'Pre-generated training charts', color: '#f59e0b' }, | |
| { file: 'config.json', desc: 'Clustering configuration', color: '#14b8a6' }, | |
| { file: 'Dockerfile', desc: 'Docker deployment ready', color: '#0ea5e9' }, | |
| ].map((item) => ( | |
| <div key={item.file} className="flex items-center gap-3 p-2.5 rounded-lg" style={{ backgroundColor: isDark ? 'rgba(255,255,255,0.03)' : 'rgba(0,0,0,0.02)' }}> | |
| <div className="w-2 h-2 rounded-full flex-shrink-0" style={{ backgroundColor: item.color }} /> | |
| <div className="min-w-0"> | |
| <p className="text-sm font-medium truncate" style={{ color: 'var(--text-primary)' }}>{item.file}</p> | |
| <p className="text-xs truncate" style={{ color: 'var(--text-muted)' }}>{item.desc}</p> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| </motion.div> | |
| </div> | |
| ); | |
| })()} | |
| {clusterActiveTab === 'predict' && ( | |
| <div className="p-6 rounded-2xl border" style={{ backgroundColor: 'var(--bg-card)', borderColor: 'var(--border-color)' }}> | |
| <h3 className="text-lg font-semibold mb-4 flex items-center gap-2" style={{ color: 'var(--text-primary)' }}> | |
| <Play className="w-5 h-5 text-purple-400" /> Predict Cluster for New Data | |
| </h3> | |
| <p className="text-sm mb-6" style={{ color: 'var(--text-muted)' }}> | |
| Enter feature values to predict which cluster a new data point belongs to | |
| </p> | |
| {clusteringResult.feature_columns ? ( | |
| <> | |
| <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-6"> | |
| {clusteringResult.feature_columns.map((feature: string) => { | |
| const stats = clusteringResult.feature_stats?.[feature]; | |
| return ( | |
| <div key={feature}> | |
| <label className="block text-sm font-medium mb-1" style={{ color: 'var(--text-primary)' }}> | |
| {feature} | |
| {stats && <span className="text-xs ml-2" style={{ color: 'var(--text-muted)' }}>({stats.type})</span>} | |
| </label> | |
| {stats?.type === 'categorical' && stats.categories ? ( | |
| <select | |
| value={clusterPredictionInput[feature] || ''} | |
| onChange={(e) => setClusterPredictionInput(prev => ({ ...prev, [feature]: e.target.value }))} | |
| className="w-full p-3 rounded-xl border bg-transparent outline-none focus:border-purple-500 transition-all" | |
| style={{ borderColor: 'var(--border-color)', color: 'var(--text-primary)' }} | |
| > | |
| {stats.categories.map((cat: string, idx: number) => ( | |
| <option key={cat} value={idx} style={{ backgroundColor: isDark ? '#1f2937' : '#fff', color: isDark ? '#fff' : '#000' }}> | |
| {cat} | |
| </option> | |
| ))} | |
| </select> | |
| ) : ( | |
| <input | |
| type="number" | |
| value={clusterPredictionInput[feature] || ''} | |
| onChange={(e) => setClusterPredictionInput(prev => ({ ...prev, [feature]: e.target.value }))} | |
| placeholder={stats ? `Range: ${stats.min?.toFixed(1)} - ${stats.max?.toFixed(1)}` : 'Enter value'} | |
| className="w-full p-3 rounded-xl border bg-transparent outline-none focus:border-purple-500 transition-all" | |
| style={{ borderColor: 'var(--border-color)', color: 'var(--text-primary)' }} | |
| /> | |
| )} | |
| {stats && <p className="text-xs mt-1" style={{ color: 'var(--text-muted)' }}>Mean: {stats.mean?.toFixed(2)}</p>} | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| <button | |
| onClick={handlePredictCluster} | |
| disabled={clusterPredicting} | |
| className="px-6 py-3 bg-gradient-to-r from-purple-600 to-pink-600 text-white rounded-xl font-medium hover:from-purple-500 hover:to-pink-500 disabled:opacity-50 transition-all flex items-center gap-2" | |
| > | |
| {clusterPredicting ? ( | |
| <><RefreshCw className="w-5 h-5 animate-spin" /> Predicting...</> | |
| ) : ( | |
| <><Play className="w-5 h-5" /> Get Cluster Prediction</> | |
| )} | |
| </button> | |
| {/* Prediction Result */} | |
| {clusterPredictionResult && ( | |
| <motion.div | |
| initial={{ opacity: 0, y: 10 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| className="mt-6 p-6 rounded-xl border-2 border-purple-500/50" | |
| style={{ backgroundColor: isDark ? 'rgba(168, 85, 247, 0.1)' : 'rgba(168, 85, 247, 0.05)' }} | |
| > | |
| <p className="text-sm mb-2" style={{ color: 'var(--text-muted)' }}>Predicted Cluster</p> | |
| <p className="text-4xl font-bold text-purple-400 mb-2">{clusterPredictionResult.cluster_name}</p> | |
| {clusterPredictionResult.confidence && ( | |
| <p className="text-sm" style={{ color: 'var(--text-muted)' }}> | |
| Confidence: <span className="text-purple-400 font-medium">{(clusterPredictionResult.confidence * 100).toFixed(1)}%</span> | |
| </p> | |
| )} | |
| {clusterPredictionResult.cluster_description && ( | |
| <p className="text-sm mt-2" style={{ color: 'var(--text-muted)' }}>{clusterPredictionResult.cluster_description}</p> | |
| )} | |
| </motion.div> | |
| )} | |
| </> | |
| ) : ( | |
| <div className="text-center py-8"> | |
| <p style={{ color: 'var(--text-muted)' }}>Feature information not available. Please re-run clustering.</p> | |
| </div> | |
| )} | |
| </div> | |
| )} | |
| </motion.div> | |
| </div> | |
| ); | |
| } | |
| // Supervised results view - original code | |
| // At this point, result must exist (we've handled null cases above) | |
| if (!result) { | |
| return null; // TypeScript guard - this shouldn't happen | |
| } | |
| const metrics = result.best_model?.metrics || {}; | |
| // Determine if classification or regression based on available metrics | |
| const isClassification = metrics.accuracy !== undefined || metrics.f1 !== undefined || metrics.precision !== undefined; | |
| const isRegression = metrics.r2 !== undefined || metrics.rmse !== undefined; | |
| let bestMetric: [string, number] = ['accuracy', 0]; | |
| if (metrics.accuracy !== undefined) { | |
| bestMetric = ['accuracy', metrics.accuracy]; | |
| } else if (metrics.f1 !== undefined) { | |
| bestMetric = ['f1', metrics.f1]; | |
| } else if (metrics.r2 !== undefined) { | |
| bestMetric = ['r2', metrics.r2]; | |
| } else { | |
| const entries = Object.entries(metrics); | |
| if (entries.length > 0) { | |
| bestMetric = entries[0] as [string, number]; | |
| } | |
| } | |
| const rankedFeatures = (result.feature_importance && result.feature_importance.length > 0) | |
| ? result.feature_importance | |
| : (result.feature_columns || []).map((f, i) => ({ | |
| feature: f, | |
| importance: 1 / (result.feature_columns?.length || 1), | |
| rank: i + 1 | |
| })); | |
| // Determine if this is an NLP-trained model (either single NLP or NLP in multi-mode) | |
| const isNlpMode = result.mode === 'nlp' || | |
| (result as any).modes_trained?.includes('nlp') || | |
| (result as any).results_per_mode?.nlp?.success || | |
| result.best_model?.name?.toLowerCase().includes('vectorizer') || | |
| result.best_model?.name?.toLowerCase().includes('tfidf'); | |
| // Get text column for NLP mode | |
| const nlpTextColumn = (result as any).results_per_mode?.nlp?.text_column || | |
| (result as any).primary_text_col || | |
| null; | |
| // Build inputFeatures with proper NLP support | |
| let inputFeatures: FeatureMetadata[] = (result.feature_metadata && result.feature_metadata.length > 0) | |
| ? result.feature_metadata | |
| : rankedFeatures.map(f => ({ | |
| name: f.feature, | |
| type: 'numeric' as const, | |
| min: 0, | |
| max: 100, | |
| mean: 50, | |
| options: undefined as string[] | undefined, | |
| placeholder: undefined as string | undefined | |
| })); | |
| // If NLP mode and no text input in feature_metadata, add the text column | |
| if (isNlpMode && nlpTextColumn && !inputFeatures.some(f => f.name === nlpTextColumn)) { | |
| inputFeatures = [{ | |
| name: nlpTextColumn, | |
| type: 'text', | |
| placeholder: `Enter ${nlpTextColumn} for NLP prediction...` | |
| }, ...inputFeatures.filter(f => f.name !== nlpTextColumn)]; | |
| } | |
| // If NLP mode and feature_metadata is empty, create text input | |
| if (isNlpMode && inputFeatures.length === 0 && nlpTextColumn) { | |
| inputFeatures = [{ | |
| name: nlpTextColumn, | |
| type: 'text', | |
| placeholder: `Enter ${nlpTextColumn} for NLP prediction...` | |
| }]; | |
| } | |
| return ( | |
| <div className="space-y-4 w-full"> | |
| {/* Header */} | |
| <motion.div | |
| initial={{ opacity: 0, y: -20 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| className="flex flex-col md:flex-row md:items-center justify-between gap-4" | |
| > | |
| <div className="flex items-center gap-4 w-full md:w-auto"> | |
| <div className="min-w-0"> | |
| <h1 className="text-2xl font-bold flex items-center gap-3 truncate" style={{ color: 'var(--text-primary)' }}> | |
| <Brain className="w-8 h-8 flex-shrink-0" style={{ color: isDark ? '#4ade80' : '#16a34a' }} /> | |
| <span className="truncate">ML Predictions</span> | |
| </h1> | |
| <p className="text-sm truncate" style={{ color: 'var(--text-muted)' }}> | |
| Target: <span className="font-medium" style={{ color: isDark ? '#4ade80' : '#16a34a' }}>{result.target_column}</span> | |
| {' • '} | |
| Task: <span className="text-blue-400 font-medium">{result.task_type}</span> | |
| {' • '} | |
| <span className="text-amber-400 font-medium">{result.processing_time_seconds?.toFixed(1)}s</span> | |
| </p> | |
| </div> | |
| </div> | |
| <div className="flex items-center gap-3 w-full md:w-auto"> | |
| <button | |
| onClick={async () => { | |
| try { | |
| const userId = getUserIdSync(); | |
| // Clear stale local cache so fresh backend files are loaded | |
| localStorage.removeItem(`ide_files_${userId}`); | |
| const res = await api.get(`/api/v1/ide/project/${userId}`); | |
| if (res.data.success) { | |
| setIdeFiles(res.data.files); | |
| setShowIde(true); | |
| } else { | |
| alert("Could not load project: " + res.data.detail); | |
| } | |
| } catch (e: any) { | |
| console.error("Failed to load IDE project", e); | |
| alert("Failed to load IDE project: " + (e.response?.data?.detail || e.message)); | |
| } | |
| }} | |
| className="w-full md:w-auto px-5 py-2.5 rounded-xl border bg-indigo-500/10 hover:bg-indigo-500/20 border-indigo-500/30 transition-colors flex items-center justify-center gap-2 text-indigo-400 font-bold shadow-lg shadow-indigo-500/10" | |
| > | |
| <Code2 className="w-4 h-4" /> | |
| <span>Launch Web IDE</span> | |
| </button> | |
| <button | |
| onClick={() => { | |
| setResult(null); | |
| loadExistingFiles(); | |
| }} | |
| className="w-full md:w-auto px-4 py-2.5 rounded-xl border transition-colors flex items-center justify-center gap-2" | |
| style={{ borderColor: 'var(--border-color)', color: 'var(--text-muted)' }} | |
| > | |
| <RefreshCw className="w-4 h-4" /> | |
| <span>New Training</span> | |
| </button> | |
| </div> | |
| </motion.div> | |
| {/* Key Metrics - Best Model + All Metrics + Production Intelligence + Models + Features */} | |
| <div className="grid grid-cols-1 md:grid-cols-5 gap-4"> | |
| <motion.div | |
| initial={{ opacity: 0, y: 20 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| transition={{ delay: 0.1 }} | |
| className="p-6 rounded-2xl border" | |
| style={{ backgroundColor: 'var(--bg-card)', borderColor: 'var(--border-color)' }} | |
| > | |
| <div className="flex items-center gap-3 mb-4"> | |
| <div className="p-2 rounded-lg bg-primary-500/20"> | |
| <Award className="w-5 h-5" style={{ color: isDark ? '#4ade80' : '#16a34a' }} /> | |
| </div> | |
| <span className="text-sm font-medium" style={{ color: 'var(--text-muted)' }}>Best Model</span> | |
| </div> | |
| <p className="text-2xl font-bold text-primary-500">{result.best_model.name}</p> | |
| </motion.div> | |
| <motion.div | |
| initial={{ opacity: 0, y: 20 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| transition={{ delay: 0.15 }} | |
| className="p-6 rounded-2xl border" | |
| style={{ backgroundColor: 'var(--bg-card)', borderColor: 'var(--border-color)' }} | |
| > | |
| <div className="flex items-center gap-3 mb-4"> | |
| <div className="p-2 rounded-lg bg-blue-500/20"> | |
| <Target className="w-5 h-5 text-blue-400" /> | |
| </div> | |
| <span className="text-sm font-medium" style={{ color: 'var(--text-muted)' }}>{bestMetric[0]?.toUpperCase()}</span> | |
| </div> | |
| <p className="text-2xl font-bold" style={{ color: getMetricColor(bestMetric[1] as number) }}> | |
| {isRegression && !isClassification | |
| ? (bestMetric[1] as number).toFixed(4) | |
| : `${((bestMetric[1] as number) * 100).toFixed(1)}%`} | |
| </p> | |
| </motion.div> | |
| {/* PRODUCTION INTELLIGENCE: Reliability Score Card - ALL MODES */} | |
| <motion.div | |
| initial={{ opacity: 0, y: 20 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| transition={{ delay: 0.17 }} | |
| className="p-6 rounded-2xl border relative overflow-hidden" | |
| style={{ | |
| backgroundColor: 'var(--bg-card)', | |
| borderColor: (result.reliability_score || result.best_model?.reliability || 75) >= 80 ? '#22c55e' : | |
| (result.reliability_score || result.best_model?.reliability || 75) >= 60 ? '#f59e0b' : '#ef4444' | |
| }} | |
| > | |
| <div className="absolute top-0 right-0 w-20 h-20 opacity-10"> | |
| <ShieldCheck className="w-full h-full" style={{ | |
| color: (result.reliability_score || result.best_model?.reliability || 75) >= 80 ? '#22c55e' : | |
| (result.reliability_score || result.best_model?.reliability || 75) >= 60 ? '#f59e0b' : '#ef4444' | |
| }} /> | |
| </div> | |
| <div className="flex items-center gap-3 mb-4"> | |
| <div className={`p-2 rounded-lg ${ | |
| (result.reliability_score || result.best_model?.reliability || 75) >= 80 ? 'bg-green-500/20' : | |
| (result.reliability_score || result.best_model?.reliability || 75) >= 60 ? 'bg-amber-500/20' : 'bg-red-500/20' | |
| }`}> | |
| <ShieldCheck className="w-5 h-5" style={{ | |
| color: (result.reliability_score || result.best_model?.reliability || 75) >= 80 ? '#22c55e' : | |
| (result.reliability_score || result.best_model?.reliability || 75) >= 60 ? '#f59e0b' : '#ef4444' | |
| }} /> | |
| </div> | |
| <span className="text-sm font-medium" style={{ color: 'var(--text-muted)' }}>Reliability</span> | |
| </div> | |
| <p className="text-2xl font-bold" style={{ | |
| color: (result.reliability_score || result.best_model?.reliability || 75) >= 80 ? '#22c55e' : | |
| (result.reliability_score || result.best_model?.reliability || 75) >= 60 ? '#f59e0b' : '#ef4444' | |
| }}> | |
| {(result.reliability_score || result.best_model?.reliability || 75).toFixed(0)}/100 | |
| </p> | |
| <p className="text-xs mt-1" style={{ color: 'var(--text-muted)' }}> | |
| {(result.reliability_score || result.best_model?.reliability || 75) >= 80 ? '✓ Production Ready' : | |
| (result.reliability_score || result.best_model?.reliability || 75) >= 60 ? '⚠ Moderate' : '⚠ Needs Review'} | |
| </p> | |
| </motion.div> | |
| <motion.div | |
| initial={{ opacity: 0, y: 20 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| transition={{ delay: 0.2 }} | |
| className="p-6 rounded-2xl border" | |
| style={{ backgroundColor: 'var(--bg-card)', borderColor: 'var(--border-color)' }} | |
| > | |
| <div className="flex items-center gap-3 mb-4"> | |
| <div className="p-2 rounded-lg bg-emerald-500/20"> | |
| <Layers className="w-5 h-5 text-emerald-400" /> | |
| </div> | |
| <span className="text-sm font-medium" style={{ color: 'var(--text-muted)' }}>Models Trained</span> | |
| </div> | |
| <p className="text-2xl font-bold text-emerald-400">{result.all_models?.length || 0}</p> | |
| </motion.div> | |
| <motion.div | |
| initial={{ opacity: 0, y: 20 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| transition={{ delay: 0.25 }} | |
| className="p-6 rounded-2xl border" | |
| style={{ backgroundColor: 'var(--bg-card)', borderColor: 'var(--border-color)' }} | |
| > | |
| <div className="flex items-center gap-3 mb-4"> | |
| <div className="p-2 rounded-lg bg-amber-500/20"> | |
| <Sparkles className="w-5 h-5 text-amber-400" /> | |
| </div> | |
| <span className="text-sm font-medium" style={{ color: 'var(--text-muted)' }}>Columns</span> | |
| </div> | |
| <p className="text-2xl font-bold text-amber-400">{result.data_summary?.columns || result.feature_importance?.length || 0}</p> | |
| </motion.div> | |
| </div> | |
| {/* PRODUCTION INTELLIGENCE: Leakage & Validation Warnings Banner - ALL MODES */} | |
| {(result.leakage_report?.has_leakage || (result.validation_warnings && result.validation_warnings.length > 0)) && ( | |
| <motion.div | |
| initial={{ opacity: 0, y: 20 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| className="p-4 rounded-xl border-2 border-amber-500/50 bg-amber-500/10" | |
| > | |
| <div className="flex items-start gap-3"> | |
| <AlertTriangle className="w-5 h-5 text-amber-400 flex-shrink-0 mt-0.5" /> | |
| <div className="flex-1"> | |
| <h4 className="font-semibold text-amber-400 mb-2">Production Intelligence Alerts</h4> | |
| {result.leakage_report?.has_leakage && ( | |
| <div className="mb-2"> | |
| <p className="text-sm text-amber-300"> | |
| 🚨 <strong>Data Leakage Detected & Fixed:</strong> {result.leakage_report.columns_removed.length} column(s) removed | |
| </p> | |
| <div className="flex flex-wrap gap-1 mt-1"> | |
| {result.leakage_report.columns_removed.map((col, idx) => ( | |
| <span key={idx} className="text-xs px-2 py-0.5 rounded bg-amber-500/30 text-amber-200"> | |
| {col} | |
| </span> | |
| ))} | |
| </div> | |
| </div> | |
| )} | |
| {result.validation_warnings && result.validation_warnings.length > 0 && ( | |
| <div> | |
| <p className="text-sm text-amber-300 mb-1">⚠️ <strong>Validation Warnings:</strong></p> | |
| <ul className="text-xs text-amber-200 space-y-1"> | |
| {result.validation_warnings.map((warning, idx) => ( | |
| <li key={idx}>• {warning}</li> | |
| ))} | |
| </ul> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| </motion.div> | |
| )} | |
| {/* Tabs - SUPERVISED ONLY (no clustering tab) */} | |
| <div className="overflow-x-auto no-scrollbar pb-2 md:pb-0"> | |
| <div | |
| className="flex gap-2 p-1.5 rounded-xl min-w-max md:min-w-0 border" | |
| style={{ backgroundColor: 'var(--bg-card)', borderColor: 'var(--border-color)' }} | |
| > | |
| {[ | |
| { id: 'overview', label: 'Overview', icon: PieChart }, | |
| { id: 'charts', label: 'ML Charts', icon: BarChart3 }, | |
| { id: 'features', label: 'Features', icon: TrendingUp }, | |
| { id: 'predict', label: 'Predict', icon: Play }, | |
| { id: 'playground', label: 'Playground', icon: Sliders }, | |
| { id: 'experiments', label: 'Experiments', icon: History }, | |
| { id: 'data', label: 'Data', icon: Database }, | |
| ].map((tab) => ( | |
| <button | |
| key={tab.id} | |
| onClick={() => setActiveTab(tab.id as any)} | |
| className={`flex-1 flex flex-shrink-0 items-center justify-center gap-2 px-4 py-3 rounded-lg font-medium transition-all whitespace-nowrap ${activeTab === tab.id | |
| ? 'bg-gradient-to-r from-primary-500 to-emerald-500 text-white shadow-lg' | |
| : '' | |
| }`} | |
| style={activeTab !== tab.id ? { color: 'var(--text-muted)' } : undefined} | |
| > | |
| <tab.icon className="w-4 h-4" /> | |
| {tab.label} | |
| </button> | |
| ))} | |
| </div> | |
| </div> | |
| {/* Tab Content */} | |
| <motion.div | |
| key={activeTab} | |
| initial={{ opacity: 0, y: 20 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| transition={{ duration: 0.3 }} | |
| > | |
| {activeTab === 'overview' && ( | |
| <div className="grid grid-cols-1 lg:grid-cols-3 gap-4 w-full"> | |
| <div className="lg:col-span-2 space-y-4"> | |
| {/* Multi-Mode Results (if applicable) */} | |
| {result.results_per_mode && Object.keys(result.results_per_mode).length > 1 && ( | |
| <div className="p-6 rounded-2xl border" style={{ backgroundColor: 'var(--bg-card)', borderColor: 'var(--border-color)' }}> | |
| <h3 className="text-lg font-semibold mb-4 flex items-center gap-2" style={{ color: 'var(--text-primary)' }}> | |
| <Layers className="w-5 h-5 text-purple-400" /> | |
| Multi-Mode Training Results | |
| </h3> | |
| <div className="grid grid-cols-1 md:grid-cols-3 gap-4"> | |
| {Object.entries(result.results_per_mode).map(([mode, modeResult]: [string, any]) => { | |
| const modeColors: Record<string, string> = { | |
| 'traditional': '#22c55e', | |
| 'nlp': '#3b82f6', | |
| 'deep_learning': '#ef4444' | |
| }; | |
| const modeIcons: Record<string, string> = { | |
| 'traditional': '🌲', | |
| 'nlp': '📝', | |
| 'deep_learning': '🧠' | |
| }; | |
| const modeLabels: Record<string, string> = { | |
| 'traditional': 'Traditional ML', | |
| 'nlp': 'NLP', | |
| 'deep_learning': 'Deep Learning' | |
| }; | |
| const isBest = result.best_overall?.mode === mode; | |
| return ( | |
| <div | |
| key={mode} | |
| className={`p-4 rounded-xl border ${isBest ? 'ring-2' : ''}`} | |
| style={{ | |
| borderColor: modeColors[mode], | |
| backgroundColor: isDark ? `${modeColors[mode]}15` : `${modeColors[mode]}10`, | |
| ['--tw-ring-color' as any]: isBest ? modeColors[mode] : undefined | |
| }} | |
| > | |
| <div className="flex items-center justify-between mb-2"> | |
| <span className="font-semibold flex items-center gap-2" style={{ color: modeColors[mode] }}> | |
| {modeIcons[mode]} {modeLabels[mode]} | |
| </span> | |
| {isBest && ( | |
| <span className="text-xs px-2 py-0.5 rounded-full text-white" style={{ backgroundColor: modeColors[mode] }}> | |
| BEST | |
| </span> | |
| )} | |
| </div> | |
| <p className="text-sm mb-1" style={{ color: 'var(--text-muted)' }}> | |
| Model: <strong style={{ color: 'var(--text-primary)' }}>{modeResult.best_model || modeResult.algorithm || modeResult.architecture || 'N/A'}</strong> | |
| </p> | |
| {modeResult.success ? ( | |
| <div className="mt-2 space-y-1"> | |
| {modeResult.metrics?.accuracy !== undefined && ( | |
| <div className="flex justify-between text-sm"> | |
| <span style={{ color: 'var(--text-muted)' }}>Accuracy</span> | |
| <span className="font-bold" style={{ color: modeColors[mode] }}>{(modeResult.metrics.accuracy * 100).toFixed(1)}%</span> | |
| </div> | |
| )} | |
| {modeResult.metrics?.precision !== undefined && ( | |
| <div className="flex justify-between text-sm"> | |
| <span style={{ color: 'var(--text-muted)' }}>Precision</span> | |
| <span className="font-bold" style={{ color: modeColors[mode] }}>{(modeResult.metrics.precision * 100).toFixed(1)}%</span> | |
| </div> | |
| )} | |
| {modeResult.metrics?.recall !== undefined && ( | |
| <div className="flex justify-between text-sm"> | |
| <span style={{ color: 'var(--text-muted)' }}>Recall</span> | |
| <span className="font-bold" style={{ color: modeColors[mode] }}>{(modeResult.metrics.recall * 100).toFixed(1)}%</span> | |
| </div> | |
| )} | |
| {modeResult.metrics?.f1 !== undefined && ( | |
| <div className="flex justify-between text-sm"> | |
| <span style={{ color: 'var(--text-muted)' }}>F1 Score</span> | |
| <span className="font-bold" style={{ color: modeColors[mode] }}>{(modeResult.metrics.f1 * 100).toFixed(1)}%</span> | |
| </div> | |
| )} | |
| {modeResult.metrics?.roc_auc !== undefined && ( | |
| <div className="flex justify-between text-sm"> | |
| <span style={{ color: 'var(--text-muted)' }}>ROC-AUC</span> | |
| <span className="font-bold" style={{ color: modeColors[mode] }}>{(modeResult.metrics.roc_auc * 100).toFixed(1)}%</span> | |
| </div> | |
| )} | |
| {modeResult.metrics?.r2 !== undefined && ( | |
| <div className="flex justify-between text-sm"> | |
| <span style={{ color: 'var(--text-muted)' }}>R²</span> | |
| <span className="font-bold" style={{ color: modeColors[mode] }}>{(modeResult.metrics.r2 as number).toFixed(4)}</span> | |
| </div> | |
| )} | |
| {modeResult.metrics?.rmse !== undefined && ( | |
| <div className="flex justify-between text-sm"> | |
| <span style={{ color: 'var(--text-muted)' }}>RMSE</span> | |
| <span className="font-bold" style={{ color: modeColors[mode] }}>{(modeResult.metrics.rmse as number).toFixed(4)}</span> | |
| </div> | |
| )} | |
| {modeResult.metrics?.mae !== undefined && ( | |
| <div className="flex justify-between text-sm"> | |
| <span style={{ color: 'var(--text-muted)' }}>MAE</span> | |
| <span className="font-bold" style={{ color: modeColors[mode] }}>{(modeResult.metrics.mae as number).toFixed(4)}</span> | |
| </div> | |
| )} | |
| </div> | |
| ) : ( | |
| <p className="text-2xl font-bold" style={{ color: modeColors[mode] }}>❌ Failed</p> | |
| )} | |
| {modeResult.error && ( | |
| <p className="text-xs text-red-400 mt-1">{modeResult.error}</p> | |
| )} | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| </div> | |
| )} | |
| {/* All Models */} | |
| <div className="p-6 rounded-2xl border" style={{ backgroundColor: 'var(--bg-card)', borderColor: 'var(--border-color)' }}> | |
| <h3 className="text-lg font-semibold mb-4 flex items-center gap-2" style={{ color: 'var(--text-primary)' }}> | |
| <Activity className="w-5 h-5 text-blue-400" /> | |
| All Models Performance | |
| <span className="text-xs px-2 py-0.5 bg-green-500/20 text-green-400 rounded ml-2"> | |
| Production Validated | |
| </span> | |
| </h3> | |
| <div className="overflow-x-auto"> | |
| <table className="w-full text-sm"> | |
| <thead> | |
| <tr className="border-b" style={{ borderColor: 'var(--border-color)' }}> | |
| <th className="text-left py-2 px-2 font-medium" style={{ color: 'var(--text-muted)' }}>Model</th> | |
| {isClassification ? ( | |
| <> | |
| <th className="text-right py-2 px-2 font-medium" style={{ color: 'var(--text-muted)' }}>Accuracy</th> | |
| <th className="text-right py-2 px-2 font-medium" style={{ color: 'var(--text-muted)' }}>Precision</th> | |
| <th className="text-right py-2 px-2 font-medium" style={{ color: 'var(--text-muted)' }}>Recall</th> | |
| <th className="text-right py-2 px-2 font-medium" style={{ color: 'var(--text-muted)' }}>F1</th> | |
| <th className="text-right py-2 px-2 font-medium" style={{ color: 'var(--text-muted)' }}>ROC-AUC</th> | |
| </> | |
| ) : ( | |
| <> | |
| <th className="text-right py-2 px-2 font-medium" style={{ color: 'var(--text-muted)' }}>R²</th> | |
| <th className="text-right py-2 px-2 font-medium" style={{ color: 'var(--text-muted)' }}>MSE</th> | |
| <th className="text-right py-2 px-2 font-medium" style={{ color: 'var(--text-muted)' }}>RMSE</th> | |
| <th className="text-right py-2 px-2 font-medium" style={{ color: 'var(--text-muted)' }}>MAE</th> | |
| </> | |
| )} | |
| <th className="text-center py-2 px-2 font-medium" style={{ color: 'var(--text-muted)' }}>Status</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {((result.leaderboard && result.leaderboard.length > 0) ? result.leaderboard : result.all_models)?.slice(0, 10).map((model: any, i: number) => { | |
| const modelName = model.model || model.name; | |
| const modeLabel = model.mode ? `[${model.mode}] ` : ''; | |
| const isBest = modelName === (result.best_overall?.name || result.best_model?.name); | |
| const m = model.metrics || {}; | |
| // Theme-aware metric colors (darker for light mode contrast) | |
| const metricColors = isDark | |
| ? { acc: '#22c55e', prec: '#3b82f6', rec: '#a855f7', f1: '#f59e0b', roc: '#ef4444', text: 'var(--text-primary)' } | |
| : { acc: '#15803d', prec: '#1d4ed8', rec: '#7e22ce', f1: '#b45309', roc: '#dc2626', text: '#0f172a' }; | |
| return ( | |
| <tr key={i} className="border-b" style={{ borderColor: 'var(--border-color)', backgroundColor: isBest ? (isDark ? 'rgba(34, 197, 94, 0.1)' : 'rgba(34, 197, 94, 0.08)') : undefined }}> | |
| <td className="py-2 px-2 font-medium truncate max-w-[200px]" style={{ color: metricColors.text }} title={modelName}> | |
| {modeLabel}{modelName} | |
| {isBest && <span className="ml-1 text-xs px-1.5 py-0.5 bg-primary-500 text-white rounded">BEST</span>} | |
| </td> | |
| {isClassification ? ( | |
| <> | |
| <td className="text-right py-2 px-2 font-mono" style={{ color: metricColors.acc }}>{m.accuracy !== undefined ? `${(m.accuracy * 100).toFixed(1)}%` : '-'}</td> | |
| <td className="text-right py-2 px-2 font-mono" style={{ color: metricColors.prec }}>{m.precision !== undefined ? `${(m.precision * 100).toFixed(1)}%` : '-'}</td> | |
| <td className="text-right py-2 px-2 font-mono" style={{ color: metricColors.rec }}>{m.recall !== undefined ? `${(m.recall * 100).toFixed(1)}%` : '-'}</td> | |
| <td className="text-right py-2 px-2 font-mono" style={{ color: metricColors.f1 }}>{m.f1 !== undefined ? `${(m.f1 * 100).toFixed(1)}%` : '-'}</td> | |
| <td className="text-right py-2 px-2 font-mono" style={{ color: metricColors.roc }}>{m.roc_auc !== undefined ? `${(m.roc_auc * 100).toFixed(1)}%` : '-'}</td> | |
| </> | |
| ) : ( | |
| <> | |
| <td className="text-right py-2 px-2 font-mono" style={{ color: metricColors.acc }}>{m.r2 !== undefined ? (m.r2 as number).toFixed(4) : '-'}</td> | |
| <td className="text-right py-2 px-2 font-mono" style={{ color: metricColors.prec }}>{m.mse !== undefined ? (m.mse as number).toFixed(4) : '-'}</td> | |
| <td className="text-right py-2 px-2 font-mono" style={{ color: metricColors.rec }}>{m.rmse !== undefined ? (m.rmse as number).toFixed(4) : '-'}</td> | |
| <td className="text-right py-2 px-2 font-mono" style={{ color: metricColors.f1 }}>{m.mae !== undefined ? (m.mae as number).toFixed(4) : '-'}</td> | |
| </> | |
| )} | |
| <td className="text-center py-2 px-2"> | |
| <span className={`text-xs px-1.5 py-0.5 rounded ${ | |
| (model.reliability_score || model.reliability || 75) >= 80 ? 'bg-green-500/20 text-green-400' : | |
| (model.reliability_score || model.reliability || 75) >= 60 ? 'bg-amber-500/20 text-amber-400' : 'bg-red-500/20 text-red-400' | |
| }`}> | |
| {model.warning ? '⚠️' : '✓'} {model.reliability_score || model.reliability || 75} | |
| </span> | |
| </td> | |
| </tr> | |
| ); | |
| })} | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| {/* Insights */} | |
| <div className="p-6 rounded-2xl border" style={{ backgroundColor: 'var(--bg-card)', borderColor: 'var(--border-color)' }}> | |
| <h3 className="text-lg font-semibold mb-4 flex items-center gap-2" style={{ color: 'var(--text-primary)' }}> | |
| <Sparkles className="w-5 h-5 text-amber-400" /> | |
| AI Insights | |
| </h3> | |
| <div className="space-y-3"> | |
| {result.insights?.slice(0, 5).map((insight, i) => ( | |
| <div | |
| key={i} | |
| className="p-4 rounded-xl border-l-4 border-l-green-500" | |
| style={{ backgroundColor: isDark ? 'rgba(255,255,255,0.03)' : 'rgba(0,0,0,0.02)' }} | |
| > | |
| <p className="text-sm" style={{ color: 'var(--text-primary)' }}>{insight}</p> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| {activeTab === 'playground' && ( | |
| <div className="p-6 rounded-2xl border" style={{ backgroundColor: 'var(--bg-card)', borderColor: 'var(--border-color)' }}> | |
| <h3 className="text-lg font-semibold mb-4 flex items-center gap-2" style={{ color: 'var(--text-primary)' }}> | |
| <Sliders className="w-5 h-5 text-violet-400" /> | |
| Interactive Prediction Playground | |
| </h3> | |
| <PlaygroundTab | |
| mode={(() => { | |
| // For multi-mode training, use the best mode from result | |
| // or 'auto' to let backend detect | |
| if (result?.mode) return result.mode as 'traditional' | 'nlp' | 'deep_learning'; | |
| if ((result as any)?.best_overall?.mode) return (result as any).best_overall.mode; | |
| // Check modes_trained for multi-mode | |
| const modesTrained = (result as any)?.modes_trained as string[] | undefined; | |
| if (modesTrained && modesTrained.length > 0) { | |
| // Return first trained mode, preferring traditional > nlp > deep_learning | |
| if (modesTrained.includes('traditional')) return 'traditional'; | |
| if (modesTrained.includes('nlp')) return 'nlp'; | |
| if (modesTrained.includes('deep_learning')) return 'deep_learning'; | |
| } | |
| // Fallback to 'auto' to let backend auto-detect | |
| return 'auto' as any; | |
| })()} | |
| onPredictionMade={(pred) => { | |
| setPredictionResult(pred); | |
| setExplainInputValues(pred.input_values || {}); | |
| }} | |
| /> | |
| </div> | |
| )} | |
| {activeTab === 'charts' && (() => { | |
| const chartEntries = result.charts ? Object.entries(result.charts) | |
| .filter(([chartName]) => !['cluster_scatter', 'elbow_method', 'silhouette_comparison', | |
| 'silhouette_plot', 'dendrogram', 'cluster_distribution'].includes(chartName)) : []; | |
| const copyChartToClipboard = async (base64: string, chartName: string) => { | |
| try { | |
| const res = await fetch(base64); | |
| const blob = await res.blob(); | |
| await navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })]); | |
| setCopiedChart(chartName); | |
| setTimeout(() => setCopiedChart(null), 2000); | |
| } catch { | |
| // fallback: open in new tab | |
| const w = window.open(); | |
| if (w) { w.document.write(`<img src="${base64}" />`); } | |
| } | |
| }; | |
| const exportAllChartsToPPT = async () => { | |
| if (chartEntries.length === 0) return; | |
| setExportingPPT(true); | |
| try { | |
| const pptx = new PptxGenJS(); | |
| pptx.title = `ML Charts - ${result.target_column || 'Model'}`; | |
| pptx.author = 'DataVision AI'; | |
| pptx.layout = 'LAYOUT_WIDE'; | |
| for (const [chartName, chartBase64] of chartEntries) { | |
| let displayName = chartName; | |
| if (chartName.startsWith('ml_')) displayName = chartName.slice(3); | |
| else if (chartName.startsWith('nlp_')) displayName = chartName.slice(4); | |
| else if (chartName.startsWith('dl_')) displayName = chartName.slice(3); | |
| const title = displayName.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase()); | |
| const slide = pptx.addSlide(); | |
| slide.addText(title, { | |
| x: 0.5, y: 0.2, w: '90%', h: 0.6, | |
| fontSize: 22, bold: true, color: '333333', | |
| fontFace: 'Segoe UI' | |
| }); | |
| const imgData = (chartBase64 as string).replace(/^data:image\/\w+;base64,/, ''); | |
| slide.addImage({ | |
| data: `image/png;base64,${imgData}`, | |
| x: 0.8, y: 1.0, w: 11.5, h: 5.8, | |
| sizing: { type: 'contain', w: 11.5, h: 5.8 } | |
| }); | |
| } | |
| await pptx.writeFile({ fileName: `ML_Charts_${result.target_column || 'Model'}.pptx` }); | |
| } catch (e) { | |
| console.error('PPT export failed:', e); | |
| } finally { | |
| setExportingPPT(false); | |
| } | |
| }; | |
| return ( | |
| <div className="space-y-3"> | |
| {/* Export toolbar */} | |
| {!chartsLoading && chartEntries.length > 0 && ( | |
| <div className="flex items-center justify-between px-1"> | |
| <p className="text-sm font-medium" style={{ color: 'var(--text-muted)' }}> | |
| {chartEntries.length} chart{chartEntries.length !== 1 ? 's' : ''} generated | |
| </p> | |
| <button | |
| onClick={exportAllChartsToPPT} | |
| disabled={exportingPPT} | |
| className="flex items-center gap-2 px-4 py-2 rounded-xl text-sm font-medium transition-all hover:scale-105" | |
| style={{ | |
| backgroundColor: isDark ? 'rgba(99,102,241,0.2)' : 'rgba(99,102,241,0.1)', | |
| color: isDark ? '#a5b4fc' : '#4f46e5', | |
| border: `1px solid ${isDark ? 'rgba(99,102,241,0.3)' : 'rgba(99,102,241,0.2)'}` | |
| }} | |
| > | |
| {exportingPPT ? ( | |
| <><RefreshCw className="w-4 h-4 animate-spin" /> Exporting...</> | |
| ) : ( | |
| <><Presentation className="w-4 h-4" /> Export All to PPT</> | |
| )} | |
| </button> | |
| </div> | |
| )} | |
| <div className="grid grid-cols-1 lg:grid-cols-2 gap-3 w-full"> | |
| {chartsLoading && ( | |
| <div className="col-span-2 text-center p-12 rounded-2xl border" style={{ backgroundColor: 'var(--bg-card)', borderColor: 'var(--border-color)' }}> | |
| <RefreshCw className="w-16 h-16 mx-auto mb-4 animate-spin" style={{ color: isDark ? '#4ade80' : '#16a34a' }} /> | |
| <p className="text-lg font-medium mb-2" style={{ color: 'var(--text-primary)' }}>Loading Charts...</p> | |
| <p className="text-sm" style={{ color: 'var(--text-muted)' }}>Fetching ML visualizations from server</p> | |
| </div> | |
| )} | |
| {/* Filter out clustering-specific charts from supervised view */} | |
| {!chartsLoading && chartEntries | |
| .map(([chartName, chartBase64]) => { | |
| // Format chart name: remove prefix and add mode label | |
| let displayName = chartName; | |
| let modeLabel = ''; | |
| if (chartName.startsWith('ml_')) { | |
| displayName = chartName.slice(3); | |
| modeLabel = 'Traditional ML'; | |
| } else if (chartName.startsWith('nlp_')) { | |
| displayName = chartName.slice(4); | |
| modeLabel = 'NLP'; | |
| } else if (chartName.startsWith('dl_')) { | |
| displayName = chartName.slice(3); | |
| modeLabel = 'Deep Learning'; | |
| } | |
| const formattedName = displayName.replace(/_/g, ' '); | |
| return ( | |
| <div | |
| key={chartName} | |
| className="rounded-2xl border overflow-hidden transition-all hover:shadow-lg" | |
| style={{ backgroundColor: 'var(--bg-card)', borderColor: 'var(--border-color)' }} | |
| > | |
| <div className="flex items-center gap-2 px-4 py-3 border-b" style={{ borderColor: 'var(--border-color)' }}> | |
| <div className="p-1.5 rounded-lg bg-gradient-to-r from-primary-500/20 to-emerald-500/20"> | |
| <BarChart3 className="w-3.5 h-3.5" style={{ color: isDark ? '#4ade80' : '#16a34a' }} /> | |
| </div> | |
| <div className="flex-1 min-w-0"> | |
| <h3 className="text-sm font-semibold capitalize truncate" style={{ color: 'var(--text-primary)' }}> | |
| {formattedName} | |
| </h3> | |
| {modeLabel && ( | |
| <span className="text-xs px-1.5 py-0.5 rounded-full" | |
| style={{ | |
| backgroundColor: modeLabel === 'NLP' ? 'rgba(59, 130, 246, 0.2)' : | |
| modeLabel === 'Deep Learning' ? 'rgba(239, 68, 68, 0.2)' : | |
| 'rgba(34, 197, 94, 0.2)', | |
| color: modeLabel === 'NLP' ? '#3b82f6' : | |
| modeLabel === 'Deep Learning' ? '#ef4444' : '#22c55e' | |
| }}> | |
| {modeLabel} | |
| </span> | |
| )} | |
| </div> | |
| {/* Copy chart button */} | |
| <button | |
| onClick={() => copyChartToClipboard(chartBase64 as string, chartName)} | |
| className="p-1.5 rounded-lg transition-all hover:scale-110" | |
| style={{ | |
| backgroundColor: copiedChart === chartName | |
| ? (isDark ? 'rgba(34,197,94,0.2)' : 'rgba(34,197,94,0.1)') | |
| : (isDark ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.05)') | |
| }} | |
| title="Copy chart to clipboard" | |
| > | |
| {copiedChart === chartName | |
| ? <Check className="w-3.5 h-3.5 text-green-500" /> | |
| : <Copy className="w-3.5 h-3.5" style={{ color: 'var(--text-muted)' }} /> | |
| } | |
| </button> | |
| </div> | |
| <div className="flex items-center justify-center p-2" style={{ backgroundColor: isDark ? '#0f172a' : '#f8fafc' }}> | |
| <img src={chartBase64 as string} alt={chartName} className="rounded-lg" style={{ width: '100%', height: 'auto', display: 'block' }} /> | |
| </div> | |
| </div> | |
| ); | |
| })} | |
| {!chartsLoading && chartEntries.length === 0 && ( | |
| <div className="col-span-2 text-center p-12 rounded-2xl border" style={{ backgroundColor: 'var(--bg-card)', borderColor: 'var(--border-color)' }}> | |
| <BarChart3 className="w-16 h-16 mx-auto mb-4" style={{ color: 'var(--text-muted)' }} /> | |
| <p className="text-lg font-medium mb-2" style={{ color: 'var(--text-primary)' }}>No Charts Available</p> | |
| <p className="text-sm mb-4" style={{ color: 'var(--text-muted)' }}>Charts may not have been generated during training.</p> | |
| <button | |
| onClick={async () => { | |
| const userId = getUserIdSync(); | |
| setChartsLoading(true); | |
| const charts = await fetchChartsFromAPI(userId); | |
| if (Object.keys(charts).length > 0) { | |
| setResult(prev => prev ? { ...prev, charts } : prev); | |
| } | |
| setChartsLoading(false); | |
| }} | |
| className="px-4 py-2 rounded-lg bg-primary-500 text-white hover:bg-primary-600 transition-colors flex items-center gap-2 mx-auto" | |
| > | |
| <RefreshCw className="w-4 h-4" /> | |
| Refresh Charts | |
| </button> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| })()} | |
| {activeTab === 'features' && ( | |
| <div className="p-6 rounded-2xl border" style={{ backgroundColor: 'var(--bg-card)', borderColor: 'var(--border-color)' }}> | |
| <h3 className="text-lg font-semibold mb-6 flex items-center justify-between" style={{ color: 'var(--text-primary)' }}> | |
| <span>Feature Importance Ranking</span> | |
| <span className="text-sm font-normal px-3 py-1 rounded-full bg-primary-500/20" style={{ color: isDark ? '#4ade80' : '#16a34a' }}> | |
| {rankedFeatures.length} features | |
| </span> | |
| </h3> | |
| <div className="space-y-3 max-h-[600px] overflow-y-auto pr-2"> | |
| {rankedFeatures.map((f, i) => ( | |
| <div | |
| key={i} | |
| className="flex items-center gap-4 p-3 rounded-xl" | |
| style={{ backgroundColor: isDark ? 'rgba(255,255,255,0.03)' : 'rgba(0,0,0,0.02)' }} | |
| > | |
| <span className="w-8 h-8 rounded-lg bg-gradient-to-r from-primary-500 to-emerald-500 flex items-center justify-center text-white font-bold"> | |
| {f.rank || i + 1} | |
| </span> | |
| <div className="flex-1"> | |
| <span className="font-medium" style={{ color: 'var(--text-primary)' }}>{f.feature}</span> | |
| <div className="w-full rounded-full h-2 mt-1" style={{ backgroundColor: isDark ? '#374151' : '#e5e7eb' }}> | |
| <div className="h-2 rounded-full bg-gradient-to-r from-primary-500 to-emerald-500" style={{ width: `${f.importance * 100}%` }} /> | |
| </div> | |
| </div> | |
| <span className="font-bold" style={{ color: isDark ? '#4ade80' : '#16a34a' }}>{(f.importance * 100).toFixed(1)}%</span> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| )} | |
| {activeTab === 'predict' && ( | |
| <div className="p-6 rounded-2xl border" style={{ backgroundColor: 'var(--bg-card)', borderColor: 'var(--border-color)' }}> | |
| <h3 className="text-lg font-semibold mb-6" style={{ color: 'var(--text-primary)' }}> | |
| Make a Prediction with {result.best_model.name} | |
| </h3> | |
| <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-6"> | |
| {inputFeatures | |
| // Filter out ID/index columns that shouldn't be user inputs | |
| .filter((meta) => { | |
| const name = meta.name.toLowerCase(); | |
| // Skip unnamed columns, index columns, and ID columns | |
| if (name.startsWith('unnamed') || name === 'index' || name === '_id') return false; | |
| // Skip pure ID columns (but keep columns like 'movie_id_rating' that might be useful) | |
| if (name === 'id') return false; | |
| return true; | |
| }) | |
| .map((meta) => { | |
| const featureName = meta.name; | |
| const lowerName = featureName.toLowerCase(); | |
| // First check if it's a text column by heuristics (PRIORITY over backend type) | |
| const textKeywords = ['text', 'content', 'body', 'email', 'review', 'description', | |
| 'summary', 'message', 'overview', 'title', 'name', 'comment', 'note', 'bio', | |
| 'abstract', 'story', 'plot', 'tagline', 'headline']; | |
| const isHeuristicText = textKeywords.some(kw => lowerName.includes(kw)); | |
| const isExplicitText = meta.type === 'text'; | |
| const isNlpTask = (result as any).is_nlp_task && (result as any).primary_text_col === featureName; | |
| // Text if: explicitly marked OR heuristic match OR NLP task primary column | |
| const isText = isExplicitText || isHeuristicText || isNlpTask; | |
| // Check for datetime type (backend may send 'date' or 'datetime') | |
| const isDatetime = meta.type === 'datetime' || meta.type === 'date'; | |
| // Only numeric if NOT a text column and NOT datetime | |
| const isNumeric = !isText && !isDatetime && meta.type === 'numeric'; | |
| const isCategorical = !isText && !isNumeric && !isDatetime && (meta.type === 'categorical' || (meta.options && meta.options.length > 0)); | |
| return ( | |
| <div key={featureName} className={isText ? "col-span-full" : ""}> | |
| <label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-muted)' }}> | |
| {featureName} | |
| <span className="ml-2 text-xs opacity-60"> | |
| ({isText ? 'text input' : isNumeric ? 'numeric' : isDatetime ? 'date' : isCategorical ? 'select' : 'input'}) | |
| </span> | |
| {isText && <span className="ml-2 text-xs bg-blue-500/20 text-blue-400 px-1.5 py-0.5 rounded">Text Content</span>} | |
| {isDatetime && <span className="ml-2 text-xs bg-purple-500/20 text-purple-400 px-1.5 py-0.5 rounded">Date</span>} | |
| </label> | |
| {isText ? ( | |
| <textarea | |
| placeholder={meta.placeholder || `Enter ${featureName}...`} | |
| value={predictionInput[featureName] || ''} | |
| onChange={(e) => setPredictionInput({ ...predictionInput, [featureName]: e.target.value })} | |
| rows={featureName.toLowerCase().includes('overview') || featureName.toLowerCase().includes('description') ? 6 : 3} | |
| className="w-full px-4 py-3 rounded-xl border focus:outline-none focus:border-primary-500 transition-colors resize-none" | |
| style={{ | |
| backgroundColor: isDark ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.02)', | |
| borderColor: 'var(--border-color)', | |
| color: 'var(--text-primary)', | |
| }} | |
| /> | |
| ) : isDatetime ? ( | |
| <input | |
| type="date" | |
| value={predictionInput[featureName] || ''} | |
| onChange={(e) => setPredictionInput({ ...predictionInput, [featureName]: e.target.value })} | |
| min={typeof meta.min === 'string' ? meta.min.split('T')[0] : undefined} | |
| max={typeof meta.max === 'string' ? meta.max.split('T')[0] : undefined} | |
| className="w-full px-4 py-3 rounded-xl border focus:outline-none focus:border-primary-500 transition-colors" | |
| style={{ | |
| backgroundColor: isDark ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.02)', | |
| borderColor: 'var(--border-color)', | |
| color: 'var(--text-primary)', | |
| }} | |
| /> | |
| ) : isNumeric ? ( | |
| <input | |
| type="number" | |
| placeholder={meta.mean !== undefined ? `e.g. ${Number(meta.mean).toFixed(1)}` : `Enter ${featureName}`} | |
| value={predictionInput[featureName] || ''} | |
| onChange={(e) => setPredictionInput({ ...predictionInput, [featureName]: e.target.value })} | |
| className="w-full px-4 py-3 rounded-xl border focus:outline-none focus:border-primary-500 transition-colors" | |
| style={{ | |
| backgroundColor: isDark ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.02)', | |
| borderColor: 'var(--border-color)', | |
| color: 'var(--text-primary)', | |
| }} | |
| /> | |
| ) : isCategorical ? ( | |
| <select | |
| value={predictionInput[featureName] || ''} | |
| onChange={(e) => setPredictionInput({ ...predictionInput, [featureName]: e.target.value })} | |
| className="w-full px-4 py-3 rounded-xl border focus:outline-none focus:border-primary-500 transition-colors" | |
| style={{ | |
| backgroundColor: isDark ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.02)', | |
| borderColor: 'var(--border-color)', | |
| color: 'var(--text-primary)', | |
| }} | |
| > | |
| <option value="">Select {featureName}...</option> | |
| {(meta.options || []).map((opt: string) => ( | |
| <option key={opt} value={opt}>{opt}</option> | |
| ))} | |
| </select> | |
| ) : ( | |
| <input | |
| type="text" | |
| placeholder={`Enter ${featureName}`} | |
| value={predictionInput[featureName] || ''} | |
| onChange={(e) => setPredictionInput({ ...predictionInput, [featureName]: e.target.value })} | |
| className="w-full px-4 py-3 rounded-xl border focus:outline-none focus:border-primary-500 transition-colors" | |
| style={{ | |
| backgroundColor: isDark ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.02)', | |
| borderColor: 'var(--border-color)', | |
| color: 'var(--text-primary)', | |
| }} | |
| /> | |
| )} | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| <div className="flex flex-col sm:flex-row gap-4 mt-6"> | |
| <button | |
| onClick={async () => { | |
| try { | |
| // Detect the correct mode for prediction | |
| let predictMode = result.mode || 'traditional'; | |
| // Check if NLP was trained (for multi-mode or single NLP training) | |
| const wasNlpTrained = (result as any).modes_trained?.includes('nlp') || | |
| (result as any).results_per_mode?.nlp?.success || | |
| result.best_model?.name?.toLowerCase().includes('vectorizer') || | |
| result.best_model?.name?.toLowerCase().includes('tfidf') || | |
| result.best_model?.name?.toLowerCase().includes('nlp'); | |
| // Check if Deep Learning was trained | |
| const wasDLTrained = (result as any).modes_trained?.includes('deep_learning') || | |
| (result as any).results_per_mode?.deep_learning?.success || | |
| result.best_model?.name?.toLowerCase().includes('mlp') || | |
| result.best_model?.name?.toLowerCase().includes('neural'); | |
| // Determine best mode based on what was trained and best model name | |
| if (wasNlpTrained && (result as any).best_overall?.mode === 'nlp') { | |
| predictMode = 'nlp'; | |
| } else if (wasNlpTrained && result.best_model?.name?.toLowerCase().includes('vectorizer')) { | |
| predictMode = 'nlp'; | |
| } else if (wasDLTrained && (result as any).best_overall?.mode === 'deep_learning') { | |
| predictMode = 'deep_learning'; | |
| } else if (wasDLTrained && result.best_model?.name?.toLowerCase().includes('mlp')) { | |
| predictMode = 'deep_learning'; | |
| } | |
| const dataToSend = { | |
| user_id: getUserIdSync(), | |
| model_name: result.best_model.name, | |
| mode: predictMode, | |
| data: Object.fromEntries( | |
| Object.entries(predictionInput).map(([k, v]) => { | |
| // Find the feature metadata to determine type | |
| const meta = inputFeatures.find(f => f.name === k); | |
| if (meta?.type === 'numeric') { | |
| // Numeric — convert to number (handles "0" correctly) | |
| const num = parseFloat(v); | |
| return [k, isNaN(num) ? 0 : num]; | |
| } else if (meta?.type === 'categorical' || meta?.type === 'date' || meta?.type === 'datetime') { | |
| // Categorical / Date — keep as string | |
| return [k, v]; | |
| } else { | |
| // Text or unknown — keep as string | |
| return [k, v]; | |
| } | |
| }) | |
| ) | |
| }; | |
| console.log('[MLPredictions] Sending prediction with mode:', predictMode); | |
| const response = await fetch('/api/v2/automl/predict', { | |
| method: 'POST', | |
| headers: getAuthHeadersSync(), | |
| body: JSON.stringify(dataToSend) | |
| }); | |
| const data = await response.json(); | |
| setPredictionResult(data); | |
| } catch (e: any) { | |
| toast.error(`Prediction failed: ${e.message}`); | |
| } | |
| }} | |
| className="flex-1 px-6 py-3 bg-gradient-to-r from-primary-500 to-emerald-500 rounded-xl text-white font-semibold hover:opacity-90 transition-opacity flex items-center justify-center gap-2" | |
| > | |
| <Play className="w-5 h-5" /> | |
| Single Prediction | |
| </button> | |
| <label className="flex-1 px-6 py-3 bg-gradient-to-r from-blue-500 to-indigo-500 rounded-xl text-white font-semibold hover:opacity-90 transition-opacity flex items-center justify-center gap-2 cursor-pointer"> | |
| <Database className="w-5 h-5" /> | |
| Batch Predict (CSV) | |
| <input | |
| type="file" | |
| accept=".csv" | |
| className="hidden" | |
| onChange={async (e) => { | |
| const file = e.target.files?.[0]; | |
| if (!file) return; | |
| try { | |
| toast.success('Batch prediction started...'); | |
| const formData = new FormData(); | |
| formData.append('file', file); | |
| formData.append('model_name', result.best_model.name); | |
| const headers = getAuthHeadersSync(); | |
| delete headers['Content-Type']; // Let browser set boundary | |
| const res = await fetch('/api/v1/automl/batch-predict', { | |
| method: 'POST', | |
| headers: headers, | |
| body: formData | |
| }); | |
| if (!res.ok) throw new Error(await res.text()); | |
| const blob = await res.blob(); | |
| const url = window.URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = `predictions_${file.name}`; | |
| a.click(); | |
| toast.success('Batch predictions downloaded!'); | |
| } catch (err: any) { | |
| toast.error(`Batch predict failed: ${err.message}`); | |
| } | |
| e.target.value = ''; // Reset | |
| }} | |
| /> | |
| </label> | |
| </div> | |
| {predictionResult && ( | |
| <motion.div | |
| initial={{ opacity: 0, y: 10 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| className="mt-6 p-4 sm:p-6 rounded-xl border-2" | |
| style={{ | |
| backgroundColor: isDark ? 'rgba(34, 197, 94, 0.1)' : 'rgba(34, 197, 94, 0.05)', | |
| borderColor: isDark ? '#22c55e' : '#16a34a', | |
| }} | |
| > | |
| <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4"> | |
| <div> | |
| <p className="text-sm font-medium mb-1" style={{ color: 'var(--text-muted)' }}> | |
| Predicted label | |
| </p> | |
| <p | |
| className="text-3xl sm:text-4xl font-bold" | |
| style={{ color: isDark ? '#4ade80' : '#15803d' }} | |
| > | |
| {predictionResult.prediction} | |
| </p> | |
| </div> | |
| <div className="flex flex-col sm:flex-row items-start sm:items-center gap-3 sm:gap-4"> | |
| {predictionResult.probability && ( | |
| <div className="sm:text-right"> | |
| <p className="text-sm font-medium mb-1" style={{ color: 'var(--text-muted)' }}>Confidence</p> | |
| <div className="flex items-center gap-2"> | |
| {(() => { | |
| const probValues = Array.isArray(predictionResult.probability) | |
| ? predictionResult.probability | |
| : Object.values(predictionResult.probability as Record<string, number>); | |
| const maxProb = Math.max(...probValues); | |
| return ( | |
| <> | |
| <div | |
| className="w-20 sm:w-24 h-3 rounded-full overflow-hidden" | |
| style={{ backgroundColor: isDark ? '#374151' : '#e5e7eb' }} | |
| > | |
| <div | |
| className="h-full rounded-full bg-gradient-to-r from-green-500 to-amber-500 transition-all duration-300" | |
| style={{ width: `${maxProb * 100}%` }} | |
| /> | |
| </div> | |
| <span className="font-bold" style={{ color: isDark ? '#4ade80' : '#15803d' }}> | |
| {(maxProb * 100).toFixed(1)}% | |
| </span> | |
| </> | |
| ); | |
| })()} | |
| </div> | |
| </div> | |
| )} | |
| <button | |
| onClick={() => { | |
| setExplainInputValues(predictionInput); | |
| setShowExplainModal(true); | |
| }} | |
| className="w-full sm:w-auto px-3 py-2 rounded-lg transition-colors flex items-center justify-center gap-1.5 text-sm font-medium" | |
| style={{ | |
| backgroundColor: isDark ? 'rgba(245, 158, 11, 0.2)' : 'rgba(245, 158, 11, 0.1)', | |
| color: isDark ? '#fbbf24' : '#b45309', | |
| }} | |
| > | |
| <HelpCircle className="w-4 h-4" /> | |
| Why this prediction? | |
| </button> | |
| </div> | |
| </div> | |
| </motion.div> | |
| )} | |
| </div> | |
| )} | |
| {activeTab === 'experiments' && ( | |
| <div className="p-6 rounded-2xl border" style={{ backgroundColor: 'var(--bg-card)', borderColor: 'var(--border-color)' }}> | |
| <ModelHistory | |
| userId={getUserIdSync()} | |
| onModelChange={() => {}} | |
| /> | |
| </div> | |
| )} | |
| {activeTab === 'data' && ( | |
| <div className="p-8 rounded-2xl border border-dashed" style={{ backgroundColor: 'var(--bg-card)', borderColor: 'var(--border-color)' }}> | |
| <motion.div initial={{ scale: 0.9, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} className="max-w-4xl mx-auto"> | |
| <div className="text-center mb-8"> | |
| <div className="w-20 h-20 mx-auto bg-primary-500/20 rounded-full flex items-center justify-center mb-6"> | |
| <Database className="w-10 h-10" style={{ color: isDark ? '#4ade80' : '#16a34a' }} /> | |
| </div> | |
| <h2 className="text-2xl font-bold mb-3" style={{ color: 'var(--text-primary)' }}> | |
| Production Assets & Code Export | |
| </h2> | |
| <p className="leading-relaxed max-w-2xl mx-auto" style={{ color: 'var(--text-muted)' }}> | |
| Download trained model, cleaned dataset, and complete end-to-end ML code for local deployment. | |
| </p> | |
| </div> | |
| <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-5"> | |
| {/* Trained Model Download */} | |
| <div className="p-5 rounded-xl border text-center" style={{ borderColor: 'var(--border-color)', backgroundColor: isDark ? 'rgba(255,255,255,0.02)' : 'rgba(0,0,0,0.02)' }}> | |
| <div className="w-14 h-14 mx-auto bg-purple-500/20 rounded-full flex items-center justify-center mb-4"> | |
| <Brain className="w-7 h-7 text-purple-500" /> | |
| </div> | |
| <h3 className="text-lg font-semibold mb-2" style={{ color: 'var(--text-primary)' }}> | |
| Trained Model | |
| </h3> | |
| <p className="text-sm mb-4" style={{ color: 'var(--text-muted)' }}> | |
| Download the trained ML model (.pkl) for deployment. | |
| </p> | |
| <a | |
| href={`/api/v2/automl/download-model?user_id=${getUserIdSync()}`} | |
| className="inline-flex items-center gap-2 px-5 py-2.5 bg-purple-500 hover:bg-purple-600 text-white rounded-xl font-semibold transition-all hover:scale-105" | |
| > | |
| <Download className="w-4 h-4" /> | |
| Download .pkl | |
| </a> | |
| <p className="text-xs mt-3 flex items-center justify-center gap-2" style={{ color: 'var(--text-muted)' }}> | |
| <CheckCircle className="w-3 h-3 text-purple-400" /> | |
| {result.best_model?.name || 'ML Model'} | |
| </p> | |
| </div> | |
| {/* Cleaned Dataset Download */} | |
| <div className="p-5 rounded-xl border text-center" style={{ borderColor: 'var(--border-color)', backgroundColor: isDark ? 'rgba(255,255,255,0.02)' : 'rgba(0,0,0,0.02)' }}> | |
| <div className="w-14 h-14 mx-auto bg-emerald-500/20 rounded-full flex items-center justify-center mb-4"> | |
| <Database className="w-7 h-7 text-emerald-500" /> | |
| </div> | |
| <h3 className="text-lg font-semibold mb-2" style={{ color: 'var(--text-primary)' }}> | |
| Cleaned Dataset | |
| </h3> | |
| <p className="text-sm mb-4" style={{ color: 'var(--text-muted)' }}> | |
| Production-ready data with imputed values and encoded features. | |
| </p> | |
| {result.cleaned_file ? ( | |
| <a | |
| href={`/api/v1/files/${getUserIdSync()}/${result.cleaned_file}/download`} | |
| className="inline-flex items-center gap-2 px-5 py-2.5 bg-emerald-500 hover:bg-emerald-600 text-white rounded-xl font-semibold transition-all hover:scale-105" | |
| > | |
| <Download className="w-4 h-4" /> | |
| Download CSV | |
| </a> | |
| ) : ( | |
| <button disabled className="inline-flex items-center gap-2 px-5 py-2.5 bg-gray-500 text-white rounded-xl font-semibold opacity-50 cursor-not-allowed"> | |
| <Download className="w-4 h-4" /> | |
| Not Available | |
| </button> | |
| )} | |
| <p className="text-xs mt-3 flex items-center justify-center gap-2" style={{ color: 'var(--text-muted)' }}> | |
| <CheckCircle className="w-3 h-3 text-emerald-400" /> | |
| Ready for Production | |
| </p> | |
| </div> | |
| {/* Complete Code ZIP Download */} | |
| <div className="p-5 rounded-xl border text-center relative overflow-hidden" style={{ borderColor: isDark ? '#f59e0b' : '#d97706', backgroundColor: isDark ? 'rgba(245,158,11,0.05)' : 'rgba(245,158,11,0.03)' }}> | |
| <div className="absolute top-0 right-0 px-2 py-0.5 text-[10px] font-bold rounded-bl-lg" style={{ backgroundColor: '#f59e0b', color: '#000' }}> | |
| NEW | |
| </div> | |
| <div className="w-14 h-14 mx-auto bg-amber-500/20 rounded-full flex items-center justify-center mb-4"> | |
| <Code2 className="w-7 h-7 text-amber-500" /> | |
| </div> | |
| <h3 className="text-lg font-semibold mb-2" style={{ color: 'var(--text-primary)' }}> | |
| Complete Code | |
| </h3> | |
| <p className="text-sm mb-4" style={{ color: 'var(--text-muted)' }}> | |
| End-to-end ML project with all training modes, API & Docker. | |
| </p> | |
| <a | |
| href={`/api/v2/automl/download-code?user_id=${getUserIdSync()}`} | |
| className="inline-flex items-center gap-2 px-5 py-2.5 bg-gradient-to-r from-amber-500 to-orange-500 hover:from-amber-600 hover:to-orange-600 text-white rounded-xl font-semibold transition-all hover:scale-105 shadow-lg shadow-amber-500/20" | |
| > | |
| <Download className="w-4 h-4" /> | |
| Download ZIP | |
| </a> | |
| <p className="text-xs mt-3 flex items-center justify-center gap-2" style={{ color: 'var(--text-muted)' }}> | |
| <CheckCircle className="w-3 h-3 text-amber-400" /> | |
| ML + NLP + DL + Fast + Ultra | |
| </p> | |
| </div> | |
| </div> | |
| {/* What's inside the ZIP */} | |
| <div className="mt-8 p-5 rounded-xl border" style={{ borderColor: 'var(--border-color)', backgroundColor: isDark ? 'rgba(255,255,255,0.02)' : 'rgba(0,0,0,0.02)' }}> | |
| <h4 className="font-semibold mb-4 flex items-center gap-2 text-base" style={{ color: 'var(--text-primary)' }}> | |
| <Code2 className="w-5 h-5 text-amber-500" /> | |
| What's Inside the ZIP | |
| </h4> | |
| <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3"> | |
| {[ | |
| { file: 'train.py', desc: 'Traditional ML (15+ sklearn models)', color: '#22c55e' }, | |
| { file: 'train_nlp.py', desc: 'NLP mode (TF-IDF + Transformers)', color: '#3b82f6' }, | |
| { file: 'train_deep_learning.py', desc: 'PyTorch neural network', color: '#ef4444' }, | |
| { file: 'train_fast.py', desc: 'Fast mode (top 3 models)', color: '#8b5cf6' }, | |
| { file: 'train_ultra.py', desc: 'Ultra mode (stacking ensemble)', color: '#f59e0b' }, | |
| { file: 'predict.py', desc: 'CLI + batch predictions', color: '#06b6d4' }, | |
| { file: 'visualize.py', desc: 'Generate all ML charts', color: '#ec4899' }, | |
| { file: 'evaluate.py', desc: 'Comprehensive evaluation', color: '#14b8a6' }, | |
| { file: 'api_server.py', desc: 'Flask REST API server', color: '#6366f1' }, | |
| { file: 'run_all.py', desc: 'Run all modes & compare', color: '#a855f7' }, | |
| { file: 'Dockerfile', desc: 'Docker deployment ready', color: '#0ea5e9' }, | |
| { file: 'README.md', desc: 'Full documentation', color: '#64748b' }, | |
| ].map((item) => ( | |
| <div key={item.file} className="flex items-center gap-3 p-2.5 rounded-lg" style={{ backgroundColor: isDark ? 'rgba(255,255,255,0.03)' : 'rgba(0,0,0,0.02)' }}> | |
| <div className="w-2 h-2 rounded-full flex-shrink-0" style={{ backgroundColor: item.color }} /> | |
| <div className="min-w-0"> | |
| <p className="text-sm font-mono font-medium truncate" style={{ color: 'var(--text-primary)' }}>{item.file}</p> | |
| <p className="text-xs truncate" style={{ color: 'var(--text-muted)' }}>{item.desc}</p> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| {/* Quick Start Guide */} | |
| <div className="mt-5 p-5 rounded-xl text-left" style={{ backgroundColor: isDark ? 'rgba(255,255,255,0.03)' : 'rgba(0,0,0,0.02)' }}> | |
| <h4 className="font-semibold mb-3 flex items-center gap-2" style={{ color: 'var(--text-primary)' }}> | |
| <Play className="w-4 h-4 text-emerald-500" /> | |
| Quick Start — Run Locally | |
| </h4> | |
| <pre className="text-xs p-4 rounded-lg overflow-x-auto" style={{ backgroundColor: isDark ? 'rgba(0,0,0,0.3)' : 'rgba(0,0,0,0.05)', color: 'var(--text-muted)' }}> | |
| {`# 1. Extract the ZIP and install dependencies | |
| unzip datavision_ml_project.zip && cd datavision_ml_project | |
| pip install -r requirements.txt | |
| # 2. Run all training modes and compare results | |
| python run_all.py | |
| # 3. Make predictions | |
| python predict.py # Interactive mode | |
| python predict.py --csv input.csv # Batch predictions | |
| # 4. Generate visualization charts | |
| python visualize.py | |
| # 5. Start REST API server | |
| python api_server.py # http://localhost:5000 | |
| # 6. Or deploy with Docker | |
| docker build -t ml-model . && docker run -p 5000:5000 ml-model`} | |
| </pre> | |
| </div> | |
| </motion.div> | |
| </div> | |
| )} | |
| </motion.div> | |
| {/* Explain Modal */} | |
| <ExplainModal | |
| isOpen={showExplainModal} | |
| onClose={() => setShowExplainModal(false)} | |
| inputValues={explainInputValues} | |
| mode={result.mode || 'traditional'} | |
| /> | |
| {/* Deploy Modal */} | |
| <DeployModal | |
| isOpen={showDeployModal} | |
| onClose={() => setShowDeployModal(false)} | |
| modelName={result.best_model?.name || "Production Model"} | |
| taskType={result.task_type || "AutoML"} | |
| /> | |
| {/* Footer */} | |
| <motion.div | |
| initial={{ opacity: 0 }} | |
| animate={{ opacity: 1 }} | |
| transition={{ delay: 0.5 }} | |
| className="p-4 rounded-xl flex items-center justify-between" | |
| style={{ backgroundColor: isDark ? 'rgba(255,255,255,0.03)' : 'rgba(0,0,0,0.02)' }} | |
| > | |
| <div className="flex items-center gap-6 text-sm" style={{ color: 'var(--text-muted)' }}> | |
| <span>📊 {result.data_summary?.rows?.toLocaleString() || 0} rows</span> | |
| <span>📁 {result.data_summary?.columns || 0} columns</span> | |
| <span>⚙️ {result.data_summary?.features_engineered || 0} features engineered</span> | |
| </div> | |
| </motion.div> | |
| {/* Web IDE Modal */} | |
| {showIde && ( | |
| <WebIDE initialFiles={ideFiles} onClose={() => setShowIde(false)} /> | |
| )} | |
| </div> | |
| ); | |
| }; | |
| export default MLPredictions; | |