diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 0cfbb42..07f5bc6 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -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 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 -**Plans**: TBD +**Plans**: 2 plans Plans: -- [ ] 03-01: SwipeService — CRUD JSON collections (swipe_file.json), endpoint FastAPI lista/aggiungi/elimina, UI Swipe File Manager (aggiungi, lista, elimina) -- [ ] 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-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-PLAN.md — Swipe-to-calendar integration: topic_overrides in CalendarRequest + picker Swipe File nel form Genera Calendario con mark-used (Wave 2) --- diff --git a/.planning/phases/03-organization-layer/03-01-PLAN.md b/.planning/phases/03-organization-layer/03-01-PLAN.md new file mode 100644 index 0000000..67c25cb --- /dev/null +++ b/.planning/phases/03-organization-layer/03-01-PLAN.md @@ -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" +--- + + +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. + + + +@C:\Users\miche\.claude/get-shit-done/workflows/execute-plan.md +@C:\Users\miche\.claude/get-shit-done/templates/summary.md + + + +@.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 + + + + + + Task 1: Backend — SwipeService + Pydantic schemas + FastAPI router + + backend/schemas/swipe.py + backend/services/swipe_service.py + backend/routers/swipe.py + backend/main.py + + +**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. + + +- `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` + + +- 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 + + + + + Task 2: Frontend — TypeScript types + TanStack Query hooks + pagina SwipeFile + navigazione + + frontend/src/types.ts + frontend/src/api/hooks.ts + frontend/src/pages/SwipeFile.tsx + frontend/src/components/Sidebar.tsx + frontend/src/App.tsx + + +**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({ + queryKey: ['swipe'], + queryFn: () => apiGet('/swipe'), + staleTime: 30_000, + }) +} + +export function useAddSwipeItem() { + const queryClient = useQueryClient() + return useMutation({ + mutationFn: (item) => apiPost('/swipe', item), + onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['swipe'] }) }, + }) +} + +export function useUpdateSwipeItem() { + const queryClient = useQueryClient() + return useMutation({ + mutationFn: ({ id, data }) => apiPut(`/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 `} />` 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: } + ``` +- Importare `Lightbulb` da lucide-react + + +- `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 + + +- 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 + + + + + + +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 + + + +- 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 + + + +After completion, create `.planning/phases/03-organization-layer/03-01-SUMMARY.md` + diff --git a/.planning/phases/03-organization-layer/03-02-PLAN.md b/.planning/phases/03-organization-layer/03-02-PLAN.md new file mode 100644 index 0000000..7d0755f --- /dev/null +++ b/.planning/phases/03-organization-layer/03-02-PLAN.md @@ -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" +--- + + +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. + + + +@C:\Users\miche\.claude/get-shit-done/workflows/execute-plan.md +@C:\Users\miche\.claude/get-shit-done/templates/summary.md + + + +@.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 + + + + + + Task 1: Backend — topic_overrides in CalendarRequest + pipeline wiring + + backend/schemas/calendar.py + backend/services/generation_pipeline.py + + +**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. + + +- `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 + + +- 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) + + + + + Task 2: Frontend — Picker Swipe File nel form Genera Calendario + mark used + + frontend/src/pages/GenerateCalendar.tsx + frontend/src/api/hooks.ts + frontend/src/types.ts + + +**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 | 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({ + mutationFn: (id) => apiPost(`/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>({}) +const [showSwipePicker, setShowSwipePicker] = useState(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 + + +- `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` + + +- 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 + + + + + + +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 + + + +- 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 + + + +After completion, create `.planning/phases/03-organization-layer/03-02-SUMMARY.md` +