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:
@@ -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()), // 3–5 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>;
|
||||
Reference in New Issue
Block a user