diff --git a/src/app/api/brandbook/route.ts b/src/app/api/brandbook/route.ts index 4ccaa2e..12fe8a9 100644 --- a/src/app/api/brandbook/route.ts +++ b/src/app/api/brandbook/route.ts @@ -5,7 +5,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { createClient } from '@/lib/supabase/server'; import { resolveActiveTenant, restGet } from '@/lib/tenant-context'; -import { isAgencyRole, serviceHeaders, supabaseUrl, userRole } from '@/lib/agency'; +import { isAgencyRole, supabaseUrl, userRole } from '@/lib/agency'; import { createClient as createAdminClient } from '@supabase/supabase-js'; import { BRANDBOOK_DOC_TITLE, @@ -29,12 +29,16 @@ async function loadDoc(tenantId: string): Promise<{ id: string | null; book: Brandbook | null; }> { - const r = await restGet<{ id: string; content: unknown }[]>( - `/rest/v1/documents?tenant_id=eq.${encodeURIComponent( - tenantId - )}&type=eq.${BRANDBOOK_DOC_TYPE}&select=id,content&order=updated_at.desc&limit=1` - ); - const row = r.data?.[0]; + const admin = getAdmin(); + const { data: row } = await admin + .from('documents') + .select('id, content') + .eq('tenant_id', tenantId) + .eq('type', BRANDBOOK_DOC_TYPE) + .order('updated_at', { ascending: false }) + .limit(1) + .maybeSingle(); + if (!row) return { id: null, book: null }; return { id: row.id, book: parseBrandbook(row.content) }; } @@ -48,8 +52,11 @@ export async function GET(req: NextRequest) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } - const isAgency = isAgencyRole(userRole(user)); - const queryTenantId = req.nextUrl.searchParams.get('tenant_id') || req.nextUrl.searchParams.get('tenantId'); + const role = userRole(user); + const isAgency = isAgencyRole(role); + const queryTenantId = + req.nextUrl.searchParams.get('tenant_id') || + req.nextUrl.searchParams.get('tenantId'); let tenantId: string | null = null; let label = 'Cliente'; @@ -88,7 +95,56 @@ export async function GET(req: NextRequest) { } const { id, book } = await loadDoc(tenantId); - const current = book || emptyBrandbook(label); + let current = book || emptyBrandbook(label); + + // Sincronizar / fusionar con tenants.creative_style para que absolutamente NADA se pierda + const admin = getAdmin(); + const { data: tenantRow } = await admin + .from('tenants') + .select('creative_style') + .eq('id', tenantId) + .maybeSingle(); + + if (tenantRow?.creative_style && typeof tenantRow.creative_style === 'object') { + const cs = tenantRow.creative_style as any; + const idObj = cs.identity || {}; + const audObj = cs.audiences || {}; + const argObj = cs.arguments || {}; + const proObj = cs.prohibitions || {}; + const voiObj = cs.voice || {}; + const visObj = cs.visual || {}; + + current = { + ...current, + website: current.website || idObj.website || '', + b2b_offering: current.b2b_offering || idObj.b2b_offering || '', + b2c_offering: current.b2c_offering || idObj.b2c_offering || '', + usp_differentiators: current.usp_differentiators || idObj.usp_differentiators || '', + current_priorities: current.current_priorities || idObj.current_priorities || '', + price_and_offer_framing: current.price_and_offer_framing || idObj.price_and_offer_framing || '', + elevator_pitch: current.elevator_pitch || idObj.summary || '', + tone: current.tone || voiObj.tone || '', + copy_structure_rules: current.copy_structure_rules || voiObj.copy_structure_rules || '', + english_terms_policy: current.english_terms_policy || voiObj.english_terms_policy || '', + power_words: current.power_words?.length ? current.power_words : (voiObj.power_words || []), + allowed_ctas: current.allowed_ctas?.length ? current.allowed_ctas : (voiObj.allowed_ctas || []), + surface_rules: current.surface_rules || visObj.surface_rules || '', + image_text_rules: current.image_text_rules || visObj.image_text_rules || '', + typography_headline: current.typography_headline || visObj.typography_headline || '', + typography_body: current.typography_body || visObj.typography_body || '', + b2b_personas: current.b2b_personas?.length ? current.b2b_personas : (audObj.b2b_personas || []), + b2c_personas: current.b2c_personas?.length ? current.b2c_personas : (audObj.b2c_personas || []), + customer_frustrations: current.customer_frustrations || audObj.customer_frustrations || '', + desired_transformation: current.desired_transformation || audObj.desired_transformation || '', + objections: current.objections?.length ? current.objections : (argObj.objections || []), + winning_hooks: current.winning_hooks?.length ? current.winning_hooks : (argObj.winning_hooks || []), + preferred_angles: current.preferred_angles?.length ? current.preferred_angles : (argObj.preferred_angles || []), + emoji_policy: current.emoji_policy || proObj.emoji_policy || '', + testimonials_policy: current.testimonials_policy || proObj.testimonials_policy || '', + competitors_never_mention: current.competitors_never_mention?.length ? current.competitors_never_mention : (proObj.competitors_never_mention || []), + }; + } + const completeness = brandbookCompleteness(current); return NextResponse.json({ diff --git a/src/app/api/creatives/style/route.ts b/src/app/api/creatives/style/route.ts index 58ad152..d03e5df 100644 --- a/src/app/api/creatives/style/route.ts +++ b/src/app/api/creatives/style/route.ts @@ -91,8 +91,36 @@ export async function GET(req: NextRequest) { brandbook = null; } } + if (!brandbook) { brandbook = seedBrandbook(tenant.slug || 'cliente', tenant.name || 'Cliente'); + } else { + if (brandbook.website) style.identity.website = brandbook.website; + if (brandbook.b2b_offering) style.identity.b2b_offering = brandbook.b2b_offering; + if (brandbook.b2c_offering) style.identity.b2c_offering = brandbook.b2c_offering; + if (brandbook.usp_differentiators) style.identity.usp_differentiators = brandbook.usp_differentiators; + if (brandbook.current_priorities) style.identity.current_priorities = brandbook.current_priorities; + if (brandbook.price_and_offer_framing) style.identity.price_and_offer_framing = brandbook.price_and_offer_framing; + if (brandbook.elevator_pitch) style.identity.summary = brandbook.elevator_pitch; + if (brandbook.tone) style.voice.tone = brandbook.tone; + if (brandbook.copy_structure_rules) style.voice.copy_structure_rules = brandbook.copy_structure_rules; + if (brandbook.english_terms_policy) style.voice.english_terms_policy = brandbook.english_terms_policy; + if (brandbook.power_words?.length) style.voice.power_words = brandbook.power_words; + if (brandbook.allowed_ctas?.length) style.voice.allowed_ctas = brandbook.allowed_ctas; + if (brandbook.surface_rules) style.visual.surface_rules = brandbook.surface_rules; + if (brandbook.image_text_rules) style.visual.image_text_rules = brandbook.image_text_rules; + if (brandbook.typography_headline) style.visual.typography_headline = brandbook.typography_headline; + if (brandbook.typography_body) style.visual.typography_body = brandbook.typography_body; + if (brandbook.b2b_personas?.length) style.audiences.b2b_personas = brandbook.b2b_personas as any; + if (brandbook.b2c_personas?.length) style.audiences.b2c_personas = brandbook.b2c_personas as any; + if (brandbook.customer_frustrations) style.audiences.customer_frustrations = brandbook.customer_frustrations; + if (brandbook.desired_transformation) style.audiences.desired_transformation = brandbook.desired_transformation; + if (brandbook.objections?.length) style.arguments.objections = brandbook.objections as any; + if (brandbook.winning_hooks?.length) style.arguments.winning_hooks = brandbook.winning_hooks; + if (brandbook.preferred_angles?.length) style.arguments.preferred_angles = brandbook.preferred_angles; + if (brandbook.emoji_policy) style.prohibitions.emoji_policy = brandbook.emoji_policy; + if (brandbook.testimonials_policy) style.prohibitions.testimonials_policy = brandbook.testimonials_policy; + if (brandbook.competitors_never_mention?.length) style.prohibitions.competitors_never_mention = brandbook.competitors_never_mention; } const compiledMarkdown = compileClientSkillFromBrandbook( @@ -241,10 +269,10 @@ export async function PUT(req: NextRequest) { return NextResponse.json({ error: updErr.message }, { status: 500 }); } - // Cargar brandbook de documents para compilar skill actualizado y mantener sincronización + // Cargar brandbook de documents para compilar skill actualizado const { data: brandDoc } = await admin .from('documents') - .select('id, content') + .select('content') .eq('tenant_id', tenantId) .eq('type', 'brandbook') .order('updated_at', { ascending: false }) @@ -267,62 +295,61 @@ export async function PUT(req: NextRequest) { brandbook = seedBrandbook(existing.slug || 'cliente', existing.name || 'Cliente'); } - // Sincronizar campos de normalizedStyle hacia brandbook - if (normalizedStyle.identity) { - if (normalizedStyle.identity.summary) brandbook.elevator_pitch = normalizedStyle.identity.summary; - if (normalizedStyle.identity.website) brandbook.website = normalizedStyle.identity.website; - if (normalizedStyle.identity.b2b_offering) brandbook.b2b_offering = normalizedStyle.identity.b2b_offering; - if (normalizedStyle.identity.b2c_offering) brandbook.b2c_offering = normalizedStyle.identity.b2c_offering; - if (normalizedStyle.identity.usp_differentiators) brandbook.usp_differentiators = normalizedStyle.identity.usp_differentiators; - if (normalizedStyle.identity.current_priorities) brandbook.current_priorities = normalizedStyle.identity.current_priorities; - if (normalizedStyle.identity.price_and_offer_framing) brandbook.price_and_offer_framing = normalizedStyle.identity.price_and_offer_framing; - } - if (normalizedStyle.voice) { - if (normalizedStyle.voice.tone) brandbook.tone = normalizedStyle.voice.tone; - if (normalizedStyle.voice.copy_structure_rules) brandbook.copy_structure_rules = normalizedStyle.voice.copy_structure_rules; - if (normalizedStyle.voice.english_terms_policy) brandbook.english_terms_policy = normalizedStyle.voice.english_terms_policy; - if (normalizedStyle.voice.power_words?.length) brandbook.power_words = normalizedStyle.voice.power_words; - if (normalizedStyle.voice.allowed_ctas?.length) brandbook.allowed_ctas = normalizedStyle.voice.allowed_ctas; - } - if (normalizedStyle.visual) { - if (normalizedStyle.visual.surface_rules) brandbook.surface_rules = normalizedStyle.visual.surface_rules; - if (normalizedStyle.visual.image_text_rules) brandbook.image_text_rules = normalizedStyle.visual.image_text_rules; - if (normalizedStyle.visual.typography_headline) brandbook.typography_headline = normalizedStyle.visual.typography_headline; - if (normalizedStyle.visual.typography_body) brandbook.typography_body = normalizedStyle.visual.typography_body; - } - if (normalizedStyle.audiences) { - if (normalizedStyle.audiences.b2b_personas?.length) brandbook.b2b_personas = normalizedStyle.audiences.b2b_personas; - if (normalizedStyle.audiences.b2c_personas?.length) brandbook.b2c_personas = normalizedStyle.audiences.b2c_personas; - if (normalizedStyle.audiences.customer_frustrations) brandbook.customer_frustrations = normalizedStyle.audiences.customer_frustrations; - if (normalizedStyle.audiences.desired_transformation) brandbook.desired_transformation = normalizedStyle.audiences.desired_transformation; - } - if (normalizedStyle.arguments) { - if (normalizedStyle.arguments.objections?.length) brandbook.objections = normalizedStyle.arguments.objections; - if (normalizedStyle.arguments.winning_hooks?.length) brandbook.winning_hooks = normalizedStyle.arguments.winning_hooks; - if (normalizedStyle.arguments.preferred_angles?.length) brandbook.preferred_angles = normalizedStyle.arguments.preferred_angles; - } - if (normalizedStyle.prohibitions) { - if (normalizedStyle.prohibitions.emoji_policy) brandbook.emoji_policy = normalizedStyle.prohibitions.emoji_policy; - if (normalizedStyle.prohibitions.testimonials_policy) brandbook.testimonials_policy = normalizedStyle.prohibitions.testimonials_policy; - if (normalizedStyle.prohibitions.competitors_never_mention?.length) brandbook.competitors_never_mention = normalizedStyle.prohibitions.competitors_never_mention; - } - if (normalizedStyle.image_ai?.photo_style) { - brandbook.photography_style = normalizedStyle.image_ai.photo_style; - } - brandbook.updated_at = new Date().toISOString(); + // Sincronizar simultáneamente con documents (brandbook) para que Zona de Marca esté siempre al día + const updatedBrandbook: Brandbook = { + ...brandbook, + elevator_pitch: normalizedStyle.identity.summary || brandbook.elevator_pitch, + website: normalizedStyle.identity.website || brandbook.website, + b2b_offering: normalizedStyle.identity.b2b_offering || brandbook.b2b_offering, + b2c_offering: normalizedStyle.identity.b2c_offering || brandbook.b2c_offering, + usp_differentiators: normalizedStyle.identity.usp_differentiators || brandbook.usp_differentiators, + current_priorities: normalizedStyle.identity.current_priorities || brandbook.current_priorities, + price_and_offer_framing: normalizedStyle.identity.price_and_offer_framing || brandbook.price_and_offer_framing, + tone: normalizedStyle.voice.tone || brandbook.tone, + copy_structure_rules: normalizedStyle.voice.copy_structure_rules || brandbook.copy_structure_rules, + english_terms_policy: normalizedStyle.voice.english_terms_policy || brandbook.english_terms_policy, + power_words: normalizedStyle.voice.power_words?.length ? normalizedStyle.voice.power_words : brandbook.power_words, + allowed_ctas: normalizedStyle.voice.allowed_ctas?.length ? normalizedStyle.voice.allowed_ctas : brandbook.allowed_ctas, + surface_rules: normalizedStyle.visual.surface_rules || brandbook.surface_rules, + image_text_rules: normalizedStyle.visual.image_text_rules || brandbook.image_text_rules, + typography_headline: normalizedStyle.visual.typography_headline || brandbook.typography_headline, + typography_body: normalizedStyle.visual.typography_body || brandbook.typography_body, + b2b_personas: normalizedStyle.audiences.b2b_personas?.length ? (normalizedStyle.audiences.b2b_personas as any) : brandbook.b2b_personas, + b2c_personas: normalizedStyle.audiences.b2c_personas?.length ? (normalizedStyle.audiences.b2c_personas as any) : brandbook.b2c_personas, + customer_frustrations: normalizedStyle.audiences.customer_frustrations || brandbook.customer_frustrations, + desired_transformation: normalizedStyle.audiences.desired_transformation || brandbook.desired_transformation, + objections: normalizedStyle.arguments.objections?.length ? (normalizedStyle.arguments.objections as any) : brandbook.objections, + winning_hooks: normalizedStyle.arguments.winning_hooks?.length ? normalizedStyle.arguments.winning_hooks : brandbook.winning_hooks, + preferred_angles: normalizedStyle.arguments.preferred_angles?.length ? normalizedStyle.arguments.preferred_angles : brandbook.preferred_angles, + emoji_policy: normalizedStyle.prohibitions.emoji_policy || brandbook.emoji_policy, + testimonials_policy: normalizedStyle.prohibitions.testimonials_policy || brandbook.testimonials_policy, + competitors_never_mention: normalizedStyle.prohibitions.competitors_never_mention?.length ? normalizedStyle.prohibitions.competitors_never_mention : brandbook.competitors_never_mention, + updated_at: new Date().toISOString(), + }; - if (brandDoc?.id) { + if (brandDoc) { await admin .from('documents') .update({ - content: brandbook, - updated_at: brandbook.updated_at, + content: updatedBrandbook, + updated_at: updatedBrandbook.updated_at, }) - .eq('id', brandDoc.id); + .eq('id', (brandDoc as any).id); + } else { + await admin + .from('documents') + .insert({ + tenant_id: tenantId, + type: 'brandbook', + title: 'Brandbook', + status: 'approved', + content: updatedBrandbook, + updated_at: updatedBrandbook.updated_at, + }); } const compiledMarkdown = compileClientSkillFromBrandbook( - brandbook, + updatedBrandbook, { image_ai: normalizedStyle.image_ai, campaign_rules: normalizedStyle.campaign_rules, diff --git a/src/components/BrandZone.tsx b/src/components/BrandZone.tsx index bb79d31..2d2e95d 100644 --- a/src/components/BrandZone.tsx +++ b/src/components/BrandZone.tsx @@ -170,7 +170,7 @@ export default function BrandZone({ setScore(j.completeness?.score ?? 0); setMissing(j.completeness?.missing || []); setExists(true); - setOkMsg('Brand kit guardado correctamente y sincronizado'); + setOkMsg('Brand kit guardado y sincronizado'); } catch (e) { setErr(e instanceof Error ? e.message : 'Error al guardar'); } finally { diff --git a/src/components/ClientCreativeStylePanel.tsx b/src/components/ClientCreativeStylePanel.tsx index 10c2538..57e3def 100644 --- a/src/components/ClientCreativeStylePanel.tsx +++ b/src/components/ClientCreativeStylePanel.tsx @@ -2,7 +2,6 @@ import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { - ExternalLink, Building2, Users, MessageSquare, @@ -262,30 +261,6 @@ export default function ClientCreativeStylePanel({ return (
- Todo lo que configures aquí (Identidad, Audiencias, Voz, Argumentos, Visual y Operativa) se sincroniza automáticamente con la Zona de Marca y alimenta a Claude en tiempo real. -
-- Todo lo que configures aquí alimenta en tiempo real a Claude (WF3) para el copy y estrategia, a Freepik Mystic para la escena fotográfica y a Canva (WF5) para el montaje automático. + Todo lo que configures aquí alimenta en tiempo real a Claude (WF3) para el copy y estrategia, a Freepik Mystic para la escena fotográfica y a Canva (WF5) para el montaje automático. Sincronizado con la Zona de Marca.