Files
Michele 5ba641e7d6 docs(02): create phase plan
Phase 02: Prompt Control + Output Review
- 2 plan(s) in 2 wave(s)
- Wave 1: 02-01 (Prompt Editor backend+frontend)
- Wave 2: 02-02 (Per-item regen + summary counter)
- Ready for execution
2026-03-08 20:17:29 +01:00

14 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, must_haves
phase plan type wave depends_on files_modified autonomous must_haves
02-prompt-control-output-review 01 execute 1
backend/routers/prompts.py
backend/main.py
frontend/src/types.ts
frontend/src/api/hooks.ts
frontend/src/pages/PromptEditor.tsx
frontend/src/App.tsx
frontend/src/components/Sidebar.tsx
true
truths artifacts key_links
GET /api/prompts ritorna lista dei prompt .txt disponibili con flag modificato/default
GET /api/prompts/{name} ritorna il contenuto grezzo del prompt e le variabili richieste
PUT /api/prompts/{name} salva il contenuto modificato del prompt con validazione variabili obbligatorie
POST /api/prompts/{name}/reset ripristina il prompt al contenuto default originale
L'utente vede la pagina Prompt Editor nella sidebar e puo' navigarci
L'utente vede la lista dei prompt, seleziona uno, e ne modifica il contenuto in una textarea
L'utente vede le variabili richieste dal prompt selezionato con il nome di ciascuna
L'utente puo' salvare un prompt modificato e il badge Modificato appare nella lista
L'utente puo' resettare un prompt al default originale con il pulsante Reset
path provides contains
backend/routers/prompts.py API CRUD per prompt: list, read, write, reset, variables router = APIRouter
path provides contains
frontend/src/pages/PromptEditor.tsx Pagina Prompt Editor con lista, textarea, variabili, salva, reset export function PromptEditor
from to via pattern
frontend/src/pages/PromptEditor.tsx /api/prompts TanStack Query hooks usePromptList, usePrompt, useSavePrompt, useResetPrompt usePromptList|usePrompt|useSavePrompt|useResetPrompt
from to via pattern
frontend/src/App.tsx frontend/src/pages/PromptEditor.tsx React Router route /prompt-editor Route.*prompt-editor
from to via pattern
frontend/src/components/Sidebar.tsx /prompt-editor NavLink prompt-editor
from to via pattern
backend/main.py backend/routers/prompts.py app.include_router include_router.*prompts
PromptService CRUD completo — endpoint FastAPI per lista/leggi/scrivi file prompt con validazione variabili e reset al default, UI Prompt Editor con lista prompt, textarea per modifica, badge modificato/default, variabili richieste, e pulsante reset.

Purpose: Dare all'utente il controllo completo sui prompt LLM direttamente dalla Web UI, senza dover accedere ai file sul server o modificare il codice. Copre i requirement PRM-05 e UI-05.

Output: Backend router prompts.py con 4 endpoint + pagina React PromptEditor.tsx con navigazione integrata

<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>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @backend/services/prompt_service.py @backend/main.py @backend/config.py @backend/routers/settings.py @frontend/src/api/client.ts @frontend/src/api/hooks.ts @frontend/src/types.ts @frontend/src/App.tsx @frontend/src/components/Sidebar.tsx Task 1: Backend prompts router — 4 endpoint CRUD per prompt con reset al default backend/routers/prompts.py, backend/main.py Crea `backend/routers/prompts.py` con `APIRouter(prefix="/api/prompts", tags=["prompts"])` seguendo lo stesso pattern di `backend/routers/settings.py`.

Crea istanza PromptService da PROMPTS_PATH (importa da backend.config).

Endpoint 1: GET /api/prompts — Lista prompt disponibili.

  • Chiama prompt_service.list_prompts() per ottenere i nomi.
  • Per ciascuno, determina se e' stato modificato rispetto al default: confronta il contenuto in PROMPTS_PATH con il file corrispondente in backend/data/prompts/ (la directory sorgente dei default). Se il file default non esiste O il contenuto differisce -> modified: true.
  • Response schema PromptListResponse:
    class PromptInfo(BaseModel):
        name: str          # Nome senza estensione (es. "pas_valore")
        modified: bool     # True se diverso dal default o se default non esiste
    
    class PromptListResponse(BaseModel):
        prompts: list[PromptInfo]
    

Endpoint 2: GET /api/prompts/{name} — Leggi prompt con variabili richieste.

  • Valida che il prompt esista con prompt_service.prompt_exists(name), altrimenti HTTPException 404.
  • Chiama prompt_service.load_prompt(name) per il contenuto.
  • Chiama prompt_service.get_required_variables(name) per le variabili.
  • Determina modified confrontando con default (come sopra).
  • Response schema PromptDetailResponse:
    class PromptDetailResponse(BaseModel):
        name: str
        content: str
        variables: list[str]  # ["brand_name", "livello_schwartz", "obiettivo_campagna", ...]
        modified: bool
    

