From 0c0977a220df4856e306223bb60973868205177e Mon Sep 17 00:00:00 2001 From: ssbg04 Date: Fri, 5 Jun 2026 23:39:24 +0800 Subject: [PATCH] feat: switch chatbot from Gemini to Groq (llama-3.3-70b, 14400 RPD free) --- .env.example | 4 +-- api/chat.php | 77 ++++++++++++++++++++--------------------------- assets/js/chat.js | 57 +++++++++++++++++------------------ views/chatbot.php | 4 +-- 4 files changed, 64 insertions(+), 78 deletions(-) diff --git a/.env.example b/.env.example index 331e1ae..584528e 100644 --- a/.env.example +++ b/.env.example @@ -11,8 +11,8 @@ RECAPTCHA_SECRET_KEY=your-recaptcha-secret-key-here # Contact Email (used in FormSubmit action) CONTACT_EMAIL=your-email@gmail.com -# gemini api key -GEMINI_API_KEY=your-gemin-api-key +# Groq AI API Key (free: 14,400 req/day — get yours at https://console.groq.com) +GROQ_API_KEY=your-groq-api-key-here # Rate Limiting (requests per hour) diff --git a/api/chat.php b/api/chat.php index d8f8258..83ed271 100644 --- a/api/chat.php +++ b/api/chat.php @@ -25,10 +25,11 @@ $_SESSION['chat_requests'][] = $now; // Load DB connection which also loads environment variables require_once __DIR__ . '/../config/db.php'; -$apiKey = $_ENV['GEMINI_API_KEY'] ?? getenv('GEMINI_API_KEY') ?? ''; +// Load Groq API key (free tier: 14,400 req/day) +$apiKey = $_ENV['GROQ_API_KEY'] ?? getenv('GROQ_API_KEY') ?? ''; if (empty($apiKey)) { http_response_code(500); - echo json_encode(['error' => 'API Key configuration is missing. Please configure GEMINI_API_KEY in your .env file.']); + echo json_encode(['error' => 'API Key configuration is missing. Please set GROQ_API_KEY in your .env file.']); exit; } @@ -121,49 +122,32 @@ $systemPrompt .= "3. If asked about contact options or hiring, point them to his $systemPrompt .= "4. Do not make up facts or external projects not listed in the context. If you don't know the answer, politely invite the user to contact Cris directly.\n"; $systemPrompt .= "5. Format your output using simple markdown (like bolding or bullet points) to make it readable.\n"; -// Map chat history -// Map chat history and prepend system instructions to the first user message -$contents = []; -$firstMsgPrepended = false; +// Build Groq messages array (OpenAI-compatible format) +// System prompt goes in its own 'system' role message +$messages = [ + ['role' => 'system', 'content' => $systemPrompt] +]; +// Append chat history if (isset($inputData['history']) && is_array($inputData['history']) && !empty($inputData['history'])) { - foreach ($inputData['history'] as $index => $msg) { + foreach ($inputData['history'] as $msg) { if (isset($msg['role']) && isset($msg['text'])) { - $role = ($msg['role'] === 'user') ? 'user' : 'model'; - $text = $msg['text']; - - // Prepend system prompt to the first user message in history - if (!$firstMsgPrepended && $role === 'user') { - $text = "System Context & Instructions:\n" . $systemPrompt . "\n\nUser Message: " . $text; - $firstMsgPrepended = true; - } - - $contents[] = [ - 'role' => $role, - 'parts' => [['text' => $text]] - ]; + $role = ($msg['role'] === 'user') ? 'user' : 'assistant'; + $messages[] = ['role' => $role, 'content' => $msg['text']]; } } } -// Add the current user message (prepend system instructions if history was empty) -$currentUserText = $userMessage; -if (!$firstMsgPrepended) { - $currentUserText = "System Context & Instructions:\n" . $systemPrompt . "\n\nUser Message: " . $userMessage; -} +// Add the current user message +$messages[] = ['role' => 'user', 'content' => $userMessage]; -$contents[] = [ - 'role' => 'user', - 'parts' => [['text' => $currentUserText]] -]; - -// Setup request body without systemInstruction key +// Groq request payload (OpenAI-compatible) $requestPayload = [ - 'contents' => $contents, - 'generationConfig' => [ - 'temperature' => 0.7, - 'maxOutputTokens' => 800 - ] + 'model' => 'llama-3.3-70b-versatile', // 14,400 RPD free + 'messages' => $messages, + 'temperature' => 0.7, + 'max_tokens' => 800, + 'stream' => false ]; // Guard: cURL must be available @@ -173,15 +157,18 @@ if (!function_exists('curl_init')) { exit; } -// Call Gemini API via cURL -// gemini-2.0-flash-lite: highest free-tier quota (1,500 req/day) -$url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-lite:generateContent?key=" . urlencode($apiKey); +// Call Groq API via cURL (OpenAI-compatible endpoint) +// Model: llama-3.3-70b-versatile — free tier, 14,400 req/day +$url = 'https://api.groq.com/openai/v1/chat/completions'; $ch = curl_init($url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($requestPayload)); -curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); +curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'Content-Type: application/json', + 'Authorization: Bearer ' . $apiKey +]); curl_setopt($ch, CURLOPT_TIMEOUT, 15); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); @@ -200,18 +187,18 @@ curl_close($ch); if ($httpCode !== 200) { http_response_code($httpCode); $errorDetails = json_decode($response, true); - $errMsg = isset($errorDetails['error']['message']) ? $errorDetails['error']['message'] : 'Failed to retrieve response from Gemini.'; - echo json_encode(['error' => $errMsg, 'raw' => $response]); + $errMsg = isset($errorDetails['error']['message']) ? $errorDetails['error']['message'] : 'Failed to retrieve response from Groq.'; + echo json_encode(['error' => $errMsg]); exit; } $responseData = json_decode($response, true); $reply = ''; -if (isset($responseData['candidates'][0]['content']['parts'][0]['text'])) { - $reply = $responseData['candidates'][0]['content']['parts'][0]['text']; +if (isset($responseData['choices'][0]['message']['content'])) { + $reply = $responseData['choices'][0]['message']['content']; } else { http_response_code(502); - echo json_encode(['error' => 'Invalid response structure from Gemini API.', 'raw' => $responseData]); + echo json_encode(['error' => 'Invalid response structure from Groq API.', 'raw' => $responseData]); exit; } diff --git a/assets/js/chat.js b/assets/js/chat.js index b5a6449..5473904 100644 --- a/assets/js/chat.js +++ b/assets/js/chat.js @@ -75,41 +75,40 @@ document.addEventListener("DOMContentLoaded", () => { console.warn("Backend API call failed, attempting client-side fallback. Details:", error); } - // CLIENT-SIDE FALLBACK (e.g. for InfinityFree or local cert errors) + // CLIENT-SIDE FALLBACK: use Groq directly (OpenAI-compatible, 14,400 RPD free) if (!replySuccess) { const config = window.chatbotConfig; - if (config && config.apiKey && config.systemPrompt) { + if (config && config.groqApiKey && config.systemPrompt) { try { - // Structure payload for Gemini client-side and inject systemInstructions in first user message - let firstMsgPrepended = false; - const contents = chatHistory.map(msg => { - let text = msg.text; - const role = msg.role === 'user' ? 'user' : 'model'; - if (!firstMsgPrepended && role === 'user') { - text = `System Context & Instructions:\n${config.systemPrompt}\n\nUser Message: ${text}`; - firstMsgPrepended = true; - } - return { - role: role, - parts: [{ text: text }] - }; + // Build OpenAI-compatible messages array + const messages = [ + { role: "system", content: config.systemPrompt } + ]; + + // Append history (excluding the latest user message which we add separately) + chatHistory.slice(0, -1).forEach(msg => { + messages.push({ + role: msg.role === "user" ? "user" : "assistant", + content: msg.text + }); }); + // Add current user message + messages.push({ role: "user", content: message }); + const requestPayload = { - contents: contents, - generationConfig: { - temperature: 0.7, - maxOutputTokens: 800 - } + model: "llama-3.3-70b-versatile", // 14,400 RPD free + messages: messages, + temperature: 0.7, + max_tokens: 800, + stream: false }; - // gemini-2.0-flash-lite: highest free-tier quota (1,500 req/day) - const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-lite:generateContent?key=${encodeURIComponent(config.apiKey)}`; - - const response = await fetch(url, { + const response = await fetch("https://api.groq.com/openai/v1/chat/completions", { method: "POST", headers: { - "Content-Type": "application/json" + "Content-Type": "application/json", + "Authorization": `Bearer ${config.groqApiKey}` }, body: JSON.stringify(requestPayload) }); @@ -117,8 +116,8 @@ document.addEventListener("DOMContentLoaded", () => { const data = await response.json(); removeTypingIndicator(typingIndicator); - if (response.ok && data.candidates && data.candidates[0].content.parts[0].text) { - const botReply = data.candidates[0].content.parts[0].text; + if (response.ok && data.choices && data.choices[0].message.content) { + const botReply = data.choices[0].message.content; addMessage(botReply, "bot-msg"); chatHistory.push({ role: "model", text: botReply }); replySuccess = true; @@ -129,11 +128,11 @@ document.addEventListener("DOMContentLoaded", () => { } catch (fallbackError) { removeTypingIndicator(typingIndicator); addMessage("Unable to connect to assistant. Please check your internet connection.", "bot-msg error-msg"); - console.error("Direct API Fallback Error:", fallbackError); + console.error("Groq Fallback Error:", fallbackError); } } else { removeTypingIndicator(typingIndicator); - addMessage("API request blocked by host firewall and fallback config is missing.", "bot-msg error-msg"); + addMessage("Chatbot is temporarily unavailable. Please try again later.", "bot-msg error-msg"); } } diff --git a/views/chatbot.php b/views/chatbot.php index adb8db5..c353bd7 100644 --- a/views/chatbot.php +++ b/views/chatbot.php @@ -80,13 +80,13 @@ $systemPrompt .= "3. If asked about contact options or hiring, point them to his $systemPrompt .= "4. Do not make up facts or external projects not listed in the context. If you don't know the answer, politely invite the user to contact Cris directly.\n"; $systemPrompt .= "5. Format your output using simple markdown (like bolding or bullet points) to make it readable.\n"; -$apiKey = $_ENV['GEMINI_API_KEY'] ?? getenv('GEMINI_API_KEY') ?? ''; +$apiKey = $_ENV['GROQ_API_KEY'] ?? getenv('GROQ_API_KEY') ?? ''; ?>