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

27
.gitignore vendored Normal file
View File

@ -0,0 +1,27 @@
# Dependencias
node_modules/
# Variables de entorno — NUNCA subir al repo
.env
.env.local
.env.*.local
# Build del frontend
dist/
.vite/
# Logs
*.log
npm-debug.log*
# Editor
.vscode/
.idea/
*.suo
*.ntvs*
*.njsproj
*.sln
# OS
.DS_Store
Thumbs.db

70
README.md Normal file
View File

@ -0,0 +1,70 @@
# Sistema Generador de Guiones IA
Este repositorio contiene el sistema completo de análisis y generación de guiones virales utilizando Whisper, GPT-4o y Supabase, con una interfaz web construida en Vue 3 y Tailwind CSS.
## Estructura del Proyecto
El proyecto está diseñado bajo una arquitectura modular y escalable:
```
/
├── backend/ # Lógica central (API, IA, Transcripción, Embeddings)
│ ├── api/ # Serverless handlers (ej. api/analizar.js)
│ ├── lib/ # Módulos de orquestación y LLM
│ └── server.js # Servidor Express.js para testeo y desarrollo local
├── frontend/ # Interfaz de usuario "Obsidian Architecture"
│ ├── src/ # Vistas en Vue 3 y componentes
│ └── tailwind... # Configuración del Design System
├── database/ # Archivos SQL para gestionar Supabase
│ ├── migrations/ # Setup de tablas (schema), funciones y RLS
│ └── seeds/ # Datos de prueba para iniciar la aplicación
└── docs/ # Archivos HTML originales del diseño (referencia)
```
## 1. Configuración de Variables de Entorno
En la carpeta `/backend/`, crea un archivo `.env` o renombra el existente con tus credenciales:
```env
RAPIDAPI_KEY= # Key de Social Download All In One
OPENAI_API_KEY= # Key de OpenAI para GPT-4o y Whisper
SUPABASE_URL= # URL de tu base de datos Supabase (https://xxxx.supabase.co)
SUPABASE_SERVICE_ROLE_KEY= # Tu Service Role Key (no exponer en el frontend)
```
## 2. Iniciar el Sistema en Local
Abre dos terminales para correr ambos entornos en simultáneo:
**Terminal 1 — Backend:**
```powershell
cd backend
npm install
npm run dev
```
> El servicio de IA y base de datos estará corriendo en `http://localhost:3001`
**Terminal 2 — Frontend:**
```powershell
cd frontend
npm install
npm run dev
```
> La interfaz estará disponible en `http://localhost:5173` (o `5174`)
## 3. Base de Datos (Supabase)
Si es la primera vez que despliegas el proyecto, dirígete a la consola SQL de tu proyecto en Supabase y ejecuta los archivos de la carpeta `/database` en el siguiente orden:
1. `database/migrations/01_schema.sql`
2. `database/migrations/02_funciones.sql`
3. `database/migrations/03_rls.sql`
4. `database/seeds/04_datos_prueba.sql` (Opcional)
## 4. Arquitectura de Despliegue
- **Frontend:** Preparado para desplegar de manera estática en Vercel, Netlify o Cloudflare Pages.
- **Backend:** Configurado con `vercel.json` para despliegues Serverless Edge/Node en Vercel (la ruta principal será `/api/analizar`). Alternativamente, funciona como un backend en la nube utilizando el `server.js` provisto.

1
api/analizar.js Normal file
View File

@ -0,0 +1 @@
export { default } from '../backend/api/analizar.js'

14
api/clientes.js Normal file
View File

@ -0,0 +1,14 @@
import { supabase } from '../backend/lib/supabase.js'
export default async function handler(req, res) {
if (req.method !== 'GET') return res.status(405).json({ error: 'Método no permitido' })
const { data, error } = await supabase
.from('clientes')
.select('id, nombre, industria')
.eq('activo', true)
.order('nombre')
if (error) return res.status(500).json({ error: error.message })
res.json(data)
}

30
api/guiones.js Normal file
View File

@ -0,0 +1,30 @@
import { supabase } from '../backend/lib/supabase.js'
export default async function handler(req, res) {
if (req.method !== 'GET') return res.status(405).json({ error: 'Método no permitido' })
const { niche, cliente_id, plataforma, page = 1, limit = 20 } = req.query
const offset = (Number(page) - 1) * Number(limit)
let query = supabase
.from('guiones')
.select(`
id, niche, sub_niche, plataforma, url_origen,
gancho_texto, gancho_tipo, estructura_narrativa, trigger_emocional,
tono, score_engagement, score_virabilidad, score_cialdini,
fecha_analisis, procesado_ok, vistas, likes, compartidos,
tema_principal, resumen_patron
`, { count: 'exact' })
.eq('procesado_ok', true)
.order('fecha_analisis', { ascending: false })
.range(offset, offset + Number(limit) - 1)
if (niche) query = query.eq('niche', niche)
if (cliente_id) query = query.eq('cliente_id', cliente_id)
if (plataforma) query = query.eq('plataforma', plataforma)
const { data, error, count } = await query
if (error) return res.status(500).json({ error: error.message })
res.json({ guiones: data, total: count, page: Number(page), limit: Number(limit) })
}

16
api/guiones/[id].js Normal file
View File

@ -0,0 +1,16 @@
import { supabase } from '../../backend/lib/supabase.js'
export default async function handler(req, res) {
if (req.method !== 'GET') return res.status(405).json({ error: 'Método no permitido' })
const { id } = req.query
const { data, error } = await supabase
.from('guiones')
.select('*')
.eq('id', id)
.single()
if (error) return res.status(404).json({ error: 'Guion no encontrado' })
res.json(data)
}

15
api/nichos.js Normal file
View File

@ -0,0 +1,15 @@
import { supabase } from '../backend/lib/supabase.js'
export default async function handler(req, res) {
if (req.method !== 'GET') return res.status(405).json({ error: 'Método no permitido' })
const { data, error } = await supabase
.from('guiones')
.select('niche')
.eq('procesado_ok', true)
if (error) return res.status(500).json({ error: error.message })
const nichos = [...new Set(data.map(r => r.niche))].sort()
res.json(nichos)
}

12
api/stats.js Normal file
View File

@ -0,0 +1,12 @@
import { supabase } from '../backend/lib/supabase.js'
export default async function handler(req, res) {
if (req.method !== 'GET') return res.status(405).json({ error: 'Método no permitido' })
const { data, error } = await supabase
.from('vista_resumen_nichos')
.select('*')
if (error) return res.status(500).json({ error: error.message })
res.json(data)
}

189
backend/api/analizar.js Normal file
View File

@ -0,0 +1,189 @@
// ============================================================
// ENDPOINT PRINCIPAL — POST /api/analizar
// Orquesta el pipeline completo:
// URL → Audio → Whisper → GPT-4o → Validar → Embedding → Supabase
// ============================================================
import { extraerAudio } from '../lib/extractor.js'
import { transcribir } from '../lib/transcriptor.js'
import { analizarTranscript } from '../lib/analizador.js'
import { validarAnalisis } from '../lib/validador.js'
import { generarEmbedding } from '../lib/embeddings.js'
import { supabase } from '../lib/supabase.js'
export default async function handler(req, res) {
// Solo aceptar POST
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Método no permitido' })
}
const inicio = Date.now()
// ── Validar body de entrada ───────────────────────────────
const {
url,
niche,
sub_niche,
mercado_objetivo,
idioma = 'es',
cliente_id = null,
proyecto_nombre = null,
competidor_referente = false,
// Métricas manuales (la API no las devuelve)
vistas = null,
likes = null,
compartidos = null,
fecha_publicacion = null,
} = req.body
if (!url) return res.status(400).json({ error: 'El campo "url" es requerido' })
if (!niche) return res.status(400).json({ error: 'El campo "niche" es requerido' })
const URL_SOPORTADAS = /^https?:\/\/(www\.)?(tiktok\.com|vm\.tiktok\.com|instagram\.com|youtube\.com|youtu\.be)/
if (!URL_SOPORTADAS.test(url)) {
return res.status(400).json({ error: 'URL no soportada. Solo se aceptan TikTok, Instagram Reels y YouTube Shorts.' })
}
let paso = 'inicio'
try {
// ── PASO 1: Extraer audio ─────────────────────────────
paso = 'extraccion'
const { audioUrl, duracion, titulo, thumbnail, plataforma } = await extraerAudio(url)
// ── PASO 2: Transcribir con Whisper ───────────────────
paso = 'transcripcion'
const transcript = await transcribir(audioUrl, idioma)
// ── PASO 3: Analizar con GPT-4o ───────────────────────
paso = 'analisis'
const analisisRaw = await analizarTranscript(transcript, niche, plataforma, duracion)
// ── PASO 4: Validar con Zod ───────────────────────────
paso = 'validacion'
const analisis = validarAnalisis(analisisRaw)
// ── PASO 5: Generar embedding vectorial ───────────────
paso = 'embedding'
const vector = await generarEmbedding(transcript, analisis)
// ── PASO 6: Guardar en Supabase ───────────────────────
paso = 'guardado'
const { data: guion, error: errorSupabase } = await supabase
.from('guiones')
.insert({
// Organización
cliente_id,
niche,
sub_niche,
mercado_objetivo,
idioma,
proyecto_nombre,
competidor_referente,
// Metadata del video
url_origen: url,
plataforma,
duracion_segundos: duracion,
vistas,
likes,
compartidos,
fecha_publicacion,
// Análisis de GPT-4o (campos de storytelling)
estructura_narrativa: analisis.estructura_narrativa,
gancho_tipo: analisis.gancho_tipo,
gancho_texto: analisis.gancho_texto,
gancho_duracion_seg: analisis.gancho_duracion_seg,
desarrollo_tipo: analisis.desarrollo_tipo,
cta_tipo: analisis.cta_tipo,
cta_texto: analisis.cta_texto,
arco_emocional: analisis.arco_emocional,
conflicto_central: analisis.conflicto_central,
resolucion: analisis.resolucion,
pacing_ritmo: analisis.pacing_ritmo,
numero_actos: analisis.numero_actos,
// Cialdini
cialdini_reciprocidad: analisis.cialdini_reciprocidad,
cialdini_escasez: analisis.cialdini_escasez,
cialdini_autoridad: analisis.cialdini_autoridad,
cialdini_consistencia: analisis.cialdini_consistencia,
cialdini_prueba_social: analisis.cialdini_prueba_social,
cialdini_simpatia: analisis.cialdini_simpatia,
cialdini_unidad: analisis.cialdini_unidad,
sesgo_cognitivo: analisis.sesgo_cognitivo,
trigger_emocional: analisis.trigger_emocional,
intensidad_emocional: analisis.intensidad_emocional,
// Neuropublicidad
atencion_visual: analisis.atencion_visual,
lenguaje_sensorial: analisis.lenguaje_sensorial,
contraste_narrativo: analisis.contraste_narrativo,
efecto_novedad: analisis.efecto_novedad,
dolor_placer: analisis.dolor_placer,
personalizacion: analisis.personalizacion,
carga_cognitiva: analisis.carga_cognitiva,
velocidad_locucion: analisis.velocidad_locucion,
uso_musica: analisis.uso_musica,
micro_compromisos: analisis.micro_compromisos,
// Contenido
tema_principal: analisis.tema_principal,
angulo_unico: analisis.angulo_unico,
palabras_clave: analisis.palabras_clave,
transcript,
tono: analisis.tono,
persona_narradora: analisis.persona_narradora,
promesa_explicita: analisis.promesa_explicita,
nivel_especificidad: analisis.nivel_especificidad,
// Métricas (score_engagement lo calcula el trigger de Supabase)
score_virabilidad: analisis.score_virabilidad,
resumen_patron: analisis.resumen_patron,
embedding_vector: `[${vector.join(',')}]`,
// Auditoría
procesado_ok: true,
version_prompt: 'v1.0',
})
.select('id, niche, score_virabilidad, resumen_patron')
.single()
if (errorSupabase) {
throw new Error(`Supabase error: ${errorSupabase.message}`)
}
const duracionTotal = ((Date.now() - inicio) / 1000).toFixed(1)
return res.status(200).json({
ok: true,
guion_id: guion.id,
niche: guion.niche,
score_virabilidad: guion.score_virabilidad,
resumen_patron: guion.resumen_patron,
tiempo_total_seg: parseFloat(duracionTotal),
})
} catch (err) {
console.error(`[analizar] Error en paso "${paso}":`, err.message)
// Guardar el error en Supabase para diagnóstico
if (paso !== 'inicio') {
await supabase.from('guiones').insert({
url_origen: url,
niche,
idioma,
cliente_id,
procesado_ok: false,
error_detalle: `[${paso}] ${err.message}`,
version_prompt: 'v1.0',
}).catch(() => {}) // silencioso si falla el insert de error
}
return res.status(500).json({
ok: false,
paso,
error: err.message,
})
}
}

106
backend/lib/analizador.js Normal file
View File

