feat: add Opus/Sonnet model selector to "Genera preventivo" page

Adds a "Modello AI" toggle (Opus default, Sonnet option) styled like the
existing Soggetto toggle. The server action validates the keyword and maps
it to the real model id; the agent receives the resolved id and uses it in
the Anthropic messages.create call. The proposals DB record stores the
actual model used.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-25 15:34:04 +02:00
parent e10d1f70cb
commit 0b8a7b3809
3 changed files with 49 additions and 5 deletions
+14 -3
View File
@@ -62,7 +62,17 @@ export async function generateProposalDraft(formData: FormData) {
.orderBy(desc(clientTranscripts.call_date)); .orderBy(desc(clientTranscripts.call_date));
} }
const MODEL = "claude-opus-4-8"; const MODEL_IDS = {
opus: "claude-opus-4-8",
sonnet: "claude-sonnet-4-6",
} as const;
const modelKey = (() => {
const raw = formData.get("model");
if (raw === "opus" || raw === "sonnet") return raw;
return "opus" as const;
})();
const resolvedModel = MODEL_IDS[modelKey];
// Chiama agente AI // Chiama agente AI
const { content: aiContent } = await generateProposalContent({ const { content: aiContent } = await generateProposalContent({
@@ -70,6 +80,7 @@ export async function generateProposalDraft(formData: FormData) {
client, client,
transcripts, transcripts,
offer, offer,
model: resolvedModel,
}); });
// Assembla contenuto completo // Assembla contenuto completo
@@ -78,7 +89,7 @@ export async function generateProposalDraft(formData: FormData) {
client, client,
offer, offer,
aiContent, aiContent,
model: MODEL, model: resolvedModel,
}); });
// Salva nel DB // Salva nel DB
@@ -94,7 +105,7 @@ export async function generateProposalDraft(formData: FormData) {
offer_macro_id: offerMacroId, offer_macro_id: offerMacroId,
title, title,
content: assembled as unknown as Record<string, unknown>, content: assembled as unknown as Record<string, unknown>,
model: MODEL, model: resolvedModel,
state: "draft", state: "draft",
}); });
@@ -16,6 +16,7 @@ export function GeneraProposalForm({ leads, clients, offers, action, preselected
const [subjectType, setSubjectType] = useState<"lead" | "client">( const [subjectType, setSubjectType] = useState<"lead" | "client">(
preselectedLeadId ? "lead" : "lead" preselectedLeadId ? "lead" : "lead"
); );
const [model, setModel] = useState<"opus" | "sonnet">("opus");
const [isPending, startTransition] = useTransition(); const [isPending, startTransition] = useTransition();
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@@ -25,6 +26,8 @@ export function GeneraProposalForm({ leads, clients, offers, action, preselected
const form = e.currentTarget; const form = e.currentTarget;
const formData = new FormData(form); const formData = new FormData(form);
formData.append("model", model);
startTransition(async () => { startTransition(async () => {
try { try {
await action(formData); await action(formData);
@@ -65,6 +68,35 @@ export function GeneraProposalForm({ leads, clients, offers, action, preselected
</div> </div>
</div> </div>
{/* Modello AI */}
<div className="space-y-2">
<label className="text-sm font-medium text-foreground">Modello AI</label>
<div className="flex gap-3">
<button
type="button"
onClick={() => setModel("opus")}
className={`px-4 py-2 rounded-md text-sm border transition-colors ${
model === "opus"
? "bg-primary text-primary-foreground border-primary"
: "border-border text-muted-foreground hover:border-primary/50"
}`}
>
Opus
</button>
<button
type="button"
onClick={() => setModel("sonnet")}
className={`px-4 py-2 rounded-md text-sm border transition-colors ${
model === "sonnet"
? "bg-primary text-primary-foreground border-primary"
: "border-border text-muted-foreground hover:border-primary/50"
}`}
>
Sonnet
</button>
</div>
</div>
{/* Select lead o cliente */} {/* Select lead o cliente */}
{subjectType === "lead" ? ( {subjectType === "lead" ? (
<div className="space-y-1"> <div className="space-y-1">
@@ -135,7 +167,7 @@ export function GeneraProposalForm({ leads, clients, offers, action, preselected
{/* Info generazione */} {/* Info generazione */}
<div className="text-xs text-muted-foreground bg-muted rounded-md px-3 py-2"> <div className="text-xs text-muted-foreground bg-muted rounded-md px-3 py-2">
L&apos;AI (Claude Opus) legge i transcript del soggetto selezionato e genera una bozza L&apos;AI (Claude {model === "opus" ? "Opus" : "Sonnet"}) legge i transcript del soggetto selezionato e genera una bozza
personalizzata. La generazione richiede 3060 secondi. personalizzata. La generazione richiede 3060 secondi.
</div> </div>
+2 -1
View File
@@ -11,6 +11,7 @@ type AgentInput = {
client?: Pick<Client, "id" | "name" | "brand_name" | "brief">; client?: Pick<Client, "id" | "name" | "brand_name" | "brief">;
transcripts: Pick<ClientTranscript, "title" | "content" | "call_date">[]; transcripts: Pick<ClientTranscript, "title" | "content" | "call_date">[];
offer: OfferEditorData; offer: OfferEditorData;
model?: string;
}; };
function buildSystemPrompt(): string { function buildSystemPrompt(): string {
@@ -128,7 +129,7 @@ export async function generateProposalContent(
const client = new Anthropic({ apiKey }); const client = new Anthropic({ apiKey });
const message = await client.messages.create({ const message = await client.messages.create({
model: MODEL, model: input.model ?? MODEL,
max_tokens: 8192, max_tokens: 8192,
system: buildSystemPrompt(), system: buildSystemPrompt(),
messages: [{ role: "user", content: buildUserPrompt(input) }], messages: [{ role: "user", content: buildUserPrompt(input) }],