打造属于你的作品集网站聊天机器人:一步步教你实现(附代码示例)
我一直很喜欢探索让作品集网站更互动和实用的方法。这就是我决定创建一个自定义聊天机器人的原因,不仅展示我在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,那就没问题了。别担心——每个构建工具处理环境变量和模块导入的方式都差不多。通常我的 Vite 设置在 package.json
文件里是这样的:
{
"scripts": {
"dev": "开发",
"build": "构建",
"preview": "预览"
}
}
进入全屏模式,退出全屏模式
但如果你的设置不同(比如使用 Create React App 或 Next.js),你的脚本可能有点不一样。重要的是确保你能启动开发服务器,这样你就能在后面测试聊天机器人小助手了。
此处为空
步骤 3:添加环境变量
无论你用的是什么构建工具,你需要环境配置来存储你的 Supabase URL 和 API 密钥。这会是这样的,如果你用的是 Vite:
- 在项目根目录下创建一个
.env
或.env.local
文件。 - 例如,添加您的 Supabase API密钥和URL等必要信息。
VITE_SUPABASE_URL=https://your-supabase-url.supabase.co
VITE_SUPABASE_KEY=你的 Supabase 密钥 // 使用匿名或公共密钥。
/* ⚠️ 注意:`VITE_SUPABASE_KEY` 是一个公开密钥,在前端代码中使用是安全的,不过
暴露你的 **服务角色密钥**,因为它能访问整个数据库。*/
- 在代码中引用它们一下:
// 如果没有使用 Vite,可以使用 `process.env.name_of_key`
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
const supabaseKey = import.meta.env.VITE_SUPABASE_KEY;
如果你使用的是不同的开发环境,概念仍然相同——只需根据你的构建系统偏好调整相应的语法和文件命名规则。
……
步骤 4:确认您的设置一切正常
一旦你安装并配置好所有内容,确保你的现有项目或现有代码库仍能够正常构建。试试运行你平时的开发命令,比如:
`运行
npm run dev` 来启动开发模式的服务器。```
全屏模式 / 退出全屏
(如果你在Vite上)或
在命令行输入 npm start
启动应用
进入全屏 退出全屏
(如果你使用的是 Create React App 开发环境)。这可以确保聊天机器人的库没有与现有代码冲突。如果你能在浏览器中正常加载你的作品集,你就可以开始添加聊天机器人的实际组件了。
这种方法保持了全局性和灵活性的特点。你不需要重写整个代码库来集成聊天机器人——我只是用Vite的方式来做这件事。接下来,我将解释如何将聊天机器人的逻辑与Supabase连接起来,以及如何添加有动画效果的界面和维护每个会话。
文件组织及详细说明一开始构建一个聊天机器人听起来可能很复杂,但一旦将其拆分为更小的部分,就会变得简单得多。在这一部分,我将向你展示一个简单的、简易版聊天机器人,你可以直接运行并调整它。我将解释每个文件如何构成更大的图景,包括如何将主 Chatbot.jsx
文件拆分成多个较小的组件,如果你觉得这样更易于管理的话。通过接下来的五个部分,你将拥有一个完全功能的聊天机器人,准备进行自定义设置。
resumeContext.js
文件 resumeContext.js
我通常用来存放聊天机器人需要的所有硬编码文本和规则。可以把它想象成机器人的参考手册。比如一个作品集聊天机器人,它可能包含你的背景信息、你想要的对话风格,甚至是关于聊天机器人能回答什么和不能回答什么的免责声明。这样所有信息都集中在一个地方,以后更新时就不用在多个文件中翻找了。
这里有一个测试版的文件,只保留了几个关键部分:
// resumeContext.js - 根据您的需求自定义此文件
const resumeContext = `
[对话说明]
- 只回答与我的作品集或经验相关的问题。
- 如果用户询问的是其他主题,请回复:
"关于我的作品集或项目的问题,我随时准备回答。如果问题无关,请尝试提问其他相关问题。"
[简历信息]
- 姓名:简·多伊
- 专长:前端开发
- 主要作品:作品集网站、React天气应用、电子商务商店
[其他注意事项]
- 鼓励用户查看我的其他作品。
- 始终保持友好和乐于助人。
`;
export default resumeContext;
点击这里进入全屏,点击这里退出全屏
这就对了,它是这么运作的:
- 对话指导: 这部分告诉聊天机器人如何表现,让它知道哪些问题可以回答或不回答。
- 简历信息: 您希望聊天机器人分享的任何个人信息,如您的姓名、技能或项目亮点或成就。
- 其他信息: 一个部分,用于您希望聊天机器人记住的其他信息。
当你最终将聊天机器人逻辑连接到这个文件时,机器人就可以访问 resumeContext.js
中的所有内容了。你可以随意修改部分名称或重新组织它们以适应自己的需求。重要的是把所有的静态内容都统一管理,方便以后更新。
Chatbot.jsx
Chatbot.jsx
文件是聊天机器人的核心,负责从会话管理到无缝衔接的所有内容。下面是一个包含你在我的原始代码中看到的逻辑的示例,并附有解释性注释。如果你觉得这个文件太大了,可以将其功能拆分到单独的文件中,例如将会话管理或动画逻辑分别放到不同的文件中,以保持条理清晰。
// Chatbot.jsx
import { AnimatePresence, motion } from "framer-motion";
import { useEffect, useRef, useState } from "react";
import resumeContext from "../data/resumeContext";
// ^ 这是在第2部分中创建的文件,包含聊天机器人的指令或简历信息
import { supabase } from "../supabaseClient";
// ^ 请用您的Supabase客户端配置替换此处,或移除此行,如果您偏好使用其他数据存储
import ChatMessage from "./ChatMessage";
// ^ 我们将在下一节中讲解ChatMessage组件
import "./styles/chatbot.scss";
// ^ 示例样式表;名称和位置可能有所不同
export default function Chatbot() {
// ----------- 1) 状态变量 -----------
// open: 切换聊天机器人的显示/隐藏
// messages: 存储对话历史
// input: 跟踪输入框中的内容
// loading: 机器人回复等待期间设置为true
// sessionId: 用于标识每个用户会话的标识符
// showFloating: 切换浮动气泡的显示
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);
// 我们将使用这些引用实现滚动效果
const messagesContainerRef = useRef(null);
const headerRef = useRef(null);
// ----------- 2) 延时隐藏浮动气泡 -----------
useEffect(() => {
const timer = setTimeout(() => {
setShowFloating(false);
}, 5000); // 5秒
return () => clearTimeout(timer);
}, []);
// ----------- 3) 生成或检索会话ID -----------
useEffect(() => {
const existingSession = localStorage.getItem("chat_session_id");
if (existingSession) {
setSessionId(existingSession);
} else {
createNewSession();
}
}, []);
// ----------- 4) 知道会话ID后获取聊天消息 -----------
useEffect(() => {
if (sessionId) {
fetchSessionMessages();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sessionId]);
// 辅助函数,用于在Supabase中创建新的会话
const createNewSession = async () => {
const newSessionId = crypto.randomUUID();
localStorage.setItem("chat_session_id", newSessionId);
setSessionId(newSessionId);
// 在“chat_sessions”表中添加一条新记录(示例名称)
await supabase.from("chat_sessions").insert([
{
session_id: newSessionId,
messages: [],
resume_context: resumeContext,
// 如果需要引用上下文,可以将其存储在数据库中
},
]);
};
// 辅助函数,用于获取当前会话的现有消息
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 {
// 如果有任何问题,则显示错误消息
setMessages([
{
role: "bot",
text: "⚠️ 获取之前的消息时出错。请稍后再试。",
},
]);
}
};
// 每当新消息添加时,更新Supabase中的消息数组
const updateSessionMessages = async (updatedMessages) => {
await supabase
.from("chat_sessions")
.update({ messages: updatedMessages })
.eq("session_id", sessionId);
};
// ----------- 5) 切换聊天窗口 -----------
const toggleChat = () => {
setOpen((prev) => !prev);
// 如果聊天窗口首次打开,添加一条欢迎消息
if (!open && messages.length === 0) {
const welcomeMessage = {
role: "bot",
text: "👋 你好!有关我的项目或背景的问题都可以问我哦。",
};
setMessages([welcomeMessage]);
if (sessionId) updateSessionMessages([welcomeMessage]);
}
};
// ----------- 6) 发送消息 -----------
const sendMessage = async () => {
// 不发送空消息
if (!input.trim()) return;
// 6a) 将用户消息添加到本地状态
const userMessage = { role: "user", text: input };
const updatedMessages = [...messages, userMessage];
setMessages(updatedMessages);
setLoading(true);
// 6b) 更新数据库以存储消息
if (sessionId) {
await updateSessionMessages(updatedMessages);
}
try {
// 6c) 将用户文本和上下文发送到聊天机器人函数
// 用您的无服务器函数或聊天端点替换URL
const response = await fetch("https://your-supabase-url.functions.supabase.co/chatbot", {
method: "POST",
headers: {
"Content-Type": "application/json",
// 示例:使用环境变量进行认证(如果需要)
Authorization: `Bearer ${import.meta.env.VITE_SUPABASE_KEY}`,
},
body: JSON.stringify({ message: input, context: resumeContext }),
});
if (!response.ok) {
throw new Error(`HTTP 错误!状态码:${response.status}`);
}
// 从无服务器函数期望接收 { reply: "some text" }
const data = await response.json();
if (!data.reply) {
throw new Error("从聊天机器人API接收无效响应。");
}
// 6d) 将机器人的回复添加到本地状态
const botMessage = { role: "bot", text: data.reply };
const finalMessages = [...updatedMessages, botMessage];
setMessages(finalMessages);
// 6e) 再次更新数据库以存储机器人的回复
if (sessionId) {
await updateSessionMessages(finalMessages);
}
// 发送后清除输入框
setInput("");
} catch (error) {
// 6f) 如果有任何错误,显示备用消息
const errorMessage = {
role: "bot",
text: "⚠️ 获取回复时出错啦。请稍后再试哦。",
};
setMessages((prev) => [...prev, errorMessage]);
if (sessionId) {
await updateSessionMessages([...updatedMessages, errorMessage]);
}
}
// 结束加载
setLoading(false);
};
// ----------- 7) 滚动到最新消息 -----------
useEffect(() => {
if (!messagesContainerRef.current || !headerRef.current) return;
if (open) {
// 等待一会儿,以便新消息能渲染
setTimeout(() => {
const container = messagesContainerRef.current;
const headerHeight = headerRef.current.offsetHeight;
container.scrollTo({
top: container.scrollHeight - headerHeight,
behavior: "smooth",
});
}, 100);
}
}, [messages, loading, open]);
// ----------- 8) 渲染聊天界面 -----------
return (
<>
{/* 最小化聊天气泡 */}
<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>嗨!与我聊天</span>
</motion.div>
)}
<div className="chatbot-avatar">
<img class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="/chatbot.svg" alt="聊天头像" />
</div>
</motion.div>
)}
</AnimatePresence>
{/* 展开的聊天界面 */}
<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 }}
>
{/* 聊天头部 */}
<div className="chatbot-header" ref={headerRef}>
<h3>聊天</h3>
<button className="chatbot-btn" onClick={() => setOpen(false)}>
关闭
</button>
</div>
{/* 消息部分 */}
<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>
{/* 输入部分 */}
<div className="chatbot-input">
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") sendMessage();
}}
placeholder="输入您的问题..."
/>
<button
className="chatbot-btn chatbot-send"
onClick={sendMessage}
disabled={loading}
>
发送
</button>
</div>
</motion.div>
)}
</AnimatePresence>
</>
);
}
切换到全屏 退出全屏
此处省略
将 Chatbot.jsx
分成更小的部分
- 会话管理
如果你觉得会话逻辑太复杂或太繁琐,可以把所有与会话相关的函数(如 createNewSession
、fetchSessionMessages
和 updateSessionMessages
)移到一个自定义钩子文件里,比如 useChatSession.js
。
- API 调用
你可以将调用无服务器聊天机器人的端点(sendMessage
)的函数放到一个专用的工具文件中,如果你想将关注点分离开的话。
- 动画及UI.
如果你想要将UI代码和数据代码分开,可以考虑将Framer Motion组件迁移到一个专门设计的UI组件中,该组件可以从主聊天程序接收props。
此处省略内容(*)
解释与重点
- 会话状态:通过在
localStorage
中保存会话 ID,我确保用户刷新页面时不会丢失聊天记录。 - Supabase 集成:我使用 Supabase 将每次对话存储在简单的表格中。如果您愿意,可以将其替换为任何数据库,甚至是简单的服务器文件。
- 动画:Framer Motion 的过渡让聊天机器人看起来更简洁。
- 滚动到视图:通过引用消息容器和标题部分,我可以自动将滚动位置调整到每次发生变化时的最新消息或加载指示器。
这个简易聊天机器人应该可以按原样工作,只要你正确设置了你的环境(如Supabase)。你可以用这个作为起点,来创建真正代表你风格或需求的聊天机器人。在下面的部分,我将解释支持组件以及可选的清理步骤,以保持你的会话有条不紊。
聊天消息组件 (ChatMessage.jsx
)
ChatMessage.jsx
文件负责以干净、一致的方式渲染每个单独的聊天消息。它决定了文本的显示和是否显示加载动画,并根据消息来源(用户或机器人)来设置不同的样式。以下是文件的一个示例代码,它反映了我自己的设置中的基本要素。
// ChatMessage.jsx
import React, { forwardRef } from "react";
import ReactMarkdown from "react-markdown";
import { motion } from "framer-motion";
// 可选的加载动画组件
function ChatbotLoadingAnimation() {
return (
<div className="loading-dots">
<span>.</span>
<span>.</span>
<span>.</span>
</div>
);
}
// 这里使用了 forwardRef(这其实也是可选的),如果不需要引用滚动或过渡,普通的组件也完全可以。
const ChatMessage = forwardRef(({ message, role, loading }, ref) => {
// 这是一个简单的检查,用于错误样式(例如,如果文本中包含警告表情符)。
// 这是可选的,如果不需要为错误设置特殊样式,可以直接省略。
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 }}
>
{/* 显示一个可选的小头像 */}
{role === "bot" && (
<div className="message-avatar">
<img class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="/chat-avatar.png" alt="Bot头像" />
</div>
)}
<div className="message-bubble">
{/* 如果还在加载中,显示加载动画。否则,渲染实际的文本。 */}
{loading ? (
<ChatbotLoadingAnimation />
) : (
<ReactMarkdown>{message}</ReactMarkdown>
)}
</div>
</motion.div>
);
});
export default ChatMessage;
点击这里进入全屏,点击这里退出全屏
此处省略内容
这个文件是怎么工作的
- 基于角色的样式:
-
如果消息来自用户 (
role === "user"
),我可能会使用不同的背景颜色或文本对齐方式。 -
如果消息来自机器人 (
role === "bot"
),我可以显示头像或为消息气泡使用不同的样式。- Markdown 支持:
-
使用 React Markdown 可以轻松格式化文本。如果机器人返回了 Markdown 语法(比如加粗文本或项目符号),消息气泡会正确显示这些格式。
- 加载动画:
-
在机器人回应到来之前,我可以显示一个占位符动画。这可以让用户感觉有事情正在发生,而不是看到空白区域。
- ForwardRef(可选):
- 我添加了
forwardRef
以防您需要通过传递 ref 来实现滚动行为或高级动画。如果您不需要,可以自由删除它,然后换成标准组件定义。
(注:此处应直接翻译为“***”,无需添加任何额外说明。)
简化后:
拆分功能
如果你喜欢小文件
- 让加载动画成为一个独立的组件: 将
ChatbotLoadingAnimation
移到自己的文件(例如ChatbotLoadingAnimation.jsx
)以避免代码混乱。 - 错误处理: 可以创建一个高阶组件或自定义钩子来检查错误信息并处理相应的错误样式。
…
要点总结
- 最小化、专注于显示逻辑的组件: 通过将
ChatMessage.jsx
限制为仅包含显示逻辑,确保可以轻松调整聊天消息的外观,而无需改动Chatbot.jsx
中的整体逻辑。 - Markdown 集成: 通过返回包含
斜体*
或*粗体**
格式的字符串,轻松为聊天机器人提供更富表现力的响应。 - 动画和错误状态: 使用 Framer Motion 可以为消息添加动画效果。如果需要突出显示错误,可以通过条件类和样式规则来突出显示错误。
在接下来的部分中,我将解释如何安排定期清理以清理旧聊天会话,如果你使用数据库的话。这一步不是必须的,但在你预计会有大量访客并且希望数据保持整洁的情况下,这会非常实用。
schedule.yml
(可选清理任务)
有些人可能会发现随着时间的推移,对话量可能会变得非常大,尤其是当很多访客都在测试我们的聊天机器人时。如果这样,你可能希望定期清理数据库中的旧聊天记录。这时,schedule.yml
文件就派上用场了。它利用 GitHub Actions 每天(或你自定义的时间表)自动执行一次清理。
访问GitHub → 打开项目仓库 → 点击操作选项卡 → 创建自定义工作流 → 粘贴以下内容
name: 清理会话
on:
schedule:
- cron: '0 3 ……
此处省略部分内容 或 此处省略内容 (chǔcǐ shěnglüè nèiróng)' # 每天 UTC 时间 3 点运行,即每天凌晨 3 点
jobs:
cleanup:
runs-on: ubuntu-latest
steps:
- name: 调用清理功能
run: curl -X POST "https://your-supabase-url.functions.supabase.co/cleanup-sessions"
全屏,退出全屏
三个星号。
它如何工作
- 每日触发
schedule:
块指示 GitHub Actions 按照预设的时间表运行此作业。在上面的例子中,cron 时间表达式 '0 3 此处省略内容'
表示“在 UTC 时间每天凌晨 3 点运行”。
- 清理:
在 jobs.cleanup.steps
中,我发送一个 POST 请求到一个无服务器函数(例如,一个 Supabase 函数),该函数负责从数据库中删除过期的会话。这有助于保持我的存储整洁,并防止不必要的数据积聚。
- 部署地点:
你可以在你的仓库的 .github/workflows
目录中存放这个文件,接着,GitHub 会自动配置这个操作,这样做更符合中文表达习惯。
调整频率。
如果你只想每周整理一次,或在其他时间间隔的话,可以根据需要修改 cron 任务。例如,'0 0 * * 0'
这会在每周日凌晨运行。
此处省略部分内容
这必须做吗?
- 根本不需要。 如果您的聊天机器人没有大量的用户基础,您可能根本不需要自动清理。您可以手动处理或完全跳过此步骤。
- 如果您预计会有高流量 或希望保持数据库整洁,此计划任务是一个方便的、无需手动干预的自动清理方法。
chat_sessions 表是存储用户消息和会话数据的核心表。这里是一个我常用的 chat_sessions 表结构示例:
去 Supabase 仪表板 → SQL 编辑器页面 → 复制并粘贴此查询 → 点击运行按钮
``
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 SQL 编辑器中创建表
1. **打开您的 Supabase 项目页面**:在您的 [Supabase 仪表盘](https://app.supabase.com/) 中选择您的项目。
2. **在左侧菜单中找到“SQL”选项**:您可以在左侧菜单中找到它。
3. **将上面的 SQL 代码复制并粘贴到编辑器中**:将上面的 SQL 代码复制并粘贴到编辑器中。
4. **点击“运行”按钮来运行查询**:点击“运行”按钮。Supabase 会创建表,如果表不存在的话。
**关键字段:**
* **session_id** : 一个 UUID,用于唯一标识每个聊天会话。
* **messages** : 一个 JSONB(二进制 JSON)字段,用于存储对话内容。
* **resume_context** : 一个可选的文本字段,用于存储您的指令或会话特定信息。
* **created_at** : 自动填充当前时间戳,以便您了解会话创建的时间。
……
### 设置行级安全(RLS,即行级安全规则)
行级安全是一种控制谁可以读写您表中行的方式。默认情况下,Supabase 会禁用新表的行级安全(RLS),这意味着只要拥有有效的 API 密钥,任何人都可以读写这些表。如果您在前端仅使用公共 API 密钥(而不是用户特定的认证),您可能需要一个更简单的安全模型。但是,如果您想提高安全性,您可以这样做:
1. **开启RLS功能**
ALTER TABLE chat_sessions ENABLE ROW LEVEL SECURITY;
启用行级安全设置于聊天会话表中。
2. **制定政策**
具体政策会根据你的使用情况而变化。例如,你可能会允许任何拥有你公共API密钥的人插入和选择数据行(因为你并不存储任何用户的个人信息)。下面给出一个非常宽松的示例:
-- 允许任何人查看 chat_sessions 表中的所有记录
CREATE POLICY "创建策略"
ON chat_sessions
FOR SELECT
TO 公共
USING (true);
-- 允许任何人插入新记录
CREATE POLICY "创建策略"
ON chat_sessions
FOR INSERT
TO 公共
WITH CHECK (TRUE);
如果你想要让它更安全(例如,通过将会话绑定到已验证的用户ID),你需要根据你的需求调整 `USING` 和 `WITH CHECK` 子句。在更复杂的设置中,你可能在表中存储一个 user_id,仅允许用户查看或修改自己的会话。
* * *
### Supabase 设置的额外技巧
1. **服务角色密钥**:如果你使用单独的Edge Function(用于清理或高级逻辑处理),你可能需要服务角色密钥来绕过 RLS 或执行删除操作。
2. **环境变量**:将你的密钥(特别是服务角色密钥)保存在环境变量中。不要在你的前端代码中硬编码它们。
3. **数据库备份**:你可以从 Supabase 控制面板启用自动备份来保护你的数据。
4. **日志和监控**:Supabase 提供了 SQL 查询、函数调用和 API 活动的日志。在调试聊天机器人或RLS策略时,检查这些日志尤为重要。
* * *
### 为什么使用RLS?
* **细粒度控制**:控制谁可以读取或修改每一行数据。
* **更强的安全性**:默认情况下,公钥只能读写你通过策略允许的数据。
* **可扩展性**:随着应用规模的扩大,良好的策略结构有助于避免意外的数据泄露。
* * *
## Supabase 的边缘功能与 Gemini
Supabase Edge Functions 是你用 JavaScript 或 TypeScript 编写的无服务器函数,并可以直接部署到 Supabase 平台上。这意味着什么呢?
* **不需要单独的服务器**。
* **自动伸缩**,可以轻松应对你需要的所有请求。
* **与 Supabase 集成**,方便数据库和认证的访问。
在我们的聊天工具场景中,我们主要依靠这些边缘功能来完成两大重要任务。
1. **聊天机器人消息处理**(`chatbot`函数)。
2. **执行定期清理**(可选的`cleanup-sessions`函数,参考`schedule.yml`文件中的内容)。
以下步骤将指导你如何将 Gemini 集成到 `chatbot` 函数中,以便它可以获取额外数据或执行 AI 处理,同时确保你的 Gemini API 密钥的安全。
* * *
在本地设置 Supabase 边缘功能
在创建或部署 Edge Function 之前,你需要准备 **Supabase CLI** :
1. **安装 Supabase。**
可以运行以下命令来安装supabase:
npm install supabase
2. **登录**:
运行这个命令来登录Supabase:
npx supabase login
请转到你的项目文件夹(你的聊天机器人代码所在的文件夹)。
* * *
在 Supabase 中配置 Gemini API 密钥
由于我们将在 Edge Function(而不是在前端)中使用 Gemini API 的密钥,我们会把它作为环境变量存放在 Supabase 中:
1. **打开你的 Supabase 面板**
访问 [app.supabase.com](https://app.supabase.com/) 然后选择你的 Supabase 项目。
2. **项目设置 → API(或环境变量)**:
在左边的菜单中,查找“项目设置”。在里面,你可以找到一个用于管理你的API和环境变量的部分。
3. **添加一个新变量:**
* 点击“新建密钥”或“新建变量”(具体界面可能因 Supabase 版本不同而有所变化)。
* 将其命名为 `GEMINI_API_KEY`。
* 将您的 Gemini API 密钥粘贴到值框中。
* 完成并保存更改。
> 重要:这会将您的 API 密钥在 Supabase 内部保持安全。从前端调用您边缘函数的任何人都永远无法看到该密钥,这有助于防止未经授权的访问。
* * *
### 创建‘对话机器人’边缘功能
生成函数
npx supabase functions new chatbot
运行此命令来创建一个新的聊天机器人函数
切换到全屏 / 退出全屏
这条命令会创建一个新的文件夹 `supabase/functions/chatbot`,并包含一些样板代码,比如。你会看到一个类似的文件,比如 `index.ts`(如果你选择了 TypeScript),或者 `index.js`(如果你选择了 JavaScript)。
加入 Gemini 逻辑代码:编辑 `index.ts` 文件
接收用户的`message`和`context`,使用秘密API密钥调用Gemini接口,并返回一个结合的回复:
更新后的示例如下:
// supabase/functions/chatbot/index.ts
import { serve } from "https://deno.land/x/sift@0.6.0/mod.ts";
// 从环境变量中获取Gemini的API密钥
const geminiApiKey = Deno.env.get("GEMINI_API_KEY") || "";
/*
此更新的函数:
-
读取传入的JSON中的
message
和context
。 -
调用Gemini API的示例(使用虚拟URL和请求体)。
-
结合您的聊天机器人逻辑和来自Gemini的数据来构建响应。
-
返回{ reply: "some text" }给前端。
*/serve({
async "/": async (req: Request) => {
try {
// 1) 解析来自Chatbot.jsx的数据
const { message, context } = await req.json();// 2) 示例Gemini API调用(使用虚拟URL和请求体)。 // 在您的实际应用中,请将其替换为Gemini的实际端点和请求结构。 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, // 如果context对Gemini相关,请将其包含进来 additionalContext: context, }), }); // 解析Gemini的响应 const geminiData = await geminiResponse.json(); // 3) 结合您的逻辑与Gemini的响应 // 假设geminiData包含一个"answer"字段。 const geminiAnswer = geminiData.answer || "没有特定回答。"; const reply = `您说了:${message}
(Gemini如是说:${geminiAnswer})
(上下文片段:${context.substring(0, 50)}...)`;// 4) 将组合后的回复返回给聊天机器人 return new Response(JSON.stringify({ reply }), { headers: { "Content-Type": "application/json" }, }); } catch (error) { return new Response( JSON.stringify({ reply: "Gemini或我们的逻辑出了些问题。" }), { status: 400, headers: { "Content-Type": "application/json" } } ); }
},
// 您也可以在此定义更多端点(例如,“/health”)
});
全屏(点击进入)/ 退出全屏(点击退出)
**要点:**
* 我们使用[Sift](https://deno.land/x/sift)来处理接收到的HTTP请求。
* **`geminiApiKey`** 通过 `Deno.env.get("GEMINI_API_KEY")` 获取。
* 如果你的Gemini端点返回更多的数据(如 `confidenceScore`,`timestamp` 等),你可以随意将其加入到最终回复中。
发布功能
当你觉得你的逻辑没问题时,
运行``npx supabase functions deploy chatbot``
全屏显示 退出全屏
Supabase 会给你一个像这样的 URL。
https://<YOUR-SUPABASE-PROJECT>.functions.supabase.co/chatbot
进入全屏 退出全屏
* * *
连接到 `Chatbot.jsx`
在你的 **`Chatbot.jsx`** 文件中,找到你调用 API 的位置:
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}`, // 如有必要
},
body: JSON.stringify({ message: input, context: resumeContext }),
});
按一下进入全屏,再按一下退出全屏
将 **`"https://<YOUR-SUPABASE-PROJECT>.functions.supabase.co/chatbot"`** 替换成实际部署后得到的 URL。现在,每当用户输入消息并点击“发送”按钮时,你的边缘函数就会。
1. **接收用户消息** 并接收 `resumeContext`。
2. **使用你的密钥来调用Gemini API**。
3. **结合Gemini的回复和你自己的上下文,构建最终回复**。
4. **将其发送回你的React聊天机器人,以显示回复**。
* * *
确保所有功能正常运行
1. **本地测试:** 如果你使用的是 Vite,则运行 `npm run dev`。打开聊天机器人并输入一条消息。确认你收到了类似 Gemini 的回复。
2. **Supabase 控制台的 日志:** 检查边缘函数是否被调用且没有错误。如果遇到问题,日志会是你的好帮手。
3. **安全性:** 你的 Gemini 密钥绝不会暴露在客户端,因为它存储在 Supabase 的环境变量中。
* * *
最后的思考和下一步行动
* **增强逻辑**:现在有了双子的参与,你可以改进文本解析,处理自定义AI特性,或者在调用时提供更多上下文。
* **错误处理**:如果双子出现问题或返回错误,你可以捕获这些错误并提供备用回复。
* **扩展能力**:随着流量的增长,Supabase会自动扩展边缘函数,因此你不需要额外的基础设施。
通过将 Gemini 集成到您的 Supabase 边缘服务中,您可以保证安全且高效的流程。用户的消息从前端 (`Chatbot.jsx`) 流向您的函数 (`chatbot/index.ts`),该函数在后台与 Gemini 进行通信。这种架构不仅保护您的机密信息,还赋予您将丰富数据和 AI 集成到聊天机器人回复的能力。
* * *
### 创建“清理会话”边缘函数(可选)
你可能还记得那个调用清理功能的 `schedule.yml` 文件。
这个命令是用来清除会话的
curl -X POST "https://your-supabase-url.functions.supabase.co/cleanup-sessions"
进入全屏,退出全屏
如果你想要这行得通,你将需要一个名为 `cleanup-sessions` 的第二个边缘函数,这个函数应该能从数据库中移除旧的聊天记录。
生成清理函数
npx supabase functions new 清理会话功能
注:此命令用于创建一个新的清理会话功能。
进入全屏模式, 退出全屏
编辑 `index.ts`(整理逻辑)
下面是一个简单的例子,用于删除特定天数之前的记录。您可以根据需要进行调整:
```sql
-- 在这里插入您的代码
// 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";
// 这是一个基于Deno的公共方法;对于私有凭据,请考虑在生产环境中使用密钥进行保护。
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 {
// 例如,删除7天前的会话
const sevenDaysAgo = new Date();
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
// 假设你有一个带有 'created_at' 字段的 'chat_sessions' 表
// 这是伪代码—请根据你的数据库模式更改 "created_at" 或 "updated_at"
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: "已成功删除旧会话。" }),
{ headers: { "Content-Type": "application/json" } },
);
} catch (error) {
return new Response(JSON.stringify({ status: "error", error: error.message }), {
status: 400,
headers: { "Content-Type": "application/json" },
});
}
},
});
进入全屏,退出全屏
要点:
- 我们使用一个服务角色密钥来执行删除操作(这个密钥一定要保密)。
- 代码会过滤掉7天前的会话(
sevenDaysAgo.setDate(...)
),但您可以根据实际情况做出调整。 - 确保您的
chat_sessions
表中有一个日期字段(如created_at
),以便这些操作能顺利进行。
启动清理任务
执行清理会话的命令:npx supabase functions deploy cleanup-sessions
全屏模式 退出全屏
就像使用聊天机器人功能一样,你会收到一个网址:
https://<YOUR-SUPABASE-PROJECT>.functions.supabase.co/cleanup-sessions
清理会话的函数 URL
全屏显示 退出全屏
与 schedule.yml
相关联。
回到你的 GitHub 仓库,打开 schedule.yml
文件(注意保持原有格式,位于 .github/workflows/
文件夹中),并确认其与新的函数 URL 匹配。
name: 会话清理
on:
schedule:
- cron: '0 3 * * *' # 每天 UTC 时间凌晨 3 点
jobs:
cleanup:
运行于: ubuntu-latest
清理步骤:
- name: 调用清理函数操作
run: 使用 curl 发送 POST 请求 "https://<YOUR-SUPABASE-PROJECT>.functions.supabase.co/cleanup-sessions"
全屏模式,退出全屏
将这个文件推送到你的 GitHub 仓库。比如,每天凌晨 3 点 UTC,操作会运行,并向你的 cleanup-sessions
函数发送一个 POST 请求,触发清理会话的逻辑。
测试一下你的设置。
试试聊天机器人的功能。
- 浏览器本地测试:
-
在
http://localhost:3000
或http://localhost:5173
(如果你使用的是 Vite。)打开你的应用。 -
发送一条聊天消息。
-
确认你在聊天中收到的回复符合你的边缘函数逻辑。
- Supabase 日志:
- 在 Supabase 控制台 中,检查“日志”或“函数”部分,查看是否有任何关于你的函数的请求没有产生错误。
试试清理功能,测试一下。
- 手动 cURL:
curl -X POST "https://<YOUR-SUPABASE-PROJECT>.functions.supabase.co/cleanup-sessions"
注:此命令用于调用清理会话的函数。请将 <YOUR-SUPABASE-PROJECT>
替换为实际的 Supabase 项目名称。
- 如果一切顺利,你应该会在控制台看到一条成功的消息,并看到你的 `chat_sessions` 表中的旧会话已被移除。
全屏 退出全屏
- 查看 GitHub Actions 选项卡:
-
进入你的仓库 → Actions → “清理会话记录” 查看是否已经安排好,或者你想要手动运行。
-
- *
所有文件的最后概览
这里快速回顾一下所有内容的位置:
resumeContext.js
: 存放聊天机器人静态文本和指令的文件。Chatbot.jsx
: 核心聊天逻辑、状态管理和界面交互。ChatMessage.jsx
: 渲染单条消息,并可选显示加载动画。schedule.yml
: GitHub Actions 工作流,每天执行清理任务。supabase/functions/chatbot/index.ts
:“处理用户消息的 Edge 函数”(示例中采用的是简单的回显逻辑)。supabase/functions/cleanup-sessions/index.ts
:用于从chat_sessions
中移除旧记录的 Edge 函数。
结合时,
- 用户在您的网站上打开聊天。
Chatbot.jsx
将用户的消息和resumeContext.js
文件 发送给 “chatbot” 边缘计算函数。- 边缘计算函数返回回复。
schedule.yml
每天调用一次 “cleanup-sessions” 来清除陈旧的数据。
构建完聊天机器人的每个部分——resumeContext.js
,Chatbot.jsx
,ChatMessage.jsx
,以及可选的schedule.yml
加上Supabase Edge Functions之后,最后一步是将所有这些部分整合到我们主要的应用程序中。以下是一个将聊天机器人集成到典型React应用中的简化集成示例。
整合组件
- 主应用组件(主要组件)
创建或打开你的主组件(通常命名为 App.jsx
)。如果你想的话,也可以将聊天机器人添加到任何其他组件中。关键是导入 Chatbot
并将其插入你希望显示的地方。
- 聊天机器人界面及其状态。
Chatbot.jsx
文件负责处理用户交互、会话数据以及调用 Supabase 函数。-
ChatMessage.jsx
负责显示每条消息。-
API 调用及清理
在你的项目中,聊天机器人功能的调用处理实际的消息。如果你设置了
schedule.yml
,它会每天自动运行,清理旧会话,保持数据库整洁。
-
代码示例
import React from "react";
import Chatbot from "./components/Chatbot";
function App() {
return (
<div className="App">
<header>
<h1>页眉</h1>
<h1>我的作品集</h1>
</header>
<Chatbot />
</div>
);
}
export default App;
全屏模式;退出全屏
一旦你启动了开发服务器(如果是 Vite,则运行 npm run dev
,如果是其他配置,则运行 npm start
),聊天机器人的气泡应该会出现。用户可以打开它,提问并查看由你的 Supabase 边缘函数提供的回答。
�izard �izard �izard 聊聊 �izard �izard �izard �izard �izard �izard �izard �izard �izard �izard �izard �izard �izard �izard �izard �izard �izard �izard �izard �izard �izard �izard �izard �izard �izard �izard �izard �izard �izard �izard �izard �izard �izard �izard �izard �izard �izard �izard
经过仔细考虑专家的建议,最终的翻译如下:
聊聊
- 模块化:
每个文件专注于一个特定的功能。如果你想更改消息的外观,请修改 ChatMessage.jsx
。如果你想存储额外的数据,可以更新 Chatbot.jsx
以及你的 Supabase 表。如果你更喜欢将逻辑拆分成更小的部分,甚至可以将 Chatbot.jsx
分解成多个钩子或工具文件。
-
轻松定制:
- 新功能: 添加基于AI的智能语言模型,添加项目链接功能,或从你的CMS读取数据。
-
样式: 调整SCSS以匹配你的品牌色,或调整Framer Motion的过渡效果以达到更强烈的视觉效果。
-
- *
这里有一些我已经尝试过或打算在未来加入的想法:
- 错误处理及通知
-
如果聊天机器人无法连接到Supabase服务器,显示一个弹出窗口或横幅通知。
-
当用户输入无关或无效的内容时,提供友好的提示。
- 自定义动画
-
在Framer Motion中调整持续时间和缓动效果,以获得独特的体验。
-
使用拖动约束,使漂浮的气泡仅在特定区域内移动。
- 样式改进
-
使用SCSS/CSS模块进行组件特定的样式设置,以提高样式的一致性和可维护性。
-
可以考虑添加暗模式支持,让用户在主题上有选择权。
- 安全考量
-
将API密钥保存在
.env
文件或GitHub Actions秘密中。 -
如果计划收集任何敏感数据,请设置身份验证机制。
-
如果存储日志或个人数据,请确保符合相关隐私法规。
-
- *
我已经向你介绍了整个过程,如何将一个免费的聊天机器人添加到一个基于React的作品集中,使用Supabase作为存储解决方案以及Edge Functions。下面快速回顾一下:
- 环境搭建: 安装 React,设置 Vite(或其他类似的工具),并配置 Supabase。
- 组件创建: 构建
Chatbot.jsx
,ChatMessage.jsx
,并存储对话数据在resumeContext.js
中。 - API 集成: 使用 Supabase Edge Function 处理聊天机器人的逻辑,并(可选地)使用清理函数清理掉旧会话。
- 计划性维护: 使用 GitHub Actions 工作流 (
schedule.yml
) 自动清理。
接下来的步骤
- 部署你的聊天机器人: 尝试在 Vercel、Netlify 或任何支持环境变量的平台(如)上托管。
- 深入自定义: 集成 AI 或高级逻辑功能,添加实时提醒,或者连接到项目数据库,回答有关作品集的特定问题。
发出行动呼吁
我很想看看你如何在自己的项目中实现或改进这个聊天机器人。如果你在过程中遇到任何问题或完成了什么成果,欢迎随时联系我分享或寻求帮助。
你也可以在我的作品集网站www.melvinprince.io上试用在线版本,看看它是如何工作的。如果你对代码感兴趣或想要提前开始自定义,可以访问我的GitHub仓库了解更多详情和示例。
祝你好运,希望你能通过一个互动的聊天机器人,让你的作品集更加生动有趣!
共同学习,写下你的评论
评论加载中...
作者其他优质文章