feat: análisis extendido (10 campos nuevos) + generador de guiones con GPT-4o
Análisis extendido: - Nuevos campos: apertura_exacta, cierre_exacto, tecnica_retencion, momento_pico_seg - Copywriting: nivel_consciencia (Schwartz), objecion_principal, avatar_descripcion - Replicabilidad: ingredientes_clave, replicabilidad, ratio_emocion_logica - analizador.js: prompt extendido con metodología Schwartz + retención - validador.js: schema Zod actualizado con 6 nuevos enums - Migración SQL 05: ALTER TABLE + nuevos ENUMs + índices Generador de guiones: - generador.js: lib GPT-4o con temperatura 0.7 y contexto de patrones - server.js: endpoints POST /api/generar, GET /api/generados, GET /api/generados/:id - backend/api/generar.js + api/generar.js + api/generados.js: Vercel handlers - Migración SQL 06: tabla guiones_generados con score_estimado, variantes, notas - GenerateView.vue: formulario completo + preview del guion con copy al portapapeles - SideNavBar: nueva entrada "Generar" con ícono auto_fix_high - Router: ruta /generate → GenerateView - api.js: api.generar() + api.generados.listar/obtener() Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@ -25,6 +25,10 @@
|
||||
<span class="material-symbols-outlined text-[20px]">description</span>
|
||||
<span class="text-sm font-medium">Guiones</span>
|
||||
</router-link>
|
||||
<router-link to="/generate" 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]">auto_fix_high</span>
|
||||
<span class="text-sm font-medium">Generar</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">Configuración</span>
|
||||
|
||||
@ -11,13 +11,18 @@ async function request(path, options = {}) {
|
||||
}
|
||||
|
||||
export const api = {
|
||||
guiones: {
|
||||
listar: (params = {}) => request('/guiones?' + new URLSearchParams(params)),
|
||||
listarTodos: (params = {}) => request('/guiones?' + new URLSearchParams({ ...params, todos: '1' })),
|
||||
obtener: (id) => request(`/guiones/${id}`),
|
||||
guiones: {
|
||||
listar: (params = {}) => request('/guiones?' + new URLSearchParams(params)),
|
||||
listarTodos: (params = {}) => request('/guiones?' + new URLSearchParams({ ...params, todos: '1' })),
|
||||
obtener: (id) => request(`/guiones/${id}`),
|
||||
},
|
||||
analizar: (body) => request('/analizar', { method: 'POST', body: JSON.stringify(body) }),
|
||||
nichos: () => request('/nichos'),
|
||||
clientes: () => request('/clientes'),
|
||||
stats: () => request('/stats'),
|
||||
generados: {
|
||||
listar: (params = {}) => request('/generados?' + new URLSearchParams(params)),
|
||||
obtener: (id) => request(`/generados/${id}`),
|
||||
},
|
||||
analizar: (body) => request('/analizar', { method: 'POST', body: JSON.stringify(body) }),
|
||||
generar: (body) => request('/generar', { method: 'POST', body: JSON.stringify(body) }),
|
||||
nichos: () => request('/nichos'),
|
||||
clientes: () => request('/clientes'),
|
||||
stats: () => request('/stats'),
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import AnalysisCreateView from '../views/AnalysisCreateView.vue'
|
||||
import AnalysisDetailView from '../views/AnalysisDetailView.vue'
|
||||
import AnalysisListView from '../views/AnalysisListView.vue'
|
||||
import ScriptsView from '../views/ScriptsView.vue'
|
||||
import GenerateView from '../views/GenerateView.vue'
|
||||
import SettingsView from '../views/SettingsView.vue'
|
||||
|
||||
const routes = [
|
||||
@ -32,6 +33,11 @@ const routes = [
|
||||
name: 'Scripts',
|
||||
component: ScriptsView
|
||||
},
|
||||
{
|
||||
path: '/generate',
|
||||
name: 'Generate',
|
||||
component: GenerateView
|
||||
},
|
||||
{
|
||||
path: '/settings',
|
||||
name: 'Settings',
|
||||
|
||||
@ -8,13 +8,16 @@
|
||||
<!-- Encabezado -->
|
||||
<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> Volver al Panel
|
||||
<router-link to="/analysis" 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> Volver al Historial
|
||||
</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 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">{{ 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>
|
||||
<span v-if="guion.replicabilidad" :class="replicabilidadBadge(guion.replicabilidad)" class="px-3 py-1 text-[11px] font-black rounded uppercase tracking-widest">
|
||||
Replicabilidad {{ guion.replicabilidad }}
|
||||
</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 || 'Análisis sin título' }}
|
||||
@ -29,20 +32,18 @@
|
||||
<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="Ver video original" @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> Exportar Vector
|
||||
<span class="material-symbols-outlined text-sm">auto_fix_high</span> Generar Guion
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Cuadrícula principal -->
|
||||
<div class="grid grid-cols-1 xl:grid-cols-12 gap-8 relative z-10">
|
||||
<!-- Columna izquierda: Analíticas -->
|
||||
<!-- Columna izquierda -->
|
||||
<div class="xl:col-span-4 flex flex-col gap-6">
|
||||
<!-- Tarjeta de Puntaje -->
|
||||
|
||||
<!-- Puntaje -->
|
||||
<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">
|
||||
@ -51,25 +52,30 @@
|
||||
<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" />
|
||||
<circle cx="50" cy="50" r="45" fill="none" class="stroke-primary 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-6xl font-black font-headline text-white tracking-tighter">{{ 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">Índice Cialdini</p>
|
||||
<div class="grid grid-cols-3 gap-3 pt-6 border-t border-white/5">
|
||||
<div class="text-center">
|
||||
<p class="text-[9px] text-outline uppercase tracking-widest font-bold mb-1">Cialdini</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">Engagement Real</p>
|
||||
<p class="text-xl font-bold text-emerald-400">{{ guion.score_engagement ? (guion.score_engagement*1).toFixed(2) + '%' : 'N/A' }}</p>
|
||||
<div class="text-center">
|
||||
<p class="text-[9px] text-outline uppercase tracking-widest font-bold mb-1">Engagement</p>
|
||||
<p class="text-xl font-bold text-secondary">{{ guion.score_engagement ? (guion.score_engagement*1).toFixed(2) + '%' : 'N/A' }}</p>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<p class="text-[9px] text-outline uppercase tracking-widest font-bold mb-1">Intensidad</p>
|
||||
<p class="text-xl font-bold text-orange-400">{{ guion.intensidad_emocional || 0 }}<span class="text-sm text-outline">/10</span></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ganchos semánticos -->
|
||||
<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> Ganchos Semánticos
|
||||
@ -77,7 +83,7 @@
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<p class="text-[10px] text-outline uppercase tracking-widest font-bold mb-2">Estructura Narrativa</p>
|
||||
<div class="glass-panel p-3 rounded-lg border border-white/5 inline-block w-full">
|
||||
<div class="p-3 rounded-lg border border-white/5 bg-surface-container-low">
|
||||
<span class="text-sm font-bold text-on-surface">{{ guion.estructura_narrativa || 'No detectada' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -86,43 +92,123 @@
|
||||
<span>Gancho Principal</span>
|
||||
<span class="text-secondary">{{ guion.gancho_duracion_seg ? guion.gancho_duracion_seg + 's' : '' }}</span>
|
||||
</p>
|
||||
<div class="glass-panel p-4 rounded-xl border border-secondary/20 relative">
|
||||
<div class="p-4 rounded-xl border border-secondary/20 bg-surface-container-low 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 || 'Gancho Estándar' }}</p>
|
||||
<p class="text-sm text-white font-medium leading-relaxed italic">"{{ guion.gancho_texto || '—' }}"</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-[10px] text-outline uppercase tracking-widest font-bold mb-2">Técnica de Retención</p>
|
||||
<div class="p-3 rounded-lg border border-primary/20 bg-primary/5 flex items-center gap-2">
|
||||
<span class="material-symbols-outlined text-primary text-base">repeat</span>
|
||||
<span class="text-sm font-bold text-primary">{{ guion.tecnica_retencion || '—' }}</span>
|
||||
<span v-if="guion.momento_pico_seg" class="ml-auto text-[10px] text-outline font-bold">Pico: {{ guion.momento_pico_seg }}s</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Avatar y Consciencia -->
|
||||
<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-tertiary">person_search</span> Avatar & Copywriting
|
||||
</h3>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<p class="text-[10px] text-outline uppercase tracking-widest font-bold mb-2">Nivel de Consciencia</p>
|
||||
<div class="relative">
|
||||
<div class="flex gap-1">
|
||||
<div v-for="(nivel, i) in nivelesConciencia" :key="nivel.key"
|
||||
class="flex-1 h-2 rounded-full transition-all"
|
||||
:class="nivelConcienciaIndex >= i ? 'bg-primary' : 'bg-surface-container-highest'"
|
||||
></div>
|
||||
</div>
|
||||
<p class="text-xs font-bold text-primary mt-2">{{ guion.nivel_consciencia?.replace(/_/g,' ') || '—' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-[10px] text-outline uppercase tracking-widest font-bold mb-2">Avatar Objetivo</p>
|
||||
<p class="text-xs text-on-surface-variant leading-relaxed">{{ guion.avatar_descripcion || '—' }}</p>
|
||||
</div>
|
||||
<div v-if="guion.objecion_principal">
|
||||
<p class="text-[10px] text-outline uppercase tracking-widest font-bold mb-2">Objeción Principal</p>
|
||||
<div class="p-3 rounded-xl border border-red-500/20 bg-red-500/5">
|
||||
<p class="text-xs text-red-400 leading-relaxed italic">"{{ guion.objecion_principal }}"</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-[10px] text-outline uppercase tracking-widest font-bold mb-2">Balance Emoción / Lógica</p>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="material-symbols-outlined text-base" :class="ratioColor">{{ ratioIcon }}</span>
|
||||
<span class="text-sm font-bold" :class="ratioColor">{{ guion.ratio_emocion_logica || '—' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Columnas Central y Derecha -->
|
||||
<!-- Columna derecha -->
|
||||
<div class="xl:col-span-8 flex flex-col gap-6">
|
||||
|
||||
<!-- Patrón ganador -->
|
||||
<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">Síntesis del Patrón Ganador</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>
|
||||
|
||||
<!-- Apertura y Cierre -->
|
||||
<div v-if="guion.apertura_exacta || guion.cierre_exacto" class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="bg-surface-container p-5 rounded-2xl border border-secondary/20">
|
||||
<p class="text-[10px] text-secondary font-black uppercase tracking-widest mb-3 flex items-center gap-2">
|
||||
<span class="material-symbols-outlined text-sm">play_arrow</span> Apertura Exacta
|
||||
</p>
|
||||
<p class="text-sm text-white font-medium leading-relaxed italic">"{{ guion.apertura_exacta }}"</p>
|
||||
</div>
|
||||
<div class="bg-surface-container p-5 rounded-2xl border border-tertiary/20">
|
||||
<p class="text-[10px] text-tertiary font-black uppercase tracking-widest mb-3 flex items-center gap-2">
|
||||
<span class="material-symbols-outlined text-sm">stop</span> Cierre Exacto
|
||||
</p>
|
||||
<p class="text-sm text-white font-medium leading-relaxed italic">"{{ guion.cierre_exacto }}"</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ingredientes clave -->
|
||||
<div v-if="guion.ingredientes_clave?.length" 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-5 flex items-center gap-2">
|
||||
<span class="material-symbols-outlined text-yellow-400">key</span> Ingredientes Clave para Replicar
|
||||
</h3>
|
||||
<div class="space-y-2">
|
||||
<div v-for="(ing, i) in guion.ingredientes_clave" :key="i" class="flex items-start gap-3 p-3 rounded-xl bg-surface-container-low border border-white/5">
|
||||
<span class="w-5 h-5 rounded-full bg-yellow-400/10 border border-yellow-400/30 text-yellow-400 text-[10px] font-black flex items-center justify-center shrink-0 mt-0.5">{{ i + 1 }}</span>
|
||||
<p class="text-sm text-on-surface-variant leading-relaxed">{{ ing }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Emocional + Cialdini -->
|
||||
<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> Resonancia Emocional
|
||||
</h3>
|
||||
<div class="mb-6">
|
||||
<div class="flex justify-between text-xs font-bold uppercase tracking-widest mb-2">
|
||||
<span class="text-outline">Intensidad</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="Trigger Principal" :value="guion.trigger_emocional" highlight />
|
||||
<DataRow label="Sesgo Cognitivo" :value="guion.sesgo_cognitivo" />
|
||||
<DataRow label="Dolor / Placer" :value="guion.dolor_placer" highlight />
|
||||
</div>
|
||||
<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> Resonancia Emocional
|
||||
</h3>
|
||||
<div class="mb-5">
|
||||
<div class="flex justify-between text-xs font-bold uppercase tracking-widest mb-2">
|
||||
<span class="text-outline">Intensidad</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-3">
|
||||
<DataRow label="Trigger Principal" :value="guion.trigger_emocional" highlight />
|
||||
<DataRow label="Arco Emocional" :value="guion.arco_emocional" />
|
||||
<DataRow label="Sesgo Cognitivo" :value="guion.sesgo_cognitivo" />
|
||||
<DataRow label="Dolor / Placer" :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">
|
||||
@ -141,6 +227,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Neuromarketing + Entrega -->
|
||||
<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">
|
||||
@ -153,6 +240,7 @@
|
||||
<DataRow label="Lenguaje Sensorial" :value="guion.lenguaje_sensorial" type="boolean" />
|
||||
<DataRow label="Contraste Narrativo" :value="guion.contraste_narrativo" type="boolean" />
|
||||
<DataRow label="Efecto Novedad" :value="guion.efecto_novedad" type="boolean" />
|
||||
<DataRow label="Micro Compromisos" :value="guion.micro_compromisos" type="boolean" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -160,29 +248,48 @@
|
||||
<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> Entrega y Alcance
|
||||
</h3>
|
||||
<div class="space-y-4 mb-6">
|
||||
<DataRow label="Tono" :value="guion.tono" highlight/>
|
||||
<DataRow label="Perspectiva" :value="guion.persona_narradora" highlight/>
|
||||
<DataRow label="Especificidad" :value="guion.nivel_especificidad" highlight/>
|
||||
<div>
|
||||
<p class="text-[10px] text-outline uppercase tracking-widest font-bold mb-2">Palabras Clave Extraídas</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 class="space-y-3 mb-5">
|
||||
<DataRow label="Tono" :value="guion.tono" highlight />
|
||||
<DataRow label="Perspectiva" :value="guion.persona_narradora" highlight />
|
||||
<DataRow label="Especificidad" :value="guion.nivel_especificidad" highlight />
|
||||
<DataRow label="Velocidad" :value="guion.velocidad_locucion" />
|
||||
<DataRow label="CTA" :value="guion.cta_tipo" highlight />
|
||||
</div>
|
||||
<div v-if="guion.cta_texto" class="p-3 rounded-xl bg-surface-container-low border border-white/5">
|
||||
<p class="text-[9px] text-outline font-black uppercase tracking-widest mb-1">Texto del CTA</p>
|
||||
<p class="text-xs text-on-surface-variant italic">"{{ guion.cta_texto }}"</p>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<p class="text-[10px] text-outline uppercase tracking-widest font-bold mb-2">Palabras Clave</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>
|
||||
|
||||
<!-- Visor de Transcripción -->
|
||||
<!-- Promesa + Conflicto -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="bg-surface-container p-5 rounded-2xl border border-outline-variant/10">
|
||||
<p class="text-[10px] text-outline uppercase tracking-widest font-bold mb-2">Promesa Explícita</p>
|
||||
<p class="text-sm text-white leading-relaxed">{{ guion.promesa_explicita || '—' }}</p>
|
||||
</div>
|
||||
<div class="bg-surface-container p-5 rounded-2xl border border-outline-variant/10">
|
||||
<p class="text-[10px] text-outline uppercase tracking-widest font-bold mb-2">Conflicto → Resolución</p>
|
||||
<p class="text-xs text-on-surface-variant leading-relaxed">{{ guion.conflicto_central || '—' }}</p>
|
||||
<p v-if="guion.resolucion" class="text-xs text-secondary mt-2 leading-relaxed">→ {{ guion.resolucion }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Transcripción -->
|
||||
<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> Transcripción Completa
|
||||
</h3>
|
||||
<button @click="showTranscript = !showTranscript" class="text-xs font-bold uppercase tracking-widest text-primary hover:text-white transition-colors">
|
||||
{{ showTranscript ? 'Colapsar' : 'Expandir' }}
|
||||
</button>
|
||||
<h3 class="text-sm font-headline font-bold text-white flex items-center gap-2">
|
||||
<span class="material-symbols-outlined text-outline">notes</span> Transcripción Completa
|
||||
</h3>
|
||||
<button @click="showTranscript = !showTranscript" class="text-xs font-bold uppercase tracking-widest text-primary hover:text-white transition-colors">
|
||||
{{ showTranscript ? 'Colapsar' : 'Expandir' }}
|
||||
</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>
|
||||
@ -191,13 +298,14 @@
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { api } from '../lib/api.js'
|
||||
import CialdiniItem from '../components/CialdiniItem.vue'
|
||||
@ -208,18 +316,49 @@ const guion = ref(null)
|
||||
const cargando = ref(true)
|
||||
const showTranscript = ref(false)
|
||||
|
||||
const nivelesConciencia = [
|
||||
{ key: 'inconsciente' },
|
||||
{ key: 'problema_consciente' },
|
||||
{ key: 'solucion_consciente' },
|
||||
{ key: 'producto_consciente' },
|
||||
{ key: 'mas_consciente' },
|
||||
]
|
||||
|
||||
const nivelConcienciaIndex = computed(() => {
|
||||
if (!guion.value?.nivel_consciencia) return -1
|
||||
return nivelesConciencia.findIndex(n => n.key === guion.value.nivel_consciencia)
|
||||
})
|
||||
|
||||
const ratioColor = computed(() => {
|
||||
const map = { emocional: 'text-orange-400', logico: 'text-blue-400', equilibrado: 'text-secondary' }
|
||||
return map[guion.value?.ratio_emocion_logica] || 'text-outline'
|
||||
})
|
||||
|
||||
const ratioIcon = computed(() => {
|
||||
const map = { emocional: 'favorite', logico: 'psychology', equilibrado: 'balance' }
|
||||
return map[guion.value?.ratio_emocion_logica] || 'help'
|
||||
})
|
||||
|
||||
function openUrl(url) {
|
||||
if(url) window.open(url, '_blank')
|
||||
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'
|
||||
reels: 'bg-fuchsia-500/20 text-fuchsia-400',
|
||||
shorts: 'bg-red-600/20 text-red-500',
|
||||
}[p] ?? 'bg-white/5 text-on-surface-variant'
|
||||
}
|
||||
|
||||
function replicabilidadBadge(r) {
|
||||
return {
|
||||
alta: 'bg-secondary/10 text-secondary border border-secondary/20',
|
||||
media: 'bg-yellow-500/10 text-yellow-400 border border-yellow-500/20',
|
||||
baja: 'bg-red-500/10 text-red-400 border border-red-500/20',
|
||||
}[r] ?? 'bg-white/5 text-outline'
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
guion.value = await api.guiones.obtener(route.params.id)
|
||||
|
||||
274
frontend/src/views/GenerateView.vue
Normal file
274
frontend/src/views/GenerateView.vue
Normal file
@ -0,0 +1,274 @@
|
||||
<template>
|
||||
<div class="max-w-7xl mx-auto flex flex-col gap-10">
|
||||
<!-- Encabezado -->
|
||||
<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">Generador de Guiones</h1>
|
||||
<p class="text-secondary text-sm font-bold flex items-center gap-2 tracking-widest uppercase">
|
||||
<span class="material-symbols-outlined text-sm">auto_fix_high</span>
|
||||
IA que aprende de tus patrones de alto rendimiento
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="grid grid-cols-1 xl:grid-cols-12 gap-10">
|
||||
<!-- Formulario -->
|
||||
<div class="xl:col-span-7 flex flex-col gap-8">
|
||||
|
||||
<!-- Paso 1: Contexto -->
|
||||
<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">01</span>
|
||||
<h2 class="text-xl font-headline font-extrabold text-white tracking-tight">Contexto del Guion</h2>
|
||||
</div>
|
||||
<div class="bg-surface-container p-8 rounded-3xl border border-white/5 shadow-2xl space-y-6">
|
||||
<div class="grid grid-cols-2 gap-6">
|
||||
<div class="space-y-2">
|
||||
<label class="text-[10px] font-black uppercase tracking-widest text-outline">Nicho</label>
|
||||
<input v-model="form.niche" list="nichos-gen" placeholder="ej. fitness, finanzas..." class="w-full bg-surface-container-low border border-white/10 rounded-2xl px-4 py-4 text-sm text-white placeholder:text-outline/40 focus:ring-2 focus:ring-secondary/40 font-black uppercase tracking-widest" :disabled="generando" />
|
||||
<datalist id="nichos-gen">
|
||||
<option v-for="n in nichos" :key="n" :value="n" />
|
||||
</datalist>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<label class="text-[10px] font-black uppercase tracking-widest text-outline">Plataforma</label>
|
||||
<select v-model="form.plataforma" class="w-full bg-surface-container-low border border-white/10 rounded-2xl px-4 py-4 text-sm text-on-surface appearance-none focus:ring-2 focus:ring-secondary/40" :disabled="generando">
|
||||
<option value="tiktok">TikTok</option>
|
||||
<option value="reels">Instagram Reels</option>
|
||||
<option value="shorts">YouTube Shorts</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<label class="text-[10px] font-black uppercase tracking-widest text-outline">Tema del Video</label>
|
||||
<input v-model="form.tema" type="text" placeholder="ej. Cómo perder 5kg en 30 días sin pasar hambre" class="w-full bg-surface-container-low border border-white/10 rounded-2xl px-4 py-4 text-sm text-white placeholder:text-outline/40 focus:ring-2 focus:ring-secondary/40" :disabled="generando" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<label class="text-[10px] font-black uppercase tracking-widest text-outline">Audiencia Objetivo</label>
|
||||
<input v-model="form.audiencia" type="text" placeholder="ej. Mujeres de 25-40 años con poco tiempo para el gimnasio" class="w-full bg-surface-container-low border border-white/10 rounded-2xl px-4 py-4 text-sm text-white placeholder:text-outline/40 focus:ring-2 focus:ring-secondary/40" :disabled="generando" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Paso 2: Parámetros -->
|
||||
<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">02</span>
|
||||
<h2 class="text-xl font-headline font-extrabold text-white tracking-tight">Parámetros de Generación</h2>
|
||||
</div>
|
||||
<div class="bg-surface-container p-8 rounded-3xl border border-white/5 shadow-2xl space-y-6">
|
||||
<div class="grid grid-cols-2 gap-6">
|
||||
<div class="space-y-2">
|
||||
<label class="text-[10px] font-black uppercase tracking-widest text-outline">Estructura Narrativa</label>
|
||||
<select v-model="form.estructura" class="w-full bg-surface-container-low border border-white/10 rounded-2xl px-4 py-4 text-sm text-on-surface appearance-none focus:ring-2 focus:ring-primary/40" :disabled="generando">
|
||||
<option value="AIDA">AIDA (Atención → Interés → Deseo → Acción)</option>
|
||||
<option value="PAS">PAS (Problema → Agitación → Solución)</option>
|
||||
<option value="hero_journey">Hero's Journey</option>
|
||||
<option value="storybrand">StoryBrand</option>
|
||||
<option value="antes_despues">Antes / Después</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<label class="text-[10px] font-black uppercase tracking-widest text-outline">Objetivo</label>
|
||||
<select v-model="form.objetivo" class="w-full bg-surface-container-low border border-white/10 rounded-2xl px-4 py-4 text-sm text-on-surface appearance-none focus:ring-2 focus:ring-primary/40" :disabled="generando">
|
||||
<option value="engagement">Engagement (likes, comentarios)</option>
|
||||
<option value="awareness">Awareness (alcance)</option>
|
||||
<option value="conversion">Conversión (ventas, leads)</option>
|
||||
<option value="educacion">Educación</option>
|
||||
<option value="entretenimiento">Entretenimiento</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-6">
|
||||
<div class="space-y-2">
|
||||
<label class="text-[10px] font-black uppercase tracking-widest text-outline">Tono</label>
|
||||
<select v-model="form.tono" class="w-full bg-surface-container-low border border-white/10 rounded-2xl px-4 py-4 text-sm text-on-surface appearance-none focus:ring-2 focus:ring-primary/40" :disabled="generando">
|
||||
<option value="educativo">Educativo</option>
|
||||
<option value="entretenimiento">Entretenimiento</option>
|
||||
<option value="inspiracional">Inspiracional</option>
|
||||
<option value="controversial">Controversial</option>
|
||||
<option value="informativo">Informativo</option>
|
||||
<option value="humoristico">Humorístico</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<label class="text-[10px] font-black uppercase tracking-widest text-outline">Duración Objetivo (seg)</label>
|
||||
<input v-model.number="form.duracion_objetivo" type="number" min="15" max="180" class="w-full bg-surface-container-low border border-white/10 rounded-2xl px-4 py-4 text-sm text-on-surface font-bold text-center focus:ring-2 focus:ring-primary/40" :disabled="generando" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<label class="text-[10px] font-black uppercase tracking-widest text-outline">Instrucciones adicionales (opcional)</label>
|
||||
<textarea v-model="form.instrucciones_extra" rows="3" placeholder="ej. Incluir una estadística de estudio, no mencionar competidores, usar lenguaje informal..." class="w-full bg-surface-container-low border border-white/10 rounded-2xl px-4 py-4 text-sm text-white placeholder:text-outline/40 focus:ring-2 focus:ring-primary/40 resize-none" :disabled="generando"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Botón -->
|
||||
<button
|
||||
@click="generar"
|
||||
:disabled="generando || !form.niche || !form.tema || !form.audiencia"
|
||||
class="w-full py-5 bg-gradient-to-br from-secondary/80 to-secondary text-on-secondary font-headline font-black rounded-2xl shadow-xl shadow-secondary/20 hover:scale-[1.02] active:scale-95 transition-all text-base uppercase tracking-widest flex items-center justify-center gap-3 disabled:opacity-40 disabled:scale-100"
|
||||
>
|
||||
<span class="material-symbols-outlined text-xl" :class="generando ? 'animate-spin' : ''">{{ generando ? 'hourglass_top' : 'auto_fix_high' }}</span>
|
||||
{{ generando ? 'Generando con GPT-4o...' : 'Generar Guion' }}
|
||||
</button>
|
||||
|
||||
<p v-if="error" class="p-4 rounded-xl bg-red-500/10 border border-red-500/30 text-red-400 text-sm">{{ error }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Panel derecho: Preview + Info -->
|
||||
<div class="xl:col-span-5 flex flex-col gap-6">
|
||||
|
||||
<!-- Resultado -->
|
||||
<div v-if="resultado" class="flex flex-col gap-4">
|
||||
<!-- Score -->
|
||||
<div class="bg-surface-container p-6 rounded-3xl border border-secondary/20 shadow-xl relative overflow-hidden">
|
||||
<div class="absolute -top-8 -right-8 w-32 h-32 bg-secondary/10 blur-3xl rounded-full"></div>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-sm font-headline font-black text-white uppercase tracking-widest">{{ resultado.guion.titulo_sugerido }}</h3>
|
||||
<div class="flex items-center gap-2 px-3 py-1.5 bg-secondary/10 rounded-full border border-secondary/20">
|
||||
<span class="material-symbols-outlined text-secondary text-sm" style="font-variation-settings:'FILL' 1;">bolt</span>
|
||||
<span class="text-sm font-black text-secondary">{{ resultado.guion.score_estimado }}/100</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2 mb-4">
|
||||
<span v-for="t in resultado.guion.tecnicas_aplicadas" :key="t" class="text-[9px] font-black uppercase tracking-widest px-2 py-1 bg-surface-container-low border border-white/5 rounded text-outline">{{ t }}</span>
|
||||
</div>
|
||||
<p class="text-[10px] text-secondary font-black uppercase tracking-widest mb-1">Duración estimada</p>
|
||||
<p class="text-2xl font-black text-white font-headline">{{ resultado.guion.duracion_estimada_seg }}s</p>
|
||||
</div>
|
||||
|
||||
<!-- Guion completo -->
|
||||
<div class="bg-surface-container rounded-3xl border border-outline-variant/10 shadow-xl overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-white/5 bg-surface-container-high/50 flex items-center justify-between">
|
||||
<h3 class="text-xs font-black text-white uppercase tracking-widest">Guion Completo</h3>
|
||||
<button @click="copiarGuion" class="text-xs font-bold text-primary hover:text-white transition-colors flex items-center gap-1">
|
||||
<span class="material-symbols-outlined text-sm">{{ copiado ? 'check' : 'content_copy' }}</span>
|
||||
{{ copiado ? 'Copiado' : 'Copiar' }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="p-6 space-y-4">
|
||||
<div>
|
||||
<p class="text-[9px] text-secondary font-black uppercase tracking-widest mb-2">Gancho</p>
|
||||
<p class="text-sm text-white leading-relaxed font-medium">{{ resultado.guion.gancho }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-[9px] text-primary font-black uppercase tracking-widest mb-2">Desarrollo</p>
|
||||
<p class="text-sm text-on-surface-variant leading-relaxed whitespace-pre-wrap">{{ resultado.guion.desarrollo }}</p>
|
||||
</div>
|
||||
<div v-if="resultado.guion.cta">
|
||||
<p class="text-[9px] text-tertiary font-black uppercase tracking-widest mb-2">Call to Action</p>
|
||||
<p class="text-sm text-white leading-relaxed font-medium">{{ resultado.guion.cta }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Variantes del gancho -->
|
||||
<div v-if="resultado.guion.variantes_gancho?.length" class="bg-surface-container p-6 rounded-3xl border border-outline-variant/10">
|
||||
<h3 class="text-xs font-black text-white uppercase tracking-widest mb-4 flex items-center gap-2">
|
||||
<span class="material-symbols-outlined text-outline text-base">shuffle</span> Variantes del Gancho
|
||||
</h3>
|
||||
<div class="space-y-3">
|
||||
<div v-for="(v, i) in resultado.guion.variantes_gancho" :key="i" class="p-3 rounded-xl bg-surface-container-low border border-white/5">
|
||||
<span class="text-[9px] text-outline font-black uppercase tracking-widest mr-2">V{{ i + 1 }}</span>
|
||||
<span class="text-sm text-on-surface-variant italic">"{{ v }}"</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notas de producción -->
|
||||
<div v-if="resultado.guion.notas_produccion" class="bg-surface-container p-6 rounded-3xl border border-yellow-500/20">
|
||||
<h3 class="text-xs font-black text-yellow-400 uppercase tracking-widest mb-3 flex items-center gap-2">
|
||||
<span class="material-symbols-outlined text-base">videocam</span> Notas de Producción
|
||||
</h3>
|
||||
<p class="text-sm text-on-surface-variant leading-relaxed">{{ resultado.guion.notas_produccion }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Nuevo guion -->
|
||||
<button @click="resultado = null; form.tema = ''; form.instrucciones_extra = ''" class="w-full py-3 bg-surface-container border border-white/5 text-outline font-bold rounded-xl text-sm hover:text-white hover:border-white/10 transition-colors">
|
||||
Generar otro guion
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Estado vacío / Info -->
|
||||
<div v-else class="bg-surface-container rounded-3xl border border-outline-variant/10 p-8 flex flex-col gap-6 sticky top-24">
|
||||
<div>
|
||||
<h3 class="text-sm font-headline font-black text-white uppercase tracking-widest mb-2">Cómo funciona</h3>
|
||||
<p class="text-xs text-outline/70 leading-relaxed">El generador analiza los guiones de mayor rendimiento de tu base de datos y aplica sus patrones estructurales, técnicas de retención y triggers emocionales a tu nuevo contenido.</p>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<div v-for="paso in pasoInfo" :key="paso.label" class="flex items-start gap-3">
|
||||
<div class="w-7 h-7 rounded-full bg-surface-container-low border border-white/5 flex items-center justify-center shrink-0">
|
||||
<span class="material-symbols-outlined text-sm text-primary">{{ paso.icon }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-black text-white">{{ paso.label }}</p>
|
||||
<p class="text-[10px] text-outline/60">{{ paso.desc }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pt-4 border-t border-white/5">
|
||||
<p class="text-[10px] text-outline/50 italic">Tiempo estimado: 5-10 segundos · Modelo: GPT-4o</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { api } from '../lib/api.js'
|
||||
|
||||
const generando = ref(false)
|
||||
const error = ref(null)
|
||||
const resultado = ref(null)
|
||||
const copiado = ref(false)
|
||||
const nichos = ref([])
|
||||
|
||||
const form = ref({
|
||||
niche: '',
|
||||
tema: '',
|
||||
audiencia: '',
|
||||
plataforma: 'tiktok',
|
||||
estructura: 'AIDA',
|
||||
objetivo: 'engagement',
|
||||
tono: 'educativo',
|
||||
duracion_objetivo: 60,
|
||||
instrucciones_extra: '',
|
||||
})
|
||||
|
||||
const pasoInfo = [
|
||||
{ icon: 'search', label: 'Busca patrones', desc: 'Selecciona los mejores guiones del niche en tu biblioteca' },
|
||||
{ icon: 'psychology', label: 'Extrae técnicas', desc: 'Identifica estructura, triggers y principios Cialdini activos' },
|
||||
{ icon: 'auto_fix_high', label: 'Genera el guion', desc: 'GPT-4o crea contenido original aplicando los patrones' },
|
||||
{ icon: 'save', label: 'Guarda en biblioteca', desc: 'El guion queda guardado con su score y notas de producción' },
|
||||
]
|
||||
|
||||
async function generar() {
|
||||
if (!form.value.niche || !form.value.tema || !form.value.audiencia) return
|
||||
generando.value = true
|
||||
error.value = null
|
||||
resultado.value = null
|
||||
|
||||
try {
|
||||
resultado.value = await api.generar(form.value)
|
||||
} catch (e) {
|
||||
error.value = e.message
|
||||
} finally {
|
||||
generando.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function copiarGuion() {
|
||||
if (!resultado.value?.guion?.guion_completo) return
|
||||
await navigator.clipboard.writeText(resultado.value.guion.guion_completo)
|
||||
copiado.value = true
|
||||
setTimeout(() => { copiado.value = false }, 2000)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try { nichos.value = await api.nichos() } catch {}
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user