Come Realizzare un Chatbot Generatore di Codice

Se volessimo realizzare un chatbot capace di generare interi progetti web su richiesta dell'utente, mantenendo la coerenza tra i file e permettendo di visualizzarli in tempo reale prima di scaricarli, dovremmo affrontare una serie di sfide interessanti. Non è una task banale: il modello deve capire cosa vogliamo, generare codice multiplo, mantenere i riferimenti corretti tra file, e noi dobbiamo saper estrarre, validare e presentare il tutto in modo sicuro. Vediamo come farlo passo per passo.

1. Preparare il Modello: Il System Prompt

Tutto inizia con le istruzioni che diamo al modello. Un LLM non è uno psichiatra che legge la mente: se vogliamo che generi codice in un formato specifico, dobbiamo essere espliciti. Il problema classico è che senza indicazioni precise, il modello genererà nomi file casuali, userà riferimenti incoerenti, e il nostro codice non funzionerà.

Supponiamo che chiediamo al chatbot: "Crea un form di contatti con validazione". Se non abbiamo detto nulla al modello, potrebbe generare:

  • Un file form.html che fa fetch a backend.php
  • Ma il file PHP si chiama send_email.php
  • Oppure il JavaScript è in validation-logic.js ma l'HTML importa app.js

Il risultato è che il codice non funziona perché i percorsi non coincidono. Per evitarlo, diamo al modello un sistema di naming rigoroso. Ecco come:

// System Prompt da usare con OpenAI
const systemPrompt = `You are a professional code generator. When asked to create a project:

1. FIRST: List all files you will generate with their exact names
   Example output:
   Files to generate:
   - index.html
   - style.css
   - app.js
   - contact.php

2. THEN: Generate each file in a markdown code block with this format:
   \`\`\`language:filename
   (your code here)
   \`\`\`
   
   Example:
   \`\`\`html:index.html
   <!DOCTYPE html>
   ...
   \`\`\`
   
   \`\`\`php:contact.php
   <?php
   ...
   \`\`\`

3. CRITICAL - File Reference Consistency:
   - If index.html has <form action="contact.php">, the PHP file MUST be named contact.php
   - If app.js imports 'utils.js', generate the file as \`\`\`javascript:utils.js
   - If style.css is referenced in HTML, generate it as \`\`\`css:style.css
   - NEVER change filenames between the code and the imports

4. Generate production-ready, well-commented code
5. Use only: HTML5, CSS3, Vanilla JavaScript, PHP, JSON
6. No frameworks, no minification, readable code`;

Il vantaggio di questo approccio è che il modello decide prima quali file creerà e poi li scrive. Durante la scrittura, sa già che il form interroga contact.php, quindi non farà sciocchezze.

2. Chiamare l'API OpenAI e Mantenere Contesto

Una volta definito il system prompt, dobbiamo chiamare l'API. Qui c'è un dettaglio importante: se vogliamo che l'utente possa fare domande di follow-up type "Aggiungi un campo email" o "Cambia il colore del bottone", il chatbot deve ricordare il codice che ha generato finora. Per questo manteniamo uno conversation history.

// La storia della conversazione
let conversationHistory = [];

