文章目录
- 前言
- 一、demo演示
- 二、node.js 使用步骤
- 1.引入库
- 2.引入包
- 前端HTML调用接口和UI
- 所有文件
- 总结
前言
关注博主,学习每天一个小demo 今天是Ai对话网站
又到了每天一个小demo的时候咯,前面我写了多人实时对话demo、和视频转换demo,今天我来使用 node.js + html 调用chatGpt Api实现一个Ai 流式对话小demo,当然现在主流的各种APi调用方式一致,你也可以接入deepseek,或者第三方的接口,效果一样
一、demo演示
下面是一个简单的demo演示,并且是支持流式的
二、node.js 使用步骤
1.引入库
代码如下:
先初始化一个package.js 文件。
npm init -y
我们需要安装 express、cors、openAi、dotenv三方库
{
"name": "web",
"version": "1.0.0",
"main": "server.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node server.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"axios": "^1.7.9",
"cors": "^2.8.5",
"dotenv": "^16.0.3",
"express": "^4.18.2",
"openai": "^4.0.0"
}
}
2.引入包
创建server.js 引入我们的第三方包 编写接口逻辑
**特别关注:
- baseURL: 这里大家可以替换其他的APi 比如deepseek(好像目前不能充值了),或者一些第三方的API地址。
- apiKey: 这里大家把key可以直接填写上,我这里为了学习知识,新建了一个.env 文件。**
const express = require('express');
const cors = require('cors');
const OpenAI = require('openai');
require('dotenv').config();
const app = express();
app.use(express.json());
app.use(cors());
// 初始化OpenAI客户端
const openai = new OpenAI({
apiKey: process.env.DEEPSEEK_API_KEY,
baseURL: "https://api.openai.com/v1" // 使用OpenAI官方API地址 这里可以可以使用别的Api地址
// 比如 deepseek 或者其他的第三方的一些
// baseURL: "https://api.deepseek.com/v1"
});
let conversationHistory = [];
app.post('/chat', async (req, res) => {
try {
const { message } = req.body;
// 设置响应头,支持流式输出
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.setHeader('Access-Control-Allow-Origin', '*'); // 添加CORS支持
// 添加用户消息到历史记录
conversationHistory.push({ role: "user", content: message });
const stream = await openai.chat.completions.create({
model: "gpt-3.5-turbo",
messages: [
{ role: "system", content: "You are a helpful assistant." },
...conversationHistory
],
temperature: 0.7,
max_tokens: 1000,
stream: true,
});
let fullResponse = '';
// 确保每次写入后立即刷新缓冲区
for await (const chunk of stream) {
const content = chunk.choices[0]?.delta?.content || '';
if (content) {
fullResponse += content;
const dataToSend = JSON.stringify({ content });
console.log('Sending to client:', dataToSend);
res.write(`data: ${dataToSend}\n\n`);
// 强制刷新缓冲区
if (res.flush) {
res.flush();
}
}
}
// 将完整回复添加到对话历史
conversationHistory.push({ role: "assistant", content: fullResponse });
if (conversationHistory.length > 10) {
conversationHistory = conversationHistory.slice(-10);
}
res.write('data: [DONE]\n\n');
if (res.flush) {
res.flush();
}
res.end();
} catch (error) {
console.error('Error:', error);
res.write(`data: ${JSON.stringify({ error: '服务器错误' })}\n\n`);
res.end();
}
});
const PORT = 3000;
app.listen(PORT, () => {
console.log(`服务器运行在端口 ${PORT}`);
});
在根目录执行 node server.js 启动服务
前端HTML调用接口和UI
不墨迹哈,直接把所有代码贴过来 大家好直接研究代码,我就不用一行一行解读了,没啥东西,难处就是对流式数据的一个处理
特别注意, 不要直接点击html打开页面,在vscode里面安装扩展Live Server,然后点击右下角 Go live启动一个微服务。
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ChatGPT 聊天</title>
<style>
#chat-container {
width: 80%;
max-width: 800px;
margin: 20px auto;
padding: 20px;
border: 1px solid #ccc;
border-radius: 5px;
}
#chat-messages {
height: 400px;
overflow-y: auto;
margin-bottom: 20px;
padding: 10px;
border: 1px solid #eee;
}
.message {
margin: 10px 0;
padding: 10px;
border-radius: 5px;
}
.user-message {
background-color: #e3f2fd;
margin-left: 20%;
}
.bot-message {
background-color: #f5f5f5;
margin-right: 20%;
}
#message-form {
display: flex;
gap: 10px;
}
#message-input {
flex: 1;
padding: 8px;
}
.typing {
opacity: 0.5;
}
.cursor {
display: inline-block;
width: 2px;
height: 15px;
background: #000;
margin-left: 2px;
animation: blink 1s infinite;
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
</style>
</head>
<body>
<div id="chat-container">
<div id="chat-messages"></div>
<form id="message-form">
<input type="text" id="message-input" placeholder="输入消息..." required>
<button type="submit" id="submit-btn">发送</button>
</form>
</div>
<script>
const messageForm = document.getElementById('message-form');
const messageInput = document.getElementById('message-input');
const chatMessages = document.getElementById('chat-messages');
const submitBtn = document.getElementById('submit-btn');
messageForm.addEventListener('submit', async (e) => {
e.preventDefault();
const message = messageInput.value.trim();
if (!message) return;
// 禁用输入和发送按钮
messageInput.disabled = true;
submitBtn.disabled = true;
// 显示用户消息
addMessage(message, 'user');
messageInput.value = '';
// 创建机器人回复的消息框
const botMessageDiv = document.createElement('div');
botMessageDiv.className = 'message bot-message typing';
chatMessages.appendChild(botMessageDiv);
// 添加光标
const cursor = document.createElement('span');
cursor.className = 'cursor';
botMessageDiv.appendChild(cursor);
try {
const response = await fetch('http://localhost:3000/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'text/event-stream'
},
body: JSON.stringify({ message })
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let botResponse = '';
let buffer = '';
try {
while (true) {
const { value, done } = await reader.read();
// 如果流结束了,就退出循环
if (done) {
console.log('Stream complete');
break;
}
// 确保有值才处理
if (value) {
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
// 保留最后一个不完整的行
buffer = lines.pop() || '';
for (const line of lines) {
if (line.trim() === '') continue;
if (!line.startsWith('data: ')) continue;
const data = line.slice(6).trim();
if (data === '[DONE]') {
botMessageDiv.classList.remove('typing');
cursor.remove();
continue;
}
try {
const parsedData = JSON.parse(data);
if (parsedData.content) {
botResponse += parsedData.content;
botMessageDiv.textContent = botResponse;
botMessageDiv.appendChild(cursor);
chatMessages.scrollTop = chatMessages.scrollHeight;
}
} catch (e) {
console.error('JSON解析错误:', e, 'Raw data:', data);
}
}
}
}
// 处理最后可能残留的数据
if (buffer.trim()) {
const finalText = decoder.decode(); // 完成流的解码
if (finalText) {
buffer += finalText;
const lines = buffer.split('\n');
for (const line of lines) {
if (line.trim() === '' || !line.startsWith('data: ')) continue;
const data = line.slice(6).trim();
if (data === '[DONE]') continue;
try {
const parsedData = JSON.parse(data);
if (parsedData.content) {
botResponse += parsedData.content;
botMessageDiv.textContent = botResponse;
}
} catch (e) {
console.error('最终解析错误:', e, 'Raw data:', data);
}
}
}
}
} catch (streamError) {
console.error('Stream processing error:', streamError);
throw streamError;
}
} catch (error) {
console.error('Error:', error);
botMessageDiv.textContent = '抱歉,发生错误。';
botMessageDiv.classList.remove('typing');
cursor.remove();
} finally {
messageInput.disabled = false;
submitBtn.disabled = false;
messageInput.focus();
}
});
function addMessage(text, sender) {
const messageDiv = document.createElement('div');
messageDiv.className = `message ${sender}-message`;
messageDiv.textContent = text;
chatMessages.appendChild(messageDiv);
chatMessages.scrollTop = chatMessages.scrollHeight;
}
</script>
</body>
</html>
所有文件
- .env 存储了APi-key
- index.html 前端代码
- server.js 后段代码
总结
尽可能的多学习一些知识,或许以后用不到,关注我每天练习一个小demo。