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:
@@ -13,6 +13,10 @@ import {
|
||||
} from 'lucide-react';
|
||||
import type { RrssMonthPlan, RrssPiece } from '@/components/RrssMonthView';
|
||||
import { monthLabel } from '@/lib/calendar-import';
|
||||
import {
|
||||
keepOnlyPieces,
|
||||
mergeKeepAndRegenerate,
|
||||
} from '@/lib/calendar-generate';
|
||||
|
||||
const MONTH_NAMES = [
|
||||
'Enero',
|
||||
@@ -122,6 +126,8 @@ export default function CalendarCreator({
|
||||
const [err, setErr] = useState<string | null>(null);
|
||||
const [dayZoom, setDayZoom] = useState<string | 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 base = ymNow();
|
||||
@@ -162,6 +168,7 @@ export default function CalendarCreator({
|
||||
setRationale('');
|
||||
setSource('');
|
||||
}
|
||||
setKeepIds(new Set());
|
||||
setMonth(ym);
|
||||
setStep('edit');
|
||||
} 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);
|
||||
setErr(null);
|
||||
try {
|
||||
const keptPieces = (plan?.pieces || []).filter((p) => keepIds.has(p.id));
|
||||
const r = await fetch('/api/calendar/preview', {
|
||||
method: 'POST',
|
||||
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();
|
||||
if (!r.ok) throw new Error(j.error || 'No se pudo generar preview');
|
||||
setPlan(j.plan);
|
||||
setRationale(j.rationale || '');
|
||||
setSource(j.source || 'heuristic');
|
||||
if (mode === 'regen_unkept' && plan?.pieces?.length && keepIds.size > 0) {
|
||||
if (keepIds.size >= plan.pieces.length) {
|
||||
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) {
|
||||
setErr(e instanceof Error ? e.message : 'Error preview');
|
||||
} 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) {
|
||||
if (!plan) return;
|
||||
setBusy(true);
|
||||
@@ -389,7 +454,7 @@ export default function CalendarCreator({
|
||||
<button
|
||||
type="button"
|
||||
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"
|
||||
>
|
||||
{busy ? (
|
||||
@@ -399,6 +464,24 @@ export default function CalendarCreator({
|
||||
)}
|
||||
Generar estrategia del mes
|
||||
</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
|
||||
type="button"
|
||||
disabled={busy || !plan?.pieces?.length}
|
||||
@@ -409,6 +492,7 @@ export default function CalendarCreator({
|
||||
</button>
|
||||
<span className="text-[11px] text-gray-500">
|
||||
{plan?.pieces?.length || 0} piezas
|
||||
{keepIds.size ? ` · ${keepIds.size} ✓` : ''}
|
||||
{source ? ` · ${source}` : ''}
|
||||
</span>
|
||||
</div>
|
||||
@@ -422,10 +506,10 @@ export default function CalendarCreator({
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-xs text-gray-500">
|
||||
Estrategia engagement 2026: tendencias del mes + Zona de
|
||||
marca / brandbook del cliente (crea ángulos nuevos, no
|
||||
copia el histórico). Completa el brandbook en Marca para
|
||||
mejores temas. O haz click en un día para añadir a mano.
|
||||
Marca ✓ las piezas o días que te gustan. Luego:{' '}
|
||||
<strong>Regenerar no marcadas</strong> (v2 solo de las
|
||||
malas) o <strong>Solo quedarme con ✓</strong> (borra el
|
||||
resto). También puedes editar/borrar día a día.
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -457,27 +541,62 @@ export default function CalendarCreator({
|
||||
const day = i + 1;
|
||||
const key = `${month}-${String(day).padStart(2, '0')}`;
|
||||
const hits = byDate.get(key) || [];
|
||||
const dayKept =
|
||||
hits.length > 0 && hits.every((p) => keepIds.has(p.id));
|
||||
return (
|
||||
<button
|
||||
<div
|
||||
key={key}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
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"
|
||||
className={`bg-white min-h-[88px] p-1.5 text-left flex flex-col gap-1 ${
|
||||
dayKept ? 'ring-1 ring-inset ring-emerald-400' : ''
|
||||
}`}
|
||||
>
|
||||
<span className="text-[11px] font-bold text-gray-700">
|
||||
{day}
|
||||
</span>
|
||||
<div className="flex flex-col gap-0.5 flex-1">
|
||||
<div className="flex items-center justify-between gap-1">
|
||||
<button
|
||||
type="button"
|
||||
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) => (
|
||||
<span
|
||||
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) }}
|
||||
title={p.title}
|
||||
>
|
||||
{keepIds.has(p.id) ? '✓ ' : ''}
|
||||
{p.title}
|
||||
</span>
|
||||
))}
|
||||
@@ -486,8 +605,8 @@ export default function CalendarCreator({
|
||||
+{hits.length - 3}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</button>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
@@ -541,14 +660,23 @@ export default function CalendarCreator({
|
||||
dayPieces.map((p) => (
|
||||
<div
|
||||
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>
|
||||
<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>
|
||||
<p className="text-[11px] text-gray-500">
|
||||
</label>
|
||||
<p className="text-[11px] text-gray-500 pl-6">
|
||||
{p.platform} · {p.format} · {p.time || '—'}
|
||||
{p.pillar ? ` · ${p.pillar}` : ''}
|
||||
</p>
|
||||
|
||||
Reference in New Issue
Block a user