// Funzione per chiamare OpenAI
async function callOpenAI(userMessage) {
  // Aggiungi il messaggio dell'utente alla storia
  conversationHistory.push({
    role: "user",
    content: userMessage
  });

  try {
    // Chiama l'API con la storia completa
    const response = await fetch('https://api.openai.com/v1/chat/completions', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${YOUR_OPENAI_KEY}` // ⚠️ Usa un backend proxy, non mettere qui!
      },
      body: JSON.stringify({
        model: 'gpt-4o-mini',  // O 'gpt-4.1-nano' o 'gpt-5-nano' se si vuole un modello reasoning
        max_tokens: 16000,  // Rinominare a max_completion_tokens per modelli gpt-5*
        temperature: 0.5,   / Creativo ma stabile - eliminare il parametro per modelli gpt-5*
        messages: [
          // Il system prompt rimane sempre
          { 
            role: "system", 
            content: systemPrompt 
          },
          // Seguito dalla storia della conversazione
          ...conversationHistory
        ]
      })
    });

    if (!response.ok) {
      const error = await response.json();
      throw new Error(`API Error: ${error.error.message}`);
    }

    const data = await response.json();
    const assistantResponse = data.choices[0].message.content;

    // Salva la risposta nella storia per i prossimi turni
    conversationHistory.push({
      role: "assistant",
      content: assistantResponse
    });

    // Ritorna sia il messaggio che la storia aggiornata
    return {
      message: assistantResponse,
      history: conversationHistory
    };

  } catch (error) {
    console.error('Errore nella chiamata OpenAI:', error);
    throw error;
  }
}

Nota: Non mettere mai l'API key direttamente nel frontend JavaScript. Se lo fai, chiunque guarda il source vede la tua chiave e può fare richieste al tuo conto. La soluzione è creare un backend proxy PHP che fa da intermediario.

⚠️ Sicurezza: Se questa è un'applicazione reale, implementa un proxy backend:
// proxy.php (backend)
<?php
$userMessage = $_POST['message'] ?? '';

$response = file_get_contents('https://api.openai.com/v1/chat/completions', false, 
  stream_context_create([
    'http' => [
      'method' => 'POST',
      'header' => 'Authorization: Bearer ' . getenv('OPENAI_KEY'),
      'content' => json_encode([...])
    ]
  ])
);

echo $response;
?>

3. Estrarre i Blocchi di Codice dalla Risposta

Bene, l'API ha risposto. Contiene testo descrittivo, liste di file, e blocchi di codice markdown. Il nostro lavoro è estrarre solo i blocchi di codice e associarli ai nomi file.

La risposta avrà questo aspetto:

Files to generate:
- index.html
- style.css
- app.js
- contact.php

Here's your contact form application:

\`\`\`html:index.html
<!DOCTYPE html>
<html>
<head>
  <title>Contact Form</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <form id="contactForm">
    <input type="email" id="email" required>
    <button type="submit">Send</button>
  </form>
  <script src="app.js"></script>
</body>
</html>
\`\`\`

\`\`\`css:style.css
body { font-family: sans-serif; }
\`\`\`

\`\`\`javascript:app.js
document.getElementById('contactForm').addEventListener('submit', (e) => {
  e.preventDefault();
  fetch('contact.php', {
    method: 'POST',
    body: new FormData(e.target)
  });
});
\`\`\`

\`\`\`php:contact.php
<?php
mail($_POST['email'], 'Test', 'Works!');
?>
\`\`\`

Dobbiamo estrarre i quattro blocchi e associare a ognuno il nome file. Ecco la funzione:

// Estrae i blocchi di codice dalla risposta del modello
function extractCodeBlocks(responseText) {
  // Regex: trova tutto ciò che sta tra ``` e ```
  // [\s\S]*? = qualsiasi carattere (spazi, newline, tutto) in modo non-greedy
  // g = flag globale (trova TUTTI i blocchi, non solo il primo)
  const blockRegex = /```[\s\S]*?```/g;
  const blocks = responseText.match(blockRegex) || [];

  // Mappa i linguaggi alle estensioni (è una commodity, facoltativa)
  const languageMap = {
    javascript: 'js', js: 'js',
    html: 'html', css: 'css',
    php: 'php', json: 'json',
    xml: 'xml', python: 'py'
  };

  // Processa ogni blocco
  return blocks
    .map((block, index) => {
      // Estrai il nome file dal formato ```language:filename
      // Es: ```html:index.html
      const headerRegex = /^```([a-zA-Z0-9_-]+):\s*([^\n]+)/;
      const match = block.match(headerRegex);

      // Se il formato non è corretto, salta il blocco
      if (!match) {
        console.warn(`Blocco ${index} malformato, skippato`);
        return null;
      }

      const language = match[1].toLowerCase();  // es: 'html'
      const filename = match[2].trim();         // es: 'index.html'
      const extension = languageMap[language] || language;

      // Estrai il codice pulito rimuovendo i backtick e l'intestazione
      const code = block
        .replace(/^```[a-zA-Z0-9_-]+:\s*[^\n]+\n?/, '')  // Rimuove ```html:index.html\n
        .replace(/```$/, '')                               // Rimuove ``` finale
        .trim();                                           // Pulizia spazi

      return {
        filename,      // es: 'index.html'
        code,          // il codice vero
        language,      // es: 'html'
        extension      // es: 'html'
      };
    })
    .filter(Boolean); // Rimuove i null (blocchi malformati)
}