@ -0,0 +1,106 @@
// ============================================================
// ANALIZADOR — GPT-4o
// Prompt maestro multidisciplinario: Storytelling + Cialdini
// + Neuropublicidad → JSON de 55 campos analizables
// ============================================================
import OpenAI from 'openai'
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY })
const PROMPT_SISTEMA = `Eres un experto en ingeniería de guiones para video corto (TikTok, Reels, YouTube Shorts) con especialización en:
- Storytelling y estructura narrativa
- Psicología de la persuasión (Cialdini, sesgos cognitivos)
- Neuropublicidad y neuromarketing
- Marketing de contenidos para múltiples nichos
Tu tarea es analizar la transcripción de un video y devolver un JSON con el análisis completo.
SOLO devuelve el JSON, sin texto adicional, sin markdown, sin explicaciones.`
/**
* @param {string} transcript Texto transcrito por Whisper
* @param {string} niche Nicho del video (ej: "fitness", "finanzas")
* @param {string} plataforma tiktok | reels | shorts
* @param {number} duracion Duración en segundos
* @returns {object} JSON con todos los campos de análisis
*/
export async function analizarTranscript(transcript, niche, plataforma, duracion) {
const promptUsuario = `Analiza este video de ${plataforma} de ${duracion} segundos del nicho "${niche}".
TRANSCRIPCIÓN:
"""
${transcript}
"""
Devuelve EXACTAMENTE este JSON con los valores que correspondan:
{
"estructura_narrativa": "<AIDA|PAS|hero_journey|storybrand|antes_despues|otra>",
"gancho_tipo": "<pregunta|declaracion_shock|dato_estadistica|historia|controversia|promesa_directa>",
"gancho_texto": "<primeras 5-8 palabras del video>",
"gancho_duracion_seg": <número entero estimado>,
"desarrollo_tipo": "<problema_solucion|lista|demostracion|testimonio|tutorial|storytelling_puro>",
"cta_tipo": "<seguir|comentar|compartir|comprar|visitar_link|guardar|ninguno>",
"cta_texto": "<texto exacto del call to action, o null si no hay>",
"arco_emocional": "<emoción inicial> → <emoción media> → <emoción final>",
"conflicto_central": "<el problema o tensión principal que articula el video>",
"resolucion": "<cómo se resuelve o qué promete resolver>",
"pacing_ritmo": "<lento|medio|rapido|variable>",
"numero_actos": <1, 2 o 3>,
"cialdini_reciprocidad": <true|false>,
"cialdini_escasez": <true|false>,
"cialdini_autoridad": <true|false>,
"cialdini_consistencia": <true|false>,
"cialdini_prueba_social": <true|false>,
"cialdini_simpatia": <true|false>,
"cialdini_unidad": <true|false>,
"sesgo_cognitivo": "<nombre del sesgo cognitivo principal, o null>",
"trigger_emocional": "<miedo|esperanza|curiosidad|ira|orgullo|tristeza|sorpresa|humor>",
"intensidad_emocional": <número entero del 1 al 10>,
"atencion_visual": "<zoom_agresivo|corte_rapido|texto_pantalla|cara_camara|broll_dinamico|ninguno>",
"lenguaje_sensorial": <true|false>,
"contraste_narrativo": <true|false>,
"efecto_novedad": <true|false>,
"dolor_placer": "<apela_dolor|apela_placer|ambos>",
"personalizacion": <true|false>,
"carga_cognitiva": "<baja|media|alta>",
"velocidad_locucion": "<lenta|normal|rapida|muy_rapida>",
"uso_musica": <true|false>,
"micro_compromisos": <true|false>,
"tema_principal": "<tema en 1-3 palabras>",
"angulo_unico": "<qué diferencia a este video de otros del mismo tema, en 1 oración>",
"palabras_clave": ["<keyword1>", "<keyword2>", "<keyword3>", "<keyword4>", "<keyword5>"],
"tono": "<educativo|entretenimiento|inspiracional|controversial|informativo|humoristico>",
"persona_narradora": "<primera_persona|segunda_persona|tercera_persona|mixta>",
"promesa_explicita": "<la promesa que hace el video al espectador, en 1 oración>",
"nivel_especificidad": "<generico|especifico|ultra_especifico>",
"score_virabilidad": <número entero del 1 al 100>,
"resumen_patron": "<párrafo de 2-3 oraciones describiendo el patrón ganador de este video>"
}`
const completion = await openai.chat.completions.create({
model: 'gpt-4o',
temperature: 0.2, // baja temperatura para análisis consistente
messages: [
{ role: 'system', content: PROMPT_SISTEMA },
{ role: 'user', content: promptUsuario },
],
})
const contenido = completion.choices[0]?.message?.content?.trim()
if (!contenido) {
throw new Error('GPT-4o devolvió una respuesta vacía')
}
// Limpiar posible markdown que GPT-4o a veces añade
const jsonLimpio = contenido
.replace(/^```json\n?/, '')
.replace(/^```\n?/, '')
.replace(/\n?```$/, '')
.trim()
return JSON.parse(jsonLimpio)
}

40
backend/lib/embeddings.js Normal file
View File

@ -0,0 +1,40 @@
// ============================================================
// EMBEDDINGS — text-embedding-3-small
// Genera el vector semántico para búsqueda con pgvector
// ============================================================
import OpenAI from 'openai'
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY })
/**
* Genera el vector de embedding para un guion.
* Vectorizamos un texto compuesto para capturar tanto el
* contenido como los patrones estructurales del video.
*
* @param {string} transcript Transcripción completa
* @param {object} analisis JSON validado de GPT-4o
* @returns {number[]} Vector de 1536 dimensiones
*/
export async function generarEmbedding(transcript, analisis) {
// Construir el texto a vectorizar combinando campos clave
// Esto hace que la búsqueda semántica encuentre videos similares
// en CONTENIDO y en ESTRUCTURA (no solo en palabras)
const textoParaVectorizar = [
`Tema: ${analisis.tema_principal}`,
`Ángulo: ${analisis.angulo_unico}`,
`Estructura: ${analisis.estructura_narrativa}`,
`Gancho: ${analisis.gancho_texto}`,
`Conflicto: ${analisis.conflicto_central}`,
`Trigger emocional: ${analisis.trigger_emocional}`,
`Promesa: ${analisis.promesa_explicita}`,
`Patrón: ${analisis.resumen_patron}`,
`Transcript: ${transcript.slice(0, 1000)}`, // primeros 1000 chars
].join('\n')
const response = await openai.embeddings.create({
model: 'text-embedding-3-small',
input: textoParaVectorizar,
})
return response.data[0].embedding
}

54
backend/lib/extractor.js Normal file
View File

@ -0,0 +1,54 @@
// ============================================================
// EXTRACTOR — Social Download All In One (RapidAPI)
// Devuelve la URL del audio MP3 y metadata del video
// ============================================================
const RAPIDAPI_HOST = 'social-download-all-in-one.p.rapidapi.com'
const RAPIDAPI_URL = `https://${RAPIDAPI_HOST}/v1/social/autolink`
/**
* @param {string} url URL del video (TikTok, Reels, YouTube Shorts)
* @returns {{ audioUrl: string, duracion: number, titulo: string, thumbnail: string, plataforma: string }}
*/
export async function extraerAudio(url) {
const response = await fetch(RAPIDAPI_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-rapidapi-host': RAPIDAPI_HOST,
'x-rapidapi-key': process.env.RAPIDAPI_KEY,
},
body: JSON.stringify({ url }),
})
if (!response.ok) {
throw new Error(`Social Download API error: ${response.status} ${response.statusText}`)
}
const data = await response.json()
if (data.error) {
throw new Error(`Social Download API devolvió error para la URL: ${url}`)
}
// Buscar el media de tipo audio
const audioMedia = data.medias?.find(m => m.type === 'audio')
if (!audioMedia?.url) {
throw new Error('La API no devolvió un archivo de audio para esta URL')
}
return {
audioUrl: audioMedia.url,
duracion: data.duration ?? null,
titulo: data.title ?? null,
thumbnail: data.thumbnail ?? null,
plataforma: detectarPlataforma(url),
}
}
function detectarPlataforma(url) {
if (url.includes('tiktok.com')) return 'tiktok'
if (url.includes('instagram.com')) return 'reels'
if (url.includes('youtube.com') || url.includes('youtu.be')) return 'shorts'
return 'tiktok' // fallback
}

7
backend/lib/supabase.js Normal file
View File

@ -0,0 +1,7 @@
import { createClient } from '@supabase/supabase-js'
// Service Role key: bypasea RLS, solo usar en backend
export const supabase = createClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_SERVICE_ROLE_KEY
)

View File

@ -0,0 +1,36 @@
// ============================================================
// TRANSCRIPTOR — OpenAI Whisper
// Descarga el audio desde la URL y lo transcribe
// ============================================================
import OpenAI from 'openai'
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY })
/**
* @param {string} audioUrl URL directa del MP3 (de Social Download API)
* @param {string} idioma Código de idioma: 'es', 'en', 'pt', etc.
* @returns {string} Transcripción completa del audio
*/
export async function transcribir(audioUrl, idioma = 'es') {
// Descargar el audio desde la URL del CDN
const audioResponse = await fetch(audioUrl)
if (!audioResponse.ok) {
throw new Error(`Error al descargar audio: ${audioResponse.status}`)
}
const audioBuffer = await audioResponse.arrayBuffer()
const audioFile = new File([audioBuffer], 'audio.mp3', { type: 'audio/mpeg' })
const transcripcion = await openai.audio.transcriptions.create({
file: audioFile,
model: 'whisper-1',
language: idioma === 'otro' ? undefined : idioma, // auto-detect si es 'otro'
response_format: 'text',
})
if (!transcripcion || transcripcion.trim().length < 10) {
throw new Error('Whisper no pudo transcribir el audio (resultado vacío o muy corto)')
}
return transcripcion.trim()
}

91
backend/lib/validador.js Normal file
View File

@ -0,0 +1,91 @@
// ============================================================
// VALIDADOR — Zod Schema
// Valida el JSON de GPT-4o antes de guardar en Supabase
// Si GPT-4o alucina un valor fuera del enum, lo atrapa aquí
// ============================================================
import { z } from 'zod'
const EstructuraEnum = z.enum(['AIDA','PAS','hero_journey','storybrand','antes_despues','otra'])
const GanchoTipoEnum = z.enum(['pregunta','declaracion_shock','dato_estadistica','historia','controversia','promesa_directa'])
const DesarrolloEnum = z.enum(['problema_solucion','lista','demostracion','testimonio','tutorial','storytelling_puro'])
const CtaTipoEnum = z.enum(['seguir','comentar','compartir','comprar','visitar_link','guardar','ninguno'])
const PacingEnum = z.enum(['lento','medio','rapido','variable'])
const TriggerEnum = z.enum(['miedo','esperanza','curiosidad','ira','orgullo','tristeza','sorpresa','humor'])
const AtencionVisualEnum= z.enum(['zoom_agresivo','corte_rapido','texto_pantalla','cara_camara','broll_dinamico','ninguno'])
const DolorPlacerEnum = z.enum(['apela_dolor','apela_placer','ambos'])
const CargaEnum = z.enum(['baja','media','alta'])
const VelocidadEnum = z.enum(['lenta','normal','rapida','muy_rapida'])
const TonoEnum = z.enum(['educativo','entretenimiento','inspiracional','controversial','informativo','humoristico'])
const PersonaEnum = z.enum(['primera_persona','segunda_persona','tercera_persona','mixta'])
const EspecificidadEnum = z.enum(['generico','especifico','ultra_especifico'])
export const AnalisisSchema = z.object({
// Storytelling
estructura_narrativa: EstructuraEnum,
gancho_tipo: GanchoTipoEnum,
gancho_texto: z.string().min(1).max(200),
gancho_duracion_seg: z.number().int().min(0).max(30),
desarrollo_tipo: DesarrolloEnum,
cta_tipo: CtaTipoEnum,
cta_texto: z.string().max(300).nullable(),
arco_emocional: z.string().min(1).max(200),
conflicto_central: z.string().min(1).max(500),
resolucion: z.string().min(1).max(500),
pacing_ritmo: PacingEnum,
numero_actos: z.number().int().min(1).max(4),
// Cialdini
cialdini_reciprocidad: z.boolean(),
cialdini_escasez: z.boolean(),
cialdini_autoridad: z.boolean(),
cialdini_consistencia: z.boolean(),
cialdini_prueba_social: z.boolean(),
cialdini_simpatia: z.boolean(),
cialdini_unidad: z.boolean(),
sesgo_cognitivo: z.string().max(100).nullable(),
trigger_emocional: TriggerEnum,
intensidad_emocional: z.number().int().min(1).max(10),
// Neuropublicidad
atencion_visual: AtencionVisualEnum,
lenguaje_sensorial: z.boolean(),
contraste_narrativo: z.boolean(),
efecto_novedad: z.boolean(),
dolor_placer: DolorPlacerEnum,
personalizacion: z.boolean(),
carga_cognitiva: CargaEnum,
velocidad_locucion: VelocidadEnum,
uso_musica: z.boolean(),
micro_compromisos: z.boolean(),
// Contenido
tema_principal: z.string().min(1).max(100),
angulo_unico: z.string().min(1).max(500),
palabras_clave: z.array(z.string()).min(1).max(10),
tono: TonoEnum,
persona_narradora: PersonaEnum,
promesa_explicita: z.string().min(1).max(500),
nivel_especificidad: EspecificidadEnum,
// Métricas calculadas por GPT-4o
score_virabilidad: z.number().int().min(1).max(100),
resumen_patron: z.string().min(10).max(1000),
})
/**
* Valida el JSON de GPT-4o y lanza error descriptivo si falla
* @param {object} data JSON crudo de GPT-4o
* @returns {object} Datos validados y tipados
*/
export function validarAnalisis(data) {
const resultado = AnalisisSchema.safeParse(data)
if (!resultado.success) {
const errores = resultado.error.errors
.map(e => `${e.path.join('.')}: ${e.message} (recibido: ${JSON.stringify(e.received ?? 'undefined')})`)
.join('\n')
throw new Error(`Validación GPT-4o falló:\n${errores}`)
}
return resultado.data
}

1289
backend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

20
backend/package.json Normal file
View File

@ -0,0 +1,20 @@
{
"name": "guiones-backend",
"version": "1.0.0",
"private": true,
"type": "module",
"engines": {
"node": ">=18"
},
"scripts": {
"dev": "node --watch server.js"
},
"dependencies": {
"@supabase/supabase-js": "^2.39.0",
"cors": "^2.8.5",
"dotenv": "^16.4.1",
"express": "^4.18.2",
"openai": "^4.28.0",
"zod": "^3.22.4"
}
}

190
backend/server.js Normal file
View File

