diff --git a/frontend/src/components/ProgressIndicator.tsx b/frontend/src/components/ProgressIndicator.tsx
new file mode 100644
index 0000000..34f3bc5
--- /dev/null
+++ b/frontend/src/components/ProgressIndicator.tsx
@@ -0,0 +1,153 @@
+/**
+ * ProgressIndicator — mostra il progresso real-time di un job di generazione.
+ *
+ * Usa useJobStatus(jobId) con polling ogni 2 secondi finché status='running'.
+ * Il polling si ferma automaticamente quando status diventa 'completed' o 'failed'.
+ *
+ * Quando il job completa, chiama onComplete(jobId) per navigare a OutputReview.
+ */
+
+import { AlertTriangle, Check, Loader2, X } from 'lucide-react'
+import { useEffect, useRef } from 'react'
+import { useJobStatus } from '../api/hooks'
+import type { PostResult } from '../types'
+
+interface ProgressIndicatorProps {
+ jobId: string
+ onComplete: (jobId: string) => void
+}
+
+export function ProgressIndicator({ jobId, onComplete }: ProgressIndicatorProps) {
+ const { data: jobStatus, error } = useJobStatus(jobId)
+ const onCompleteRef = useRef(onComplete)
+ onCompleteRef.current = onComplete
+
+ // Quando il job diventa completed, chiama onComplete
+ useEffect(() => {
+ if (jobStatus?.status === 'completed') {
+ onCompleteRef.current(jobId)
+ }
+ }, [jobId, jobStatus?.status])
+
+ if (error) {
+ return (
+
+
+
Errore polling: {error.message}
+
+ )
+ }
+
+ if (!jobStatus) {
+ return (
+
+
+ Connessione al server...
+
+ )
+ }
+
+ const { total, completed, status, results } = jobStatus
+ const pct = total > 0 ? Math.round((completed / total) * 100) : 0
+
+ return (
+
+ {/* Header con stato */}
+
+
+ {status === 'running' && (
+
+ )}
+ {status === 'completed' && (
+
+ )}
+ {status === 'failed' && (
+
+ )}
+
+ {status === 'running' && `Post ${completed} / ${total} in generazione...`}
+ {status === 'completed' && `Generazione completata — ${completed} / ${total} post`}
+ {status === 'failed' && 'Generazione fallita'}
+
+
+
{pct}%
+
+
+ {/* Barra di progresso */}
+
+
+ {/* Lista post con stato individuale */}
+ {results.length > 0 && (
+
+ {/* Rende i risultati ricevuti finora */}
+ {results.map((r: PostResult) => (
+
+ ))}
+
+ {/* Slot ancora in attesa (non ancora nei risultati) */}
+ {Array.from({ length: total - results.length }).map((_, i) => (
+
+
+ Post {results.length + i + 1} — in attesa
+
+ ))}
+
+ )}
+
+ {/* Messaggio quando nessun risultato ancora */}
+ {results.length === 0 && status === 'running' && (
+
+ Generazione in corso — i risultati appariranno qui...
+
+ )}
+
+ )
+}
+
+// ---------------------------------------------------------------------------
+// Singola riga stato post
+// ---------------------------------------------------------------------------
+
+function PostStatusRow({ result }: { result: PostResult }) {
+ const { slot_index, status, post, error } = result
+
+ const icon = {
+ success: ,
+ failed: ,
+ pending: ,
+ }[status]
+
+ const title = post?.cover_title ?? (error ? 'Errore' : `Post ${slot_index + 1}`)
+
+ return (
+
+ {icon}
+
+ Post {slot_index + 1}
+ {title && title !== `Post ${slot_index + 1}` && (
+ <> — {title}>
+ )}
+ {error && ({error.slice(0, 60)}...)}
+
+
+ )
+}
diff --git a/frontend/src/components/SlideViewer.tsx b/frontend/src/components/SlideViewer.tsx
index 258b8a1..3ca02f1 100644
--- a/frontend/src/components/SlideViewer.tsx
+++ b/frontend/src/components/SlideViewer.tsx
@@ -1,20 +1,371 @@
/**
- * SlideViewer — stub per Task 2a.
- * Completato in Task 2b.
+ * SlideViewer — visualizzazione slide-by-slide con navigazione frecce e edit inline.
+ *
+ * Struttura carosello:
+ * Slide 0 = Cover (cover_title, cover_subtitle, cover_image_keyword)
+ * Slide 1-6 = Slide centrali s2-s7 (headline, body, image_keyword)
+ * Slide 7 = CTA (cta_text, cta_subtext, cta_image_keyword)
+ * + Caption Instagram editabile
+ *
+ * Ogni campo testo è editabile inline: click trasforma in input/textarea.
+ * Le modifiche aggiornano lo stato locale e propagano via callback onEdit.
+ * Keyboard navigation: frecce sinistra/destra.
*/
-import type { PostResult } from '../types'
+import { ChevronLeft, ChevronRight, Edit3, Image } from 'lucide-react'
+import { useCallback, useEffect, useRef, useState } from 'react'
+import type { GeneratedPost, PostResult, SlideContent } from '../types'
+
+// Numero totale di slide: Cover + 6 centrali + CTA = 8
+const TOTAL_SLIDES = 8
interface SlideViewerProps {
result: PostResult
onEdit: (updated: PostResult) => void
}
-export function SlideViewer({ result, onEdit: _onEdit }: SlideViewerProps) {
- if (!result.post) return null
+// ---------------------------------------------------------------------------
+// Componente campo editabile inline
+// ---------------------------------------------------------------------------
+
+interface EditableFieldProps {
+ value: string
+ multiline?: boolean
+ className?: string
+ placeholder?: string
+ onChange: (v: string) => void
+}
+
+function EditableField({ value, multiline = false, className = '', placeholder, onChange }: EditableFieldProps) {
+ const [editing, setEditing] = useState(false)
+ const inputRef = useRef(null)
+
+ useEffect(() => {
+ if (editing && inputRef.current) {
+ inputRef.current.focus()
+ if ('select' in inputRef.current) {
+ inputRef.current.select()
+ }
+ }
+ }, [editing])
+
+ function handleBlur() {
+ setEditing(false)
+ }
+
+ function handleKeyDown(e: React.KeyboardEvent) {
+ if (!multiline && e.key === 'Enter') {
+ setEditing(false)
+ }
+ if (e.key === 'Escape') {
+ setEditing(false)
+ }
+ }
+
+ const baseClasses = 'w-full bg-transparent outline-none border-0 resize-none'
+ const editClasses = 'ring-1 ring-amber-500/50 rounded px-1.5 py-1 bg-stone-700/50'
+
+ if (editing) {
+ if (multiline) {
+ return (
+