Endpoint 3: PUT /api/prompts/{name} — Salva prompt modificato.

  • Request body:
    class SavePromptRequest(BaseModel):
        content: str = Field(..., min_length=10, description="Contenuto del prompt")
    
  • Chiama prompt_service.save_prompt(name, content).
  • Dopo il salvataggio, ritorna PromptDetailResponse con i dati aggiornati (incluse le variabili ricalcolate dal nuovo contenuto).
  • Se prompt_service.save_prompt() lancia ValueError (nome non valido), ritorna HTTPException 400.

Endpoint 4: POST /api/prompts/{name}/reset — Reset al default.

  • Trova il file default in Path(__file__).parent.parent / "data" / "prompts" / f"{name}.txt" (stessa directory usata in main.py lifespan per la copia iniziale).
  • Se il default non esiste, HTTPException 404 con messaggio "Nessun default disponibile per questo prompt".
  • Copia il contenuto default su PROMPTS_PATH sovrascrivendo il corrente.
  • Ritorna PromptDetailResponse con il contenuto ripristinato.

Costante helper per la directory default:

_DEFAULT_PROMPTS_DIR = Path(__file__).parent.parent / "data" / "prompts"

Funzione helper _is_modified(name: str) -> bool:

def _is_modified(name: str) -> bool:
    default_path = _DEFAULT_PROMPTS_DIR / f"{name}.txt"
    if not default_path.exists():
        return True  # No default = considered custom/modified
    current = prompt_service.load_prompt(name)
    default = default_path.read_text(encoding="utf-8")
    return current != default

Registra il router in main.py: Aggiungi from backend.routers import prompts e app.include_router(prompts.router) PRIMA del SPA catch-all mount, dopo gli altri router (stessa posizione degli altri include_router). Verifica che il file backend/routers/prompts.py esista e contenga i 4 endpoint. Verifica che backend/main.py includa app.include_router(prompts.router). Apri un terminale: cd lab/postgenerator && python -c "from backend.routers.prompts import router; print('OK:', [r.path for r in router.routes])" per verificare che il modulo si importi senza errori. 4 endpoint registrati: GET /api/prompts, GET /api/prompts/{name}, PUT /api/prompts/{name}, POST /api/prompts/{name}/reset. Il router si importa senza errori e main.py lo include.

Task 2: Frontend Prompt Editor — pagina, hooks, types, route, sidebar frontend/src/types.ts, frontend/src/api/hooks.ts, frontend/src/pages/PromptEditor.tsx, frontend/src/App.tsx, frontend/src/components/Sidebar.tsx **1. Types** — Aggiungi in fondo a `frontend/src/types.ts`: ```typescript // --------------------------------------------------------------------------- // Prompt Editor // ---------------------------------------------------------------------------

export interface PromptInfo { name: string modified: boolean }

export interface PromptListResponse { prompts: PromptInfo[] }

export interface PromptDetail { name: string content: string variables: string[] modified: boolean }


**2. Hooks** — Aggiungi in fondo a `frontend/src/api/hooks.ts`:
```typescript
// ---------------------------------------------------------------------------
// Prompt Editor
// ---------------------------------------------------------------------------

/** Lista tutti i prompt disponibili con flag modificato/default. */
export function usePromptList() {
  return useQuery<PromptListResponse>({
    queryKey: ['prompts'],
    queryFn: () => apiGet<PromptListResponse>('/prompts'),
    staleTime: 30_000,
  })
}

/** Carica contenuto + variabili di un singolo prompt. */
export function usePrompt(name: string | null) {
  return useQuery<PromptDetail>({
    queryKey: ['prompts', name],
    queryFn: () => apiGet<PromptDetail>(`/prompts/${name}`),
    enabled: !!name,
    staleTime: 0,  // Sempre fresco dopo edit
  })
}

/** Salva il contenuto modificato di un prompt. */
export function useSavePrompt() {
  const queryClient = useQueryClient()
  return useMutation<PromptDetail, Error, { name: string; content: string }>({
    mutationFn: ({ name, content }) =>
      apiPut<PromptDetail>(`/prompts/${name}`, { content }),
    onSuccess: (data) => {
      queryClient.invalidateQueries({ queryKey: ['prompts'] })
      queryClient.setQueryData(['prompts', data.name], data)
    },
  })
}

/** Reset un prompt al default originale. */
export function useResetPrompt() {
  const queryClient = useQueryClient()
  return useMutation<PromptDetail, Error, string>({
    mutationFn: (name) => apiPost<PromptDetail>(`/prompts/${name}/reset`),
    onSuccess: (data) => {
      queryClient.invalidateQueries({ queryKey: ['prompts'] })
      queryClient.setQueryData(['prompts', data.name], data)
    },
  })
}

