feat: contexto de video, análisis extendido y métricas sociales
- Campo "Contexto del Video" en formulario de análisis (Paso 03) → se pasa a GPT-4o para enriquecer el análisis - 4 nuevos campos de diagnóstico: fortalezas, debilidades, sugerencias_mejora, hashtags_sugeridos (click para copiar) - Vista de detalle: card de métricas sociales (vistas/likes/compartidos con engagement rate calculado) - Muestra contexto original ingresado por el usuario - Migración SQL 07: 5 nuevas columnas en tabla guiones - validador.js: 4 nuevos campos en schema Zod - server.js + api/analizar.js: acepta y guarda contexto_video Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@ -33,6 +33,7 @@ export default async function handler(req, res) {
|
|||||||
likes = null,
|
likes = null,
|
||||||
compartidos = null,
|
compartidos = null,
|
||||||
fecha_publicacion = null,
|
fecha_publicacion = null,
|
||||||
|
contexto_video = '',
|
||||||
} = req.body
|
} = req.body
|
||||||
|
|
||||||
if (!url) return res.status(400).json({ error: 'El campo "url" es requerido' })
|
if (!url) return res.status(400).json({ error: 'El campo "url" es requerido' })
|
||||||
@ -56,7 +57,7 @@ export default async function handler(req, res) {
|
|||||||
|
|
||||||
// ── PASO 3: Analizar con GPT-4o ───────────────────────
|
// ── PASO 3: Analizar con GPT-4o ───────────────────────
|
||||||
paso = 'analisis'
|
paso = 'analisis'
|
||||||
const analisisRaw = await analizarTranscript(transcript, niche, plataforma, duracion)
|
const analisisRaw = await analizarTranscript(transcript, niche, plataforma, duracion, contexto_video)
|
||||||
|
|
||||||
// ── PASO 4: Validar con Zod ───────────────────────────
|
// ── PASO 4: Validar con Zod ───────────────────────────
|
||||||
paso = 'validacion'
|
paso = 'validacion'
|
||||||
@ -136,6 +137,13 @@ export default async function handler(req, res) {
|
|||||||
persona_narradora: analisis.persona_narradora,
|
persona_narradora: analisis.persona_narradora,
|
||||||
promesa_explicita: analisis.promesa_explicita,
|
promesa_explicita: analisis.promesa_explicita,
|
||||||
nivel_especificidad: analisis.nivel_especificidad,
|
nivel_especificidad: analisis.nivel_especificidad,
|
||||||
|
contexto_video: contexto_video || null,
|
||||||
|
|
||||||
|
// Diagnóstico
|
||||||
|
fortalezas: analisis.fortalezas,
|
||||||
|
debilidades: analisis.debilidades,
|
||||||
|
sugerencias_mejora: analisis.sugerencias_mejora,
|
||||||
|
hashtags_sugeridos: analisis.hashtags_sugeridos,
|
||||||
|
|
||||||
// Métricas (score_engagement lo calcula el trigger de Supabase)
|
// Métricas (score_engagement lo calcula el trigger de Supabase)
|
||||||
score_virabilidad: analisis.score_virabilidad,
|
score_virabilidad: analisis.score_virabilidad,
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
// ANALIZADOR — GPT-4o
|
// ANALIZADOR — GPT-4o
|
||||||
// Prompt maestro multidisciplinario: Storytelling + Cialdini
|
// Prompt maestro multidisciplinario: Storytelling + Cialdini
|
||||||
// + Neuropublicidad + Copywriting → JSON de 45 campos
|
// + Neuropublicidad + Copywriting → JSON de 49 campos
|
||||||
// ============================================================
|
// ============================================================
|
||||||
import OpenAI from 'openai'
|
import OpenAI from 'openai'
|
||||||
|
|
||||||
@ -22,12 +22,17 @@ SOLO devuelve el JSON, sin texto adicional, sin markdown, sin explicaciones.`
|
|||||||
* @param {string} niche Nicho del video (ej: "fitness", "finanzas")
|
* @param {string} niche Nicho del video (ej: "fitness", "finanzas")
|
||||||
* @param {string} plataforma tiktok | reels | shorts
|
* @param {string} plataforma tiktok | reels | shorts
|
||||||
* @param {number} duracion Duración en segundos
|
* @param {number} duracion Duración en segundos
|
||||||
|
* @param {string} contextoVideo Contexto adicional opcional sobre el video
|
||||||
* @returns {object} JSON con todos los campos de análisis
|
* @returns {object} JSON con todos los campos de análisis
|
||||||
*/
|
*/
|
||||||
export async function analizarTranscript(transcript, niche, plataforma, duracion) {
|
export async function analizarTranscript(transcript, niche, plataforma, duracion, contextoVideo = '') {
|
||||||
|
const bloqueContexto = contextoVideo
|
||||||
|
? `CONTEXTO ADICIONAL DEL VIDEO (úsalo para enriquecer el análisis):\n"""\n${contextoVideo}\n"""\n\n`
|
||||||
|
: ''
|
||||||
|
|
||||||
const promptUsuario = `Analiza este video de ${plataforma} de ${duracion} segundos del nicho "${niche}".
|
const promptUsuario = `Analiza este video de ${plataforma} de ${duracion} segundos del nicho "${niche}".
|
||||||
|
|
||||||
TRANSCRIPCIÓN:
|
${bloqueContexto}TRANSCRIPCIÓN:
|
||||||
"""
|
"""
|
||||||
${transcript}
|
${transcript}
|
||||||
"""
|
"""
|
||||||
@ -88,6 +93,11 @@ Devuelve EXACTAMENTE este JSON con los valores que correspondan:
|
|||||||
"ingredientes_clave": ["<elemento 1 que NO puede faltar si se replica este guion>", "<elemento 2>", "<elemento 3>"],
|
"ingredientes_clave": ["<elemento 1 que NO puede faltar si se replica este guion>", "<elemento 2>", "<elemento 3>"],
|
||||||
"replicabilidad": "<alta|media|baja>",
|
"replicabilidad": "<alta|media|baja>",
|
||||||
|
|
||||||
|
"fortalezas": ["<fortaleza 1 del video: qué hace especialmente bien>", "<fortaleza 2>", "<fortaleza 3>"],
|
||||||
|
"debilidades": ["<debilidad o área de mejora 1>", "<debilidad 2>"],
|
||||||
|
"sugerencias_mejora": ["<sugerencia accionable concreta 1 para mejorar el video>", "<sugerencia 2>", "<sugerencia 3>"],
|
||||||
|
"hashtags_sugeridos": ["<hashtag1>", "<hashtag2>", "<hashtag3>", "<hashtag4>", "<hashtag5>", "<hashtag6>", "<hashtag7>"],
|
||||||
|
|
||||||
"score_virabilidad": <número entero del 1 al 100>,
|
"score_virabilidad": <número entero del 1 al 100>,
|
||||||
"resumen_patron": "<párrafo de 3-4 oraciones describiendo el patrón ganador de este video: qué hace, por qué funciona psicológicamente y cómo se puede replicar>"
|
"resumen_patron": "<párrafo de 3-4 oraciones describiendo el patrón ganador de este video: qué hace, por qué funciona psicológicamente y cómo se puede replicar>"
|
||||||
}`
|
}`
|
||||||
|
|||||||
@ -81,6 +81,12 @@ export const AnalisisSchema = z.object({
|
|||||||
ingredientes_clave: z.array(z.string()).min(1).max(7),
|
ingredientes_clave: z.array(z.string()).min(1).max(7),
|
||||||
replicabilidad: ReplicabilidadEnum,
|
replicabilidad: ReplicabilidadEnum,
|
||||||
|
|
||||||
|
// Diagnóstico y mejora
|
||||||
|
fortalezas: z.array(z.string()).min(1).max(5),
|
||||||
|
debilidades: z.array(z.string()).min(1).max(5),
|
||||||
|
sugerencias_mejora: z.array(z.string()).min(1).max(5),
|
||||||
|
hashtags_sugeridos: z.array(z.string()).min(1).max(10),
|
||||||
|
|
||||||
// Métricas
|
// Métricas
|
||||||
score_virabilidad: z.number().int().min(1).max(100),
|
score_virabilidad: z.number().int().min(1).max(100),
|
||||||
resumen_patron: z.string().min(10).max(1500),
|
resumen_patron: z.string().min(10).max(1500),
|
||||||
|
|||||||
@ -111,6 +111,7 @@ app.post('/api/analizar', async (req, res) => {
|
|||||||
competidor_referente = false,
|
competidor_referente = false,
|
||||||
vistas = null, likes = null, compartidos = null,
|
vistas = null, likes = null, compartidos = null,
|
||||||
fecha_publicacion = null,
|
fecha_publicacion = null,
|
||||||
|
contexto_video = '',
|
||||||
} = req.body
|
} = req.body
|
||||||
|
|
||||||
if (!url) return res.status(400).json({ error: 'El campo "url" es requerido' })
|
if (!url) return res.status(400).json({ error: 'El campo "url" es requerido' })
|
||||||
@ -135,7 +136,7 @@ app.post('/api/analizar', async (req, res) => {
|
|||||||
|
|
||||||
paso = 'analisis'
|
paso = 'analisis'
|
||||||
console.log(`[3/5] Analizando con GPT-4o...`)
|
console.log(`[3/5] Analizando con GPT-4o...`)
|
||||||
const analisisRaw = await analizarTranscript(transcript, niche, plataforma, duracion)
|
const analisisRaw = await analizarTranscript(transcript, niche, plataforma, duracion, contexto_video)
|
||||||
|
|
||||||
paso = 'validacion'
|
paso = 'validacion'
|
||||||
console.log(`[4/5] Validando schema...`)
|
console.log(`[4/5] Validando schema...`)
|
||||||
@ -152,6 +153,7 @@ app.post('/api/analizar', async (req, res) => {
|
|||||||
proyecto_nombre, competidor_referente,
|
proyecto_nombre, competidor_referente,
|
||||||
url_origen: url, plataforma, duracion_segundos: duracion,
|
url_origen: url, plataforma, duracion_segundos: duracion,
|
||||||
vistas, likes, compartidos, fecha_publicacion,
|
vistas, likes, compartidos, fecha_publicacion,
|
||||||
|
contexto_video: contexto_video || null,
|
||||||
...analisis,
|
...analisis,
|
||||||
transcript,
|
transcript,
|
||||||
embedding_vector: `[${vector.join(',')}]`,
|
embedding_vector: `[${vector.join(',')}]`,
|
||||||
|
|||||||
17
database/migrations/07_diagnostico_contexto.sql
Normal file
17
database/migrations/07_diagnostico_contexto.sql
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
-- ============================================================
|
||||||
|
-- MIGRACIÓN 07 — Contexto, Diagnóstico y Hashtags
|
||||||
|
-- Ejecutar en Supabase SQL Editor después de la migración 06
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
alter table guiones
|
||||||
|
add column if not exists contexto_video text,
|
||||||
|
add column if not exists fortalezas text[],
|
||||||
|
add column if not exists debilidades text[],
|
||||||
|
add column if not exists sugerencias_mejora text[],
|
||||||
|
add column if not exists hashtags_sugeridos text[];
|
||||||
|
|
||||||
|
comment on column guiones.contexto_video is 'Contexto o idea del video ingresado por el usuario antes del análisis';
|
||||||
|
comment on column guiones.fortalezas is 'Lo que el video hace especialmente bien (generado por GPT-4o)';
|
||||||
|
comment on column guiones.debilidades is 'Áreas de mejora detectadas por GPT-4o';
|
||||||
|
comment on column guiones.sugerencias_mejora is 'Sugerencias accionables para mejorar el video';
|
||||||
|
comment on column guiones.hashtags_sugeridos is 'Hashtags sugeridos por GPT-4o para maximizar alcance';
|
||||||
@ -99,6 +99,30 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- Paso 3: Contexto del Video -->
|
||||||
|
<section class="space-y-6">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<span class="w-8 h-8 rounded-lg bg-tertiary/10 text-tertiary flex items-center justify-center font-black text-sm border border-tertiary/20">03</span>
|
||||||
|
<h2 class="text-xl font-headline font-extrabold text-white tracking-tight">Contexto del Video <span class="text-outline font-normal text-base">(opcional)</span></h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="glass-panel p-8 rounded-3xl border border-white/5 shadow-2xl space-y-4">
|
||||||
|
<p class="text-xs text-outline/70 leading-relaxed">
|
||||||
|
Describe de qué trata el video, cuál era la intención del creador, o cualquier detalle que la transcripción sola no captura (ej. tono irónico, contexto de tendencia, referencia cultural). GPT-4o usará esto para enriquecer el análisis.
|
||||||
|
</p>
|
||||||
|
<div class="relative">
|
||||||
|
<span class="material-symbols-outlined absolute left-4 top-4 text-tertiary text-lg">lightbulb</span>
|
||||||
|
<textarea
|
||||||
|
v-model="form.contexto_video"
|
||||||
|
rows="4"
|
||||||
|
placeholder="Ej. Este video es una respuesta a una tendencia viral donde los creadores muestran su rutina matutina. El creador usa humor sarcástico y habla a emprendedores que trabajan desde casa..."
|
||||||
|
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-tertiary/40 focus:border-tertiary/40 text-white placeholder:text-outline/40 transition-all resize-none leading-relaxed"
|
||||||
|
:disabled="analizando"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Columna de Estado del Pipeline -->
|
<!-- Columna de Estado del Pipeline -->
|
||||||
@ -176,7 +200,8 @@ const form = ref({
|
|||||||
compartidos: null,
|
compartidos: null,
|
||||||
cliente_id: null,
|
cliente_id: null,
|
||||||
proyecto_nombre: '',
|
proyecto_nombre: '',
|
||||||
competidor_referente: false
|
competidor_referente: false,
|
||||||
|
contexto_video: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
const pasosVisibles = [
|
const pasosVisibles = [
|
||||||
|
|||||||
@ -43,6 +43,34 @@
|
|||||||
<!-- Columna izquierda -->
|
<!-- Columna izquierda -->
|
||||||
<div class="xl:col-span-4 flex flex-col gap-6">
|
<div class="xl:col-span-4 flex flex-col gap-6">
|
||||||
|
|
||||||
|
<!-- Métricas Sociales -->
|
||||||
|
<div v-if="guion.vistas || guion.likes || guion.compartidos" class="bg-surface-container p-5 rounded-3xl border border-outline-variant/10 shadow-xl">
|
||||||
|
<h3 class="text-xs font-headline font-bold text-white mb-4 flex items-center gap-2">
|
||||||
|
<span class="material-symbols-outlined text-outline text-base">bar_chart</span> Métricas del Video
|
||||||
|
</h3>
|
||||||
|
<div class="grid grid-cols-3 gap-3">
|
||||||
|
<div class="text-center p-3 rounded-2xl bg-surface-container-low border border-white/5">
|
||||||
|
<span class="material-symbols-outlined text-blue-400 text-lg block mb-1">visibility</span>
|
||||||
|
<p class="text-[9px] text-outline font-black uppercase tracking-widest mb-1">Vistas</p>
|
||||||
|
<p class="text-base font-black text-white font-headline">{{ formatNum(guion.vistas) }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="text-center p-3 rounded-2xl bg-surface-container-low border border-white/5">
|
||||||
|
<span class="material-symbols-outlined text-red-400 text-lg block mb-1" style="font-variation-settings:'FILL' 1;">favorite</span>
|
||||||
|
<p class="text-[9px] text-outline font-black uppercase tracking-widest mb-1">Likes</p>
|
||||||
|
<p class="text-base font-black text-white font-headline">{{ formatNum(guion.likes) }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="text-center p-3 rounded-2xl bg-surface-container-low border border-white/5">
|
||||||
|
<span class="material-symbols-outlined text-secondary text-lg block mb-1">share</span>
|
||||||
|
<p class="text-[9px] text-outline font-black uppercase tracking-widest mb-1">Compartidos</p>
|
||||||
|
<p class="text-base font-black text-white font-headline">{{ formatNum(guion.compartidos) }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="guion.score_engagement" class="mt-3 flex items-center justify-between px-3 py-2 rounded-xl bg-surface-container-lowest border border-white/5">
|
||||||
|
<span class="text-[10px] text-outline font-black uppercase tracking-widest">Engagement Rate</span>
|
||||||
|
<span class="text-sm font-black text-secondary">{{ (guion.score_engagement * 1).toFixed(2) }}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Puntaje -->
|
<!-- Puntaje -->
|
||||||
<div class="bg-surface-container p-6 rounded-3xl border border-outline-variant/10 shadow-2xl relative overflow-hidden group">
|
<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>
|
<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>
|
||||||
@ -59,15 +87,11 @@
|
|||||||
<span class="text-xs font-bold text-primary uppercase tracking-widest mt-1">/ 100</span>
|
<span class="text-xs font-bold text-primary uppercase tracking-widest mt-1">/ 100</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-3 gap-3 pt-6 border-t border-white/5">
|
<div class="grid grid-cols-2 gap-4 pt-6 border-t border-white/5">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<p class="text-[9px] text-outline uppercase tracking-widest font-bold mb-1">Cialdini</p>
|
<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>
|
<p class="text-xl font-bold text-white">{{ guion.score_cialdini ?? 0 }}<span class="text-sm text-outline">/7</span></p>
|
||||||
</div>
|
</div>
|
||||||
<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">
|
<div class="text-center">
|
||||||
<p class="text-[9px] text-outline uppercase tracking-widest font-bold mb-1">Intensidad</p>
|
<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>
|
<p class="text-xl font-bold text-orange-400">{{ guion.intensidad_emocional || 0 }}<span class="text-sm text-outline">/10</span></p>
|
||||||
@ -281,6 +305,65 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Diagnóstico: Fortalezas / Debilidades / Sugerencias -->
|
||||||
|
<div v-if="guion.fortalezas?.length || guion.debilidades?.length" class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div class="bg-surface-container p-6 rounded-3xl border border-secondary/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-secondary text-base" style="font-variation-settings:'FILL' 1;">thumb_up</span> Fortalezas
|
||||||
|
</h3>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div v-for="(f, i) in guion.fortalezas" :key="i" class="flex items-start gap-2">
|
||||||
|
<span class="w-1.5 h-1.5 rounded-full bg-secondary mt-2 shrink-0"></span>
|
||||||
|
<p class="text-xs text-on-surface-variant leading-relaxed">{{ f }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-surface-container p-6 rounded-3xl border border-red-500/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-red-400 text-base">build</span> Áreas de Mejora
|
||||||
|
</h3>
|
||||||
|
<div class="space-y-2 mb-5">
|
||||||
|
<div v-for="(d, i) in guion.debilidades" :key="i" class="flex items-start gap-2">
|
||||||
|
<span class="w-1.5 h-1.5 rounded-full bg-red-400 mt-2 shrink-0"></span>
|
||||||
|
<p class="text-xs text-on-surface-variant leading-relaxed">{{ d }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="guion.sugerencias_mejora?.length">
|
||||||
|
<p class="text-[9px] text-outline font-black uppercase tracking-widest mb-2">Sugerencias</p>
|
||||||
|
<div class="space-y-1.5">
|
||||||
|
<div v-for="(s, i) in guion.sugerencias_mejora" :key="i" class="flex items-start gap-2">
|
||||||
|
<span class="text-[9px] font-black text-tertiary shrink-0 mt-0.5">{{ i + 1 }}.</span>
|
||||||
|
<p class="text-xs text-on-surface-variant leading-relaxed">{{ s }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Hashtags sugeridos -->
|
||||||
|
<div v-if="guion.hashtags_sugeridos?.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-4 flex items-center gap-2">
|
||||||
|
<span class="material-symbols-outlined text-outline text-base">tag</span> Hashtags Sugeridos
|
||||||
|
</h3>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<span
|
||||||
|
v-for="tag in guion.hashtags_sugeridos"
|
||||||
|
:key="tag"
|
||||||
|
class="px-3 py-1.5 bg-primary/10 border border-primary/20 rounded-full text-xs font-bold text-primary cursor-pointer hover:bg-primary/20 transition-colors"
|
||||||
|
@click="copiarTag(tag)"
|
||||||
|
>#{{ tag.replace(/^#/, '') }}</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-[9px] text-outline/50 mt-3 italic">Haz clic en un hashtag para copiarlo</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Contexto original del usuario -->
|
||||||
|
<div v-if="guion.contexto_video" class="bg-surface-container p-5 rounded-2xl border border-tertiary/10">
|
||||||
|
<p class="text-[10px] text-tertiary font-black uppercase tracking-widest mb-2 flex items-center gap-2">
|
||||||
|
<span class="material-symbols-outlined text-sm">lightbulb</span> Contexto ingresado
|
||||||
|
</p>
|
||||||
|
<p class="text-sm text-on-surface-variant leading-relaxed italic">{{ guion.contexto_video }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Transcripción -->
|
<!-- Transcripción -->
|
||||||
<div class="bg-surface-container p-6 rounded-3xl border border-outline-variant/10 shadow-xl">
|
<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">
|
<div class="flex items-center justify-between mb-2">
|
||||||
@ -343,6 +426,17 @@ function openUrl(url) {
|
|||||||
if (url) window.open(url, '_blank')
|
if (url) window.open(url, '_blank')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatNum(n) {
|
||||||
|
if (!n) return '—'
|
||||||
|
if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + 'M'
|
||||||
|
if (n >= 1_000) return (n / 1_000).toFixed(1) + 'K'
|
||||||
|
return n.toLocaleString()
|
||||||
|
}
|
||||||
|
|
||||||
|
function copiarTag(tag) {
|
||||||
|
navigator.clipboard.writeText('#' + tag.replace(/^#/, ''))
|
||||||
|
}
|
||||||
|
|
||||||
function plataformaBadge(p) {
|
function plataformaBadge(p) {
|
||||||
return {
|
return {
|
||||||
tiktok: 'bg-red-500/20 text-red-400',
|
tiktok: 'bg-red-500/20 text-red-400',
|
||||||
|
|||||||
Reference in New Issue
Block a user