Phase 2: Implementation (Backend & Frontend)
Building the secure Cloudflare API and the React chat widget.
Phase 2: Implementation
With the design finalized, I moved on to writing the code. The implementation is split into two distinct parts: the secure backend endpoint and the interactive frontend widget.
1. The Secure Backend (Cloudflare Function)
To keep my Gemini API key secure, I created a serverless endpoint using Astro’s API routes, which will be compiled down to Cloudflare Pages Functions.
I created an endpoint at src/pages/api/chat.ts. This endpoint receives the chat history from the frontend, communicates with Gemini, and returns the response.
// src/pages/api/chat.ts
import type { APIRoute } from 'astro';
export const POST: APIRoute = async ({ request }) => {
try {
const { messages } = await request.json();
const apiKey = import.meta.env.GEMINI_API_KEY;
if (!apiKey) {
return new Response(JSON.stringify({ error: 'API key not configured' }), { status: 500 });
}
// Prepare the payload for Gemini
const payload = {
contents: messages.map((msg: any) => ({
role: msg.role === 'user' ? 'user' : 'model',
parts: [{ text: msg.content }]
}))
};
const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=${apiKey}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await response.json();
// Extract the text from the Gemini response
const replyText = data.candidates[0].content.parts[0].text;
return new Response(JSON.stringify({ reply: replyText }), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
} catch (error) {
console.error("Chat API Error:", error);
return new Response(JSON.stringify({ error: 'Failed to process request' }), { status: 500 });
}
}
Security Note: Notice how
GEMINI_API_KEYis loaded fromimport.meta.env. Because this code runs server-side (on Cloudflare), the key is never shipped to the browser.
2. The Frontend Chat Widget (React)
For the UI, I built a floating React component that users can toggle. It handles its own state (open/closed, loading, messages array) and is styled using our global Nord theme CSS variables to blend in flawlessly.
Here is a simplified version of the React component logic:
// src/components/ChatWidget.tsx
import React, { useState } from 'react';
import { MessageCircle, X, Send } from 'lucide-react';
interface Message {
role: 'user' | 'assistant';
content: string;
}
export default function ChatWidget() {
const [isOpen, setIsOpen] = useState(false);
const [messages, setMessages] = useState<Message[]>([
{ role: 'assistant', content: 'Hi there! I am an AI assistant trained on Narsing\'s portfolio. How can I help you today?' }
]);
const [input, setInput] = useState('');
const [isLoading, setIsLoading] = useState(false);
const sendMessage = async (e: React.FormEvent) => {
e.preventDefault();
if (!input.trim()) return;
const newMessages = [...messages, { role: 'user', content: input }];
setMessages(newMessages);
setInput('');
setIsLoading(true);
try {
const res = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ messages: newMessages }),
});
const data = await res.json();
if (data.reply) {
setMessages(prev => [...prev, { role: 'assistant', content: data.reply }]);
}
} catch (error) {
console.error("Failed to send message", error);
} finally {
setIsLoading(false);
}
};
// UI Rendering omitted for brevity
return (
<div className="fixed bottom-6 right-6 z-50">
{/* Chat UI implementation */}
</div>
);
}
By decoupling the frontend widget from the direct API call, I ensured that the portfolio remains performant and the API keys stay completely private.