Phase 02: Prompt Control + Output Review - 2 plan(s) in 2 wave(s) - Wave 1: 02-01 (Prompt Editor backend+frontend) - Wave 2: 02-02 (Per-item regen + summary counter) - Ready for execution
14 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 02-prompt-control-output-review | 02 | execute | 2 |
|
|
true |
|
Purpose: L'utente puo' rigenerare singoli post insoddisfacenti direttamente dalla Output Review, con la possibilita' di specificare un topic diverso o note aggiuntive. Il summary counter fornisce una overview rapida dello stato del batch. Copre il terzo success criterion della Phase 2.
Output: PostCard.tsx potenziato con regen popover + OutputReview.tsx con tracking conteggi
<execution_context> @C:\Users\miche.claude/get-shit-done/workflows/execute-plan.md @C:\Users\miche.claude/get-shit-done/templates/summary.md </execution_context>
@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/02-prompt-control-output-review/02-01-SUMMARY.md @frontend/src/components/PostCard.tsx @frontend/src/pages/OutputReview.tsx @frontend/src/api/hooks.ts @frontend/src/types.ts Task 1: PostCard — bottone Rigenera con popover inline, topic override, badge rigenerato frontend/src/components/PostCard.tsx Modifica `frontend/src/components/PostCard.tsx` per aggiungere il bottone Rigenera e il popover inline sui post con status=success.Nuove props — Aggiungi alla interface PostCardProps:
/** True se questo post e' stato rigenerato (non originale) */
isRegenerated?: boolean
Stato interno — Aggiungi:
const [showRegenForm, setShowRegenForm] = useState(false)
const [regenTopic, setRegenTopic] = useState('')
const [regenNotes, setRegenNotes] = useState('')
Funzione handleRegen — Sostituisce/affianca handleRetry per i post success:
async function handleRegen() {
if (!slot) return
// Se l'utente ha specificato un topic, usalo come override
const overriddenSlot = regenTopic.trim()
? { ...slot, topic: regenTopic.trim() }
: slot
const req: GenerateRequest = {
slot: overriddenSlot,
obiettivo_campagna: obiettivoCampagna,
brand_name: brandName,
tono: regenNotes.trim() || tono, // Se note fornite, usale come tono override
}
try {
const newResult = await generateSingle.mutateAsync(req)
onRegenerated({ ...newResult, slot })
setShowRegenForm(false)
setRegenTopic('')
setRegenNotes('')
} catch {
// Errore gestito da generateSingle.error
}
}
NOTA: Il campo tono nella GenerateRequest viene usato per passare note aggiuntive dell'utente. Questo funziona perche' il tono viene iniettato nel prompt come contesto aggiuntivo — e' il modo piu' pragmatico per influenzare la rigenerazione senza aggiungere un nuovo campo al backend.
Sezione SUCCESS della card — Modifica la sezione che renderizza i post con status=success:
-
Badge rigenerato — Se
isRegeneratede' true, mostra un'iconaRefreshCw(gia' importata) accanto al numero slot:{isRegenerated && ( <span className="text-amber-400" title="Post rigenerato"> <RefreshCw size={12} /> </span> )}Posiziona il badge nella riga dei badge, dopo
#N, prima dei BadgePN/BadgeSchwartz. -
Bottone Rigenera nell'header della card success — Aggiungi un piccolo bottone accanto alla chevron expand:
<button onClick={(e) => { e.stopPropagation() // Non togglare expand setShowRegenForm(v => !v) }} className="text-stone-500 hover:text-amber-400 transition-colors flex-shrink-0" title="Rigenera questo post" > <RefreshCw size={14} /> </button>Posiziona PRIMA della chevron expand/collapse nel div flex justify-between.
-
Popover/form inline — Sotto l'header della card, se
showRegenForme' true, mostra un form leggero:{showRegenForm && ( <div className="px-4 pb-3 border-t border-stone-700/50 pt-3 space-y-3"> <div> <label className="text-xs text-stone-400 block mb-1"> Topic alternativo <span className="text-stone-600">(opzionale)</span> </label> <input type="text" value={regenTopic} onChange={(e) => setRegenTopic(e.target.value)} placeholder="Es: 3 errori comuni nel marketing digitale" className="w-full px-3 py-2 rounded-lg bg-stone-900 border border-stone-700 text-sm text-stone-200 placeholder:text-stone-600 focus:border-amber-500/50 focus:outline-none" /> </div> <div> <label className="text-xs text-stone-400 block mb-1"> Note aggiuntive <span className="text-stone-600">(opzionale)</span> </label> <input type="text" value={regenNotes} onChange={(e) => setRegenNotes(e.target.value)} placeholder="Es: tono piu' provocatorio, focus su ROI" className="w-full px-3 py-2 rounded-lg bg-stone-900 border border-stone-700 text-sm text-stone-200 placeholder:text-stone-600 focus:border-amber-500/50 focus:outline-none" /> </div> <div className="flex items-center gap-2"> <button onClick={handleRegen} disabled={generateSingle.isPending} className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-amber-500 text-stone-950 text-xs font-semibold hover:bg-amber-400 disabled:opacity-50 transition-colors" > {generateSingle.isPending ? ( <Loader2 size={12} className="animate-spin" /> ) : ( <RefreshCw size={12} /> )} {generateSingle.isPending ? 'Rigenerazione...' : 'Rigenera'} </button> <button onClick={() => { setShowRegenForm(false) setRegenTopic('') setRegenNotes('') }} className="px-3 py-1.5 rounded-lg text-xs text-stone-400 hover:text-stone-200 transition-colors" > Annulla </button> </div> {generateSingle.error && ( <p className="text-xs text-red-400">{generateSingle.error.message}</p> )} </div> )}
Il popover form appare SOTTO l'header ma SOPRA la sezione espansa (SlideViewer). Posizionalo tra il </button> dell'header e il blocco {expanded && ...}.
handleRetry per post FAILED — Rimane invariato (gia' funzionante). handleRetry e' per i post falliti, handleRegen e' per i post success.
Verifica che PostCard.tsx contenga: handleRegen, showRegenForm, isRegenerated, regenTopic, regenNotes.
Verifica che la prop isRegenerated sia nella interface PostCardProps.
Esegui cd lab/postgenerator/frontend && npx tsc --noEmit per verificare che TypeScript compili.
PostCard mostra un bottone Rigenera su ogni post success, con un form inline per topic/note override. I post rigenerati hanno un badge icona visivo. Il form e' leggero (inline, non modale). La rigenerazione funziona tramite l'endpoint POST /api/generate/single gia' esistente.
Stato tracking — Aggiungi un Set per tracciare gli indici dei post rigenerati:
const [regeneratedSlots, setRegeneratedSlots] = useState<Set<number>>(new Set())
Aggiorna handleRegenerated per tracciare i post rigenerati:
function handleRegenerated(updated: PostResult) {
setLocalResults((prev) =>
prev.map((r) => (r.slot_index === updated.slot_index ? updated : r))
)
setRegeneratedSlots((prev) => new Set(prev).add(updated.slot_index))
}
Calcolo conteggi per il summary counter:
const successCount = localResults.filter((r) => r.status === 'success').length
const failedCount = localResults.filter((r) => r.status === 'failed').length
const regeneratedCount = regeneratedSlots.size
// "Modificato manualmente" = post con status success che sono cambiati rispetto ai dati originali
// Per semplicita', contiamo solo i post che hanno ricevuto edit inline (diversi dal jobData originale)
const editedCount = localResults.filter((r, i) => {
if (r.status !== 'success' || !r.post) return false
const original = jobData?.results?.[i]
if (!original?.post) return false
// Confronto shallow: se qualsiasi campo e' diverso, e' stato editato
return JSON.stringify(r.post) !== JSON.stringify(original.post)
}).length
// Sottrai i rigenerati dagli editati per evitare doppio conteggio
const manuallyEditedCount = Math.max(0, editedCount - regeneratedCount)
NOTA: I conteggi editedCount e manuallyEditedCount usano un confronto JSON.stringify pragmatico. Non e' il piu' performante, ma con 13 post e' trascurabile. Funziona perche' localResults tiene le versioni correnti e jobData.results tiene gli originali dal server.
Summary counter UI — Sostituisci la sezione header stats esistente (le righe con {successCount} generati e {failedCount} falliti) con:
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 mt-2">
<span className="text-xs text-stone-300 font-medium">{localResults.length} post</span>
<span className="text-xs text-emerald-400">{successCount} generati</span>
{failedCount > 0 && (
<span className="text-xs text-red-400">{failedCount} falliti</span>
)}
{regeneratedCount > 0 && (
<span className="text-xs text-amber-400 flex items-center gap-1">
<RefreshCw size={10} />
{regeneratedCount} rigenerati
</span>
)}
{manuallyEditedCount > 0 && (
<span className="text-xs text-blue-400">
{manuallyEditedCount} modificati
</span>
)}
<span className="text-xs text-stone-600">job: {jobId}</span>
</div>
Aggiungi RefreshCw agli import da lucide-react.
Passare isRegenerated a PostCard — Nella griglia dei post, passa la prop:
localResults.map((result) => (
<PostCard
key={result.slot_index}
result={result}
obiettivoCampagna={jobData.campagna}
brandName={null}
tono={null}
onRegenerated={handleRegenerated}
onEdit={handleEdit}
isRegenerated={regeneratedSlots.has(result.slot_index)}
/>
))
Aggiorna il messaggio informativo sotto l'header — Modifica il box info per includere regen:
<div className="px-4 py-2.5 rounded-lg bg-stone-800/50 border border-stone-700 text-xs text-stone-500">
Clicca su una card per espandere le slide. I campi di testo sono editabili inline.
Usa il pulsante <RefreshCw size={10} className="inline" /> per rigenerare singoli post con topic diversi.
Le modifiche saranno incluse nel CSV scaricato.
</div>
<success_criteria>
- L'utente puo' rigenerare un singolo post dalla Output Review specificando un topic diverso o note aggiuntive
- I post rigenerati sono visivamente distinguibili con un badge icona
- Il summary counter mostra una overview dello stato del batch in tempo reale </success_criteria>