Files
4ventures-client-portal/src/lib/brandbook.ts
4Ventures e49111f63e Add Zona de marca brandbooks and strategist calendar generation.
Per-tenant brandbooks power ads, RRSS calendars, LinkedIn B2B and blog; Nov calendars now create from trends + client DNA instead of copying history.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-10 01:05:15 +02:00

572 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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<string, Partial<Brandbook>> = {
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<Brandbook>;
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 };
}