Il contesto

PanelControl è il gestionale interno usato dal team commerciale per tracciare ordini, lead, attivazioni e bonus mensili. Tutti i dati vivono su Firebase Realtime Database. Il team fa quotidianamente domande ripetitive: chi ha venduto di più questo mese? Quante attivazioni mancano per raggiungere la soglia bonus? Come funziona la procedura X?

L'idea è stata aggiungere un pulsante ✦ Chiedi all'AI che apra un pannello di conversazione, stesso stile glassmorphism già presente nel gestionale, e che risponda con piena consapevolezza del contesto aziendale e dei dati live del mese corrente.

Il punto tecnico centrale: un modello AI non sa nulla del tuo gestionale. Devi tu costruire il contesto e passarglielo ad ogni domanda nel system prompt. Questo articolo documenta come è stato fatto, compreso il processo di scelta dell'API e i problemi di versioning dei modelli Gemini.

Gemini API vs Anthropic API: la scelta

La prima valutazione è stata quale API usare. Le opzioni principali erano due: Google Gemini API (tramite generativelanguage.googleapis.com) e Anthropic API (tramite api.anthropic.com).

Gemini API (Google) Anthropic API
Free tier 1.500 req/giorno su Gemini Flash $5 di credito iniziale
Fatturazione Account Google Cloud richiesto (carta non addebitata nel free tier) Separata dall'abbonamento claude.ai
Pattern chiamata fetch su endpoint REST fetch su endpoint REST
CORS in Netlify Nessun blocco (dominio pubblico) Già in whitelist Netlify

La scelta è ricaduta su Gemini per il free tier più generoso per un uso interno leggero (poche decine di domande al giorno). Un punto importante: Google Cloud richiede un account di fatturazione anche per usare il piano gratuito, ma inserire la carta non comporta addebiti finché si resta nel free tier.

La chiamata fetch: nessuna libreria necessaria

La Gemini API si chiama con una semplice fetch POST. Il corpo della richiesta contiene il prompt nel campo contents. La risposta torna in candidates[0].content.parts[0].text.

JavaScript — chiamata base a Gemini API
async function askGemini(userMessage, systemPrompt) {
  const GEMINI_KEY = '[GEMINI_API_KEY]';
  const MODEL     = 'gemini-2.5-flash-lite';
  const ENDPOINT  = `https://generativelanguage.googleapis.com/v1beta/models/${MODEL}:generateContent?key=${GEMINI_KEY}`;

  const response = await fetch(ENDPOINT, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      system_instruction: { parts: [{ text: systemPrompt }] },
      contents: [{ parts: [{ text: userMessage }] }]
    })
  });

  const data = await response.json();

  // Gestione errore modello non disponibile
  if (!response.ok) {
    const msg = data?.error?.message || 'Errore API';
    throw new Error(msg);
  }

  return data.candidates[0].content.parts[0].text;
}
⚠️

Non mettere mai la API key direttamente nel frontend di un'app pubblica — chiunque apra il sorgente la vedrebbe. Per un gestionale interno ad accesso limitato è un tradeoff accettabile. In alternativa: salvarla come variabile d'ambiente su Netlify e fare le chiamate tramite una Netlify Function proxy.

Il problema del versioning modelli Gemini

Il primo ostacolo non è stato tecnico, ma di disponibilità. Gemini depreca i modelli rapidamente per i nuovi account. La sequenza di errori incontrata:

  • gemini-2.0-flash"no longer available to new users"
  • gemini-2.0-flash-lite → stesso errore
  • gemini-2.5-flash-lite → funzionante, free tier attivo

La lezione: non fidarsi del nome del modello hardcodato in un tutorial. Prima di integrare, verificare sempre su ai.google.dev/gemini-api/docs/models quali modelli sono disponibili per il proprio account e piano. Anche il formato della API key conta: le chiavi Gemini iniziano sempre con AIzaSy, non con altri prefissi.

Il pulsante UI e il pannello glassmorphism