// Uso:
const userMessage = "Crea un form di contatti";
const { message: aiResponse } = await callOpenAI(userMessage);
const codeBlocks = extractCodeBlocks(aiResponse);

console.log('File estratti:', codeBlocks.map(b => b.filename));
// Output: [ 'index.html', 'style.css', 'app.js', 'contact.php' ]

La funzione ritorna un array di oggetti, ognuno con il nome file e il codice. Se il modello ha generato il formato corretto (come gli abbiamo detto di fare nel system prompt), tutto funziona perfetto.

Tip: Aggiungi una validazione che i file referenziati nel codice esistono effettivamente nei blocchi estratti. Se index.html fa fetch("api.php") ma non c'è un api.php tra i blocchi, il codice non funzionerà.

4. Sandboxing in un Iframe per Preview Sicuro

Adesso abbiamo i blocchi estratti. Potremmo metterli direttamente nel DOM, ma è pericoloso: se il modello ha generato JavaScript malevolo (o anche solo buggato), potrebbe mandare in crash la pagina o fare cose strane. La soluzione è usare un iframe sandbox.

Un iframe sandbox è una "gabbia" isolata dove il codice può girare senza accedere al contesto esterno. Possiamo decidere esattamente quali permessi dargli.

// Crea un iframe sandbox con il codice generato
function createSandboxPreview(codeBlocks) {
  // Raccogli i file per tipo
  let htmlCode = '';
  let cssCode = '';
  let jsCode = '';
  let phpFiles = []; // Per riferimenti

  codeBlocks.forEach(block => {
    if (block.filename.endsWith('.html')) htmlCode = block.code;
    else if (block.filename.endsWith('.css')) cssCode = block.code;
    else if (block.filename.endsWith('.js')) jsCode = block.code;
    else if (block.filename.endsWith('.php')) phpFiles.push(block);
  });

  // Assembla un documento HTML completo
  // CSS viene inline nello <style>
  // JS viene inline nello <script>
  // Così rimane tutto dentro l'iframe
  const sandboxDocument = `
    <!DOCTYPE html>
    <html>
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <style>
        /* Stile di default */
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: system-ui; padding: 1rem; }
        
        /* CSS generato dal modello */
        ${cssCode}
      </style>
    </head>
    <body>
      <!-- HTML generato dal modello -->
      ${htmlCode}
      
      <script>
        // Intercepta i fetch verso PHP
        // Se il codice fa fetch('contact.php'), 
        // lo redirige verso il nostro backend reale
        const originalFetch = window.fetch;
        window.fetch = function(url, options) {
          // Se è una richiesta a un file PHP, la invia al server vero
          if (typeof url === 'string' && url.endsWith('.php')) {
            // Puoi aggiungere un prefisso come /api/
            return originalFetch('/api' + url, options);
          }
          // Altrimenti, comportamento normale
          return originalFetch.apply(this, arguments);
        };
        
        // JavaScript generato dal modello
        ${jsCode}
      </script>
    </body>
    </html>
  `;

  // Crea l'elemento iframe
  const iframe = document.createElement('iframe');
  iframe.style.width = '100%';
  iframe.style.height = '600px';
  iframe.style.border = '1px solid #ddd';
  iframe.style.borderRadius = '6px';
  iframe.style.boxShadow = '0 2px 4px rgba(0,0,0,0.1)';
  
  // Sandbox: permetti solo ciò che serve
  iframe.setAttribute('sandbox', 
    'allow-scripts ' +        // JavaScript
    'allow-forms '   +        // Submit form
    'allow-same-origin'       // Accesso fetch al dominio stesso
  );
  // Non permettere: allow-top-navigation, allow-pointer-lock, ecc.
  
  // Inietta il documento nell'iframe
  iframe.srcdoc = sandboxDocument;
  
  return iframe;
}

