Files
Michele 5ba641e7d6 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
2026-03-08 20:17:29 +01:00

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
02-01
frontend/src/components/PostCard.tsx
frontend/src/pages/OutputReview.tsx
true
truths artifacts key_links
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)
path provides contains
frontend/src/components/PostCard.tsx PostCard con bottone Rigenera, popover inline, badge rigenerato handleRegen
path provides contains
frontend/src/pages/OutputReview.tsx OutputReview con summary counter rigenerati/modificati regeneratedCount
from to via pattern
frontend/src/components/PostCard.tsx /api/generate/single useGenerateSingle mutation con topic override useGenerateSingle|generateSingle
from to via pattern
frontend/src/pages/OutputReview.tsx frontend/src/components/PostCard.tsx onRegenerated callback che aggiorna localResults e traccia conteggi handleRegenerated|regeneratedCount
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

<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:

  1. Badge rigenerato — Se isRegenerated e' true, mostra un'icona RefreshCw (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.

  2. 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.

  3. Popover/form inline — Sotto l'header della card, se showRegenForm e' 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.

Task 2: OutputReview — summary counter con tracking rigenerati/modificati frontend/src/pages/OutputReview.tsx 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:

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>
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. 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). 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)

<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>
After completion, create `.planning/phases/02-prompt-control-output-review/02-02-SUMMARY.md`