@ -0,0 +1,190 @@
import 'dotenv/config'
import express from 'express'
import cors from 'cors'
import { extraerAudio } from './lib/extractor.js'
import { transcribir } from './lib/transcriptor.js'
import { analizarTranscript } from './lib/analizador.js'
import { validarAnalisis } from './lib/validador.js'
import { generarEmbedding } from './lib/embeddings.js'
import { supabase } from './lib/supabase.js'
// ── Validar variables de entorno requeridas ──────────────────
const REQUIRED_ENV = ['OPENAI_API_KEY', 'SUPABASE_URL', 'SUPABASE_SERVICE_ROLE_KEY', 'RAPIDAPI_KEY']
const missingVars = REQUIRED_ENV.filter(k => !process.env[k])
if (missingVars.length > 0) {
console.error(`\n❌ Variables de entorno faltantes: ${missingVars.join(', ')}\n Configura el archivo backend/.env antes de iniciar.\n`)
process.exit(1)
}
const app = express()
const PORT = process.env.PORT || 3001
app.use(cors({ origin: process.env.ALLOWED_ORIGIN || 'http://localhost:5173' }))
app.use(express.json())
// ── GET /api/guiones ────────────────────────────────────────
// Lista todos los guiones con paginación y filtros
app.get('/api/guiones', async (req, res) => {
const { niche, cliente_id, plataforma, page = 1, limit = 20 } = req.query
const offset = (page - 1) * limit
let query = supabase
.from('guiones')
.select(`
id, niche, sub_niche, plataforma, url_origen,
gancho_texto, estructura_narrativa, trigger_emocional,
tono, score_engagement, score_virabilidad, score_cialdini,
fecha_analisis, procesado_ok, vistas, likes, compartidos,
tema_principal, resumen_patron
`, { count: 'exact' })
.eq('procesado_ok', true)
.order('fecha_analisis', { ascending: false })
.range(offset, offset + limit - 1)
if (niche) query = query.eq('niche', niche)
if (cliente_id) query = query.eq('cliente_id', cliente_id)
if (plataforma) query = query.eq('plataforma', plataforma)
const { data, error, count } = await query
if (error) return res.status(500).json({ error: error.message })
res.json({ guiones: data, total: count, page: Number(page), limit: Number(limit) })
})
// ── GET /api/guiones/:id ────────────────────────────────────
// Detalle completo de un guion
app.get('/api/guiones/:id', async (req, res) => {
const { data, error } = await supabase
.from('guiones')
.select('*')
.eq('id', req.params.id)
.single()
if (error) return res.status(404).json({ error: 'Guion no encontrado' })
res.json(data)
})
// ── GET /api/nichos ─────────────────────────────────────────
// Lista de nichos distintos para el selector del formulario
app.get('/api/nichos', async (req, res) => {
const { data, error } = await supabase
.from('guiones')
.select('niche')
.eq('procesado_ok', true)
if (error) return res.status(500).json({ error: error.message })
const nichos = [...new Set(data.map(r => r.niche))].sort()
res.json(nichos)
})
// ── GET /api/clientes ───────────────────────────────────────
app.get('/api/clientes', async (req, res) => {
const { data, error } = await supabase
.from('clientes')
.select('id, nombre, industria')
.eq('activo', true)
.order('nombre')
if (error) return res.status(500).json({ error: error.message })
res.json(data)
})
// ── GET /api/stats ──────────────────────────────────────────
// Estadísticas para el dashboard
app.get('/api/stats', async (req, res) => {
const { data, error } = await supabase
.from('vista_resumen_nichos')
.select('*')
if (error) return res.status(500).json({ error: error.message })
res.json(data)
})
// ── POST /api/analizar ──────────────────────────────────────
// Pipeline completo: URL → Audio → Whisper → GPT-4o → Supabase
app.post('/api/analizar', async (req, res) => {
const {
url, niche, sub_niche, mercado_objetivo,
idioma = 'es', cliente_id = null, proyecto_nombre = null,
competidor_referente = false,
vistas = null, likes = null, compartidos = null,
fecha_publicacion = null,
} = req.body
if (!url) return res.status(400).json({ error: 'El campo "url" es requerido' })
if (!niche) return res.status(400).json({ error: 'El campo "niche" es requerido' })
const URL_SOPORTADAS = /^https?:\/\/(www\.)?(tiktok\.com|vm\.tiktok\.com|instagram\.com|youtube\.com|youtu\.be)/
if (!URL_SOPORTADAS.test(url)) {
return res.status(400).json({ error: 'URL no soportada. Solo se aceptan TikTok, Instagram Reels y YouTube Shorts.' })
}
const inicio = Date.now()
let paso = 'inicio'
try {
paso = 'extraccion'
console.log(`[1/5] Extrayendo audio de: ${url}`)
const { audioUrl, duracion, plataforma } = await extraerAudio(url)
paso = 'transcripcion'
console.log(`[2/5] Transcribiendo audio (${duracion}s)...`)
const transcript = await transcribir(audioUrl, idioma)
paso = 'analisis'
console.log(`[3/5] Analizando con GPT-4o...`)
const analisisRaw = await analizarTranscript(transcript, niche, plataforma, duracion)
paso = 'validacion'
console.log(`[4/5] Validando schema...`)
const analisis = validarAnalisis(analisisRaw)
paso = 'embedding'
console.log(`[5/5] Generando embedding y guardando...`)
const vector = await generarEmbedding(transcript, analisis)
const { data: guion, error: errorSupabase } = await supabase
.from('guiones')
.insert({
cliente_id, niche, sub_niche, mercado_objetivo, idioma,
proyecto_nombre, competidor_referente,
url_origen: url, plataforma, duracion_segundos: duracion,
vistas, likes, compartidos, fecha_publicacion,
...analisis,
transcript,
embedding_vector: `[${vector.join(',')}]`,
procesado_ok: true,
version_prompt: 'v1.0',
})
.select('id, niche, score_virabilidad, resumen_patron')
.single()
if (errorSupabase) throw new Error(`Supabase: ${errorSupabase.message}`)
const segundos = ((Date.now() - inicio) / 1000).toFixed(1)
console.log(`✓ Completado en ${segundos}s — ID: ${guion.id}`)
res.json({
ok: true,
guion_id: guion.id,
niche: guion.niche,
score_virabilidad: guion.score_virabilidad,
resumen_patron: guion.resumen_patron,
tiempo_total_seg: parseFloat(segundos),
})
} catch (err) {
console.error(`✗ Error en paso "${paso}":`, err.message)
await supabase.from('guiones').insert({
url_origen: url, niche, idioma, cliente_id,
procesado_ok: false,
error_detalle: `[${paso}] ${err.message}`,
version_prompt: 'v1.0',
}).catch(() => {})
res.status(500).json({ ok: false, paso, error: err.message })
}
})
app.listen(PORT, () => console.log(`Backend local corriendo en http://localhost:${PORT}`))

7
backend/vercel.json Normal file
View File

@ -0,0 +1,7 @@
{
"functions": {
"api/*.js": {
"maxDuration": 60
}
}
}

View File

@ -0,0 +1,189 @@
-- ============================================================
-- FASE 1 — SCHEMA PRINCIPAL
-- Sistema de Ingeniería de Guiones V4.0
-- Ejecutar en Supabase SQL Editor en este orden exacto
-- ============================================================
-- 1. EXTENSIÓN VECTORIAL
create extension if not exists vector;
-- ============================================================
-- 2. ENUMS (tipos controlados)
-- ============================================================
create type plataforma_enum as enum ('tiktok', 'reels', 'shorts');
create type estructura_narrativa_enum as enum (
'AIDA', 'PAS', 'hero_journey', 'storybrand', 'antes_despues', 'otra'
);
create type gancho_tipo_enum as enum (
'pregunta', 'declaracion_shock', 'dato_estadistica', 'historia', 'controversia', 'promesa_directa'
);
create type desarrollo_tipo_enum as enum (
'problema_solucion', 'lista', 'demostracion', 'testimonio', 'tutorial', 'storytelling_puro'
);
create type cta_tipo_enum as enum (
'seguir', 'comentar', 'compartir', 'comprar', 'visitar_link', 'guardar', 'ninguno'
);
create type pacing_enum as enum ('lento', 'medio', 'rapido', 'variable');
create type trigger_emocional_enum as enum (
'miedo', 'esperanza', 'curiosidad', 'ira', 'orgullo', 'tristeza', 'sorpresa', 'humor'
);
create type atencion_visual_enum as enum (
'zoom_agresivo', 'corte_rapido', 'texto_pantalla', 'cara_camara', 'broll_dinamico', 'ninguno'
);
create type dolor_placer_enum as enum ('apela_dolor', 'apela_placer', 'ambos');
create type carga_cognitiva_enum as enum ('baja', 'media', 'alta');
create type velocidad_locucion_enum as enum ('lenta', 'normal', 'rapida', 'muy_rapida');
create type tono_enum as enum (
'educativo', 'entretenimiento', 'inspiracional', 'controversial', 'informativo', 'humoristico'
);
create type persona_narradora_enum as enum (
'primera_persona', 'segunda_persona', 'tercera_persona', 'mixta'
);
create type nivel_especificidad_enum as enum ('generico', 'especifico', 'ultra_especifico');
create type idioma_enum as enum ('es', 'en', 'pt', 'fr', 'otro');
-- ============================================================
-- 3. TABLA CLIENTES
-- ============================================================
create table clientes (
id uuid primary key default gen_random_uuid(),
nombre text not null,
industria text not null,
sub_industrias text[], -- nichos específicos del cliente
mercados text[], -- países/regiones que atiende
activo boolean default true,
notas text,
fecha_alta timestamp with time zone default now()
);
-- ============================================================
-- 4. TABLA PRINCIPAL: GUIONES
-- ============================================================
create table guiones (
-- ── BLOQUE 0: Identificadores ──────────────────────────────
id uuid primary key default gen_random_uuid(),
cliente_id uuid references clientes(id) on delete set null,
-- ── BLOQUE 1: Organización de Agencia ─────────────────────
niche text not null,
sub_niche text,
mercado_objetivo text,
idioma idioma_enum default 'es',
proyecto_nombre text,
competidor_referente boolean default false, -- video analizado de la competencia
-- ── BLOQUE 2: Metadata del Video ──────────────────────────
url_origen text,
plataforma plataforma_enum,
duracion_segundos integer,
vistas bigint,
likes bigint,
compartidos bigint,
fecha_publicacion date,
-- ── BLOQUE 3: Storytelling ─────────────────────────────────
estructura_narrativa estructura_narrativa_enum,
gancho_tipo gancho_tipo_enum,
gancho_texto text, -- primeras 3-7 palabras del video
gancho_duracion_seg integer,
desarrollo_tipo desarrollo_tipo_enum,
cta_tipo cta_tipo_enum,
cta_texto text,
arco_emocional text, -- ej: "curiosidad → sorpresa → alivio"
conflicto_central text,
resolucion text,
pacing_ritmo pacing_enum,
numero_actos integer check (numero_actos between 1 and 4),
-- ── BLOQUE 4: Psicología / Cialdini ───────────────────────
cialdini_reciprocidad boolean default false,
cialdini_escasez boolean default false,
cialdini_autoridad boolean default false,
cialdini_consistencia boolean default false,
cialdini_prueba_social boolean default false,
cialdini_simpatia boolean default false,
cialdini_unidad boolean default false,
sesgo_cognitivo text, -- ej: "FOMO", "Efecto halo", "Anclaje"
trigger_emocional trigger_emocional_enum,
intensidad_emocional integer check (intensidad_emocional between 1 and 10),
-- ── BLOQUE 5: Neuropublicidad ──────────────────────────────
atencion_visual atencion_visual_enum,
lenguaje_sensorial boolean default false,
contraste_narrativo boolean default false, -- antes/después, ellos/nosotros
efecto_novedad boolean default false, -- algo inesperado en primeros 3s
dolor_placer dolor_placer_enum,
personalizacion boolean default false, -- habla directamente al "tú"
carga_cognitiva carga_cognitiva_enum,
velocidad_locucion velocidad_locucion_enum,
uso_musica boolean default false,
micro_compromisos boolean default false, -- pequeño compromiso antes del CTA
-- ── BLOQUE 6: Análisis de Contenido ───────────────────────
tema_principal text,
angulo_unico text,
palabras_clave text[],
transcript text,
tono tono_enum,
persona_narradora persona_narradora_enum,
promesa_explicita text,
nivel_especificidad nivel_especificidad_enum,
-- ── BLOQUE 7: Métricas Calculadas ─────────────────────────
score_engagement numeric(6,4), -- (likes + compartidos*3) / vistas * 100
score_virabilidad integer check (score_virabilidad between 1 and 100),
score_cialdini integer generated always as (
(cialdini_reciprocidad::int + cialdini_escasez::int +
cialdini_autoridad::int + cialdini_consistencia::int +
cialdini_prueba_social::int + cialdini_simpatia::int +
cialdini_unidad::int)
) stored,
resumen_patron text, -- párrafo generado por GPT-4o
embedding_vector vector(1536), -- text-embedding-3-small
-- ── BLOQUE 8: Auditoría del Sistema ───────────────────────
fecha_analisis timestamp with time zone default now(),
version_prompt text default 'v1.0', -- versión del prompt que generó el análisis
procesado_ok boolean default false, -- false si hubo error en el pipeline
error_detalle text -- log del error si procesado_ok = false
);
-- ============================================================
-- 5. ÍNDICES DE RENDIMIENTO
-- ============================================================
-- Búsqueda rápida por niche (el más frecuente en queries de agencia)
create index idx_guiones_niche on guiones(niche);
-- Búsqueda por cliente
create index idx_guiones_cliente on guiones(cliente_id);
-- Filtro por plataforma y engagement para rankings
create index idx_guiones_engagement on guiones(score_engagement desc nulls last);
-- Índice HNSW para búsqueda vectorial semántica (el más rápido para pgvector)
create index idx_guiones_vector on guiones
using hnsw (embedding_vector vector_cosine_ops)
with (m = 16, ef_construction = 64);
-- Índice compuesto niche + engagement para resumen_patrones()
create index idx_guiones_niche_engagement on guiones(niche, score_engagement desc nulls last);

View File

