/** * Brandbook per tenant — source of truth for ads, RRSS calendars, * LinkedIn B2B and corporate blog strategy. * Stored in documents as type=brandbook (one active doc per tenant). */ import { brandBrief } from '@/lib/calendar-generate-brand'; export const BRANDBOOK_DOC_TYPE = 'brandbook'; export const BRANDBOOK_DOC_TITLE = 'Brandbook'; export type BrandbookSectionId = | 'identidad' | 'voz' | 'visual' | 'audiencia' | 'oferta' | 'pilares' | 'canales' | 'ads' | 'b2b' | 'blog' | 'compliance' | 'fuentes'; export type Brandbook = { version: 1; updated_at?: string; /** Identity */ brand_name: string; tagline: string; mission: string; vision: string; values: string[]; vertical: string; /** Voice */ tone: string; personality: string[]; voice_do: string[]; voice_dont: string[]; vocabulary_prefer: string[]; vocabulary_avoid: string[]; /** Visual */ colors_primary: string[]; colors_secondary: string[]; fonts: string[]; visual_notes: string; logo_usage: string; /** Audience */ icp: string; personas: Array<{ name: string; description: string; pains: string }>; geo: string; languages: string[]; /** Offer */ products_services: string[]; differentiators: string[]; proof_points: string[]; pricing_notes: string; /** Content pillars */ pillars_rrss: string[]; series_recurrentes: string[]; formats_priority: string[]; /** Channels */ channels: { instagram: string; facebook: string; linkedin: string; tiktok: string; blog: string; other: string; }; /** Ads */ ads_usps: string[]; ads_objections: string[]; ads_ctas: string[]; ads_offers: string[]; ads_angles: string[]; /** B2B / LinkedIn */ linkedin_themes: string[]; linkedin_formats: string[]; thought_leadership: string[]; buyer_roles: string[]; /** Blog corporativo */ blog_themes: string[]; blog_seo_keywords: string[]; blog_cadence: string; /** Compliance */ claims_forbidden: string[]; legal_notes: string; sector_rules: string; /** Uploaded / pasted source material */ source_excerpts: Array<{ id: string; filename: string; excerpt: string; added_at: string; }>; freeform_notes: string; }; export const BRANDBOOK_SECTIONS: { id: BrandbookSectionId; label: string; hint: string; }[] = [ { id: 'identidad', label: 'Identidad', hint: 'Nombre, misión, visión, valores, vertical', }, { id: 'voz', label: 'Voz y tono', hint: 'Cómo habla la marca; do/don’t y léxico', }, { id: 'visual', label: 'Visual', hint: 'Colores, tipografías, logo y notas de arte', }, { id: 'audiencia', label: 'Audiencia', hint: 'ICP, personas, geo e idiomas', }, { id: 'oferta', label: 'Oferta', hint: 'Productos/servicios, diferenciadores, prueba', }, { id: 'pilares', label: 'Pilares de contenido', hint: 'Series RRSS y formatos prioritarios', }, { id: 'canales', label: 'Canales', hint: 'Rol de cada red y blog', }, { id: 'ads', label: 'Anuncios', hint: 'USPs, objeciones, CTAs, ofertas y ángulos', }, { id: 'b2b', label: 'LinkedIn B2B', hint: 'Temas, formatos y thought leadership', }, { id: 'blog', label: 'Blog corporativo', hint: 'Temas, SEO y cadencia', }, { id: 'compliance', label: 'Compliance', hint: 'Claims prohibidos y notas legales', }, { id: 'fuentes', label: 'Fuentes', hint: 'Textos subidos / pegados del brandbook', }, ]; export function emptyBrandbook(label = ''): Brandbook { return { version: 1, brand_name: label, tagline: '', mission: '', vision: '', values: [], vertical: '', tone: '', personality: [], voice_do: [], voice_dont: [], vocabulary_prefer: [], vocabulary_avoid: [], colors_primary: [], colors_secondary: [], fonts: [], visual_notes: '', logo_usage: '', icp: '', personas: [], geo: 'España', languages: ['es'], products_services: [], differentiators: [], proof_points: [], pricing_notes: '', pillars_rrss: [], series_recurrentes: [], formats_priority: ['Reel', 'Carrusel', 'Stories', 'Post'], channels: { instagram: '', facebook: '', linkedin: '', tiktok: '', blog: '', other: '', }, ads_usps: [], ads_objections: [], ads_ctas: [], ads_offers: [], ads_angles: [], linkedin_themes: [], linkedin_formats: ['Carrusel', 'Documento', 'Post texto', 'Video corto'], thought_leadership: [], buyer_roles: [], blog_themes: [], blog_seo_keywords: [], blog_cadence: '', claims_forbidden: [], legal_notes: '', sector_rules: '', source_excerpts: [], freeform_notes: '', }; } /** Seed a usable brandbook from hard-coded brand DNA + sensible defaults. */ export function seedBrandbook(slug: string, label: string): Brandbook { const brief = brandBrief(slug, label); const base = emptyBrandbook(label); const seeds: Record> = { bps: { vertical: 'Escuela de idiomas', tagline: 'Inglés real, cultura pop y certificaciones', mission: 'Ayudar a alumnos a hablar inglés con confianza para trabajo, viajes y exámenes.', tone: brief.tone, personality: ['cercano', 'divertido', 'riguroso', 'pop'], voice_do: [ 'Explicar con ejemplos reales', 'Usar series FALSE FRIEND / SLANG / MTW', 'Micro-lecciones accionables', ], voice_dont: [ 'Listas genéricas de Black Friday', 'Tono académico seco', 'Copiar títulos mes a mes', ], pillars_rrss: brief.pillars, series_recurrentes: [ 'FALSE FRIEND', 'SLANG', 'Same word different level', 'MTW', 'Canción del día', 'English Through TV', 'Fix it', ], products_services: [ 'Cursos generales', 'Intensivos', 'Trinity / IELTS / Cambridge', 'Inglés oposiciones', 'Madrid / Málaga', ], ads_usps: [ 'Metodología práctica', 'Profes nativos / preparados', 'Certificaciones oficiales', ], ads_ctas: ['Test de nivel', 'Reservar plaza', 'WhatsApp'], ads_objections: [ 'No tengo tiempo', 'Ya probé y no aprendí', 'Es caro', ], linkedin_themes: [ 'Inglés profesional', 'Employability', 'Casos de alumnos', ], blog_themes: [ 'Guías de certificación', 'Errores comunes', 'Cómo elegir curso', ], channels: { instagram: 'Alcance + comunidad (Reels + Stories)', facebook: 'Comunidad local / padres', linkedin: 'B2B ligero / empleabilidad', tiktok: 'Opcional micro-lecciones', blog: 'SEO certificaciones y objeciones', other: '', }, }, 'talento-y-deporte': { vertical: brief.vertical, tone: brief.tone, pillars_rrss: brief.pillars, series_recurrentes: [ 'Nuevo alumno', 'Beca académica', 'Beca deportiva', 'Campus USA', 'Objeciones', 'English tips', ], products_services: [ 'Becas deportivas USA', 'Acompañamiento académico', 'Campus visits', ], ads_usps: ['Becas reales', 'Acompañamiento end-to-end', 'Historias de alumnos'], ads_ctas: ['Agenda llamada', 'Evalúa tu perfil'], linkedin_themes: ['Educación internacional', 'Deporte + estudios'], blog_themes: ['Guía becas USA', 'Requisitos TOEFL/SAT', 'Vida en campus'], }, 'halal-safety': { vertical: brief.vertical, tone: brief.tone, pillars_rrss: brief.pillars, products_services: [ 'Certificación Halal', 'Consultoría exportación', 'Formación', ], ads_usps: ['Autoridad técnica', 'Mercados export', 'Proceso claro'], ads_ctas: ['Solicitar info', 'Auditoría'], linkedin_themes: [ 'Exportación Halal', 'Normativa', 'Casos sector alimentación/cosmética', ], thought_leadership: [ 'Tendencias mercados musulmanes', 'Compliance vs marketing Halal', ], blog_themes: ['Guías de certificación', 'Mercados clave', 'FAQ técnica'], buyer_roles: ['Quality', 'Export manager', 'CEO pyme alimentaria'], }, 'brava-dermoestetica': { vertical: brief.vertical, tone: brief.tone, pillars_rrss: brief.pillars, products_services: ['Peelings', 'Tratamientos facial', 'Rutinas'], ads_usps: ['Resultados naturales', 'Equipo médico', 'Protocolos personalizados'], ads_ctas: ['Pedir cita', 'Consulta valoración'], claims_forbidden: [ 'Promesas milagro sin matiz', 'Antes/después sin consentimiento', ], sector_rules: 'Publicidad sanitaria: claims prudentes, sin garantías absolutas.', }, noberplast: { vertical: brief.vertical, tone: brief.tone, pillars_rrss: brief.pillars, formats_priority: ['Carrusel', 'Post LinkedIn', 'Documento', 'Reel'], linkedin_themes: [ 'Plásticos técnicos', 'Casos industriales', 'Innovación materiales', ], thought_leadership: [ 'Sostenibilidad industrial', 'Especificaciones técnicas', ], buyer_roles: ['Ingeniería', 'Compras', 'Producción'], blog_themes: ['Fichas técnicas', 'Aplicaciones sectoriales', 'FAQ producto'], channels: { instagram: 'Marca / producto visual', facebook: 'Bajo', linkedin: 'Canal principal B2B', tiktok: 'No prioritario', blog: 'SEO técnico + captación inbound', other: '', }, }, '4ventures-hub': { vertical: brief.vertical, tone: brief.tone, pillars_rrss: brief.pillars, linkedin_themes: ['Ops de agencia', 'Growth', 'Casos clientes'], thought_leadership: ['Centro de mando', 'Atribución', 'Contenido que vende'], }, }; const resolvedSlug = slug === 'blue-parrot-school' ? 'bps' : slug; const extra = seeds[resolvedSlug] || { vertical: brief.vertical, tone: brief.tone, pillars_rrss: brief.pillars, products_services: [], freeform_notes: brief.notes, }; return { ...base, ...extra, brand_name: label, vertical: extra.vertical || brief.vertical, tone: extra.tone || brief.tone, pillars_rrss: extra.pillars_rrss?.length ? extra.pillars_rrss : brief.pillars, freeform_notes: extra.freeform_notes || brief.notes, updated_at: new Date().toISOString(), }; } export function parseBrandbook(raw: unknown): Brandbook | null { if (!raw || typeof raw !== 'object') return null; const o = raw as Partial; if (o.version !== 1 && !o.brand_name && !o.vertical) { // Allow loose objects that look like brandbooks if (!('tone' in o) && !('pillars_rrss' in o) && !('mission' in o)) { return null; } } const empty = emptyBrandbook(String(o.brand_name || '')); return { ...empty, ...o, version: 1, values: arr(o.values), personality: arr(o.personality), voice_do: arr(o.voice_do), voice_dont: arr(o.voice_dont), vocabulary_prefer: arr(o.vocabulary_prefer), vocabulary_avoid: arr(o.vocabulary_avoid), colors_primary: arr(o.colors_primary), colors_secondary: arr(o.colors_secondary), fonts: arr(o.fonts), personas: Array.isArray(o.personas) ? o.personas : [], languages: arr(o.languages).length ? arr(o.languages) : ['es'], products_services: arr(o.products_services), differentiators: arr(o.differentiators), proof_points: arr(o.proof_points), pillars_rrss: arr(o.pillars_rrss), series_recurrentes: arr(o.series_recurrentes), formats_priority: arr(o.formats_priority), channels: { ...empty.channels, ...(o.channels || {}) }, ads_usps: arr(o.ads_usps), ads_objections: arr(o.ads_objections), ads_ctas: arr(o.ads_ctas), ads_offers: arr(o.ads_offers), ads_angles: arr(o.ads_angles), linkedin_themes: arr(o.linkedin_themes), linkedin_formats: arr(o.linkedin_formats), thought_leadership: arr(o.thought_leadership), buyer_roles: arr(o.buyer_roles), blog_themes: arr(o.blog_themes), blog_seo_keywords: arr(o.blog_seo_keywords), claims_forbidden: arr(o.claims_forbidden), source_excerpts: Array.isArray(o.source_excerpts) ? o.source_excerpts : [], }; } function arr(v: unknown): string[] { if (!Array.isArray(v)) return []; return v.map((x) => String(x || '').trim()).filter(Boolean); } /** Lines / commas → string[] for form fields. */ export function linesToList(text: string): string[] { return text .split(/\n|,/) .map((s) => s.trim()) .filter(Boolean); } export function listToLines(list: string[]): string { return (list || []).join('\n'); } /** Heuristic: fold uploaded plain text into freeform + optional lists. */ export function ingestSourceText( book: Brandbook, filename: string, text: string ): Brandbook { const excerpt = text.slice(0, 12000).trim(); if (!excerpt) return book; const id = `src_${Date.now().toString(36)}`; const next: Brandbook = { ...book, source_excerpts: [ { id, filename: filename || 'pegado.txt', excerpt, added_at: new Date().toISOString(), }, ...(book.source_excerpts || []), ].slice(0, 20), freeform_notes: [book.freeform_notes, `--- ${filename} ---\n${excerpt.slice(0, 4000)}`] .filter(Boolean) .join('\n\n') .slice(0, 50000), updated_at: new Date().toISOString(), }; // Light keyword pulls into empty fields const lower = excerpt.toLowerCase(); if (!next.mission && /misi[oó]n/.test(lower)) { const m = excerpt.match(/misi[oó]n[:\s]+(.{20,200})/i); if (m) next.mission = m[1].trim(); } if (!next.tone && /tono[:\s]+(.{10,120})/i.test(excerpt)) { const m = excerpt.match(/tono[:\s]+(.{10,120})/i); if (m) next.tone = m[1].trim(); } return next; } /** Compact prompt block for strategists / Anthropic. */ export function brandbookToPrompt(book: Brandbook): string { const lines = [ `Marca: ${book.brand_name}`, book.tagline && `Tagline: ${book.tagline}`, book.vertical && `Vertical: ${book.vertical}`, book.mission && `Misión: ${book.mission}`, book.tone && `Tono: ${book.tone}`, book.personality.length && `Personalidad: ${book.personality.join(', ')}`, book.voice_do.length && `Hacer: ${book.voice_do.join('; ')}`, book.voice_dont.length && `Evitar: ${book.voice_dont.join('; ')}`, book.series_recurrentes.length && `Series RRSS: ${book.series_recurrentes.join(', ')}`, book.pillars_rrss.length && `Pilares: ${book.pillars_rrss.join(', ')}`, book.products_services.length && `Oferta: ${book.products_services.join(', ')}`, book.differentiators.length && `Diferenciadores: ${book.differentiators.join(', ')}`, book.icp && `ICP: ${book.icp}`, book.ads_usps.length && `USPs ads: ${book.ads_usps.join('; ')}`, book.ads_ctas.length && `CTAs: ${book.ads_ctas.join('; ')}`, book.ads_objections.length && `Objeciones: ${book.ads_objections.join('; ')}`, book.linkedin_themes.length && `LinkedIn: ${book.linkedin_themes.join(', ')}`, book.thought_leadership.length && `Thought leadership: ${book.thought_leadership.join(', ')}`, book.blog_themes.length && `Blog: ${book.blog_themes.join(', ')}`, book.blog_seo_keywords.length && `SEO: ${book.blog_seo_keywords.join(', ')}`, book.claims_forbidden.length && `Prohibido: ${book.claims_forbidden.join('; ')}`, book.legal_notes && `Legal: ${book.legal_notes}`, book.freeform_notes && `Notas: ${book.freeform_notes.slice(0, 1500)}`, ].filter(Boolean); return lines.join('\n'); } export function brandbookCompleteness(book: Brandbook): { score: number; missing: string[]; } { const checks: [string, boolean][] = [ ['Identidad (misión/vertical)', !!(book.mission || book.vertical)], ['Voz/tono', !!book.tone], ['Audiencia/ICP', !!(book.icp || book.personas.length)], ['Oferta', book.products_services.length > 0], ['Pilares/series RRSS', !!(book.pillars_rrss.length || book.series_recurrentes.length)], ['Ads (USP o CTA)', !!(book.ads_usps.length || book.ads_ctas.length)], ['LinkedIn B2B o blog', !!(book.linkedin_themes.length || book.blog_themes.length)], ]; const missing = checks.filter(([, ok]) => !ok).map(([l]) => l); const score = Math.round( ((checks.length - missing.length) / checks.length) * 100 ); return { score, missing }; }