Spaces:
Build error
Build error
Invalid JSON: Unexpected non-whitespace character after JSONat line 24, column 1
| { | |
| "name": "gemini-chatbot", | |
| "version": "1.0.0", | |
| "private": true, | |
| "scripts": { | |
| "dev": "next dev", | |
| "build": "next build", | |
| "start": "next start", | |
| "lint": "next lint" | |
| }, | |
| "dependencies": { | |
| "@google/generative-ai": "^0.2.1", | |
| "next": "14.1.0", | |
| "react": "^18.2.0", | |
| "react-dom": "^18.2.0" | |
| }, | |
| "devDependencies": { | |
| "autoprefixer": "^10.4.17", | |
| "postcss": "^8.4.35", | |
| "tailwindcss": "^3.4.1" | |
| } | |
| } | |
| === next.config.js = | |
| /** @type {import('next').NextConfig} */ | |
| const nextConfig = { | |
| reactStrictMode: true, | |
| images: { | |
| domains: ['www.google.com', 'fonts.googleapis.com'], | |
| }, | |
| } | |
| module.exports = nextConfig | |
| === postcss.config.js = | |
| module.exports = { | |
| plugins: { | |
| tailwindcss: {}, | |
| autoprefixer: {}, | |
| }, | |
| } | |
| === tailwind.config.js = | |
| /** @type {import('tailwindcss').Config} */ | |
| module.exports = { | |
| content: [ | |
| './pages/**/*.{js,ts,jsx,tsx,mdx}', | |
| './components/**/*.{js,ts,jsx,tsx,mdx}', | |
| ], | |
| theme: { | |
| extend: { | |
| colors: { | |
| gemini: { | |
| light: '#e8f0fe', | |
| DEFAULT: '#8ab4f8', | |
| dark: '#1a73e8', | |
| }, | |
| }, | |
| }, | |
| }, | |
| plugins: [], | |
| } | |
| === styles/globals.css = | |
| @tailwind base; | |
| @tailwind components; | |
| @tailwind utilities; | |
| * { | |
| box-sizing: border-box; | |
| padding: 0; | |
| margin: 0; | |
| } | |
| html, | |
| body { | |
| max-width: 100vw; | |
| overflow-x: hidden; | |
| } | |
| body { | |
| background: #0f0f0f; | |
| color: #ffffff; | |
| } | |
| /* Custom scrollbar */ | |
| ::-webkit-scrollbar { | |
| width: 8px; | |
| -track { | |
| background: #1a} | |
| ::-webkit-scrollbar1a1a; | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: #333; | |
| border-radius: 4px; | |
| } | |
| ::-webkit-scrollbar-thumb:hover { | |
| background: #444; | |
| } | |
| /* Animation for typing indicator */ | |
| @keyframes bounce { | |
| 0%, 60%, 100% { | |
| transform: translateY(0); | |
| } | |
| 30% { | |
| transform: translateY(-4px); | |
| } | |
| } | |
| .typing-dot { | |
| animation: bounce 1.4s infinite ease-in-out; | |
| } | |
| .typing-dot:nth-child(1) { | |
| animation-delay: 0s; | |
| } | |
| .typing-dot:nth-child(2) { | |
| animation-delay: 0.2s; | |
| } | |
| .typing-dot:nth-child(3) { | |
| animation-delay: 0.4s; | |
| } | |
| === components/ChatMessage.jsx = | |
| import React from 'react'; | |
| export default function ChatMessage({ message, isUser }) { | |
| return ( | |
| <div className={`flex w-full ${isUser ? 'justify-end' : 'justify-start'} mb-4`}> | |
| <div | |
| className={`max-w-[80%] md:max-w-[70%] rounded-2xl px-4 py-3 ${ | |
| isUser | |
| ? 'bg-gemini-dark text-white rounded-br-md' | |
| : 'bg-gray-800 text-gray-100 rounded-bl-md' | |
| }`} | |
| > | |
| <div className="flex items-start gap-3"> | |
| {!isUser && ( | |
| <div className="flex-shrink-0 w-8 h-8 rounded-full bg-gradient-to-r from-purple-500 to-pink-500 flex items-center justify-center"> | |
| <svg | |
| className="w-4 h-4 text-white" | |
| fill="none" | |
| stroke="currentColor" | |
| viewBox="0 0 24 24" | |
| > | |
| <path | |
| strokeLinecap="round" | |
| strokeLinejoin="round" | |
| strokeWidth={2} | |
| d="M13 10V3L4 14h7v7l9-11h-7z" | |
| /> | |
| </svg> | |
| </div> | |
| )} | |
| <div className="flex-1 break-words whitespace-pre-wrap"> | |
| {message} | |
| </div> | |
| {isUser && ( | |
| <div className="flex-shrink-0 w-8 h-8 rounded-full bg-blue-600 flex items-center justify-center"> | |
| <svg | |
| className="w-4 h-4 text-white" | |
| fill="none" | |
| stroke="currentColor" | |
| viewBox="0 0 24 24" | |
| > | |
| <path | |
| strokeLinecap="round" | |
| strokeLinejoin="round" | |
| strokeWidth={2} | |
| d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" | |
| /> | |
| </svg> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| === components/ApiKeyInput.jsx = | |
| import React, { useState } from 'react'; | |
| export default function ApiKeyInput({ onSave, initialKey }) { | |
| const [apiKey, setApiKey] = useState(initialKey || ''); | |
| const [showKey, setShowKey] = useState(false); | |
| const handleSubmit = (e) => { | |
| e.preventDefault(); | |
| if (apiKey.trim()) { | |
| onSave(apiKey.trim()); | |
| } | |
| }; | |
| return ( | |
| <div className="w-full max-w-md mx-auto"> | |
| <div className="bg-gray-900 border border-gray-700 rounded-2xl p-6 shadow-2xl"> | |
| <div className="text-center mb-6"> | |
| <div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-gradient-to-br from-purple-500 to-pink-500 mb-4"> | |
| <svg | |
| className="w-8 h-8 text-white" | |
| fill="none" | |
| stroke="currentColor" | |
| viewBox="0 0 24 24" | |
| > | |
| <path | |
| strokeLinecap="round" | |
| strokeLinejoin="round" | |
| strokeWidth={2} | |
| d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" | |
| /> | |
| </svg> | |
| </div> | |
| <h2 className="text-xl font-bold text-white">Enter Gemini API Key</h2> | |
| <p className="text-gray-400 mt-2 text-sm"> | |
| Get your free API key from{' '} | |
| <a | |
| href="https://aistudio.google.com/app/apikey" | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| className="text-gemini hover:underline" | |
| > | |
| Google AI Studio | |
| </a> | |
| </p> | |
| </div> | |
| <form onSubmit={handleSubmit} className="space-y-4"> | |
| <div className="relative"> | |
| <input | |
| type={showKey ? 'text' : 'password'} | |
| value={apiKey} | |
| onChange={(e) => setApiKey(e.target.value)} | |
| placeholder="Paste your Gemini API key here..." | |
| className="w-full px-4 py-3 pr-12 bg-gray-800 border border-gray-600 rounded-xl text-white placeholder-gray-500 focus:outline-none focus:border-gemini-dark focus:ring-1 focus:ring-gemini-dark transition-colors" | |
| /> | |
| <button | |
| type="button" | |
| onClick={() => setShowKey(!showKey)} | |
| className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-white transition-colors" | |
| > | |
| {showKey ? ( | |
| <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" /> | |
| </svg> | |
| ) : ( | |
| <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /> | |
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" /> | |
| </svg> | |
| )} | |
| </button> | |
| </div> | |
| <button | |
| type="submit" | |
| disabled={!apiKey.trim()} | |
| className="w-full py-3 px-4 bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 disabled:from-gray-600 disabled:to-gray-600 text-white font-semibold rounded-xl transition-all duration-200 disabled:cursor-not-allowed" | |
| > | |
| Start Chatting | |
| </button> | |
| </form> | |
| <div className="mt-4 p-3 bg-gray-800/50 rounded-lg"> | |
| <p className="text-xs text-gray-400"> | |
| <span className="text-yellow-400">⚠️</span> Your API key is stored locally in your browser and never sent to any server except Google's Gemini API. | |
| </p> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| === components/ChatInput.jsx = | |
| import React, { useState } from 'react'; | |
| export default function ChatInput({ onSend, disabled }) { | |
| const [message, setMessage] = useState(''); | |
| const handleSubmit = (e) => { | |
| e.preventDefault(); | |
| if (message.trim() && !disabled) { | |
| onSend(message.trim()); | |
| setMessage(''); | |
| } | |
| }; | |
| const handleKeyDown = (e) => { | |
| if (e.key === 'Enter' && !e.shiftKey) { | |
| e.preventDefault(); | |
| handleSubmit(e); | |
| } | |
| }; | |
| return ( | |
| <form onSubmit={handleSubmit} className="flex items-end gap-3 p-4 bg-gray-900 border-t border-gray-800"> | |
| <div className="flex-1 relative"> | |
| <textarea | |
| value={message} | |
| onChange={(e) => setMessage(e.target.value)} | |
| onKeyDown={handleKeyDown} | |
| placeholder="Type your message..." | |
| disabled={disabled} | |
| rows={1} | |
| className="w-full px-4 py-3 bg-gray-800 border border-gray-700 rounded-2xl text-white placeholder-gray-500 focus:outline-none focus:border-gemini-dark focus:ring-1 focus:ring-gemini-dark transition-colors resize-none disabled:opacity-50" | |
| style={{ minHeight: '48px', maxHeight: '120px' }} | |
| /> | |
| </div> | |
| <button | |
| type="submit" | |
| disabled={!message.trim() || disabled} | |
| className="flex-shrink-0 p-3 bg-gemini-dark hover:bg-blue-700 disabled:bg-gray-600 disabled:cursor-not-allowed text-white rounded-2xl transition-colors" | |
| > | |
| {disabled ? ( | |
| <svg className="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24"> | |
| <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" /> | |
| <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" /> | |
| </svg> | |
| ) : ( | |
| <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" /> | |
| </svg> | |
| )} | |
| </button> | |
| </form> | |
| ); | |
| } | |
| === components/Header.jsx = | |
| import React from 'react'; | |
| export default function Header({ onClearChat, onChangeKey }) { | |
| return ( | |
| <header className="sticky top-0 z-50 bg-gray-900/95 backdrop-blur-sm border-b border-gray-800"> | |
| <div className="max-w-4xl mx-auto px-4 py-3 flex items-center justify-between"> | |
| <div className="flex items-center gap-3"> | |
| <div className="flex items-center justify-center w-10 h-10 rounded-xl bg-gradient-to-br from-purple-500 to-pink-500"> | |
| <svg | |
| className="w-5 h-5 text-white" | |
| fill="none" | |
| stroke="currentColor" | |
| viewBox="0 0 24 24" | |
| > | |
| <path | |
| strokeLinecap="round" | |
| strokeLinejoin="round" | |
| strokeWidth={2} | |
| d="M13 10V3L4 14h7v7l9-11h-7z" | |
| /> | |
| </svg> | |
| </div> | |
| <div> | |
| <h1 className="text-lg font-bold text-white">Gemini Chat</h1> | |
| <p className="text-xs text-gray-400">Powered by Google Gemini</p> | |
| </div> | |
| </div> | |
| <div className="flex items-center gap-2"> | |
| <button | |
| onClick={onClearChat} | |
| className="p-2 text-gray-400 hover:text-white hover:bg-gray-800 rounded-lg transition-colors" | |
| title="Clear chat" | |
| > | |
| <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /> | |
| </svg> | |
| </button> | |
| <button | |
| onClick={onChangeKey} | |
| className="p-2 text-gray-400 hover:text-white hover:bg-gray-800 rounded-lg transition-colors" | |
| title="Change API key" | |
| > | |
| <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" /> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| </header> | |
| ); | |
| } | |
| === components/TypingIndicator.jsx = | |
| import React from 'react'; | |
| export default function TypingIndicator() { | |
| return ( | |
| <div className="flex items-center gap-2 px-4 py-3 bg-gray-800 rounded-2xl rounded-bl-md w-fit"> | |
| <div className="flex gap-1"> | |
| <span className="w-2 h-2 bg-gray-400 rounded-full typing-dot"></span> | |
| <span className="w-2 h-2 bg-gray-400 rounded-full typing-dot"></span> | |
| <span className="w-2 h-2 bg-gray-400 rounded-full typing-dot"></span> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| === pages/_app.js = | |
| import '../styles/globals.css'; | |
| import Head from 'next/head'; | |
| function MyApp({ Component, pageProps }) { | |
| return ( | |
| <> | |
| <Head> | |
| <title>Gemini AI Chatbot</title> | |
| <meta name="description" content="Chat with Google Gemini AI" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | |
| <link rel="icon" href="/favicon.ico" /> | |
| </Head> | |
| <Component {...pageProps} /> | |
| </> | |
| ); | |
| } | |
| export default MyApp; | |
| === pages/index.js = | |
| import React, { useState, useEffect, useRef } from 'react'; | |
| import Header from '../components/Header'; | |
| import ChatMessage from '../components/ChatMessage'; | |
| import ChatInput from '../components/ChatInput'; | |
| import ApiKeyInput from '../components/ApiKeyInput'; | |
| import TypingIndicator from '../components/TypingIndicator'; | |
| export default function Home() { | |
| const [apiKey, setApiKey] = useState(''); | |
| const [showKeyInput, setShowKeyInput] = useState(true); | |
| const [messages, setMessages] = useState([]); | |
| const [isLoading, setIsLoading] = useState(false); | |
| const [error, setError] = useState(''); | |
| const messagesEndRef = useRef(null); | |
| // Load API key from localStorage on mount | |
| useEffect(() => { | |
| const savedKey = localStorage.getItem('gemini_api_key'); | |
| if (savedKey) { | |
| setApiKey(savedKey); | |
| setShowKeyInput(false); | |
| } | |
| }, []); | |
| // Auto-scroll to bottom of messages | |
| useEffect(() => { | |
| messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); | |
| }, [messages]); | |
| const handleSaveApiKey = (key) => { | |
| localStorage.setItem('gemini_api_key', key); | |
| setApiKey(key); | |
| setShowKeyInput(false); | |
| }; | |
| const handleChangeKey = () => { | |
| setShowKeyInput(true); | |
| setMessages([]); | |
| setError(''); | |
| }; | |
| const handleClearChat = () => { | |
| setMessages([]); | |
| setError(''); | |
| }; | |
| const handleSendMessage = async (text) => { | |
| // Add user message | |
| const userMessage = { role: 'user', content: text }; | |
| setMessages((prev) => [...prev, userMessage]); | |
| setError(''); | |
| // Add empty assistant message for streaming effect | |
| setMessages((prev) => [...prev, { role: 'assistant', content: '' }]); | |
| setIsLoading(true); | |
| try { | |
| const response = await fetch('/api/chat', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ | |
| message: text, | |
| apiKey: apiKey, | |
| history: messages.filter(m => m.role !== 'assistant' || m.content).map(m => ({ | |
| role: m.role === 'user' ? 'user' : 'model', | |
| parts: [{ text: m.content }] | |
| })), | |
| }), | |
| }); | |
| if (!response.ok) { | |
| const errorData = await response.json(); | |
| throw new Error(errorData.error || 'Failed to get response from Gemini'); | |
| } | |
| const data = await response.json(); | |
| // Update the last message with the response | |
| setMessages((prev) => { | |
| const newMessages = [...prev]; | |
| newMessages[newMessages.length - 1] = { | |
| role: 'assistant', | |
| content: data.response, | |
| }; | |
| return newMessages; | |
| }); | |
| } catch (err) { | |
| setError(err.message); | |
| // Remove the empty assistant message | |
| setMessages((prev) => prev.slice(0, -1)); | |
| } finally { | |
| setIsLoading(false); | |
| } | |
| }; | |
| // Show API key input if no key is set | |
| if (showKeyInput) { | |
| return ( | |
| <div className="min-h-screen bg-[#0f0f0f] flex items-center justify-center p-4"> | |
| <div className="absolute top-4 right-4"> | |
| <a | |
| href="https://huggingface.co/spaces/akhaliq/anycoder" | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| className="text-sm text-gray-500 hover:text-gemini transition-colors" | |
| > | |
| Built with anycoder | |
| </a> | |
| </div> | |
| <ApiKeyInput onSave={handleSaveApiKey} initialKey={apiKey} /> | |
| </div> | |
| ); | |
| } | |
| return ( | |
| <div className="min-h-screen bg-[#0f0f0f] flex flex-col"> | |
| <div className="absolute top-4 right-4 z-10"> | |
| <a | |
| href="https://huggingface.co/spaces/akhaliq/anycoder" | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| className="text-sm text-gray-500 hover:text-gemini transition-colors" | |
| > | |
| Built with anycoder | |
| </a> | |
| </div> | |
| <Header onClearChat={handleClearChat} onChangeKey={handleChangeKey} /> | |
| {/* Error banner */} | |
| {error && ( | |
| <div className="bg-red-900/50 border border-red-700 text-red-200 px-4 py-3 mx-4 mt-4 rounded-lg"> | |
| <div className="flex items-center justify-between"> | |
| <span>{error}</span> | |
| <button | |
| onClick={() => setError('')} | |
| className="text-red-400 hover:text-white" | |
| > | |
| ✕ | |
| </button> | |
| </div> | |
| </div> | |
| )} | |
| {/* Chat messages */} | |
| <div className="flex-1 overflow-y-auto p-4 pb-2"> | |
| <div className="max-w-4xl mx-auto"> | |
| {messages.length === 0 && ( | |
| <div className="flex flex-col items-center justify-center h-[60vh] text-center"> | |
| <div className="w-20 h-20 rounded-2xl bg-gradient-to-br from-purple-500 to-pink-500 flex items-center justify-center mb-6"> | |
| <svg | |
| className="w-10 h-10 text-white" | |
| fill="none" | |
| stroke="currentColor" | |
| viewBox="0 0 24 24" | |
| > | |
| <path | |
| strokeLinecap="round" | |
| strokeLinejoin="round" | |
| strokeWidth={2} | |
| d="M13 10V3L4 14h7v7l9-11h-7z" | |
| /> | |
| </svg> | |
| </div> | |
| <h2 className="text-2xl font-bold text-white mb-2"> | |
| Start a conversation | |
| </h2> | |
| <p className="text-gray-400 max-w-md"> | |
| Send a message to begin chatting with Gemini AI. Your API key is | |
| securely stored locally. | |
| </p> | |
| </div> | |
| )} | |
| {messages.map((message, index) => ( | |
| <ChatMessage | |
| key={index} | |
| message={message.content} | |
| isUser={message.role === 'user'} | |
| /> | |
| ))} | |
| {isLoading && messages.length > 0 && messages[messages.length - 1].role !== 'assistant' && ( | |
| <div className="flex w-full justify-start mb-4"> | |
| <TypingIndicator /> | |
| </div> | |
| )} | |
| <div ref={messagesEndRef} /> | |
| </div> | |
| </div> | |
| {/* Chat input */} | |
| <ChatInput onSend={handleSendMessage} disabled={isLoading} /> | |
| </div> | |
| ); | |
| } | |
| === pages/api/chat.js = | |
| import { GoogleGenerativeAI } from '@google/generative-ai'; | |
| export default async function handler(req, res) { | |
| if (req.method !== 'POST') { | |
| return res.status(405).json({ error: 'Method not allowed' }); | |
| } | |
| try { | |
| const { message, apiKey, history } = req.body; | |
| if (!message) { | |
| return res.status(400).json({ error: 'Message is required' }); | |
| } | |
| if (!apiKey) { | |
| return res.status(400).json({ error: 'API key is required' }); | |
| } | |
| // Initialize Gemini with the user's API key | |
| const genAI = new GoogleGenerativeAI(apiKey); | |
| // Use gemini-pro model | |
| const model = genAI.getGenerativeModel({ model: 'gemini-pro' }); | |
| // Build chat history for context | |
| const chatHistory = history || []; | |
| // Start chat with history | |
| const chat = model.startChat({ | |
| history: chatHistory, | |
| generationConfig: { | |
| temperature: 0.9, | |
| topP: 1, | |
| topK: 1, | |
| maxOutputTokens: 2048, | |
| }, | |
| }); | |
| // Send message and get response | |
| const result = await chat.sendMessage(message); | |
| const response = result.response.text(); | |
| res.status(200).json({ response }); | |
| } catch (error) { | |
| console.error('Gemini API Error:', error); | |
| // Handle specific error cases | |
| if (error.message?.includes('API_KEY')) { | |
| return res.status(401).json({ error: 'Invalid API key. Please check your Gemini API key.' }); | |
| } | |
| if (error.message?.includes('quota')) { | |
| return res.status(429).json({ error: 'API quota exceeded. Please check your Google Cloud quota.' }); | |
| } | |
| res.status(500).json({ error: error.message || 'Failed to get response from Gemini' }); | |
| } | |
| } |