Initial commit — Sistema Generador de Guiones V4.0

Pipeline completo: URL → Whisper → GPT-4o → pgvector → Supabase
Frontend Vue 3 + Tailwind, Backend Express + Vercel serverless functions
This commit is contained in:
2026-03-28 16:02:59 -05:00
commit 7695dd0be6
47 changed files with 7552 additions and 0 deletions

14
frontend/index.html Normal file
View File

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="es" class="dark">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Guiones IA</title>
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
</head>
<body class="text-on-surface antialiased overflow-x-hidden font-body bg-surface selection:bg-primary-container selection:text-on-primary-container">
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

2488
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

25
frontend/package.json Normal file
View File

@ -0,0 +1,25 @@
{
"name": "guiones-frontend",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@supabase/supabase-js": "^2.39.0",
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/forms": "^0.5.11",
"pinia": "^2.1.7",
"vue": "^3.4.15",
"vue-router": "^4.2.5"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.3",
"autoprefixer": "^10.4.17",
"postcss": "^8.4.35",
"tailwindcss": "^3.4.1",
"vite": "^5.0.12"
}
}

View File

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

31
frontend/src/App.vue Normal file
View File

@ -0,0 +1,31 @@
<template>
<div class="bg-surface min-h-screen text-on-surface selection:bg-primary/30">
<SideNavBar />
<TopAppBar />
<!-- Main Content Canvas -->
<main class="ml-64 pt-24 pb-12 px-8 min-h-screen relative">
<router-view v-slot="{ Component }">
<transition name="fade" mode="out-in">
<component :is="Component" />
</transition>
</router-view>
</main>
</div>
</template>
<script setup>
import SideNavBar from './components/SideNavBar.vue'
import TopAppBar from './components/TopAppBar.vue'
</script>
<style>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.2s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>

View File

@ -0,0 +1,15 @@
<template>
<div class="flex items-center gap-3 p-2.5 rounded-xl border transition-colors" :class="active ? 'bg-indigo-500/10 border-indigo-500/30' : 'bg-surface-container-lowest border-white/5'">
<div class="w-4 h-4 rounded-full flex items-center justify-center shrink-0" :class="active ? 'bg-indigo-500/20 text-indigo-400' : 'bg-white/5 text-transparent'">
<span v-if="active" class="material-symbols-outlined text-[12px] font-bold">check</span>
</div>
<span class="text-xs font-bold tracking-wide" :class="active ? 'text-white' : 'text-outline'">{{ label }}</span>
</div>
</template>
<script setup>
defineProps({
label: String,
active: Boolean
})
</script>

View File

@ -0,0 +1,30 @@
<template>
<div class="flex items-center justify-between border-b border-white/5 pb-2 last:border-0 last:pb-0">
<span class="text-[10px] font-bold text-outline uppercase tracking-wider">{{ label }}</span>
<template v-if="type === 'boolean'">
<span :class="value ? 'text-primary' : 'text-outline/40'" class="text-xs font-bold uppercase tracking-widest">
{{ value ? 'YES' : 'NO' }}
</span>
</template>
<template v-else>
<span v-if="value" class="text-xs font-medium text-right max-w-[150px] truncate" :class="highlight ? 'text-white' : 'text-on-surface-variant'">
{{ value }}
</span>
<span v-else class="text-gray-600 text-xs"></span>
</template>
</div>
</template>
<script setup>
defineProps({
label: String,
value: [String, Number, Boolean],
type: {
type: String,
default: 'text' // or 'boolean'
},
highlight: Boolean
})
</script>

View File

@ -0,0 +1,45 @@
<template>
<aside class="fixed left-0 top-0 h-full z-40 flex flex-col p-4 w-64 border-r border-white/5 bg-[#13131a] font-['Manrope'] antialiased shadow-2xl shadow-indigo-500/10">
<!-- Branding -->
<div class="flex items-center gap-3 mb-10 px-2">
<div class="w-8 h-8 rounded bg-gradient-to-br from-primary-container to-primary flex items-center justify-center">
<span class="material-symbols-outlined text-on-primary-container text-lg" style="font-variation-settings: 'FILL' 1;">psychology</span>
</div>
<div>
<h1 class="text-xl font-bold tracking-tight text-white">Guiones IA</h1>
<p class="text-[10px] uppercase tracking-widest text-primary/60 font-bold">Marketing Pro</p>
</div>
</div>
<!-- Navigation -->
<nav class="flex-1 space-y-1">
<router-link to="/" class="flex items-center gap-3 px-3 py-2.5 text-[#c7c4d7] hover:text-white hover:bg-white/5 transition-all duration-200 group relative" active-class="bg-white/10 !text-white rounded-lg font-semibold">
<span class="material-symbols-outlined text-[20px]">dashboard</span>
<span class="text-sm font-medium">Dashboard</span>
</router-link>
<router-link to="/analysis" class="flex items-center gap-3 px-3 py-2.5 text-[#c7c4d7] hover:text-white hover:bg-white/5 transition-all duration-200 group relative" active-class="bg-white/10 !text-white rounded-lg font-semibold">
<span class="material-symbols-outlined text-[20px]">analytics</span>
<span class="text-sm font-medium">Analysis</span>
</router-link>
<router-link to="/scripts" class="flex items-center gap-3 px-3 py-2.5 text-[#c7c4d7] hover:text-white hover:bg-white/5 transition-all duration-200 group relative" active-class="bg-white/10 !text-white rounded-lg font-semibold">
<span class="material-symbols-outlined text-[20px]">description</span>
<span class="text-sm font-medium">Scripts</span>
</router-link>
<router-link to="/settings" class="flex items-center gap-3 px-3 py-2.5 text-[#c7c4d7] hover:text-white hover:bg-white/5 transition-all duration-200 group relative" active-class="bg-white/10 !text-white rounded-lg font-semibold">
<span class="material-symbols-outlined text-[20px]">settings</span>
<span class="text-sm font-medium">Settings</span>
</router-link>
</nav>
<!-- Footer Action -->
<div class="mt-auto pt-6 border-t border-white/5">
<router-link to="/new-analysis" class="w-full flex items-center justify-center gap-2 py-3 px-4 bg-primary-container text-on-primary-container rounded-lg font-bold text-sm shadow-lg shadow-primary/20 scale-95 active:scale-90 transition-transform hover:brightness-110">
<span class="material-symbols-outlined text-sm">add</span>
New Analysis
</router-link>
</div>
</aside>
</template>
<script setup>
</script>