Il pulsante ✦ Chiedi all'AI è posizionato sopra il pulsante chat già esistente nel gestionale, con gradiente verde/teal per distinguersi visivamente dal blu della chat. Al click apre un pannello laterale con la stessa estetica glassmorphism del resto dell'interfaccia.

Il pannello contiene: header con icona e identificativo del provider AI, area messaggi con typing indicator animato, suggerimenti rapidi cliccabili al primo avvio, e un campo input con auto-resize e invio con Enter.

HTML — struttura pannello AI
<!-- Pulsante trigger -->
<button id="aiBtn" class="fab-btn ai-fab">
  <span class="fab-label">✦ Chiedi all'AI</span>
  <span class="fab-icon">✦</span>
</button>

<!-- Pannello conversazione -->
<div id="aiPanel" class="ai-panel hidden">
  <div class="ai-panel-header">
    <span>✦ Assistente AI · [PROVIDER]</span>
    <button onclick="closeAiPanel()">✕</button>
  </div>
  <div id="aiMessages" class="ai-messages"></div>
  <div class="ai-input-row">
    <textarea id="aiInput" rows="1" placeholder="Scrivi una domanda..."></textarea>
    <button onclick="sendAiMessage()">↑</button>
  </div>
</div>

Il system prompt dinamico: dati live da Firebase

Il punto più interessante dell'integrazione è la costruzione del system prompt. Ad ogni domanda, prima di inviare la richiesta all'API, viene assemblato un blocco di testo che serializza lo stato corrente del gestionale: utente connesso, mese corrente, classifica operatori con ordini per categoria, totale lead e attivazioni, soglie bonus.

Tutti questi dati sono già in memoria nel gestionale perché Firebase li carica all'avvio tramite listener onValue. Il system prompt li legge dallo stato JavaScript esistente — nessuna chiamata aggiuntiva al database.

JavaScript — costruzione del system prompt dinamico
function buildSystemPrompt() {
  const mese   = getCurrentMonthLabel();   // es. "Giugno 2026"
  const utente = sessionStorage.getItem('panelUser') || 'Sconosciuto';

  // Classifica operatori con totali per categoria
  const rankingText = Object.entries(state.operatori)
    .map(([nome, dati]) =>
      `${nome}: ${dati.totale} ordini (Cat-A: ${dati.catA}, Cat-B: ${dati.catB})`
    ).join('\n');

  return `Sei un assistente interno per il team commerciale di [AZIENDA].
Rispondi sempre in italiano, in modo diretto e professionale.

=== CONTESTO ATTUALE ===
Utente connesso: ${utente}
Mese di riferimento: ${mese}
Sezione aperta: ${state.sezioneAttiva}

=== OPERATORI E RISULTATI ===
${rankingText}

=== KPI MENSILI ===
Lead totali: ${state.leadTotali}
Attivazioni: ${state.attivazioni}
Annullati: ${state.annullati}
Soglia bonus: ${state.sogliaBonus} attivazioni

=== CONOSCENZA AZIENDALE ===
[Qui va la conoscenza statica — vedi sezione successiva]
`;
}
💡

La chiave è che il modello riceve ogni volta un system prompt fresco con lo snapshot aggiornato dei dati. Non c'è memoria tra una sessione e l'altra, ma all'interno della conversazione i messaggi precedenti vengono accodati nell'array contents per mantenere il filo del dialogo.

Conoscenza statica: il "manuale aziendale" nel prompt

Oltre ai dati live, il system prompt include un blocco di conoscenza statica che non cambia: prodotti e tariffe commercializzati dall'azienda, procedure operative (come confermare un'email a un cliente, come registrare un'attivazione), ruoli del team, glossario dei termini interni.

Questo blocco è scritto direttamente come stringa JavaScript nel codice del gestionale. Non viene letto da Firebase — è parte del sorgente. Il vantaggio: nessuna latenza aggiuntiva. Lo svantaggio: aggiornarlo richiede un deploy.

Il bilanciamento giusto è mettere nel blocco statico le cose che cambiano raramente (struttura prodotti, procedure core, glossario) e lasciare ai dati Firebase tutto quello che cambia quotidianamente (chi ha venduto cosa, lead del mese, target raggiunto).

