mlstocks / frontend /src /components /AIBuilderNeural.vue
github-actions[bot]
Deploy to Hugging Face Space
abf702c
<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']);
// Main Tabs: Fund, Tech, Data
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 },
];
// Sub-tabs for Data tab: Fundamental, Technical
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'); // Type to initialize modal with
const trainedModels = ref([]); // List of completed model tasks
import { api } from '../services/api';
const fetchFeaturesStatus = async () => {
try {
const data = await api.get('/ai/features-status');
featuresStatus.value = data;
// Update all features with the same status
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', {});
// Wait a moment for backend to finish processing
await new Promise(resolve => setTimeout(resolve, 1000));
await fetchFeaturesStatus();
await fetchUniverse(); // Refresh universe list as well
} catch (error) {
console.error('Sync failed:', error);
} finally {
isFetchingUniverse.value = false;
}
};
// Feature Groups and Indicators
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 },
]);
// Stats
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'); // 'idle', 'training', 'completed'
const trainingProgress = ref(0);
const trainingLogs = ref([]);
// Helper function to simulate step-by-step training logs with model-specific details
const simulateTrainingSteps = async (symbol, modelType, features) => {
const isDeepLearning = ['LSTM', 'CNN 1D', 'Transformer', 'Neural Network (MLP)'].includes(modelType);
const sampleSize = 1250; // Larger for neural
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-[#05080f] text-left">
<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>