diff --git a/skills/clients/blue-parrot-school.md b/skills/clients/blue-parrot-school.md new file mode 100644 index 0000000..af186ee --- /dev/null +++ b/skills/clients/blue-parrot-school.md @@ -0,0 +1,66 @@ +# Skill de cliente · Blue Parrot School (slug: bps) + +> Esta skill define cómo habla y cómo se ve Blue Parrot School en cualquier creatividad. Se inyecta como contexto fijo a Claude en cada petición de relevo. Si algo aquí contradice un brief puntual, manda esta skill. + +## Quién es + +Academia de idiomas con sede en Madrid y Málaga, clases presenciales, online e in-company. Dos líneas: + +- **B2B (empresas):** programas de inglés profesional para equipos. Diferencia frente a apps conversacionales tipo Twenix: profesor fijo, sesiones de 60 minutos, programas por sector (Inglés Legal, IT, Financiero, Comercial Internacional), modalidad híbrida. Es **Entidad Organizadora de FUNDAE** (gestiona la bonificación directamente, no a través de un partner). +- **B2C (particulares):** cursos de inglés para adultos, preparación de exámenes oficiales, intensivos. + +Web: blueparrotschool.com + +## A quién le hablamos + +**B2B** +- *Marta* — RRHH / Formación. Quiere un proveedor que no le dé trabajo: gestión FUNDAE resuelta, informes de asistencia, un interlocutor. +- *Carlos* — director o socio de despacho / consultora. Le importa que su equipo hable con clientes internacionales sin hacer el ridículo. Compra rigor, no "clases". +- *Lucía* — directora comercial / internacional. Necesita resultados medibles en poco tiempo: presentaciones, negociación, llamadas. + +**B2C** +- Adultos 25-45 que necesitan el inglés para el trabajo o para un examen (Cambridge, Aptis, IELTS) y han fracasado con apps. + +## Cómo habla + +- **Tono:** profesional, seguro, directo. Cercano sin ser coloquial. Nunca infantil, nunca grito de descuento. +- **Postura:** "formación profesional estructurada", no "aprende inglés fácil". Vendemos **programas**, no clases sueltas. +- **Español de España**, tú o usted según el público: *tú* en B2C, *tú* en B2B cuando hablamos a la persona, *usted* nunca. +- **Frases cortas.** Un dato concreto vale más que un adjetivo. +- **Inglés en el copy:** solo nombres propios de exámenes o términos sin traducción natural. Nada de "boost", "unlock", "level up". + +### Argumentos que funcionan +1. Profesor fijo + 60 minutos: progreso real, no mantenimiento. +2. FUNDAE gestionado por nosotros: coste cero o casi cero para la empresa. +3. Programas por sector: tu equipo aprende el inglés de su trabajo. +4. Presencial en Madrid y Málaga + online: híbrido de verdad. +5. Informes de asistencia y progreso para RRHH. + +### Objeciones a desactivar +- "Ya usamos una app" → la app mantiene, no avanza. +- "Es caro" → FUNDAE; coste por empleado y mes. +- "No tenemos tiempo" → sesiones in-company en horario laboral. + +## Prohibido +- Prometer niveles o plazos ("B2 en 3 meses"). +- Comparar nombrando a la competencia (ni Twenix ni ninguna). +- Descuentos agresivos, cuentas atrás, "última oportunidad". +- Emojis en creatividades de imagen. En copy, como mucho uno al inicio y solo en B2C. +- Fotografía de stock genérica de "gente feliz con portátil" con aspecto de banco de imágenes. +- Testimonios inventados o con nombre y cargo de personas que no existen. Si no hay un caso real aprobado, no se usa testimonio. +- Deformar el logo, recolorearlo o recrearlo. Se coloca el archivo oficial. + +## Identidad visual + +- **Paleta exacta y única:** Azul principal `#2A4D9C` · Amarillo `#FBC02D` · Negro `#000000` · Blanco `#FFFFFF`. Ningún otro color. +- **Uso de superficies:** azul y blanco como base. Amarillo solo para resaltar una palabra, un dato o el CTA. Negro reservado a mensajes de formalidad o urgencia (FUNDAE que caduca, manifiesto). +- **Tipografía:** Poppins. Extrabold en titulares, Regular en cuerpo. Nunca otra fuente. +- **Logo:** lockup oficial *Blue Parrot* + *SCHOOL* en amarillo; versión con filete blanco sobre fondos azul o negro. Área de protección respetada, esquina inferior o superior, nunca centrado ni dominando. +- **Estilo de imagen:** fotografía real de aula, oficina o reunión con luz natural; personas adultas de 25-50 en contexto de trabajo; sin poses de catálogo. Si es ilustración o composición gráfica, geometría limpia, mucho aire, una sola idea. +- **Texto en imagen:** máximo 2 niveles (titular + apoyo). El titular no supera 8 palabras. Nada de texto tocando bordes: margen mínimo 8 % del lado corto. + +## Ejemplos de hooks que han funcionado (referencia de registro, no para copiar) +- "Tu equipo no necesita más inglés. Necesita el inglés de su trabajo." +- "FUNDAE lo paga. Nosotros lo gestionamos." +- "Un profesor. Sesenta minutos. Cada semana. Así se avanza." +- "El inglés legal no se aprende en una app." diff --git a/skills/clients/bps.md b/skills/clients/bps.md new file mode 100644 index 0000000..af186ee --- /dev/null +++ b/skills/clients/bps.md @@ -0,0 +1,66 @@ +# Skill de cliente · Blue Parrot School (slug: bps) + +> Esta skill define cómo habla y cómo se ve Blue Parrot School en cualquier creatividad. Se inyecta como contexto fijo a Claude en cada petición de relevo. Si algo aquí contradice un brief puntual, manda esta skill. + +## Quién es + +Academia de idiomas con sede en Madrid y Málaga, clases presenciales, online e in-company. Dos líneas: + +- **B2B (empresas):** programas de inglés profesional para equipos. Diferencia frente a apps conversacionales tipo Twenix: profesor fijo, sesiones de 60 minutos, programas por sector (Inglés Legal, IT, Financiero, Comercial Internacional), modalidad híbrida. Es **Entidad Organizadora de FUNDAE** (gestiona la bonificación directamente, no a través de un partner). +- **B2C (particulares):** cursos de inglés para adultos, preparación de exámenes oficiales, intensivos. + +Web: blueparrotschool.com + +## A quién le hablamos + +**B2B** +- *Marta* — RRHH / Formación. Quiere un proveedor que no le dé trabajo: gestión FUNDAE resuelta, informes de asistencia, un interlocutor. +- *Carlos* — director o socio de despacho / consultora. Le importa que su equipo hable con clientes internacionales sin hacer el ridículo. Compra rigor, no "clases". +- *Lucía* — directora comercial / internacional. Necesita resultados medibles en poco tiempo: presentaciones, negociación, llamadas. + +**B2C** +- Adultos 25-45 que necesitan el inglés para el trabajo o para un examen (Cambridge, Aptis, IELTS) y han fracasado con apps. + +## Cómo habla + +- **Tono:** profesional, seguro, directo. Cercano sin ser coloquial. Nunca infantil, nunca grito de descuento. +- **Postura:** "formación profesional estructurada", no "aprende inglés fácil". Vendemos **programas**, no clases sueltas. +- **Español de España**, tú o usted según el público: *tú* en B2C, *tú* en B2B cuando hablamos a la persona, *usted* nunca. +- **Frases cortas.** Un dato concreto vale más que un adjetivo. +- **Inglés en el copy:** solo nombres propios de exámenes o términos sin traducción natural. Nada de "boost", "unlock", "level up". + +### Argumentos que funcionan +1. Profesor fijo + 60 minutos: progreso real, no mantenimiento. +2. FUNDAE gestionado por nosotros: coste cero o casi cero para la empresa. +3. Programas por sector: tu equipo aprende el inglés de su trabajo. +4. Presencial en Madrid y Málaga + online: híbrido de verdad. +5. Informes de asistencia y progreso para RRHH. + +### Objeciones a desactivar +- "Ya usamos una app" → la app mantiene, no avanza. +- "Es caro" → FUNDAE; coste por empleado y mes. +- "No tenemos tiempo" → sesiones in-company en horario laboral. + +## Prohibido +- Prometer niveles o plazos ("B2 en 3 meses"). +- Comparar nombrando a la competencia (ni Twenix ni ninguna). +- Descuentos agresivos, cuentas atrás, "última oportunidad". +- Emojis en creatividades de imagen. En copy, como mucho uno al inicio y solo en B2C. +- Fotografía de stock genérica de "gente feliz con portátil" con aspecto de banco de imágenes. +- Testimonios inventados o con nombre y cargo de personas que no existen. Si no hay un caso real aprobado, no se usa testimonio. +- Deformar el logo, recolorearlo o recrearlo. Se coloca el archivo oficial. + +## Identidad visual + +- **Paleta exacta y única:** Azul principal `#2A4D9C` · Amarillo `#FBC02D` · Negro `#000000` · Blanco `#FFFFFF`. Ningún otro color. +- **Uso de superficies:** azul y blanco como base. Amarillo solo para resaltar una palabra, un dato o el CTA. Negro reservado a mensajes de formalidad o urgencia (FUNDAE que caduca, manifiesto). +- **Tipografía:** Poppins. Extrabold en titulares, Regular en cuerpo. Nunca otra fuente. +- **Logo:** lockup oficial *Blue Parrot* + *SCHOOL* en amarillo; versión con filete blanco sobre fondos azul o negro. Área de protección respetada, esquina inferior o superior, nunca centrado ni dominando. +- **Estilo de imagen:** fotografía real de aula, oficina o reunión con luz natural; personas adultas de 25-50 en contexto de trabajo; sin poses de catálogo. Si es ilustración o composición gráfica, geometría limpia, mucho aire, una sola idea. +- **Texto en imagen:** máximo 2 niveles (titular + apoyo). El titular no supera 8 palabras. Nada de texto tocando bordes: margen mínimo 8 % del lado corto. + +## Ejemplos de hooks que han funcionado (referencia de registro, no para copiar) +- "Tu equipo no necesita más inglés. Necesita el inglés de su trabajo." +- "FUNDAE lo paga. Nosotros lo gestionamos." +- "Un profesor. Sesenta minutos. Cada semana. Así se avanza." +- "El inglés legal no se aprende en una app." diff --git a/skills/formats/estatico-4-5.md b/skills/formats/estatico-4-5.md new file mode 100644 index 0000000..97f6df0 --- /dev/null +++ b/skills/formats/estatico-4-5.md @@ -0,0 +1,65 @@ +# Skill de formato · Estático para Meta Ads (4:5) + +> Se aplica junto a la skill del cliente. Define la estructura de un anuncio estático de imagen para feed de Instagram y Facebook. Siempre 4:5 (1080×1350). + +## Qué entregas por cada variante + +Un objeto JSON con exactamente estos campos: + +```json +{ + "angle": "nombre corto del ángulo (p. ej. 'objeción tiempo', 'prueba social', 'coste FUNDAE', 'antes/después', 'autoridad')", + "hook": "titular que va EN la imagen, máx. 8 palabras", + "support": "línea de apoyo en la imagen, máx. 12 palabras, opcional (null si no hace falta)", + "headline": "título del anuncio en Meta, máx. 40 caracteres", + "primary_text": "texto principal del anuncio, 2-4 frases, máx. 300 caracteres, la primera frase es el gancho", + "cta": "uno de: 'Más información', 'Solicitar presupuesto', 'Reservar', 'Contactar', 'Registrarte'", + "image_prompt": "prompt en inglés para el generador de imagen, SIN texto ni logo, describiendo escena, personas, luz, encuadre y paleta", + "layout": "una de: 'foto-con-franja' (foto + franja de color con titular), 'tipografico' (solo color y texto), 'dato-grande' (una cifra enorme + contexto)", + "rationale": "una frase: por qué este ángulo puede relevar a la fatigada" +} +``` + +## Reglas de ángulo + +- Cada variante ataca un **ángulo distinto** entre sí y distinto del de la creatividad fatigada (viene en `avoid_angles`). +- Prioriza ángulos que aún no se hayan probado en el cliente (mira `top_creatives` del contexto: lo que ya funciona no se repite, se complementa). +- Rotación recomendada: dolor → solución → prueba → objeción → oferta. Si la fatigada era "oferta", el relevo vuelve a "dolor" o "prueba". + +## Reglas de hook (titular en imagen) + +- Máximo 8 palabras. Si necesita más, no es un hook. +- Una sola idea. Un verbo o un dato. +- Sin signos de exclamación. Sin mayúsculas completas. +- Se lee en 1,5 segundos en móvil con el pulgar en movimiento. +- Prohibido empezar por "¿Sabías que", "Descubre", "Imagina". + +## Reglas de copy (primary_text) + +- Frase 1: repite o amplía el hook, nunca lo contradice. +- Frase 2-3: un beneficio concreto o un dato. +- Frase final: el CTA en palabras, coherente con el botón. +- Sin hashtags. Sin enlaces en el texto. + +## Reglas de imagen (image_prompt) + +- Siempre en inglés, 40-80 palabras. +- Describe: sujeto, acción, entorno, luz, encuadre (vertical 4:5, espacio libre arriba o abajo para texto), paleta en términos de la skill del cliente. +- Termina siempre con: `no text, no logos, no watermarks, photorealistic, vertical 4:5 composition with clear negative space for a headline`. +- Nunca pidas personas reconocibles, marcas de terceros, ni niños. +- Una generación por variante. No pidas variaciones. + +## Reglas de layout + +- Zona segura: 8 % de margen en todos los lados. Texto nunca en el 15 % superior ni inferior (lo tapan la interfaz y el CTA de Meta). +- Jerarquía: hook ≥ 2× el tamaño del support. +- Contraste mínimo 4,5:1 entre texto y fondo. +- El logo ocupa como máximo el 12 % del ancho. + +## Autocheck antes de entregar + +- [ ] ¿Las variantes tienen ángulos distintos entre sí y del `avoid_angles`? +- [ ] ¿Cada hook tiene ≤ 8 palabras? +- [ ] ¿El primary_text cabe en 300 caracteres? +- [ ] ¿El image_prompt no pide texto ni logo? +- [ ] ¿Se respeta la lista de prohibiciones de la skill del cliente? diff --git a/skills/formats/estatico.md b/skills/formats/estatico.md new file mode 100644 index 0000000..97f6df0 --- /dev/null +++ b/skills/formats/estatico.md @@ -0,0 +1,65 @@ +# Skill de formato · Estático para Meta Ads (4:5) + +> Se aplica junto a la skill del cliente. Define la estructura de un anuncio estático de imagen para feed de Instagram y Facebook. Siempre 4:5 (1080×1350). + +## Qué entregas por cada variante + +Un objeto JSON con exactamente estos campos: + +```json +{ + "angle": "nombre corto del ángulo (p. ej. 'objeción tiempo', 'prueba social', 'coste FUNDAE', 'antes/después', 'autoridad')", + "hook": "titular que va EN la imagen, máx. 8 palabras", + "support": "línea de apoyo en la imagen, máx. 12 palabras, opcional (null si no hace falta)", + "headline": "título del anuncio en Meta, máx. 40 caracteres", + "primary_text": "texto principal del anuncio, 2-4 frases, máx. 300 caracteres, la primera frase es el gancho", + "cta": "uno de: 'Más información', 'Solicitar presupuesto', 'Reservar', 'Contactar', 'Registrarte'", + "image_prompt": "prompt en inglés para el generador de imagen, SIN texto ni logo, describiendo escena, personas, luz, encuadre y paleta", + "layout": "una de: 'foto-con-franja' (foto + franja de color con titular), 'tipografico' (solo color y texto), 'dato-grande' (una cifra enorme + contexto)", + "rationale": "una frase: por qué este ángulo puede relevar a la fatigada" +} +``` + +## Reglas de ángulo + +- Cada variante ataca un **ángulo distinto** entre sí y distinto del de la creatividad fatigada (viene en `avoid_angles`). +- Prioriza ángulos que aún no se hayan probado en el cliente (mira `top_creatives` del contexto: lo que ya funciona no se repite, se complementa). +- Rotación recomendada: dolor → solución → prueba → objeción → oferta. Si la fatigada era "oferta", el relevo vuelve a "dolor" o "prueba". + +## Reglas de hook (titular en imagen) + +- Máximo 8 palabras. Si necesita más, no es un hook. +- Una sola idea. Un verbo o un dato. +- Sin signos de exclamación. Sin mayúsculas completas. +- Se lee en 1,5 segundos en móvil con el pulgar en movimiento. +- Prohibido empezar por "¿Sabías que", "Descubre", "Imagina". + +## Reglas de copy (primary_text) + +- Frase 1: repite o amplía el hook, nunca lo contradice. +- Frase 2-3: un beneficio concreto o un dato. +- Frase final: el CTA en palabras, coherente con el botón. +- Sin hashtags. Sin enlaces en el texto. + +## Reglas de imagen (image_prompt) + +- Siempre en inglés, 40-80 palabras. +- Describe: sujeto, acción, entorno, luz, encuadre (vertical 4:5, espacio libre arriba o abajo para texto), paleta en términos de la skill del cliente. +- Termina siempre con: `no text, no logos, no watermarks, photorealistic, vertical 4:5 composition with clear negative space for a headline`. +- Nunca pidas personas reconocibles, marcas de terceros, ni niños. +- Una generación por variante. No pidas variaciones. + +## Reglas de layout + +- Zona segura: 8 % de margen en todos los lados. Texto nunca en el 15 % superior ni inferior (lo tapan la interfaz y el CTA de Meta). +- Jerarquía: hook ≥ 2× el tamaño del support. +- Contraste mínimo 4,5:1 entre texto y fondo. +- El logo ocupa como máximo el 12 % del ancho. + +## Autocheck antes de entregar + +- [ ] ¿Las variantes tienen ángulos distintos entre sí y del `avoid_angles`? +- [ ] ¿Cada hook tiene ≤ 8 palabras? +- [ ] ¿El primary_text cabe en 300 caracteres? +- [ ] ¿El image_prompt no pide texto ni logo? +- [ ] ¿Se respeta la lista de prohibiciones de la skill del cliente? diff --git a/src/app/api/integrations/creatives/[id]/context/route.ts b/src/app/api/integrations/creatives/[id]/context/route.ts index dc42f6c..75064f6 100644 --- a/src/app/api/integrations/creatives/[id]/context/route.ts +++ b/src/app/api/integrations/creatives/[id]/context/route.ts @@ -108,12 +108,32 @@ export async function GET( topCreatives = fallbackRows || []; } - return NextResponse.json({ - client_id: tenantId, - tenant_id: tenantId, + // 5. Obtener datos del cliente (drive_folder_id, slug, nombre) + const { data: tenant } = await adminClient + .from('tenants') + .select('id, name, slug, drive_folder_id, meta_ad_account_id') + .eq('id', tenantId) + .maybeSingle(); + + const contextData = { + source_creative: creative, creative, metrics, rules: rules || null, + top_creatives: topCreatives, top_historical_creatives: topCreatives, + client: { + id: tenantId, + name: tenant?.name || null, + slug: tenant?.slug || null, + drive_folder_id: tenant?.drive_folder_id || null, + }, + }; + + return NextResponse.json({ + data: contextData, + client_id: tenantId, + tenant_id: tenantId, + ...contextData, }); } diff --git a/src/app/api/integrations/creatives/route.ts b/src/app/api/integrations/creatives/route.ts index 58c4bcd..7854e80 100644 --- a/src/app/api/integrations/creatives/route.ts +++ b/src/app/api/integrations/creatives/route.ts @@ -66,6 +66,7 @@ export async function POST(req: NextRequest) { created_by: 'claude', // creadas por Claude parent_id: c.parent_id || defaultParentId, generation_request_id: c.generation_request_id || genRequestId || null, + meta: c.meta || {}, })); const { data: inserted, error } = await adminClient diff --git a/src/app/api/integrations/skills/route.ts b/src/app/api/integrations/skills/route.ts new file mode 100644 index 0000000..9c7f4ba --- /dev/null +++ b/src/app/api/integrations/skills/route.ts @@ -0,0 +1,73 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { authenticateIntegrationRequest } from '@/lib/integrations-auth'; +import { createClient } from '@supabase/supabase-js'; +import fs from 'fs'; +import path from 'path'; + +export async function GET(req: NextRequest) { + const auth = await authenticateIntegrationRequest(req, { requireTenant: true }); + if (!auth.ok) { + return auth.errorResponse!; + } + + const tenantId = auth.tenantId!; + const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!; + const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY!; + const adminClient = createClient(supabaseUrl, serviceRoleKey); + + // 1. Obtener slug del cliente + const { data: tenant, error: tErr } = await adminClient + .from('tenants') + .select('id, name, slug') + .eq('id', tenantId) + .maybeSingle(); + + if (tErr || !tenant) { + return NextResponse.json({ error: 'Tenant not found' }, { status: 404 }); + } + + const slug = (tenant.slug || 'bps').toLowerCase().trim(); + const formatParam = (req.nextUrl.searchParams.get('format') || 'estatico').toLowerCase().trim(); + + // 2. Resolver skill del cliente (skills/clients/.md) + const skillsDir = path.join(process.cwd(), 'skills'); + const clientCandidates = [ + path.join(skillsDir, 'clients', `${slug}.md`), + path.join(skillsDir, 'clients', slug === 'bps' ? 'blue-parrot-school.md' : `${slug}.md`), + path.join(skillsDir, 'clients', 'blue-parrot-school.md'), + ]; + + let clientSkill = ''; + for (const p of clientCandidates) { + if (fs.existsSync(p)) { + clientSkill = fs.readFileSync(p, 'utf-8'); + break; + } + } + + // 3. Resolver skill del formato (skills/formats/-4-5.md o skills/formats/.md, fallback a estatico) + const formatCandidates = [ + path.join(skillsDir, 'formats', `${formatParam}-4-5.md`), + path.join(skillsDir, 'formats', `${formatParam}.md`), + path.join(skillsDir, 'formats', 'estatico-4-5.md'), + path.join(skillsDir, 'formats', 'estatico.md'), + ]; + + let formatSkill = ''; + for (const p of formatCandidates) { + if (fs.existsSync(p)) { + formatSkill = fs.readFileSync(p, 'utf-8'); + break; + } + } + + return NextResponse.json({ + data: { + client_skill: clientSkill, + format_skill: formatSkill, + }, + // Compatibilidad en primer nivel + client_skill: clientSkill, + format_skill: formatSkill, + }); +}