View File

@ -0,0 +1,26 @@
<template>
<header class="fixed top-0 right-0 left-64 h-16 flex items-center justify-between px-8 z-30 bg-[#13131a]/80 backdrop-blur-xl font-['Manrope'] text-sm">
<div class="relative w-96 group">
<span class="material-symbols-outlined absolute left-3 top-1/2 -translate-y-1/2 text-outline text-lg group-focus-within:text-primary transition-colors">search</span>
<input class="w-full bg-surface-container-lowest border-none rounded-full pl-10 pr-4 py-2 text-sm focus:ring-2 focus:ring-primary/40 text-on-surface placeholder:text-outline/50 transition-all font-medium" placeholder="Search analyzed scripts..." type="text"/>
</div>
<div class="flex items-center gap-6">
<button class="relative text-[#c7c4d7] hover:text-white transition-opacity">
<span class="material-symbols-outlined">notifications</span>
<span class="absolute top-0 right-0 w-2 h-2 bg-secondary rounded-full border-2 border-[#13131a]"></span>
</button>
<div class="flex items-center gap-3 pl-6 border-l border-white/5">
<div class="text-right">
<p class="text-xs font-bold text-white leading-none">Alex Rivera</p>
<p class="text-[10px] text-outline font-medium">Growth Lead</p>
</div>
<div class="w-8 h-8 rounded-full overflow-hidden bg-surface-container-highest flex items-center justify-center">
<span class="material-symbols-outlined text-outline">account_circle</span>
</div>
</div>
</div>
</header>
</template>
<script setup>
</script>

22
frontend/src/lib/api.js Normal file
View File

@ -0,0 +1,22 @@
const BASE = '/api'
async function request(path, options = {}) {
const res = await fetch(`${BASE}${path}`, {
headers: { 'Content-Type': 'application/json' },
...options,
})
const data = await res.json()
if (!res.ok) throw new Error(data.error || `Error ${res.status}`)
return data
}
export const api = {
guiones: {
listar: (params = {}) => request('/guiones?' + new URLSearchParams(params)),
obtener: (id) => request(`/guiones/${id}`),
},
analizar: (body) => request('/analizar', { method: 'POST', body: JSON.stringify(body) }),
nichos: () => request('/nichos'),
clientes: () => request('/clientes'),
stats: () => request('/stats'),
}

7
frontend/src/main.js Normal file
View File

@ -0,0 +1,7 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import router from './router/index.js'
import App from './App.vue'
import './style.css'
createApp(App).use(createPinia()).use(router).mount('#app')

View File

@ -0,0 +1,43 @@
import { createRouter, createWebHistory } from 'vue-router'
import DashboardView from '../views/DashboardView.vue'
import AnalysisCreateView from '../views/AnalysisCreateView.vue'
import AnalysisDetailView from '../views/AnalysisDetailView.vue'
const routes = [
{
path: '/',
name: 'Dashboard',
component: DashboardView
},
{
path: '/new-analysis',
name: 'AnalysisCreate',
component: AnalysisCreateView
},
{
path: '/analysis/:id',
name: 'AnalysisDetail',
component: AnalysisDetailView
},
// Placeholders for sidebar consistency
{
path: '/analysis',
name: 'AnalysisList',
redirect: '/'
},
{
path: '/scripts',
name: 'Scripts',
component: () => import('../views/DashboardView.vue') // Placeholder
},
{
path: '/settings',
name: 'Settings',
component: () => import('../views/DashboardView.vue') // Placeholder
}
]
export default createRouter({
history: createWebHistory(),
routes
})

35
frontend/src/style.css Normal file
View File

@ -0,0 +1,35 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { @apply bg-surface; }
::-webkit-scrollbar-thumb { @apply bg-surface-container-highest rounded-full; }
}
@layer utilities {
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 300, 'GRAD' 0, 'opsz' 24;
}
.glass-panel {
background: rgba(31, 31, 38, 0.6);
backdrop-filter: blur(20px);
}
.neon-glow {
text-shadow: 0 0 10px rgba(78, 222, 163, 0.4);
}
.radial-gradient-score {
background: conic-gradient(from 0deg, #4edea3 88%, #1f1f26 0%);
}
.step-pulse {
box-shadow: 0 0 0 0 rgba(192, 193, 255, 0.4);
animation: pulse 2s infinite;
}
}
@keyframes pulse {
0% { box-shadow: 0 0 0 0 rgba(192, 193, 255, 0.4); }
70% { box-shadow: 0 0 0 10px rgba(192, 193, 255, 0); }
100% { box-shadow: 0 0 0 0 rgba(192, 193, 255, 0); }
}

View File

