Allow keeping selected calendar pieces/days and regenerating the rest.
Agency can mark good stories, keep only those days, or request a partial v2 for unmarked slots without replacing the whole month. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -41,11 +41,20 @@ export async function POST(req: NextRequest) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const body = (await req.json().catch(() => ({}))) as { month?: string };
|
const body = (await req.json().catch(() => ({}))) as {
|
||||||
|
month?: string;
|
||||||
|
avoid_titles?: string[];
|
||||||
|
};
|
||||||
const month = String(body.month || '').slice(0, 7);
|
const month = String(body.month || '').slice(0, 7);
|
||||||
if (!/^\d{4}-\d{2}$/.test(month)) {
|
if (!/^\d{4}-\d{2}$/.test(month)) {
|
||||||
return NextResponse.json({ error: 'month YYYY-MM requerido' }, { status: 400 });
|
return NextResponse.json({ error: 'month YYYY-MM requerido' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
const avoidTitles = Array.isArray(body.avoid_titles)
|
||||||
|
? body.avoid_titles
|
||||||
|
.filter((t): t is string => typeof t === 'string' && t.trim().length > 0)
|
||||||
|
.map((t) => t.trim().slice(0, 160))
|
||||||
|
.slice(0, 40)
|
||||||
|
: [];
|
||||||
|
|
||||||
let slug = 'cliente';
|
let slug = 'cliente';
|
||||||
const t = await restGet<{ slug?: string }[]>(
|
const t = await restGet<{ slug?: string }[]>(
|
||||||
@@ -122,6 +131,7 @@ export async function POST(req: NextRequest) {
|
|||||||
history,
|
history,
|
||||||
brandbook,
|
brandbook,
|
||||||
publishedPosts,
|
publishedPosts,
|
||||||
|
avoidTitles,
|
||||||
});
|
});
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import type { RrssMonthPlan, RrssPiece } from '@/components/RrssMonthView';
|
import type { RrssMonthPlan, RrssPiece } from '@/components/RrssMonthView';
|
||||||
import { monthLabel } from '@/lib/calendar-import';
|
import { monthLabel } from '@/lib/calendar-import';
|
||||||
|
import {
|
||||||
|
keepOnlyPieces,
|
||||||
|
mergeKeepAndRegenerate,
|
||||||
|
} from '@/lib/calendar-generate';
|
||||||
|
|
||||||
const MONTH_NAMES = [
|
const MONTH_NAMES = [
|
||||||
'Enero',
|
'Enero',
|
||||||
@@ -122,6 +126,8 @@ export default function CalendarCreator({
|
|||||||
const [err, setErr] = useState<string | null>(null);
|
const [err, setErr] = useState<string | null>(null);
|
||||||
const [dayZoom, setDayZoom] = useState<string | null>(null);
|
const [dayZoom, setDayZoom] = useState<string | null>(null);
|
||||||
const [form, setForm] = useState<PieceForm | null>(null);
|
const [form, setForm] = useState<PieceForm | null>(null);
|
||||||
|
/** Piezas a conservar (✓) en selección parcial */
|
||||||
|
const [keepIds, setKeepIds] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
const monthChoices = useMemo(() => {
|
const monthChoices = useMemo(() => {
|
||||||
const base = ymNow();
|
const base = ymNow();
|
||||||
@@ -162,6 +168,7 @@ export default function CalendarCreator({
|
|||||||
setRationale('');
|
setRationale('');
|
||||||
setSource('');
|
setSource('');
|
||||||
}
|
}
|
||||||
|
setKeepIds(new Set());
|
||||||
setMonth(ym);
|
setMonth(ym);
|
||||||
setStep('edit');
|
setStep('edit');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -171,20 +178,64 @@ export default function CalendarCreator({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runPreview() {
|
function toggleKeep(id: string) {
|
||||||
|
setKeepIds((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
if (next.has(id)) next.delete(id);
|
||||||
|
else next.add(id);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleKeepDay(date: string) {
|
||||||
|
const ids = (byDate.get(date) || []).map((p) => p.id);
|
||||||
|
if (!ids.length) return;
|
||||||
|
setKeepIds((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
const allOn = ids.every((id) => next.has(id));
|
||||||
|
if (allOn) ids.forEach((id) => next.delete(id));
|
||||||
|
else ids.forEach((id) => next.add(id));
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runPreview(mode: 'replace' | 'regen_unkept' = 'replace') {
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
setErr(null);
|
setErr(null);
|
||||||
try {
|
try {
|
||||||
|
const keptPieces = (plan?.pieces || []).filter((p) => keepIds.has(p.id));
|
||||||
const r = await fetch('/api/calendar/preview', {
|
const r = await fetch('/api/calendar/preview', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ month }),
|
body: JSON.stringify({
|
||||||
|
month,
|
||||||
|
avoid_titles: keptPieces
|
||||||
|
.map((p) => p.title || p.hook || '')
|
||||||
|
.filter(Boolean),
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
const j = await r.json();
|
const j = await r.json();
|
||||||
if (!r.ok) throw new Error(j.error || 'No se pudo generar preview');
|
if (!r.ok) throw new Error(j.error || 'No se pudo generar preview');
|
||||||
setPlan(j.plan);
|
if (mode === 'regen_unkept' && plan?.pieces?.length && keepIds.size > 0) {
|
||||||
setRationale(j.rationale || '');
|
if (keepIds.size >= plan.pieces.length) {
|
||||||
setSource(j.source || 'heuristic');
|
throw new Error(
|
||||||
|
'Desmarca al menos una pieza (o un día) para regenerar solo las malas.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const merged = mergeKeepAndRegenerate(plan, j.plan, keepIds);
|
||||||
|
setPlan(merged);
|
||||||
|
setRationale(
|
||||||
|
`${j.rationale || ''}\n\nSelección: ${keepIds.size} conservadas · ${
|
||||||
|
merged.pieces.length - keepIds.size
|
||||||
|
} regeneradas.`
|
||||||
|
);
|
||||||
|
setSource(`${j.source || 'heuristic'}+parcial`);
|
||||||
|
} else {
|
||||||
|
setPlan(j.plan);
|
||||||
|
setRationale(j.rationale || '');
|
||||||
|
setSource(j.source || 'heuristic');
|
||||||
|
setKeepIds(new Set());
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setErr(e instanceof Error ? e.message : 'Error preview');
|
setErr(e instanceof Error ? e.message : 'Error preview');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -192,6 +243,20 @@ export default function CalendarCreator({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function keepOnlySelected() {
|
||||||
|
if (!plan?.pieces?.length) return;
|
||||||
|
if (keepIds.size === 0) {
|
||||||
|
setErr('Marca al menos una pieza o un día para quedarte solo con eso.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setPlan(keepOnlyPieces(plan, keepIds));
|
||||||
|
setRationale(
|
||||||
|
`Selección manual: te quedas con ${keepIds.size} pieza(s). Guarda el mes cuando quieras.`
|
||||||
|
);
|
||||||
|
setSource('seleccion');
|
||||||
|
setErr(null);
|
||||||
|
}
|
||||||
|
|
||||||
async function savePlan(force = false) {
|
async function savePlan(force = false) {
|
||||||
if (!plan) return;
|
if (!plan) return;
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
@@ -389,7 +454,7 @@ export default function CalendarCreator({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
onClick={() => void runPreview()}
|
onClick={() => void runPreview('replace')}
|
||||||
className="inline-flex items-center gap-1.5 rounded-lg bg-violet-600 px-3 py-1.5 text-xs font-semibold text-white hover:bg-violet-700 disabled:opacity-60"
|
className="inline-flex items-center gap-1.5 rounded-lg bg-violet-600 px-3 py-1.5 text-xs font-semibold text-white hover:bg-violet-700 disabled:opacity-60"
|
||||||
>
|
>
|
||||||
{busy ? (
|
{busy ? (
|
||||||
@@ -399,6 +464,24 @@ export default function CalendarCreator({
|
|||||||
)}
|
)}
|
||||||
Generar estrategia del mes
|
Generar estrategia del mes
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={busy || !plan?.pieces?.length || keepIds.size === 0}
|
||||||
|
onClick={() => void runPreview('regen_unkept')}
|
||||||
|
title="Conserva las ✓ y pide ángulos nuevos para el resto"
|
||||||
|
className="inline-flex items-center gap-1.5 rounded-lg bg-sky-600 px-3 py-1.5 text-xs font-semibold text-white hover:bg-sky-700 disabled:opacity-60"
|
||||||
|
>
|
||||||
|
Regenerar no marcadas
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={busy || keepIds.size === 0}
|
||||||
|
onClick={() => keepOnlySelected()}
|
||||||
|
title="Borra del preview todo lo que no esté marcado"
|
||||||
|
className="rounded-lg border border-amber-300 bg-amber-50 px-3 py-1.5 text-xs font-semibold text-amber-900 disabled:opacity-60"
|
||||||
|
>
|
||||||
|
Solo quedarme con ✓
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={busy || !plan?.pieces?.length}
|
disabled={busy || !plan?.pieces?.length}
|
||||||
@@ -409,6 +492,7 @@ export default function CalendarCreator({
|
|||||||
</button>
|
</button>
|
||||||
<span className="text-[11px] text-gray-500">
|
<span className="text-[11px] text-gray-500">
|
||||||
{plan?.pieces?.length || 0} piezas
|
{plan?.pieces?.length || 0} piezas
|
||||||
|
{keepIds.size ? ` · ${keepIds.size} ✓` : ''}
|
||||||
{source ? ` · ${source}` : ''}
|
{source ? ` · ${source}` : ''}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -422,10 +506,10 @@ export default function CalendarCreator({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-xs text-gray-500">
|
<p className="text-xs text-gray-500">
|
||||||
Estrategia engagement 2026: tendencias del mes + Zona de
|
Marca ✓ las piezas o días que te gustan. Luego:{' '}
|
||||||
marca / brandbook del cliente (crea ángulos nuevos, no
|
<strong>Regenerar no marcadas</strong> (v2 solo de las
|
||||||
copia el histórico). Completa el brandbook en Marca para
|
malas) o <strong>Solo quedarme con ✓</strong> (borra el
|
||||||
mejores temas. O haz click en un día para añadir a mano.
|
resto). También puedes editar/borrar día a día.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -457,27 +541,62 @@ export default function CalendarCreator({
|
|||||||
const day = i + 1;
|
const day = i + 1;
|
||||||
const key = `${month}-${String(day).padStart(2, '0')}`;
|
const key = `${month}-${String(day).padStart(2, '0')}`;
|
||||||
const hits = byDate.get(key) || [];
|
const hits = byDate.get(key) || [];
|
||||||
|
const dayKept =
|
||||||
|
hits.length > 0 && hits.every((p) => keepIds.has(p.id));
|
||||||
return (
|
return (
|
||||||
<button
|
<div
|
||||||
key={key}
|
key={key}
|
||||||
type="button"
|
className={`bg-white min-h-[88px] p-1.5 text-left flex flex-col gap-1 ${
|
||||||
onClick={() => {
|
dayKept ? 'ring-1 ring-inset ring-emerald-400' : ''
|
||||||
setDayZoom(key);
|
}`}
|
||||||
setForm(null);
|
|
||||||
}}
|
|
||||||
className="bg-white min-h-[88px] p-1.5 text-left hover:bg-sky-50/80 transition flex flex-col gap-1"
|
|
||||||
>
|
>
|
||||||
<span className="text-[11px] font-bold text-gray-700">
|
<div className="flex items-center justify-between gap-1">
|
||||||
{day}
|
<button
|
||||||
</span>
|
type="button"
|
||||||
<div className="flex flex-col gap-0.5 flex-1">
|
onClick={() => {
|
||||||
|
setDayZoom(key);
|
||||||
|
setForm(null);
|
||||||
|
}}
|
||||||
|
className="text-[11px] font-bold text-gray-700 hover:text-sky-700"
|
||||||
|
>
|
||||||
|
{day}
|
||||||
|
</button>
|
||||||
|
{hits.length > 0 ? (
|
||||||
|
<label
|
||||||
|
className="flex items-center gap-0.5 text-[9px] text-emerald-700 cursor-pointer"
|
||||||
|
title="Marcar/desmarcar todo el día"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={dayKept}
|
||||||
|
onChange={() => toggleKeepDay(key)}
|
||||||
|
className="h-3 w-3"
|
||||||
|
/>
|
||||||
|
día
|
||||||
|
</label>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setDayZoom(key);
|
||||||
|
setForm(null);
|
||||||
|
}}
|
||||||
|
className="flex flex-col gap-0.5 flex-1 text-left hover:bg-sky-50/80 rounded"
|
||||||
|
>
|
||||||
{hits.slice(0, 3).map((p) => (
|
{hits.slice(0, 3).map((p) => (
|
||||||
<span
|
<span
|
||||||
key={p.id}
|
key={p.id}
|
||||||
className="truncate rounded px-1 py-0.5 text-[9px] font-semibold text-white"
|
className={`truncate rounded px-1 py-0.5 text-[9px] font-semibold text-white ${
|
||||||
|
keepIds.has(p.id)
|
||||||
|
? 'outline outline-1 outline-offset-[-1px] outline-white'
|
||||||
|
: ''
|
||||||
|
}`}
|
||||||
style={{ background: colorFor(p) }}
|
style={{ background: colorFor(p) }}
|
||||||
title={p.title}
|
title={p.title}
|
||||||
>
|
>
|
||||||
|
{keepIds.has(p.id) ? '✓ ' : ''}
|
||||||
{p.title}
|
{p.title}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
@@ -486,8 +605,8 @@ export default function CalendarCreator({
|
|||||||
+{hits.length - 3}
|
+{hits.length - 3}
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</button>
|
||||||
</button>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
@@ -541,14 +660,23 @@ export default function CalendarCreator({
|
|||||||
dayPieces.map((p) => (
|
dayPieces.map((p) => (
|
||||||
<div
|
<div
|
||||||
key={p.id}
|
key={p.id}
|
||||||
className="rounded-xl border border-gray-200 p-3 space-y-2"
|
className={`rounded-xl border p-3 space-y-2 ${
|
||||||
|
keepIds.has(p.id)
|
||||||
|
? 'border-emerald-300 bg-emerald-50/40'
|
||||||
|
: 'border-gray-200'
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-start justify-between gap-2">
|
<div className="flex items-start justify-between gap-2">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-bold text-gray-900">
|
<label className="flex items-center gap-2 text-sm font-bold text-gray-900 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={keepIds.has(p.id)}
|
||||||
|
onChange={() => toggleKeep(p.id)}
|
||||||
|
/>
|
||||||
{p.title}
|
{p.title}
|
||||||
</p>
|
</label>
|
||||||
<p className="text-[11px] text-gray-500">
|
<p className="text-[11px] text-gray-500 pl-6">
|
||||||
{p.platform} · {p.format} · {p.time || '—'}
|
{p.platform} · {p.format} · {p.time || '—'}
|
||||||
{p.pillar ? ` · ${p.pillar}` : ''}
|
{p.pillar ? ` · ${p.pillar}` : ''}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ export type GeneratePreviewInput = {
|
|||||||
brandbook?: Brandbook | null;
|
brandbook?: Brandbook | null;
|
||||||
/** Publicaciones reales (pipeline / IG) además del histórico de calendarios. */
|
/** Publicaciones reales (pipeline / IG) además del histórico de calendarios. */
|
||||||
publishedPosts?: PublishedNetworkPost[];
|
publishedPosts?: PublishedNetworkPost[];
|
||||||
|
/** Títulos / hooks a no repetir (piezas que el usuario conserva). */
|
||||||
|
avoidTitles?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GeneratePreviewResult = {
|
export type GeneratePreviewResult = {
|
||||||
@@ -88,6 +90,7 @@ async function anthropicStrategist(
|
|||||||
label: input.label,
|
label: input.label,
|
||||||
dna,
|
dna,
|
||||||
trends,
|
trends,
|
||||||
|
avoidTitles: input.avoidTitles || [],
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -216,4 +219,83 @@ export function mergePreviewIntoPlan(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Conserva piezas marcadas y sustituye el resto con un preview nuevo
|
||||||
|
* (prioriza mismo día; si faltan, rellena con otras del preview).
|
||||||
|
*/
|
||||||
|
export function mergeKeepAndRegenerate(
|
||||||
|
existing: RrssMonthPlan,
|
||||||
|
preview: RrssMonthPlan,
|
||||||
|
keepIds: Iterable<string>
|
||||||
|
): RrssMonthPlan {
|
||||||
|
const keep = new Set(keepIds);
|
||||||
|
const kept = (existing.pieces || []).filter((p) => keep.has(p.id));
|
||||||
|
const keptDates = new Set(kept.map((p) => p.date.slice(0, 10)));
|
||||||
|
|
||||||
|
const needByDate = new Map<string, number>();
|
||||||
|
for (const p of existing.pieces || []) {
|
||||||
|
if (keep.has(p.id)) continue;
|
||||||
|
const d = p.date.slice(0, 10);
|
||||||
|
needByDate.set(d, (needByDate.get(d) || 0) + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const usedPreview = new Set<string>();
|
||||||
|
const replacements: RrssPiece[] = [];
|
||||||
|
|
||||||
|
const takeFromPreview = (preferDate: string | null, n: number): RrssPiece[] => {
|
||||||
|
const pool = (preview.pieces || []).filter((p) => !usedPreview.has(p.id));
|
||||||
|
const same = preferDate
|
||||||
|
? pool.filter((p) => p.date.slice(0, 10) === preferDate)
|
||||||
|
: [];
|
||||||
|
const rest = pool.filter((p) => p.date.slice(0, 10) !== preferDate);
|
||||||
|
const picked = [...same, ...rest].slice(0, n);
|
||||||
|
for (const p of picked) usedPreview.add(p.id);
|
||||||
|
return picked;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const [date, n] of needByDate) {
|
||||||
|
const picked = takeFromPreview(date, n);
|
||||||
|
picked.forEach((p, i) => {
|
||||||
|
replacements.push({
|
||||||
|
...p,
|
||||||
|
id: newPieceId(date, `${p.title}-${i}`),
|
||||||
|
date,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...existing,
|
||||||
|
pieces: [...kept, ...replacements].sort((a, b) =>
|
||||||
|
a.date.localeCompare(b.date)
|
||||||
|
),
|
||||||
|
hero: {
|
||||||
|
...existing.hero,
|
||||||
|
...(preview.hero || {}),
|
||||||
|
chips: [
|
||||||
|
`${kept.length} conservadas`,
|
||||||
|
`${replacements.length} regeneradas`,
|
||||||
|
...(preview.hero?.chips || []).slice(0, 2),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Deja solo las piezas/días marcados. */
|
||||||
|
export function keepOnlyPieces(
|
||||||
|
existing: RrssMonthPlan,
|
||||||
|
keepIds: Iterable<string>
|
||||||
|
): RrssMonthPlan {
|
||||||
|
const keep = new Set(keepIds);
|
||||||
|
const pieces = (existing.pieces || []).filter((p) => keep.has(p.id));
|
||||||
|
return {
|
||||||
|
...existing,
|
||||||
|
pieces,
|
||||||
|
hero: {
|
||||||
|
...existing.hero,
|
||||||
|
chips: [`${pieces.length} piezas`, 'selección manual'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export { monthLabel };
|
export { monthLabel };
|
||||||
|
|||||||
@@ -860,6 +860,7 @@ export function strategistSystemPrompt(input: {
|
|||||||
label: string;
|
label: string;
|
||||||
dna: ClientDna;
|
dna: ClientDna;
|
||||||
trends: TrendBeat[];
|
trends: TrendBeat[];
|
||||||
|
avoidTitles?: string[];
|
||||||
}): string {
|
}): string {
|
||||||
const netBlock =
|
const netBlock =
|
||||||
input.dna.networkSamples.length > 0
|
input.dna.networkSamples.length > 0
|
||||||
@@ -867,6 +868,13 @@ export function strategistSystemPrompt(input: {
|
|||||||
.map((s) => `- ${s}`)
|
.map((s) => `- ${s}`)
|
||||||
.join('\n')}\n`
|
.join('\n')}\n`
|
||||||
: '';
|
: '';
|
||||||
|
const avoid = (input.avoidTitles || []).filter(Boolean).slice(0, 40);
|
||||||
|
const avoidBlock =
|
||||||
|
avoid.length > 0
|
||||||
|
? `\nPIEZAS QUE EL CLIENTE YA CONSERVA (no repitas estos títulos ni el mismo ángulo):\n${avoid
|
||||||
|
.map((t) => `- ${t}`)
|
||||||
|
.join('\n')}\n`
|
||||||
|
: '';
|
||||||
return `Eres Head of Social de 4Ventures. Crea un calendario NUEVO para ${input.label} (${input.ym}).
|
return `Eres Head of Social de 4Ventures. Crea un calendario NUEVO para ${input.label} (${input.ym}).
|
||||||
NO copies títulos viejos ni captiones publicadas. Usa como constraint creativo:
|
NO copies títulos viejos ni captiones publicadas. Usa como constraint creativo:
|
||||||
1) Zona de marca / brandbook
|
1) Zona de marca / brandbook
|
||||||
@@ -876,7 +884,7 @@ ADN series: ${input.dna.signatureSeries.join(', ')}.
|
|||||||
Voz: ${input.dna.brand.tone}. Vertical: ${input.dna.brand.vertical}.
|
Voz: ${input.dna.brand.tone}. Vertical: ${input.dna.brand.vertical}.
|
||||||
Volumen guía: ~${input.dna.avgPerWeek} piezas/semana · Stories ratio hist. ${(input.dna.storiesRatio * 100).toFixed(0)}%.
|
Volumen guía: ~${input.dna.avgPerWeek} piezas/semana · Stories ratio hist. ${(input.dna.storiesRatio * 100).toFixed(0)}%.
|
||||||
${input.dna.brandbookPrompt ? `\nBRANDBOOK:\n${input.dna.brandbookPrompt}\n` : ''}
|
${input.dna.brandbookPrompt ? `\nBRANDBOOK:\n${input.dna.brandbookPrompt}\n` : ''}
|
||||||
${netBlock}
|
${netBlock}${avoidBlock}
|
||||||
Playbook 2026 ES: ${PLAYBOOK_2026.principle}
|
Playbook 2026 ES: ${PLAYBOOK_2026.principle}
|
||||||
Tendencias del mes:
|
Tendencias del mes:
|
||||||
${input.trends.map((t) => `- ${t.label}: ${t.why}`).join('\n')}
|
${input.trends.map((t) => `- ${t.label}: ${t.why}`).join('\n')}
|
||||||
|
|||||||
Reference in New Issue
Block a user