@ -0,0 +1,238 @@
-- ============================================================
-- FASE 1 — FUNCIONES DE INTELIGENCIA
-- Sistema de Ingeniería de Guiones V4.0
-- Ejecutar DESPUÉS de 01_schema.sql
-- ============================================================
-- ============================================================
-- FUNCIÓN 1: buscar_guiones_similares()
-- Encuentra los N guiones más parecidos semánticamente
-- filtrando por niche para que los referentes sean relevantes
-- ============================================================
create or replace function buscar_guiones_similares(
p_vector vector(1536), -- embedding del texto a comparar
p_niche text, -- niche para filtrar (obligatorio)
p_limite integer default 5,
p_cliente_id uuid default null, -- opcional: filtrar por cliente
p_solo_exitosos boolean default true -- si true, solo score_engagement > 0
)
returns table (
id uuid,
niche text,
sub_niche text,
plataforma plataforma_enum,
gancho_texto text,
estructura_narrativa estructura_narrativa_enum,
trigger_emocional trigger_emocional_enum,
score_engagement numeric(6,4),
score_virabilidad integer,
score_cialdini integer,
resumen_patron text,
similitud float
)
language sql stable
as $$
select
g.id,
g.niche,
g.sub_niche,
g.plataforma,
g.gancho_texto,
g.estructura_narrativa,
g.trigger_emocional,
g.score_engagement,
g.score_virabilidad,
g.score_cialdini,
g.resumen_patron,
1 - (g.embedding_vector <=> p_vector) as similitud
from guiones g
where
g.procesado_ok = true
and g.embedding_vector is not null
and g.niche = p_niche
and (p_cliente_id is null or g.cliente_id = p_cliente_id)
and (not p_solo_exitosos or (g.score_engagement is not null and g.score_engagement > 0))
order by
g.embedding_vector <=> p_vector -- menor distancia coseno = más similar
limit p_limite;
$$;
-- ============================================================
-- FUNCIÓN 2: resumen_patrones()
-- Agrega los patrones ganadores de un niche para el generador
-- Retorna un JSON estructurado que se enviará a GPT-4o
-- como contexto al generar guiones nuevos
-- ============================================================
create or replace function resumen_patrones(
p_niche text,
p_cliente_id uuid default null,
p_top_n integer default 20, -- cuántos guiones top analizar
p_min_engagement numeric default 0.1 -- filtro mínimo de engagement
)
returns json
language plpgsql stable
as $$
declare
v_resultado json;
begin
with top_guiones as (
-- Seleccionar los mejores guiones del niche
select *
from guiones
where
niche = p_niche
and procesado_ok = true
and score_engagement >= p_min_engagement
and (p_cliente_id is null or cliente_id = p_cliente_id)
order by score_engagement desc
limit p_top_n
),
conteos as (
select
-- Storytelling patterns
mode() within group (order by estructura_narrativa) as estructura_dominante,
mode() within group (order by gancho_tipo) as gancho_dominante,
mode() within group (order by cta_tipo) as cta_dominante,
mode() within group (order by pacing_ritmo) as pacing_dominante,
-- Psicología patterns
mode() within group (order by trigger_emocional) as trigger_dominante,
mode() within group (order by sesgo_cognitivo) as sesgo_dominante,
avg(intensidad_emocional) as intensidad_promedio,
-- Cialdini frecuencias
round(avg(cialdini_reciprocidad::int) * 100) as pct_reciprocidad,
round(avg(cialdini_escasez::int) * 100) as pct_escasez,
round(avg(cialdini_autoridad::int) * 100) as pct_autoridad,
round(avg(cialdini_consistencia::int) * 100) as pct_consistencia,
round(avg(cialdini_prueba_social::int) * 100) as pct_prueba_social,
round(avg(cialdini_simpatia::int) * 100) as pct_simpatia,
round(avg(cialdini_unidad::int) * 100) as pct_unidad,
-- Neuropublicidad patterns
mode() within group (order by atencion_visual) as atencion_dominante,
mode() within group (order by dolor_placer) as dolor_placer_dominante,
round(avg(lenguaje_sensorial::int) * 100) as pct_lenguaje_sensorial,
round(avg(contraste_narrativo::int) * 100) as pct_contraste,
round(avg(efecto_novedad::int) * 100) as pct_novedad,
-- Contenido patterns
mode() within group (order by tono) as tono_dominante,
mode() within group (order by persona_narradora) as persona_dominante,
mode() within group (order by nivel_especificidad) as especificidad_dominante,
-- Métricas generales
avg(score_engagement) as engagement_promedio,
avg(score_virabilidad) as virabilidad_promedio,
avg(score_cialdini) as cialdini_promedio,
avg(duracion_segundos) as duracion_promedio,
count(*) as total_analizados
from top_guiones
)
select json_build_object(
'niche', p_niche,
'total_analizados', c.total_analizados,
'fecha_generado', now(),
'metricas_promedio', json_build_object(
'engagement', round(c.engagement_promedio::numeric, 4),
'virabilidad', round(c.virabilidad_promedio::numeric, 1),
'cialdini_score', round(c.cialdini_promedio::numeric, 1),
'duracion_seg', round(c.duracion_promedio::numeric, 0)
),
'storytelling', json_build_object(
'estructura_dominante', c.estructura_dominante,
'gancho_dominante', c.gancho_dominante,
'cta_dominante', c.cta_dominante,
'pacing_dominante', c.pacing_dominante
),
'psicologia', json_build_object(
'trigger_dominante', c.trigger_dominante,
'sesgo_dominante', c.sesgo_dominante,
'intensidad_promedio', round(c.intensidad_promedio::numeric, 1),
'cialdini_frecuencias', json_build_object(
'reciprocidad', c.pct_reciprocidad,
'escasez', c.pct_escasez,
'autoridad', c.pct_autoridad,
'consistencia', c.pct_consistencia,
'prueba_social', c.pct_prueba_social,
'simpatia', c.pct_simpatia,
'unidad', c.pct_unidad
)
),
'neuropublicidad', json_build_object(
'atencion_dominante', c.atencion_dominante,
'dolor_placer_dominante', c.dolor_placer_dominante,
'pct_lenguaje_sensorial', c.pct_lenguaje_sensorial,
'pct_contraste_narrativo', c.pct_contraste,
'pct_efecto_novedad', c.pct_novedad
),
'contenido', json_build_object(
'tono_dominante', c.tono_dominante,
'persona_dominante', c.persona_dominante,
'especificidad_dominante', c.especificidad_dominante
)
) into v_resultado
from conteos c;
return v_resultado;
end;
$$;
-- ============================================================
-- FUNCIÓN 3: calcular_score_engagement()
-- Trigger para calcular automáticamente score_engagement
-- cuando se inserta o actualiza vistas/likes/compartidos
-- ============================================================
create or replace function calcular_score_engagement()
returns trigger
language plpgsql
as $$
begin
if new.vistas is not null and new.vistas > 0 then
new.score_engagement := round(
((coalesce(new.likes, 0) + coalesce(new.compartidos, 0) * 3)::numeric
/ new.vistas::numeric * 100)::numeric,
4
);
else
new.score_engagement := null;
end if;
return new;
end;
$$;
create trigger trg_calcular_engagement
before insert or update of vistas, likes, compartidos
on guiones
for each row
execute function calcular_score_engagement();
-- ============================================================
-- FUNCIÓN 4: guiones_por_niche()
-- Vista de resumen rápido para el dashboard
-- ============================================================
create or replace view vista_resumen_nichos as
select
g.niche,
g.cliente_id,
c.nombre as cliente_nombre,
count(*) as total_guiones,
round(avg(g.score_engagement)::numeric, 4) as engagement_promedio,
round(avg(g.score_virabilidad)::numeric, 1) as virabilidad_promedio,
round(avg(g.score_cialdini)::numeric, 1) as cialdini_promedio,
max(g.score_engagement) as mejor_engagement,
max(g.fecha_analisis) as ultimo_analisis
from guiones g
left join clientes c on c.id = g.cliente_id
where g.procesado_ok = true
group by g.niche, g.cliente_id, c.nombre
order by engagement_promedio desc nulls last;

View File

@ -0,0 +1,80 @@
-- ============================================================
-- FASE 1 — ROW LEVEL SECURITY (RLS)
-- Sistema de Ingeniería de Guiones V4.0
-- Ejecutar DESPUÉS de 01_schema.sql y 02_funciones.sql
-- ============================================================
-- IMPORTANTE: En Supabase, las políticas RLS controlan qué
-- filas puede ver/editar cada usuario autenticado.
-- El usuario autenticado se obtiene con auth.uid().
-- ============================================================
-- ── Habilitar RLS en ambas tablas ─────────────────────────
alter table clientes enable row level security;
alter table guiones enable row level security;
-- ============================================================
-- TABLA: clientes
-- ============================================================
-- Los usuarios autenticados pueden ver todos los clientes.
-- Si en el futuro cada usuario es de una agencia específica,
-- se puede agregar un campo agencia_id y filtrar aquí.
create policy "clientes_select"
on clientes for select
to authenticated
using (true);
create policy "clientes_insert"
on clientes for insert
to authenticated
with check (true);
create policy "clientes_update"
on clientes for update
to authenticated
using (true);
-- Solo usuarios con rol 'admin' pueden eliminar clientes
create policy "clientes_delete"
on clientes for delete
to authenticated
using (auth.jwt() ->> 'role' = 'admin');
-- ============================================================
-- TABLA: guiones
-- ============================================================
-- Cualquier usuario autenticado puede leer guiones
create policy "guiones_select"
on guiones for select
to authenticated
using (true);
-- Cualquier usuario autenticado puede insertar guiones
create policy "guiones_insert"
on guiones for insert
to authenticated
with check (true);
-- Actualización permitida para todos los autenticados
create policy "guiones_update"
on guiones for update
to authenticated
using (true);
-- Solo admin puede eliminar guiones
create policy "guiones_delete"
on guiones for delete
to authenticated
using (auth.jwt() ->> 'role' = 'admin');
-- ============================================================
-- ACCESO DE SERVICIO (Service Role)
-- El backend de Vercel usa la SERVICE_ROLE key de Supabase
-- que bypasea RLS automáticamente — no necesita políticas.
-- NUNCA exponer la SERVICE_ROLE key en el frontend.
-- ============================================================

View File

@ -0,0 +1,74 @@
-- ============================================================
-- FASE 1 — DATOS DE PRUEBA
-- Sistema de Ingeniería de Guiones V4.0
-- Ejecutar ÚLTIMO para validar que el schema funciona
-- ============================================================
-- 1. Insertar clientes de ejemplo
insert into clientes (nombre, industria, sub_industrias, mercados) values
('Agencia Demo', 'Marketing Digital', array['redes sociales', 'contenido'], array['MX', 'CO', 'AR']),
('Cliente Fitness', 'Salud y Bienestar', array['fitness', 'nutricion'], array['MX', 'US']),
('Cliente Finanzas', 'Finanzas Personales', array['inversion', 'ahorro', 'cripto'], array['MX', 'CO']);
-- 2. Insertar guion de ejemplo para validar el schema completo
-- (el embedding_vector se llenará cuando el backend procese videos reales)
insert into guiones (
niche, sub_niche, mercado_objetivo, idioma, proyecto_nombre,
plataforma, duracion_segundos,
vistas, likes, compartidos, fecha_publicacion,
estructura_narrativa, gancho_tipo, gancho_texto, gancho_duracion_seg,
desarrollo_tipo, cta_tipo, cta_texto,
arco_emocional, conflicto_central, resolucion,
pacing_ritmo, numero_actos,
cialdini_reciprocidad, cialdini_escasez, cialdini_autoridad,
cialdini_consistencia, cialdini_prueba_social, cialdini_simpatia,
cialdini_unidad,
sesgo_cognitivo, trigger_emocional, intensidad_emocional,
atencion_visual, lenguaje_sensorial, contraste_narrativo,
efecto_novedad, dolor_placer, personalizacion, carga_cognitiva,
velocidad_locucion, uso_musica, micro_compromisos,
tema_principal, angulo_unico, palabras_clave,
transcript, tono, persona_narradora, promesa_explicita,
nivel_especificidad, score_virabilidad, resumen_patron,
procesado_ok, version_prompt
) values (
'fitness', 'pérdida de peso', 'MX', 'es', 'Campaña Verano 2025',
'reels', 45,
850000, 62000, 18000, '2025-01-15',
'PAS', 'pregunta', '¿Por qué no bajas de peso aunque...', 4,
'problema_solucion', 'seguir', 'Sígueme para más tips',
'frustración → esperanza → motivación',
'La persona hace todo bien pero no ve resultados',
'El problema es el cortisol, no las calorías',
'rapido', 3,
true, false, true,
false, true, true,
false,
'Sesgo de confirmación', 'curiosidad', 8,
'cara_camara', true, true,
true, 'apela_dolor', true, 'baja',
'rapida', true, false,
'pérdida de peso', 'ataca el cortisol como causa real, no las calorías',
array['cortisol', 'bajar de peso', 'metabolismo', 'fitness'],
'¿Por qué no bajas de peso aunque comes bien y haces ejercicio? El problema no son las calorías. Es el cortisol...',
'educativo', 'segunda_persona', 'Aprenderás por qué tu cuerpo retiene grasa a pesar del ejercicio',
'ultra_especifico', 87,
'Video educativo que ataca creencia falsa común. Usa pregunta retórica como gancho, autoridad con dato científico y prueba social implícita.',
true, 'v1.0'
);
-- 3. Verificar que el trigger calculó el score de engagement
select
id,
niche,
vistas,
likes,
compartidos,
score_engagement,
score_cialdini
from guiones
where niche = 'fitness';
-- Resultado esperado:
-- score_engagement = (62000 + 18000*3) / 850000 * 100 = 13.647...
-- score_cialdini = 4 (reciprocidad + autoridad + prueba_social + simpatia)

View File

