anycoder-b8e925b1 / package.json
BikoRiko's picture
Upload package.json with huggingface_hub
6116632 verified
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&apos;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' });
}
}