BikoRiko commited on
Commit
6116632
·
verified ·
1 Parent(s): b1efa6c

Upload package.json with huggingface_hub

Browse files
Files changed (1) hide show
  1. package.json +682 -0
package.json ADDED
@@ -0,0 +1,682 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "gemini-chatbot",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "next build",
8
+ "start": "next start",
9
+ "lint": "next lint"
10
+ },
11
+ "dependencies": {
12
+ "@google/generative-ai": "^0.2.1",
13
+ "next": "14.1.0",
14
+ "react": "^18.2.0",
15
+ "react-dom": "^18.2.0"
16
+ },
17
+ "devDependencies": {
18
+ "autoprefixer": "^10.4.17",
19
+ "postcss": "^8.4.35",
20
+ "tailwindcss": "^3.4.1"
21
+ }
22
+ }
23
+
24
+ === next.config.js =
25
+ /** @type {import('next').NextConfig} */
26
+ const nextConfig = {
27
+ reactStrictMode: true,
28
+ images: {
29
+ domains: ['www.google.com', 'fonts.googleapis.com'],
30
+ },
31
+ }
32
+
33
+ module.exports = nextConfig
34
+
35
+ === postcss.config.js =
36
+ module.exports = {
37
+ plugins: {
38
+ tailwindcss: {},
39
+ autoprefixer: {},
40
+ },
41
+ }
42
+
43
+ === tailwind.config.js =
44
+ /** @type {import('tailwindcss').Config} */
45
+ module.exports = {
46
+ content: [
47
+ './pages/**/*.{js,ts,jsx,tsx,mdx}',
48
+ './components/**/*.{js,ts,jsx,tsx,mdx}',
49
+ ],
50
+ theme: {
51
+ extend: {
52
+ colors: {
53
+ gemini: {
54
+ light: '#e8f0fe',
55
+ DEFAULT: '#8ab4f8',
56
+ dark: '#1a73e8',
57
+ },
58
+ },
59
+ },
60
+ },
61
+ plugins: [],
62
+ }
63
+
64
+ === styles/globals.css =
65
+ @tailwind base;
66
+ @tailwind components;
67
+ @tailwind utilities;
68
+
69
+ * {
70
+ box-sizing: border-box;
71
+ padding: 0;
72
+ margin: 0;
73
+ }
74
+
75
+ html,
76
+ body {
77
+ max-width: 100vw;
78
+ overflow-x: hidden;
79
+ }
80
+
81
+ body {
82
+ background: #0f0f0f;
83
+ color: #ffffff;
84
+ }
85
+
86
+ /* Custom scrollbar */
87
+ ::-webkit-scrollbar {
88
+ width: 8px;
89
+ -track {
90
+ background: #1a}
91
+
92
+ ::-webkit-scrollbar1a1a;
93
+ }
94
+
95
+ ::-webkit-scrollbar-thumb {
96
+ background: #333;
97
+ border-radius: 4px;
98
+ }
99
+
100
+ ::-webkit-scrollbar-thumb:hover {
101
+ background: #444;
102
+ }
103
+
104
+ /* Animation for typing indicator */
105
+ @keyframes bounce {
106
+ 0%, 60%, 100% {
107
+ transform: translateY(0);
108
+ }
109
+ 30% {
110
+ transform: translateY(-4px);
111
+ }
112
+ }
113
+
114
+ .typing-dot {
115
+ animation: bounce 1.4s infinite ease-in-out;
116
+ }
117
+
118
+ .typing-dot:nth-child(1) {
119
+ animation-delay: 0s;
120
+ }
121
+
122
+ .typing-dot:nth-child(2) {
123
+ animation-delay: 0.2s;
124
+ }
125
+
126
+ .typing-dot:nth-child(3) {
127
+ animation-delay: 0.4s;
128
+ }
129
+
130
+ === components/ChatMessage.jsx =
131
+ import React from 'react';
132
+
133
+ export default function ChatMessage({ message, isUser }) {
134
+ return (
135
+ <div className={`flex w-full ${isUser ? 'justify-end' : 'justify-start'} mb-4`}>
136
+ <div
137
+ className={`max-w-[80%] md:max-w-[70%] rounded-2xl px-4 py-3 ${
138
+ isUser
139
+ ? 'bg-gemini-dark text-white rounded-br-md'
140
+ : 'bg-gray-800 text-gray-100 rounded-bl-md'
141
+ }`}
142
+ >
143
+ <div className="flex items-start gap-3">
144
+ {!isUser && (
145
+ <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">
146
+ <svg
147
+ className="w-4 h-4 text-white"
148
+ fill="none"
149
+ stroke="currentColor"
150
+ viewBox="0 0 24 24"
151
+ >
152
+ <path
153
+ strokeLinecap="round"
154
+ strokeLinejoin="round"
155
+ strokeWidth={2}
156
+ d="M13 10V3L4 14h7v7l9-11h-7z"
157
+ />
158
+ </svg>
159
+ </div>
160
+ )}
161
+ <div className="flex-1 break-words whitespace-pre-wrap">
162
+ {message}
163
+ </div>
164
+ {isUser && (
165
+ <div className="flex-shrink-0 w-8 h-8 rounded-full bg-blue-600 flex items-center justify-center">
166
+ <svg
167
+ className="w-4 h-4 text-white"
168
+ fill="none"
169
+ stroke="currentColor"
170
+ viewBox="0 0 24 24"
171
+ >
172
+ <path
173
+ strokeLinecap="round"
174
+ strokeLinejoin="round"
175
+ strokeWidth={2}
176
+ d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
177
+ />
178
+ </svg>
179
+ </div>
180
+ )}
181
+ </div>
182
+ </div>
183
+ </div>
184
+ );
185
+ }
186
+
187
+ === components/ApiKeyInput.jsx =
188
+ import React, { useState } from 'react';
189
+
190
+ export default function ApiKeyInput({ onSave, initialKey }) {
191
+ const [apiKey, setApiKey] = useState(initialKey || '');
192
+ const [showKey, setShowKey] = useState(false);
193
+
194
+ const handleSubmit = (e) => {
195
+ e.preventDefault();
196
+ if (apiKey.trim()) {
197
+ onSave(apiKey.trim());
198
+ }
199
+ };
200
+
201
+ return (
202
+ <div className="w-full max-w-md mx-auto">
203
+ <div className="bg-gray-900 border border-gray-700 rounded-2xl p-6 shadow-2xl">
204
+ <div className="text-center mb-6">
205
+ <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">
206
+ <svg
207
+ className="w-8 h-8 text-white"
208
+ fill="none"
209
+ stroke="currentColor"
210
+ viewBox="0 0 24 24"
211
+ >
212
+ <path
213
+ strokeLinecap="round"
214
+ strokeLinejoin="round"
215
+ strokeWidth={2}
216
+ 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"
217
+ />
218
+ </svg>
219
+ </div>
220
+ <h2 className="text-xl font-bold text-white">Enter Gemini API Key</h2>
221
+ <p className="text-gray-400 mt-2 text-sm">
222
+ Get your free API key from{' '}
223
+ <a
224
+ href="https://aistudio.google.com/app/apikey"
225
+ target="_blank"
226
+ rel="noopener noreferrer"
227
+ className="text-gemini hover:underline"
228
+ >
229
+ Google AI Studio
230
+ </a>
231
+ </p>
232
+ </div>
233
+
234
+ <form onSubmit={handleSubmit} className="space-y-4">
235
+ <div className="relative">
236
+ <input
237
+ type={showKey ? 'text' : 'password'}
238
+ value={apiKey}
239
+ onChange={(e) => setApiKey(e.target.value)}
240
+ placeholder="Paste your Gemini API key here..."
241
+ 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"
242
+ />
243
+ <button
244
+ type="button"
245
+ onClick={() => setShowKey(!showKey)}
246
+ className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-white transition-colors"
247
+ >
248
+ {showKey ? (
249
+ <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
250
+ <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" />
251
+ </svg>
252
+ ) : (
253
+ <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
254
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
255
+ <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" />
256
+ </svg>
257
+ )}
258
+ </button>
259
+ </div>
260
+
261
+ <button
262
+ type="submit"
263
+ disabled={!apiKey.trim()}
264
+ 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"
265
+ >
266
+ Start Chatting
267
+ </button>
268
+ </form>
269
+
270
+ <div className="mt-4 p-3 bg-gray-800/50 rounded-lg">
271
+ <p className="text-xs text-gray-400">
272
+ <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.
273
+ </p>
274
+ </div>
275
+ </div>
276
+ </div>
277
+ );
278
+ }
279
+
280
+ === components/ChatInput.jsx =
281
+ import React, { useState } from 'react';
282
+
283
+ export default function ChatInput({ onSend, disabled }) {
284
+ const [message, setMessage] = useState('');
285
+
286
+ const handleSubmit = (e) => {
287
+ e.preventDefault();
288
+ if (message.trim() && !disabled) {
289
+ onSend(message.trim());
290
+ setMessage('');
291
+ }
292
+ };
293
+
294
+ const handleKeyDown = (e) => {
295
+ if (e.key === 'Enter' && !e.shiftKey) {
296
+ e.preventDefault();
297
+ handleSubmit(e);
298
+ }
299
+ };
300
+
301
+ return (
302
+ <form onSubmit={handleSubmit} className="flex items-end gap-3 p-4 bg-gray-900 border-t border-gray-800">
303
+ <div className="flex-1 relative">
304
+ <textarea
305
+ value={message}
306
+ onChange={(e) => setMessage(e.target.value)}
307
+ onKeyDown={handleKeyDown}
308
+ placeholder="Type your message..."
309
+ disabled={disabled}
310
+ rows={1}
311
+ 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"
312
+ style={{ minHeight: '48px', maxHeight: '120px' }}
313
+ />
314
+ </div>
315
+ <button
316
+ type="submit"
317
+ disabled={!message.trim() || disabled}
318
+ 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"
319
+ >
320
+ {disabled ? (
321
+ <svg className="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24">
322
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
323
+ <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" />
324
+ </svg>
325
+ ) : (
326
+ <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
327
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" />
328
+ </svg>
329
+ )}
330
+ </button>
331
+ </form>
332
+ );
333
+ }
334
+
335
+ === components/Header.jsx =
336
+ import React from 'react';
337
+
338
+ export default function Header({ onClearChat, onChangeKey }) {
339
+ return (
340
+ <header className="sticky top-0 z-50 bg-gray-900/95 backdrop-blur-sm border-b border-gray-800">
341
+ <div className="max-w-4xl mx-auto px-4 py-3 flex items-center justify-between">
342
+ <div className="flex items-center gap-3">
343
+ <div className="flex items-center justify-center w-10 h-10 rounded-xl bg-gradient-to-br from-purple-500 to-pink-500">
344
+ <svg
345
+ className="w-5 h-5 text-white"
346
+ fill="none"
347
+ stroke="currentColor"
348
+ viewBox="0 0 24 24"
349
+ >
350
+ <path
351
+ strokeLinecap="round"
352
+ strokeLinejoin="round"
353
+ strokeWidth={2}
354
+ d="M13 10V3L4 14h7v7l9-11h-7z"
355
+ />
356
+ </svg>
357
+ </div>
358
+ <div>
359
+ <h1 className="text-lg font-bold text-white">Gemini Chat</h1>
360
+ <p className="text-xs text-gray-400">Powered by Google Gemini</p>
361
+ </div>
362
+ </div>
363
+ <div className="flex items-center gap-2">
364
+ <button
365
+ onClick={onClearChat}
366
+ className="p-2 text-gray-400 hover:text-white hover:bg-gray-800 rounded-lg transition-colors"
367
+ title="Clear chat"
368
+ >
369
+ <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
370
+ <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" />
371
+ </svg>
372
+ </button>
373
+ <button
374
+ onClick={onChangeKey}
375
+ className="p-2 text-gray-400 hover:text-white hover:bg-gray-800 rounded-lg transition-colors"
376
+ title="Change API key"
377
+ >
378
+ <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
379
+ <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" />
380
+ </svg>
381
+ </button>
382
+ </div>
383
+ </div>
384
+ </header>
385
+ );
386
+ }
387
+
388
+ === components/TypingIndicator.jsx =
389
+ import React from 'react';
390
+
391
+ export default function TypingIndicator() {
392
+ return (
393
+ <div className="flex items-center gap-2 px-4 py-3 bg-gray-800 rounded-2xl rounded-bl-md w-fit">
394
+ <div className="flex gap-1">
395
+ <span className="w-2 h-2 bg-gray-400 rounded-full typing-dot"></span>
396
+ <span className="w-2 h-2 bg-gray-400 rounded-full typing-dot"></span>
397
+ <span className="w-2 h-2 bg-gray-400 rounded-full typing-dot"></span>
398
+ </div>
399
+ </div>
400
+ );
401
+ }
402
+
403
+ === pages/_app.js =
404
+ import '../styles/globals.css';
405
+ import Head from 'next/head';
406
+
407
+ function MyApp({ Component, pageProps }) {
408
+ return (
409
+ <>
410
+ <Head>
411
+ <title>Gemini AI Chatbot</title>
412
+ <meta name="description" content="Chat with Google Gemini AI" />
413
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
414
+ <link rel="icon" href="/favicon.ico" />
415
+ </Head>
416
+ <Component {...pageProps} />
417
+ </>
418
+ );
419
+ }
420
+
421
+ export default MyApp;
422
+
423
+ === pages/index.js =
424
+ import React, { useState, useEffect, useRef } from 'react';
425
+ import Header from '../components/Header';
426
+ import ChatMessage from '../components/ChatMessage';
427
+ import ChatInput from '../components/ChatInput';
428
+ import ApiKeyInput from '../components/ApiKeyInput';
429
+ import TypingIndicator from '../components/TypingIndicator';
430
+
431
+ export default function Home() {
432
+ const [apiKey, setApiKey] = useState('');
433
+ const [showKeyInput, setShowKeyInput] = useState(true);
434
+ const [messages, setMessages] = useState([]);
435
+ const [isLoading, setIsLoading] = useState(false);
436
+ const [error, setError] = useState('');
437
+ const messagesEndRef = useRef(null);
438
+
439
+ // Load API key from localStorage on mount
440
+ useEffect(() => {
441
+ const savedKey = localStorage.getItem('gemini_api_key');
442
+ if (savedKey) {
443
+ setApiKey(savedKey);
444
+ setShowKeyInput(false);
445
+ }
446
+ }, []);
447
+
448
+ // Auto-scroll to bottom of messages
449
+ useEffect(() => {
450
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
451
+ }, [messages]);
452
+
453
+ const handleSaveApiKey = (key) => {
454
+ localStorage.setItem('gemini_api_key', key);
455
+ setApiKey(key);
456
+ setShowKeyInput(false);
457
+ };
458
+
459
+ const handleChangeKey = () => {
460
+ setShowKeyInput(true);
461
+ setMessages([]);
462
+ setError('');
463
+ };
464
+
465
+ const handleClearChat = () => {
466
+ setMessages([]);
467
+ setError('');
468
+ };
469
+
470
+ const handleSendMessage = async (text) => {
471
+ // Add user message
472
+ const userMessage = { role: 'user', content: text };
473
+ setMessages((prev) => [...prev, userMessage]);
474
+ setError('');
475
+
476
+ // Add empty assistant message for streaming effect
477
+ setMessages((prev) => [...prev, { role: 'assistant', content: '' }]);
478
+ setIsLoading(true);
479
+
480
+ try {
481
+ const response = await fetch('/api/chat', {
482
+ method: 'POST',
483
+ headers: {
484
+ 'Content-Type': 'application/json',
485
+ },
486
+ body: JSON.stringify({
487
+ message: text,
488
+ apiKey: apiKey,
489
+ history: messages.filter(m => m.role !== 'assistant' || m.content).map(m => ({
490
+ role: m.role === 'user' ? 'user' : 'model',
491
+ parts: [{ text: m.content }]
492
+ })),
493
+ }),
494
+ });
495
+
496
+ if (!response.ok) {
497
+ const errorData = await response.json();
498
+ throw new Error(errorData.error || 'Failed to get response from Gemini');
499
+ }
500
+
501
+ const data = await response.json();
502
+
503
+ // Update the last message with the response
504
+ setMessages((prev) => {
505
+ const newMessages = [...prev];
506
+ newMessages[newMessages.length - 1] = {
507
+ role: 'assistant',
508
+ content: data.response,
509
+ };
510
+ return newMessages;
511
+ });
512
+ } catch (err) {
513
+ setError(err.message);
514
+ // Remove the empty assistant message
515
+ setMessages((prev) => prev.slice(0, -1));
516
+ } finally {
517
+ setIsLoading(false);
518
+ }
519
+ };
520
+
521
+ // Show API key input if no key is set
522
+ if (showKeyInput) {
523
+ return (
524
+ <div className="min-h-screen bg-[#0f0f0f] flex items-center justify-center p-4">
525
+ <div className="absolute top-4 right-4">
526
+ <a
527
+ href="https://huggingface.co/spaces/akhaliq/anycoder"
528
+ target="_blank"
529
+ rel="noopener noreferrer"
530
+ className="text-sm text-gray-500 hover:text-gemini transition-colors"
531
+ >
532
+ Built with anycoder
533
+ </a>
534
+ </div>
535
+ <ApiKeyInput onSave={handleSaveApiKey} initialKey={apiKey} />
536
+ </div>
537
+ );
538
+ }
539
+
540
+ return (
541
+ <div className="min-h-screen bg-[#0f0f0f] flex flex-col">
542
+ <div className="absolute top-4 right-4 z-10">
543
+ <a
544
+ href="https://huggingface.co/spaces/akhaliq/anycoder"
545
+ target="_blank"
546
+ rel="noopener noreferrer"
547
+ className="text-sm text-gray-500 hover:text-gemini transition-colors"
548
+ >
549
+ Built with anycoder
550
+ </a>
551
+ </div>
552
+
553
+ <Header onClearChat={handleClearChat} onChangeKey={handleChangeKey} />
554
+
555
+ {/* Error banner */}
556
+ {error && (
557
+ <div className="bg-red-900/50 border border-red-700 text-red-200 px-4 py-3 mx-4 mt-4 rounded-lg">
558
+ <div className="flex items-center justify-between">
559
+ <span>{error}</span>
560
+ <button
561
+ onClick={() => setError('')}
562
+ className="text-red-400 hover:text-white"
563
+ >
564
+
565
+ </button>
566
+ </div>
567
+ </div>
568
+ )}
569
+
570
+ {/* Chat messages */}
571
+ <div className="flex-1 overflow-y-auto p-4 pb-2">
572
+ <div className="max-w-4xl mx-auto">
573
+ {messages.length === 0 && (
574
+ <div className="flex flex-col items-center justify-center h-[60vh] text-center">
575
+ <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">
576
+ <svg
577
+ className="w-10 h-10 text-white"
578
+ fill="none"
579
+ stroke="currentColor"
580
+ viewBox="0 0 24 24"
581
+ >
582
+ <path
583
+ strokeLinecap="round"
584
+ strokeLinejoin="round"
585
+ strokeWidth={2}
586
+ d="M13 10V3L4 14h7v7l9-11h-7z"
587
+ />
588
+ </svg>
589
+ </div>
590
+ <h2 className="text-2xl font-bold text-white mb-2">
591
+ Start a conversation
592
+ </h2>
593
+ <p className="text-gray-400 max-w-md">
594
+ Send a message to begin chatting with Gemini AI. Your API key is
595
+ securely stored locally.
596
+ </p>
597
+ </div>
598
+ )}
599
+
600
+ {messages.map((message, index) => (
601
+ <ChatMessage
602
+ key={index}
603
+ message={message.content}
604
+ isUser={message.role === 'user'}
605
+ />
606
+ ))}
607
+
608
+ {isLoading && messages.length > 0 && messages[messages.length - 1].role !== 'assistant' && (
609
+ <div className="flex w-full justify-start mb-4">
610
+ <TypingIndicator />
611
+ </div>
612
+ )}
613
+
614
+ <div ref={messagesEndRef} />
615
+ </div>
616
+ </div>
617
+
618
+ {/* Chat input */}
619
+ <ChatInput onSend={handleSendMessage} disabled={isLoading} />
620
+ </div>
621
+ );
622
+ }
623
+
624
+ === pages/api/chat.js =
625
+ import { GoogleGenerativeAI } from '@google/generative-ai';
626
+
627
+ export default async function handler(req, res) {
628
+ if (req.method !== 'POST') {
629
+ return res.status(405).json({ error: 'Method not allowed' });
630
+ }
631
+
632
+ try {
633
+ const { message, apiKey, history } = req.body;
634
+
635
+ if (!message) {
636
+ return res.status(400).json({ error: 'Message is required' });
637
+ }
638
+
639
+ if (!apiKey) {
640
+ return res.status(400).json({ error: 'API key is required' });
641
+ }
642
+
643
+ // Initialize Gemini with the user's API key
644
+ const genAI = new GoogleGenerativeAI(apiKey);
645
+
646
+ // Use gemini-pro model
647
+ const model = genAI.getGenerativeModel({ model: 'gemini-pro' });
648
+
649
+ // Build chat history for context
650
+ const chatHistory = history || [];
651
+
652
+ // Start chat with history
653
+ const chat = model.startChat({
654
+ history: chatHistory,
655
+ generationConfig: {
656
+ temperature: 0.9,
657
+ topP: 1,
658
+ topK: 1,
659
+ maxOutputTokens: 2048,
660
+ },
661
+ });
662
+
663
+ // Send message and get response
664
+ const result = await chat.sendMessage(message);
665
+ const response = result.response.text();
666
+
667
+ res.status(200).json({ response });
668
+ } catch (error) {
669
+ console.error('Gemini API Error:', error);
670
+
671
+ // Handle specific error cases
672
+ if (error.message?.includes('API_KEY')) {
673
+ return res.status(401).json({ error: 'Invalid API key. Please check your Gemini API key.' });
674
+ }
675
+
676
+ if (error.message?.includes('quota')) {
677
+ return res.status(429).json({ error: 'API quota exceeded. Please check your Google Cloud quota.' });
678
+ }
679
+
680
+ res.status(500).json({ error: error.message || 'Failed to get response from Gemini' });
681
+ }
682
+ }