feat(21-22): agente AI generazione preventivo + pagina pubblica deck

- Migration 0010: tabella proposals (id, slug, lead_id, client_id,
  offer_macro_id, content jsonb, state, selected_tier, accepted_at)
  applicata a prod via SSH tunnel
- @anthropic-ai/sdk@0.105.0 installato; ANTHROPIC_API_KEY in .env.local
- src/lib/proposal/: schema Zod ProposalContent, agente Claude Opus 4.8,
  assemble (AI + offerta DB + config consulente), queries, profile.ts
- Admin: /admin/preventivi lista + /genera (pre-fill ?lead_id=X) + /[id] review
- Sidebar: voce Preventivi + CTA globale lime "Genera preventivo"
- LeadDetail: pulsante "Genera preventivo" → /admin/preventivi/genera?lead_id=X
- Pagina pubblica /preventivo/[slug]: deck 20+ slide light-mode iamcavalli,
  navigazione frecce + dot + keyboard, accept/reject con guard immutabilità
- STATE.md aggiornato (80%), 21-PLAN.md scritto nel formato GSD

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-20 14:37:05 +02:00
parent 86c86cd420
commit fdcc938252
41 changed files with 2818 additions and 19 deletions
+112
View File
@@ -0,0 +1,112 @@
import { z } from "zod";
// ── Schema delle sezioni generate dall'AI ──────────────────────────────────
// Le sezioni da OFFER DB (pricing tier) e CONFIG (bio/testimonianze) NON
// sono prodotte dall'AI e non fanno parte di questo schema.
export const ProblemNodeSchema = z.object({
id: z.string(), // "01" | "02" | ... | "05"
title: z.string(), // es. "La voce non è una. Sono tre."
subtitle: z.string(), // es. "LA VOCE"
body: z.string(), // paragrafo descrittivo
risk: z.string(), // testo del box "IL RISCHIO"
quote: z.string(), // citazione verbatim dal transcript
quoteAttribution: z.string(), // "NOME COGNOME · DISCOVERY YYYY-MM-DD"
});
export const SolutionNodeSchema = z.object({
id: z.string(), // specchia il problem node
title: z.string(), // es. "Tre voci coordinate, un solo brand."
subtitle: z.string(), // es. "SOLUZIONE 01 · LA VOCE"
transformation: z.string(), // testo del box "LA TRASFORMAZIONE"
throughWhat: z.array(z.string()), // bullet "ATTRAVERSO COSA" (3 voci)
});
export const DiagramNodeSchema = z.object({
id: z.string(),
label: z.string(),
});
export const ScopeSectionSchema = z.object({
scopeTitle: z.string(),
scopeBody: z.string(),
objectives: z.array(z.string()), // 35 bullet con "checkmark" visivo
});
export const DeliverablesSectionSchema = z.object({
deliverables: z.array(z.string()), // elenco output finali (tier-agnostic, macro)
outOfScope: z.array(z.string()), // ciò che NON è incluso
});
export const TimelineSectionSchema = z.object({
totalWeeks: z.number(),
phases: z.array(z.object({
label: z.string(), // es. "Audit Strategico"
startWeek: z.number(),
endWeek: z.number(),
color: z.enum(["cyan", "purple", "green"]), // palette fissa light-mode
})),
disclaimer: z.string(),
});
export const StagesRecapSchema = z.object({
stages: z.array(z.object({
number: z.string(), // "01" | "02" | "03"
tierLetter: z.string(), // "A" | "B" | "C"
label: z.string(), // es. "OPZIONE A AUDIT"
headline: z.string(), // es. "Si capisce."
body: z.string(),
deliverable: z.string(), // es. "Audit decision document"
})),
});
export const ComparisonRowSchema = z.object({
component: z.string(), // es. "Brand Architecture System"
description: z.string(), // sottotitolo
inA: z.boolean(),
inB: z.boolean(),
inC: z.boolean(),
});
// Root schema — contenuto generato dall'AI
export const ProposalContentSchema = z.object({
// Slide "dove andiamo" (02)
vision: z.object({
headline: z.string(), // es. "Tre voci. Un solo brand."
body: z.string(), // paragrafo breve
}),
// Cap.02 — 5 nodi problema estratti dai transcript
problems: z.array(ProblemNodeSchema).min(3).max(5),
// Sintesi problema (5 nodi → 1 radice)
problemSynthesis: z.object({
rootCause: z.string(), // la domanda strategica non ancora risolta
nodeLabels: z.array(z.string()).length(5), // label per il diagramma
}),
// Cap.03 — soluzioni mirror
solutions: z.array(SolutionNodeSchema).min(3).max(5),
// Sintesi soluzione (1 direzione → 5 elevazioni)
solutionSynthesis: z.object({
direction: z.string(),
elevations: z.array(DiagramNodeSchema).length(5),
}),
// Cap.04 — scope, deliverable, timeline
scope: ScopeSectionSchema,
deliverables: DeliverablesSectionSchema,
timeline: TimelineSectionSchema,
// Slide di recap (3 tappe progressive)
stagesRecap: StagesRecapSchema,
// Tabella comparativa componenti × A/B/C
comparisonMatrix: z.array(ComparisonRowSchema).min(4).max(10),
});
export type ProposalContent = z.infer<typeof ProposalContentSchema>;
export type ProblemNode = z.infer<typeof ProblemNodeSchema>;
export type SolutionNode = z.infer<typeof SolutionNodeSchema>;
export type ComparisonRow = z.infer<typeof ComparisonRowSchema>;