docs(02): create phase plan

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
This commit is contained in:
Michele
2026-03-08 20:17:29 +01:00
parent c22d9dde97
commit 5ba641e7d6
3 changed files with 651 additions and 3 deletions

View File

@@ -0,0 +1,333 @@
---
phase: 02-prompt-control-output-review
plan: 02
type: execute
wave: 2
depends_on: ["02-01"]
files_modified:
- frontend/src/components/PostCard.tsx
- frontend/src/pages/OutputReview.tsx
autonomous: true
must_haves:
truths:
- "Ogni PostCard con status=success mostra un bottone Rigenera visibile nell'header della card"
- "Cliccando Rigenera si apre un popover/inline form leggero con campo topic opzionale e note opzionali"
- "Dopo la rigenerazione, il post aggiornato sostituisce quello originale nella griglia"
- "I post rigenerati hanno un badge visivo (icona freccia circolare) che li distingue dai post originali"
- "Un summary counter in cima alla Output Review mostra: N post - X rigenerati - Y modificati manualmente"
- "Il summary counter si aggiorna in tempo reale dopo ogni rigenerazione o modifica"
- "Il CSV scaricato contiene sempre le ultime versioni dei post (originali, rigenerati, e modificati inline)"
artifacts:
- path: "frontend/src/components/PostCard.tsx"
provides: "PostCard con bottone Rigenera, popover inline, badge rigenerato"
contains: "handleRegen"
- path: "frontend/src/pages/OutputReview.tsx"
provides: "OutputReview con summary counter rigenerati/modificati"
contains: "regeneratedCount"
key_links:
- from: "frontend/src/components/PostCard.tsx"
to: "/api/generate/single"
via: "useGenerateSingle mutation con topic override"
pattern: "useGenerateSingle|generateSingle"
- from: "frontend/src/pages/OutputReview.tsx"
to: "frontend/src/components/PostCard.tsx"
via: "onRegenerated callback che aggiorna localResults e traccia conteggi"
pattern: "handleRegenerated|regeneratedCount"
---
<objective>
Per-item regeneration con UX migliorata — bottone Rigenera diretto su ogni PostCard con form inline per topic/note override, badge visivo per post rigenerati, e summary counter in cima alla Output Review.
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
</objective>
<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>
<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
</context>
<tasks>
<task type="auto">
<name>Task 1: PostCard — bottone Rigenera con popover inline, topic override, badge rigenerato</name>
<files>frontend/src/components/PostCard.tsx</files>
<action>
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:
```typescript
/** True se questo post e' stato rigenerato (non originale) */
isRegenerated?: boolean
```
**Stato interno** — Aggiungi:
```typescript
const [showRegenForm, setShowRegenForm] = useState(false)
const [regenTopic, setRegenTopic] = useState('')
const [regenNotes, setRegenNotes] = useState('')
```
**Funzione handleRegen** — Sostituisce/affianca handleRetry per i post success:
```typescript
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:
1. **Badge rigenerato** — Se `isRegenerated` e' true, mostra un'icona `RefreshCw` (gia' importata) accanto al numero slot:
```tsx
{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.
2. **Bottone Rigenera** nell'header della card success — Aggiungi un piccolo bottone accanto alla chevron expand:
```tsx
<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.
3. **Popover/form inline** — Sotto l'header della card, se `showRegenForm` e' true, mostra un form leggero:
```tsx
{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.
</action>
<verify>
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.
</verify>
<done>
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.
</done>
</task>
<task type="auto">
<name>Task 2: OutputReview — summary counter con tracking rigenerati/modificati</name>
<files>frontend/src/pages/OutputReview.tsx</files>
<action>
Modifica `frontend/src/pages/OutputReview.tsx` per aggiungere il summary counter e il tracking dei post rigenerati.
**Stato tracking** — Aggiungi un Set per tracciare gli indici dei post rigenerati:
```typescript
const [regeneratedSlots, setRegeneratedSlots] = useState<Set<number>>(new Set())
```
**Aggiorna handleRegenerated** per tracciare i post rigenerati:
```typescript
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:
```typescript
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:
```tsx
<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:
```tsx
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:
```tsx
<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>
```
</action>
<verify>
Verifica che `OutputReview.tsx` contenga: regeneratedSlots, regeneratedCount, manuallyEditedCount, isRegenerated prop passata a PostCard.
Verifica che il summary counter sia presente nell'header.
Esegui `cd lab/postgenerator/frontend && npx tsc --noEmit` per verificare che TypeScript compili.
</verify>
<done>
OutputReview mostra un summary counter con "N post - X rigenerati - Y modificati" che si aggiorna in tempo reale. I post rigenerati sono tracciati e la prop isRegenerated viene passata a PostCard per il badge visivo. Il CSV scaricato contiene sempre le ultime versioni (handleRegenerated aggiorna localResults che viene inviato al backend via POST).
</done>
</task>
</tasks>
<verification>
1. PostCard success mostra un bottone Rigenera (icona RefreshCw) nell'header
2. Click sul bottone Rigenera apre un form inline con campi topic e note (entrambi opzionali)
3. La rigenerazione chiama POST /api/generate/single con il topic/tono override e aggiorna la card
4. Post rigenerati mostrano un badge icona (RefreshCw amber) accanto al numero slot
5. Il summary counter in cima alla OutputReview mostra conteggi aggiornati per generati/falliti/rigenerati/modificati
6. Dopo rigenerazione, il summary counter si aggiorna immediatamente
7. Il CSV scaricato contiene la versione piu' recente di ogni post (originale, rigenerato, o editato)
8. TypeScript compila senza errori (npx tsc --noEmit)
</verification>
<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>
<output>
After completion, create `.planning/phases/02-prompt-control-output-review/02-02-SUMMARY.md`
</output>