node.js + html调用ChatGPTApi实现Ai网站demo(带源码)

文章目录


前言

关注博主,学习每天一个小demo 今天是Ai对话网站

又到了每天一个小demo的时候咯,前面我写了多人实时对话demo、和视频转换demo,今天我来使用 node.js + html 调用chatGpt Api实现一个Ai 流式对话小demo,当然现在主流的各种APi调用方式一致,你也可以接入deepseek,或者第三方的接口,效果一样


一、demo演示

下面是一个简单的demo演示,并且是支持流式的

二、node.js 使用步骤

1.引入库

代码如下:

先初始化一个package.js 文件。

c 复制代码
npm init -y

我们需要安装 express、cors、openAi、dotenv三方库

c 复制代码
{
  "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 引入我们的第三方包 编写接口逻辑

**特别关注:

  1. baseURL: 这里大家可以替换其他的APi 比如deepseek(好像目前不能充值了),或者一些第三方的API地址。
  2. apiKey: 这里大家把key可以直接填写上,我这里为了学习知识,新建了一个.env 文件。**
c 复制代码
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启动一个微服务。

c 复制代码
<!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> 

所有文件

  1. .env 存储了APi-key
  2. index.html 前端代码
  3. server.js 后段代码

总结

尽可能的多学习一些知识,或许以后用不到,关注我每天练习一个小demo。

相关推荐
gnip4 小时前
企业级配置式表单组件封装
前端·javascript·vue.js
一只叫煤球的猫5 小时前
写代码很6,面试秒变菜鸟?不卖课,面试官视角走心探讨
前端·后端·面试
飞哥数智坊5 小时前
从CodeBuddy翻车到MasterGo救场,我的小程序UI终于焕然一新
人工智能
excel6 小时前
Three.js 材质(Material)详解 —— 区别、原理、场景与示例
前端
掘金安东尼6 小时前
抛弃自定义模态框:原生Dialog的实力
前端·javascript·github
AKAMAI8 小时前
跳过复杂环节:Akamai应用平台让Kubernetes生产就绪——现已正式发布
人工智能·云原生·云计算
新智元9 小时前
阿里王牌 Agent 横扫 SOTA,全栈开源力压 OpenAI!博士级难题一键搞定
人工智能·openai
新智元9 小时前
刚刚,OpenAI/Gemini 共斩 ICPC 2025 金牌!OpenAI 满分碾压横扫全场
人工智能·openai
hj5914_前端新手10 小时前
javascript基础- 函数中 this 指向、call、apply、bind
前端·javascript
薛定谔的算法10 小时前
低代码编辑器项目设计与实现:以JSON为核心的数据驱动架构
前端·react.js·前端框架