// Uso:
const preview = createSandboxPreview(codeBlocks);
document.getElementById('previewContainer').appendChild(preview);

In questo modo il codice generato gira in isolamento. Se ha errori JavaScript, la pagina principale non si rompe. Se prova a fare window.top.location = 'phishing.com', l'iframe blocca l'operazione.

Nota su PHP: Un iframe non può eseguire PHP direttamente (PHP gira sul server). Se l'utente fa fetch verso un endpoint PHP, l'iframe fa la richiesta al dominio principale, che ha un backend PHP reale. Assicurati che il backend sia pronto a servire gli endpoint.

5. Salvare i File Singoli o in Zip

La preview è utile per vedere se il codice funziona, ma alla fine l'utente vuole scaricare i file. Abbiamo due opzioni: scaricare ogni file singolo, oppure creareun archivio ZIP con tutto il progetto.

Opzione A: Download Singoli File

// Scarica un singolo file
function downloadFile(filename, content) {
  // Crea un blob (binary large object) dal contenuto
  const blob = new Blob([content], { type: 'text/plain' });
  
  // Genera un URL temporaneo per il blob
  const url = URL.createObjectURL(blob);
  
  // Crea un link <a> invisibile
  const link = document.createElement('a');
  link.href = url;
  link.download = filename; // Questo dice al browser come chiamare il file
  
  // Aggiungi al DOM (necessario per Firefox)
  document.body.appendChild(link);
  
  // Simula il click
  link.click();
  
  // Pulisci
  document.body.removeChild(link);
  URL.revokeObjectURL(url); // Libera la memoria
}

// Scarica tutti i file
function downloadAllFiles(codeBlocks) {
  codeBlocks.forEach(block => {
    downloadFile(block.filename, block.code);
  });
}

// Uso:
const dlBtn = document.getElementById('downloadAllBtn');
dlBtn.addEventListener('click', () => {
  downloadAllFiles(codeBlocks);
  // Il browser scaricherà: index.html, style.css, app.js, contact.php
  // (uno dopo l'altro, oppure in parallelo a seconda del browser)
});

Opzione B: Download come ZIP (Consigliato)

Se hai molti file, è meglio fare un ZIP. Serve la libreria JSZip.

// In HTML, includi: <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>

async function downloadAsZip(codeBlocks, projectName) {
  // Crea un nuovo archivio ZIP
  const zip = new JSZip();
  
  // Aggiungi ogni file al ZIP
  codeBlocks.forEach(block => {
    zip.file(block.filename, block.code);
  });
  
  // Genera il blob dell'archivio (operazione asincrona)
  const zipBlob = await zip.generateAsync({ type: 'blob' });
  
  // Scarica il ZIP come un file normale
  const url = URL.createObjectURL(zipBlob);
  const link = document.createElement('a');
  link.href = url;
  link.download = `${projectName || 'project'}.zip`;
  document.body.appendChild(link);
  link.click();
  document.body.removeChild(link);
  URL.revokeObjectURL(url);
}

// Uso:
document.getElementById('downloadZipBtn').addEventListener('click', () => {
  downloadAsZip(codeBlocks, 'my-contact-form');
  // Scarica: my-contact-form.zip
});

6. Orchestrazione Completa: Mettere Tutto Insieme

Finora abbiamo visto i pezzi singoli. Ora li assembliamo in un'applicazione funzionante. L'idea è: l'utente digita una richiesta, il chatbot genera il codice, lo visualizziamo in preview, e diamo l'opzione di scaricare.

// Stato globale dell'applicazione
let conversationHistory = [];
let currentCodeBlocks = [];

// Elemento UI
const chatInput = document.getElementById('userInput');
const sendBtn = document.getElementById('sendBtn');
const previewContainer = document.getElementById('previewContainer');
const downloadBtn = document.getElementById('downloadZipBtn');