JavaScript — frammento di conoscenza statica nel prompt
const STATIC_KNOWLEDGE = `
=== PRODOTTI ===
[PRODOTTO_A]: terminale POS portatile. Piani disponibili: [PIANO_BASE] (€0/mese, commissione 1,20%),
  [PIANO_PRO] (€12/mese, commissione 0,95%), [PIANO_CUSTOM] (negoziato).
[PRODOTTO_B]: terminale fisso per banco. Disponibile solo a noleggio.
[PRODOTTO_C]: conto business + carta prepagata. Piani: Freemium (gratuito),
  Smart (€9/mese), Business (€25/mese).

=== PROCEDURE ===
Conferma email cliente:
  1. Aprire il portale [PORTALE_INTERNO]
  2. Cercare il cliente per codice fiscale o ragione sociale
  3. Inviare il link di verifica dalla sezione "Comunicazioni"
  4. Attendere la conferma (solitamente entro 24h)

Registrazione attivazione:
  1. Inserire il numero pratica nel gestionale
  2. Verificare che lo stato sia "Attivo" sul portale provider
  3. Aggiornare il record nel database con data attivazione

=== GLOSSARIO ===
Lead: contatto commerciale acquisito, non ancora convertito
Attivazione: contratto firmato e servizio attivo sul portale
Annullato: pratica ritirata dal cliente o rifiutata dal provider
[TERMINE_INTERNO_1]: sistema di firma contratti digitale
[TERMINE_INTERNO_2]: piattaforma CRM per la gestione lead
`;

Conversazione multi-turn: accodare i messaggi

Gemini API non ha memoria propria. Per simulare una conversazione con più scambi, ogni chiamata successiva deve includere nell'array contents tutti i messaggi precedenti — alternando role: "user" e role: "model".

JavaScript — conversazione multi-turn
let conversationHistory = []; // resettata all'apertura del pannello

async function sendAiMessage() {
  const userText = document.getElementById('aiInput').value.trim();
  if (!userText) return;

  // Aggiunge il turno utente alla storia
  conversationHistory.push({
    role: 'user',
    parts: [{ text: userText }]
  });

  const response = await fetch(ENDPOINT, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      system_instruction: { parts: [{ text: buildSystemPrompt() }] },
      contents: conversationHistory  // intera storia ad ogni chiamata
    })
  });

  const data   = await response.json();
  const aiText = data.candidates[0].content.parts[0].text;

  // Aggiunge la risposta del modello alla storia
  conversationHistory.push({
    role: 'model',
    parts: [{ text: aiText }]
  });

  renderMessage('ai', aiText);
}
💡

Attenzione alla dimensione del contesto: con conversazioni molto lunghe, l'array contents cresce e ogni chiamata diventa più pesante. Per un uso interno con domande brevi non è un problema, ma è buona pratica resettare la storia al riavvio del pannello o ogni N turni.

Cosa abbiamo imparato

  1. Il system prompt è il prodotto. La qualità delle risposte dipende quasi interamente da quanto è ben strutturato il contesto che passi al modello. L'AI non è magica — risponde con quello che le dai.
  2. I modelli Gemini si deprecano rapidamente. Non hardcodare il nome del modello senza verificare la disponibilità corrente. Tenerlo in una costante facile da aggiornare e documentare quale modello si usa e perché.
  3. Dati live + conoscenza statica = il giusto mix. Separare quello che cambia ogni giorno (dati Firebase) da quello che cambia raramente (procedure, glossario) rende il prompt mantenibile nel tempo.
  4. Per app interne, la sicurezza della API key è un tradeoff. La soluzione ideale è una Netlify Function proxy che nasconde la chiave dal frontend. Per un gestionale con accesso limitato e un budget mensile impostato sulla console del provider, il rischio è accettabile.
  5. Nessuna libreria necessaria. Una fetch, un JSON e un po' di DOM sono tutto quello che serve per integrare un modello AI in un'applicazione vanilla JavaScript esistente.