docs(03): create phase plan
Phase 03: Organization Layer - 2 plan(s) in 2 wave(s) - Wave 1: SwipeService CRUD + API + UI (parallel-ready) - Wave 2: Swipe-to-calendar integration (depends on 03-01) - Ready for execution Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -63,11 +63,11 @@ Plans:
|
|||||||
1. L'utente aggiunge un'idea al Swipe File con topic, nicchia e note; l'idea appare immediatamente nella lista e persiste al riavvio del container
|
1. L'utente aggiunge un'idea al Swipe File con topic, nicchia e note; l'idea appare immediatamente nella lista e persiste al riavvio del container
|
||||||
2. L'utente puo' eliminare una voce dal Swipe File e la lista si aggiorna
|
2. L'utente puo' eliminare una voce dal Swipe File e la lista si aggiorna
|
||||||
3. L'utente puo' selezionare un topic dallo Swipe File come override per uno slot specifico prima di avviare la generazione del calendario
|
3. L'utente puo' selezionare un topic dallo Swipe File come override per uno slot specifico prima di avviare la generazione del calendario
|
||||||
**Plans**: TBD
|
**Plans**: 2 plans
|
||||||
|
|
||||||
Plans:
|
Plans:
|
||||||
- [ ] 03-01: SwipeService — CRUD JSON collections (swipe_file.json), endpoint FastAPI lista/aggiungi/elimina, UI Swipe File Manager (aggiungi, lista, elimina)
|
- [ ] 03-01-PLAN.md — SwipeService CRUD backend + Pydantic schemas + FastAPI router + pagina SwipeFile UI con form aggiunta, modifica inline, eliminazione con conferma, filtro nicchia (Wave 1)
|
||||||
- [ ] 03-02: Swipe-to-calendar integration — meccanismo per selezionare topic da Swipe File come override slot nel form Genera Calendario (CAL-07 + SWP-04)
|
- [ ] 03-02-PLAN.md — Swipe-to-calendar integration: topic_overrides in CalendarRequest + picker Swipe File nel form Genera Calendario con mark-used (Wave 2)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
338
.planning/phases/03-organization-layer/03-01-PLAN.md
Normal file
338
.planning/phases/03-organization-layer/03-01-PLAN.md
Normal file
@@ -0,0 +1,338 @@
|
|||||||
|
---
|
||||||
|
phase: 03-organization-layer
|
||||||
|
plan: 01
|
||||||
|
type: execute
|
||||||
|
wave: 1
|
||||||
|
depends_on: []
|
||||||
|
files_modified:
|
||||||
|
- backend/services/swipe_service.py
|
||||||
|
- backend/schemas/swipe.py
|
||||||
|
- backend/routers/swipe.py
|
||||||
|
- backend/main.py
|
||||||
|
- frontend/src/types.ts
|
||||||
|
- frontend/src/api/hooks.ts
|
||||||
|
- frontend/src/pages/SwipeFile.tsx
|
||||||
|
- frontend/src/components/Sidebar.tsx
|
||||||
|
- frontend/src/App.tsx
|
||||||
|
autonomous: true
|
||||||
|
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "L'utente accede alla pagina Swipe File dalla sidebar e vede la lista delle idee salvate (o un messaggio vuoto)"
|
||||||
|
- "L'utente aggiunge un'idea con topic e opzionalmente nicchia e note; l'idea appare immediatamente nella lista"
|
||||||
|
- "L'utente clicca su un'idea e la modifica inline; il salvataggio persiste"
|
||||||
|
- "L'utente elimina un'idea con conferma; la lista si aggiorna"
|
||||||
|
- "Le idee persistono al riavvio del container (file JSON su disco)"
|
||||||
|
- "La lista e' ordinata cronologicamente (piu' recenti prima) e filtrabile per nicchia"
|
||||||
|
artifacts:
|
||||||
|
- path: "backend/services/swipe_service.py"
|
||||||
|
provides: "CRUD per swipe_file.json con load/save/add/update/delete"
|
||||||
|
contains: "class SwipeService"
|
||||||
|
- path: "backend/schemas/swipe.py"
|
||||||
|
provides: "Pydantic models per SwipeItem, SwipeItemCreate, SwipeItemUpdate"
|
||||||
|
contains: "class SwipeItem"
|
||||||
|
- path: "backend/routers/swipe.py"
|
||||||
|
provides: "REST API endpoints GET/POST/PUT/DELETE per swipe file"
|
||||||
|
contains: "router = APIRouter"
|
||||||
|
- path: "frontend/src/pages/SwipeFile.tsx"
|
||||||
|
provides: "Pagina SwipeFile con lista, form aggiunta, modifica inline, eliminazione con conferma"
|
||||||
|
contains: "export function SwipeFile"
|
||||||
|
key_links:
|
||||||
|
- from: "backend/routers/swipe.py"
|
||||||
|
to: "backend/services/swipe_service.py"
|
||||||
|
via: "SwipeService instance"
|
||||||
|
pattern: "SwipeService"
|
||||||
|
- from: "frontend/src/pages/SwipeFile.tsx"
|
||||||
|
to: "/api/swipe"
|
||||||
|
via: "TanStack Query hooks"
|
||||||
|
pattern: "useSwipeItems|useAddSwipeItem|useUpdateSwipeItem|useDeleteSwipeItem"
|
||||||
|
- from: "backend/main.py"
|
||||||
|
to: "backend/routers/swipe.py"
|
||||||
|
via: "app.include_router"
|
||||||
|
pattern: "swipe\\.router"
|
||||||
|
- from: "frontend/src/App.tsx"
|
||||||
|
to: "frontend/src/pages/SwipeFile.tsx"
|
||||||
|
via: "React Router Route"
|
||||||
|
pattern: "swipe-file"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
SwipeService backend (CRUD JSON file-based), endpoint FastAPI REST, e pagina Web UI completa per gestione Swipe File con aggiunta rapida, modifica inline, eliminazione con conferma, filtro per nicchia.
|
||||||
|
|
||||||
|
Purpose: Dare all'utente uno strumento di cattura veloce per idee e topic interessanti, consultabile e persistente, prima dell'integrazione con il form Genera Calendario (Piano 02).
|
||||||
|
Output: Backend CRUD funzionante su data/swipe_file.json + pagina Swipe File nella Web UI con tutte le operazioni CRUD.
|
||||||
|
</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/03-organization-layer/03-CONTEXT.md
|
||||||
|
@.planning/phases/01-core-generation-pipeline/01-04-SUMMARY.md
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Backend — SwipeService + Pydantic schemas + FastAPI router</name>
|
||||||
|
<files>
|
||||||
|
backend/schemas/swipe.py
|
||||||
|
backend/services/swipe_service.py
|
||||||
|
backend/routers/swipe.py
|
||||||
|
backend/main.py
|
||||||
|
</files>
|
||||||
|
<action>
|
||||||
|
**1. Crea `backend/schemas/swipe.py`** — Pydantic models:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class SwipeItem(BaseModel):
|
||||||
|
id: str # UUID generato dal backend
|
||||||
|
topic: str # Obbligatorio, min 3 chars
|
||||||
|
nicchia: Optional[str] = None # Opzionale (es. "dentisti", "ecommerce")
|
||||||
|
note: Optional[str] = None # Note libere opzionali
|
||||||
|
created_at: str # ISO datetime
|
||||||
|
updated_at: str # ISO datetime
|
||||||
|
used: bool = False # True quando usato come override in calendario
|
||||||
|
|
||||||
|
class SwipeItemCreate(BaseModel):
|
||||||
|
topic: str = Field(..., min_length=3, max_length=200)
|
||||||
|
nicchia: Optional[str] = None
|
||||||
|
note: Optional[str] = None
|
||||||
|
|
||||||
|
class SwipeItemUpdate(BaseModel):
|
||||||
|
topic: Optional[str] = Field(None, min_length=3, max_length=200)
|
||||||
|
nicchia: Optional[str] = None
|
||||||
|
note: Optional[str] = None
|
||||||
|
|
||||||
|
class SwipeListResponse(BaseModel):
|
||||||
|
items: list[SwipeItem]
|
||||||
|
total: int
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. Crea `backend/services/swipe_service.py`** — SwipeService class:
|
||||||
|
|
||||||
|
- `__init__(self, data_path: Path)`: riceve la directory DATA_PATH, file = `data_path / "swipe_file.json"`
|
||||||
|
- `_load() -> list[dict]`: legge il file JSON, ritorna lista vuota se non esiste
|
||||||
|
- `_save(items: list[dict])`: scrive il file JSON con encoding utf-8, indent=2
|
||||||
|
- `list_items() -> list[SwipeItem]`: carica, ordina per `created_at` descending (piu' recenti prima), ritorna come SwipeItem
|
||||||
|
- `add_item(data: SwipeItemCreate) -> SwipeItem`: genera UUID, datetime now ISO, salva, ritorna item creato
|
||||||
|
- `update_item(item_id: str, data: SwipeItemUpdate) -> SwipeItem`: trova per id, aggiorna solo campi non-None, aggiorna `updated_at`, salva. Raise ValueError se non trovato.
|
||||||
|
- `delete_item(item_id: str) -> bool`: trova per id, rimuove, salva. Raise ValueError se non trovato.
|
||||||
|
- `mark_used(item_id: str) -> SwipeItem`: setta `used=True`, aggiorna `updated_at`. Raise ValueError se non trovato.
|
||||||
|
- Usa `uuid.uuid4().hex[:12]` per ID brevi. Usa `datetime.now(timezone.utc).isoformat()` per timestamp.
|
||||||
|
|
||||||
|
**3. Crea `backend/routers/swipe.py`** — FastAPI router:
|
||||||
|
|
||||||
|
- `router = APIRouter(prefix="/api/swipe", tags=["swipe"])`
|
||||||
|
- Lazy init pattern identico a prompts.py: `_swipe_service: SwipeService | None = None` + `_get_swipe_service()` che crea l'istanza con `DATA_PATH`
|
||||||
|
- Endpoint:
|
||||||
|
- `GET /` -> `SwipeListResponse` — lista tutte le idee
|
||||||
|
- `POST /` -> `SwipeItem` (status 201) — aggiunge idea
|
||||||
|
- `PUT /{item_id}` -> `SwipeItem` — modifica idea (ValueError -> 404)
|
||||||
|
- `DELETE /{item_id}` -> `{"deleted": True}` (status 200) — elimina idea (ValueError -> 404)
|
||||||
|
- `POST /{item_id}/mark-used` -> `SwipeItem` — segna come usata (ValueError -> 404)
|
||||||
|
|
||||||
|
**4. Registra router in `backend/main.py`**:
|
||||||
|
|
||||||
|
- Importa `from backend.routers import ... swipe`
|
||||||
|
- Aggiungi `app.include_router(swipe.router)` PRIMA del SPA catch-all mount, nella sezione con gli altri router. Ordine: calendar, generate, export, settings, prompts, **swipe**.
|
||||||
|
- Aggiungi `DATA_PATH` al lifespan `mkdir` se non gia' incluso (verificare).
|
||||||
|
|
||||||
|
**Pattern da seguire**: Il router prompts.py e' il template di riferimento per lo stile, lazy init, error handling.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
- `python -c "from backend.schemas.swipe import SwipeItem, SwipeItemCreate, SwipeItemUpdate, SwipeListResponse; print('Schemas OK')"` passa
|
||||||
|
- `python -c "from backend.services.swipe_service import SwipeService; print('Service OK')"` passa
|
||||||
|
- `python -c "from backend.routers.swipe import router; print('Router OK')"` passa
|
||||||
|
- `python -c "from backend.main import app; print([r.path for r in app.routes])"` include `/api/swipe`
|
||||||
|
</verify>
|
||||||
|
<done>
|
||||||
|
- SwipeService legge/scrive data/swipe_file.json con CRUD completo
|
||||||
|
- Router espone 5 endpoint REST: list, add, update, delete, mark-used
|
||||||
|
- Router registrato in main.py prima del SPA catch-all
|
||||||
|
</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Frontend — TypeScript types + TanStack Query hooks + pagina SwipeFile + navigazione</name>
|
||||||
|
<files>
|
||||||
|
frontend/src/types.ts
|
||||||
|
frontend/src/api/hooks.ts
|
||||||
|
frontend/src/pages/SwipeFile.tsx
|
||||||
|
frontend/src/components/Sidebar.tsx
|
||||||
|
frontend/src/App.tsx
|
||||||
|
</files>
|
||||||
|
<action>
|
||||||
|
**1. Aggiungi types in `frontend/src/types.ts`** — Alla fine del file, sezione "Swipe File":
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Swipe File
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export interface SwipeItem {
|
||||||
|
id: string
|
||||||
|
topic: string
|
||||||
|
nicchia?: string | null
|
||||||
|
note?: string | null
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
used: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SwipeItemCreate {
|
||||||
|
topic: string
|
||||||
|
nicchia?: string | null
|
||||||
|
note?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SwipeItemUpdate {
|
||||||
|
topic?: string | null
|
||||||
|
nicchia?: string | null
|
||||||
|
note?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SwipeListResponse {
|
||||||
|
items: SwipeItem[]
|
||||||
|
total: number
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. Aggiungi hooks in `frontend/src/api/hooks.ts`** — Alla fine del file, sezione "Swipe File":
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Swipe File
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export function useSwipeItems() {
|
||||||
|
return useQuery<SwipeListResponse>({
|
||||||
|
queryKey: ['swipe'],
|
||||||
|
queryFn: () => apiGet<SwipeListResponse>('/swipe'),
|
||||||
|
staleTime: 30_000,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAddSwipeItem() {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
return useMutation<SwipeItem, Error, SwipeItemCreate>({
|
||||||
|
mutationFn: (item) => apiPost<SwipeItem>('/swipe', item),
|
||||||
|
onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['swipe'] }) },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUpdateSwipeItem() {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
return useMutation<SwipeItem, Error, { id: string; data: SwipeItemUpdate }>({
|
||||||
|
mutationFn: ({ id, data }) => apiPut<SwipeItem>(`/swipe/${id}`, data),
|
||||||
|
onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['swipe'] }) },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDeleteSwipeItem() {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
return useMutation<{ deleted: boolean }, Error, string>({
|
||||||
|
mutationFn: (id) => apiFetch<{ deleted: boolean }>(`/swipe/${id}`, { method: 'DELETE' }),
|
||||||
|
onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['swipe'] }) },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Importare i nuovi tipi (`SwipeItem`, `SwipeItemCreate`, `SwipeItemUpdate`, `SwipeListResponse`) e `apiFetch` dove necessario.
|
||||||
|
|
||||||
|
**3. Crea `frontend/src/pages/SwipeFile.tsx`** — Pagina completa:
|
||||||
|
|
||||||
|
Layout: stone/amber palette coerente con il resto dell'app.
|
||||||
|
|
||||||
|
**Struttura pagina:**
|
||||||
|
- Header: titolo "Swipe File", sottotitolo breve, contatore totale idee
|
||||||
|
- Form aggiunta rapida (inline, sempre visibile sopra la lista):
|
||||||
|
- Input topic (obbligatorio, placeholder "Scrivi un'idea o topic...")
|
||||||
|
- Input nicchia (opzionale, placeholder "Es. dentisti")
|
||||||
|
- Input note (opzionale, placeholder "Note aggiuntive...")
|
||||||
|
- Bottone "Aggiungi" (amber, con icona Plus)
|
||||||
|
- Reset form dopo successo
|
||||||
|
- Filtro nicchia: chip/pill cliccabili sopra la lista. Mostra tutte le nicchie uniche trovate nelle idee + chip "Tutte". Click filtra la lista. Chip attivo evidenziato in amber.
|
||||||
|
- Lista idee (cronologico inverso): card stone-800 per ogni idea con:
|
||||||
|
- Topic in bold come titolo
|
||||||
|
- Badge nicchia (se presente) — pill colorata
|
||||||
|
- Badge "Usato" se `used === true` — amber badge con icona CheckCircle
|
||||||
|
- Note se presenti — testo secondario stone-400
|
||||||
|
- Data relativa ("3 giorni fa", "1 ora fa") — usa logica semplice basata su differenza date, non libreria
|
||||||
|
- Bottone modifica (icona Pencil) — click attiva editing inline:
|
||||||
|
- I campi della card diventano input editabili
|
||||||
|
- Mostra bottoni Salva (Check) e Annulla (X)
|
||||||
|
- Salva chiama `useUpdateSwipeItem`, Annulla ripristina valori originali
|
||||||
|
- Bottone elimina (icona Trash2) — click mostra dialog conferma:
|
||||||
|
- Testo: "Eliminare questa idea? L'azione e' irreversibile."
|
||||||
|
- Bottoni: "Annulla" e "Elimina" (rosso)
|
||||||
|
- Conferma chiama `useDeleteSwipeItem`
|
||||||
|
- Stato vuoto: messaggio "Nessuna idea salvata" con invito a usare il form sopra
|
||||||
|
|
||||||
|
**Pattern UI da seguire**: Stile card identico a PostCard (stone-800, border stone-700, rounded-xl). Palette amber per accent. Lucide icons coerenti.
|
||||||
|
|
||||||
|
**Icone lucide da usare**: `Plus`, `Pencil`, `Trash2`, `CheckCircle`, `X`, `Check`, `Lightbulb`, `Filter`
|
||||||
|
|
||||||
|
**State management**:
|
||||||
|
- `editingId: string | null` — quale idea e' in modalita' edit
|
||||||
|
- `editForm: { topic, nicchia, note }` — valori temporanei durante editing
|
||||||
|
- `deleteConfirmId: string | null` — quale idea mostra il dialog di conferma
|
||||||
|
- `filterNicchia: string | null` — nicchia selezionata per filtro (null = tutte)
|
||||||
|
- `newItem: { topic, nicchia, note }` — form di aggiunta
|
||||||
|
|
||||||
|
**4. Aggiungi route in `frontend/src/App.tsx`**:
|
||||||
|
|
||||||
|
- Importa `SwipeFile` da `./pages/SwipeFile`
|
||||||
|
- Aggiungi `<Route path="/swipe-file" element={<SwipeFile />} />` dopo la route `/prompt-editor`
|
||||||
|
|
||||||
|
**5. Aggiungi voce navigazione in `frontend/src/components/Sidebar.tsx`**:
|
||||||
|
|
||||||
|
- Aggiungi nella lista `navItems`, DOPO "Prompt Editor" e PRIMA di "Impostazioni":
|
||||||
|
```
|
||||||
|
{ to: '/swipe-file', label: 'Swipe File', icon: <Lightbulb size={18} /> }
|
||||||
|
```
|
||||||
|
- Importare `Lightbulb` da lucide-react
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
- `cd frontend && npx tsc --noEmit` — nessun errore TypeScript
|
||||||
|
- `cd frontend && npm run build` — build pulita senza errori
|
||||||
|
- Sidebar contiene 6 link di navigazione (Dashboard, Genera Calendario, Genera Singolo Post, Prompt Editor, Swipe File, Impostazioni)
|
||||||
|
- Route `/swipe-file` esiste in App.tsx
|
||||||
|
- hooks.ts contiene 4 nuovi hook: useSwipeItems, useAddSwipeItem, useUpdateSwipeItem, useDeleteSwipeItem
|
||||||
|
</verify>
|
||||||
|
<done>
|
||||||
|
- Pagina SwipeFile completa con form aggiunta, lista con filtro nicchia, modifica inline, eliminazione con conferma
|
||||||
|
- 4 TanStack Query hooks che cablano tutti gli endpoint swipe
|
||||||
|
- Navigazione sidebar con link "Swipe File" con icona Lightbulb
|
||||||
|
- Route `/swipe-file` registrata in App.tsx
|
||||||
|
- TypeScript compila senza errori
|
||||||
|
</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
1. Backend: `python -c "from backend.main import app; print('OK')"` — app importa senza errori
|
||||||
|
2. Frontend: `cd frontend && npx tsc --noEmit && npm run build` — build pulita
|
||||||
|
3. Sidebar mostra 6 link di navigazione con Swipe File presente
|
||||||
|
4. Route `/swipe-file` registrata e componente SwipeFile renderizza
|
||||||
|
5. types.ts contiene le 4 interfacce SwipeItem, SwipeItemCreate, SwipeItemUpdate, SwipeListResponse
|
||||||
|
6. hooks.ts contiene 4 nuovi hook per CRUD swipe
|
||||||
|
7. Router swipe registrato in main.py con 5 endpoint
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- SwipeService legge e scrive data/swipe_file.json con CRUD completo (add, update, delete, mark_used, list con ordinamento cronologico inverso)
|
||||||
|
- 5 endpoint REST funzionanti: GET /api/swipe, POST /api/swipe, PUT /api/swipe/{id}, DELETE /api/swipe/{id}, POST /api/swipe/{id}/mark-used
|
||||||
|
- Pagina SwipeFile accessibile dalla sidebar con form aggiunta inline, lista filtrata per nicchia, modifica inline, eliminazione con conferma dialog
|
||||||
|
- TypeScript e build frontend compilano senza errori
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.planning/phases/03-organization-layer/03-01-SUMMARY.md`
|
||||||
|
</output>
|
||||||
212
.planning/phases/03-organization-layer/03-02-PLAN.md
Normal file
212
.planning/phases/03-organization-layer/03-02-PLAN.md
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
---
|
||||||
|
phase: 03-organization-layer
|
||||||
|
plan: 02
|
||||||
|
type: execute
|
||||||
|
wave: 2
|
||||||
|
depends_on: ["03-01"]
|
||||||
|
files_modified:
|
||||||
|
- frontend/src/pages/GenerateCalendar.tsx
|
||||||
|
- frontend/src/api/hooks.ts
|
||||||
|
autonomous: true
|
||||||
|
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "L'utente puo' aprire un picker dallo Swipe File nel form Genera Calendario e selezionare un topic come override per uno slot specifico"
|
||||||
|
- "Dopo la selezione, l'idea nello Swipe File viene marcata come 'usata' con badge visivo"
|
||||||
|
- "L'utente vede i topic override selezionati nel form prima di avviare la generazione"
|
||||||
|
- "I topic override selezionati vengono passati alla generazione bulk e applicati agli slot corrispondenti"
|
||||||
|
artifacts:
|
||||||
|
- path: "frontend/src/pages/GenerateCalendar.tsx"
|
||||||
|
provides: "Sezione override topic con picker da Swipe File integrato nel form"
|
||||||
|
contains: "swipe"
|
||||||
|
key_links:
|
||||||
|
- from: "frontend/src/pages/GenerateCalendar.tsx"
|
||||||
|
to: "/api/swipe"
|
||||||
|
via: "useSwipeItems hook + useMarkSwipeUsed"
|
||||||
|
pattern: "useSwipeItems|markUsed"
|
||||||
|
- from: "frontend/src/pages/GenerateCalendar.tsx"
|
||||||
|
to: "/api/generate/bulk"
|
||||||
|
via: "CalendarRequest with topic_overrides"
|
||||||
|
pattern: "topic_overrides"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Integrazione Swipe File nel form Genera Calendario: meccanismo per selezionare topic dallo Swipe File come override per slot specifici prima della generazione batch.
|
||||||
|
|
||||||
|
Purpose: Permette all'utente di usare idee catturate nello Swipe File come topic pre-assegnati nel calendario, combinando l'ispirazione spontanea con la generazione strategica.
|
||||||
|
Output: Sezione "Topic Override" nel form Genera Calendario con picker modale da Swipe File, invio degli override alla generazione bulk.
|
||||||
|
</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/03-organization-layer/03-CONTEXT.md
|
||||||
|
@.planning/phases/03-organization-layer/03-01-SUMMARY.md
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Backend — topic_overrides in CalendarRequest + pipeline wiring</name>
|
||||||
|
<files>
|
||||||
|
backend/schemas/calendar.py
|
||||||
|
backend/services/generation_pipeline.py
|
||||||
|
</files>
|
||||||
|
<action>
|
||||||
|
**1. Aggiorna `backend/schemas/calendar.py`** — Aggiungi campo `topic_overrides` a `CalendarRequest`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
topic_overrides: Optional[dict[int, str]] = Field(
|
||||||
|
default=None,
|
||||||
|
description="Override topic per slot specifici. Chiave: indice slot (0-12), valore: topic. "
|
||||||
|
"Gli slot con override skipperanno la generazione topic LLM.",
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Questo campo e' un dizionario `{slot_index: topic_string}`. Esempio: `{2: "3 errori che costano clienti", 7: "Case study dentista Milano"}`.
|
||||||
|
|
||||||
|
**2. Aggiorna `backend/services/generation_pipeline.py`** — Nel metodo che genera i topic per ogni slot (dentro `generate_bulk_async` o il metodo chiamato da esso), dopo aver creato i CalendarSlot:
|
||||||
|
|
||||||
|
- Se `request.topic_overrides` esiste e contiene l'indice dello slot corrente, usa il topic override invece di chiamare l'LLM per generare il topic.
|
||||||
|
- Applica il topic override al campo `slot.topic` dello slot corrispondente.
|
||||||
|
- La logica e': `if topic_overrides and slot.indice in topic_overrides: slot.topic = topic_overrides[slot.indice]` — skip la chiamata LLM per generare il topic per quello slot.
|
||||||
|
|
||||||
|
Assicurarsi che il campo topic venga applicato PRIMA della generazione del contenuto del carosello, perche' il topic viene passato al prompt LLM.
|
||||||
|
|
||||||
|
**Pattern**: Leggere attentamente `generation_pipeline.py` per capire dove i topic vengono generati/assegnati e inserire l'override nel punto giusto del flusso.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
- `python -c "from backend.schemas.calendar import CalendarRequest; r = CalendarRequest(obiettivo_campagna='Test obiettivo campagna', topic_overrides={0: 'Test topic'}); print(r.topic_overrides)"` stampa `{0: 'Test topic'}`
|
||||||
|
- `python -c "from backend.services.generation_pipeline import GenerationPipeline; print('OK')"` importa senza errori
|
||||||
|
</verify>
|
||||||
|
<done>
|
||||||
|
- CalendarRequest accetta `topic_overrides` opzionale (dict[int, str])
|
||||||
|
- GenerationPipeline applica topic override agli slot corrispondenti prima della generazione LLM
|
||||||
|
- Slot senza override continuano a funzionare come prima (topic generato dall'LLM)
|
||||||
|
</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Frontend — Picker Swipe File nel form Genera Calendario + mark used</name>
|
||||||
|
<files>
|
||||||
|
frontend/src/pages/GenerateCalendar.tsx
|
||||||
|
frontend/src/api/hooks.ts
|
||||||
|
frontend/src/types.ts
|
||||||
|
</files>
|
||||||
|
<action>
|
||||||
|
**1. Aggiorna `frontend/src/types.ts`** — Aggiungi `topic_overrides` a `CalendarRequest`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export interface CalendarRequest {
|
||||||
|
obiettivo_campagna: string
|
||||||
|
settimane?: number
|
||||||
|
nicchie?: string[] | null
|
||||||
|
frequenza_post?: number
|
||||||
|
data_inizio?: string | null
|
||||||
|
topic_overrides?: Record<number, string> | null // slot_index -> topic
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. Aggiungi hook `useMarkSwipeUsed` in `frontend/src/api/hooks.ts`** (nella sezione Swipe File):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export function useMarkSwipeUsed() {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
return useMutation<SwipeItem, Error, string>({
|
||||||
|
mutationFn: (id) => apiPost<SwipeItem>(`/swipe/${id}/mark-used`),
|
||||||
|
onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['swipe'] }) },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. Modifica `frontend/src/pages/GenerateCalendar.tsx`** — Aggiungi sezione "Topic Override dallo Swipe File":
|
||||||
|
|
||||||
|
**Nuovo state:**
|
||||||
|
```typescript
|
||||||
|
const [topicOverrides, setTopicOverrides] = useState<Record<number, { topic: string; swipeId: string }>>({})
|
||||||
|
const [showSwipePicker, setShowSwipePicker] = useState<number | null>(null) // slot index attivo
|
||||||
|
```
|
||||||
|
|
||||||
|
**Sezione UI** — Aggiungere DOPO il campo "Tono di voce" e PRIMA del checkbox Nicchie:
|
||||||
|
|
||||||
|
- Titolo: "Topic Override (opzionale)" con sottotitolo "Seleziona topic dallo Swipe File per slot specifici"
|
||||||
|
- Griglia 13 slot (indici 0-12), ognuno con:
|
||||||
|
- Label: "Slot {N+1}" (1-based per utente)
|
||||||
|
- Se override assegnato: mostra topic troncato + bottone X per rimuovere override
|
||||||
|
- Se nessun override: bottone piccolo "Da Swipe File" (icona Lightbulb) per aprire il picker
|
||||||
|
- Il picker e' un pannello/dropdown che appare inline sotto lo slot cliccato:
|
||||||
|
- Mostra lista idee dallo Swipe File (via `useSwipeItems`)
|
||||||
|
- Ogni idea mostra: topic, nicchia badge, badge "usato" se gia' usata
|
||||||
|
- Click su un'idea: assegna il topic allo slot, chiude picker, chiama `useMarkSwipeUsed(swipeId)`
|
||||||
|
- Bottone "Chiudi" per chiudere senza selezionare
|
||||||
|
- Se Swipe File vuoto: messaggio "Nessuna idea nel tuo Swipe File" con link a `/swipe-file`
|
||||||
|
|
||||||
|
**Modifica handleSubmit** — Includi topic_overrides nella request:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const req: CalendarRequest = {
|
||||||
|
obiettivo_campagna: obiettivo.trim(),
|
||||||
|
settimane,
|
||||||
|
frequenza_post: settings?.frequenza_post ?? 3,
|
||||||
|
nicchie: customNicchie ? settings?.nicchie_attive : undefined,
|
||||||
|
topic_overrides: Object.keys(topicOverrides).length > 0
|
||||||
|
? Object.fromEntries(
|
||||||
|
Object.entries(topicOverrides).map(([k, v]) => [parseInt(k), v.topic])
|
||||||
|
)
|
||||||
|
: undefined,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Stile UI:**
|
||||||
|
- La griglia dei 13 slot usa un layout compatto: 3-4 colonne su desktop, 1 su mobile
|
||||||
|
- Ogni slot e' una mini-card stone-800 con bordo stone-700
|
||||||
|
- Slot con override: bordo amber-500/30, background amber-500/5
|
||||||
|
- Il picker inline usa la stessa palette stone/amber
|
||||||
|
- Animazione ingresso/uscita non necessaria — keep it simple
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
- `cd frontend && npx tsc --noEmit` — nessun errore TypeScript
|
||||||
|
- `cd frontend && npm run build` — build pulita
|
||||||
|
- CalendarRequest in types.ts include `topic_overrides`
|
||||||
|
- hooks.ts contiene `useMarkSwipeUsed`
|
||||||
|
- GenerateCalendar.tsx importa `useSwipeItems` e `useMarkSwipeUsed`
|
||||||
|
</verify>
|
||||||
|
<done>
|
||||||
|
- Il form Genera Calendario mostra 13 slot con possibilita' di assegnare topic override dallo Swipe File
|
||||||
|
- Click "Da Swipe File" apre picker inline con lista idee
|
||||||
|
- Selezione di un'idea assegna il topic allo slot e marca l'idea come "usata"
|
||||||
|
- Override rimovibili con bottone X
|
||||||
|
- Gli override vengono passati come `topic_overrides` nella CalendarRequest alla generazione bulk
|
||||||
|
- TypeScript compila senza errori
|
||||||
|
</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
1. Frontend: `cd frontend && npx tsc --noEmit && npm run build` — build pulita
|
||||||
|
2. Backend: `python -c "from backend.schemas.calendar import CalendarRequest; print(CalendarRequest.model_fields.keys())"` include topic_overrides
|
||||||
|
3. CalendarRequest in types.ts include topic_overrides
|
||||||
|
4. GenerateCalendar.tsx ha griglia 13 slot con bottoni picker
|
||||||
|
5. Hook useMarkSwipeUsed presente in hooks.ts
|
||||||
|
6. La generazione bulk riceve e applica topic_overrides
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- L'utente puo' selezionare topic dallo Swipe File per slot specifici nel form Genera Calendario
|
||||||
|
- I topic selezionati vengono visualizzati nella griglia slot prima della generazione
|
||||||
|
- I topic override vengono inviati alla generazione bulk via CalendarRequest.topic_overrides
|
||||||
|
- L'idea Swipe File selezionata viene marcata come "usata" con badge visivo
|
||||||
|
- Gli override sono rimovibili (bottone X) prima di avviare la generazione
|
||||||
|
- Slot senza override continuano a generare topic automaticamente via LLM
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.planning/phases/03-organization-layer/03-02-SUMMARY.md`
|
||||||
|
</output>
|
||||||
Reference in New Issue
Block a user