mlstocks / frontend /src /components /OptionsAnalysisModal.vue
github-actions[bot]
Deploy to Hugging Face Space
abf702c
<script setup>
import { ref, watch, onMounted } from 'vue';
import { X, TrendingUp, Brain, AlertTriangle, CheckCircle, Clock, Zap, Target, Activity, ChevronDown, ChevronUp } from 'lucide-vue-next';
import { api } from '../services/api';
const props = defineProps({
isOpen: Boolean,
symbol: String
});
const emit = defineEmits(['close']);
const loading = ref(false);
const analysisData = ref(null);
const loadingStep = ref('');
const loadingSteps = [
'Fetching real-time market data...',
'Calculating volatility & technical indicators...',
'Searching latest news & analyzing sentiment...',
'Consulting Strategy Advisor agent...',
'Validating with Risk Manager...',
'Finalizing recommendation...'
];
let loadingInterval = null;
const startLoadingAnimation = () => {
let step = 0;
loadingStep.value = loadingSteps[0];
loadingInterval = setInterval(() => {
step = (step + 1) % loadingSteps.length;
loadingStep.value = loadingSteps[step];
}, 5000);
};
const stopLoadingAnimation = () => {
if (loadingInterval) {
clearInterval(loadingInterval);
loadingInterval = null;
}
};
const error = ref(null);
// Agent logs for streaming display
const agentLogs = ref([]);
const AGENT_ICONS = {
'MarketAnalyst': 'πŸ“Š',
'SentimentAnalyst': 'πŸ“°',
'StrategyAdvisor': '🧠',
'RiskManager': 'πŸ›‘οΈ',
'System': 'πŸ€–',
'User': 'πŸ•΅οΈβ€β™‚οΈ'
};
const isStreaming = ref(false);
const streamingStep = ref('');
const streamingProgress = ref(0);
const runAnalysis = async () => {
if (!props.symbol) return;
loading.value = true;
isStreaming.value = false;
error.value = null;
agentLogs.value = [];
analysisData.value = null;
startLoadingAnimation();
try {
const response = await api.post('/options-analysis', {
symbol: props.symbol,
provider: 'google'
});
if (response) {
loading.value = false;
stopLoadingAnimation();
await streamSimulation(response.logs || [], response.result);
}
} catch (err) {
console.error('Analysis error:', err);
error.value = err.message || 'Failed to analyze options strategy';
loading.value = false;
stopLoadingAnimation();
}
};
const streamSimulation = async (logs, result) => {
isStreaming.value = true;
analysisData.value = null;
agentLogs.value = [];
const TOTAL_DURATION = 3500;
const stepDelay = Math.max(100, TOTAL_DURATION / (logs.length || 1));
for (let i = 0; i < logs.length; i++) {
const log = logs[i];
streamingStep.value = `ACTIVE NODE: ${log.source.toUpperCase()}`;
streamingProgress.value = ((i + 1) / logs.length) * 100;
agentLogs.value.push(log);
await new Promise(resolve => setTimeout(resolve, stepDelay));
}
await new Promise(resolve => setTimeout(resolve, 600));
isStreaming.value = false;
analysisData.value = result;
};
watch([() => props.symbol, () => props.isOpen], ([newSymbol, newIsOpen]) => {
if (newSymbol && newIsOpen) {
analysisData.value = null;
agentLogs.value = [];
runAnalysis();
}
}, { immediate: true });
const logsContainer = ref(null);
const getAgentColor = (source) => {
switch (source) {
case 'MarketAnalyst': return 'text-blue-400';
case 'SentimentAnalyst': return 'text-rose-400';
case 'StrategyAdvisor': return 'text-amber-400';
case 'RiskManager': return 'text-emerald-400';
case 'System': return 'text-slate-500';
default: return 'text-white';
}
};
watch(agentLogs, () => {
if (logsContainer.value) {
setTimeout(() => {
logsContainer.value.scrollTop = logsContainer.value.scrollHeight;
}, 50);
}
}, { deep: true });
const closeModal = () => {
emit('close');
};
const showLogsMobile = ref(false);
</script>
<template>
<div v-if="isOpen" class="fixed inset-0 z-[120] flex items-center justify-center md:p-4 overflow-hidden">
<!-- Backdrop -->
<div class="absolute inset-0 bg-slate-950/98 backdrop-blur-xl" @click="closeModal"></div>
<!-- Modal Container -->
<div class="relative w-full h-full md:h-[85vh] md:max-w-6xl bg-[#0b1120] border-0 md:border md:border-slate-800 md:rounded-[2.5rem] shadow-2xl flex flex-col overflow-hidden animate-in fade-in duration-300">
<!-- HEADER -->
<div class="flex-none flex items-center justify-between px-6 py-4 md:py-8 border-b border-slate-800 bg-slate-900/40">
<div class="flex items-center gap-4">
<div class="w-10 h-10 md:w-14 md:h-14 bg-blue-600/10 rounded-xl md:rounded-2xl border border-blue-500/20 flex items-center justify-center text-blue-500 shadow-lg">
<Zap class="w-6 h-6 md:w-8 md:h-8" />
</div>
<div>
<h2 class="text-base md:text-2xl font-black text-white tracking-widest uppercase">Agentic <span class="text-blue-500">Strategist</span></h2>
<div class="flex items-center gap-2 mt-0.5">
<span class="text-[9px] md:text-[11px] font-black text-slate-500 uppercase tracking-widest">Asset Target:</span>
<span class="px-2 py-0.5 rounded-lg bg-blue-600/10 text-blue-400 font-black text-[10px] md:text-xs border border-blue-500/20">{{ symbol }}</span>
</div>
</div>
</div>
<button @click="closeModal" class="p-2 md:p-3 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded-xl md:rounded-2xl transition-all shadow-lg active:scale-95">
<X class="w-5 h-5 md:w-6 md:h-6" />
</button>
</div>
<!-- MAIN AREA: STACKED ON MOBILE -->
<div class="flex-1 flex flex-col lg:flex-row overflow-hidden bg-[#05080f]/30 min-h-0">
<!-- LEFT: VERDICT & STRATEGY -->
<div class="flex-1 overflow-y-auto custom-scrollbar p-6 md:p-12 space-y-8 md:space-y-12 min-h-0 overscroll-contain" style="-webkit-overflow-scrolling: touch;">
<!-- LOADING STATE -->
<div v-if="loading || isStreaming" class="flex flex-col items-center justify-center py-20 text-center space-y-8">
<div class="relative">
<div class="w-24 h-24 md:w-32 md:h-32 border-4 border-slate-800 rounded-[2.5rem] md:rounded-[3.5rem] flex items-center justify-center shadow-2xl">
<Brain class="w-10 h-10 md:w-16 md:h-16 text-blue-500 animate-pulse" />
<div class="absolute inset-0 border-4 border-blue-500 border-t-transparent rounded-[2.5rem] md:rounded-[3.5rem] animate-spin"></div>
</div>
<div class="absolute -bottom-2 -right-2 bg-emerald-500 w-8 h-8 rounded-full border-4 border-[#0b1120] flex items-center justify-center animate-bounce">
<Activity class="w-4 h-4 text-white" />
</div>
</div>
<div class="space-y-2">
<h3 class="text-xl md:text-3xl font-black text-white tracking-tight leading-tight">{{ streamingStep || loadingStep }}</h3>
<p class="text-xs md:text-sm font-black text-slate-500 uppercase tracking-[0.3em]">Decrypting multi-agent consensus</p>
</div>
<div class="w-full max-w-sm h-1.5 bg-slate-900 rounded-full overflow-hidden border border-slate-800">
<div class="h-full bg-blue-500 shadow-lg shadow-blue-500/50 transition-all duration-300" :style="{ width: streamingProgress + '%' }"></div>
</div>
</div>
<!-- SUCCESS STATE -->
<template v-else-if="analysisData">
<!-- VERDICT CARD -->
<div class="relative p-6 md:p-10 rounded-[2rem] md:rounded-[3rem] border-2 group transition-all overflow-hidden"
:class="analysisData.final_decision === 'TRADE' ? 'bg-emerald-500/10 border-emerald-500/40 shadow-xl shadow-emerald-500/5' : 'bg-amber-500/10 border-amber-500/40 shadow-xl shadow-amber-500/5'">
<Zap class="absolute -right-10 -top-10 w-40 h-40 opacity-5 group-hover:scale-110 transition-transform duration-700" :class="analysisData.final_decision === 'TRADE' ? 'text-emerald-500' : 'text-amber-500'" />
<div class="relative z-10 flex flex-col sm:flex-row justify-between items-start sm:items-center gap-6">
<div class="space-y-2">
<span class="text-[10px] md:text-xs font-black uppercase tracking-[.4em] opacity-60 block" :class="analysisData.final_decision === 'TRADE' ? 'text-emerald-400' : 'text-amber-400'">Final Intelligence Decision</span>
<h3 class="text-4xl md:text-6xl font-black text-white tracking-tighter">{{ analysisData.final_decision === 'TRADE' ? 'EXECUTE' : 'MAINTAIN' }}</h3>
<div class="flex items-center gap-3">
<div class="flex-1 h-2 bg-black/40 rounded-full overflow-hidden w-40">
<div class="h-full bg-current transition-all duration-1000 shadow-inner" :style="{ width: analysisData.confidence + '%' }" :class="analysisData.final_decision === 'TRADE' ? 'bg-emerald-500' : 'bg-amber-500'"></div>
</div>
<span class="text-sm font-black text-white whitespace-nowrap">{{ analysisData.confidence }}% CONFIDENCE</span>
</div>
</div>
<div class="w-16 h-16 md:w-24 md:h-24 rounded-3xl md:rounded-[2.5rem] bg-white/5 flex items-center justify-center border border-white/10 shadow-2xl backdrop-blur-md">
<component :is="analysisData.final_decision === 'TRADE' ? TrendingUp : Clock" class="w-8 h-8 md:w-12 md:h-12" :class="analysisData.final_decision === 'TRADE' ? 'text-emerald-400' : 'text-amber-400'" />
</div>
</div>
</div>
<!-- STRATEGY BREAKDOWN -->
<div class="space-y-6">
<div class="flex items-center gap-4">
<div class="h-px bg-slate-800 flex-1"></div>
<span class="text-[10px] font-black text-slate-500 uppercase tracking-widest">Logic Vectors</span>
<div class="h-px bg-slate-800 flex-1"></div>
</div>
<div class="bg-slate-900 border border-slate-800 p-6 md:p-8 rounded-[2rem] shadow-xl relative overflow-hidden group">
<div class="absolute inset-0 bg-gradient-to-br from-blue-500/[0.02] to-transparent opacity-0 group-hover:opacity-100 transition-opacity"></div>
<div class="relative z-10 flex flex-col gap-6">
<div class="flex items-start gap-5">
<div class="w-12 h-12 bg-blue-600/10 rounded-2xl flex items-center justify-center text-blue-400 border border-blue-500/20 shrink-0">
<Target class="w-6 h-6" />
</div>
<div class="space-y-1">
<h4 class="text-xs font-black text-slate-500 uppercase tracking-widest">Actionable Intelligence</h4>
<p class="text-sm md:text-xl font-bold text-white leading-relaxed">{{ analysisData.actionable_recommendation }}</p>
</div>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="bg-slate-950/80 border border-slate-800 p-5 rounded-2xl flex items-center gap-4">
<div class="w-10 h-10 bg-indigo-500/10 rounded-xl flex items-center justify-center text-indigo-400 border border-indigo-500/20">
<Zap class="w-5 h-5" />
</div>
<div>
<div class="text-[9px] font-black text-slate-600 uppercase tracking-widest mb-0.5">Entry Target</div>
<div class="text-base font-black text-white font-mono">${{ analysisData.entry_price || 'MARKET' }}</div>
</div>
</div>
<div class="bg-slate-950/80 border border-slate-800 p-5 rounded-2xl flex items-center gap-4">
<div class="w-10 h-10 bg-rose-500/10 rounded-xl flex items-center justify-center text-rose-400 border border-rose-500/20">
<AlertTriangle class="w-5 h-5" />
</div>
<div>
<div class="text-[9px] font-black text-slate-600 uppercase tracking-widest mb-0.5">Risk Exposure</div>
<div class="text-xs font-bold text-slate-300 leading-tight">{{ analysisData.risk_warning?.substring(0, 50) || 'Standard Vector Integrity' }}...</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- TOGGLE FOR MOBILE LOGS -->
<button @click="showLogsMobile = !showLogsMobile" class="lg:hidden w-full py-4 bg-slate-900 border border-slate-800 rounded-2xl flex items-center justify-center gap-2 text-xs font-black uppercase tracking-widest text-slate-400 transition-all">
<Activity class="w-4 h-4" /> {{ showLogsMobile ? 'Hide Process Feed' : 'Show Process Feed' }}
<ChevronDown class="w-4 h-4 transition-transform duration-300" :class="showLogsMobile ? 'rotate-180' : ''" />
</button>
</template>
<!-- ERROR STATE -->
<div v-else-if="error" class="flex flex-col items-center justify-center py-20 text-center px-10">
<div class="w-20 h-20 bg-rose-500/10 rounded-3xl flex items-center justify-center border border-rose-500/20 text-rose-500 mb-6 animate-pulse">
<AlertTriangle class="w-10 h-10" />
</div>
<h3 class="text-xl font-bold text-white mb-2">Architect Link Severed</h3>
<p class="text-sm text-slate-500 max-w-xs mb-8">{{ error }}</p>
<button @click="runAnalysis" class="px-8 py-3 bg-white text-slate-950 font-black rounded-xl text-xs uppercase tracking-widest hover:scale-105 transition-all shadow-xl active:scale-95">Re-Establish Connection</button>
</div>
</div>
<!-- RIGHT: INTELLIGENCE FEED (TERMINAL) -->
<div class="w-full lg:w-[450px] bg-black border-t lg:border-t-0 lg:border-l border-slate-800 flex flex-col font-mono overflow-hidden transition-all duration-300 min-h-0"
:class="[showLogsMobile ? 'flex-1' : 'hidden lg:flex lg:flex-none h-0 lg:h-full']">
<div class="p-4 bg-slate-900/80 border-b border-slate-800 flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="w-2 h-2 bg-emerald-500 rounded-full animate-pulse shadow-[0_0_8px_rgba(16,185,129,0.8)]"></div>
<span class="text-[10px] font-black text-slate-400 tracking-widest">NEXUS_FEED_V2.1</span>
</div>
<span class="text-[9px] font-bold text-slate-600">ENCRYPTION: AES-256</span>
</div>
<div class="flex-1 overflow-y-auto px-4 md:px-6 py-6 space-y-6 custom-scrollbar min-h-0 overscroll-contain" ref="logsContainer" style="-webkit-overflow-scrolling: touch;">
<div v-if="agentLogs.length === 0" class="flex flex-col items-center justify-center mt-20 text-slate-800">
<Activity class="w-12 h-12 mb-4 opacity-10" />
<span class="text-[10px] font-black uppercase tracking-[0.4em]">Listening for signal...</span>
</div>
<div v-for="(log, i) in agentLogs" :key="i" class="animate-in slide-in-from-left duration-500">
<div class="flex gap-4">
<div class="w-10 h-10 rounded-xl bg-slate-900 border border-slate-800 flex items-center justify-center text-xl shadow-inner shrink-0">
{{ AGENT_ICONS[log.source] || 'πŸ€–' }}
</div>
<div class="flex-1 min-w-0">
<div class="flex items-center justify-between mb-1.5">
<span class="text-[9px] md:text-[10px] uppercase font-black tracking-widest" :class="getAgentColor(log.source)">{{ log.source }}</span>
<span class="text-[8px] text-slate-700">T+{{ i < 10 ? '0' + i : i }}ms</span>
</div>
<div class="text-[11px] md:text-[12px] leading-relaxed text-slate-400 bg-slate-950/50 p-3 rounded-2xl border border-white/5 whitespace-pre-wrap" v-html="log.content"></div>
</div>
</div>
</div>
<div v-if="loading || isStreaming" class="flex items-center gap-3 pt-4 border-t border-slate-900 border-dashed">
<div class="flex gap-1">
<div class="w-1 h-1 bg-blue-500 rounded-full animate-bounce"></div>
<div class="w-1 h-1 bg-blue-500 rounded-full animate-bounce [animation-delay:0.2s]"></div>
<div class="w-1 h-1 bg-blue-500 rounded-full animate-bounce [animation-delay:0.4s]"></div>
</div>
<span class="text-[9px] font-black text-blue-500 uppercase tracking-widest animate-pulse">Stream established...</span>
</div>
</div>
<div class="p-3 bg-slate-900/80 border-t border-slate-800 flex justify-between">
<div class="flex items-center gap-4 text-[8px] font-black text-slate-600 uppercase tracking-widest">
<span>Nodes: 4/4</span>
<span>Latency: 12ms</span>
</div>
<Activity class="w-3 h-3 text-slate-700" />
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.custom-scrollbar::-webkit-scrollbar { width: 4px; }
.custom-scrollbar::-webkit-scrollbar-track { background: transparent; }
.custom-scrollbar::-webkit-scrollbar-thumb { background: #1e293b; border-radius: 10px; }
.animate-in {
animation-duration: 0.4s;
animation-fill-mode: both;
}
@keyframes fadeIn {
from { opacity: 0; transform: scale(1.02); }
to { opacity: 1; transform: scale(1); }
}
.fade-in { animation-name: fadeIn; }
@keyframes slideInLeft {
from { opacity: 0; transform: translateX(-20px); }
to { opacity: 1; transform: translateX(0); }
}
.slide-in-from-left { animation-name: slideInLeft; }
</style>