|
|
<script setup>
|
|
|
import {
|
|
|
X,
|
|
|
ArrowLeft,
|
|
|
LineChart,
|
|
|
TrendingUp,
|
|
|
Activity,
|
|
|
BarChart3,
|
|
|
Layers,
|
|
|
ChevronDown,
|
|
|
ChevronUp,
|
|
|
RotateCcw,
|
|
|
Trash2,
|
|
|
Save,
|
|
|
Check,
|
|
|
Database,
|
|
|
Search,
|
|
|
Plus,
|
|
|
Download,
|
|
|
CheckCircle2,
|
|
|
Clock,
|
|
|
Zap,
|
|
|
Globe
|
|
|
} from 'lucide-vue-next';
|
|
|
import { ref, onMounted, computed, watch } from 'vue';
|
|
|
import { api } from '../services/api';
|
|
|
|
|
|
const props = defineProps({
|
|
|
isOpen: Boolean,
|
|
|
initialTab: String
|
|
|
});
|
|
|
|
|
|
const emit = defineEmits(['close', 'universeUpdated']);
|
|
|
|
|
|
const activeTab = ref('universe');
|
|
|
const tabs = [
|
|
|
{ id: 'universe', name: 'Stock Universe', icon: Globe },
|
|
|
{ id: 'features', name: 'Feature Vault', icon: Activity },
|
|
|
{ id: 'sync', name: 'Feature Sync', icon: Zap }
|
|
|
];
|
|
|
|
|
|
|
|
|
const stockUniverse = ref([]);
|
|
|
const isFetchingUniverse = ref(false);
|
|
|
const symbolQuery = ref('');
|
|
|
const isSearching = ref(false);
|
|
|
const searchResults = ref([]);
|
|
|
|
|
|
const fetchUniverse = async () => {
|
|
|
isFetchingUniverse.value = true;
|
|
|
try {
|
|
|
const data = await api.get('/ai/universe');
|
|
|
stockUniverse.value = data;
|
|
|
} 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();
|
|
|
emit('universeUpdated');
|
|
|
fetchStatus();
|
|
|
} catch (error) {
|
|
|
console.error('Error adding to universe:', error);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
|
|
|
const categories = ref([
|
|
|
{
|
|
|
id: 'trend',
|
|
|
name: 'Trend Signals',
|
|
|
icon: TrendingUp,
|
|
|
color: 'text-blue-400',
|
|
|
indicators: [
|
|
|
{ id: 'sma', name: 'SMA (Moving Average)' },
|
|
|
{ id: 'ema', name: 'EMA (Exponential MA)' },
|
|
|
{ id: 'bb', name: 'Bollinger Bands' },
|
|
|
{ id: 'macd', name: 'MACD Convergence' },
|
|
|
{ id: 'ichimoku', name: 'Ichimoku Cloud' }
|
|
|
],
|
|
|
isOpen: true
|
|
|
},
|
|
|
{
|
|
|
id: 'momentum',
|
|
|
name: 'Momentum Logic',
|
|
|
icon: Activity,
|
|
|
color: 'text-emerald-400',
|
|
|
indicators: [
|
|
|
{ id: 'rsi', name: 'RSI Strength' },
|
|
|
{ id: 'stoch', name: 'Stochastic Osc' },
|
|
|
{ id: 'cci', name: 'Commodity Channel' },
|
|
|
{ id: 'williams', name: 'Williams %R' }
|
|
|
],
|
|
|
isOpen: false
|
|
|
},
|
|
|
{
|
|
|
id: 'volume',
|
|
|
name: 'Volume Matrices',
|
|
|
icon: BarChart3,
|
|
|
color: 'text-amber-400',
|
|
|
indicators: [
|
|
|
{ id: 'volume', name: 'Market Volume' },
|
|
|
{ id: 'obv', name: 'OBV Flow' },
|
|
|
{ id: 'vwap', name: 'VWAP Price' },
|
|
|
{ id: 'cmf', name: 'Chaikin Flow' }
|
|
|
],
|
|
|
isOpen: false
|
|
|
},
|
|
|
{
|
|
|
id: 'volatility',
|
|
|
name: 'Volatility Scope',
|
|
|
icon: Layers,
|
|
|
color: 'text-rose-400',
|
|
|
indicators: [
|
|
|
{ id: 'atr', name: 'ATR Range' },
|
|
|
{ id: 'keltner', name: 'Keltner Logic' },
|
|
|
{ id: 'donchian', name: 'Donchian Channels' }
|
|
|
],
|
|
|
isOpen: false
|
|
|
}
|
|
|
]);
|
|
|
|
|
|
const selectedIndicators = ref(new Set());
|
|
|
const isSaving = ref(false);
|
|
|
|
|
|
const fetchUserIndicators = async () => {
|
|
|
try {
|
|
|
const data = await api.get('/user/indicators');
|
|
|
selectedIndicators.value = new Set(data);
|
|
|
} catch (error) {
|
|
|
console.error('Error fetching indicators:', error);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
const saveIndicators = async () => {
|
|
|
isSaving.value = true;
|
|
|
try {
|
|
|
await api.post('/user/indicators', {
|
|
|
indicators: Array.from(selectedIndicators.value)
|
|
|
});
|
|
|
} catch (error) {
|
|
|
console.error('Error saving indicators:', error);
|
|
|
} finally {
|
|
|
isSaving.value = false;
|
|
|
}
|
|
|
};
|
|
|
|
|
|
const toggleIndicator = (id) => {
|
|
|
if (selectedIndicators.value.has(id)) {
|
|
|
selectedIndicators.value.delete(id);
|
|
|
} else {
|
|
|
selectedIndicators.value.add(id);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
const getActiveCount = (category) => {
|
|
|
return category.indicators.filter(ind => selectedIndicators.value.has(ind.id)).length;
|
|
|
};
|
|
|
|
|
|
|
|
|
const featuresStatus = ref({ display: '0/0', ready: 0, total: 0 });
|
|
|
const isSyncing = ref(false);
|
|
|
|
|
|
const fetchStatus = async () => {
|
|
|
try {
|
|
|
const data = await api.get('/ai/features-status');
|
|
|
featuresStatus.value = data;
|
|
|
} catch (error) {
|
|
|
console.error('Error fetching status:', error);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
const syncAll = async () => {
|
|
|
isSyncing.value = true;
|
|
|
try {
|
|
|
await api.post('/ai/universe/sync-all');
|
|
|
await fetchStatus();
|
|
|
} catch (error) {
|
|
|
console.error('Sync failed:', error);
|
|
|
} finally {
|
|
|
isSyncing.value = false;
|
|
|
}
|
|
|
};
|
|
|
|
|
|
onMounted(() => {
|
|
|
fetchUniverse();
|
|
|
fetchUserIndicators();
|
|
|
fetchStatus();
|
|
|
});
|
|
|
|
|
|
watch(() => props.isOpen, (newVal) => {
|
|
|
if (newVal) {
|
|
|
if (props.initialTab) activeTab.value = props.initialTab;
|
|
|
fetchUniverse();
|
|
|
fetchUserIndicators();
|
|
|
fetchStatus();
|
|
|
}
|
|
|
});
|
|
|
</script>
|
|
|
|
|
|
<template>
|
|
|
<div v-if="isOpen" class="fixed inset-0 z-[120] flex items-center justify-center md:p-4 overflow-hidden">
|
|
|
|
|
|
<div class="absolute inset-0 bg-slate-950/98 backdrop-blur-xl" @click="$emit('close')"></div>
|
|
|
|
|
|
|
|
|
<div class="relative w-full h-full md:h-auto md:max-w-4xl 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 md:max-h-[90vh]">
|
|
|
|
|
|
|
|
|
<div class="flex items-center justify-between px-6 py-5 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">
|
|
|
<Database class="w-6 h-6 md:w-8 md:h-8" />
|
|
|
</div>
|
|
|
<div>
|
|
|
<h2 class="text-xl md:text-2xl font-black text-white tracking-tight uppercase">Data <span class="text-blue-500">Hub</span></h2>
|
|
|
<p class="text-[9px] md:text-[10px] text-slate-500 font-black uppercase tracking-[0.2em] mt-0.5">Primary Vector Provisioning Node</p>
|
|
|
</div>
|
|
|
</div>
|
|
|
<button @click="$emit('close')" class="p-2.5 bg-slate-800 hover:bg-slate-700 rounded-xl transition-all">
|
|
|
<X class="w-5 h-5 text-slate-300" />
|
|
|
</button>
|
|
|
</div>
|
|
|
|
|
|
|
|
|
<div class="flex-none flex px-4 md:px-10 gap-2 border-b border-slate-800 bg-slate-950/20 overflow-x-auto custom-scrollbar">
|
|
|
<button v-for="t in tabs" :key="t.id" @click="activeTab = t.id"
|
|
|
class="px-6 py-5 text-[10px] md:text-xs font-black uppercase tracking-[0.2em] transition-all relative shrink-0"
|
|
|
:class="activeTab === t.id ? 'text-blue-400' : 'text-slate-500 hover:text-white'">
|
|
|
<span class="flex items-center gap-2">
|
|
|
<component :is="t.icon" class="w-4 h-4" />
|
|
|
{{ t.name }}
|
|
|
</span>
|
|
|
<div v-if="activeTab === t.id" class="absolute bottom-0 left-0 right-0 h-1 bg-blue-500 rounded-t-full shadow-[0_0_10px_rgba(59,130,246,0.5)]"></div>
|
|
|
</button>
|
|
|
</div>
|
|
|
|
|
|
|
|
|
<div class="flex-1 overflow-y-auto custom-scrollbar p-6 md:p-10 bg-[#05080f]/30 min-h-0 overscroll-contain" style="-webkit-overflow-scrolling: touch;">
|
|
|
|
|
|
|
|
|
<div v-if="activeTab === 'universe'" class="space-y-8 animate-in fade-in duration-500">
|
|
|
<div class="bg-blue-600/5 border border-blue-500/10 rounded-3xl p-6 flex flex-col sm:flex-row sm:items-center justify-between gap-6">
|
|
|
<div>
|
|
|
<h3 class="text-lg font-black text-white mb-1">Stock Universe</h3>
|
|
|
<p class="text-xs text-slate-500">Define the global pool of assets and indices for NEXUS to monitor.</p>
|
|
|
</div>
|
|
|
<div class="flex items-center gap-3">
|
|
|
<div class="relative">
|
|
|
<Search class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500" />
|
|
|
<input
|
|
|
v-model="symbolQuery"
|
|
|
@keydown.enter="performSearch"
|
|
|
type="text"
|
|
|
placeholder="Add Symbol..."
|
|
|
class="bg-slate-950 border border-slate-800 rounded-xl py-2.5 pl-10 pr-4 text-xs text-white focus:border-blue-500 focus:outline-none w-40 sm:w-56"
|
|
|
/>
|
|
|
</div>
|
|
|
<button @click="performSearch" class="p-2.5 bg-blue-600 hover:bg-blue-500 text-white rounded-xl transition-all shadow-lg active:scale-95">
|
|
|
<Plus class="w-5 h-5" />
|
|
|
</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
|
|
|
<div v-if="searchResults.length > 0" class="bg-slate-900 border border-slate-800 rounded-3xl overflow-hidden divide-y divide-slate-800 max-h-60 overflow-y-auto">
|
|
|
<div v-for="res in searchResults" :key="res.symbol" @click="addSymbol(res)" class="p-4 hover:bg-blue-600/10 cursor-pointer flex items-center justify-between group">
|
|
|
<div class="flex items-center gap-4">
|
|
|
<span class="text-base font-black text-white">{{ res.symbol }}</span>
|
|
|
<span class="text-xs text-slate-500 font-bold truncate">{{ res.name }}</span>
|
|
|
</div>
|
|
|
<Plus class="w-4 h-4 text-slate-700 group-hover:text-blue-500" />
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
|
|
|
<div class="grid grid-cols-2 sm:grid-cols-4 lg:grid-cols-6 gap-3">
|
|
|
<div v-for="s in stockUniverse" :key="s" class="p-3 bg-slate-900 border border-slate-800 rounded-2xl flex items-center justify-between group hover:border-blue-500/30 transition-all">
|
|
|
<span class="text-sm font-black text-white">{{ s }}</span>
|
|
|
<CheckCircle2 class="w-3.5 h-3.5 text-emerald-500/40 group-hover:text-emerald-500 transition-colors" />
|
|
|
</div>
|
|
|
<div v-if="stockUniverse.length === 0" class="col-span-full py-16 text-center border-2 border-dashed border-slate-800 rounded-[2.5rem]">
|
|
|
<Globe class="w-12 h-12 text-slate-800 mx-auto mb-4" />
|
|
|
<p class="text-xs font-black text-slate-600 uppercase tracking-widest">No assets provisioned</p>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
|
|
|
<div v-if="activeTab === 'features'" class="space-y-8 animate-in fade-in duration-500">
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
|
<div v-for="cat in categories" :key="cat.id" class="border border-slate-800 rounded-[2rem] bg-slate-900/40 overflow-hidden">
|
|
|
<div @click="cat.isOpen = !cat.isOpen" class="p-5 md:p-6 flex items-center justify-between cursor-pointer hover:bg-white/5 transition-colors">
|
|
|
<div class="flex items-center gap-4">
|
|
|
<div :class="['w-10 h-10 rounded-xl flex items-center justify-center bg-slate-950', cat.color]">
|
|
|
<component :is="cat.icon" class="w-5 h-5" />
|
|
|
</div>
|
|
|
<span class="font-black text-sm text-white uppercase tracking-widest">{{ cat.name }}</span>
|
|
|
</div>
|
|
|
<ChevronDown class="w-4 h-4 text-slate-600 transition-transform" :class="cat.isOpen ? 'rotate-180' : ''" />
|
|
|
</div>
|
|
|
<div v-if="cat.isOpen" class="px-5 pb-5 grid grid-cols-1 gap-1">
|
|
|
<button v-for="ind in cat.indicators" :key="ind.id" @click="toggleIndicator(ind.id)"
|
|
|
class="flex items-center justify-between p-3 rounded-xl transition-all border-2"
|
|
|
:class="selectedIndicators.has(ind.id) ? 'bg-blue-600/10 border-blue-500/30 text-white' : 'border-transparent text-slate-500 hover:bg-white/5'">
|
|
|
<span class="text-xs font-bold">{{ ind.name }}</span>
|
|
|
<div class="w-5 h-5 rounded-md border-2 flex items-center justify-center" :class="selectedIndicators.has(ind.id) ? 'bg-blue-600 border-blue-500' : 'border-slate-800 bg-slate-950'">
|
|
|
<Check v-if="selectedIndicators.has(ind.id)" class="w-3.5 h-3.5 text-white stroke-[3px]" />
|
|
|
</div>
|
|
|
</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
<button @click="saveIndicators" :disabled="isSaving" class="w-full py-4 bg-blue-600 hover:bg-blue-500 text-white font-black uppercase tracking-widest rounded-2xl shadow-xl shadow-blue-900/40 active:scale-95 disabled:opacity-50">
|
|
|
{{ isSaving ? 'Committing Preferences...' : 'Apply Config' }}
|
|
|
</button>
|
|
|
</div>
|
|
|
|
|
|
|
|
|
<div v-if="activeTab === 'sync'" class="space-y-8 animate-in fade-in duration-500 h-full flex flex-col justify-center max-w-2xl mx-auto py-10">
|
|
|
<div class="text-center space-y-4">
|
|
|
<div class="relative inline-block">
|
|
|
<div class="w-24 h-24 md:w-32 md:h-32 border-4 border-slate-800 rounded-[2.5rem] flex items-center justify-center">
|
|
|
<Zap class="w-10 h-10 md:w-16 md:h-16 text-blue-500" :class="isSyncing ? 'animate-pulse' : ''" />
|
|
|
<div v-if="isSyncing" class="absolute inset-0 border-4 border-blue-500 border-t-transparent rounded-[2.5rem] animate-spin"></div>
|
|
|
</div>
|
|
|
<div class="absolute -bottom-2 -right-2 w-10 h-10 bg-emerald-500 border-4 border-[#0b1120] rounded-full flex items-center justify-center">
|
|
|
<Activity class="w-5 h-5 text-white" />
|
|
|
</div>
|
|
|
</div>
|
|
|
<h3 class="text-2xl md:text-3xl font-black text-white tracking-tighter">Vault Synchronization</h3>
|
|
|
<p class="text-sm text-slate-500 font-medium">Reconcile local vector cache with historical market data.</p>
|
|
|
</div>
|
|
|
|
|
|
<div class="grid grid-cols-2 gap-4">
|
|
|
<div class="bg-slate-900 border border-slate-800 p-6 rounded-3xl">
|
|
|
<span class="text-[10px] font-black text-slate-600 uppercase tracking-widest mb-1 block">Provisioned</span>
|
|
|
<div class="text-3xl font-black text-white">{{ featuresStatus.total }} <span class="text-sm text-slate-600">Assets</span></div>
|
|
|
</div>
|
|
|
<div class="bg-slate-900 border border-slate-800 p-6 rounded-3xl">
|
|
|
<span class="text-[10px] font-black text-slate-600 uppercase tracking-widest mb-1 block">Vector Integrity</span>
|
|
|
<div class="text-3xl font-black text-emerald-400">{{ featuresStatus.ready }} <span class="text-sm text-slate-600">Ready</span></div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div class="space-y-4">
|
|
|
<div class="flex justify-between items-center text-[10px] font-black uppercase tracking-widest">
|
|
|
<span class="text-slate-500">Global Progress</span>
|
|
|
<span class="text-blue-500">{{ Math.round((featuresStatus.ready / featuresStatus.total) * 100) || 0 }}% Synced</span>
|
|
|
</div>
|
|
|
<div class="h-2 bg-slate-900 rounded-full border border-slate-800 overflow-hidden shadow-inner">
|
|
|
<div class="h-full bg-blue-600 shadow-[0_0_15px_rgba(37,99,235,0.5)] transition-all duration-1000" :style="{ width: (featuresStatus.ready / featuresStatus.total * 100) + '%' }"></div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div class="pt-6">
|
|
|
<button @click="syncAll" :disabled="isSyncing" class="w-full py-5 bg-white text-slate-950 font-black uppercase tracking-[.2em] rounded-3xl shadow-2xl hover:scale-[1.02] active:scale-95 transition-all text-xs flex items-center justify-center gap-4">
|
|
|
<Download class="w-5 h-5" /> {{ isSyncing ? 'Processing Swarm...' : 'Fetch All Assets' }}
|
|
|
</button>
|
|
|
<p class="text-[9px] text-center text-slate-600 font-bold uppercase tracking-widest mt-6 italic">Caution: High bandwidth operation will consume NEXUS quota.</p>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<style scoped>
|
|
|
.custom-scrollbar::-webkit-scrollbar { width: 5px; }
|
|
|
.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) translateY(10px); } to { opacity: 1; transform: scale(1) translateY(0); } }
|
|
|
.fade-in { animation-name: fadeIn; }
|
|
|
</style>
|
|
|
|