@ -0,0 +1,238 @@
<template>
<div class="max-w-7xl mx-auto flex flex-col gap-10">
<!-- Header -->
<header class="flex flex-col md:flex-row md:items-end justify-between gap-6 border-b border-white/5 pb-8">
<div>
<h1 class="text-5xl font-extrabold font-headline tracking-tighter text-white mb-2 leading-tight">New Video Analysis</h1>
<p class="text-primary text-sm font-bold flex items-center gap-2 tracking-widest uppercase">
<span class="material-symbols-outlined text-sm">rocket_launch</span>
GPT-4o + Whisper Pipeline Engine
</p>
</div>
<div class="flex items-center gap-4">
<button class="px-6 py-3 bg-surface-container border border-white/5 text-outline font-bold rounded-xl text-sm hover:text-white transition-colors">Save as Draft</button>
<button class="px-8 py-3 bg-gradient-to-br from-primary-container to-primary text-on-primary-container font-headline font-bold rounded-xl shadow-lg shadow-primary/20 hover:scale-105 active:scale-95 transition-all text-sm h-12 flex items-center gap-2" :disabled="analizando" @click="iniciarAnalisis">
<span class="material-symbols-outlined text-sm">{{ analizando ? 'hourglass_top' : 'auto_fix_high' }}</span>
{{ analizando ? 'Analyzing...' : 'Start Pipeline' }}
</button>
</div>
</header>
<div class="grid grid-cols-1 xl:grid-cols-12 gap-12">
<!-- Form Column -->
<div class="xl:col-span-7 flex flex-col gap-10">
<!-- Step 1: Source -->
<section class="space-y-6">
<div class="flex items-center gap-3">
<span class="w-8 h-8 rounded-lg bg-primary/10 text-primary flex items-center justify-center font-black text-sm border border-primary/20">01</span>
<h2 class="text-xl font-headline font-extrabold text-white tracking-tight">Source Identification</h2>
</div>
<div class="glass-panel p-8 rounded-3xl border border-white/5 shadow-2xl space-y-8">
<div class="space-y-3">
<label class="text-[10px] font-black uppercase tracking-widest text-outline ml-1">Video Destination URL</label>
<div class="relative group">
<span class="material-symbols-outlined absolute left-4 top-1/2 -translate-y-1/2 text-primary text-lg group-focus-within:animate-pulse">link</span>
<input v-model="form.url" type="url" placeholder="https://www.tiktok.com/@user/video/..." class="w-full bg-surface-container-low border border-white/10 rounded-2xl pl-12 pr-4 py-4 text-sm focus:ring-2 focus:ring-primary/40 focus:border-primary/40 text-white placeholder:text-outline/40 transition-all font-medium font-serif" :disabled="analizando"/>
</div>
<p class="text-[10px] text-outline/60 italic px-1">Supports TikTok, Instagram Reels and YouTube Shorts.</p>
</div>
<div class="grid grid-cols-2 gap-6">
<div class="space-y-3">
<label class="text-[10px] font-black uppercase tracking-widest text-outline ml-1">Organization / Client</label>
<select v-model="form.cliente_id" class="w-full bg-surface-container-low border border-white/10 rounded-2xl px-4 py-4 text-sm focus:ring-2 focus:ring-primary/40 text-on-surface font-medium appearance-none transition-all shadow-inner" :disabled="analizando">
<option :value="null">In-house / Internal</option>
<option v-for="c in clientes" :key="c.id" :value="c.id">{{ c.nombre }}</option>
</select>
</div>
<div class="space-y-3">
<label class="text-[10px] font-black uppercase tracking-widest text-outline ml-1">Project Name (Optional)</label>
<input v-model="form.proyecto_nombre" type="text" placeholder="e.g. Q1 Campaign" class="w-full bg-surface-container-low border border-white/10 rounded-2xl px-4 py-4 text-sm focus:ring-2 focus:ring-primary/40 text-on-surface font-medium transition-all shadow-inner" :disabled="analizando"/>
</div>
</div>
</div>
</section>
<!-- Step 2: Contextual Metrics -->
<section class="space-y-6">
<div class="flex items-center gap-3">
<span class="w-8 h-8 rounded-lg bg-secondary/10 text-secondary flex items-center justify-center font-black text-sm border border-secondary/20">02</span>
<h2 class="text-xl font-headline font-extrabold text-white tracking-tight">Market Intelligence</h2>
</div>
<div class="glass-panel p-8 rounded-3xl border border-white/5 shadow-2xl space-y-8">
<div class="grid grid-cols-2 gap-6">
<div class="space-y-3">
<label class="text-[10px] font-black uppercase tracking-widest text-outline ml-1">Primary Niche</label>
<div class="relative">
<input v-model="form.niche" list="nichos-list" placeholder="Select or type..." class="w-full bg-surface-container-low border border-white/10 rounded-2xl px-4 py-4 text-sm focus:ring-2 focus:ring-primary/40 text-on-surface font-black uppercase tracking-widest shadow-inner" :disabled="analizando"/>
<datalist id="nichos-list">
<option v-for="n in nichos" :key="n" :value="n">{{ n }}</option>
</datalist>
</div>
</div>
<div class="space-y-3">
<label class="text-[10px] font-black uppercase tracking-widest text-outline ml-1">Target Audience</label>
<input v-model="form.mercado_objetivo" type="text" placeholder="e.g. Female Entrepreneurs 25-35" class="w-full bg-surface-container-low border border-white/10 rounded-2xl px-4 py-4 text-sm focus:ring-2 focus:ring-primary/40 text-on-surface font-medium shadow-inner" :disabled="analizando"/>
</div>
</div>
<div class="grid grid-cols-3 gap-6">
<div class="space-y-3">
<label class="text-[10px] font-black uppercase tracking-widest text-outline ml-1">Views</label>
<input v-model.number="form.vistas" type="number" placeholder="0" class="w-full bg-surface-container-low border border-white/10 rounded-2xl px-4 py-4 text-sm focus:ring-2 focus:ring-primary/40 text-on-surface font-bold text-center" :disabled="analizando"/>
</div>
<div class="space-y-3">
<label class="text-[10px] font-black uppercase tracking-widest text-outline ml-1">Likes</label>
<input v-model.number="form.likes" type="number" placeholder="0" class="w-full bg-surface-container-low border border-white/10 rounded-2xl px-4 py-4 text-sm focus:ring-2 focus:ring-primary/40 text-on-surface font-bold text-center" :disabled="analizando"/>
</div>
<div class="space-y-3">
<label class="text-[10px] font-black uppercase tracking-widest text-outline ml-1">Shares / Saves</label>
<input v-model.number="form.compartidos" type="number" placeholder="0" class="w-full bg-surface-container-low border border-white/10 rounded-2xl px-4 py-4 text-sm focus:ring-2 focus:ring-primary/40 text-on-surface font-bold text-center" :disabled="analizando"/>
</div>
</div>
<div class="flex items-center gap-4 p-4 rounded-2xl bg-surface-container-lowest border border-white/5">
<input v-model="form.competidor_referente" type="checkbox" id="check-comp" class="w-5 h-5 rounded border-white/10 bg-surface text-primary focus:ring-primary focus:ring-offset-surface-container ring-offset-2 transition-all cursor-pointer" :disabled="analizando"/>
<label for="check-comp" class="text-xs font-bold text-on-surface cursor-pointer select-none">Mark as Strategic Competitor Reference</label>
</div>
</div>
</section>
</div>
<!-- Status Column -->
<div class="xl:col-span-5 flex flex-col gap-6">
<div class="sticky top-24">
<div class="bg-surface-container p-8 rounded-[40px] border border-outline-variant/10 shadow-3xl relative overflow-hidden flex flex-col gap-8">
<div class="absolute -top-12 -right-12 w-32 h-32 bg-secondary/10 blur-3xl rounded-full"></div>
<div class="flex items-center justify-between border-b border-white/5 pb-6">
<div>
<h3 class="text-sm font-headline font-black text-white uppercase tracking-widest mb-1">Pipeline Engine</h3>
<p class="text-[10px] text-outline uppercase font-bold tracking-tight">Real-time Analysis Status</p>
</div>
<div class="flex items-center gap-2 px-3 py-1 bg-surface-container-low rounded-full border border-white/5">
<span class="w-1.5 h-1.5 rounded-full" :class="analizando ? 'bg-secondary animate-pulse' : 'bg-outline/30'"></span>
<span class="text-[10px] font-black tracking-widest uppercase" :class="analizando ? 'text-secondary' : 'text-outline'">{{ analizando ? 'Active' : 'Idle' }}</span>
</div>
</div>
<div class="space-y-8 relative">
<div class="absolute left-4 top-2 bottom-2 w-px bg-white/5 z-0"></div>
<div v-for="(s, idx) in pasisVisibles" :key="s.id" class="flex gap-4 relative z-10" :class="idx > currentStepIdx ? 'opacity-30' : 'opacity-100'">
<div class="w-8 h-8 rounded-full flex items-center justify-center shrink-0 transition-all duration-300" :class="idx === currentStepIdx ? 'bg-secondary text-on-secondary shadow-lg shadow-secondary/40 scale-110' : (idx < currentStepIdx ? 'bg-primary/20 text-primary' : 'bg-surface-container-highest text-outline')">
<span class="material-symbols-outlined text-sm font-black">{{ idx < currentStepIdx ? 'check' : s.icon }}</span>
</div>
<div>
<p class="text-xs font-black uppercase tracking-widest mb-1" :class="idx === currentStepIdx ? 'text-white' : 'text-outline'">{{ s.label }}</p>
<p class="text-[10px] font-bold tracking-tight leading-none" :class="idx === currentStepIdx ? 'text-secondary' : 'text-outline/40'">{{ s.desc }}</p>
</div>
</div>
</div>
<div v-if="error" class="p-4 rounded-xl bg-red-500/10 border border-red-500/30 text-red-500 text-[11px] font-bold leading-relaxed shadow-lg">
<div class="flex items-center gap-2 mb-1">
<span class="material-symbols-outlined text-sm">report</span>
<span class="uppercase tracking-widest">Pipeline Error</span>
</div>
{{ error }}
</div>
<div v-if="analizando" class="mt-4 pt-4 border-t border-white/5 flex flex-col gap-3">
<div class="w-full bg-surface-container-low h-1 rounded-full overflow-hidden">
<div class="bg-secondary h-full transition-all duration-500" :style="{ width: ((currentStepIdx / 4) * 100) + '%' }"></div>
</div>
<p class="text-[10px] text-outline italic text-center animate-pulse">Processing vector embeddings and OpenAI request. Estimated time remaining: 15s</p>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router'
import { api } from '../lib/api.js'
const router = useRouter()
const analizando = ref(false)
const error = ref(null)
const paso = ref('inicio')
const nichos = ref([])
const clientes = ref([])
const form = ref({
url: '',
niche: '',
sub_niche: '',
mercado_objetivo: '',
vistas: null,
likes: null,
compartidos: null,
cliente_id: null,
proyecto_nombre: '',
competidor_referente: false
})
const pasisVisibles = [
{ id: 'extraccion', label: 'Audio Extraction', icon: 'downloading', desc: 'Fetching source video and demuxing audio stream.' },
{ id: 'transcripcion', label: 'Whisper Context', icon: 'mic', desc: 'Generating millisecond-perfect SRD transcription.' },
{ id: 'analisis', label: 'GPT-4o Reasoning', icon: 'psychology', desc: 'Analyzing semantic hooks and neuro-marketing patterns.' },
{ id: 'embedding', label: 'Vector Encoding', icon: 'hub', desc: 'Finalizing database injection and embedding generation.' },
]
const currentStepIdx = computed(() => {
if (!analizando.value) return -1
return pasisVisibles.findIndex(p => p.id === paso.value)
})
onMounted(async () => {
try {
const [n, c] = await Promise.all([api.nichos(), api.clientes()])
nichos.value = n
clientes.value = c
} catch (e) { console.error(e) }
})
async function iniciarAnalisis() {
if (!form.value.url || !form.value.niche) {
error.value = "Missing URL or Niche. These parameters are mandatory for the pipeline."
return
}
const URL_SOPORTADAS = /^https?:\/\/(www\.)?(tiktok\.com|vm\.tiktok\.com|instagram\.com|youtube\.com|youtu\.be)/
if (!URL_SOPORTADAS.test(form.value.url)) {
error.value = "URL not supported. Only TikTok, Instagram Reels and YouTube Shorts are accepted."
return
}
analizando.value = true
error.value = null
paso.value = 'extraccion'
const fakeInterval = setInterval(() => {
if (paso.value === 'extraccion') paso.value = 'transcripcion';
else if (paso.value === 'transcripcion') paso.value = 'analisis';
else if (paso.value === 'analisis') paso.value = 'embedding';
}, 4000)
try {
const res = await api.analizar(form.value)
clearInterval(fakeInterval)
paso.value = 'embedding'
setTimeout(() => {
router.push({ name: 'AnalysisDetail', params: { id: res.guion_id } })
}, 1000)
} catch (err) {
clearInterval(fakeInterval)
error.value = err.message
} finally {
analizando.value = false
}
}
</script>

