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
334 lines
14 KiB
Markdown
334 lines
14 KiB
Markdown
---
|
|
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>
|