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_KEY is loaded from import.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.