@ -0,0 +1,455 @@
<!DOCTYPE html>
<html class="dark" lang="en"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>Guiones IA - Analysis Detail View</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&amp;family=Manrope:wght@500;600;700;800&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
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"],
"body": ["Inter"],
"label": ["Inter"]
},
borderRadius: {"DEFAULT": "0.125rem", "lg": "0.25rem", "xl": "0.5rem", "full": "0.75rem"},
},
},
}
</script>
<style>
body { font-family: 'Inter', sans-serif; }
.font-headline { font-family: 'Manrope', sans-serif; }
.material-symbols-outlined { font-variation-settings: 'FILL' 0, 'wght' 400, '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%);
}
</style>
</head>
<body class="bg-surface text-on-surface selection:bg-primary-container selection:text-on-primary-container">
<!-- Side Navigation Rail -->
<aside class="fixed left-0 top-0 h-full z-40 flex flex-col p-4 h-screen w-64 border-r border-white/5 bg-[#13131a] dark:bg-[#13131a] font-['Manrope'] antialiased">
<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 text-on-primary">
<span class="material-symbols-outlined text-sm" style="font-variation-settings: 'FILL' 1;">bolt</span>
</div>
<div>
<h1 class="text-xl font-bold tracking-tight text-white">Guiones IA</h1>
<p class="text-[10px] text-on-surface-variant/50 uppercase tracking-widest font-bold">Marketing Pro</p>
</div>
</div>
<nav class="flex-1 space-y-2">
<a class="flex items-center gap-3 px-3 py-2.5 text-[#c7c4d7] hover:text-white transition-all duration-200 hover:bg-white/5 group" href="#">
<span class="material-symbols-outlined group-hover:scale-110 transition-transform">dashboard</span>
<span class="text-sm">Dashboard</span>
</a>
<a class="flex items-center gap-3 px-3 py-2.5 bg-white/10 text-white rounded-lg font-semibold transition-all duration-200" href="#">
<span class="material-symbols-outlined" style="font-variation-settings: 'FILL' 1;">analytics</span>
<span class="text-sm">Analysis</span>
</a>
<a class="flex items-center gap-3 px-3 py-2.5 text-[#c7c4d7] hover:text-white transition-all duration-200 hover:bg-white/5 group" href="#">
<span class="material-symbols-outlined group-hover:scale-110 transition-transform">description</span>
<span class="text-sm">Scripts</span>
</a>
<a class="flex items-center gap-3 px-3 py-2.5 text-[#c7c4d7] hover:text-white transition-all duration-200 hover:bg-white/5 group" href="#">
<span class="material-symbols-outlined group-hover:scale-110 transition-transform">settings</span>
<span class="text-sm">Settings</span>
</a>
</nav>
<button class="mt-auto w-full bg-primary-container hover:bg-primary text-on-primary-container font-bold py-3 rounded-xl flex items-center justify-center gap-2 shadow-lg shadow-primary-container/20 transition-all active:scale-95">
<span class="material-symbols-outlined text-sm">add</span>
<span class="text-sm">New Script</span>
</button>
</aside>
<!-- Top App Bar -->
<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 transition-all border-b border-white/5">
<div class="flex items-center gap-4 bg-surface-container-lowest px-4 py-2 rounded-full border border-white/5 w-96 focus-within:ring-2 focus-within:ring-indigo-500/40">
<span class="material-symbols-outlined text-on-surface-variant text-sm">search</span>
<input class="bg-transparent border-none text-sm focus:ring-0 text-white w-full placeholder:text-on-surface-variant/50" placeholder="Search analysis history..." type="text"/>
</div>
<div class="flex items-center gap-6">
<button class="relative text-on-surface-variant hover:text-white transition-colors">
<span class="material-symbols-outlined">notifications</span>
<span class="absolute -top-1 -right-1 w-2 h-2 bg-secondary rounded-full"></span>
</button>
<div class="flex items-center gap-3 pl-6 border-l border-white/10">
<div class="text-right">
<p class="text-xs font-bold text-white leading-none">Alex Rivera</p>
<p class="text-[10px] text-on-surface-variant leading-none mt-1">Creative Director</p>
</div>
<div class="w-8 h-8 rounded-full bg-surface-container-highest overflow-hidden ring-1 ring-white/10">
<img class="w-full h-full object-cover" data-alt="portrait of a professional creative director in a modern office with cinematic lighting" src="https://lh3.googleusercontent.com/aida-public/AB6AXuAFuneRKhoHPStJ_1mAld_fptQHCQ764fbkti1Nh4FQw0qdfMzrVWqTlYyt0-bv5z8ehhC03-0J9hq1hiQRbqDq3KqLwpCppuVKy5Alg948iEbFYqrUC2FHqvOobtM_7Fef7Z0l7FtCCBreOj-TfpmBKo5ZdkA5TgIZmEw2h6ZV069ktmIqi0ihWKyG_zc7bechvCpwpW9Lp0a8CdzypAIKwVIp_RyODEvE-5yurARPac8YZnl_bj-sei4iYDFaD3be27MIm1DQ6xLY"/>
</div>
</div>
</div>
</header>
<!-- Main Content Canvas -->
<main class="ml-64 pt-24 px-8 pb-12 min-h-screen">
<!-- Hero Analysis Header -->
<section class="mb-12 flex flex-col md:flex-row items-end gap-8 bg-surface-container-low p-8 rounded-2xl relative overflow-hidden group">
<div class="absolute inset-0 bg-gradient-to-tr from-primary/5 via-transparent to-transparent opacity-50"></div>
<!-- Virality Gauge -->
<div class="relative w-48 h-48 flex-shrink-0 flex items-center justify-center bg-surface-container rounded-full ring-8 ring-surface-container-lowest">
<div class="absolute inset-0 rounded-full radial-gradient-score p-4">
<div class="w-full h-full bg-surface-container-low rounded-full flex flex-col items-center justify-center">
<span class="text-5xl font-extrabold font-headline text-secondary neon-glow">88</span>
<span class="text-[10px] font-bold text-on-surface-variant/70 uppercase tracking-widest">Virality Score</span>
</div>
</div>
</div>
<!-- Video Meta -->
<div class="flex-1 z-10">
<nav class="flex items-center gap-2 text-xs text-on-surface-variant mb-4">
<span>Analysis</span>
<span class="material-symbols-outlined text-[10px]">chevron_right</span>
<span class="text-white">YT-VLOG-2024-042</span>
</nav>
<h2 class="text-4xl font-extrabold font-headline text-white mb-3 tracking-tight">The 5 AM Productivity Myth: Scientific Breakdown</h2>
<div class="flex flex-wrap gap-4 items-center">
<span class="px-3 py-1 bg-primary/10 border border-primary/20 text-primary text-[10px] font-bold rounded-full uppercase">High Performance</span>
<div class="flex items-center gap-2 text-on-surface-variant text-sm">
<span class="material-symbols-outlined text-sm">timer</span>
<span>12:45 Runtime</span>
</div>
<div class="flex items-center gap-2 text-on-surface-variant text-sm">
<span class="material-symbols-outlined text-sm">visibility</span>
<span>Est. 1.2M Reach</span>
</div>
</div>
</div>
<!-- Thumbnail Preview -->
<div class="hidden lg:block w-72 h-40 rounded-xl overflow-hidden shadow-2xl shadow-black relative group-hover:scale-[1.02] transition-transform duration-500 ring-1 ring-white/10">
<img class="w-full h-full object-cover" data-alt="cinematic close up of a vintage alarm clock on a wooden desk with moody low light and indigo morning glow" src="https://lh3.googleusercontent.com/aida-public/AB6AXuCVQZB5awSshFMLUh067teqMYFVuTcwnrPEbJd4sR5HckElPdU3n1jjLlTzI7eoRPESwCmslKMv6DUbc20QJHZyysXzUMALqZtAue85QfhiijRz71dsFh85JZbjGA4WN5sIuot-vx3TmwvT95ZBjNMkz0LNHjmUBLdItQDrNeVn5redzoJh_3ppdXOzTZMf3TWhmn3777j4V1v1sxcd-gtmzay2Q2e78BxN8fpE8mWrvuS0Jt1yuEe6phQ0MD0aUWHujWWmfvVIPGV1"/>
<div class="absolute inset-0 bg-black/40 flex items-center justify-center group-hover:bg-black/20 transition-colors">
<div class="w-12 h-12 rounded-full bg-white/20 backdrop-blur-md flex items-center justify-center text-white ring-1 ring-white/30">
<span class="material-symbols-outlined" style="font-variation-settings: 'FILL' 1;">play_arrow</span>
</div>
</div>
</div>
</section>
<!-- Analysis Grid -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-12">
<!-- 1. Storytelling Section -->
<div class="bg-surface-container-low p-8 rounded-2xl relative">
<div class="flex items-center gap-3 mb-8">
<span class="material-symbols-outlined text-primary text-2xl">auto_stories</span>
<h3 class="text-xl font-bold font-headline text-white">Storytelling Architecture</h3>
</div>
<div class="space-y-8">
<div>
<label class="text-[10px] font-bold text-primary uppercase tracking-widest mb-2 block">The Hook Effectiveness</label>
<p class="text-on-surface-variant leading-relaxed text-sm">
"We've been lied to about the morning." — A curiosity gap combined with an authority challenge that spikes retention in the first 7 seconds.
</p>
</div>
<div>
<label class="text-[10px] font-bold text-primary uppercase tracking-widest mb-4 block">Narrative Structure Diagram</label>
<div class="flex items-center justify-between px-2">
<div class="flex flex-col items-center gap-2">
<div class="w-12 h-12 rounded-full bg-primary/20 flex items-center justify-center text-primary ring-1 ring-primary/40">
<span class="material-symbols-outlined">anchor</span>
</div>
<span class="text-[10px] font-medium">Hook</span>
</div>
<div class="h-[1px] flex-1 bg-surface-container-highest mx-2"></div>
<div class="flex flex-col items-center gap-2">
<div class="w-12 h-12 rounded-full bg-surface-container-highest flex items-center justify-center text-on-surface-variant ring-1 ring-white/5">
<span class="material-symbols-outlined">psychology_alt</span>
</div>
<span class="text-[10px] font-medium text-on-surface-variant">Rising</span>
</div>
<div class="h-[1px] flex-1 bg-surface-container-highest mx-2"></div>
<div class="flex flex-col items-center gap-2">
<div class="w-12 h-12 rounded-full bg-surface-container-highest flex items-center justify-center text-on-surface-variant ring-1 ring-white/5">
<span class="material-symbols-outlined">mountain_flag</span>
</div>
<span class="text-[10px] font-medium text-on-surface-variant">Climax</span>
</div>
<div class="h-[1px] flex-1 bg-surface-container-highest mx-2"></div>
<div class="flex flex-col items-center gap-2">
<div class="w-12 h-12 rounded-full bg-secondary/20 flex items-center justify-center text-secondary ring-1 ring-secondary/40">
<span class="material-symbols-outlined">ads_click</span>
</div>
<span class="text-[10px] font-medium text-secondary">CTA</span>
</div>
</div>
</div>
<div>
<label class="text-[10px] font-bold text-primary uppercase tracking-widest mb-2 block">Emotional Arc Visualization</label>
<div class="h-16 w-full flex items-end gap-1 overflow-hidden">
<div class="flex-1 bg-primary/20 h-4 rounded-t-sm"></div>
<div class="flex-1 bg-primary/30 h-8 rounded-t-sm"></div>
<div class="flex-1 bg-primary/40 h-10 rounded-t-sm"></div>
<div class="flex-1 bg-primary/60 h-14 rounded-t-sm"></div>
<div class="flex-1 bg-primary/50 h-11 rounded-t-sm"></div>
<div class="flex-1 bg-primary/30 h-6 rounded-t-sm"></div>
<div class="flex-1 bg-primary/20 h-4 rounded-t-sm"></div>
<div class="flex-1 bg-secondary h-16 rounded-t-sm shadow-[0_0_10px_rgba(78,222,163,0.3)]"></div>
</div>
<div class="flex justify-between text-[8px] text-on-surface-variant uppercase mt-1 tracking-tighter">
<span>Intrigue</span>
<span>Confusion</span>
<span>Revelation</span>
<span>Empowerment</span>
</div>
</div>
</div>
</div>
<!-- 2. Psychology/Cialdini Section -->
<div class="bg-surface-container-low p-8 rounded-2xl border-l-2 border-secondary/20">
<div class="flex items-center gap-3 mb-8">
<span class="material-symbols-outlined text-secondary text-2xl">lightbulb</span>
<h3 class="text-xl font-bold font-headline text-white">Psychological Framework</h3>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-8">
<!-- Principles Grid -->
<div class="col-span-2 grid grid-cols-2 md:grid-cols-4 gap-3">
<div class="p-3 bg-surface-container rounded-xl border border-white/5 flex items-center gap-3">
<div class="w-2 h-2 rounded-full bg-secondary shadow-[0_0_8px_rgba(78,222,163,0.5)]"></div>
<span class="text-xs font-semibold">Authority</span>
</div>
<div class="p-3 bg-surface-container rounded-xl border border-white/5 flex items-center gap-3">
<div class="w-2 h-2 rounded-full bg-secondary shadow-[0_0_8px_rgba(78,222,163,0.5)]"></div>
<span class="text-xs font-semibold">Consensus</span>
</div>
<div class="p-3 bg-surface-container-lowest opacity-40 rounded-xl border border-white/5 flex items-center gap-3">
<div class="w-2 h-2 rounded-full bg-white/20"></div>
<span class="text-xs font-semibold">Scarcity</span>
</div>
<div class="p-3 bg-surface-container rounded-xl border border-white/5 flex items-center gap-3">
<div class="w-2 h-2 rounded-full bg-secondary shadow-[0_0_8px_rgba(78,222,163,0.5)]"></div>
<span class="text-xs font-semibold">Unity</span>
</div>
<div class="p-3 bg-surface-container rounded-xl border border-white/5 flex items-center gap-3">
<div class="w-2 h-2 rounded-full bg-secondary shadow-[0_0_8px_rgba(78,222,163,0.5)]"></div>
<span class="text-xs font-semibold">Consistency</span>
</div>
<div class="p-3 bg-surface-container-lowest opacity-40 rounded-xl border border-white/5 flex items-center gap-3">
<div class="w-2 h-2 rounded-full bg-white/20"></div>
<span class="text-xs font-semibold">Reciprocity</span>
</div>
<div class="p-3 bg-surface-container rounded-xl border border-white/5 flex items-center gap-3">
<div class="w-2 h-2 rounded-full bg-secondary shadow-[0_0_8px_rgba(78,222,163,0.5)]"></div>
<span class="text-xs font-semibold">Liking</span>
</div>
</div>
</div>
<div class="space-y-6">
<div class="bg-surface-container p-6 rounded-xl border border-white/5">
<label class="text-[10px] font-bold text-secondary uppercase tracking-widest mb-3 block">Primary Emotional Trigger</label>
<div class="flex items-center gap-4">
<div class="text-3xl">🧩</div>
<div>
<h4 class="text-lg font-bold text-white leading-tight">Intellectual Reframing</h4>
<p class="text-xs text-on-surface-variant">The content challenges the viewer's ego by suggesting their "hard work" might be inefficient.</p>
</div>
</div>
</div>
<div class="bg-gradient-to-r from-secondary/10 to-transparent p-4 rounded-xl border border-secondary/10">
<p class="text-xs italic text-secondary leading-relaxed">
"The use of Unity here is subtle: connecting the viewer with 'Elite Performers' who don't follow the mainstream 5 AM advice."
</p>
</div>
</div>
</div>
<!-- 3. Neuromarketing Section -->
<div class="bg-surface-container-low p-8 rounded-2xl">
<div class="flex items-center gap-3 mb-8">
<span class="material-symbols-outlined text-primary text-2xl">neurology</span>
<h3 class="text-xl font-bold font-headline text-white">Neuromarketing Insights</h3>
</div>
<div class="space-y-8">
<div>
<div class="flex justify-between items-end mb-4">
<label class="text-[10px] font-bold text-primary uppercase tracking-widest">Visual Attention Score</label>
<span class="text-xl font-bold text-white">92%</span>
</div>
<div class="w-full h-2 bg-surface-container-highest rounded-full overflow-hidden">
<div class="h-full bg-gradient-to-r from-primary to-primary-container w-[92%]"></div>
</div>
<p class="text-[10px] text-on-surface-variant mt-2 italic">High motion density in first 60s keeps dopamine firing high.</p>
</div>
<div class="grid grid-cols-2 gap-4">
<div class="bg-surface-container p-4 rounded-xl">
<label class="text-[10px] font-bold text-on-surface-variant uppercase tracking-widest block mb-1">Cognitive Load</label>
<div class="flex items-center gap-2">
<span class="text-secondary font-bold">Low</span>
<div class="flex gap-1">
<div class="w-4 h-1 bg-secondary rounded-full"></div>
<div class="w-4 h-1 bg-surface-container-highest rounded-full"></div>
<div class="w-4 h-1 bg-surface-container-highest rounded-full"></div>
</div>
</div>
</div>
<div class="bg-surface-container p-4 rounded-xl">
<label class="text-[10px] font-bold text-on-surface-variant uppercase tracking-widest block mb-1">Sensory Language</label>
<span class="text-white font-bold text-sm">Auditory-First</span>
</div>
</div>
<div>
<label class="text-[10px] font-bold text-primary uppercase tracking-widest block mb-4">Focus Map Heat Overlay</label>
<div class="aspect-video rounded-xl bg-surface-container-lowest overflow-hidden relative border border-white/5">
<img class="w-full h-full object-cover opacity-30 grayscale" data-alt="data visualization dashboard showing heatmap overlays on video frames with vibrant teal and violet gradients" src="https://lh3.googleusercontent.com/aida-public/AB6AXuBBuDe4U5eH6nN47xvd3jOGdUqVS4SCo-d4UfTWtvgPif1OtkhKSea7TngCEzwfqij8cUz_olcJFQYpenjMmgVvMac3URJrjuCx0AvkRGfT5AU6hk2Wut4-9ASsXchwYu0V-WJVy8B0Jggf0B4XgVLtYg8DkKdN4Z3ZoeMMw84vX-_ANzie5yqQUu1r9wnag1vRzZ6c4vfhvVy6WBugqNfg4KwKxJy0W5C_nvy7z-KnJ69p5OKfTxh7fJRrXVdrT4P_4NdvLS1Hybrm"/>
<div class="absolute inset-0 flex items-center justify-center">
<div class="w-32 h-32 bg-primary/20 blur-3xl rounded-full"></div>
<div class="w-16 h-16 bg-secondary/40 blur-xl rounded-full"></div>
</div>
<div class="absolute bottom-4 left-4 flex gap-2">
<div class="px-2 py-1 bg-black/60 backdrop-blur-md rounded text-[8px] font-bold uppercase">Frame 1024: Peak Focus</div>
</div>
</div>
</div>
</div>
</div>
<!-- 4. Content Section -->
<div class="bg-surface-container-low p-8 rounded-2xl relative overflow-hidden group">
<div class="absolute top-0 right-0 w-32 h-32 bg-primary/5 rounded-full -translate-y-16 translate-x-16 blur-2xl"></div>
<div class="flex items-center gap-3 mb-8">
<span class="material-symbols-outlined text-secondary text-2xl">stylus_note</span>
<h3 class="text-xl font-bold font-headline text-white">Content Synthesis</h3>
</div>
<div class="space-y-6">
<div class="flex gap-4">
<div class="flex-1">
<label class="text-[10px] font-bold text-on-surface-variant uppercase tracking-widest block mb-1">Overall Tone</label>
<div class="text-white font-bold flex items-center gap-2">
Assertive &amp; Data-Driven
<span class="material-symbols-outlined text-xs text-secondary">verified</span>
</div>
</div>
<div class="flex-1 text-right">
<label class="text-[10px] font-bold text-on-surface-variant uppercase tracking-widest block mb-1">Target Persona</label>
<div class="text-white font-bold">Aspiring Solopreneurs</div>
</div>
</div>
<div class="bg-surface-container p-6 rounded-xl border border-white/5">
<label class="text-[10px] font-bold text-secondary uppercase tracking-widest mb-3 block">Primary Promise</label>
<p class="text-white text-lg font-medium leading-snug">
"Master your biology to achieve in 4 hours what others do in 14."
</p>
</div>
<div>
<label class="text-[10px] font-bold text-on-surface-variant uppercase tracking-widest mb-3 block">Top Ranking Keywords</label>
<div class="flex flex-wrap gap-2">
<span class="px-3 py-1 bg-surface-container-highest rounded text-xs border border-white/5 text-on-surface hover:text-white hover:border-primary/40 cursor-default transition-all">Circadian Rhythm</span>
<span class="px-3 py-1 bg-surface-container-highest rounded text-xs border border-white/5 text-on-surface hover:text-white hover:border-primary/40 cursor-default transition-all">Dopamine Baseline</span>
<span class="px-3 py-1 bg-surface-container-highest rounded text-xs border border-white/5 text-on-surface hover:text-white hover:border-primary/40 cursor-default transition-all">Deep Work</span>
<span class="px-3 py-1 bg-surface-container-highest rounded text-xs border border-white/5 text-on-surface hover:text-white hover:border-primary/40 cursor-default transition-all">Productivity Trap</span>
<span class="px-3 py-1 bg-surface-container-highest rounded text-xs border border-white/5 text-on-surface hover:text-white hover:border-primary/40 cursor-default transition-all">Biohacking</span>
</div>
</div>
</div>
</div>
</div>
<!-- Collapsible Full Transcript Section -->
<section class="bg-surface-container-low rounded-2xl overflow-hidden border border-white/5">
<button class="w-full px-8 py-5 flex items-center justify-between hover:bg-white/5 transition-colors">
<div class="flex items-center gap-4">
<div class="w-10 h-10 rounded-lg bg-surface-container-highest flex items-center justify-center">
<span class="material-symbols-outlined text-on-surface-variant">subject</span>
</div>
<div class="text-left">
<h4 class="text-white font-bold">Full Transcript Analysis</h4>
<p class="text-[10px] text-on-surface-variant uppercase tracking-widest">1,842 Words Analyzed by IA-Engine</p>
</div>
</div>
<span class="material-symbols-outlined text-on-surface-variant">expand_more</span>
</button>
<!-- Preview content for UI richness (Simulating it's collapsed state but visible for the task) -->
<div class="px-8 pb-8 pt-4">
<div class="bg-surface-container-lowest p-6 rounded-xl border border-white/5 text-on-surface-variant text-sm font-body leading-relaxed max-h-48 overflow-y-auto scrollbar-thin scrollbar-thumb-white/10">
<p class="mb-4"><span class="text-primary font-bold mr-2">[00:00]</span> Look, we've all seen the videos. The 5 AM morning routine, the ice bath, the immediate meditation. But what if I told you that for 60% of people, this is actually destroying your productivity?</p>
<p class="mb-4"><span class="text-primary font-bold mr-2">[01:15]</span> Research from the Sleep Science Institute shows that forcing your chronotype into a schedule it wasn't designed for causes a permanent state of brain fog known as social jetlag.</p>
<p class="mb-4"><span class="text-primary font-bold mr-2">[02:45]</span> Today we're breaking down the bio-individual approach to high-performance work...</p>
<p class="opacity-50">...[Click to expand full analysis]</p>
</div>
</div>
</section>
</main>
<!-- Mobile Navigation Bar (md:hidden) -->
<footer class="md:hidden fixed bottom-0 left-0 right-0 h-16 bg-surface-container-low border-t border-white/5 z-50 flex items-center justify-around px-4">
<button class="flex flex-col items-center gap-1 text-[#c7c4d7]">
<span class="material-symbols-outlined">dashboard</span>
<span class="text-[10px]">Dashboard</span>
</button>
<button class="flex flex-col items-center gap-1 text-white">
<span class="material-symbols-outlined" style="font-variation-settings: 'FILL' 1;">analytics</span>
<span class="text-[10px] font-bold">Analysis</span>
</button>
<div class="relative -top-6">
<button class="w-14 h-14 bg-primary-container text-on-primary-container rounded-full shadow-lg shadow-primary-container/40 flex items-center justify-center">
<span class="material-symbols-outlined">add</span>
</button>
</div>
<button class="flex flex-col items-center gap-1 text-[#c7c4d7]">
<span class="material-symbols-outlined">description</span>
<span class="text-[10px]">Scripts</span>
</button>
<button class="flex flex-col items-center gap-1 text-[#c7c4d7]">
<span class="material-symbols-outlined">settings</span>
<span class="text-[10px]">Settings</span>
</button>
</footer>
</body></html>

View File

@ -0,0 +1,393 @@
<!DOCTYPE html>
<html class="dark" lang="en"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>Guiones IA - Marketing Pro Dashboard</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;600;700;800&amp;family=Inter:wght@400;500;600&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<style>
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 300, 'GRAD' 0, 'opsz' 24;
}
body {
font-family: 'Inter', sans-serif;
background-color: #13131a;
}
.font-headline {
font-family: 'Manrope', sans-serif;
}
.glass-panel {
background: rgba(31, 31, 38, 0.6);
backdrop-filter: blur(20px);
}
</style>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
colors: {
"surface-container-low": "#1b1b22",
"surface-container": "#1f1f26",
"surface-container-highest": "#34343c",
"surface": "#13131a",
"primary": "#c0c1ff",
"primary-container": "#8083ff",
"on-primary-container": "#0d0096",
"secondary": "#4edea3",
"on-surface": "#e4e1ec",
"on-surface-variant": "#c7c4d7",
"outline-variant": "#464554",
},
fontFamily: {
"headline": ["Manrope"],
"body": ["Inter"],
},
borderRadius: {"DEFAULT": "0.125rem", "lg": "0.25rem", "xl": "0.5rem", "full": "0.75rem"},
},
},
}
</script>
</head>
<body class="text-on-surface antialiased selection:bg-primary/30">
<!-- SideNavBar Shell -->
<aside class="fixed left-0 top-0 h-full z-40 flex flex-col p-4 h-screen w-64 border-r border-white/5 bg-[#13131a] font-['Manrope'] antialiased shadow-2xl shadow-indigo-500/10">
<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>
<nav class="flex-1 space-y-1">
<a class="flex items-center gap-3 px-3 py-2.5 bg-white/10 text-white rounded-lg font-semibold transition-all duration-200" href="#">
<span class="material-symbols-outlined text-[20px]">dashboard</span>
<span class="text-sm">Dashboard</span>
</a>
<a 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" href="#">
<span class="material-symbols-outlined text-[20px]">analytics</span>
<span class="text-sm">Analysis</span>
</a>
<a 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" href="#">
<span class="material-symbols-outlined text-[20px]">description</span>
<span class="text-sm">Scripts</span>
</a>
<a 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" href="#">
<span class="material-symbols-outlined text-[20px]">settings</span>
<span class="text-sm">Settings</span>
</a>
</nav>
<div class="mt-auto pt-6 border-t border-white/5">
<button 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">
<span class="material-symbols-outlined text-sm">add</span>
New Script
</button>
</div>
</aside>
<!-- TopAppBar Shell -->
<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" 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">Growth Lead</p>
</div>
<div class="w-8 h-8 rounded-full overflow-hidden bg-surface-container-highest">
<img class="w-full h-full object-cover" data-alt="professional headshot of a marketing executive with confident expression, neutral studio background, high-end photography" src="https://lh3.googleusercontent.com/aida-public/AB6AXuAF_qikv11E-m1fg7HE7CLMu_qdasNYKJt0vmz-v3yT6b2kpw_lMre-iwzo_G52AH5bkHIpJXAQ7OrqNTKiXwPybCJ2AO950DIm2ARnq7t9bgP_ZIr2f1IBAJ1uNSBj6ZtHupN9byFF50rsf5_LseEknLEetvpcMkk4dw-38bYDw4vfQXdDzQ7xvEJlPD46c788j7cCJjzumWd04CvMP41-3bIH-JssH_uvOrNB7GUfUt7k1NaWW9T_jMMzwhViAJ6syW7o8IGsR6NA"/>
</div>
</div>
</div>
</header>
<!-- Main Content Canvas -->
<main class="ml-64 pt-24 pb-12 px-8 min-h-screen">
<!-- Header Section -->
<header class="mb-10 flex items-end justify-between">
<div>
<h2 class="text-4xl font-extrabold font-headline tracking-tight text-white mb-2">Engineering Hub</h2>
<p class="text-on-surface-variant max-w-lg">Transforming high-performance viral data into editorial script masterpieces.</p>
</div>
<div class="flex gap-3">
<button class="px-4 py-2 bg-surface-container-low text-on-surface rounded-lg text-sm font-semibold border border-outline-variant/20 hover:bg-surface-container transition-colors">Export Report</button>
<button class="px-4 py-2 bg-primary text-on-primary rounded-lg text-sm font-bold shadow-lg shadow-primary/10 hover:brightness-110 transition-all">Run Analysis</button>
</div>
</header>
<!-- KPI Bento Grid -->
<section class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-12">
<!-- Card 1 -->
<div class="bg-surface-container-low p-6 rounded-xl border border-outline-variant/10 group hover:border-primary/20 transition-all">
<div class="flex items-center justify-between mb-4">
<span class="text-outline text-xs font-bold uppercase tracking-wider">Top Niche</span>
<span class="material-symbols-outlined text-secondary text-lg" style="font-variation-settings: 'FILL' 1;">trending_up</span>
</div>
<h3 class="text-2xl font-bold text-white font-headline">Finance</h3>
<div class="flex items-center gap-2 mt-2">
<span class="text-[10px] px-2 py-0.5 rounded bg-secondary/10 text-secondary border border-secondary/20 font-bold">+12.4%</span>
<span class="text-[10px] text-outline">vs last month</span>
</div>
</div>
<!-- Card 2 -->
<div class="bg-surface-container-low p-6 rounded-xl border border-outline-variant/10 group hover:border-primary/20 transition-all">
<div class="flex items-center justify-between mb-4">
<span class="text-outline text-xs font-bold uppercase tracking-wider">Virality Score</span>
<span class="material-symbols-outlined text-primary text-lg">bolt</span>
</div>
<h3 class="text-2xl font-bold text-white font-headline">82<span class="text-sm text-outline font-normal ml-1">/100</span></h3>
<div class="w-full bg-surface-container-highest h-1 rounded-full mt-4 overflow-hidden">
<div class="bg-gradient-to-r from-primary-container to-primary h-full w-[82%]"></div>
</div>
</div>
<!-- Card 3 -->
<div class="bg-surface-container-low p-6 rounded-xl border border-outline-variant/10 group hover:border-primary/20 transition-all">
<div class="flex items-center justify-between mb-4">
<span class="text-outline text-xs font-bold uppercase tracking-wider">Analyzed Videos</span>
<span class="material-symbols-outlined text-outline text-lg">videocam</span>
</div>
<h3 class="text-2xl font-bold text-white font-headline">142</h3>
<p class="text-[10px] text-outline mt-2 italic">Processed in last 7 days</p>
</div>
<!-- Card 4 -->
<div class="bg-surface-container-low p-6 rounded-xl border border-outline-variant/10 group hover:border-primary/20 transition-all">
<div class="flex items-center justify-between mb-4">
<span class="text-outline text-xs font-bold uppercase tracking-wider">Top Platform</span>
<span class="material-symbols-outlined text-red-400 text-lg">favorite</span>
</div>
<h3 class="text-2xl font-bold text-white font-headline">TikTok</h3>
<div class="flex items-center gap-2 mt-2">
<span class="text-[10px] px-2 py-0.5 rounded bg-red-400/10 text-red-400 border border-red-400/20 font-bold">Viral Dominance</span>
</div>
</div>
</section>
<!-- Main Data Table Section -->
<section class="bg-surface-container-low rounded-2xl border border-outline-variant/10 overflow-hidden shadow-2xl">
<div class="p-6 flex items-center justify-between border-b border-white/5 bg-surface-container/30">
<h4 class="font-headline font-bold text-lg text-white">Analyzed Videos Pipeline</h4>
<div class="flex gap-2">
<button class="p-2 text-outline hover:text-white transition-colors">
<span class="material-symbols-outlined text-xl">filter_list</span>
</button>
<button class="p-2 text-outline hover:text-white transition-colors">
<span class="material-symbols-outlined text-xl">sort</span>
</button>
</div>
</div>
<div class="overflow-x-auto">
<table class="w-full text-left border-collapse">
<thead>
<tr class="text-[11px] uppercase tracking-widest text-outline border-b border-white/5 bg-surface-container-low">
<th class="px-6 py-4 font-bold">Platform</th>
<th class="px-6 py-4 font-bold">Niche</th>
<th class="px-6 py-4 font-bold">Narrative Structure</th>
<th class="px-6 py-4 font-bold">Emotional Trigger</th>
<th class="px-6 py-4 font-bold text-center">Virality</th>
<th class="px-6 py-4 font-bold text-center">Eng. %</th>
<th class="px-6 py-4 font-bold text-center">Cialdini</th>
<th class="px-6 py-4 font-bold text-right">Action</th>
</tr>
</thead>
<tbody class="divide-y divide-white/[0.03]">
<!-- Row 1 -->
<tr class="hover:bg-white/[0.02] transition-colors group">
<td class="px-6 py-5">
<span class="px-3 py-1 text-[10px] font-bold rounded-full border border-red-500/30 bg-red-500/10 text-red-400 uppercase tracking-tighter shadow-[0_0_10px_rgba(239,68,68,0.1)]">TikTok</span>
</td>
<td class="px-6 py-5">
<span class="px-2 py-1 bg-surface-container-highest text-on-surface-variant text-[11px] rounded font-medium">Marketing</span>
</td>
<td class="px-6 py-5 text-sm font-medium text-on-surface">Hook-Problem-Solution</td>
<td class="px-6 py-5 text-sm text-on-surface-variant">Urgency</td>
<td class="px-6 py-5">
<div class="flex flex-col items-center">
<span class="text-lg font-bold text-secondary">94</span>
<div class="w-12 h-0.5 bg-secondary/20 rounded-full mt-1">
<div class="bg-secondary h-full w-[94%]"></div>
</div>
</div>
</td>
<td class="px-6 py-5 text-center text-sm font-medium">4.2%</td>
<td class="px-6 py-5 text-center">
<div class="flex items-center justify-center gap-0.5">
<span class="w-1.5 h-1.5 rounded-full bg-primary"></span>
<span class="w-1.5 h-1.5 rounded-full bg-primary"></span>
<span class="w-1.5 h-1.5 rounded-full bg-primary"></span>
<span class="w-1.5 h-1.5 rounded-full bg-primary/20"></span>
<span class="w-1.5 h-1.5 rounded-full bg-primary/20"></span>
<span class="text-[10px] ml-1 text-primary font-bold">3/7</span>
</div>
</td>
<td class="px-6 py-5 text-right">
<button class="text-outline hover:text-white transition-colors">
<span class="material-symbols-outlined text-lg">more_vert</span>
</button>
</td>
</tr>
<!-- Row 2 -->
<tr class="bg-surface-container-lowest/30 hover:bg-white/[0.02] transition-colors group">
<td class="px-6 py-5">
<span class="px-3 py-1 text-[10px] font-bold rounded-full border border-fuchsia-500/30 bg-fuchsia-500/10 text-fuchsia-400 uppercase tracking-tighter shadow-[0_0_10px_rgba(217,70,239,0.1)]">Reels</span>
</td>
<td class="px-6 py-5">
<span class="px-2 py-1 bg-surface-container-highest text-on-surface-variant text-[11px] rounded font-medium">E-commerce</span>
</td>
<td class="px-6 py-5 text-sm font-medium text-on-surface">Product-Benefit-CTA</td>
<td class="px-6 py-5 text-sm text-on-surface-variant">Curiosity</td>
<td class="px-6 py-5">
<div class="flex flex-col items-center">
<span class="text-lg font-bold text-secondary">78</span>
<div class="w-12 h-0.5 bg-secondary/20 rounded-full mt-1">
<div class="bg-secondary h-full w-[78%]"></div>
</div>
</div>
</td>
<td class="px-6 py-5 text-center text-sm font-medium">3.8%</td>
<td class="px-6 py-5 text-center">
<div class="flex items-center justify-center gap-0.5">
<span class="w-1.5 h-1.5 rounded-full bg-primary"></span>
<span class="w-1.5 h-1.5 rounded-full bg-primary"></span>
<span class="w-1.5 h-1.5 rounded-full bg-primary"></span>
<span class="w-1.5 h-1.5 rounded-full bg-primary"></span>
<span class="w-1.5 h-1.5 rounded-full bg-primary"></span>
<span class="text-[10px] ml-1 text-primary font-bold">5/7</span>
</div>
</td>
<td class="px-6 py-5 text-right">
<button class="text-outline hover:text-white transition-colors">
<span class="material-symbols-outlined text-lg">more_vert</span>
</button>
</td>
</tr>
<!-- Row 3 -->
<tr class="hover:bg-white/[0.02] transition-colors group">
<td class="px-6 py-5">
<span class="px-3 py-1 text-[10px] font-bold rounded-full border border-red-600/30 bg-red-600/10 text-red-500 uppercase tracking-tighter">Shorts</span>
</td>
<td class="px-6 py-5">
<span class="px-2 py-1 bg-surface-container-highest text-on-surface-variant text-[11px] rounded font-medium">Finance</span>
</td>
<td class="px-6 py-5 text-sm font-medium text-on-surface">Education-Shock-Value</td>
<td class="px-6 py-5 text-sm text-on-surface-variant">Authority</td>
<td class="px-6 py-5">
<div class="flex flex-col items-center">
<span class="text-lg font-bold text-secondary">62</span>
<div class="w-12 h-0.5 bg-secondary/20 rounded-full mt-1">
<div class="bg-secondary h-full w-[62%]"></div>
</div>
</div>
</td>
<td class="px-6 py-5 text-center text-sm font-medium">5.1%</td>
<td class="px-6 py-5 text-center">
<div class="flex items-center justify-center gap-0.5">
<span class="w-1.5 h-1.5 rounded-full bg-primary"></span>
<span class="w-1.5 h-1.5 rounded-full bg-primary/20"></span>
<span class="w-1.5 h-1.5 rounded-full bg-primary/20"></span>
<span class="w-1.5 h-1.5 rounded-full bg-primary/20"></span>
<span class="w-1.5 h-1.5 rounded-full bg-primary/20"></span>
<span class="text-[10px] ml-1 text-primary font-bold">1/7</span>
</div>
</td>
<td class="px-6 py-5 text-right">
<button class="text-outline hover:text-white transition-colors">
<span class="material-symbols-outlined text-lg">more_vert</span>
</button>
</td>
</tr>
<!-- Row 4 -->
<tr class="bg-surface-container-lowest/30 hover:bg-white/[0.02] transition-colors group">
<td class="px-6 py-5">
<span class="px-3 py-1 text-[10px] font-bold rounded-full border border-red-500/30 bg-red-500/10 text-red-400 uppercase tracking-tighter shadow-[0_0_10px_rgba(239,68,68,0.1)]">TikTok</span>
</td>
<td class="px-6 py-5">
<span class="px-2 py-1 bg-surface-container-highest text-on-surface-variant text-[11px] rounded font-medium">Personal Brand</span>
</td>
<td class="px-6 py-5 text-sm font-medium text-on-surface">Vlog-Insight-Call</td>
<td class="px-6 py-5 text-sm text-on-surface-variant">Belonging</td>
<td class="px-6 py-5">
<div class="flex flex-col items-center">
<span class="text-lg font-bold text-secondary">88</span>
<div class="w-12 h-0.5 bg-secondary/20 rounded-full mt-1">
<div class="bg-secondary h-full w-[88%]"></div>
</div>
</div>
</td>
<td class="px-6 py-5 text-center text-sm font-medium">2.9%</td>
<td class="px-6 py-5 text-center">
<div class="flex items-center justify-center gap-0.5">
<span class="w-1.5 h-1.5 rounded-full bg-primary"></span>
<span class="w-1.5 h-1.5 rounded-full bg-primary"></span>
<span class="w-1.5 h-1.5 rounded-full bg-primary"></span>
<span class="w-1.5 h-1.5 rounded-full bg-primary"></span>
<span class="w-1.5 h-1.5 rounded-full bg-primary/20"></span>
<span class="text-[10px] ml-1 text-primary font-bold">4/7</span>
</div>
</td>
<td class="px-6 py-5 text-right">
<button class="text-outline hover:text-white transition-colors">
<span class="material-symbols-outlined text-lg">more_vert</span>
</button>
</td>
</tr>
</tbody>
</table>
</div>
<div class="p-4 border-t border-white/5 flex items-center justify-between text-[11px] text-outline font-medium uppercase tracking-wider">
<span>Showing 1-4 of 142 results</span>
<div class="flex gap-2">
<button class="px-3 py-1 bg-surface-container rounded hover:text-white transition-colors">Previous</button>
<button class="px-3 py-1 bg-primary/10 text-primary border border-primary/20 rounded font-bold">1</button>
<button class="px-3 py-1 bg-surface-container rounded hover:text-white transition-colors">2</button>
<button class="px-3 py-1 bg-surface-container rounded hover:text-white transition-colors">Next</button>
</div>
</div>
</section>
<!-- Script Concept Feature Section (Editorial Layout) -->
<section class="mt-12 grid grid-cols-1 lg:grid-cols-3 gap-8">
<div class="lg:col-span-2 relative h-[400px] rounded-2xl overflow-hidden group border border-outline-variant/10 shadow-2xl">
<img class="absolute inset-0 w-full h-full object-cover group-hover:scale-105 transition-transform duration-700" data-alt="abstract digital waves with deep indigo and vibrant neon violet colors, high contrast, minimalist futuristic aesthetic" src="https://lh3.googleusercontent.com/aida-public/AB6AXuCIUHH9UAkU59-MsrvYMz5YiBKmgP86klpQs957mt91g016ePYWPvq3SIjn00joLQmQA7MOc8qDhMLW2i_sr-kQwFKIAV_xA5jP580BKRqALEaspHXST8QwTmaBSXLyG3f3NbEJdVBf9vyip0T-3Ev9rzbWB8XinetwSRi0sZLBJ_PYwwpgB_PpYU3UxwFqibEzzreYxEZYk1CcTzShVwNoozei6HhGgBW3d-Ggf_DuoDrS0A9Y6JFQ-e6cr9aI5GVhTzKfPYvM80Oi"/>
<div class="absolute inset-0 bg-gradient-to-t from-surface via-surface/60 to-transparent"></div>
<div class="absolute bottom-0 left-0 p-10">
<span class="text-[10px] font-bold bg-primary-container text-on-primary-container px-3 py-1 rounded-full uppercase tracking-widest mb-4 inline-block">Algorithm Pick</span>
<h3 class="text-4xl font-extrabold font-headline text-white mb-4 leading-tight max-w-lg">Mastering the 'Debt-Free' Hook Strategy for 2024</h3>
<p class="text-on-surface-variant max-w-sm mb-6 text-sm">Our AI detected a 340% spike in narrative hooks using "negative visualization" in the personal finance niche.</p>
<button class="flex items-center gap-2 text-white font-bold group/btn">
Learn how to engineer this
<span class="material-symbols-outlined group-hover/btn:translate-x-1 transition-transform">arrow_forward</span>
</button>
</div>
</div>
<div class="bg-surface-container p-8 rounded-2xl border border-outline-variant/10 flex flex-col justify-center">
<div class="w-12 h-12 rounded-full bg-secondary/10 flex items-center justify-center mb-6">
<span class="material-symbols-outlined text-secondary text-2xl" style="font-variation-settings: 'FILL' 1;">auto_awesome</span>
</div>
<h4 class="text-xl font-bold text-white font-headline mb-4">Smart Recommendations</h4>
<p class="text-on-surface-variant text-sm leading-relaxed mb-6">Based on your recent analyses, we recommend switching your Hook structure to <strong>"Pattern Interrupt"</strong> for YouTube Shorts to capture 1.2s more average watch time.</p>
<ul class="space-y-3 mb-8">
<li class="flex items-center gap-3 text-xs text-on-surface">
<span class="w-1 h-1 rounded-full bg-primary"></span>
Increase visual pacing in first 3s
</li>
<li class="flex items-center gap-3 text-xs text-on-surface">
<span class="w-1 h-1 rounded-full bg-primary"></span>
Use higher contrast text overlays
</li>
<li class="flex items-center gap-3 text-xs text-on-surface">
<span class="w-1 h-1 rounded-full bg-primary"></span>
Leverage "FOMO" emotional trigger
</li>
</ul>
<button class="w-full py-3 bg-surface-container-highest text-white text-xs font-bold rounded-lg border border-white/5 hover:bg-white/10 transition-colors">Apply to Scripts</button>
</div>
</section>
</main>
</body></html>

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>

