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:
27
.gitignore
vendored
Normal file
27
.gitignore
vendored
Normal 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
70
README.md
Normal 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
1
api/analizar.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default } from '../backend/api/analizar.js'
|
||||||
14
api/clientes.js
Normal file
14
api/clientes.js
Normal 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
30
api/guiones.js
Normal 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
16
api/guiones/[id].js
Normal 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
15
api/nichos.js
Normal 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
12
api/stats.js
Normal 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
189
backend/api/analizar.js
Normal 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
106
backend/lib/analizador.js
Normal 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
40
backend/lib/embeddings.js
Normal 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
54
backend/lib/extractor.js
Normal 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
7
backend/lib/supabase.js
Normal 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
|
||||||
|
)
|
||||||
36
backend/lib/transcriptor.js
Normal file
36
backend/lib/transcriptor.js
Normal 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
91
backend/lib/validador.js
Normal 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
1289
backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
backend/package.json
Normal file
20
backend/package.json
Normal 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
190
backend/server.js
Normal 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
7
backend/vercel.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"functions": {
|
||||||
|
"api/*.js": {
|
||||||
|
"maxDuration": 60
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
189
database/migrations/01_schema.sql
Normal file
189
database/migrations/01_schema.sql
Normal 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);
|
||||||
238
database/migrations/02_funciones.sql
Normal file
238
database/migrations/02_funciones.sql
Normal 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;
|
||||||
80
database/migrations/03_rls.sql
Normal file
80
database/migrations/03_rls.sql
Normal 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.
|
||||||
|
-- ============================================================
|
||||||
74
database/seeds/04_datos_prueba.sql
Normal file
74
database/seeds/04_datos_prueba.sql
Normal 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)
|
||||||
455
docs/reference_design/analysis_detail.html
Normal file
455
docs/reference_design/analysis_detail.html
Normal 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&family=Manrope:wght@500;600;700;800&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"/>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&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 & 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>
|
||||||
393
docs/reference_design/dashboard.html
Normal file
393
docs/reference_design/dashboard.html
Normal 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&family=Inter:wght@400;500;600&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"/>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&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>
|
||||||
14
docs/reference_design/index.html
Normal file
14
docs/reference_design/index.html
Normal 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>
|
||||||
313
docs/reference_design/new_analysis.html
Normal file
313
docs/reference_design/new_analysis.html
Normal 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&family=Manrope:wght@500;600;700;800&display=swap" rel="stylesheet"/>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght@100..700,0..1&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"/>
|
||||||
|
<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 & Tech</option>
|
||||||
|
<option>Personal Finance</option>
|
||||||
|
<option>Health & 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
14
frontend/index.html
Normal 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
2488
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
25
frontend/package.json
Normal file
25
frontend/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
frontend/postcss.config.js
Normal file
6
frontend/postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
31
frontend/src/App.vue
Normal file
31
frontend/src/App.vue
Normal 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>
|
||||||
15
frontend/src/components/CialdiniItem.vue
Normal file
15
frontend/src/components/CialdiniItem.vue
Normal 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>
|
||||||
30
frontend/src/components/DataRow.vue
Normal file
30
frontend/src/components/DataRow.vue
Normal 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>
|
||||||
45
frontend/src/components/SideNavBar.vue
Normal file
45
frontend/src/components/SideNavBar.vue
Normal 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>
|
||||||
26
frontend/src/components/TopAppBar.vue
Normal file
26
frontend/src/components/TopAppBar.vue
Normal 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
22
frontend/src/lib/api.js
Normal 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
7
frontend/src/main.js
Normal 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')
|
||||||
43
frontend/src/router/index.js
Normal file
43
frontend/src/router/index.js
Normal 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
35
frontend/src/style.css
Normal 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); }
|
||||||
|
}
|
||||||
238
frontend/src/views/AnalysisCreateView.vue
Normal file
238
frontend/src/views/AnalysisCreateView.vue
Normal 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>
|
||||||
231
frontend/src/views/AnalysisDetailView.vue
Normal file
231
frontend/src/views/AnalysisDetailView.vue
Normal 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>
|
||||||
222
frontend/src/views/DashboardView.vue
Normal file
222
frontend/src/views/DashboardView.vue
Normal 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>
|
||||||
67
frontend/tailwind.config.js
Normal file
67
frontend/tailwind.config.js
Normal 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
15
frontend/vite.config.js
Normal 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
12
package.json
Normal 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
10
vercel.json
Normal 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" }
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user