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:
4Ventures
2026-10-13 13:57:13 +02:00
parent 714f250eca
commit 2b57b93220
4 changed files with 258 additions and 30 deletions

View File

@@ -35,6 +35,8 @@ export type GeneratePreviewInput = {
brandbook?: Brandbook | null;
/** Publicaciones reales (pipeline / IG) además del histórico de calendarios. */
publishedPosts?: PublishedNetworkPost[];
/** Títulos / hooks a no repetir (piezas que el usuario conserva). */
avoidTitles?: string[];
};
export type GeneratePreviewResult = {
@@ -88,6 +90,7 @@ async function anthropicStrategist(
label: input.label,
dna,
trends,
avoidTitles: input.avoidTitles || [],
});
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 };

View File

@@ -860,6 +860,7 @@ export function strategistSystemPrompt(input: {
label: string;
dna: ClientDna;
trends: TrendBeat[];
avoidTitles?: string[];
}): string {
const netBlock =
input.dna.networkSamples.length > 0
@@ -867,6 +868,13 @@ export function strategistSystemPrompt(input: {
.map((s) => `- ${s}`)
.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}).
NO copies títulos viejos ni captiones publicadas. Usa como constraint creativo:
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}.
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` : ''}
${netBlock}
${netBlock}${avoidBlock}
Playbook 2026 ES: ${PLAYBOOK_2026.principle}
Tendencias del mes:
${input.trends.map((t) => `- ${t.label}: ${t.why}`).join('\n')}