View File

@ -0,0 +1,313 @@
<!DOCTYPE html>
<html class="dark" lang="en"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>New Analysis - Guiones IA</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&amp;family=Manrope:wght@500;600;700;800&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
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"],
"body": ["Inter"],
"label": ["Inter"]
},
borderRadius: {"DEFAULT": "0.125rem", "lg": "0.25rem", "xl": "0.5rem", "full": "0.75rem"},
},
},
}
</script>
<style>
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 300, 'GRAD' 0, 'opsz' 24;
}
.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); }
}
body { background-color: #13131a; }
</style>
</head>
<body class="font-body text-on-surface antialiased overflow-x-hidden">
<!-- SideNavBar (Authority: Shared Components JSON) -->
<aside class="fixed left-0 top-0 h-full z-40 flex flex-col p-4 bg-[#13131a] dark:bg-[#13131a] font-['Manrope'] antialiased h-screen w-64 border-r border-white/5 shadow-2xl shadow-indigo-500/10">
<div class="mb-8 px-2">
<h1 class="text-xl font-bold tracking-tight text-white">Guiones IA</h1>
<p class="text-xs text-on-surface-variant/60 font-medium uppercase tracking-widest mt-1">Marketing Pro</p>
</div>
<nav class="flex-1 space-y-1">
<a class="flex items-center gap-3 px-3 py-2.5 text-[#c7c4d7] hover:text-white transition-all duration-200 hover:bg-white/5 group" href="#">
<span class="material-symbols-outlined group-hover:text-primary transition-colors">dashboard</span>
<span class="text-sm">Dashboard</span>
</a>
<a class="flex items-center gap-3 px-3 py-2.5 bg-white/10 text-white rounded-lg font-semibold transition-all duration-200" href="#">
<span class="material-symbols-outlined">analytics</span>
<span class="text-sm">Analysis</span>
</a>
<a class="flex items-center gap-3 px-3 py-2.5 text-[#c7c4d7] hover:text-white transition-all duration-200 hover:bg-white/5 group" href="#">
<span class="material-symbols-outlined group-hover:text-primary transition-colors">description</span>
<span class="text-sm">Scripts</span>
</a>
<a class="flex items-center gap-3 px-3 py-2.5 text-[#c7c4d7] hover:text-white transition-all duration-200 hover:bg-white/5 group" href="#">
<span class="material-symbols-outlined group-hover:text-primary transition-colors">settings</span>
<span class="text-sm">Settings</span>
</a>
</nav>
<div class="mt-auto pt-6 border-t border-white/5">
<button class="w-full flex items-center justify-center gap-2 bg-primary-container/20 border border-primary-container/30 text-primary py-2.5 rounded-xl text-sm font-semibold hover:bg-primary-container/30 transition-all active:scale-95">
<span class="material-symbols-outlined text-[20px]">add</span>
New Script
</button>
</div>
</aside>
<!-- TopAppBar (Authority: Shared Components JSON) -->
<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="flex items-center flex-1 max-w-xl">
<div class="relative w-full flex items-center bg-surface-container-low rounded-full px-4 py-1.5 focus-within:ring-2 focus-within:ring-indigo-500/40 transition-all">
<span class="material-symbols-outlined text-outline mr-3">search</span>
<input class="bg-transparent border-none outline-none text-on-surface w-full text-sm placeholder:text-outline-variant focus:ring-0" placeholder="Search analysis or scripts..." type="text"/>
</div>
</div>
<div class="flex items-center gap-4 text-[#c7c4d7]">
<button class="material-symbols-outlined hover:text-white transition-opacity">notifications</button>
<div class="h-8 w-8 rounded-full overflow-hidden bg-surface-container-highest border border-outline-variant/30">
<img class="w-full h-full object-cover" data-alt="close-up portrait of professional male digital marketing manager with friendly expression and modern lighting" src="https://lh3.googleusercontent.com/aida-public/AB6AXuB_Elmeta1tEgUINlAQLR8dS8IkyxYo2kDcULeqaE2b0zhyUz_imTluyAKchJWc-q_AXkbbGGiMfR32Wm_8D2XlyrqhDhfMJWq5WWzQPsgF7g9nmNkCWZ6QtSN70BWl8jVvcajp1wqKvPm03edoqOrfDhlxOa8I3x3fWaT_x6C6DFE2TZuNicRtxvJjv48Mb3uPHDkN4l09FkTnUl26swmUd9HYfSLSpANnGLcFtS_kYb55L7Ip4OAFsmPB1PvB8EUT0YD1B_J0cZi6"/>
</div>
</div>
</header>
<!-- Main Content Canvas -->
<main class="ml-64 pt-24 pb-12 px-12 min-h-screen">
<div class="max-w-4xl mx-auto">
<!-- Header Section -->
<div class="mb-10 text-center">
<h2 class="text-4xl font-headline font-extrabold text-white tracking-tight mb-3">Engineer New Analysis</h2>
<p class="text-on-surface-variant max-w-lg mx-auto leading-relaxed">Transform content data into high-converting AI script models with Obsidian Architecture.</p>
</div>
<!-- Multi-step Wizard Container -->
<div class="relative">
<!-- Progress Pipeline (Floating Top-Bar) -->
<div class="mb-8 grid grid-cols-5 gap-4 px-4 py-6 bg-surface-container-low rounded-2xl border border-outline-variant/10 shadow-xl">
<!-- Step 1: Complete -->
<div class="flex flex-col items-center gap-2 opacity-50">
<div class="w-10 h-10 rounded-full flex items-center justify-center bg-secondary text-on-secondary">
<span class="material-symbols-outlined text-[20px]">check</span>
</div>
<span class="text-[10px] font-bold uppercase tracking-widest text-secondary">Audio Extracted</span>
</div>
<!-- Step 2: Complete -->
<div class="flex flex-col items-center gap-2 opacity-50">
<div class="w-10 h-10 rounded-full flex items-center justify-center bg-secondary text-on-secondary">
<span class="material-symbols-outlined text-[20px]">check</span>
</div>
<span class="text-[10px] font-bold uppercase tracking-widest text-secondary">Transcribed</span>
</div>
<!-- Step 3: Active -->
<div class="flex flex-col items-center gap-2">
<div class="w-10 h-10 rounded-full flex items-center justify-center bg-primary text-on-primary step-pulse">
<span class="material-symbols-outlined text-[20px]" style="font-variation-settings: 'FILL' 1;">psychology</span>
</div>
<span class="text-[10px] font-bold uppercase tracking-widest text-primary">GPT-4o Analysis</span>
</div>
<!-- Step 4: Pending -->
<div class="flex flex-col items-center gap-2 opacity-30">
<div class="w-10 h-10 rounded-full flex items-center justify-center bg-surface-container-highest text-on-surface">
<span class="material-symbols-outlined text-[20px]">memory</span>
</div>
<span class="text-[10px] font-bold uppercase tracking-widest">Embedding</span>
</div>
<!-- Step 5: Pending -->
<div class="flex flex-col items-center gap-2 opacity-30">
<div class="w-10 h-10 rounded-full flex items-center justify-center bg-surface-container-highest text-on-surface">
<span class="material-symbols-outlined text-[20px]">storage</span>
</div>
<span class="text-[10px] font-bold uppercase tracking-widest">Saving</span>
</div>
</div>
<!-- Form Card -->
<div class="bg-surface-container rounded-3xl p-8 border border-outline-variant/5 shadow-2xl relative overflow-hidden">
<!-- Aesthetic Light Leak Background Decor -->
<div class="absolute -top-24 -right-24 w-64 h-64 bg-primary/10 blur-[100px] rounded-full pointer-events-none"></div>
<div class="absolute -bottom-24 -left-24 w-64 h-64 bg-secondary/5 blur-[100px] rounded-full pointer-events-none"></div>
<form class="grid grid-cols-1 md:grid-cols-2 gap-8 relative z-10">
<!-- Primary Input: URL -->
<div class="md:col-span-2">
<label class="block text-xs font-bold uppercase tracking-wider text-on-surface-variant mb-3">Video Source URL</label>
<div class="relative group">
<div class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
<span class="material-symbols-outlined text-primary group-focus-within:text-white transition-colors">link</span>
</div>
<input class="block w-full pl-12 pr-4 py-4 bg-surface-container-lowest border-none rounded-xl text-white placeholder:text-outline/40 focus:ring-2 focus:ring-primary/30 transition-all font-body" placeholder="https://tiktok.com/@creator/video/..." type="text"/>
</div>
</div>
<!-- Dropdown: Niche -->
<div>
<label class="block text-xs font-bold uppercase tracking-wider text-on-surface-variant mb-3">Target Niche</label>
<div class="relative">
<select class="block w-full pl-4 pr-10 py-4 bg-surface-container-lowest border-none rounded-xl text-white appearance-none focus:ring-2 focus:ring-primary/30 font-body">
<option>SaaS &amp; Tech</option>
<option>Personal Finance</option>
<option>Health &amp; Fitness</option>
<option>E-commerce</option>
<option>Agency Growth</option>
</select>
<div class="absolute inset-y-0 right-0 pr-4 flex items-center pointer-events-none">
<span class="material-symbols-outlined text-outline">expand_more</span>
</div>
</div>
</div>
<!-- Text Input: Sub-niche -->
<div>
<label class="block text-xs font-bold uppercase tracking-wider text-on-surface-variant mb-3">Sub-Niche focus</label>
<input class="block w-full px-4 py-4 bg-surface-container-lowest border-none rounded-xl text-white placeholder:text-outline/40 focus:ring-2 focus:ring-primary/30 font-body" placeholder="e.g., AI Automation for Real Estate" type="text"/>
</div>
<!-- Dropdown: Language -->
<div>
<label class="block text-xs font-bold uppercase tracking-wider text-on-surface-variant mb-3">Analysis Language</label>
<div class="relative">
<select class="block w-full pl-4 pr-10 py-4 bg-surface-container-lowest border-none rounded-xl text-white appearance-none focus:ring-2 focus:ring-primary/30 font-body">
<option>English (US/UK)</option>
<option>Spanish (ES/LATAM)</option>
<option>Portuguese (BR)</option>
<option>German</option>
</select>
<div class="absolute inset-y-0 right-0 pr-4 flex items-center pointer-events-none">
<span class="material-symbols-outlined text-outline">language</span>
</div>
</div>
</div>
<!-- Dropdown: Client Selector -->
<div>
<label class="block text-xs font-bold uppercase tracking-wider text-on-surface-variant mb-3">Assign to Client</label>
<div class="relative">
<select class="block w-full pl-4 pr-10 py-4 bg-surface-container-lowest border-none rounded-xl text-white appearance-none focus:ring-2 focus:ring-primary/30 font-body">
<option>Internal Portfolio</option>
<option>Nexus Marketing Inc.</option>
<option>Aria Aesthetics</option>
<option>Skyline Real Estate</option>
</select>
<div class="absolute inset-y-0 right-0 pr-4 flex items-center pointer-events-none">
<span class="material-symbols-outlined text-outline">group</span>
</div>
</div>
</div>
<!-- Optional: Manual Metrics -->
<div class="md:col-span-2 pt-6 border-t border-outline-variant/10">
<h3 class="text-sm font-bold text-white mb-6 flex items-center gap-2">
<span class="material-symbols-outlined text-secondary text-[18px]">show_chart</span>
Content Metrics (Optional)
</h3>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div>
<label class="block text-[10px] font-bold uppercase tracking-widest text-outline mb-2">Views</label>
<input class="block w-full px-4 py-3 bg-surface-container-lowest/50 border border-outline-variant/20 rounded-lg text-white placeholder:text-outline/40 focus:ring-1 focus:ring-secondary/40 text-sm" placeholder="240k" type="number"/>
</div>
<div>
<label class="block text-[10px] font-bold uppercase tracking-widest text-outline mb-2">Likes</label>
<input class="block w-full px-4 py-3 bg-surface-container-lowest/50 border border-outline-variant/20 rounded-lg text-white placeholder:text-outline/40 focus:ring-1 focus:ring-secondary/40 text-sm" placeholder="12k" type="number"/>
</div>
<div>
<label class="block text-[10px] font-bold uppercase tracking-widest text-outline mb-2">Shares</label>
<input class="block w-full px-4 py-3 bg-surface-container-lowest/50 border border-outline-variant/20 rounded-lg text-white placeholder:text-outline/40 focus:ring-1 focus:ring-secondary/40 text-sm" placeholder="850" type="number"/>
</div>
</div>
</div>
<!-- CTA Section -->
<div class="md:col-span-2 pt-6 flex flex-col sm:flex-row items-center justify-between gap-6">
<div class="flex items-center gap-2 text-on-surface-variant/60">
<span class="material-symbols-outlined text-sm">info</span>
<span class="text-xs">Processing typically takes 45-60 seconds.</span>
</div>
<div class="flex items-center gap-4 w-full sm:w-auto">
<button class="flex-1 sm:flex-none px-8 py-3.5 rounded-xl border border-outline-variant/30 text-white font-semibold text-sm hover:bg-white/5 transition-all" type="button">
Cancel
</button>
<button class="flex-1 sm:flex-none px-10 py-3.5 rounded-xl bg-gradient-to-br from-primary-container to-primary text-on-primary font-bold text-sm shadow-xl shadow-primary/20 hover:scale-105 active:scale-95 transition-all flex items-center justify-center gap-2" type="submit">
Start Analysis
<span class="material-symbols-outlined text-[18px]">bolt</span>
</button>
</div>
</div>
</form>
</div>
</div>
<!-- Contextual Insight (Bento-style snippet) -->
<div class="mt-12 grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="bg-surface-container-low p-6 rounded-2xl border border-outline-variant/5">
<span class="material-symbols-outlined text-primary mb-4">smart_toy</span>
<h4 class="text-white font-bold text-sm mb-2">Neural Transcription</h4>
<p class="text-xs text-on-surface-variant leading-relaxed">Multi-speaker recognition and punctuation recovery powered by Whisper v3 Large.</p>
</div>
<div class="bg-surface-container-low p-6 rounded-2xl border border-outline-variant/5">
<span class="material-symbols-outlined text-secondary mb-4">auto_awesome</span>
<h4 class="text-white font-bold text-sm mb-2">Hook Extraction</h4>
<p class="text-xs text-on-surface-variant leading-relaxed">Identifies the exact second the viral hook ends and the narrative retention begins.</p>
</div>
<div class="bg-surface-container-low p-6 rounded-2xl border border-outline-variant/5">
<span class="material-symbols-outlined text-tertiary mb-4">database</span>
<h4 class="text-white font-bold text-sm mb-2">Vector Storage</h4>
<p class="text-xs text-on-surface-variant leading-relaxed">Embeddings are saved in your private Knowledge Base for future script synthesis.</p>
</div>
</div>
</div>
</main>
</body></html>

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,
}
}
}
})

12
package.json Normal file
View File

@ -0,0 +1,12 @@
{
"name": "guiones-sistema",
"version": "1.0.0",
"private": true,
"type": "module",
"engines": { "node": "18.x" },
"dependencies": {
"@supabase/supabase-js": "^2.39.0",
"openai": "^4.28.0",
"zod": "^3.22.4"
}
}

10
vercel.json Normal file
View File

@ -0,0 +1,10 @@
{
"buildCommand": "cd frontend && npm install && npm run build",
"outputDirectory": "frontend/dist",
"functions": {
"api/**/*.js": { "maxDuration": 60 }
},
"rewrites": [
{ "source": "/:path*", "destination": "/index.html" }
]
}