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:
333
.planning/phases/02-prompt-control-output-review/02-02-PLAN.md
Normal file
333
.planning/phases/02-prompt-control-output-review/02-02-PLAN.md
Normal 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>
|
||||
Reference in New Issue
Block a user