// Event listener: quando l'utente invia un messaggio
sendBtn.addEventListener('click', async () => {
  const userMessage = chatInput.value.trim();
  
  if (!userMessage) return;
  
  // Disabilita il bottone mentre elabora
  sendBtn.disabled = true;
  sendBtn.textContent = 'Generating...';
  
  try {
    // Chiama l'API OpenAI
    const { message: aiResponse } = await callOpenAI(userMessage);
    
    // Estrai i blocchi di codice dalla risposta
    const blocks = extractCodeBlocks(aiResponse);
    
    // Validazione basica
    if (blocks.length === 0) {
      alert('Non ho trovato blocchi di codice nella risposta. Riprova.');
      return;
    }
    
    // Salva i blocchi nello stato globale
    currentCodeBlocks = blocks;
    
    // Crea e mostra il preview
    const sandbox = createSandboxPreview(blocks);
    previewContainer.innerHTML = '';
    previewContainer.appendChild(sandbox);
    
    // Mostra la lista dei file generati (per debug/confirmazione)
    const fileList = document.createElement('div');
    fileList.style.marginTop = '1rem';
    fileList.style.color = '#666';
    fileList.innerHTML = '<strong>File generati:</strong> ' + 
                        blocks.map(b => b.filename).join(', ');
    previewContainer.appendChild(fileList);
    
    // Abilita il download
    downloadBtn.disabled = false;
    
    // Pulisci l'input
    chatInput.value = '';
    
  } catch (error) {
    alert('Errore: ' + error.message);
  } finally {
    // Riabilita il bottone
    sendBtn.disabled = false;
    sendBtn.textContent = 'Generate';
  }
});

// Event listener: download
downloadBtn.addEventListener('click', async () => {
  if (currentCodeBlocks.length === 0) {
    alert('Nessun codice da scaricare');
    return;
  }
  
  downloadBtn.disabled = true;
  downloadBtn.textContent = 'Downloading...';
  
  try {
    await downloadAsZip(currentCodeBlocks, 'generated-project');
  } catch (error) {
    alert('Errore nel download: ' + error.message);
  } finally {
    downloadBtn.disabled = false;
    downloadBtn.textContent = 'Download ZIP';
  }
});

E l'HTML minimo per supportare tutto:

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Code Generator Chatbot</title>
  <!-- JSZip per il download ZIP -->
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
</head>
<body>
  <h1>🤖 Code Generator</h1>
  
  <div style="margin-bottom: 1rem;">
    <input type="text" id="userInput" placeholder="Descrivi il progetto che vuoi..." style="width: 80%; padding: 0.5rem;">
    <button id="sendBtn">Generate</button>
  </div>
  
  <div id="previewContainer"></div>
  
  <button id="downloadZipBtn" disabled>Download ZIP</button>
  
  <script>
    // Qui incolli tutto il codice delle funzioni sopra
    // callOpenAI(), extractCodeBlocks(), createSandboxPreview(), downloadAsZip()
  </script>
</body>
</html>

7. Considerazioni Finali

Questo sistema funziona, ma ci sono alcuni dettagli importanti da non dimenticare:

  • Backend Proxy: Non mettere l'API key nel frontend. Usa un backend (PHP, Node, Python) che fa da intermediario.
  • Rate Limiting: Se il sito è pubblico, l'API di OpenAI costa denaro. Aggiungi rate limiting per evitare abusi.
  • Validazione File: Prima di scaricare, verifica che i percorsi nei file siano coerenti (niente dead links interni).
  • Error Handling: L'API può fallire, il modello può generare codice malformato, il sandbox può avere problemi. Gestisci sempre gli errori.
  • Cache della Conversazione: Se il progetto è grande, la history della conversazione cresce e ogni richiesta diventa più costosa. Implementa un sistema di "reset" periodico.
  • Timeout: Le richieste all'API possono essere lente. Aggiungi un timeout (es. 30 secondi) per evitare che l'utente aspetti per sempre.
Cosa fare dopo: Un'evoluzione naturale è permettere che l'utente carichi il progetto generato su un server, o che faccia refactoring progressivo ("Aggiungi autenticazione", "Crea un dashboard", ecc.). Con la history della conversazione, il modello può aggiornare il codice mantenendo il contesto.