I’ve always enjoyed exploring ways to make portfolio sites more interactive and helpful. That’s why I decided to create a custom chatbot—something that not only demonstrates my expertise in React but also serves as a practical feature for anyone visiting my site. When someone lands on my portfolio, I want them to get immediate answers about my projects, skills, and background without sifting through multiple pages. A chatbot is an easy way to provide that experience.
I built this chatbot using a free stack on both Supabase and Gemini, which means there’s no barrier for anyone to replicate what I’ve done. In the sections ahead, I’ll share the step-by-step process, including how I set up Supabase for data storage, used animations to enhance the chat interface, and managed each user session. My goal is to keep this guide so straightforward that even if you’re new to React, you’ll be able to follow along and build your own version.
For those who want to see it in action before diving into the technical details, I have a live demo running on my website at www.melvinprince.io. Feel free to try it out and see how the chatbot interacts with visitors in real time.
Project Overview
When I set out to build this chatbot, I wanted to ensure it would smoothly handle user interactions, store messages in a reliable database, and deliver responses without delay. Here’s the basic flow of how everything works:
- User clicks on the Chat Bubble: Once someone opens the chat, a session ID is either created or retrieved. This session helps me keep track of each conversation so I can load or store messages at any time.
- Message Exchange via Supabase: Every question a user asks gets added to the chat history, then I process that message and send it to my backend function. The response that comes back is also saved, which keeps the conversation synced—even if the user refreshes the page.
- UI and Animations: I use Framer Motion to animate both the floating bubble and the expanded chat window. This adds a polished look and makes the chatbot feel natural rather than robotic.
- Cleanup and Maintenance: I set up a daily cron job (using a schedule file) to clear old sessions, so my database stays organized and doesn’t grow unmanageably large.
I rely on React for the frontend, Supabase for data handling, and simple serverless functions for the chatbot logic. Everything in this setup uses free or free-tier plans, including the Gemini API and Supabase, which is perfect for anyone wanting to replicate the project without incurring hosting charges.
At a high level, that’s the architecture. In the next sections, I’ll dive deeper into each piece, making sure you have all the details you need to build something similar for your own portfolio.
Remember to check out the live version at www.melvinprince.io if you want to see how it all works in practice.
Integrating the Chatbot into an Existing Portfolio
Many of us already have a working React portfolio, so there’s no need to rebuild everything from scratch. In my case, I happen to use Vite because it’s both fast and simple to set up, but you can adapt the following steps to your own configuration (whether it’s Create React App, Next.js, or another React framework). The main idea is to install a few packages, connect to Supabase, and drop in the chatbot components.
Step 1: Install the Required Packages
In your existing React project folder, open a terminal and run:
npm install framer-motion react-markdown @supabase/supabase-js
Here’s a quick rundown of why I chose these:
- framer-motion: Handles smooth animations (like the floating bubble and chat panel transitions).
- react-markdown: Allows me to render messages in markdown format.
- @supabase/supabase-js: Connects my frontend to Supabase so I can manage user sessions and store messages.
Step 2: Using Vite (If Applicable)
If you’re already on Vite, you’re good to go. If not, don’t worry—every build tool has a similar approach to environment variables and module imports. Here’s what my Vite setup typically looks like in the package.json
:
{
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
}
}
But if you’re on a different setup (like Create React App or Next.js), your scripts might be a bit different. The important part is to ensure you can run your dev server so you can test the chatbot later.
Step 3: Add Environment Variables
Regardless of your build tool, you’ll need environment variables to store your Supabase URL and API key. Here’s what that looks like if you’re using Vite:
- Create a
.env
or.env.local
file at the root of your project. -
Add your Supabase credentials:
VITE_SUPABASE_URL=https://your-supabase-url.supabase.co VITE_SUPABASE_KEY=your-supabase-key //use anon/public key. /* ⚠️ Important: The `VITE_SUPABASE_KEY` is a public key and safe to use in frontend code. However, never expose your **service role key** as it has full database access.*/
-
Reference them in your code:
//use process.env.name_of_key if not using vite const supabaseUrl = import.meta.env.VITE_SUPABASE_URL; const supabaseKey = import.meta.env.VITE_SUPABASE_KEY;
If you’re using a different environment, the concept stays the same—just adjust the syntax and file naming based on your preferred build system.
Step 4: Confirm Your Setup Works
Once you have everything installed and configured, make sure your existing portfolio still builds without errors. Run your usual development command, such as:
npm run dev
(If you’re on Vite) or
npm start
(If you’re on Create React App). This ensures the chatbot libraries haven’t caused any conflicts. If you can load your portfolio in the browser as usual, you’re ready to begin adding the actual chatbot components.
This approach keeps things global and flexible. You don’t have to rework your entire codebase to integrate the chatbot—I’m just showing you how I did it with Vite. In the next sections, I’ll explain the specifics of linking your chatbot logic to Supabase, adding an animated interface, and maintaining each conversation session.
File Structure & Detailed Walkthrough
Building a working chatbot might sound complicated at first, but it becomes much simpler once you break it down into smaller pieces. In this section, I’m going to show you a straightforward, dummy chatbot that you can adapt and run right away. I’ll explain how each file fits into the bigger picture, including how you can split the main Chatbot.jsx
file into multiple smaller components if you find that more manageable. By the end of these five parts, you’ll have a fully functional chatbot, ready for customization.
resumeContext.js
The file resumeContext.js
is where I like to store any hard-coded text or rules my chatbot needs. You can think of it as the bot’s reference manual. For a portfolio chatbot, it might hold details about your background, the conversation style you want, or even disclaimers about what the chatbot can and can’t answer. By keeping all this information in one place, it’s easier to update later without having to dig through multiple files.
Here’s a dummy version of the file, stripped down to just a few essential pieces:
// resumeContext.js - Customise this File according to your need
const resumeContext = `
[CONVERSATION INSTRUCTIONS]
- Only answer questions that relate to my portfolio or experience.
- If the user asks something outside these topics, reply with:
"I'm here to help with questions about my portfolio and projects. Please try asking something else."
[RESUME INFORMATION]
- Name: Jane Doe
- Specialization: Frontend Developer
- Key Projects: Portfolio Website, React Weather App, E-commerce Store
[ADDITIONAL NOTES]
- Encourage users to explore my other projects.
- Always be friendly and helpful.
`;
export default resumeContext;
Here’s how it works:
- Conversation Instructions: This tells the chatbot how to behave, so it knows what questions to answer or refuse.
- Resume Information: Any personal details you want the chatbot to share—like your name, skill set, or project highlights.
- Additional Notes: A catch-all section for anything else you might want the chatbot to keep in mind.
When you eventually connect your chatbot logic to this file, the bot will have access to everything in resumeContext.js
. Feel free to rename or reorganize the sections to fit your own needs. The important thing is keeping all the static text in one place so it’s easy to update later.
Chatbot.jsx
The Chatbot.jsx
file is the heart of the chatbot, handling everything from session management to smooth transitions. Below is a dummy example that incorporates the logic you saw in my original code, along with explanatory comments. If you find this file too large, feel free to split its functions into smaller components—such as a separate file for session management or animation logic—to keep things organized.
// Chatbot.jsx
import { AnimatePresence, motion } from "framer-motion";
import { useEffect, useRef, useState } from "react";
import resumeContext from "../data/resumeContext";
// ^ This is the file we created in Part 2 with chatbot instructions or resume info
import { supabase } from "../supabaseClient";
// ^ Replace with your Supabase client configuration or remove if you prefer another data store
import ChatMessage from "./ChatMessage";
// ^ We'll cover ChatMessage in the next part
import "./styles/chatbot.scss";
// ^ Example stylesheet; name and location can vary
export default function Chatbot() {
// ----------- 1) State Variables -----------
// open: toggles chatbot open/close
// messages: stores the conversation history
// input: tracks what's typed in the text field
// loading: true while we wait for a bot response
// sessionId: identifies each user session
// showFloating: toggles the "floating bubble" text
const [open, setOpen] = useState(false);
const [messages, setMessages] = useState([]);
const [input, setInput] = useState("");
const [loading, setLoading] = useState(false);
const [sessionId, setSessionId] = useState(null);
const [showFloating, setShowFloating] = useState(true);
// We’ll use these references for scroll effects
const messagesContainerRef = useRef(null);
const headerRef = useRef(null);
// ----------- 2) Hide Floating Bubble After Delay -----------
useEffect(() => {
const timer = setTimeout(() => {
setShowFloating(false);
}, 5000); // 5 seconds
return () => clearTimeout(timer);
}, []);
// ----------- 3) Generate or Retrieve Session ID -----------
useEffect(() => {
const existingSession = localStorage.getItem("chat_session_id");
if (existingSession) {
setSessionId(existingSession);
} else {
createNewSession();
}
}, []);
// ----------- 4) Fetch Chat Messages Once SessionID is Known -----------
useEffect(() => {
if (sessionId) {
fetchSessionMessages();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sessionId]);
// Helper function to create a new session in Supabase
const createNewSession = async () => {
const newSessionId = crypto.randomUUID();
localStorage.setItem("chat_session_id", newSessionId);
setSessionId(newSessionId);
// Add a new record in our "chat_sessions" table (dummy name)
await supabase.from("chat_sessions").insert([
{
session_id: newSessionId,
messages: [],
resume_context: resumeContext,
// You can store the context in the DB if you want to reference it
},
]);
};
// Helper function to fetch existing messages for the current session
const fetchSessionMessages = async () => {
const { data, error } = await supabase
.from("chat_sessions")
.select("messages")
.eq("session_id", sessionId)
.single();
if (!error && data) {
setMessages(data.messages);
} else {
// If there's any issue, show an error message
setMessages([
{
role: "bot",
text: "⚠️ Error fetching previous messages. Please try again later.",
},
]);
}
};
// Updates messages array in Supabase whenever a new message is added
const updateSessionMessages = async (updatedMessages) => {
await supabase
.from("chat_sessions")
.update({ messages: updatedMessages })
.eq("session_id", sessionId);
};
// ----------- 5) Toggling the Chat Window -----------
const toggleChat = () => {
setOpen((prev) => !prev);
// If the chat is being opened for the first time, add a welcome message
if (!open && messages.length === 0) {
const welcomeMessage = {
role: "bot",
text: "👋 Hi! Ask me anything about my projects or background.",
};
setMessages([welcomeMessage]);
if (sessionId) updateSessionMessages([welcomeMessage]);
}
};
// ----------- 6) Sending a Message -----------
const sendMessage = async () => {
// Don’t send an empty message
if (!input.trim()) return;
// 6a) Add the user's message to local state
const userMessage = { role: "user", text: input };
const updatedMessages = [...messages, userMessage];
setMessages(updatedMessages);
setLoading(true);
// 6b) Update the database so the message is stored
if (sessionId) {
await updateSessionMessages(updatedMessages);
}
try {
// 6c) Post the user’s text + context to our chatbot function
// Replace the URL with your own serverless function or chat endpoint
const response = await fetch("https://your-supabase-url.functions.supabase.co/chatbot", {
method: "POST",
headers: {
"Content-Type": "application/json",
// Example of using an environment variable for auth (if needed)
Authorization: `Bearer ${import.meta.env.VITE_SUPABASE_KEY}`,
},
body: JSON.stringify({ message: input, context: resumeContext }),
});
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
// Expecting { reply: "some text" } from the serverless function
const data = await response.json();
if (!data.reply) {
throw new Error("Invalid response from chatbot API.");
}
// 6d) Add the bot’s reply to local state
const botMessage = { role: "bot", text: data.reply };
const finalMessages = [...updatedMessages, botMessage];
setMessages(finalMessages);
// 6e) Update the database again with the bot's reply
if (sessionId) {
await updateSessionMessages(finalMessages);
}
// Clear the input box after sending
setInput("");
} catch (error) {
// 6f) If there’s any error, show a fallback message
const errorMessage = {
role: "bot",
text: "⚠️ Error fetching response. Please try again later.",
};
setMessages((prev) => [...prev, errorMessage]);
if (sessionId) {
await updateSessionMessages([...updatedMessages, errorMessage]);
}
}
// End loading
setLoading(false);
};
// ----------- 7) Auto-Scrolling to the Latest Message -----------
useEffect(() => {
if (!messagesContainerRef.current || !headerRef.current) return;
if (open) {
// We wait a bit so the new message has time to render
setTimeout(() => {
const container = messagesContainerRef.current;
const headerHeight = headerRef.current.offsetHeight;
container.scrollTo({
top: container.scrollHeight - headerHeight,
behavior: "smooth",
});
}, 100);
}
}, [messages, loading, open]);
// ----------- 8) Rendering the Chat Interface -----------
return (
<>
{/* Minimized Chat Bubble */}
<AnimatePresence>
{!open && (
<motion.div
className="chatbot-wrapper"
onClick={toggleChat}
drag
dragMomentum={false}
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 50 }}
transition={{ duration: 0.5 }}
>
{showFloating && (
<motion.div
className="chatbot-floating"
whileHover={{ scale: 1.05 }}
>
<span>Hey! Chat with me</span>
</motion.div>
)}
<div className="chatbot-avatar">
<img src="/chatbot.svg" alt="Chat Avatar" />
</div>
</motion.div>
)}
</AnimatePresence>
{/* Expanded Chat Interface */}
<AnimatePresence>
{open && (
<motion.div
className="chatbot-container"
initial={{ opacity: 0, scale: 0.9, y: 50 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.9, y: 50 }}
transition={{ duration: 0.5 }}
>
{/* Chat Header */}
<div className="chatbot-header" ref={headerRef}>
<h3>Chat</h3>
<button className="chatbot-btn" onClick={() => setOpen(false)}>
Close
</button>
</div>
{/* Messages Section */}
<div className="chatbot-messages" ref={messagesContainerRef}>
{messages.map((msg, index) => (
<ChatMessage key={index} message={msg.text} role={msg.role} />
))}
{loading && (
<ChatMessage key="loading" role="bot" loading={true} />
)}
</div>
{/* Input Section */}
<div className="chatbot-input">
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") sendMessage();
}}
placeholder="Type your question..."
/>
<button
className="chatbot-btn chatbot-send"
onClick={sendMessage}
disabled={loading}
>
Send
</button>
</div>
</motion.div>
)}
</AnimatePresence>
</>
);
}
Breaking Chatbot.jsx
into Smaller Parts
-
Session Management
If you find session logic too bulky, you can move all session-related functions (like
createNewSession
,fetchSessionMessages
, andupdateSessionMessages
) into a custom hook file, for example,useChatSession.js
. -
API Calls
The function that calls your serverless chatbot endpoint (
sendMessage
) can also be placed in a dedicated utility file if you’d like to separate concerns. -
Animations & UI
If you want to keep your UI code separate from your data code, consider migrating the Framer Motion components to a specialized UI component that receives props from the main chatbot component.
Explanation & Key Takeaways
-
Session State: By storing a session ID in
localStorage
, I ensure that users don’t lose their chat history when they refresh the page. - Supabase Integration: I’m using Supabase to store each conversation in a simple table. If you prefer, you could swap this out with any database or even a simple server file.
- Animations: Framer Motion transitions give the chatbot a clean look.
- Scroll-to-View: By referencing the message container and header, I can automatically scroll to the latest message or loader every time something changes.
This dummy chatbot should work as-is if you set up the rest of your environment (like Supabase) correctly. Use this as a launchpad for creating a chatbot that truly represents your style or portfolio needs. In the next sections, I’ll explain the supporting components and the optional cleanup routine to keep your sessions organized.
ChatMessage.jsx
The ChatMessage.jsx
file is responsible for rendering each individual chat message in a clean, consistent way. It decides how the text looks, whether to display a loading animation, and how to style messages differently for the user versus the bot. Below is a dummy version of the file that mirrors the essentials from my own setup.
// ChatMessage.jsx
import React, { forwardRef } from "react";
import ReactMarkdown from "react-markdown";
import { motion } from "framer-motion";
// Optional loading animation component
function ChatbotLoadingAnimation() {
return (
<div className="loading-dots">
<span>.</span>
<span>.</span>
<span>.</span>
</div>
);
}
// I'm using forwardRef here, but it's purely optional.
// If you don't need refs for scrolling or transitions, a normal component is fine.
const ChatMessage = forwardRef(({ message, role, loading }, ref) => {
// This is a simple check for an error style (e.g., if the text contains some warning emoji).
// It's optional—omit if you don't need special styling for errors.
const isError = role === "bot" && message && message.includes("⚠️");
return (
<motion.div
ref={ref}
className={`message-wrapper ${role} ${isError ? "error" : ""}`}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
>
{/* A small avatar (optional) if the role is "bot" */}
{role === "bot" && (
<div className="message-avatar">
<img src="/chat-avatar.png" alt="Bot Avatar" />
</div>
)}
<div className="message-bubble">
{/* If we're still loading, show an animation. Otherwise, render the actual text. */}
{loading ? (
<ChatbotLoadingAnimation />
) : (
<ReactMarkdown>{message}</ReactMarkdown>
)}
</div>
</motion.div>
);
});
export default ChatMessage;
How This File Works
-
Role-Based Styling:
- If a message comes from the user (
role === "user"
), I might want a different background color or text alignment. - If it’s from the bot (
role === "bot"
), I can display an avatar or style the message bubble differently.
- If a message comes from the user (
-
Markdown Support:
- Using React Markdown allows me to easily format the text. If the bot returns Markdown syntax (like bold text or bullet points), it gets rendered correctly in the message bubble.
-
Loading Animation:
- Before the bot’s response arrives, I can display a placeholder animation. This helps users feel like something is happening instead of showing a blank space.
-
ForwardRef (Optional):
- I’ve added
forwardRef
in case you want to pass a ref down for scroll behavior or advanced animations. If you don’t need it, feel free to remove it and switch to a standard component definition.
- I’ve added
Splitting Out Features
If you prefer smaller files:
-
Loading Animation as a Separate Component: Move
ChatbotLoadingAnimation
to its own file (e.g.,ChatbotLoadingAnimation.jsx
) to avoid clutter. - Error Handling: You can create a higher-order component or a custom hook that checks for error messages and applies the error class.
Key Takeaways
-
Minimal, Focused Component: Keeping
ChatMessage.jsx
limited to display logic ensures it’s easy to tweak how your chat messages look without touching your chatbot’s overall logic inChatbot.jsx
. -
Markdown Integration: A quick way to give your chatbot more expressive responses—just by returning strings that include
italics*
or*bold**
, for instance. - Animation and Error States: Framer Motion lets you animate how messages appear. If you need to highlight errors, a conditional class and style rule can do the trick.
In the next part, I’ll explain how you can schedule a routine cleanup to remove old chat sessions if you’re using a database. That step isn’t mandatory, but it’s handy if you expect a lot of visitors and want to keep your data tidy.
schedule.yml
(Optional Cleanup Routine)
Some of us may see a high volume of conversations over time, especially if a lot of visitors are testing out our chatbot. If that happens, you might want to periodically remove old chat sessions from your database. That’s where schedule.yml
comes in. This file uses GitHub Actions to run an automated cleanup every day (or any schedule you define).
Go to GitHub → Open Project Repository → Actions tab → Setup custom workflow → Paste the below content
name: Cleanup Sessions
on:
schedule:
- cron: '0 3 * * *' # Runs every day at 3 AM UTC
jobs:
cleanup:
runs-on: ubuntu-latest
steps:
- name: Call Cleanup Function
run: curl -X POST "https://your-supabase-url.functions.supabase.co/cleanup-sessions"
How It Works
-
Daily Trigger:
The
schedule:
block tells GitHub Actions to run this job on a set schedule. In the example above, the cron expression'0 3 * * *'
means “run every day at 3 AM UTC.” -
Cleanup Steps:
Inside
jobs.cleanup.steps
, I make a POST request to a serverless function (e.g., a Supabase function) that’s responsible for deleting outdated sessions from the database. This keeps my storage lean and prevents clutter from building up. -
Deployment Location:
You store this file in a
.github/workflows
directory within your repository. GitHub then automatically sets up the action. -
Adjusting the Frequency:
If you only want a cleanup once a week, or at some other interval, modify the cron string accordingly. For instance,
'0 0 * * 0'
would run at midnight every Sunday.
Is This Mandatory?
- Not at all. If your chatbot doesn’t have a large user base, you might not need an automated cleanup. You can always handle it manually or skip it entirely.
- If you do expect high traffic or want to ensure a tidy database, this scheduled job is a convenient, hands-off approach.
Supabase Table Schema
The chat_sessions table is central to how I store user messages and session data. Here’s an example schema I use:
Go to Supabase Dashboard → SQL Editor → Paste this query → Click "Run"
CREATE TABLE IF NOT EXISTS chat_sessions (
session_id UUID PRIMARY KEY,
messages JSONB NOT NULL,
resume_context TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
How to Create the Table with the Supabase SQL Editor
- Open Your Supabase Project: In your Supabase Dashboard, select your project.
- Click on “SQL”: You’ll find this in the left-side menu.
- Paste the CREATE TABLE Query: Copy and paste the SQL snippet above into the editor.
- Run the Query: Click on the “Run” button. Supabase will create the table if it doesn’t already exist.
Key Fields:
- session_id: A UUID that uniquely identifies each chat session.
- messages: A JSONB (binary JSON) field that holds the conversation array.
- resume_context: An optional text field for storing your instructions or other session-specific context.
- created_at: Auto-populated with the current timestamp so you know when the session was created.
Setting Up Row-Level Security (RLS)
Row-level security is a way of controlling who can read and write rows in your tables. By default, Supabase disables RLS on new tables, meaning everyone with a valid API key can read/write. If you’re only using a public API key in your front-end (and not user-specific authentication), you may have a simpler security model. However, if you want to tighten things up, here’s how:
-
Enable RLS
ALTER TABLE chat_sessions ENABLE ROW LEVEL SECURITY;
-
Create Policies
The exact policies depend on your use case. For instance, you might allow anyone with your public API key to insert and select rows (because you’re not storing personal user data). Below is a very permissive example:
-- Allow anyone to SELECT all rows from chat_sessions CREATE POLICY "Allow all select" ON chat_sessions FOR SELECT TO public USING (true); -- Allow anyone to INSERT new sessions CREATE POLICY "Allow all insert" ON chat_sessions FOR INSERT TO public WITH CHECK (true);
If you want to make it more secure (for instance, by tying sessions to authenticated user IDs), you’d adjust the
USING
andWITH CHECK
clauses to match your requirements. In a more advanced setup, you might store a user_id in the table and only allow a user to see or modify their own sessions.
Additional Supabase Setup Tips
- Service Role Key: If you’re using a separate Edge Function (for cleanup or advanced logic), you might need the service role key to bypass RLS or perform deletions.
- Environment Variables: Keep your keys (especially service role) in environment variables. Never hardcode them in your public front-end code.
- Database Backups: You can enable automatic backups from the Supabase Dashboard to safeguard your data.
- Logs and Monitoring: Supabase provides logs for SQL queries, function calls, and API activity. Checking these logs is crucial when you’re debugging issues with the chatbot or RLS policies.
Why Use RLS?
- Granular Control: Decide who can read or modify each row of data.
- Better Security: By default, the public key can read/write only what you allow through policies.
- Scalability: As your app grows, well-structured policies help prevent accidental data leaks.
Supabase Edge Functions with Gemini
Supabase Edge Functions are serverless functions you can write in JavaScript or TypeScript and deploy directly to the Supabase platform. This means:
- No separate server needed.
- Automatic scaling, so it can handle as many requests as you need.
- Integration with Supabase for easy database and auth access.
In our chatbot scenario, we rely on these Edge Functions for two major tasks:
-
Processing Chatbot Messages (the
chatbot
function). -
Performing Scheduled Cleanup (the
cleanup-sessions
function referenced inschedule.yml
—optional).
The steps below will show you how to include Gemini in your chatbot
function so it can fetch additional data or perform AI-based processing, all while keeping your Gemini API key secure.
Setting Up Supabase Edge Functions Locally
Before creating or deploying an Edge Function, you’ll need the Supabase CLI:
-
Install Supabase
npm install supabase
-
Log in:
npx supabase login
Navigate to your project folder (the same folder where your chatbot code lives).
Configuring Your Gemini API Key in Supabase
Since we’ll use the Gemini API key in our Edge Function (and not in our frontend), we’ll store it as an environment variable in Supabase:
-
Open Your Supabase Dashboard:
Go to app.supabase.com and select your project.
-
Project Settings → API (or Environment Variables):
In the left menu, look for “Project Settings.” Inside, you’ll find a section for managing your API or environment variables.
-
Add a New Variable:
- Click on “New Secret” or “New Variable” (the interface may vary depending on Supabase versions).
- Name it
GEMINI_API_KEY
. - Paste your Gemini API key as the value.
- Save your changes.
Important: This keeps your API key in a secure environment on Supabase. Anyone calling your Edge Function from the frontend never sees the key, which helps prevent unauthorized usage.
Creating the “chatbot” Edge Function
Generate the Function
npx supabase functions new chatbot
This command creates a new folder supabase/functions/chatbot
with some boilerplate code. You’ll see a file like index.ts
(if you chose TypeScript) or index.js
(if JavaScript).
Edit index.ts
(Incorporating Gemini Logic)
Below is an updated example that receives the user’s message
and context
, calls Gemini with the secret API key, and returns a combined response:
// supabase/functions/chatbot/index.ts
import { serve } from "https://deno.land/x/sift@0.6.0/mod.ts";
// Retrieve your Gemini API key from environment variables
const geminiApiKey = Deno.env.get("GEMINI_API_KEY") || "";
/*
This updated function:
1. Reads the incoming JSON body for `message` and `context`.
2. Makes a dummy call to Gemini's API (replace with the real endpoint).
3. Crafts a combined response using your chatbot logic plus any data from Gemini.
4. Returns { reply: "some text" } to the front-end.
*/
serve({
async "/": async (req: Request) => {
try {
// 1) Parse the incoming data from Chatbot.jsx
const { message, context } = await req.json();
// 2) Example Gemini API call (dummy URL and body).
// In your real application, replace this with Gemini's actual endpoint and request structure.
const geminiResponse = await fetch("https://api.gemini.example.com/query", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${geminiApiKey}`,
},
body: JSON.stringify({
userMessage: message,
// You might also include the context if that’s relevant to Gemini
additionalContext: context,
}),
});
// Parse the response from Gemini
const geminiData = await geminiResponse.json();
// 3) Combine your logic with Gemini’s response
// Let's say geminiData includes an "answer" field.
const geminiAnswer = geminiData.answer || "No specific answer from Gemini.";
const reply = `You said: ${message}
(Gemini says: ${geminiAnswer})
(Context snippet: ${context.substring(0, 50)}...)`;
// 4) Return the combined reply to the chatbot
return new Response(JSON.stringify({ reply }), {
headers: { "Content-Type": "application/json" },
});
} catch (error) {
return new Response(
JSON.stringify({ reply: "Something went wrong with Gemini or our logic." }),
{ status: 400, headers: { "Content-Type": "application/json" } }
);
}
},
// You can define more endpoints here if you want (e.g., "/health")
});
Key Points:
- We’re using Sift to handle incoming HTTP requests.
-
geminiApiKey
is retrieved fromDeno.env.get("GEMINI_API_KEY")
. - If your Gemini endpoint returns more data (like
confidenceScore
,timestamp
, etc.), feel free to add it to the final reply.
Deploy the Function
Once you’re satisfied with your logic:
npx supabase functions deploy chatbot
Supabase will give you a URL like:
https://<YOUR-SUPABASE-PROJECT>.functions.supabase.co/chatbot
Connecting the Function to Chatbot.jsx
In your Chatbot.jsx
file, find where you make the API call to your chatbot function:
const response = await fetch("https://<YOUR-SUPABASE-PROJECT>.functions.supabase.co/chatbot", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${import.meta.env.VITE_SUPABASE_KEY}`, // If needed
},
body: JSON.stringify({ message: input, context: resumeContext }),
});
Replace "https://<YOUR-SUPABASE-PROJECT>.functions.supabase.co/chatbot"
with the actual URL you got after deploying. Now, whenever a user types a message and clicks “Send,” your Edge Function will:
-
Receive the user message and the
resumeContext
. - Call the Gemini API using your secret key.
- Construct a final reply combining the Gemini response with your custom context.
- Send that back to your React chatbot, which displays it.
Verifying Everything Works
-
Local Testing: Run your frontend (e.g.,
npm run dev
if you’re on Vite). Open the chatbot and type a message. Confirm you see the Gemini-inspired reply. - Supabase Dashboard → Logs: Check that your Edge Function is being called without errors. If something goes wrong, logs are your friend.
- Security: Your Gemini key is never exposed on the client side, because it lives in Supabase environment variables.
Final Thoughts and Next Steps
- Enhance the Logic: Now that Gemini is involved, you can refine the text parsing, handle custom AI features, or pass more context to your calls.
- Error Handling: If Gemini is down or returns an error, you might catch that and provide a fallback response.
- Scaling: As traffic grows, Supabase automatically scales Edge Functions, so you shouldn’t need any extra infrastructure.
By integrating Gemini into your Supabase Edge Function, you maintain a secure, streamlined workflow. The user’s message flows from the front-end (Chatbot.jsx
) to your function (chatbot/index.ts
), which communicates with Gemini behind the scenes. This architecture keeps your secrets safe while giving you the power to incorporate rich data and AI into your chatbot replies.
Creating the “cleanup-sessions” Edge Function (Optional)
You might remember the schedule.yml
file that calls a cleanup function:
curl -X POST "https://your-supabase-url.functions.supabase.co/cleanup-sessions"
If you want that to work, you’ll need a second Edge Function named cleanup-sessions
that knows how to delete old chat records from your database.
Generate the Cleanup Function
npx supabase functions new cleanup-sessions
Edit index.ts
(Housekeeping Logic)
Below is a simple example that deletes records older than a specific number of days. Adjust it to your needs:
// supabase/functions/cleanup-sessions/index.ts
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
import { serve } from "https://deno.land/x/sift@0.6.0/mod.ts";
// This is a public Deno-based approach; for private credentials, consider using secrets in production.
const supabaseUrl = Deno.env.get("SUPABASE_URL") || "";
const supabaseKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") || "";
const supabase = createClient(supabaseUrl, supabaseKey);
serve({
async "/": async (req: Request) => {
try {
// For example, delete sessions older than 7 days
const sevenDaysAgo = new Date();
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
// Assuming you have a 'chat_sessions' table with a 'created_at' field
// This is pseudo-code—change "created_at" or "updated_at" to match your schema
const { error } = await supabase
.from("chat_sessions")
.delete()
.lt("created_at", sevenDaysAgo.toISOString());
if (error) throw error;
return new Response(
JSON.stringify({ status: "success", message: "Old sessions deleted." }),
{ headers: { "Content-Type": "application/json" } },
);
} catch (error) {
return new Response(JSON.stringify({ status: "error", error: error.message }), {
status: 400,
headers: { "Content-Type": "application/json" },
});
}
},
});
Key Points:
- We use a service role key so we can perform deletions (this key should remain secret).
- The code filters sessions older than 7 days (
sevenDaysAgo.setDate(...)
), but adjust as you see fit. - Make sure your
chat_sessions
table has a date column (e.g.,created_at
) for this to work.
Deploy the Cleanup Function
npx supabase functions deploy cleanup-sessions
Just like the chatbot function, you’ll get a URL:
https://<YOUR-SUPABASE-PROJECT>.functions.supabase.co/cleanup-sessions
Linking It With schedule.yml
Back in your GitHub repo, open your schedule.yml
file (in .github/workflows/
) and confirm it matches the new function URL:
name: Cleanup Sessions
on:
schedule:
- cron: '0 3 * * *' # Every day at 3 AM UTC
jobs:
cleanup:
runs-on: ubuntu-latest
steps:
- name: Call Cleanup Function
run: curl -X POST "https://<YOUR-SUPABASE-PROJECT>.functions.supabase.co/cleanup-sessions"
Push this file to your GitHub repository. When the Action runs (daily at 3 AM UTC in this example), it will send a POST request to your cleanup-sessions
function, triggering the housekeeping logic.
Testing Your Setup
Test the Chatbot Function
-
Local Test in Browser:
- Open your application at
http://localhost:3000
orhttp://localhost:5173
(if you’re using Vite). - Send a chat message.
- Confirm that the response you get in the chat matches your Edge Function logic.
- Open your application at
-
Logs in Supabase:
- In the Supabase Dashboard, check the “Logs” or “Functions” section to see if requests are hitting your function without errors.
Test the Cleanup Function
-
Manual cURL:
curl -X POST "https://<YOUR-SUPABASE-PROJECT>.functions.supabase.co/cleanup-sessions"
- If everything is working, you should get a success message in the console and see old sessions removed from your `chat_sessions` table.
-
Check the GitHub Actions Tab:
- Go to your repository → Actions → “Cleanup Sessions” to see if it’s scheduled or if you want to run it manually.
Final Overview of All Files
Here’s a quick recap of where everything lives:
-
resumeContext.js
: Holds static text and instructions for the chatbot. -
Chatbot.jsx
: Core chat logic, state management, and UI interactions. -
ChatMessage.jsx
: Renders individual messages with optional loading animations. -
schedule.yml
: GitHub Actions workflow for hitting the cleanup function daily. -
supabase/functions/chatbot/index.ts
: Edge Function that processes user messages (dummy echo logic in our example). -
supabase/functions/cleanup-sessions/index.ts
: Edge Function that removes old entries fromchat_sessions
.
When combined:
- A user opens the chat on your site.
-
Chatbot.jsx
sends the user’s message + theresumeContext.js
to the “chatbot” Edge Function. - The Edge Function returns a reply.
-
schedule.yml
calls “cleanup-sessions” once a day to clear out stale data.
Bringing It All Together
Having built each piece of the chatbot separately—resumeContext.js
, Chatbot.jsx
, ChatMessage.jsx
, and optionally our schedule.yml
plus Supabase Edge Functions—the final step is combining everything in our main application. Below is a streamlined example of how I integrate the chatbot into a typical React app.
Integrating the Components
-
Main App Component
Create or open your main component—often named
App.jsx
. You can also add the chatbot to any other component if you prefer. The key is to import theChatbot
and insert it where you want it displayed. -
Chatbot UI and State
- The
Chatbot.jsx
file manages user interactions, session data, and calls to Supabase functions. -
ChatMessage.jsx
is responsible for rendering each message.
- The
-
API Calls and Cleanup
- The call to the chatbot function in your Supabase project handles actual message logic.
- If you set up
schedule.yml
, it runs daily and cleans up old sessions so your database stays tidy.
Code Example
import React from "react";
import Chatbot from "./components/Chatbot";
function App() {
return (
<div className="App">
<header>
<h1>My Portfolio</h1>
</header>
<Chatbot />
</div>
);
}
export default App;
Once you run your development server (npm run dev
if you’re using Vite, or npm start
with other setups), the chatbot bubble should appear. Users can open it, ask questions, and see responses from your Supabase Edge Function.
Discussion
-
Modularity:
Each file focuses on a specific job. If you want to change how messages look, modify
ChatMessage.jsx
. If you want to store extra data, updateChatbot.jsx
and your Supabase table. You can even breakChatbot.jsx
into multiple hooks or utility files if you prefer smaller chunks of logic. -
Easy Customization:
- New Features: Add an AI-based language model, integrate project links, or include a feature that reads data from your CMS.
- Styling: Adjust the SCSS to match your brand colors or tweak the Framer Motion transitions for a more dramatic effect.
Additional Features and Enhancements
Here are some ideas I’ve either experimented with or plan to include in the future:
-
Error Handling & Notifications
- Show a pop-up or banner if the chatbot can’t reach Supabase.
- Provide user-friendly hints when they type something off-topic or invalid.
-
Custom Animations
- Tweak the duration and easing in Framer Motion for a unique feel.
- Use drag constraints so the floating bubble only moves within a certain area.
-
Styling Improvements
- Switch to SCSS/CSS modules for component-specific styling.
- Consider adding dark-mode support to give users a choice in theme.
-
Security Considerations
- Keep API keys in a
.env
file or GitHub Actions secrets. - Set up user authentication if you plan on gathering any sensitive info.
- If you store logs or personal data, ensure compliance with privacy regulations.
- Keep API keys in a
Conclusion
I’ve walked you through the entire process of adding a free chatbot to a React-based portfolio using Supabase for storage and Edge Functions. Here’s a quick recap:
- Environment Setup: Install React, set up Vite (or another tool), and configure Supabase.
-
Component Creation: Build
Chatbot.jsx
,ChatMessage.jsx
, and keep your conversation data inresumeContext.js
. - API Integration: Use a Supabase Edge Function for chatbot logic and (optionally) a cleanup function for old sessions.
-
Scheduled Maintenance: Automate cleanup with a GitHub Actions workflow (
schedule.yml
).
Next Steps
- Deploy Your Chatbot: Try hosting on Vercel, Netlify, or any platform that supports environment variables.
- Explore Deeper Customization: Incorporate AI or advanced logic, add real-time alerts, or even connect with project databases to answer specific queries about your portfolio items.
Call to Action
I’d love to see how you implement or improve this chatbot in your own projects. Feel free to reach out, share your results, or ask questions if you hit any snags along the way.
You can also try the live version on my portfolio at www.melvinprince.io to see it in action. If you’re curious about the source code or want a head start on customization, check out my GitHub repository for more details and examples.
Good luck, and have fun bringing your portfolio to life with a helpful, interactive chatbot!
Top comments (13)
I have designed so many websites but never created a portfolio. This blog was very helpful for me, and now I created a portfolio according to your instructions. I also included one of my favorite projects, professional resume writing services in dubai, after creating this portfolio
nice thank for sharing
Wow, this is such an amazing guide! 😄 I love how you've made it easy for anyone, even beginners, to implement a chatbot using React and Supabase. I'm especially excited about how you’ve included animations and personalized user sessions—it makes the experience feel so much more dynamic! 🤩 I have a question: How do you handle complex user queries that may require deeper context or multi-turn conversations? Would love to hear more about that! 🔥
For that you can tune the model from gemini dashboard and then use it for answers...implementing that is also a good option but there are lot more restrictions on doing it.. for something like resume sending a longer chat was more than enough
It looks like you're sharing an article about building a chatbot for a portfolio website using React, Supabase, and Gemini. Are you looking for feedback, help implementing something similar, or do you want to improve the guide in some way? Let me know how I can assist
please do tell if there is something to improve or fix
model: "gemini-1.5-flash"
this is the available model on free tier in Gemini.
you can view models available to you by sending a request
$ curl -X GET "generativelanguage.googleapis.com/..."
Very nice to know they provide this
I have build so many DIscord bot, check out in my Github profile
Cảm ơn vì những gì bạn đã chia sẻ
Fantastic guide! Loved the step-by-step approach for integrating a chatbot into a portfolio. Can’t wait to try it out. Thanks for sharing! hcalcuators.com/
Nice