Aggiungi gli import dei nuovi types (PromptListResponse, PromptDetail) nell'import block di hooks.ts.

3. Pagina PromptEditor.tsx — Crea frontend/src/pages/PromptEditor.tsx.

Layout: due colonne affiancate su desktop (lg:), una colonna su mobile.

  • Colonna sinistra (1/3 su lg): Lista dei prompt con badge "Modificato" (amber pill) o "Default" (stone pill). Click su un prompt lo seleziona.
  • Colonna destra (2/3 su lg): Editor per il prompt selezionato.
    • Header: nome prompt + badge modificato/default
    • Textarea monospace full-width, min-height 400px, background stone-900, border stone-700. Il testo e' il contenuto grezzo del prompt.
    • Sotto la textarea: sezione "Variabili richieste" con chip per ogni variabile (es. {{topic}}, {{obiettivo_campagna}}). Ogni chip in stone-700/amber-200 testo. Le variabili si aggiornano live dalla textarea (regex client-side) per dare feedback immediato PRIMA del salvataggio.
    • Due bottoni:
      • "Salva" (amber-500, primario) — chiama useSavePrompt, mostra toast-style feedback (testo sotto il bottone "Salvato con successo" / errore)
      • "Reset al Default" (stone-600, secondario) — chiama useResetPrompt. Mostra conferma inline ("Sei sicuro? Ripristinera' il prompt originale.") prima di eseguire. Bottone disabilitato se il prompt non e' modificato. Dopo reset, aggiorna la textarea con il contenuto default.

Gestione stato:

  • selectedPrompt: string | null — nome del prompt selezionato
  • editContent: string — contenuto nella textarea (inizializzato da usePrompt, aggiornato dall'utente)
  • dirty: boolean — true se editContent != contenuto originale dal server (abilita bottone Salva)
  • showResetConfirm: boolean — per la conferma inline del reset
  • clientVariables: string[] — variabili estratte client-side da editContent con regex /\{\{(\w+)\}\}/g

Quando l'utente cambia prompt nella lista: se dirty, nessun prompt guard (non bloccare la navigazione, perdi le modifiche — workflow leggero). Resetta editContent con i dati del nuovo prompt.

Palette: stone/amber consistente con il resto dell'app. Textarea con font mono (font-mono).

4. Route — In frontend/src/App.tsx, aggiungi:

  • Import: import { PromptEditor } from './pages/PromptEditor'
  • Route: <Route path="/prompt-editor" element={<PromptEditor />} />

5. Sidebar — In frontend/src/components/Sidebar.tsx:

  • Aggiungi icona import: Pencil da lucide-react (gia' disponibile nel pacchetto)
  • Aggiungi nav item DOPO "Genera Singolo Post" e PRIMA di "Impostazioni":
    {
      to: '/prompt-editor',
      label: 'Prompt Editor',
      icon: <Pencil size={18} />,
    },
    

Verifica che frontend/src/pages/PromptEditor.tsx esista e contenga export function PromptEditor. Verifica che frontend/src/App.tsx contenga la route /prompt-editor. Verifica che frontend/src/components/Sidebar.tsx contenga il link al Prompt Editor. Verifica che frontend/src/api/hooks.ts contenga usePromptList, usePrompt, useSavePrompt, useResetPrompt. Verifica che frontend/src/types.ts contenga PromptInfo, PromptListResponse, PromptDetail. Esegui cd lab/postgenerator/frontend && npx tsc --noEmit per verificare che il TypeScript compili senza errori. La pagina Prompt Editor e' navigabile dalla sidebar, mostra la lista dei prompt con badge modificato/default, permette di editare il contenuto in una textarea monospace, mostra le variabili richieste come chip, salva le modifiche via API, e resetta al default con conferma. TypeScript compila senza errori.

1. Il backend risponde a GET /api/prompts con la lista dei prompt e flag modified 2. Il backend risponde a GET /api/prompts/pas_valore con contenuto, variabili, e flag modified 3. PUT /api/prompts/pas_valore salva il nuovo contenuto e ritorna le variabili aggiornate 4. POST /api/prompts/pas_valore/reset ripristina il contenuto default 5. La pagina Prompt Editor e' raggiungibile dalla sidebar 6. L'utente puo' selezionare un prompt, modificarlo, salvarlo, e resettarlo al default 7. Le variabili richieste si aggiornano live durante la modifica 8. TypeScript compila senza errori (npx tsc --noEmit)

<success_criteria>

  • L'utente apre /prompt-editor, vede la lista dei prompt .txt, ne seleziona uno e ne modifica il contenuto
  • Dopo aver salvato, un badge "Modificato" appare nella lista
  • Il pulsante "Reset al Default" ripristina il contenuto originale
  • Le variabili richieste sono visibili accanto all'editor </success_criteria>
After completion, create `.planning/phases/02-prompt-control-output-review/02-01-SUMMARY.md`