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:
@@ -29,13 +29,13 @@ Decimal phases appear between their surrounding integers in numeric order.
|
|||||||
3. Al termine della generazione, l'utente puo' scaricare un file CSV che si apre correttamente in Excel con caratteri italiani intatti e header che corrispondono esattamente ai placeholder del template Canva
|
3. Al termine della generazione, l'utente puo' scaricare un file CSV che si apre correttamente in Excel con caratteri italiani intatti e header che corrispondono esattamente ai placeholder del template Canva
|
||||||
4. Il CSV contiene esattamente 13 righe di contenuto con la distribuzione Persuasion Nurturing corretta (4 valore, 2 storytelling, 2 news, 3 riprova, 1 coinvolgimento, 1 promo) e i livelli Schwartz assegnati
|
4. Il CSV contiene esattamente 13 righe di contenuto con la distribuzione Persuasion Nurturing corretta (4 valore, 2 storytelling, 2 news, 3 riprova, 1 coinvolgimento, 1 promo) e i livelli Schwartz assegnati
|
||||||
5. Se la generazione di un singolo post fallisce (errore API), gli altri post del batch sono salvati e scaricabili; il post fallito e' marcato come errore senza bloccare il resto
|
5. Se la generazione di un singolo post fallisce (errore API), gli altri post del batch sono salvati e scaricabili; il post fallito e' marcato come errore senza bloccare il resto
|
||||||
**Plans**: TBD
|
**Plans**: 4 plans
|
||||||
|
|
||||||
Plans:
|
Plans:
|
||||||
- [ ] 01-01: Infrastructure setup — FastAPI + React SPA single container, Docker multi-stage build, subpath /postgenerator/ configurato correttamente (root_path via Uvicorn, Vite base, nginx lab-router)
|
- [ ] 01-01-PLAN.md — Infrastructure setup: FastAPI skeleton + React SPA + Docker multi-stage build + subpath /postgenerator/ (Wave 1)
|
||||||
- [ ] 01-02: Core services — CalendarService (distribuzione 13 post, Schwartz, nicchie, date), FormatSelector (mapping tipo x Schwartz → formato), PromptService base (carica .txt, compile variabili), file-based storage layout
|
- [ ] 01-02-PLAN.md — Core services: CalendarService + FormatSelector + PromptService + costanti dominio + 5 prompt italiani (Wave 1)
|
||||||
- [ ] 01-03: LLM pipeline — LLMService (Claude API, retry, backoff, rate limit, JSON validation), CSVBuilder (header Canva locked, utf-8-sig encoding), per-item error isolation
|
- [ ] 01-03-PLAN.md — LLM pipeline: LLMService + CSVBuilder + GenerationPipeline + API routers (Wave 2)
|
||||||
- [ ] 01-04: Web UI base — Dashboard, form Genera Calendario, form Genera Singolo Post, Output Review (anteprima slide-by-slide), Impostazioni, progress indicator bulk
|
- [ ] 01-04-PLAN.md — Web UI: Dashboard + Genera Calendario + Output Review + Genera Singolo + Settings (Wave 3)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -93,7 +93,7 @@ Phases execute in numeric order: 1 → 2 → 3 → 4
|
|||||||
|
|
||||||
| Phase | Plans Complete | Status | Completed |
|
| Phase | Plans Complete | Status | Completed |
|
||||||
|-------|----------------|--------|-----------|
|
|-------|----------------|--------|-----------|
|
||||||
| 1. Core Generation Pipeline | 0/4 | Not started | - |
|
| 1. Core Generation Pipeline | 0/4 | Planned (3 waves) | - |
|
||||||
| 2. Prompt Control + Output Review | 0/2 | Not started | - |
|
| 2. Prompt Control + Output Review | 0/2 | Not started | - |
|
||||||
| 3. Organization Layer | 0/2 | Not started | - |
|
| 3. Organization Layer | 0/2 | Not started | - |
|
||||||
| 4. Enrichment | 0/1 | Not started | - |
|
| 4. Enrichment | 0/1 | Not started | - |
|
||||||
|
|||||||
255
.planning/phases/01-core-generation-pipeline/01-01-PLAN.md
Normal file
255
.planning/phases/01-core-generation-pipeline/01-01-PLAN.md
Normal 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>
|
||||||
311
.planning/phases/01-core-generation-pipeline/01-02-PLAN.md
Normal file
311
.planning/phases/01-core-generation-pipeline/01-02-PLAN.md
Normal 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>
|
||||||
265
.planning/phases/01-core-generation-pipeline/01-03-PLAN.md
Normal file
265
.planning/phases/01-core-generation-pipeline/01-03-PLAN.md
Normal 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>
|
||||||
328
.planning/phases/01-core-generation-pipeline/01-04-PLAN.md
Normal file
328
.planning/phases/01-core-generation-pipeline/01-04-PLAN.md
Normal 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>
|
||||||
Reference in New Issue
Block a user