docs(01): create phase plan

Phase 01: Core Generation Pipeline
- 4 plan(s) in 3 wave(s)
- Wave 1: 01-01 (infra) + 01-02 (core services) parallel
- Wave 2: 01-03 (LLM pipeline + API routers)
- Wave 3: 01-04 (Web UI) with human-verify checkpoint
- Ready for execution
This commit is contained in:
Michele
2026-03-08 01:27:25 +01:00
parent 595b6ee7e7
commit 3f1dbbf396
5 changed files with 1165 additions and 6 deletions

View File

@@ -0,0 +1,255 @@
---
phase: 01-core-generation-pipeline
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- backend/main.py
- backend/config.py
- backend/__init__.py
- backend/routers/__init__.py
- frontend/vite.config.ts
- frontend/package.json
- frontend/tsconfig.json
- frontend/tsconfig.app.json
- frontend/src/main.tsx
- frontend/src/App.tsx
- frontend/src/index.css
- frontend/src/api/client.ts
- frontend/index.html
- Dockerfile
- docker-compose.yml
- requirements.txt
- .env.example
- .gitignore
autonomous: true
must_haves:
truths:
- "FastAPI app starts on port 8000 and responds to GET /api/health with 200"
- "React SPA builds with Vite and base path /postgenerator/"
- "Docker multi-stage build produces a working container that serves both API and SPA"
- "SPA catch-all returns index.html for non-API routes without breaking /api/ routes"
artifacts:
- path: "backend/main.py"
provides: "FastAPI app with SPAStaticFiles catch-all, health endpoint, CORS-free single origin"
contains: "SPAStaticFiles"
- path: "backend/config.py"
provides: "Centralized path constants from env vars (DATA_PATH, PROMPTS_PATH, OUTPUTS_PATH)"
contains: "DATA_PATH"
- path: "frontend/vite.config.ts"
provides: "Vite config with base /postgenerator/, Tailwind v4 plugin, dev proxy to :8000"
contains: "base: '/postgenerator/'"
- path: "frontend/src/api/client.ts"
provides: "API client with base URL /postgenerator/api for production"
contains: "/postgenerator/api"
- path: "Dockerfile"
provides: "Multi-stage build: Node builds React, Python serves everything"
contains: "frontend-builder"
- path: "docker-compose.yml"
provides: "Single service with volume mount for data persistence"
contains: "lab-postgenerator-app"
- path: "requirements.txt"
provides: "Python dependencies pinned"
contains: "fastapi"
key_links:
- from: "frontend/vite.config.ts"
to: "backend/main.py"
via: "Vite dev proxy /api -> localhost:8000"
pattern: "proxy.*api.*8000"
- from: "Dockerfile"
to: "backend/main.py"
via: "CMD uvicorn with --root-path /postgenerator"
pattern: "root-path"
- from: "backend/main.py"
to: "frontend/dist"
via: "SPAStaticFiles mount"
pattern: "SPAStaticFiles.*static"
---
<objective>
Creare lo scheletro infrastrutturale completo: FastAPI app con SPA catch-all, React + Vite + Tailwind v4 project, Docker multi-stage build, e tutte le configurazioni per il subpath /postgenerator/.
Purpose: Stabilire il plumbing funzionante prima di qualsiasi business logic. Verificare che il container Docker serve API e SPA correttamente, evitando i pitfall 4 (root_path double-path) e 9 (React API URL subpath).
Output: Container Docker buildabile che serve una pagina React vuota su / e risponde a /api/health, pronto per ricevere routers e servizi nei piani successivi.
</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/research/STACK.md
@.planning/research/ARCHITECTURE.md
@.planning/research/PITFALLS.md
@.planning/phases/01-core-generation-pipeline/01-CONTEXT.md
</context>
<tasks>
<task type="auto">
<name>Task 1: Backend FastAPI skeleton + config + Docker build</name>
<files>
backend/__init__.py
backend/main.py
backend/config.py
backend/routers/__init__.py
requirements.txt
.env.example
.gitignore
Dockerfile
docker-compose.yml
</files>
<action>
1. Aggiornare .gitignore con: __pycache__/, .venv/, *.pyc, node_modules/, frontend/dist/, .env, backend/data/outputs/
2. Creare backend/__init__.py (vuoto).
3. Creare backend/config.py:
- DATA_PATH = Path(os.getenv("DATA_PATH", "./data"))
- PROMPTS_PATH = DATA_PATH / "prompts"
- OUTPUTS_PATH = DATA_PATH / "outputs"
- CAMPAIGNS_PATH = DATA_PATH / "campaigns"
- CONFIG_PATH = DATA_PATH / "config"
- Funzione get_settings() che legge ANTHROPIC_API_KEY e LLM_MODEL da env
4. Creare backend/routers/__init__.py (vuoto).
5. Creare backend/main.py:
- FastAPI() senza root_path nel costruttore (CRITICO: root_path solo via Uvicorn --root-path)
- Classe SPAStaticFiles che estende StaticFiles con fallback a index.html
- Health endpoint: GET /api/health -> {"status": "ok"}
- Mount SPAStaticFiles su "/" come ULTIMA operazione (dopo tutti i router)
- Startup event che crea le directory data/ se non esistono (prompts, outputs, campaigns, config)
6. Creare requirements.txt:
- fastapi[standard]==0.135.1
- anthropic==0.84.0
- httpx==0.28.1
- python-dotenv==1.2.2
- aiofiles
7. Creare .env.example:
- ANTHROPIC_API_KEY=your-key-here
- LLM_MODEL=claude-sonnet-4-5
- DATA_PATH=/app/data
8. Creare Dockerfile multi-stage:
- Stage 1 (frontend-builder): FROM node:22-slim, WORKDIR /app/frontend, COPY package*.json, npm ci, COPY tutto, npm run build
- Stage 2 (runtime): FROM python:3.12-slim, WORKDIR /app, COPY requirements.txt, pip install --no-cache-dir, COPY backend/ ./backend/, COPY --from=frontend-builder /app/frontend/dist ./static, CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "8000", "--root-path", "/postgenerator"]
9. Creare docker-compose.yml:
- service "app" con container_name "lab-postgenerator-app"
- build context "." con dockerfile "Dockerfile"
- env_file: .env
- volumes: postgenerator-data:/app/data (named volume per persistenza)
- networks: proxy_net (external: true)
- deploy.resources.limits: memory 1024M, cpus '1.0' (Next.js-level per build React)
- NO porte esposte pubblicamente
</action>
<verify>
- File backend/main.py contiene SPAStaticFiles e NON contiene root_path nel costruttore FastAPI()
- File Dockerfile contiene --root-path /postgenerator nel CMD
- File docker-compose.yml contiene proxy_net e container_name lab-postgenerator-app
- requirements.txt contiene fastapi[standard]==0.135.1 e anthropic==0.84.0
</verify>
<done>
Backend skeleton e Docker config pronti. FastAPI app definita senza root_path nel costruttore. Dockerfile multi-stage con --root-path solo in Uvicorn CMD. docker-compose.yml con volume per dati e rete proxy_net.
</done>
</task>
<task type="auto">
<name>Task 2: React + Vite + Tailwind v4 SPA scaffold con API client</name>
<files>
frontend/package.json
frontend/vite.config.ts
frontend/tsconfig.json
frontend/tsconfig.app.json
frontend/index.html
frontend/src/main.tsx
frontend/src/App.tsx
frontend/src/index.css
frontend/src/api/client.ts
</files>
<action>
1. Creare il progetto React + TypeScript con Vite:
- cd al progetto, eseguire: npm create vite@latest frontend -- --template react-ts
- Questo genera la struttura base
2. Installare dipendenze frontend:
- cd frontend
- npm install tailwindcss @tailwindcss/vite
- npm install react-router-dom @tanstack/react-query
- npm install lucide-react
3. Configurare vite.config.ts:
- import react from '@vitejs/plugin-react'
- import tailwindcss from '@tailwindcss/vite'
- plugins: [react(), tailwindcss()]
- base: '/postgenerator/'
- server.proxy: { '/postgenerator/api': { target: 'http://localhost:8000', changeOrigin: true, rewrite: path => path.replace('/postgenerator', '') } }
4. Configurare frontend/src/index.css:
- Rimuovere contenuto default Vite
- Aggiungere: @import "tailwindcss";
5. Creare frontend/src/api/client.ts:
- const API_BASE = '/postgenerator/api'
- Funzione generica apiFetch<T>(endpoint: string, options?: RequestInit): Promise<T>
- Gestione errori con throw su status non-ok
- Export API_BASE e apiFetch
6. Aggiornare frontend/src/App.tsx:
- Import BrowserRouter con basename="/postgenerator"
- Import QueryClientProvider da @tanstack/react-query
- Struttura base con Routes placeholder
- Pagina placeholder con titolo "PostGenerator" e messaggio "Setup completo"
7. Aggiornare frontend/src/main.tsx:
- StrictMode + render App
8. Aggiornare frontend/index.html:
- title: "PostGenerator"
- Rimuovere favicon Vite default
ATTENZIONE Pitfall 9: L'API client DEVE usare '/postgenerator/api' come base, NON '/api'. Questo e' l'absolute path che funziona dietro nginx lab-router.
</action>
<verify>
- cd frontend && npm run build completa senza errori
- vite.config.ts contiene base: '/postgenerator/'
- frontend/src/api/client.ts contiene '/postgenerator/api'
- frontend/src/App.tsx contiene basename="/postgenerator"
- index.css contiene @import "tailwindcss"
</verify>
<done>
React SPA scaffold completo con Tailwind v4, react-router con basename corretto, TanStack Query configurato, API client con path /postgenerator/api. Build Vite produce output in frontend/dist/ pronto per essere copiato nel container Docker.
</done>
</task>
</tasks>
<verification>
1. `cd frontend && npm run build` produce output in dist/ senza errori
2. FastAPI main.py: SPAStaticFiles registrato DOPO health endpoint
3. Dockerfile: --root-path /postgenerator nel CMD, NON nel costruttore FastAPI()
4. Nessun hardcoded `/api/` senza prefisso /postgenerator/ nel frontend
5. docker-compose.yml: NO porte esposte, usa proxy_net
</verification>
<success_criteria>
- Backend FastAPI app definita con health endpoint e SPA catch-all
- Frontend React builds con base path /postgenerator/
- Docker multi-stage build configurato
- API client usa path corretto /postgenerator/api
- Tutti i pitfall infrastrutturali (4, 9) indirizzati nella configurazione
</success_criteria>
<output>
After completion, create `.planning/phases/01-core-generation-pipeline/01-01-SUMMARY.md`
</output>

View File

@@ -0,0 +1,311 @@
---
phase: 01-core-generation-pipeline
plan: 02
type: execute
wave: 1
depends_on: []
files_modified:
- backend/services/__init__.py
- backend/services/calendar_service.py
- backend/services/format_selector.py
- backend/services/prompt_service.py
- backend/schemas/__init__.py
- backend/schemas/calendar.py
- backend/schemas/generate.py
- backend/data/format_mapping.json
- backend/data/prompts/system_prompt.txt
- backend/data/prompts/pas_valore.txt
- backend/data/prompts/listicle_valore.txt
- backend/data/prompts/bab_storytelling.txt
- backend/data/prompts/aida_promozione.txt
- backend/data/prompts/dato_news.txt
- backend/constants.py
autonomous: true
must_haves:
truths:
- "CalendarService genera esattamente 13 slot con distribuzione PN corretta (4 valore, 2 storytelling, 2 news, 3 riprova, 1 coinvolgimento, 1 promo)"
- "Ogni slot ha livello Schwartz assegnato con distribuzione corretta (L5+L4=6, L3=4, L2=2, L1=1)"
- "FormatSelector mappa ogni combinazione tipo_contenuto x livello_schwartz a un formato narrativo"
- "PromptService carica, lista e compila prompt .txt con sostituzione variabili"
- "CANVA_FIELDS e' definito come costante locked e contiene tutti i nomi colonna CSV"
- "Almeno 5 prompt base esistono come file .txt scritti IN italiano"
artifacts:
- path: "backend/services/calendar_service.py"
provides: "CalendarService con generate_calendar() che produce 13 slot PN + Schwartz + nicchie + date"
contains: "class CalendarService"
- path: "backend/services/format_selector.py"
provides: "FormatSelector con select_format(tipo, livello) -> formato narrativo"
contains: "class FormatSelector"
- path: "backend/services/prompt_service.py"
provides: "PromptService con load, list, compile con variabili"
contains: "class PromptService"
- path: "backend/constants.py"
provides: "CANVA_FIELDS, PERSUASION_DISTRIBUTION, SCHWARTZ_DISTRIBUTION costanti locked"
contains: "CANVA_FIELDS"
- path: "backend/schemas/calendar.py"
provides: "CalendarSlot, CalendarRequest, CalendarResponse Pydantic models"
contains: "class CalendarSlot"
- path: "backend/schemas/generate.py"
provides: "SlideContent, GeneratedPost Pydantic models per output LLM e CSV"
contains: "class GeneratedPost"
- path: "backend/data/format_mapping.json"
provides: "Tabella mapping tipo_contenuto x livello_schwartz -> formato narrativo"
- path: "backend/data/prompts/system_prompt.txt"
provides: "System prompt scritto IN italiano per generazione caroselli"
key_links:
- from: "backend/services/calendar_service.py"
to: "backend/constants.py"
via: "Importa PERSUASION_DISTRIBUTION e SCHWARTZ_DISTRIBUTION"
pattern: "from.*constants.*import"
- from: "backend/services/format_selector.py"
to: "backend/data/format_mapping.json"
via: "Carica mapping da file JSON"
pattern: "format_mapping"
- from: "backend/services/prompt_service.py"
to: "backend/config.py"
via: "Usa PROMPTS_PATH per localizzare file .txt"
pattern: "PROMPTS_PATH"
---
<objective>
Creare i servizi core del dominio: CalendarService (distribuzione 13 post PN + Schwartz + nicchie + date), FormatSelector (mapping tipo x livello -> formato), PromptService (carica/compila prompt .txt), e definire le costanti fondamentali (CANVA_FIELDS, distribuzioni).
Purpose: Costruire la logica di dominio pura (zero dipendenza LLM) che orchestra il calendario editoriale e prepara i prompt per la generazione. Queste sono le fondamenta su cui LLMService e CSVBuilder si appoggeranno.
Output: Servizi Python testabili indipendentemente, 5 prompt .txt in italiano, schema Pydantic per slot calendario e post generato, costante CANVA_FIELDS locked.
</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/research/ARCHITECTURE.md
@.planning/research/PITFALLS.md
@.planning/phases/01-core-generation-pipeline/01-CONTEXT.md
</context>
<tasks>
<task type="auto">
<name>Task 1: Costanti di dominio, Pydantic schemas, FormatSelector</name>
<files>
backend/constants.py
backend/schemas/__init__.py
backend/schemas/calendar.py
backend/schemas/generate.py
backend/services/__init__.py
backend/services/format_selector.py
backend/data/format_mapping.json
</files>
<action>
1. Creare backend/constants.py con le costanti LOCKED del progetto:
PERSUASION_DISTRIBUTION — distribuzione per ciclo di 13 post:
- "valore": 4
- "storytelling": 2
- "news": 2
- "riprova_sociale": 3
- "coinvolgimento": 1
- "promozione": 1
SCHWARTZ_DISTRIBUTION — distribuzione livelli per 13 post:
- L5: 3 post (storytelling + news)
- L4: 3 post (valore + storytelling)
- L3: 4 post (valore + riprova)
- L2: 2 post (riprova + coinvolgimento)
- L1: 1 post (promozione)
Nota: L5+L4=6, L3=4, L2=2, L1=1 come da requirement CAL-02.
CANVA_FIELDS — lista ORDINATA dei nomi colonna CSV per Canva Bulk Create:
Metadati: campagna, fase_campagna, tipo_contenuto, formato_narrativo, funzione, livello_schwartz, target_nicchia, data_pub_suggerita
Slide (8 slide x 3 campi = 24):
cover_title, cover_subtitle, cover_image_keyword
s2_headline, s2_body, s2_image_keyword
s3_headline, s3_body, s3_image_keyword
s4_headline, s4_body, s4_image_keyword
s5_headline, s5_body, s5_image_keyword
s6_headline, s6_body, s6_image_keyword
s7_headline, s7_body, s7_image_keyword
cta_text, cta_subtext, cta_image_keyword
Extra: caption_instagram
Totale: 8 metadati + 24 slide + 1 caption = 33 colonne
FORMATI_NARRATIVI — lista dei 7 formati: PAS, AIDA, BAB, Listicle, Storytelling, Dato_Implicazione, Obiezione_Risposta
FUNZIONI_CONTENUTO — le 4 funzioni: Intrattenere, Educare, Persuadere, Convertire
FASI_CAMPAGNA — le 4 fasi: Attira, Cattura, Coinvolgi, Converti
NICCHIE_DEFAULT — lista default: ["generico", "dentisti", "avvocati", "ecommerce", "local_business", "agenzie"]
POST_PER_CICLO = 13
Nota: usare _image_keyword (non _image_url) per le colonne immagine — l'URL verra' solo con Unsplash in Phase 4. Per ora il CSV contiene keyword testuali.
2. Creare backend/schemas/__init__.py (vuoto).
3. Creare backend/schemas/calendar.py con Pydantic models:
- CalendarSlot: indice (int), tipo_contenuto (str), livello_schwartz (str), formato_narrativo (str), funzione (str), fase_campagna (str), target_nicchia (str), data_pub_suggerita (str, formato YYYY-MM-DD), topic (Optional[str], default None — verra' generato dall'LLM o overridden dall'utente)
- CalendarRequest: obiettivo_campagna (str), settimane (int, default 2), nicchie (Optional[list[str]]), frequenza_post (int, default 3 — post a settimana), data_inizio (Optional[str])
- CalendarResponse: campagna (str), slots (list[CalendarSlot]), totale_post (int)
4. Creare backend/schemas/generate.py con Pydantic models:
- SlideContent: headline (str), body (str), image_keyword (str)
- GeneratedPost: cover_title (str), cover_subtitle (str), cover_image_keyword (str), slides (list[SlideContent] — 6 slide centrali s2-s7), cta_text (str), cta_subtext (str), cta_image_keyword (str), caption_instagram (str)
- GenerateRequest: slot (CalendarSlot), obiettivo_campagna (str), brand_name (Optional[str]), tono (Optional[str])
- PostResult: slot_index (int), status (Literal["success", "failed", "pending"]), post (Optional[GeneratedPost]), error (Optional[str])
- GenerateResponse: campagna (str), results (list[PostResult]), total (int), success_count (int), failed_count (int)
5. Creare backend/data/format_mapping.json:
Matrice tipo_contenuto x livello_schwartz -> formato_narrativo.
Struttura: { "valore": { "L5": "Listicle", "L4": "PAS", "L3": "PAS", "L2": "Obiezione_Risposta", "L1": "AIDA" }, ... }
Coprire tutte le 6 combinazioni tipo x 5 livelli con i 7 formati disponibili, scegliendo il formato piu' efficace per ogni combinazione.
6. Creare backend/services/__init__.py (vuoto).
7. Creare backend/services/format_selector.py:
- class FormatSelector con __init__ che carica format_mapping.json
- Metodo select_format(tipo_contenuto: str, livello_schwartz: str) -> str che ritorna il formato narrativo
- Fallback a "PAS" se combinazione non trovata
- Metodo get_mapping() -> dict per esporre la tabella completa
</action>
<verify>
- backend/constants.py: CANVA_FIELDS ha esattamente 33 elementi, PERSUASION_DISTRIBUTION somma a 13, SCHWARTZ_DISTRIBUTION somma a 13
- backend/schemas/calendar.py: CalendarSlot importabile, CalendarRequest ha campo obiettivo_campagna
- backend/schemas/generate.py: GeneratedPost ha slides come list[SlideContent], PostResult ha campo status
- backend/data/format_mapping.json: contiene tutte le 6 chiavi tipo_contenuto, ciascuna con 5 livelli
- backend/services/format_selector.py: FormatSelector ha metodo select_format
</verify>
<done>
Costanti di dominio locked (CANVA_FIELDS, PERSUASION_DISTRIBUTION, SCHWARTZ_DISTRIBUTION). Pydantic schemas per calendario e generazione definiti. FormatSelector carica mapping da JSON e mappa tipo x livello -> formato. Tutto il dominio ha tipi espliciti, nessuna stringa magica.
</done>
</task>
<task type="auto">
<name>Task 2: CalendarService, PromptService, e prompt .txt in italiano</name>
<files>
backend/services/calendar_service.py
backend/services/prompt_service.py
backend/data/prompts/system_prompt.txt
backend/data/prompts/pas_valore.txt
backend/data/prompts/listicle_valore.txt
backend/data/prompts/bab_storytelling.txt
backend/data/prompts/aida_promozione.txt
backend/data/prompts/dato_news.txt
</files>
<action>
1. Creare backend/services/calendar_service.py:
- class CalendarService
- Metodo generate_calendar(request: CalendarRequest) -> CalendarResponse:
a. Genera 13 slot con distribuzione PERSUASION_DISTRIBUTION (import da constants)
b. Assegna livelli Schwartz secondo la logica naturale:
- valore: L4 (2), L3 (2) — educare chi e' consapevole del problema/soluzioni
- storytelling: L5 (2) — attirare inconsapevoli con storie
- news: L5 (1), L4 (1) — intrattenere/educare
- riprova_sociale: L3 (2), L2 (1) — persuadere chi conosce soluzioni/prodotto
- coinvolgimento: L2 (1) — interagire con chi conosce il prodotto
- promozione: L1 (1) — convertire chi e' pronto
Verifica che totali siano: L5=3, L4=3, L3=4, L2=2, L1=1
c. Assegna funzioni contenuto: Intrattenere (storytelling, news, coinvolgimento), Educare (valore), Persuadere (riprova_sociale), Convertire (promozione)
d. Distribuisce nelle 4 fasi campagna: Attira (L5), Cattura (L4+L3), Coinvolgi (L3+L2), Converti (L1+L2)
e. Ordina gli slot nella sequenza campagna (Attira -> Cattura -> Coinvolgi -> Converti)
f. Calcola date_pub_suggerita dalla data_inizio con la frequenza specificata (default: 3 post/settimana, lun-mer-ven)
g. Assegna nicchie con rotazione: 50% generico, 50% verticali in rotazione dalla lista nicchie
- Usa FormatSelector internamente per assegnare formato_narrativo a ogni slot
- Metodo statico _distribute_niches(slots, nicchie) per la logica rotazione
2. Creare backend/services/prompt_service.py:
- class PromptService(__init__ riceve prompts_dir: Path)
- list_prompts() -> list[str]: elenca tutti i .txt nella directory
- load_prompt(name: str) -> str: carica contenuto file .txt
- compile_prompt(name: str, variables: dict[str, str]) -> str: carica e sostituisce {{variabile}} con valori dal dict
- Usa doppia graffa {{variabile}} come delimitatore (Pitfall 7)
- Solleva ValueError se una variabile nel template non ha corrispondenza nel dict
- save_prompt(name: str, content: str) -> None: salva contenuto (per Phase 2 editor)
- get_required_variables(name: str) -> list[str]: parse il template e ritorna lista variabili {{...}} trovate
3. Creare i 5 prompt base + system prompt, TUTTI scritti IN italiano (Pitfall 8):
backend/data/prompts/system_prompt.txt:
- Ruolo: esperto di content marketing B2B per PMI italiane
- Tono: diretto, provocatorio ma costruttivo, usa il "tu"
- Target: imprenditori e manager italiani
- Regole: "cosa fare" mai "come farlo", benefici concreti, evitare jargon
- Lingua: italiano naturale, NON tradotto dall'inglese
- Output: JSON strutturato con i campi specificati nello schema
backend/data/prompts/pas_valore.txt (formato PAS per post valore):
- Sezioni: SYSTEM (ref system_prompt), USER, OUTPUT_SCHEMA
- Variabili: {{obiettivo_campagna}}, {{target_nicchia}}, {{livello_schwartz}}, {{topic}}, {{brand_name}}
- Istruzioni PAS: Problema -> Agitazione -> Soluzione
- Output schema JSON esplicito con campi GeneratedPost
backend/data/prompts/listicle_valore.txt (formato Listicle per post valore):
- Variabili: stesse di PAS
- Istruzioni Listicle: lista numerata di consigli pratici
- Enfasi su valore educativo
backend/data/prompts/bab_storytelling.txt (formato BAB per storytelling):
- Variabili: stesse + enfasi emotiva
- Istruzioni BAB: Before -> After -> Bridge
- Tono narrativo, storia di trasformazione
backend/data/prompts/aida_promozione.txt (formato AIDA per promo):
- Variabili: stesse + {{call_to_action}}
- Istruzioni AIDA: Attenzione -> Interesse -> Desiderio -> Azione
- Focus su conversione
backend/data/prompts/dato_news.txt (formato Dato+Implicazione per news):
- Variabili: stesse
- Istruzioni: dato/statistica -> cosa significa -> come agire
- Focus su urgenza informata
TUTTI i prompt devono:
- Essere scritti interamente in italiano (istruzioni E esempi)
- Usare {{variabile}} per tutti i parametri dinamici
- Specificare l'output JSON schema esplicito con i campi di GeneratedPost
- Includere istruzioni sul tono (tu, diretto, concreto)
- Specificare il numero di slide e la struttura (cover, 6 slide centrali, CTA)
</action>
<verify>
- CalendarService.generate_calendar() con CalendarRequest(obiettivo_campagna="test", settimane=2) produce CalendarResponse con esattamente 13 slot
- Distribuzione PN: contare tipi -> 4 valore, 2 storytelling, 2 news, 3 riprova, 1 coinvolgimento, 1 promo
- Distribuzione Schwartz: contare livelli -> L5=3, L4=3, L3=4, L2=2, L1=1
- PromptService.list_prompts() ritorna almeno 6 file (system + 5 base)
- PromptService.compile_prompt("pas_valore", {"obiettivo_campagna": "test", ...}) sostituisce tutte le variabili senza errori
- Tutti i prompt .txt contengono SOLO testo italiano, nessuna istruzione in inglese
- Nessun prompt contiene numeri hardcoded per slide count — usano {{num_slides}} o la struttura e' definita nell'output schema
</verify>
<done>
CalendarService genera 13 slot con distribuzione PN e Schwartz corretta, assegna fasi campagna, calcola date, ruota nicchie. PromptService carica e compila prompt con variabili {{...}}. 5 prompt base + system prompt scritti IN italiano, con output JSON schema esplicito. Nessun valore hardcoded nei template.
</done>
</task>
</tasks>
<verification>
1. CANVA_FIELDS ha 33 elementi e corrisponde allo schema in PROJECT.md (+ caption_instagram)
2. CalendarService produce 13 slot con distribuzione verificabile contando tipi e livelli
3. Ogni prompt .txt usa solo {{variabile}} e non contiene numeri/strutture hardcoded
4. FormatSelector mappa tutte le 30 combinazioni (6 tipi x 5 livelli)
5. Pydantic schemas validano correttamente un esempio di GeneratedPost
</verification>
<success_criteria>
- CalendarService genera calendario con distribuzione PN e Schwartz corretta
- FormatSelector mappa ogni combinazione tipo x livello a un formato
- PromptService carica, lista, compila prompt con sostituzione variabili
- 5 prompt base + system prompt esistono come file .txt in italiano
- CANVA_FIELDS locked come costante con tutti i nomi colonna
- Pydantic schemas per calendario, generazione, e risultati definiti
</success_criteria>
<output>
After completion, create `.planning/phases/01-core-generation-pipeline/01-02-SUMMARY.md`
</output>

View File

@@ -0,0 +1,265 @@
---
phase: 01-core-generation-pipeline
plan: 03
type: execute
wave: 2
depends_on: ["01-01", "01-02"]
files_modified:
- backend/services/llm_service.py
- backend/services/csv_builder.py
- backend/services/generation_pipeline.py
- backend/routers/calendar.py
- backend/routers/generate.py
- backend/routers/export.py
- backend/routers/settings.py
- backend/schemas/settings.py
- backend/main.py
autonomous: true
must_haves:
truths:
- "LLMService chiama Claude API con retry e backoff, gestisce 429 leggendo retry-after header"
- "LLMService valida il JSON output con Pydantic GeneratedPost e rigetta output malformato"
- "CSVBuilder produce CSV con encoding utf-8-sig, header CANVA_FIELDS, e caratteri italiani intatti"
- "GenerationPipeline genera 13 post con per-item error isolation: un fallimento non blocca il batch"
- "API endpoint POST /api/calendar/generate ritorna CalendarResponse con 13 slot"
- "API endpoint POST /api/generate/bulk ritorna GenerateResponse con risultati per-item (success/failed)"
- "API endpoint GET /api/export/{job_id}/csv scarica file CSV con Content-Disposition attachment"
- "API endpoint GET /api/settings ritorna configurazione corrente, PUT /api/settings salva"
artifacts:
- path: "backend/services/llm_service.py"
provides: "LLMService con retry, backoff, rate limit, JSON validation via Pydantic"
contains: "class LLMService"
- path: "backend/services/csv_builder.py"
provides: "CSVBuilder con CANVA_FIELDS header locked, utf-8-sig encoding, write to disk"
contains: "class CSVBuilder"
- path: "backend/services/generation_pipeline.py"
provides: "GenerationPipeline che orchestra calendario -> LLM -> CSV con per-item isolation"
contains: "class GenerationPipeline"
- path: "backend/routers/generate.py"
provides: "POST /api/generate/bulk e POST /api/generate/single endpoints"
contains: "router = APIRouter"
- path: "backend/routers/calendar.py"
provides: "POST /api/calendar/generate endpoint"
contains: "router = APIRouter"
- path: "backend/routers/export.py"
provides: "GET /api/export/{job_id}/csv endpoint con FileResponse"
contains: "router = APIRouter"
- path: "backend/routers/settings.py"
provides: "GET/PUT /api/settings endpoint per API key e configurazione"
contains: "router = APIRouter"
key_links:
- from: "backend/services/llm_service.py"
to: "Claude API"
via: "anthropic.Anthropic client con retry loop"
pattern: "client\\.messages\\.create"
- from: "backend/services/csv_builder.py"
to: "backend/constants.py"
via: "Importa CANVA_FIELDS per header CSV"
pattern: "CANVA_FIELDS"
- from: "backend/services/generation_pipeline.py"
to: "backend/services/llm_service.py"
via: "Chiama generate() per ogni slot con try/except per-item"
pattern: "llm_service\\.generate"
- from: "backend/routers/generate.py"
to: "backend/services/generation_pipeline.py"
via: "Chiama pipeline.generate_bulk()"
pattern: "pipeline\\.generate"
- from: "backend/main.py"
to: "backend/routers/"
via: "include_router per tutti i routers"
pattern: "include_router"
---
<objective>
Creare la pipeline LLM completa: LLMService (Claude API con retry/backoff/rate limit), CSVBuilder (CSV Canva-compatibile con utf-8-sig), GenerationPipeline (orchestrazione con per-item error isolation), e tutti gli API routers (calendar, generate, export, settings).
Purpose: Connettere i servizi di dominio (Plan 02) alla Claude API e al CSV export, creando gli endpoint REST che il frontend (Plan 04) consumera'. Indirizzare i pitfall 1 (soft failures), 3 (CSV encoding), 5 (all-or-nothing batch), e 6 (rate limit).
Output: API backend completa che accetta una richiesta di generazione calendario, chiama Claude per ogni post con error isolation, produce un CSV scaricabile con encoding corretto.
</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/research/STACK.md
@.planning/research/ARCHITECTURE.md
@.planning/research/PITFALLS.md
@.planning/phases/01-core-generation-pipeline/01-CONTEXT.md
@.planning/phases/01-core-generation-pipeline/01-01-SUMMARY.md
@.planning/phases/01-core-generation-pipeline/01-02-SUMMARY.md
</context>
<tasks>
<task type="auto">
<name>Task 1: LLMService, CSVBuilder, GenerationPipeline</name>
<files>
backend/services/llm_service.py
backend/services/csv_builder.py
backend/services/generation_pipeline.py
</files>
<action>
1. Creare backend/services/llm_service.py:
- class LLMService(__init__ riceve api_key: str, model: str = "claude-sonnet-4-5", max_retries: int = 3, inter_request_delay: float = 2.0)
- Usa anthropic.Anthropic(api_key=api_key) per il client
- Metodo generate(system_prompt: str, user_prompt: str, response_schema: Type[BaseModel]) -> BaseModel:
a. Loop retry con max_retries tentativi
b. Chiama client.messages.create con model, max_tokens=4096, system=system_prompt, messages user
c. Parse response.content[0].text come JSON
d. Valida con response_schema.model_validate_json(raw_text)
e. Gestione errori SPECIFICA:
- anthropic.RateLimitError (429): leggi response header retry-after, attendi quel tempo esatto, poi riprova
- anthropic.APIStatusError (5xx): exponential backoff con jitter (base_delay * 2^attempt + random 0-1s)
- Pydantic ValidationError: riprova UNA volta con istruzione correttiva appesa al prompt ("Il tuo output precedente non era JSON valido. Rispondi SOLO con JSON valido secondo lo schema.")
- Qualsiasi altra eccezione: non ritentare, solleva
f. Dopo ogni chiamata riuscita, applica inter_request_delay (time.sleep) per rispettare OTPM Tier 1
- Metodo generate_topic(system_prompt: str, obiettivo: str, tipo_contenuto: str, nicchia: str) -> str:
Genera un topic specifico per lo slot dato l'obiettivo campagna. Ritorna una stringa topic.
- Log strutturato: ogni chiamata logga model, tokens in/out, tempo risposta, tentativo N/max
2. Creare backend/services/csv_builder.py:
- class CSVBuilder
- Importa CANVA_FIELDS da backend.constants
- Metodo build_csv(posts: list[PostResult], calendar: CalendarResponse, job_id: str) -> Path:
a. Filtra solo PostResult con status="success"
b. Per ogni post success, mappa GeneratedPost + CalendarSlot -> dict con chiavi CANVA_FIELDS
- Metadati: campagna, fase_campagna, tipo_contenuto, formato_narrativo, funzione, livello_schwartz, target_nicchia, data_pub_suggerita (da CalendarSlot)
- Cover: cover_title, cover_subtitle, cover_image_keyword (da GeneratedPost)
- Slide s2-s7: headline, body, image_keyword (da GeneratedPost.slides[0..5])
- CTA: cta_text, cta_subtext, cta_image_keyword (da GeneratedPost)
- caption_instagram (da GeneratedPost)
c. Scrive CSV su disco in OUTPUTS_PATH / f"{job_id}.csv"
d. Encoding: utf-8-sig (BOM) — CRITICO per Excel + caratteri italiani (Pitfall 3)
e. Usa csv.DictWriter con fieldnames=CANVA_FIELDS
f. Ritorna il Path del file scritto
- Metodo build_csv_content(posts, calendar, job_id) -> str: come sopra ma ritorna stringa CSV (per preview)
3. Creare backend/services/generation_pipeline.py:
- class GenerationPipeline(__init__ riceve llm_service: LLMService, prompt_service: PromptService, calendar_service: CalendarService, format_selector: FormatSelector, csv_builder: CSVBuilder)
- Metodo generate_bulk(request: CalendarRequest, api_key: str) -> GenerateResponse:
a. Genera calendario via calendar_service.generate_calendar(request)
b. Per ogni slot del calendario:
- Genera topic via llm_service.generate_topic() se slot.topic e' None
- Seleziona il prompt template corretto in base a formato_narrativo (es. "pas_valore" per PAS + valore)
- Compila il prompt con variabili (obiettivo, nicchia, livello, topic, brand)
- Chiama llm_service.generate(system_prompt, user_prompt, GeneratedPost)
- Se successo: PostResult(status="success", post=risultato)
- Se fallimento: PostResult(status="failed", error=str(e))
- CRITICO Pitfall 5: ogni slot in try/except INDIVIDUALE. Un fallimento NON blocca il loop.
c. Genera job_id (UUID)
d. Chiama csv_builder.build_csv() con i risultati
e. Salva job metadata in OUTPUTS_PATH / f"{job_id}.json" (per ricaricamento)
f. Ritorna GenerateResponse con risultati per-item
- Metodo generate_single(slot: CalendarSlot, obiettivo: str, api_key: str) -> PostResult:
Genera un singolo post. Utile per rigenerazione di post falliti.
- Metodo _select_prompt_template(formato: str, tipo: str) -> str:
Mappa formato_narrativo + tipo_contenuto al nome del file prompt (es. "PAS" + "valore" -> "pas_valore")
Fallback a "pas_valore" se template specifico non esiste
</action>
<verify>
- LLMService ha gestione specifica per RateLimitError con lettura retry-after
- LLMService ha inter_request_delay dopo ogni chiamata riuscita
- CSVBuilder importa CANVA_FIELDS e usa encoding='utf-8-sig'
- GenerationPipeline ha try/except dentro il loop per-slot (non attorno al loop intero)
- GenerationPipeline salva job metadata JSON per ricaricamento
</verify>
<done>
LLMService chiama Claude con retry, backoff specifico per 429, e validation Pydantic. CSVBuilder produce CSV con encoding utf-8-sig e header CANVA_FIELDS locked. GenerationPipeline orchestra il flusso completo con per-item error isolation.
</done>
</task>
<task type="auto">
<name>Task 2: API routers e wiring in main.py</name>
<files>
backend/routers/calendar.py
backend/routers/generate.py
backend/routers/export.py
backend/routers/settings.py
backend/schemas/settings.py
backend/main.py
</files>
<action>
1. Creare backend/schemas/settings.py:
- class Settings(BaseModel): api_key (Optional[str]), llm_model (str, default "claude-sonnet-4-5"), nicchie_attive (list[str], default NICCHIE_DEFAULT), lingua (str, default "italiano"), frequenza_post (int, default 3), brand_name (Optional[str]), tono (Optional[str], default "diretto e concreto")
- Settings salvate in CONFIG_PATH / "settings.json"
2. Creare backend/routers/calendar.py:
- router = APIRouter(prefix="/api/calendar", tags=["calendar"])
- POST /generate: riceve CalendarRequest, usa CalendarService, ritorna CalendarResponse
- GET /formats: ritorna il mapping formati da FormatSelector
3. Creare backend/routers/generate.py:
- router = APIRouter(prefix="/api/generate", tags=["generate"])
- POST /bulk: riceve CalendarRequest (+ eventuali topic overrides), usa GenerationPipeline.generate_bulk(), ritorna GenerateResponse
- Prima verifica che API key sia configurata (da settings), ritorna 400 se mancante
- Ritorna 200 anche con risultati parziali (alcuni failed) — il frontend gestisce lo stato per-item
- POST /single: riceve GenerateRequest (singolo slot), usa GenerationPipeline.generate_single(), ritorna PostResult
- GET /job/{job_id}: ritorna i risultati salvati di un job precedente (carica da OUTPUTS_PATH/{job_id}.json)
4. Creare backend/routers/export.py:
- router = APIRouter(prefix="/api/export", tags=["export"])
- GET /{job_id}/csv: trova file CSV in OUTPUTS_PATH/{job_id}.csv
- Ritorna FileResponse con media_type="text/csv; charset=utf-8"
- Headers: Content-Disposition: attachment; filename="postgenerator_{job_id}.csv"
- Ritorna 404 se file non esiste
5. Creare backend/routers/settings.py:
- router = APIRouter(prefix="/api/settings", tags=["settings"])
- GET /: carica settings da settings.json, ritorna Settings (con api_key mascherata: mostra solo ultimi 4 caratteri)
- PUT /: riceve Settings, salva in settings.json, ritorna Settings aggiornate
- GET /status: ritorna {"api_key_configured": bool, "llm_model": str} — usato dal frontend per abilitare/disabilitare pulsante genera
6. Aggiornare backend/main.py:
- Importare tutti i router: calendar, generate, export, settings
- app.include_router() per ciascuno, PRIMA del mount SPAStaticFiles
- Aggiungere lifespan/startup che:
a. Crea directory dati se non esistono
b. Copia prompts default in DATA_PATH/prompts/ se la directory e' vuota (primo avvio)
- Ordine mount: health -> routers -> SPAStaticFiles (ULTIMO)
NOTA: I routers sono thin — validano input, chiamano service, ritornano output. Nessuna logica di business nei routers.
</action>
<verify>
- backend/main.py include tutti e 4 i router PRIMA del mount SPAStaticFiles
- POST /api/calendar/generate accetta CalendarRequest body
- POST /api/generate/bulk verifica API key prima di procedere
- GET /api/export/{job_id}/csv ha Content-Disposition header
- GET /api/settings/status ritorna api_key_configured boolean
- Nessun router contiene logica di business (solo validazione + chiamata service + return)
</verify>
<done>
4 routers API (calendar, generate, export, settings) creati e montati in main.py. Ogni endpoint ha schema request/response Pydantic. Generate verifica API key. Export serve CSV con header corretti. Settings gestisce configurazione persistente.
</done>
</task>
</tasks>
<verification>
1. `python -c "from backend.services.llm_service import LLMService; print('OK')"` — importa senza errori
2. `python -c "from backend.services.csv_builder import CSVBuilder; print('OK')"` — importa senza errori
3. `python -c "from backend.main import app; print(app.routes)"` — mostra tutti i routes registrati
4. CSVBuilder usa encoding='utf-8-sig' nel codice (grep)
5. GenerationPipeline ha try/except PER SINGOLO slot, non attorno al loop
6. LLMService gestisce RateLimitError separatamente dalle altre eccezioni
7. Nessun import circolare tra moduli
</verification>
<success_criteria>
- LLMService chiama Claude con retry specifico per 429 e validation Pydantic
- CSVBuilder produce CSV con utf-8-sig encoding e CANVA_FIELDS header
- GenerationPipeline ha per-item error isolation
- 4 API routers montati e funzionali
- Settings endpoint gestisce API key
- Job results salvati su disco per ricaricamento
</success_criteria>
<output>
After completion, create `.planning/phases/01-core-generation-pipeline/01-03-SUMMARY.md`
</output>

View File

@@ -0,0 +1,328 @@
---
phase: 01-core-generation-pipeline
plan: 04
type: execute
wave: 3
depends_on: ["01-01", "01-02", "01-03"]
files_modified:
- frontend/src/App.tsx
- frontend/src/pages/Dashboard.tsx
- frontend/src/pages/GenerateCalendar.tsx
- frontend/src/pages/GenerateSingle.tsx
- frontend/src/pages/OutputReview.tsx
- frontend/src/pages/Settings.tsx
- frontend/src/components/Layout.tsx
- frontend/src/components/Sidebar.tsx
- frontend/src/components/PostCard.tsx
- frontend/src/components/SlideViewer.tsx
- frontend/src/components/ProgressIndicator.tsx
- frontend/src/components/BadgePN.tsx
- frontend/src/components/BadgeSchwartz.tsx
- frontend/src/api/client.ts
- frontend/src/api/hooks.ts
- frontend/src/types.ts
autonomous: false
must_haves:
truths:
- "L'utente vede una Dashboard con link a Genera Calendario, Genera Singolo Post, e Impostazioni"
- "L'utente compila il form Genera Calendario (obiettivo + settimane) e clicca Genera — vede progress indicator per ogni post"
- "L'utente vede i 13 post generati come griglia di card con badge colorati PN e Schwartz"
- "L'utente clicca su una card e vede le slide con navigazione frecce laterali + caption Instagram"
- "L'utente puo' modificare il testo di una slide inline (click to edit) e le modifiche si riflettono nel CSV"
- "L'utente scarica il CSV cliccando un pulsante Download CSV"
- "Post falliti appaiono come card errore con pulsante Riprova"
- "Il pulsante Genera e' disabilitato se API key non configurata, con messaggio che rimanda a Impostazioni"
- "La pagina Impostazioni permette di configurare API key, modello LLM, nicchie, frequenza"
artifacts:
- path: "frontend/src/pages/Dashboard.tsx"
provides: "Dashboard con stato e navigazione"
min_lines: 30
- path: "frontend/src/pages/GenerateCalendar.tsx"
provides: "Form generazione calendario con progress indicator"
min_lines: 80
- path: "frontend/src/pages/OutputReview.tsx"
provides: "Griglia card post con espansione slide, edit inline, download CSV"
min_lines: 100
- path: "frontend/src/pages/Settings.tsx"
provides: "Form configurazione con API key, modello, nicchie"
min_lines: 50
- path: "frontend/src/components/PostCard.tsx"
provides: "Card singolo post con badge PN e Schwartz"
min_lines: 40
- path: "frontend/src/components/SlideViewer.tsx"
provides: "Navigazione slide con frecce laterali"
min_lines: 50
- path: "frontend/src/api/hooks.ts"
provides: "TanStack Query hooks per tutte le API calls"
contains: "useQuery"
key_links:
- from: "frontend/src/api/hooks.ts"
to: "/postgenerator/api"
via: "apiFetch con endpoint completo"
pattern: "apiFetch"
- from: "frontend/src/pages/GenerateCalendar.tsx"
to: "frontend/src/api/hooks.ts"
via: "useMutation per POST /api/generate/bulk"
pattern: "useMutation"
- from: "frontend/src/pages/OutputReview.tsx"
to: "frontend/src/components/PostCard.tsx"
via: "Render griglia di PostCard"
pattern: "PostCard"
- from: "frontend/src/components/PostCard.tsx"
to: "frontend/src/components/SlideViewer.tsx"
via: "Espansione card mostra SlideViewer"
pattern: "SlideViewer"
---
<objective>
Creare l'intera Web UI: Dashboard, form Genera Calendario con progress indicator, Output Review con griglia card + navigazione slide + edit inline + download CSV, form Genera Singolo Post, e pagina Impostazioni.
Purpose: Dare all'utente l'interfaccia per interagire con la pipeline di generazione. Questa e' l'unica via di accesso al sistema — senza UI il backend non e' utilizzabile.
Output: SPA React completa con tutte le pagine e componenti per il workflow: configura -> genera -> rivedi -> scarica.
</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/01-core-generation-pipeline/01-CONTEXT.md
@.planning/phases/01-core-generation-pipeline/01-01-SUMMARY.md
@.planning/phases/01-core-generation-pipeline/01-02-SUMMARY.md
@.planning/phases/01-core-generation-pipeline/01-03-SUMMARY.md
</context>
<tasks>
<task type="auto">
<name>Task 1: Layout, routing, API hooks, tipi TypeScript, pagine Settings e Dashboard</name>
<files>
frontend/src/types.ts
frontend/src/api/client.ts
frontend/src/api/hooks.ts
frontend/src/components/Layout.tsx
frontend/src/components/Sidebar.tsx
frontend/src/App.tsx
frontend/src/pages/Dashboard.tsx
frontend/src/pages/Settings.tsx
</files>
<action>
1. Creare frontend/src/types.ts con i tipi TypeScript che rispecchiano gli schemas Pydantic del backend:
- CalendarSlot, CalendarRequest, CalendarResponse
- SlideContent, GeneratedPost, PostResult, GenerateResponse
- Settings (api_key, llm_model, nicchie_attive, lingua, frequenza_post, brand_name, tono)
- SettingsStatus (api_key_configured: boolean, llm_model: string)
2. Aggiornare frontend/src/api/client.ts (gia' creato in Plan 01):
- Aggiungere metodi specifici: apiGet<T>, apiPost<T>, apiPut<T>
- Gestione download file: apiDownload(endpoint) -> Blob
3. Creare frontend/src/api/hooks.ts con TanStack Query hooks:
- useSettings(): useQuery per GET /api/settings
- useSettingsStatus(): useQuery per GET /api/settings/status
- useUpdateSettings(): useMutation per PUT /api/settings
- useGenerateCalendar(): useMutation per POST /api/generate/bulk — ritorna GenerateResponse
- useGenerateSingle(): useMutation per POST /api/generate/single
- useJobResults(jobId): useQuery per GET /api/generate/job/{jobId}
- useDownloadCsv(): funzione che chiama apiDownload e triggera download browser
- useFormats(): useQuery per GET /api/calendar/formats
4. Creare frontend/src/components/Layout.tsx:
- Layout wrapper con Sidebar a sinistra e contenuto a destra
- Responsive: sidebar collassabile su mobile
- Tailwind CSS per styling
5. Creare frontend/src/components/Sidebar.tsx:
- Logo/titolo "PostGenerator" in alto
- Navigazione con link: Dashboard, Genera Calendario, Genera Singolo Post, Impostazioni
- Usa react-router-dom NavLink con activeClassName
- Icone da lucide-react
6. Aggiornare frontend/src/App.tsx:
- BrowserRouter con basename="/postgenerator"
- QueryClientProvider con QueryClient
- Layout wrapping
- Routes: / -> Dashboard, /genera -> GenerateCalendar, /genera-singolo -> GenerateSingle, /risultati/:jobId -> OutputReview, /impostazioni -> Settings
7. Creare frontend/src/pages/Dashboard.tsx:
- Card di benvenuto con nome progetto e descrizione breve
- Quick actions: "Genera Calendario" (link), "Impostazioni" (link)
- Se API key non configurata: banner prominente "Configura la tua API key Claude nelle Impostazioni"
- Se ci sono job recenti (check /api/generate/job/latest): mostra ultimo job con link a risultati
- Usa useSettingsStatus() per verificare API key
8. Creare frontend/src/pages/Settings.tsx:
- Form con campi:
- API Key Claude (input password con toggle visibilita', mostra solo ultimi 4 char se gia' configurata)
- Modello LLM (select: claude-sonnet-4-5, claude-haiku-3-5)
- Brand Name (input text, opzionale)
- Tono di voce (input text, default "diretto e concreto")
- Nicchie attive (lista checkbox con le nicchie default + possibilita' di aggiungerne)
- Frequenza post settimanale (number input, 1-7, default 3)
- Pulsante Salva con feedback (successo/errore)
- Usa useSettings() e useUpdateSettings()
NOTA DESIGN: Claude ha discrezione sul design UI. Scegliere uno stile pulito e professionale, NON il solito template generico con gradienti viola. Colori suggeriti: palette terrosa o industriale adatta a B2B.
</action>
<verify>
- frontend/src/App.tsx ha BrowserRouter con basename="/postgenerator"
- frontend/src/api/hooks.ts ha almeno 7 hooks (settings, settingsStatus, updateSettings, generateCalendar, generateSingle, jobResults, downloadCsv)
- frontend/src/types.ts ha CalendarSlot, GeneratedPost, PostResult, Settings
- Sidebar ha 4 link di navigazione
- Settings ha campo API key con tipo password
- Dashboard mostra banner se API key non configurata
- npm run build completa senza errori TypeScript
</verify>
<done>
Layout con sidebar, routing completo, API hooks TanStack Query per tutti gli endpoint, tipi TypeScript, Dashboard con stato API key, Settings form funzionale.
</done>
</task>
<task type="auto">
<name>Task 2: Genera Calendario, Output Review con card/slide/edit, Genera Singolo Post</name>
<files>
frontend/src/pages/GenerateCalendar.tsx
frontend/src/pages/GenerateSingle.tsx
frontend/src/pages/OutputReview.tsx
frontend/src/components/PostCard.tsx
frontend/src/components/SlideViewer.tsx
frontend/src/components/ProgressIndicator.tsx
frontend/src/components/BadgePN.tsx
frontend/src/components/BadgeSchwartz.tsx
</files>
<action>
1. Creare frontend/src/components/BadgePN.tsx:
- Badge colorato per tipo Persuasion Nurturing
- Colori distinti per ogni tipo: valore (blu), storytelling (viola), news (verde), riprova_sociale (arancione), coinvolgimento (giallo), promozione (rosso)
- Mostra label (es. "Valore", "Storytelling")
2. Creare frontend/src/components/BadgeSchwartz.tsx:
- Badge per livello Schwartz (L1-L5)
- Colori progressivi (L5 chiaro -> L1 scuro) per indicare vicinanza all'acquisto
- Tooltip con descrizione livello
3. Creare frontend/src/components/ProgressIndicator.tsx:
- Mostra progresso generazione bulk: "Post 3/13 in generazione..."
- Barra di progresso visuale
- Lista dei post con stato: pending (grigio), processing (spinner), success (verde check), failed (rosso X)
- Animazione per il post attualmente in generazione
4. Creare frontend/src/components/PostCard.tsx:
- Card per singolo post nel risultato
- Mostra: indice, tipo PN (badge), livello Schwartz (badge), formato narrativo, nicchia, data
- Se status=success: mostra cover_title come titolo card, click per espandere
- Se status=failed: card con sfondo rosso chiaro, icona errore, messaggio errore, pulsante "Riprova"
- Pulsante Riprova chiama useGenerateSingle() per rigenerare quel slot
- Click su card success -> espande per mostrare SlideViewer
5. Creare frontend/src/components/SlideViewer.tsx:
- Visualizzazione slide-by-slide con navigazione frecce laterali (stile Instagram stories)
- Mostra: slide corrente N/8, headline, body, image_keyword
- Freccia sinistra/destra per navigare
- Ogni campo testo e' EDITABILE inline: click per trasformare in input/textarea
- Le modifiche aggiornano lo stato locale (PostResult) che verra' usato per il CSV download
- Sotto le slide: caption Instagram in textarea editabile
- Keyboard navigation: frecce sinistra/destra per cambiare slide
6. Creare frontend/src/pages/GenerateCalendar.tsx:
- Form con campi:
- Obiettivo campagna (textarea, obbligatorio, placeholder "Es: Aumentare awareness sull'AI per PMI italiane")
- Settimane (number, default 2, range 1-4)
- Brand name (input, opzionale — prende default da Settings)
- Tono di voce (input, opzionale — prende default da Settings)
- Nicchie (multi-select o checkbox, prende default da Settings)
- Pulsante "Genera Calendario" con stati:
- Se API key non configurata: disabilitato, messaggio "Configura API key nelle Impostazioni"
- Se configurata: abilitato, al click mostra ProgressIndicator
- Usa useSettingsStatus() per controllare API key
- Usa useGenerateCalendar() mutation
- Al completamento (successo o parziale): redirect a OutputReview con jobId
7. Creare frontend/src/pages/OutputReview.tsx:
- Riceve jobId da route params
- Carica risultati con useJobResults(jobId)
- Header con: nome campagna, conteggio successi/falliti, pulsante "Download CSV"
- Griglia di PostCard (3 colonne desktop, 2 tablet, 1 mobile)
- PostCard espandibile con SlideViewer
- Pulsante "Download CSV":
- Chiama useDownloadCsv(jobId)
- Se ci sono post falliti: mostra nota "Il CSV contiene solo i N post generati con successo"
- Se tutti i post sono falliti: messaggio "Nessun post generato con successo. Riprova."
8. Creare frontend/src/pages/GenerateSingle.tsx:
- Form per generare un singolo post manualmente:
- Topic (textarea, obbligatorio)
- Tipo contenuto (select: valore, storytelling, news, riprova_sociale, coinvolgimento, promozione)
- Livello Schwartz (select: L1-L5)
- Nicchia (select dalla lista)
- Formato narrativo (select, auto-compilato in base a tipo+livello ma override possibile)
- Al submit: chiama useGenerateSingle()
- Mostra risultato con SlideViewer direttamente nella pagina
- Pulsante download CSV per singolo post
GESTIONE STATO EDIT INLINE (importante):
- OutputReview mantiene stato locale dei post (copia di GenerateResponse)
- Quando utente edita una slide in SlideViewer, aggiorna lo stato locale
- Il pulsante Download CSV usa lo stato locale aggiornato (non l'originale dal server)
- Questo significa che il CSV riflette le modifiche dell'utente
</action>
<verify>
- GenerateCalendar.tsx ha form con obiettivo e settimane, pulsante disabilitato senza API key
- OutputReview.tsx mostra griglia di PostCard con badge PN e Schwartz
- SlideViewer.tsx ha navigazione frecce e campi editabili inline
- PostCard.tsx ha stati distinti per success e failed, con pulsante Riprova
- ProgressIndicator.tsx mostra progresso per-item
- GenerateSingle.tsx ha form con select per tipo, livello, nicchia, formato
- npm run build completa senza errori TypeScript
</verify>
<done>
Web UI completa: form Genera Calendario con progress, griglia risultati con card/badge, SlideViewer con navigazione e edit inline, download CSV con modifiche utente, Genera Singolo Post, gestione errori per-item con Riprova.
</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<what-built>
Web UI completa con tutte le pagine: Dashboard, Genera Calendario, Output Review, Genera Singolo Post, Impostazioni. Inclusi progress indicator, griglia card con badge, navigazione slide, edit inline.
</what-built>
<how-to-verify>
1. Verificare che `cd frontend && npm run build` completa senza errori
2. Verificare che la struttura delle pagine e componenti sia coerente
3. Controllare che il routing in App.tsx abbia tutte le 5 route
4. Verificare che l'API client usi /postgenerator/api come base
5. Opzionale: se deployato, navigare su https://lab.mlhub.it/postgenerator/ e verificare che la UI si carichi
</how-to-verify>
<resume-signal>Digita "approved" se la UI e' accettabile, oppure descrivi i problemi da correggere</resume-signal>
</task>
</tasks>
<verification>
1. `cd frontend && npm run build` produce dist/ senza errori
2. App.tsx ha 5 route definite con BrowserRouter basename="/postgenerator"
3. Tutti i componenti importano tipi da types.ts (non definiscono tipi inline)
4. API hooks usano /postgenerator/api come base URL
5. PostCard ha due varianti visive: success (espandibile) e failed (errore + riprova)
6. SlideViewer supporta edit inline e navigazione frecce
7. GenerateCalendar disabilita pulsante se API key non configurata
8. OutputReview fa download CSV con le modifiche inline dell'utente
</verification>
<success_criteria>
- Dashboard mostra stato API key e quick actions
- Settings permette configurazione API key, modello, nicchie, frequenza
- Genera Calendario ha form, progress indicator, redirect a risultati
- Output Review mostra griglia card con badge, slide viewer con edit, download CSV
- Post falliti mostrano errore e pulsante Riprova
- Genera Singolo Post ha form completo con anteprima risultato
- Build frontend completa senza errori
</success_criteria>
<output>
After completion, create `.planning/phases/01-core-generation-pipeline/01-04-SUMMARY.md`
</output>