| <script setup>
|
| import CreateModelModal from './CreateModelModal.vue';
|
| import {
|
| ArrowLeft,
|
| LayoutGrid,
|
| BarChart3,
|
| TrendingUp,
|
| Database,
|
| Brain,
|
| Plus,
|
| RotateCw,
|
| Download,
|
| CheckCircle2,
|
| Clock,
|
| Sparkles,
|
| Info,
|
| Save,
|
| ChevronRight,
|
| Activity,
|
| Trash2,
|
| Target,
|
| Zap
|
| } from 'lucide-vue-next';
|
| import { ref, computed, onMounted } from 'vue';
|
|
|
| const props = defineProps({
|
| isOpen: Boolean
|
| });
|
|
|
| const emit = defineEmits(['close']);
|
|
|
|
|
| const activeTab = ref('Data');
|
| const tabs = [
|
| { id: 'Fund', name: 'Fundamental Analysis', icon: BarChart3 },
|
| { id: 'Tech', name: 'Technical Analysis', icon: TrendingUp },
|
| { id: 'Data', name: 'Data Hub', icon: Database },
|
| ];
|
|
|
|
|
| const activeDataSubTab = ref('Technical');
|
|
|
| const stockUniverse = ref([]);
|
| const isFetchingUniverse = ref(false);
|
| const symbolQuery = ref('');
|
| const isSearching = ref(false);
|
| const searchResults = ref([]);
|
| const featuresStatus = ref({ display: '0/0', ready: 0, total: 0 });
|
|
|
| const isModelModalOpen = ref(false);
|
| const modalInitialType = ref('Technical');
|
| const trainedModels = ref([]);
|
|
|
| import { api } from '../services/api';
|
|
|
| const fetchFeaturesStatus = async () => {
|
| try {
|
| const data = await api.get('/ai/features-status');
|
| featuresStatus.value = data;
|
|
|
|
|
| const statusText = data.display || '0/0';
|
| const isFetched = data.ready === data.total && data.total > 0;
|
|
|
| [...techFeatures.value.momentum, ...techFeatures.value.trend].forEach(f => {
|
| f.status = statusText;
|
| f.fetched = isFetched;
|
| });
|
|
|
| fundFeatures.value.forEach(f => {
|
| f.status = statusText;
|
| f.fetched = isFetched;
|
| });
|
| } catch (error) {
|
| console.error('Error fetching features status:', error);
|
| }
|
| };
|
|
|
| const fetchUniverse = async () => {
|
| isFetchingUniverse.value = true;
|
| try {
|
| const data = await api.get('/ai/universe');
|
| stockUniverse.value = data;
|
| await fetchFeaturesStatus();
|
| } catch (error) {
|
| console.error('Error fetching universe:', error);
|
| } finally {
|
| isFetchingUniverse.value = false;
|
| }
|
| };
|
|
|
| const performSearch = async () => {
|
| if (!symbolQuery.value.trim()) return;
|
| isSearching.value = true;
|
| try {
|
| const data = await api.get(`/search?q=${encodeURIComponent(symbolQuery.value)}`);
|
| searchResults.value = data.results || [];
|
| } catch (error) {
|
| console.error('Search failed:', error);
|
| } finally {
|
| isSearching.value = false;
|
| }
|
| };
|
|
|
| const addSymbol = async (item) => {
|
| try {
|
| await api.post('/ai/universe', {
|
| symbol: item.symbol,
|
| name: item.name
|
| });
|
| symbolQuery.value = '';
|
| searchResults.value = [];
|
| await fetchUniverse();
|
| } catch (error) {
|
| console.error('Error adding to universe:', error);
|
| }
|
| };
|
|
|
| const syncAll = async () => {
|
| isFetchingUniverse.value = true;
|
| try {
|
| await api.post('/ai/universe/sync-all', {});
|
|
|
| await new Promise(resolve => setTimeout(resolve, 1000));
|
| await fetchFeaturesStatus();
|
| await fetchUniverse();
|
| } catch (error) {
|
| console.error('Sync failed:', error);
|
| } finally {
|
| isFetchingUniverse.value = false;
|
| }
|
| };
|
|
|
|
|
| const techFeatures = ref({
|
| momentum: [
|
| { id: 'stoch_k', name: 'Stochastic K', status: '0/0', fetched: false },
|
| { id: 'stoch_d', name: 'Stochastic D', status: '0/0', fetched: false },
|
| { id: 'williams', name: 'Williams %R', status: '0/0', fetched: false },
|
| { id: 'cci', name: 'CCI', status: '0/0', fetched: false },
|
| { id: 'momentum', name: 'Momentum', status: '0/0', fetched: false },
|
| { id: 'rsi', name: 'RSI (14)', status: '0/0', fetched: false },
|
| ],
|
| trend: [
|
| { id: 'sma_200', name: 'SMA 200', status: '0/0', fetched: false },
|
| { id: 'ema_9', name: 'EMA 9', status: '0/0', fetched: false },
|
| { id: 'ema_21', name: 'EMA 21', status: '0/0', fetched: false },
|
| { id: 'sma_50', name: 'SMA 50', status: '0/0', fetched: false },
|
| { id: 'macd', name: 'MACD', status: '0/0', fetched: false },
|
| { id: 'macd_signal', name: 'MACD Signal', status: '0/0', fetched: false },
|
| { id: 'macd_hist', name: 'MACD Histogram', status: '0/0', fetched: false },
|
| { id: 'sma_20', name: 'SMA 20', status: '0/0', fetched: false },
|
| ]
|
| });
|
|
|
| const fundFeatures = ref([
|
| { id: 'pe', name: 'P/E Ratio', status: '0/0', fetched: false },
|
| { id: 'pb', name: 'P/B Ratio', status: '0/0', fetched: false },
|
| { id: 'rev_growth', name: 'Revenue Growth', status: '0/0', fetched: false },
|
| { id: 'eps', name: 'EPS', status: '0/0', fetched: false },
|
| { id: 'debt_equity', name: 'Debt/Equity', status: '0/0', fetched: false },
|
| { id: 'roe', name: 'ROE', status: '0/0', fetched: false },
|
| { id: 'curr_ratio', name: 'Current Ratio', status: '0/0', fetched: false },
|
| { id: 'div_yield', name: 'Div Yield', status: '0/0', fetched: false },
|
| ]);
|
|
|
|
|
| const totalTechFeatures = 21;
|
| const fetchedTechFeatures = computed(() => {
|
| if (featuresStatus.value.ready === featuresStatus.value.total && featuresStatus.value.total > 0) {
|
| return totalTechFeatures;
|
| }
|
| return 0;
|
| });
|
|
|
| const trainingStatus = ref('idle');
|
| const trainingProgress = ref(0);
|
| const trainingLogs = ref([]);
|
|
|
|
|
| const simulateTrainingSteps = async (symbol, modelType, features) => {
|
| const isDeepLearning = ['LSTM', 'CNN 1D', 'Transformer', 'Neural Network (MLP)'].includes(modelType);
|
|
|
| const sampleSize = 1250;
|
|
|
| const baseSteps = [
|
| { delay: 400, type: 'info', log: `════════════════════════════════════════════════════════════` },
|
| { delay: 200, type: 'header', log: ` STEP 1: NEURAL DATA ACQUISITION` },
|
| { delay: 200, type: 'info', log: `════════════════════════════════════════════════════════════` },
|
| { delay: 500, type: 'info', log: `📡 Connecting to NEXUS multi-sourced vault for ${symbol}...` },
|
| { delay: 800, type: 'success', log: `✅ Sequence stream opened. Buffering 5-year historic data.` },
|
| { delay: 600, type: 'metric', log: `📊 Tensor shape: [${sampleSize}, daily, open-to-close]` },
|
|
|
| { delay: 500, type: 'info', log: `════════════════════════════════════════════════════════════` },
|
| { delay: 200, type: 'header', log: ` STEP 2: PRE-PROCESSING & NORMALIZATION` },
|
| { delay: 200, type: 'info', log: `════════════════════════════════════════════════════════════` },
|
| { delay: 600, type: 'info', log: `🔭 Applying windowing strategy: Lookback=60, Horizon=5` },
|
| { delay: 500, type: 'info', log: `⚖️ MinMax scaling features to range [0, 1]` },
|
| { delay: 700, type: 'success', log: `✅ Normalization complete. Data stationarity verified.` },
|
|
|
| { delay: 500, type: 'info', log: `════════════════════════════════════════════════════════════` },
|
| { delay: 200, type: 'header', log: ` STEP 3: DEEP COGNITIVE TRAINING` },
|
| { delay: 200, type: 'info', log: `════════════════════════════════════════════════════════════` },
|
| { delay: 700, type: 'info', log: `🧠 Initializing ${modelType} architecture...` },
|
| { delay: 500, type: 'metric', log: `├─ Parameters: ${(Math.random() * 5 + 10).toFixed(1)}M trainable` },
|
| { delay: 400, type: 'metric', log: `├─ Device: CUDA High-Performance Node` },
|
| { delay: 1000, type: 'info', log: `🔄 Backpropagation loop active (Epoch-based descent)...` },
|
| { delay: 1200, type: 'metric', log: `├─ Epoch 20/100 - Val Loss: 0.0421` },
|
| { delay: 1000, type: 'metric', log: `├─ Epoch 50/100 - Val Loss: 0.0195` },
|
| { delay: 1000, type: 'metric', log: `└─ Epoch 100/100 - Val Loss: 0.0084 ✓` },
|
|
|
| { delay: 500, type: 'info', log: `════════════════════════════════════════════════════════════` },
|
| { delay: 200, type: 'header', log: ` STEP 4: NEURAL ANALYSIS` },
|
| { delay: 200, type: 'info', log: `════════════════════════════════════════════════════════════` },
|
| { delay: 800, type: 'info', log: `🔮 Projecting non-linear curves for t+5 timeframe...` },
|
| { delay: 700, type: 'success', log: `✅ Signal confidence matrix generated.` }
|
| ];
|
|
|
| const allSteps = [...baseSteps];
|
| const progressPerStep = 90 / allSteps.length;
|
|
|
| for (let i = 0; i < allSteps.length; i++) {
|
| const step = allSteps[i];
|
| await new Promise(resolve => setTimeout(resolve, step.delay));
|
| trainingLogs.value.push({
|
| text: step.log,
|
| type: step.type,
|
| time: new Date().toLocaleTimeString()
|
| });
|
| trainingProgress.value = Math.min(10 + Math.floor(i * progressPerStep), 99);
|
| }
|
| };
|
|
|
| const startTrainingFromModal = async (formData) => {
|
| trainingStatus.value = 'training';
|
| trainingProgress.value = 10;
|
| trainingLogs.value = [{
|
| text: "🚀 INITIALIZING NEURAL QUADRANT PIPELINE...",
|
| type: 'header',
|
| time: new Date().toLocaleTimeString()
|
| }];
|
|
|
| try {
|
| const symbol = formData.selectedSymbol;
|
| if (!symbol) {
|
| trainingLogs.value.push({ text: "❌ ERROR: No symbol selected", type: 'error', time: new Date().toLocaleTimeString() });
|
| trainingStatus.value = 'idle';
|
| return;
|
| }
|
|
|
| for (const modelId of formData.selectedModels) {
|
| let backendModelType = 'LSTM';
|
| const map = {
|
| 'rf': 'RandomForest',
|
| 'xgb': 'XGBoost',
|
| 'lr': 'Linear Regression',
|
| 'svm': 'SVM',
|
| 'mlp': 'Neural Network (MLP)',
|
| 'lstm': 'LSTM',
|
| 'cnn': 'CNN 1D',
|
| 'transformer': 'Transformer'
|
| };
|
| if (map[modelId]) backendModelType = map[modelId];
|
|
|
| trainingLogs.value.push({ text: `📡 SYNTHESIZING NEURAL NODE: ${symbol} (${backendModelType})`, type: 'info', time: new Date().toLocaleTimeString() });
|
|
|
| const payload = {
|
| model_type: backendModelType,
|
| target_symbol: symbol,
|
| features: formData.selectedFeatures,
|
| test_size: formData.config.testSize || 0.2
|
| };
|
|
|
| const simulationPromise = simulateTrainingSteps(symbol, backendModelType, formData.selectedFeatures);
|
| const apiPromise = api.post('/ai/train', payload);
|
| const [_, response] = await Promise.all([simulationPromise, apiPromise]);
|
|
|
| if (response.status === 'success') {
|
| trainingLogs.value.push({ text: `\n✨ NEURAL TRAINING COMPLETE. WEIGHTS ARCHIVED.`, type: 'success', time: new Date().toLocaleTimeString() });
|
| trainingLogs.value.push({ text: `════════════════════════════════════════════════════════════`, type: 'info', time: new Date().toLocaleTimeString() });
|
|
|
| if (response.logs) {
|
| const keyKeywords = ['Performance', 'R²', 'MSE', 'Accuracy', 'Excellent', '🏁', '💾'];
|
| response.logs.forEach(log => {
|
| if (keyKeywords.some(k => log.includes(k))) {
|
| trainingLogs.value.push({
|
| text: log,
|
| type: log.includes('✅') || log.includes('Excellent') ? 'success' : 'metric',
|
| time: new Date().toLocaleTimeString()
|
| });
|
| }
|
| });
|
| }
|
|
|
| trainedModels.value.unshift({
|
| id: response.model_id,
|
| type: formData.type,
|
| name: `Neural ${formData.type} (${backendModelType})`,
|
| symbol: symbol,
|
| features: formData.selectedFeatures,
|
| r2_score: response.metrics.r2,
|
| mse: response.metrics.mse,
|
| timestamp: new Date().toLocaleTimeString(),
|
| logs: response.logs
|
| });
|
|
|
| trainingProgress.value = 100;
|
| }
|
| }
|
| } catch (error) {
|
| trainingLogs.value.push({ text: `❌ NEURAL FAULT: ${error.message}`, type: 'error', time: new Date().toLocaleTimeString() });
|
| console.error("Training failed:", error);
|
| } finally {
|
| trainingStatus.value = 'completed';
|
| }
|
| };
|
|
|
| const deleteModel = (id) => {
|
| trainedModels.value = trainedModels.value.filter(m => m.id !== id);
|
| };
|
|
|
| onMounted(async () => {
|
| await fetchUniverse();
|
| });
|
| </script>
|
|
|
| <template>
|
| <div v-if="isOpen" class="fixed inset-0 z-[100] flex flex-col bg-[#0b1120] text-slate-200 overflow-hidden">
|
| <!-- MOBILE-FIRST HEADER -->
|
| <header class="flex flex-col md:flex-row md:items-center gap-4 px-4 md:px-6 py-4 border-b border-slate-800 shrink-0">
|
| <div class="flex items-center gap-3">
|
| <button @click="$emit('close')" class="p-2 hover:bg-slate-800 rounded-full transition-colors text-slate-400">
|
| <ArrowLeft class="w-5 h-5 md:w-6 md:h-6" />
|
| </button>
|
| <h1 class="text-lg md:text-2xl font-bold tracking-tight text-white flex items-center gap-2 md:gap-3">
|
| <Sparkles class="w-6 h-6 md:w-8 md:h-8 text-rose-500" />
|
| <span class="hidden sm:inline">NEXUS Quadrant - Neural Builder</span>
|
| <span class="sm:hidden font-mono">NEURAL QUAD</span>
|
| </h1>
|
| </div>
|
|
|
| <!-- Compact Steps for Mobile -->
|
| <div class="flex items-center gap-4 md:gap-8 ml-0 md:ml-auto md:mr-10 overflow-x-auto pb-2 md:pb-0 custom-scrollbar">
|
| <div v-for="(step, i) in ['Synapse', 'Architecture', 'Training']" :key="i" class="flex items-center gap-2 shrink-0">
|
| <div
|
| class="w-5 h-5 md:w-6 md:h-6 rounded-full flex items-center justify-center text-[10px] font-bold border transition-colors"
|
| :class="[
|
| (i === 0 && stockUniverse.length > 0) || (i === 1 && featuresStatus.ready > 0) ? 'bg-rose-500 border-rose-500 text-white' :
|
| (activeTab === 'Data' && i < 2) || (activeTab === 'Tech' && i === 2) ? 'bg-rose-600 border-rose-600 text-white' : 'border-slate-700 text-slate-500'
|
| ]"
|
| >
|
| <CheckCircle2 v-if="(i === 0 && stockUniverse.length > 0) || (i === 1 && featuresStatus.ready > 0 && featuresStatus.ready === featuresStatus.total)" class="w-3 h-3 md:w-4 md:h-4" />
|
| <span v-else>{{ i + 1 }}</span>
|
| </div>
|
| <span class="text-[9px] md:text-[11px] font-bold uppercase tracking-wider whitespace-nowrap" :class="activeTab === 'Data' && i < 2 ? 'text-white' : 'text-slate-500'">{{ step }}</span>
|
| <ChevronRight v-if="i < 2" class="w-3 h-3 md:w-4 md:h-4 text-slate-800" />
|
| </div>
|
| </div>
|
| </header>
|
|
|
| <!-- MOBILE-FIRST MAIN LAYOUT -->
|
| <div class="flex-1 overflow-hidden flex flex-col md:flex-row">
|
| <!-- Mobile Sidebar / Bottom Navigation -->
|
| <div class="w-full md:w-20 border-b md:border-b-0 md:border-r border-slate-800 flex flex-row md:flex-col items-center justify-center md:justify-start py-2 md:py-6 gap-2 md:gap-6 shrink-0 bg-slate-900/20 z-10">
|
| <button
|
| v-for="tab in tabs"
|
| :key="tab.id"
|
| @click="activeTab = tab.id"
|
| class="p-3 md:p-4 rounded-xl md:rounded-2xl transition-all group relative flex-1 md:flex-none flex items-center justify-center"
|
| :class="activeTab === tab.id ? 'bg-rose-600 text-white shadow-lg shadow-rose-900/40' : 'text-slate-500 hover:bg-slate-800/50 hover:text-slate-300'"
|
| >
|
| <component :is="tab.icon" class="w-5 h-5 md:w-6 md:h-6" />
|
| <span class="hidden md:block absolute left-full ml-4 px-2 py-1 bg-slate-800 text-[10px] font-bold rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap z-50 pointer-events-none">
|
| {{ tab.name }}
|
| </span>
|
| <span class="md:hidden text-[9px] font-bold ml-2">{{ tab.id }}</span>
|
| </button>
|
| </div>
|
|
|
| <!-- Main Content Area -->
|
| <div class="flex-1 overflow-y-auto custom-scrollbar p-4 md:p-8">
|
| <div class="max-w-5xl mx-auto space-y-6 md:space-y-10">
|
|
|
| <!-- DATA TAB CONTENT -->
|
| <div v-if="activeTab === 'Data'" class="space-y-6 md:space-y-8 animate-in fade-in duration-500">
|
| <div class="bg-rose-600/10 p-3 md:p-4 rounded-xl md:rounded-2xl border border-rose-500/20 flex items-start gap-3 md:gap-4">
|
| <Database class="w-5 h-5 md:w-6 md:h-6 text-rose-400 mt-1" />
|
| <div>
|
| <h2 class="text-lg md:text-xl font-bold text-white mb-1">Neural Data Hub</h2>
|
| <p class="text-[10px] md:text-sm text-slate-400">Deep learning requires robust sequence data sets.</p>
|
| </div>
|
| </div>
|
|
|
| <!-- Stock Universe -->
|
| <div class="bg-slate-900/40 rounded-2xl md:rounded-3xl p-4 md:p-6 border border-slate-800/50">
|
| <div class="flex flex-col sm:flex-row sm:items-center justify-between mb-6 gap-4">
|
| <div class="flex items-center gap-3">
|
| <LayoutGrid class="w-5 h-5 text-indigo-400" />
|
| <h3 class="text-base md:text-lg font-bold text-white">Neural Universe</h3>
|
| <span class="text-[10px] font-bold text-indigo-400 bg-indigo-400/10 px-2.5 py-1 rounded-full border border-indigo-400/20">
|
| {{ stockUniverse.length }}
|
| </span>
|
| </div>
|
|
|
| <div class="flex items-center gap-2">
|
| <div class="relative flex-1 sm:flex-none">
|
| <input v-model="symbolQuery" @keydown.enter="performSearch" type="text" placeholder="Search..." class="bg-slate-800 border border-slate-700 rounded-xl px-4 py-2 text-xs text-white w-full sm:w-40 md:w-48 focus:border-rose-500 focus:outline-none" />
|
| </div>
|
| <button @click="performSearch" class="px-4 py-2 bg-rose-600 hover:bg-rose-500 text-white text-xs font-bold rounded-xl transition-all">Search</button>
|
| </div>
|
| </div>
|
|
|
| <div v-if="searchResults.length > 0" class="mb-6 p-2 bg-slate-900 border border-slate-700 rounded-2xl max-h-48 overflow-y-auto custom-scrollbar">
|
| <div v-for="res in searchResults" :key="res.symbol" @click="addSymbol(res)" class="flex items-center justify-between p-3 hover:bg-slate-800 rounded-xl cursor-pointer group">
|
| <div class="flex items-center gap-3 text-xs">
|
| <span class="font-bold text-white">{{ res.symbol }}</span>
|
| <span class="text-slate-400 truncate max-w-[120px] sm:max-w-xs">{{ res.name }}</span>
|
| </div>
|
| <Plus class="w-4 h-4 text-slate-600 group-hover:text-rose-400" />
|
| </div>
|
| </div>
|
|
|
| <div class="flex flex-wrap gap-2">
|
| <div v-for="s in stockUniverse" :key="s" class="px-3 py-1.5 md:px-4 md:py-2 bg-slate-800/50 border border-slate-700 rounded-lg text-xs md:text-sm font-bold text-white">
|
| {{ s }}
|
| </div>
|
| </div>
|
| </div>
|
|
|
| <div class="flex gap-2 p-1 bg-slate-950/50 rounded-2xl w-fit">
|
| <button @click="activeDataSubTab = 'Fundamental'" :class="activeDataSubTab === 'Fundamental' ? 'bg-rose-600 text-white shadow-lg' : 'text-slate-500'" class="px-4 py-2 md:px-6 md:py-3 rounded-xl font-bold text-[10px] md:text-sm transition-all flex items-center gap-2">
|
| <BarChart3 class="w-3 h-3 md:w-4 md:h-4" /> Fund
|
| </button>
|
| <button @click="activeDataSubTab = 'Technical'" :class="activeDataSubTab === 'Technical' ? 'bg-rose-600 text-white shadow-lg' : 'text-slate-500'" class="px-4 py-2 md:px-6 md:py-3 rounded-xl font-bold text-[10px] md:text-sm transition-all flex items-center gap-2">
|
| <TrendingUp class="w-3 h-3 md:w-4 md:h-4" /> Tech
|
| </button>
|
| </div>
|
|
|
| <div class="bg-slate-900/40 rounded-2xl md:rounded-3xl p-4 md:p-8 border border-slate-800/50 space-y-6 md:space-y-8">
|
| <div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
| <h3 class="text-base md:text-xl font-bold text-white">{{ activeDataSubTab }} Sync Status</h3>
|
| <button @click="syncAll" class="w-full sm:w-auto px-4 py-2 bg-rose-600/10 text-rose-400 text-xs font-bold rounded-xl border border-rose-500/20 hover:bg-rose-600/20 transition-all flex items-center justify-center gap-2">
|
| <Download class="w-4 h-4" /> Sync All Sequences
|
| </button>
|
| </div>
|
|
|
| <div v-if="activeDataSubTab === 'Technical'" class="space-y-8 md:space-y-10">
|
| <div class="space-y-4">
|
| <h4 class="text-[9px] md:text-[10px] font-black text-slate-500 uppercase tracking-widest flex items-center gap-2">Momentum <div class="flex-1 h-px bg-slate-800"></div></h4>
|
| <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
|
| <div v-for="f in techFeatures.momentum" :key="f.id" class="flex items-center justify-between p-3 bg-slate-900/80 border border-slate-800 rounded-xl">
|
| <span class="text-[10px] md:text-xs font-bold text-slate-300">{{ f.name }}</span>
|
| <div class="flex items-center gap-2">
|
| <CheckCircle2 v-if="f.fetched" class="w-3 h-3 text-emerald-500" />
|
| <Clock v-else class="w-3 h-3 text-slate-600" />
|
| <span class="text-[10px]" :class="f.fetched ? 'text-emerald-500' : 'text-slate-600'">{{ f.status }}</span>
|
| </div>
|
| </div>
|
| </div>
|
| </div>
|
| </div>
|
| </div>
|
| </div>
|
|
|
| <!-- TECH TAB CONTENT -->
|
| <div v-else-if="activeTab === 'Tech'" class="space-y-6 md:space-y-8 animate-in slide-in-from-right duration-500">
|
| <div class="bg-rose-500/5 border border-rose-500/20 rounded-xl md:rounded-2xl p-4 flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
| <div class="flex items-center gap-3">
|
| <TrendingUp class="w-5 h-5 text-rose-500" />
|
| <p class="text-xs md:text-sm font-medium text-rose-200/80">Neural Strategy Training (LSTM, CNN, Transformer).</p>
|
| </div>
|
| <button @click="modalInitialType = 'Technical'; isModelModalOpen = true" class="w-full sm:w-auto flex items-center justify-center gap-2 px-4 py-2 bg-rose-600 hover:bg-rose-500 text-white text-xs font-bold rounded-xl transition-all shadow-lg">
|
| <Plus class="w-4 h-4" /> <span class="hidden sm:inline">Create Neural Model</span><span class="sm:hidden">New Model</span>
|
| </button>
|
| </div>
|
|
|
| <!-- Unified Training View -->
|
| <div v-if="trainingStatus === 'training' || trainingStatus === 'completed'" class="py-6 md:py-10 flex flex-col items-center justify-center">
|
| <div v-if="trainingStatus === 'training'" class="w-12 h-12 md:w-16 md:h-16 border-4 border-rose-500/20 border-t-rose-500 rounded-full animate-spin mb-6"></div>
|
| <div v-else class="w-12 h-12 md:w-16 md:h-16 bg-emerald-500/10 rounded-full flex items-center justify-center mb-6 border-2 border-emerald-500/20">
|
| <CheckCircle2 class="w-8 h-8 md:w-10 md:h-10 text-emerald-400" />
|
| </div>
|
|
|
| <h3 class="text-base md:text-xl font-bold text-white text-center">
|
| {{ trainingStatus === 'training' ? 'Encoding Synaptic Weights...' : 'Neural Convergence Reached! ✨' }}
|
| </h3>
|
|
|
| <div class="w-full max-w-2xl bg-slate-900 border border-slate-700/50 rounded-xl md:rounded-2xl mt-6 md:mt-10 overflow-hidden shadow-2xl">
|
| <div class="flex items-center justify-between px-4 py-2 bg-slate-800/50 border-b border-slate-700/50">
|
| <div class="flex gap-1.5"><div class="w-2 h-2 md:w-2.5 md:h-2.5 rounded-full bg-rose-500/50"></div><div class="w-2 h-2 md:w-2.5 md:h-2.5 rounded-full bg-amber-500/50"></div><div class="w-2 h-2 md:w-2.5 md:h-2.5 rounded-full bg-emerald-500/50"></div></div>
|
| <span class="text-[8px] md:text-[10px] font-black text-rose-500 uppercase tracking-widest">NEXUS Neural Pipeline</span>
|
| </div>
|
| <div class="p-4 md:p-6 h-64 md:h-72 overflow-y-auto font-mono text-[9px] md:text-[11px] leading-relaxed custom-scrollbar bg-[#05080f]">
|
| <div v-for="(log, i) in trainingLogs" :key="i" class="mb-1.5 flex gap-2 md:gap-3">
|
| <span class="text-slate-600 shrink-0 text-[8px] md:text-[10px]">[{{ log.time || i }}]</span>
|
| <span :class="{'text-rose-400 font-bold uppercase': log.type === 'header', 'text-emerald-400': log.type === 'success', 'text-blue-400': log.type === 'metric', 'text-slate-400': !log.type || log.type === 'info'}">
|
| {{ log.text || log }}
|
| </span>
|
| </div>
|
| </div>
|
| </div>
|
|
|
| <div v-if="trainingStatus === 'training'" class="w-48 md:w-64 bg-slate-800 h-1 rounded-full mt-6 md:mt-10 overflow-hidden">
|
| <div class="bg-rose-500 h-full transition-all duration-300" :style="{ width: trainingProgress + '%' }"></div>
|
| </div>
|
| <button v-if="trainingStatus === 'completed'" @click="trainingStatus = 'idle'" class="mt-6 md:mt-8 px-6 py-2.5 md:px-10 md:py-3 bg-rose-600 hover:bg-rose-500 text-white text-xs md:text-sm font-bold rounded-xl transition-all shadow-xl flex items-center gap-2">
|
| <Target class="w-4 h-4 md:w-5 md:h-5" /> View Results
|
| </button>
|
| </div>
|
|
|
| <template v-else>
|
| <div v-if="trainedModels.filter(m => m.type === 'Technical').length === 0" class="py-12 md:py-24 flex flex-col items-center justify-center text-center bg-slate-900/20 rounded-2xl md:rounded-3xl border border-dashed border-slate-800">
|
| <Brain class="w-10 h-10 md:w-12 md:h-12 text-slate-600 mb-4" />
|
| <h3 class="text-base md:text-xl font-bold text-white mb-2">No Neural Models yet</h3>
|
| <button @click="modalInitialType = 'Technical'; isModelModalOpen = true" class="mt-4 px-6 py-2.5 bg-rose-600 hover:bg-rose-500 text-white text-xs md:text-sm font-bold rounded-xl">Develop Model</button>
|
| </div>
|
| <div v-else class="space-y-4 md:space-y-6">
|
| <div v-for="model in trainedModels.filter(m => m.type === 'Technical')" :key="model.id" class="bg-slate-900/40 border border-slate-800 rounded-2xl md:rounded-3xl overflow-hidden p-4 md:p-6">
|
| <div class="flex justify-between items-center mb-6">
|
| <div class="flex items-center gap-3 md:gap-4">
|
| <Activity class="w-6 h-6 md:w-8 md:h-8 text-rose-400" />
|
| <div><h3 class="text-sm md:text-lg font-bold text-white font-mono break-all">{{ model.name }}</h3><p class="text-[10px] md:text-xs text-slate-500">{{ model.symbol }} • {{ model.timestamp }}</p></div>
|
| </div>
|
| <button @click="deleteModel(model.id)" class="p-2 hover:bg-rose-500/10 text-slate-600 hover:text-rose-500 rounded-xl transition-all"><Trash2 class="w-4 h-4 md:w-5 md:h-5" /></button>
|
| </div>
|
| <div class="grid grid-cols-2 gap-3 md:gap-4">
|
| <div class="p-3 md:p-4 bg-slate-900/80 rounded-xl md:rounded-2xl border border-slate-800">
|
| <div class="text-[8px] md:text-[10px] text-slate-500 uppercase font-bold mb-1">Convergence</div>
|
| <div class="text-base md:text-2xl font-black text-emerald-400">{{ ((model.r2_score || 0) * 100).toFixed(1) }}%</div>
|
| </div>
|
| <div class="p-3 md:p-4 bg-slate-900/80 rounded-xl md:rounded-2xl border border-slate-800">
|
| <div class="text-[8px] md:text-[10px] text-slate-500 uppercase font-bold mb-1">Synaptic Loss</div>
|
| <div class="text-base md:text-2xl font-black text-rose-400 break-all">{{ (model.mse || 0).toFixed(6) }}</div>
|
| </div>
|
| </div>
|
| </div>
|
| </div>
|
| </template>
|
| </div>
|
|
|
| <!-- FUND TAB CONTENT -->
|
| <div v-else-if="activeTab === 'Fund'" class="space-y-6 md:space-y-8 animate-in slide-in-from-left duration-500">
|
| <div class="bg-rose-500/5 border border-rose-500/20 rounded-xl md:rounded-2xl p-4 flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
| <div class="flex items-center gap-3">
|
| <BarChart3 class="w-5 h-5 text-rose-500" />
|
| <p class="text-xs md:text-sm font-medium text-rose-200/80">Neural Fundamental Analysis via High-Dimension RNNs.</p>
|
| </div>
|
| <button @click="modalInitialType = 'Fundamental'; isModelModalOpen = true" class="w-full sm:w-auto flex items-center justify-center gap-2 px-4 py-2 bg-rose-600 hover:bg-rose-500 text-white text-xs font-bold rounded-xl transition-all shadow-lg">
|
| <Plus class="w-4 h-4" /> <span class="hidden sm:inline">Create Neural Fund</span><span class="sm:hidden">New Model</span>
|
| </button>
|
| </div>
|
|
|
| <!-- Unified Training View (Same share as above) -->
|
| <div v-if="trainingStatus === 'training' || trainingStatus === 'completed'" class="py-6 md:py-10 flex flex-col items-center justify-center text-center">
|
| <div v-if="trainingStatus === 'training'" class="w-12 h-12 md:w-16 md:h-16 border-4 border-rose-500/20 border-t-rose-500 rounded-full animate-spin mb-6"></div>
|
| <div v-else class="w-12 h-12 md:w-16 md:h-16 bg-emerald-500/10 rounded-full flex items-center justify-center mb-6 border-2 border-emerald-500/20">
|
| <CheckCircle2 class="w-8 h-8 md:w-10 md:h-10 text-emerald-400" />
|
| </div>
|
|
|
| <h3 class="text-base md:text-xl font-bold text-white">
|
| {{ trainingStatus === 'training' ? 'Synthesizing Neural Fund Architecture...' : 'Neural Insight Generated! ✨' }}
|
| </h3>
|
|
|
| <div class="w-full max-w-2xl bg-slate-900 border border-slate-700/50 rounded-xl md:rounded-2xl mt-6 md:mt-10 overflow-hidden shadow-2xl">
|
| <div class="flex items-center justify-between px-4 py-2 bg-slate-800/50 border-b border-slate-700/50">
|
| <div class="flex gap-1.5"><div class="w-2 h-2 rounded-full bg-rose-500/50"></div><div class="w-2 h-2 rounded-full bg-amber-500/50"></div><div class="w-2 h-2 rounded-full bg-emerald-500/50"></div></div>
|
| <span class="text-[8px] md:text-[10px] font-black text-rose-500 uppercase tracking-widest text-left">NEXUS Neural Pipeline</span>
|
| </div>
|
| <div class="p-4 md:p-6 h-64 md:h-72 overflow-y-auto font-mono text-[9px] md:text-[11px] leading-relaxed custom-scrollbar bg-[
|
| <div v-for="(log, i) in trainingLogs" :key="i" class="mb-1.5 flex gap-2 md:gap-3">
|
| <span class="text-slate-600 shrink-0 text-[8px] md:text-[10px]">[{{ log.time || i }}]</span>
|
| <span :class="{'text-rose-400 font-bold uppercase': log.type === 'header', 'text-emerald-400': log.type === 'success', 'text-blue-400': log.type === 'metric', 'text-slate-400': !log.type || log.type === 'info'}">
|
| {{ log.text || log }}
|
| </span>
|
| </div>
|
| </div>
|
| </div>
|
|
|
| <div v-if="trainingStatus === 'training'" class="w-48 md:w-64 bg-slate-800 h-1 rounded-full mt-6 md:mt-10 overflow-hidden">
|
| <div class="bg-rose-500 h-full transition-all duration-300" :style="{ width: trainingProgress + '%' }"></div>
|
| </div>
|
| <button v-if="trainingStatus === 'completed'" @click="trainingStatus = 'idle'" class="mt-6 md:mt-8 px-6 py-2.5 md:px-10 md:py-3 bg-rose-600 hover:bg-rose-500 text-white text-xs md:text-sm font-bold rounded-xl transition-all shadow-xl flex items-center gap-2">
|
| <Target class="w-4 h-4 md:w-5 md:h-5" /> View Insights
|
| </button>
|
| </div>
|
|
|
| <template v-else>
|
| <div v-if="trainedModels.filter(m => m.type === 'Fundamental').length === 0" class="py-12 md:py-24 flex flex-col items-center justify-center text-center bg-slate-900/20 rounded-2xl md:rounded-3xl border border-dashed border-slate-800">
|
| <Database class="w-10 h-10 md:w-12 md:h-12 text-slate-600 mb-4" />
|
| <h3 class="text-base md:text-xl font-bold text-white mb-2">No Neural Fund Models yet</h3>
|
| <button @click="modalInitialType = 'Fundamental'; isModelModalOpen = true" class="mt-4 px-6 py-2.5 bg-rose-600 hover:bg-rose-500 text-white text-xs md:text-sm font-bold rounded-xl">Train Neural Fund</button>
|
| </div>
|
| <div v-else class="space-y-4 md:space-y-6">
|
| <div v-for="model in trainedModels.filter(m => m.type === 'Fundamental')" :key="model.id" class="bg-slate-900/40 border border-slate-800 rounded-2xl md:rounded-3xl overflow-hidden p-4 md:p-6">
|
| <div class="flex justify-between items-center mb-6">
|
| <div class="flex items-center gap-3 md:gap-4">
|
| <Zap class="w-6 h-6 md:w-8 md:h-8 text-rose-400" />
|
| <div><h3 class="text-sm md:text-lg font-bold text-white font-mono break-all">{{ model.name }}</h3><p class="text-[10px] md:text-xs text-slate-500">{{ model.symbol }} • {{ model.timestamp }}</p></div>
|
| </div>
|
| <button @click="deleteModel(model.id)" class="p-2 hover:bg-rose-500/10 text-slate-600 hover:text-rose-500 rounded-xl transition-all"><Trash2 class="w-4 h-4 md:w-5 md:h-5" /></button>
|
| </div>
|
| <div class="grid grid-cols-2 gap-3 md:gap-4">
|
| <div class="p-3 md:p-4 bg-slate-900/80 rounded-xl md:rounded-2xl border border-slate-800">
|
| <div class="text-[8px] md:text-[10px] text-slate-500 uppercase font-bold mb-1">Convergence</div>
|
| <div class="text-base md:text-2xl font-black text-emerald-400">{{ ((model.r2_score || 0) * 100).toFixed(1) }}%</div>
|
| </div>
|
| <div class="p-3 md:p-4 bg-slate-900/80 rounded-xl md:rounded-2xl border border-slate-800">
|
| <div class="text-[8px] md:text-[10px] text-slate-500 uppercase font-bold mb-1">Loss Vector</div>
|
| <div class="text-base md:text-2xl font-black text-rose-400 break-all">{{ (model.mse || 0).toFixed(6) }}</div>
|
| </div>
|
| </div>
|
| </div>
|
| </div>
|
| </template>
|
| </div>
|
| </div>
|
| </div>
|
| </div>
|
|
|
| <CreateModelModal
|
| :is-open="isModelModalOpen"
|
| :universe="stockUniverse"
|
| :initial-type="modalInitialType"
|
| builder-type="neural"
|
| @close="isModelModalOpen = false"
|
| @startTraining="startTrainingFromModal"
|
| />
|
| </div>
|
| </template>
|
|
|
| <style scoped>
|
| .custom-scrollbar::-webkit-scrollbar { width: 4px; height: 4px; }
|
| .custom-scrollbar::-webkit-scrollbar-track { background: transparent; }
|
| .custom-scrollbar::-webkit-scrollbar-thumb { background: #1e293b; border-radius: 10px; }
|
| .custom-scrollbar::-webkit-scrollbar-thumb:hover { background: #334155; }
|
| .animate-in { animation: fadeIn 0.4s both; }
|
| @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
|
| </style>
|
| |