Add Gemini Chat: conversational AI assistant for kitchen help

- Gemini star icon button next to camera in header
- Full chat page with message bubbles, typing indicator
- Conversation history persisted in localStorage (last 50 messages)
- System context includes: full inventory with expiry dates, appliances, dietary restrictions
- Multi-turn conversation with Gemini 2.0 Flash
- Pre-built suggestion chips: snack, juice/smoothie, light meal, use expiring items
- Clear chat button for fresh conversations
- Indigo/purple themed UI matching Gemini branding
- PHP gemini_chat API endpoint with inventory context injection
This commit is contained in:
dadaloop82
2026-03-11 15:26:19 +00:00
parent ff1f27fe8d
commit af3b5941a0
5 changed files with 618 additions and 3 deletions
+165
View File
@@ -95,6 +95,10 @@ try {
geminiIdentifyProduct();
break;
case 'gemini_chat':
geminiChat($db);
break;
// ===== BRING! SHOPPING LIST =====
case 'bring_list':
bringGetList();
@@ -807,6 +811,167 @@ function geminiReadExpiry(): void {
]);
}
// ===== GEMINI CHAT =====
function geminiChat(PDO $db): void {
// Load API key
$envFile = __DIR__ . '/../.env';
$apiKey = '';
if (file_exists($envFile)) {
$lines = file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
if (strpos($line, '#') === 0) continue;
if (strpos($line, '=') !== false) {
list($key, $val) = explode('=', $line, 2);
if (trim($key) === 'GEMINI_API_KEY') {
$apiKey = trim($val);
}
}
}
}
if (empty($apiKey)) {
echo json_encode(['success' => false, 'error' => 'no_api_key']);
return;
}
$input = json_decode(file_get_contents('php://input'), true);
$message = $input['message'] ?? '';
$history = $input['history'] ?? [];
$appliances = $input['appliances'] ?? [];
$dietaryRestrictions = $input['dietary_restrictions'] ?? '';
if (empty($message)) {
echo json_encode(['success' => false, 'error' => 'Messaggio vuoto']);
return;
}
// Fetch inventory context
$stmt = $db->query("
SELECT p.name, p.brand, p.category, i.quantity, p.unit, i.location, i.expiry_date,
CASE WHEN i.expiry_date IS NOT NULL THEN julianday(i.expiry_date) - julianday('now') ELSE 999 END AS days_left
FROM inventory i
JOIN products p ON p.id = i.product_id
WHERE i.quantity > 0
ORDER BY days_left ASC
");
$items = $stmt->fetchAll(PDO::FETCH_ASSOC);
$ingredientLines = [];
foreach ($items as $item) {
$line = "- {$item['name']}";
if ($item['brand']) $line .= " ({$item['brand']})";
$line .= ": {$item['quantity']} {$item['unit']}";
if ($item['expiry_date']) {
$daysLeft = intval($item['days_left']);
if ($daysLeft < 0) {
$line .= " [SCADUTO da " . abs($daysLeft) . " giorni]";
} elseif ($daysLeft <= 3) {
$line .= " [SCADE TRA $daysLeft GIORNI]";
} elseif ($daysLeft <= 7) {
$line .= " [scade tra $daysLeft giorni]";
}
}
$line .= " (in {$item['location']})";
$ingredientLines[] = $line;
}
$ingredientsText = implode("\n", $ingredientLines);
$appliancesText = '';
if (!empty($appliances)) {
$appliancesText = "\nElettodomestici disponibili: " . implode(', ', $appliances) . " (più fornelli e forno sempre disponibili).";
}
$dietaryText = '';
if (!empty($dietaryRestrictions)) {
$dietaryText = "\nRestrizioni alimentari dell'utente: {$dietaryRestrictions}. Rispetta SEMPRE queste restrizioni.";
}
$systemPrompt = <<<PROMPT
Sei un assistente cucina italiano esperto, amichevole e conciso. L'utente ha una dispensa e ti chiede consigli su cosa preparare.
CONTESTO - INGREDIENTI DISPONIBILI IN DISPENSA:
{$ingredientsText}
{$appliancesText}{$dietaryText}
REGOLE:
1. Rispondi SEMPRE in italiano, in modo colloquiale e amichevole
2. Usa SOLO gli ingredienti dalla dispensa dell'utente (più acqua, sale, pepe, olio che si presumono sempre disponibili)
3. Dai priorità agli ingredienti in scadenza
4. Sii conciso: non fare liste chilometriche, vai al sodo
5. Se l'utente chiede una ricetta o preparazione, dai istruzioni chiare con quantità
6. Se non ci sono ingredienti adatti per la richiesta, dillo onestamente e suggerisci alternative
7. Puoi suggerire combinazioni creative
8. Quando menzioni quantità, usa le stesse unità di misura della dispensa
9. Ricorda il contesto della conversazione precedente
PROMPT;
// Build conversation for Gemini
$contents = [];
// System instruction as first user+model turn
$contents[] = [
'role' => 'user',
'parts' => [['text' => $systemPrompt]]
];
$contents[] = [
'role' => 'model',
'parts' => [['text' => 'Ciao! Sono il tuo assistente cucina. Conosco tutto quello che hai in dispensa e sono pronto ad aiutarti. Cosa ti va di preparare? 😊']]
];
// Add conversation history
foreach ($history as $msg) {
$role = ($msg['role'] === 'user') ? 'user' : 'model';
$contents[] = [
'role' => $role,
'parts' => [['text' => $msg['text']]]
];
}
// Add current message
$contents[] = [
'role' => 'user',
'parts' => [['text' => $message]]
];
$url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key={$apiKey}";
$payload = [
'contents' => $contents,
'generationConfig' => [
'temperature' => 0.8,
'maxOutputTokens' => 1500
]
];
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 60,
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($response === false || $httpCode !== 200) {
echo json_encode(['success' => false, 'error' => 'Errore API Gemini', 'http_code' => $httpCode]);
return;
}
$data = json_decode($response, true);
$reply = $data['candidates'][0]['content']['parts'][0]['text'] ?? '';
if (empty($reply)) {
echo json_encode(['success' => false, 'error' => 'Risposta vuota da Gemini']);
return;
}
echo json_encode(['success' => true, 'reply' => $reply]);
}
// ===== RECIPE GENERATION WITH GEMINI =====
function generateRecipe(PDO $db): void {
// Load API key from .env