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.
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:
form.html che fa fetch a backend.phpsend_email.phpvalidation-logic.js ma l'HTML importa app.jsIl 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.
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.
// 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;
?>
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.
index.html fa fetch("api.php") ma non c'è un api.php tra i blocchi, il codice non funzionerà.
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.
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.
// 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)
});
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
});
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>
Questo sistema funziona, ma ci sono alcuni dettagli importanti da non dimenticare: