为个人网站快速集成 AI 聊天机器人
分步教程帮助开发者在作品集网站添加聊天机器人。教程性内容,门槛较低。
分步教程帮助开发者在作品集网站添加聊天机器人。教程性内容,门槛较低。
我一直喜欢探索让投资组合网站更具交互性和实用性的方式。正因如此,我决定创建一个自定义聊天机器人——它不仅展示了我在 React 方面的专业知识,还为访问我网站的任何人提供了实用功能。当有人登陆我的投资组合时,我希望他们能够立即获得关于我的项目、技能和背景的答案,而无需翻阅多个页面。聊天机器人是提供这种体验的简单方法。
我使用 Supabase 和 Gemini 上的免费栈构建了这个聊天机器人,这意味着任何人都可以轻松复制我所做的一切,而不会有任何成本障碍。在接下来的章节中,我将分享分步流程,包括如何设置 Supabase 进行数据存储、使用动画增强聊天界面,以及管理每个用户会话。我的目标是使这份指南直观明了,即使你是 React 新手,你也能够跟上步伐并构建自己的版本。
对于那些想在深入技术细节之前先看看实际效果的人,我在我的网站 www.melvinprince.io 上有一个实时演示。随时尝试一下,看看聊天机器人如何与访问者实时交互。
当我着手构建这个聊天机器人时,我希望确保它能够流畅地处理用户交互、在可靠的数据库中存储消息,并及时传递响应。以下是一切工作方式的基本流程:
用户点击聊天气泡:当有人打开聊天时,会创建或检索一个会话 ID。这个会话帮助我跟踪每个对话,以便我可以随时加载或存储消息。
通过 Supabase 进行消息交换:用户提出的每个问题都会被添加到聊天历史中,然后我处理该消息并将其发送到我的后端函数。返回的响应也会被保存,这使对话保持同步——即使用户刷新页面也不例外。
UI 和动画:我使用 Framer Motion 对浮动气泡和展开的聊天窗口进行动画处理。这增加了抛光外观,使聊天机器人感觉自然而非机械。
清理和维护:我设置了一个日常 cron 作业(使用时间表文件)来清除旧会话,以便我的数据库保持有组织且不会无限扩大。
我依靠 React 作为前端、Supabase 处理数据,以及简单的无服务器函数来处理聊天机器人逻辑。此设置中的所有内容都使用免费或免费层计划,包括 Gemini API 和 Supabase,这对于任何想要复制该项目而不产生托管费用的人来说都是完美的。
从高层来看,这就是架构。在接下来的章节中,我将深入探讨每一部分,确保你拥有为自己的投资组合构建类似内容所需的所有详细信息。
记住,如果你想看看它在实践中的工作方式,可以访问 www.melvinprince.io 的实时版本。
我们中的许多人已经有了一个可用的 React 投资组合,所以没有必要从头开始重建所有内容。在我的情况下,我碰巧使用 Vite 因为它既快又易于设置,但你可以将以下步骤调整到你自己的配置(无论是 Create React App、Next.js 还是另一个 React 框架)。主要思想是安装几个包、连接到 Supabase,并添加聊天机器人组件。
在你现有的 React 项目文件夹中,打开终端并运行:
npm install framer-motion react-markdown @supabase/supabase-js
以下是我选择这些库的简要说明:
framer-motion:处理平滑动画(如浮动气泡和聊天面板过渡)。
react-markdown:允许我以 markdown 格式呈现消息。
@supabase/supabase-js:将我的前端连接到 Supabase,以便我可以管理用户会话并存储消息。
如果你已经在使用 Vite,那么你已经准备好了。如果没有,别担心——每个构建工具都有类似的方法来处理环境变量和模块导入。以下是我的 Vite 设置在 package.json 中通常的样子:
{
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
}
}
但如果你在不同的设置上(如 Create React App 或 Next.js),你的脚本可能会有所不同。重要的是确保你可以运行开发服务器,以便稍后可以测试聊天机器人。
无论你的构建工具如何,你都需要环境变量来存储你的 Supabase URL 和 API 密钥。以下是如果你使用 Vite 会是什么样子:
在项目根目录创建一个 .env 或 .env.local 文件。
添加你的 Supabase 凭据:
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.*/
在你的代码中引用它们:
//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;
如果你使用的是不同的环境,概念保持不变——只需根据你首选的构建系统调整语法和文件命名。
安装和配置所有内容后,确保你现有的投资组合仍然没有错误地构建。运行你通常的开发命令,例如:
npm run dev
(如果你使用 Vite)或
npm start
(如果你使用 Create React App)。这确保聊天机器人库没有造成任何冲突。如果你能像往常一样在浏览器中加载你的投资组合,你已准备好开始添加实际的聊天机器人组件。
这种方法保持事物全局和灵活。你不必重构整个代码库来集成聊天机器人——我只是向你展示我用 Vite 如何做到的。在接下来的章节中,我将解释将你的聊天机器人逻辑链接到 Supabase、添加动画界面,以及维护每个对话会话的具体内容。
构建一个可用的聊天机器人一开始可能听起来很复杂,但一旦你将其分解为更小的部分,它就变得简单得多。在本节中,我将向你展示一个直观的示例聊天机器人,你可以立即调整并运行。我将解释每个文件如何融入更大的框架,包括如果你觉得更易管理,如何将主要的 Chatbot.jsx 文件分解为多个较小的组件。通过这五个部分的结束,你将拥有一个完全可用的聊天机器人,准备好自定义。
resumeContext.js 文件是我喜欢存储聊天机器人需要的任何硬编码文本或规则的地方。你可以将其视为机器人的参考手册。对于投资组合聊天机器人,它可能包含有关你的背景、你想要的对话风格,甚至关于聊天机器人可以和不能回答什么的免责声明的详细信息。通过将所有这些信息保存在一个地方,更新会变得容易,而不必挖掘多个文件。
以下是该文件的虚拟版本,简化为仅几个基本部分:
// 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;
对话指导:这告诉聊天机器人如何表现,所以它知道应该回答或拒绝哪些问题。
简历信息:你想让聊天机器人分享的任何个人详细信息——如你的名字、技能集或项目亮点。
附加说明:一个包罗万象的部分,用于任何其他你可能希望聊天机器人牢记的内容。
当你最终将你的聊天机器人逻辑连接到此文件时,机器人将能够访问 resumeContext.js 中的所有内容。随时重新命名或重新组织这些部分以适应你自己的需求。重要的是将所有静态文本保存在一个地方,以便以后更新时容易找到。
Chatbot.jsx 文件是聊天机器人的核心,负责处理从会话管理到流畅过渡动画的所有工作。下面是一个示例,其中包含了你在我的原始代码中看到的逻辑,并附有解释性注释。如果你觉得这个文件太大,可以将其中的函数拆分成更小的组件,例如将会话管理或动画逻辑单独放到一个文件中,以便保持代码井然有序。
// 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 Messag
如果你觉得会话逻辑过于臃肿,可以将所有与会话相关的函数(例如 createNewSession、fetchSessionMessages 和 updateSessionMessages)移到一个自定义 Hook 文件中,例如 useChatSession.js。
如果你觉得会话逻辑过于臃肿,可以将所有与会话相关的函数(例如 createNewSession、fetchSessionMessages 和 updateSessionMessages)移到一个自定义 Hook 文件中,例如 useChatSession.js。
调用无服务器聊天机器人端点的函数(sendMessage)也可以放在专门的工具文件中,以便实现关注点分离。
调用无服务器聊天机器人端点的函数(sendMessage)也可以放在专门的工具文件中,以便实现关注点分离。
如果你希望将 UI 代码与数据代码分开,可以考虑把 Framer Motion 组件迁移到一个专门的 UI 组件中,由聊天机器人主组件向其传递 props。
如果你希望将 UI 代码与数据代码分开,可以考虑把 Framer Motion 组件迁移到一个专门的 UI 组件中,由聊天机器人主组件向其传递 props。
会话状态:通过将会话 ID 存储在 localStorage 中,我可以确保用户刷新页面时不会丢失聊天记录。
Supabase 集成:我使用 Supabase 将每段对话存储在一个简单的表中。如果你愿意,也可以将其替换为任何数据库,甚至是一个简单的服务器文件。
动画:Framer Motion 过渡效果让聊天机器人的外观更加简洁流畅。
滚动至可见区域:通过引用消息容器和标题,每当内容发生变化时,我都可以自动滚动到最新消息或加载指示器的位置。
只要正确设置其余环境(例如 Supabase),这个示例聊天机器人应该可以直接运行。你可以将它作为起点,创建一个真正符合个人风格或作品集需求的聊天机器人。在接下来的章节中,我将介绍配套组件,以及用于保持会话井然有序的可选清理流程。
ChatMessage.jsx 文件负责以简洁、一致的方式渲染每一条聊天消息。它决定文本的显示方式、是否显示加载动画,以及如何为用户消息和机器人消息设置不同的样式。下面是该文件的一个示例版本,其中复现了我自己的配置中最核心的部分。
// 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;
基于角色的样式:如果消息来自用户(role === "user"),我可能会使用不同的背景颜色或文本对齐方式。如果消息来自机器人(role === "bot"),则可以显示头像,或者为消息气泡设置不同的样式。
如果消息来自用户(role === "user"),我可能会使用不同的背景颜色或文本对齐方式。
如果消息来自机器人(role === "bot"),则可以显示头像,或者为消息气泡设置不同的样式。
Markdown 支持:使用 React Markdown 可以让我轻松设置文本格式。如果机器人返回 Markdown 语法(例如粗体文本或项目符号列表),它就能在消息气泡中正确渲染。
使用 React Markdown 可以让我轻松设置文本格式。如果机器人返回 Markdown 语法(例如粗体文本或项目符号列表),它就能在消息气泡中正确渲染。
加载动画:在机器人的响应到达之前,我可以显示一个占位动画。这能让用户感觉到系统正在处理,而不是面对一片空白。
在机器人的响应到达之前,我可以显示一个占位动画。这能让用户感觉到系统正在处理,而不是面对一片空白。
ForwardRef(可选):我添加了 forwardRef,以便你在需要时向下传递 ref,用于实现滚动行为或高级动画。如果不需要,可以直接将其移除,并改用标准的组件定义。
我添加了 forwardRef,以便你在需要时向下传递 ref,用于实现滚动行为或高级动画。如果不需要,可以直接将其移除,并改用标准的组件定义。
如果你更喜欢使用较小的文件:
将加载动画拆分为单独的组件:把 ChatbotLoadingAnimation 移到它自己的文件中(例如 ChatbotLoadingAnimation.jsx),以避免代码杂乱。
错误处理:你可以创建一个高阶组件或自定义 Hook,用来检查错误消息并应用 error 类。
精简且职责明确的组件:让 ChatMessage.jsx 仅负责显示逻辑,可以确保你轻松调整聊天消息的外观,而无需修改 Chatbot.jsx 中聊天机器人的整体逻辑。
Markdown 集成:这是让聊天机器人给出更丰富响应的一种快捷方式——只需返回包含斜体或粗体**等语法的字符串即可。
动画和错误状态:Framer Motion 可以为消息的出现方式添加动画。如果需要突出显示错误,可以通过条件类和样式规则来实现。
下一部分中,我将介绍在使用数据库的情况下,如何安排定期清理任务来删除旧的聊天会话。这一步并非必需,但如果你预计会有大量访问者,并且希望保持数据整洁,它会非常实用。
随着时间推移,我们中的一些人可能会遇到大量对话,尤其是在许多访问者都来测试聊天机器人的情况下。如果出现这种情况,你可能需要定期从数据库中删除旧的聊天会话。这正是 schedule.yml 的用途。这个文件使用 GitHub Actions 每天执行一次自动清理任务(你也可以定义其他调度周期)。
前往 GitHub → 打开项目仓库 → Actions 选项卡 → 设置自定义工作流 → 粘贴以下内容
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"
每日触发:schedule: 块告诉 GitHub Actions 按设定的时间表运行此任务。在上面的示例中,cron 表达式 '0 3 * * *' 表示“每天 UTC 时间凌晨 3 点运行”。
schedule: 块告诉 GitHub Actions 按设定的时间表运行此任务。在上面的示例中,cron 表达式 '0 3 * * *' 表示“每天 UTC 时间凌晨 3 点运行”。
清理步骤:在 jobs.cleanup.steps 中,我向一个无服务器函数(例如 Supabase 函数)发起 POST 请求,该函数负责从数据库中删除过期会话。这可以精简存储空间,并防止杂乱数据不断累积。
在 jobs.cleanup.steps 中,我向一个无服务器函数(例如 Supabase 函数)发起 POST 请求,该函数负责从数据库中删除过期会话。这可以精简存储空间,并防止杂乱数据不断累积。
部署位置:将此文件存放在仓库中的 .github/workflows 目录内。随后,GitHub 会自动设置该 Action。
将此文件存放在仓库中的 .github/workflows 目录内。随后,GitHub 会自动设置该 Action。
调整频率:如果你只想每周清理一次,或者希望使用其他时间间隔,可以相应地修改 cron 字符串。例如,'0 0 * * 0' 会在每周日午夜运行。
调整频率:
如果你只想每周清理一次,或者希望使用其他时间间隔,可以相应地修改 cron 字符串。例如,'0 0 * * 0' 会在每周日午夜运行。
完全没必要。如果你的聊天机器人没有庞大的用户群体,可能并不需要自动清理。你随时可以手动处理,也可以完全跳过这一步。
如果你确实预计会有较高的流量,或者希望确保数据库保持整洁,那么这个定时任务就是一种方便且无需人工干预的方案。
chat_sessions 表是我存储用户消息和会话数据的核心。以下是我使用的一个示例表结构:
前往 Supabase Dashboard → SQL Editor → 粘贴此查询 → 单击“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()
);
打开你的 Supabase 项目:在 Supabase Dashboard 中选择你的项目。
单击“SQL”:你可以在左侧菜单中找到它。
粘贴 CREATE TABLE 查询:将上面的 SQL 代码片段复制并粘贴到编辑器中。
运行查询:单击“Run”按钮。如果该表尚不存在,Supabase 就会创建它。
session_id:用于唯一标识每个聊天会话的 UUID。
messages:用于保存对话数组的 JSONB(二进制 JSON)字段。
resume_context:用于存储你的指令或其他会话特定上下文的可选文本字段。
created_at:自动填充当前时间戳,以便你了解会话的创建时间。
行级安全性是一种控制哪些人能够读取和写入表中数据行的方式。默认情况下,Supabase 会对新表禁用 RLS,这意味着任何拥有有效 API 密钥的人都可以进行读写。如果你只是使