View File

@ -0,0 +1,231 @@
<template>
<div v-if="cargando" class="flex flex-col items-center justify-center py-24 gap-4 min-h-[50vh]">
<div class="w-10 h-10 border-2 border-primary border-t-transparent rounded-full animate-spin"></div>
<p class="text-outline text-sm animate-pulse">Loading Neuro-Semantic Data...</p>
</div>
<div v-else-if="guion" class="max-w-6xl mx-auto relative flex flex-col gap-8 pb-12">
<!-- Header Section -->
<header class="relative z-10 flex flex-col md:flex-row md:items-end justify-between gap-6">
<div>
<router-link to="/" class="flex items-center gap-2 text-outline hover:text-white transition-colors text-sm font-bold uppercase tracking-widest mb-6 w-fit">
<span class="material-symbols-outlined text-lg">west</span> Back to Hub
</router-link>
<div class="flex items-center gap-3 mb-3 flex-wrap">
<span class="px-3 py-1 bg-surface-container-highest text-on-surface-variant text-[11px] font-black rounded uppercase tracking-widest">{{ guion.niche }}</span>
<span v-if="guion.sub_niche" class="px-3 py-1 bg-surface-container-low border border-outline-variant/20 text-outline text-[11px] font-bold rounded shadow-sm">{{ guion.sub_niche }}</span>
<span :class="plataformaBadge(guion.plataforma)" class="px-3 py-1 text-[11px] font-black rounded uppercase tracking-widest">{{ guion.plataforma }}</span>
</div>
<h1 class="text-4xl md:text-5xl font-extrabold font-headline tracking-tighter text-white mb-2 leading-tight max-w-3xl">
{{ guion.tema_principal || 'Untitled Analysis' }}
</h1>
<p class="text-primary text-sm font-bold flex items-center gap-2">
<span class="material-symbols-outlined text-sm">visibility</span>
{{ guion.angulo_unico || 'Unique Angle Not Specified' }}
</p>
</div>
<div class="flex items-center gap-4 shrink-0">
<button v-if="guion.url_origen" class="h-12 w-12 rounded-xl bg-surface-container-low border border-outline-variant/20 flex items-center justify-center text-on-surface hover:bg-surface-container transition-colors shadow-lg" title="Watch Original Web" @click="openUrl(guion.url_origen)">
<span class="material-symbols-outlined">link</span>
</button>
<div class="h-12 w-12 rounded-xl bg-surface-container-low border border-outline-variant/20 flex items-center justify-center text-on-surface hover:bg-surface-container transition-colors shadow-lg cursor-pointer">
<span class="material-symbols-outlined">bookmark</span>
</div>
<button class="px-6 py-3 bg-gradient-to-br from-primary-container to-primary text-on-primary-container font-headline font-bold rounded-xl shadow-lg shadow-primary/20 hover:scale-105 active:scale-95 transition-all text-sm h-12 flex items-center gap-2">
<span class="material-symbols-outlined text-sm">download</span> Vector Package
</button>
</div>
</header>
<!-- Main Grid -->
<div class="grid grid-cols-1 xl:grid-cols-12 gap-8 relative z-10">
<!-- Left Column: Core Analytics -->
<div class="xl:col-span-4 flex flex-col gap-6">
<!-- Score Card -->
<div class="bg-surface-container p-6 rounded-3xl border border-outline-variant/10 shadow-2xl relative overflow-hidden group">
<div class="absolute -top-24 -right-24 w-48 h-48 bg-primary/20 blur-3xl rounded-full group-hover:bg-primary/30 transition-colors pointer-events-none"></div>
<h3 class="text-sm font-headline font-bold text-white mb-8 flex items-center gap-2">
<span class="material-symbols-outlined text-primary">analytics</span> Neuro-Engagement Score
</h3>
<div class="flex justify-center mb-6 relative">
<svg class="w-48 h-48 transform -rotate-90" viewBox="0 0 100 100">
<circle cx="50" cy="50" r="45" fill="none" stroke="rgba(255,255,255,0.05)" stroke-width="8" />
<circle cx="50" cy="50" r="45" fill="none" class="stroke-primary drop-shadow-[0_0_8px_rgba(78,222,163,0.5)] transition-all duration-1000 ease-out" stroke-width="8" :stroke-dasharray="`${(guion.score_virabilidad || 0)/100 * 283} 283`" stroke-linecap="round" />
</svg>
<div class="absolute inset-0 flex flex-col items-center justify-center">
<span class="text-6xl font-black font-headline text-white tracking-tighter neon-glow">{{ guion.score_virabilidad || 0 }}</span>
<span class="text-xs font-bold text-primary uppercase tracking-widest mt-1">/ 100</span>
</div>
</div>
<div class="grid grid-cols-2 gap-4 pt-6 border-t border-white/5">
<div>
<p class="text-[10px] text-outline uppercase tracking-widest font-bold mb-1">Cialdini Index</p>
<p class="text-xl font-bold text-white">{{ guion.score_cialdini ?? 0 }}<span class="text-sm text-outline">/7</span></p>
</div>
<div>
<p class="text-[10px] text-outline uppercase tracking-widest font-bold mb-1">Real Eng.</p>
<p class="text-xl font-bold text-emerald-400">{{ guion.score_engagement ? (guion.score_engagement*1).toFixed(2) + '%' : 'N/A' }}</p>
</div>
</div>
</div>
<div class="bg-surface-container p-6 rounded-3xl border border-outline-variant/10 shadow-xl">
<h3 class="text-sm font-headline font-bold text-white mb-6 flex items-center gap-2">
<span class="material-symbols-outlined text-secondary">psychology_alt</span> Semantic Hooks
</h3>
<div class="space-y-4">
<div>
<p class="text-[10px] text-outline uppercase tracking-widest font-bold mb-2">Narrative Framework</p>
<div class="glass-panel p-3 rounded-lg border border-white/5 inline-block w-full">
<span class="text-sm font-bold text-on-surface">{{ guion.estructura_narrativa || 'Not Detected' }}</span>
</div>
</div>
<div>
<p class="text-[10px] text-outline uppercase tracking-widest font-bold mb-2 flex justify-between">
<span>Core Hook</span>
<span class="text-secondary">{{ guion.gancho_duracion_seg ? guion.gancho_duracion_seg + 's delay' : '' }}</span>
</p>
<div class="glass-panel p-4 rounded-xl border border-secondary/20 relative">
<div class="absolute top-0 right-0 p-2"><span class="w-1.5 h-1.5 rounded-full bg-secondary block"></span></div>
<p class="text-xs text-secondary font-bold uppercase tracking-wider mb-2">{{ guion.gancho_tipo || 'Standard Hook' }}</p>
<p class="text-sm text-white font-medium leading-relaxed italic">"{{ guion.gancho_texto || '—' }}"</p>
</div>
</div>
</div>
</div>
</div>
<!-- Center & Right Columns -->
<div class="xl:col-span-8 flex flex-col gap-6">
<div class="bg-surface-container p-8 rounded-3xl border border-primary/20 shadow-2xl relative overflow-hidden">
<div class="absolute inset-0 bg-gradient-to-br from-primary/5 to-transparent pointer-events-none"></div>
<p class="text-xs text-primary font-bold uppercase tracking-widest mb-4">Winning Pattern Synthesis</p>
<p class="text-lg md:text-xl text-white font-medium leading-relaxed max-w-3xl relative z-10">{{ guion.resumen_patron }}</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="bg-surface-container p-6 rounded-3xl border border-outline-variant/10 shadow-xl">
<h3 class="text-sm font-headline font-bold text-white mb-6 flex items-center gap-2">
<span class="material-symbols-outlined text-orange-400">local_fire_department</span> Emotional Resonance
</h3>
<div class="mb-6">
<div class="flex justify-between text-xs font-bold uppercase tracking-widest mb-2">
<span class="text-outline">Intensity</span>
<span class="text-orange-400">{{ guion.intensidad_emocional || 0 }}/10</span>
</div>
<div class="w-full bg-surface-container-highest h-1.5 rounded-full overflow-hidden">
<div class="bg-gradient-to-r from-orange-500/50 to-orange-400 h-full" :style="{ width: ((guion.intensidad_emocional||0)*10) + '%' }"></div>
</div>
</div>
<div class="space-y-4">
<DataRow label="Primary Trigger" :value="guion.trigger_emocional" highlight />
<DataRow label="Cognitive Bias" :value="guion.sesgo_cognitivo" />
<DataRow label="Pain / Pleasure" :value="guion.dolor_placer" highlight />
</div>
</div>
<div class="bg-surface-container p-6 rounded-3xl border border-outline-variant/10 shadow-xl flex flex-col">
<h3 class="text-sm font-headline font-bold text-white mb-6 flex items-center gap-2">
<span class="material-symbols-outlined text-indigo-400">group_work</span> Cialdini Framework
</h3>
<div class="grid grid-cols-2 gap-3 flex-1">
<CialdiniItem label="Reciprocity" :active="!!guion.cialdini_reciprocidad" />
<CialdiniItem label="Scarcity" :active="!!guion.cialdini_escasez" />
<CialdiniItem label="Authority" :active="!!guion.cialdini_autoridad" />
<CialdiniItem label="Consistency" :active="!!guion.cialdini_consistencia" />
<CialdiniItem label="Social Proof" :active="!!guion.cialdini_prueba_social" />
<CialdiniItem label="Liking" :active="!!guion.cialdini_simpatia" />
<CialdiniItem label="Unity" :active="!!guion.cialdini_unidad" />
</div>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="bg-surface-container p-6 rounded-3xl border border-outline-variant/10 shadow-xl">
<h3 class="text-sm font-headline font-bold text-white mb-6 flex items-center gap-2">
<span class="material-symbols-outlined text-fuchsia-400">biotech</span> Neuromarketing
</h3>
<div class="space-y-3">
<DataRow label="Visual Attention" :value="guion.atencion_visual" highlight />
<DataRow label="Cognitive Load" :value="guion.carga_cognitiva" highlight />
<DataRow label="Pacing" :value="guion.pacing_ritmo" highlight />
<DataRow label="Sensory Language" :value="guion.lenguaje_sensorial" type="boolean" />
<DataRow label="Contrast" :value="guion.contraste_narrativo" type="boolean" />
<DataRow label="Novelty Effect" :value="guion.efecto_novedad" type="boolean" />
</div>
</div>
<div class="bg-surface-container p-6 rounded-3xl border border-outline-variant/10 shadow-xl flex flex-col">
<h3 class="text-sm font-headline font-bold text-white mb-6 flex items-center gap-2">
<span class="material-symbols-outlined text-cyan-400">record_voice_over</span> Delivery & Reach
</h3>
<div class="space-y-4 mb-6">
<DataRow label="Tone" :value="guion.tono" highlight/>
<DataRow label="Perspective" :value="guion.persona_narradora" highlight/>
<DataRow label="Specificity" :value="guion.nivel_especificidad" highlight/>
<div>
<p class="text-[10px] text-outline uppercase tracking-widest font-bold mb-2">Keywords Extracted</p>
<div class="flex flex-wrap gap-2">
<span v-for="kw in guion.palabras_clave" :key="kw" class="px-2 py-1 bg-surface-container-lowest border border-white/5 rounded text-[10px] font-bold text-on-surface-variant">{{ kw }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- Transcript Viewer -->
<div class="bg-surface-container p-6 rounded-3xl border border-outline-variant/10 shadow-xl">
<div class="flex items-center justify-between mb-2">
<h3 class="text-sm font-headline font-bold text-white flex items-center gap-2">
<span class="material-symbols-outlined text-outline">notes</span> Full Transcription
</h3>
<button @click="showTranscript = !showTranscript" class="text-xs font-bold uppercase tracking-widest text-primary hover:text-white transition-colors">
{{ showTranscript ? 'Collapse' : 'Expand' }}
</button>
</div>
<div :class="showTranscript ? 'max-h-[800px]' : 'max-h-24'" class="overflow-hidden relative transition-all duration-500 ease-in-out">
<div v-if="!showTranscript" class="absolute inset-0 bg-gradient-to-t from-surface-container to-transparent z-10"></div>
<p class="text-sm text-on-surface-variant leading-relaxed whitespace-pre-wrap font-serif pt-4">
{{ guion.transcript || 'Video without available transcription.' }}
</p>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { api } from '../lib/api.js'
import CialdiniItem from '../components/CialdiniItem.vue'
import DataRow from '../components/DataRow.vue'
const route = useRoute()
const guion = ref(null)
const cargando = ref(true)
const showTranscript = ref(false)
function openUrl(url) {
if(url) window.open(url, '_blank')
}
function plataformaBadge(p) {
return {
tiktok: 'bg-red-500/20 text-red-400',
reels: 'bg-fuchsia-500/20 text-fuchsia-400',
shorts: 'bg-red-600/20 text-red-500'
}[p] ?? 'bg-white/5 text-on-surface-variant'
}
onMounted(async () => {
try {
guion.value = await api.guiones.obtener(route.params.id)
} finally {
cargando.value = false
}
})
</script>

View File

@ -0,0 +1,222 @@
<template>
<div class="max-w-7xl mx-auto flex flex-col gap-10">
<!-- Header -->
<header class="flex flex-col md:flex-row md:items-end justify-between gap-6 border-b border-white/5 pb-8">
<div>
<h1 class="text-5xl font-extrabold font-headline tracking-tighter text-white mb-2 leading-tight">Engineering Hub</h1>
<p class="text-primary text-sm font-bold flex items-center gap-2 tracking-widest uppercase">
<span class="material-symbols-outlined text-sm">terminal</span>
System Overview & Performance Analytics
</p>
</div>
<div class="flex items-center gap-4">
<button class="px-6 py-3 bg-surface-container border border-white/5 text-outline font-bold rounded-xl text-sm hover:text-white transition-colors" @click="cargarDatos">Refresh Hub</button>
<router-link to="/new-analysis" class="px-8 py-3 bg-gradient-to-br from-primary-container to-primary text-on-primary-container font-headline font-bold rounded-xl shadow-lg shadow-primary/20 hover:scale-105 active:scale-95 transition-all text-sm h-12 flex items-center gap-2">
<span class="material-symbols-outlined text-sm">add</span> New Analysis
</router-link>
</div>
</header>
<!-- KPI Bento Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<div v-for="stat in stats" :key="stat.niche" class="bg-surface-container p-6 rounded-3xl border border-outline-variant/10 shadow-xl relative overflow-hidden group">
<div class="absolute -bottom-6 -right-6 w-24 h-24 bg-primary/5 blur-2xl rounded-full group-hover:bg-primary/10 transition-colors"></div>
<p class="text-[10px] text-outline uppercase tracking-widest font-black mb-1">{{ stat.niche }}</p>
<div class="flex items-end justify-between">
<h2 class="text-3xl font-black font-headline text-white tracking-tighter">{{ stat.total_guiones }}</h2>
<div class="text-right">
<p class="text-[10px] text-primary font-bold uppercase tracking-tighter">Avg Score</p>
<p class="text-lg font-black text-primary leading-none">{{ (stat.avg_score || 0).toFixed(1) }}</p>
</div>
</div>
<div class="mt-4 w-full bg-surface-container-highest h-1 rounded-full overflow-hidden">
<div class="bg-primary h-full transition-all duration-1000" :style="{ width: (stat.avg_score || 0) + '%' }"></div>
</div>
</div>
<!-- Placeholder if no stats -->
<div v-if="stats.length === 0" class="lg:col-span-4 bg-surface-container/50 border border-dashed border-white/5 p-8 rounded-3xl flex items-center justify-center italic text-outline text-sm">
Connect database to view niche performance.
</div>
</div>
<!-- Main Content Grid -->
<div class="grid grid-cols-1 lg:grid-cols-12 gap-10">
<!-- Detailed Table -->
<div class="lg:col-span-12 xl:col-span-8 bg-surface-container rounded-3xl border border-outline-variant/10 shadow-2xl overflow-hidden flex flex-col">
<div class="px-8 py-6 border-b border-white/5 bg-surface-container-high/50 flex items-center justify-between">
<h3 class="text-sm font-headline font-black text-white uppercase tracking-widest flex items-center gap-2">
<span class="material-symbols-outlined text-outline text-lg">database</span> Analyzed Scripts
</h3>
<div class="flex gap-2">
<button class="px-4 py-2 bg-surface-container-low text-[10px] font-black uppercase text-outline rounded hover:text-white transition-colors" @click="cambiarPagina(filtros.page - 1)" :disabled="filtros.page <= 1">Prev</button>
<button class="px-4 py-2 bg-surface-container-low text-[10px] font-black uppercase text-outline rounded hover:text-white transition-colors" @click="cambiarPagina(filtros.page + 1)" :disabled="guiones.length < filtros.limit">Next</button>
</div>
</div>
<div class="overflow-x-auto flex-1 max-h-[600px] scrollbar-custom">
<table class="w-full text-left border-collapse">
<thead class="sticky top-0 bg-surface-container z-10 border-b border-white/5">
<tr class="text-[10px] font-black text-outline uppercase tracking-widest bg-surface-container-high/30">
<th class="px-8 py-4">Context</th>
<th class="px-6 py-4">Viral Score</th>
<th class="px-6 py-4">Neurometric Pattern</th>
<th class="px-8 py-4 text-right">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-white/5">
<tr v-for="g in guiones" :key="g.id" class="group hover:bg-white/[0.02] transition-colors cursor-pointer" @click="verDetalle(g.id)">
<td class="px-8 py-6">
<div class="flex flex-col gap-1.5">
<div class="flex items-center gap-2">
<span :class="plataformaBadge(g.plataforma)" class="text-[8px] font-black px-1.5 py-0.5 rounded uppercase tracking-widest">{{ g.plataforma }}</span>
<span class="text-[10px] text-outline font-bold uppercase tracking-wider">{{ g.niche }}</span>
</div>
<p class="text-sm font-bold text-white leading-tight group-hover:text-primary transition-colors line-clamp-2 max-w-sm">{{ g.tema_principal || 'No semantic title detected' }}</p>
<p class="text-[10px] text-outline/60 italic font-medium truncate max-w-xs">{{ g.url_origen }}</p>
</div>
</td>
<td class="px-6 py-6 font-headline">
<div class="flex flex-col gap-2">
<div class="flex items-center justify-between text-[11px] font-black tracking-tighter">
<span class="text-white">{{ g.score_virabilidad || 0 }}/100</span>
<span class="text-primary">{{ (g.score_engagement || 0).toFixed(1) }}% ENG</span>
</div>
<div class="w-32 bg-surface-container-highest h-1.5 rounded-full overflow-hidden">
<div class="bg-gradient-to-r from-primary/50 to-primary h-full" :style="{ width: (g.score_virabilidad || 0) + '%' }"></div>
</div>
</div>
</td>
<td class="px-6 py-6">
<div class="max-w-xs">
<p class="text-[11px] text-outline font-bold uppercase tracking-widest mb-1">{{ g.gancho_tipo || 'Hook' }}</p>
<p class="text-xs text-on-surface-variant line-clamp-2 leading-relaxed italic">"{{ g.gancho_texto }}"</p>
</div>
</td>
<td class="px-8 py-6 text-right">
<button class="p-2.5 rounded-xl bg-surface-container-low border border-white/5 text-outline hover:text-white hover:border-primary/20 transition-all opacity-0 group-hover:opacity-100 scale-90 group-hover:scale-100">
<span class="material-symbols-outlined text-xl">open_in_new</span>
</button>
</td>
</tr>
<tr v-if="guiones.length === 0 && !cargando">
<td colspan="4" class="py-24 text-center">
<div class="flex flex-col items-center gap-2 opacity-30">
<span class="material-symbols-outlined text-5xl">inventory_2</span>
<p class="text-sm font-bold uppercase tracking-widest">Repository Empty</p>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Right Column: Algorithm Pick -->
<div class="lg:col-span-12 xl:col-span-4 flex flex-col gap-8">
<div class="bg-surface-container p-8 rounded-[40px] border border-secondary/20 shadow-3xl relative overflow-hidden flex-1 flex flex-col gap-8 group">
<div class="absolute -top-12 -right-12 w-48 h-48 bg-secondary/5 blur-3xl rounded-full group-hover:bg-secondary/10 transition-colors"></div>
<div>
<div class="flex items-center gap-2 text-secondary font-black text-[10px] uppercase tracking-[0.2em] mb-3">
<span class="material-symbols-outlined text-sm animate-pulse">new_releases</span> Editors Pick
</div>
<h3 class="text-3xl font-headline font-black text-white tracking-widest leading-none mb-2">MASTER SCRIPT</h3>
<p class="text-xs text-outline font-bold">Top Performing Neuromarketing Pattern</p>
</div>
<div v-if="guionTop" class="space-y-6 flex-1 flex flex-col">
<div class="p-4 rounded-2xl bg-surface-container-low border border-white/5 flex flex-col gap-2">
<p class="text-[10px] text-secondary font-bold uppercase tracking-widest">{{ guionTop.niche }} {{ guionTop.sub_niche }}</p>
<p class="text-lg font-black text-white leading-tight font-headline">{{ guionTop.tema_principal }}</p>
</div>
<div class="flex-1 space-y-4">
<div>
<p class="text-[10px] text-outline font-black uppercase tracking-widest mb-2">Narrative Core</p>
<p class="text-sm text-on-surface-variant leading-relaxed line-clamp-4">{{ guionTop.resumen_patron }}</p>
</div>
<div class="grid grid-cols-2 gap-4">
<div class="p-3 rounded-xl bg-surface-container-low border border-white/5">
<p class="text-[10px] text-outline font-bold uppercase mb-1">Cialdini</p>
<p class="text-xl font-black text-white">{{ guionTop.score_cialdini }}/7</p>
</div>
<div class="p-3 rounded-xl bg-surface-container-low border border-white/5">
<p class="text-[10px] text-outline font-bold uppercase mb-1">Intensity</p>
<p class="text-xl font-black text-white">{{ guionTop.score_virabilidad }}%</p>
</div>
</div>
</div>
<button @click="verDetalle(guionTop.id)" class="w-full py-4 bg-white text-surface rounded-2xl font-black text-sm uppercase tracking-widest hover:bg-secondary hover:text-white transition-all transform active:scale-95 shadow-xl shadow-white/5">Analyze Vector</button>
</div>
<div v-else class="flex flex-col items-center justify-center flex-1 italic text-outline text-xs opacity-50">
No top patterns analyzed yet.
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router'
import { api } from '../lib/api.js'
const router = useRouter()
const guiones = ref([])
const stats = ref([])
const cargando = ref(true)
const filtros = ref({
page: 1,
limit: 20
})
const guionTop = computed(() => {
if (guiones.value.length === 0) return null
return [...guiones.value].sort((a,b) => (b.score_virabilidad || 0) - (a.score_virabilidad || 0))[0]
})
async function cargarDatos() {
cargando.value = true
try {
const [dg, ds] = await Promise.all([
api.guiones.listar(filtros.value),
api.stats()
])
guiones.value = dg.guiones
stats.value = ds
} catch (e) {
console.error(e)
} finally {
cargando.value = false
}
}
function cambiarPagina(p) {
if (p < 1) return
filtros.value.page = p
cargarDatos()
}
function verDetalle(id) {
router.push({ name: 'AnalysisDetail', params: { id } })
}
function plataformaBadge(p) {
const map = {
tiktok: 'bg-red-500/10 text-red-500 border border-red-500/20',
reels: 'bg-fuchsia-500/10 text-fuchsia-500 border border-fuchsia-500/20',
shorts: 'bg-red-600/10 text-red-600 border border-red-600/20'
}
return map[p] || 'bg-white/5 text-outline'
}
onMounted(() => {
cargarDatos()
})
</script>

View File

@ -0,0 +1,67 @@
/** @type {import('tailwindcss').Config} */
export default {
darkMode: "class",
content: ['./index.html', './src/**/*.{vue,js}'],
theme: {
extend: {
colors: {
"on-tertiary-fixed": "#301400",
"tertiary-fixed": "#ffdcc5",
"primary-container": "#8083ff",
"on-tertiary-fixed-variant": "#703700",
"surface-container-low": "#1b1b22",
"on-primary-container": "#0d0096",
"on-error-container": "#ffdad6",
"secondary": "#4edea3",
"surface-container-high": "#2a2931",
"surface": "#13131a",
"error-container": "#93000a",
"on-secondary-fixed-variant": "#005236",
"surface-container-lowest": "#0e0e15",
"surface-container": "#1f1f26",
"primary": "#c0c1ff",
"on-secondary-container": "#00311f",
"tertiary-fixed-dim": "#ffb783",
"on-surface": "#e4e1ec",
"surface-dim": "#13131a",
"outline": "#908fa0",
"on-error": "#690005",
"on-primary-fixed-variant": "#2f2ebe",
"inverse-on-surface": "#303038",
"surface-container-highest": "#34343c",
"surface-bright": "#393840",
"tertiary-container": "#d97721",
"background": "#13131a",
"secondary-container": "#00a572",
"secondary-fixed-dim": "#4edea3",
"on-tertiary": "#4f2500",
"primary-fixed-dim": "#c0c1ff",
"on-primary-fixed": "#07006c",
"on-primary": "#1000a9",
"on-surface-variant": "#c7c4d7",
"surface-variant": "#34343c",
"secondary-fixed": "#6ffbbe",
"outline-variant": "#464554",
"error": "#ffb4ab",
"inverse-surface": "#e4e1ec",
"on-tertiary-container": "#452000",
"inverse-primary": "#494bd6",
"on-background": "#e4e1ec",
"on-secondary": "#003824",
"surface-tint": "#c0c1ff",
"tertiary": "#ffb783",
"on-secondary-fixed": "#002113",
"primary-fixed": "#e1e0ff"
},
fontFamily: {
"headline": ["Manrope", "sans-serif"],
"body": ["Inter", "sans-serif"],
"label": ["Inter", "sans-serif"]
},
}
},
plugins: [
require('@tailwindcss/forms'),
require('@tailwindcss/container-queries')
],
}

15
frontend/vite.config.js Normal file
View File

@ -0,0 +1,15 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
server: {
port: 5173,
proxy: {
'/api': {
target: 'http://localhost:3001',
changeOrigin: true,
}
}
}
})