使用JavaScript构建Nanbeige 4.1-3B模型的交互式前端应用

张开发
2026/6/13 0:46:43 15 分钟阅读
使用JavaScript构建Nanbeige 4.1-3B模型的交互式前端应用
使用JavaScript构建Nanbeige 4.1-3B模型的交互式前端应用你是不是已经部署好了Nanbeige 4.1-3B模型的后端服务看着那个API接口地址却不知道如何把它变成一个用户能直接上手聊天的网页应用或者你厌倦了每次都在命令行里敲curl来测试模型想要一个更直观、更现代的前端界面这篇文章就是为你准备的。我们将抛开复杂的前端框架只用最基础的JavaScriptES6和浏览器自带的Fetch API一步步教你如何构建一个与AI模型对话的交互式前端应用。整个过程就像搭积木你会学到如何发送请求、处理模型返回的“一句话一句话”的流式响应、管理对话历史并打造一个流畅的用户体验。读完本文你就能亲手做出一个功能完整的聊天界面。1. 项目准备与环境搭建在开始写代码之前我们需要先明确目标和准备好“工具箱”。我们的目标是创建一个单页的HTML应用它能够通过HTTP请求与后端的Nanbeige 4.1-3B模型API进行通信。首先确保你的模型后端已经启动并运行。通常这类模型会提供一个类似http://your-server-address/v1/chat/completions的API端点。你需要知道这个地址以及可能需要用到的API密钥如果后端设置了认证。接下来创建一个新的项目文件夹比如叫做nanbeige-chat-frontend。在这个文件夹里我们只需要三个最基础的文件index.html: 网页的结构和骨架。style.css: 让我们的聊天界面看起来更舒服的样式。app.js: 所有交互逻辑的核心JavaScript代码。你可以使用任何你喜欢的代码编辑器比如VS Code、Sublime Text甚至记事本。不需要安装Node.js或任何复杂的构建工具我们的代码将直接在现代浏览器中运行。2. 构建聊天界面的基础骨架让我们先从用户能看到的部分开始。打开index.html文件构建一个简单的聊天界面结构。!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 titleNanbeige 4.1-3B 对话助手/title link relstylesheet hrefstyle.css link relstylesheet hrefhttps://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css /head body div classapp-container header classapp-header h1i classfas fa-robot/i Nanbeige AI 助手/h1 p classsubtitle与 Nanbeige 4.1-3B 模型进行对话/p /header main classchat-container !-- 消息显示区域 -- div classmessages-container idmessagesContainer !-- 初始欢迎消息 -- div classmessage ai-message div classavatari classfas fa-robot/i/div div classbubble 你好我是基于 Nanbeige 4.1-3B 模型的AI助手。有什么可以帮你的吗 /div /div /div !-- 输入区域 -- div classinput-area div classinput-wrapper textarea iduserInput placeholder输入你的问题...ShiftEnter换行Enter发送 rows1 /textarea button idsendButton classsend-btn title发送 i classfas fa-paper-plane/i /button /div div classcontrols button idclearButton classsecondary-btn i classfas fa-trash-alt/i 清空对话 /button div classstatus idstatusIndicator就绪/div /div /div /main /div script srcapp.js/script /body /html然后我们来添加一些基本的样式让界面不那么“骨感”。创建style.css文件* { box-sizing: border-box; margin: 0; padding: 0; } body { font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; display: flex; justify-content: center; align-items: center; padding: 20px; } .app-container { width: 100%; max-width: 900px; height: 90vh; background-color: white; border-radius: 20px; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); display: flex; flex-direction: column; overflow: hidden; } .app-header { background: linear-gradient(to right, #4f46e5, #7c3aed); color: white; padding: 25px 30px; text-align: center; } .app-header h1 { font-size: 1.8rem; margin-bottom: 5px; } .app-header .subtitle { opacity: 0.9; font-size: 0.95rem; } .chat-container { flex: 1; display: flex; flex-direction: column; padding: 20px; } .messages-container { flex: 1; overflow-y: auto; padding: 15px; border: 1px solid #e5e7eb; border-radius: 12px; margin-bottom: 20px; background-color: #f9fafb; } .message { display: flex; margin-bottom: 20px; animation: fadeIn 0.3s ease-out; } keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } .user-message { flex-direction: row-reverse; } .ai-message { flex-direction: row; } .avatar { width: 40px; height: 40px; border-radius: 50%; display: flex; align-items: center; justify-content: center; flex-shrink: 0; margin: 0 12px; font-size: 1.2rem; } .user-message .avatar { background-color: #4f46e5; color: white; } .ai-message .avatar { background-color: #10b981; color: white; } .bubble { max-width: 70%; padding: 14px 18px; border-radius: 18px; line-height: 1.5; word-wrap: break-word; } .user-message .bubble { background-color: #4f46e5; color: white; border-bottom-right-radius: 4px; } .ai-message .bubble { background-color: #e5e7eb; color: #111827; border-bottom-left-radius: 4px; } .bubble.streaming { background-color: #d1fae5; border-left: 3px solid #10b981; } .input-area { border-top: 1px solid #e5e7eb; padding-top: 20px; } .input-wrapper { display: flex; gap: 12px; margin-bottom: 15px; } .input-wrapper textarea { flex: 1; padding: 16px; border: 2px solid #d1d5db; border-radius: 12px; font-size: 1rem; font-family: inherit; resize: none; transition: border-color 0.2s; max-height: 120px; } .input-wrapper textarea:focus { outline: none; border-color: #4f46e5; } .send-btn { background-color: #4f46e5; color: white; border: none; border-radius: 12px; width: 56px; font-size: 1.2rem; cursor: pointer; transition: background-color 0.2s; } .send-btn:hover:not(:disabled) { background-color: #4338ca; } .send-btn:disabled { background-color: #9ca3af; cursor: not-allowed; } .controls { display: flex; justify-content: space-between; align-items: center; } .secondary-btn { padding: 10px 18px; background-color: #f3f4f6; color: #4b5563; border: 1px solid #d1d5db; border-radius: 10px; cursor: pointer; font-size: 0.9rem; display: flex; align-items: center; gap: 8px; transition: all 0.2s; } .secondary-btn:hover { background-color: #e5e7eb; } .status { font-size: 0.85rem; color: #6b7280; padding: 6px 12px; background-color: #f3f4f6; border-radius: 20px; } .status.thinking { color: #d97706; background-color: #fef3c7; } .status.error { color: #dc2626; background-color: #fee2e2; }现在打开index.html文件你应该能看到一个带有基本样式的聊天界面了。虽然还不能对话但架子已经搭好了。3. 核心使用Fetch API与模型后端通信这是整个应用最核心的部分。我们将编写app.js处理用户输入并将其发送到你的Nanbeige模型后端。3.1 配置与基础函数首先在app.js的开头我们需要定义一些配置和获取页面上的关键元素。// app.js - 核心逻辑 // 1. 配置你的模型API端点 // 将下面的地址替换成你实际部署的Nanbeige模型API地址 const API_BASE_URL http://localhost:8000; // 示例地址请修改 const API_ENDPOINT ${API_BASE_URL}/v1/chat/completions; // 如果后端需要API密钥请在此处配置注意前端暴露密钥有风险生产环境应使用后端代理 const API_KEY ; // 可选如果需要的话 // 2. 获取DOM元素 const messagesContainer document.getElementById(messagesContainer); const userInput document.getElementById(userInput); const sendButton document.getElementById(sendButton); const clearButton document.getElementById(clearButton); const statusIndicator document.getElementById(statusIndicator); // 3. 管理对话状态 let conversationHistory [ { role: system, content: 你是一个乐于助人的AI助手。请用清晰、友好的中文回答用户的问题。 }, { role: assistant, content: 你好我是基于 Nanbeige 4.1-3B 模型的AI助手。有什么可以帮你的吗 } ]; let isWaitingForResponse false; // 4. 工具函数向消息列表中添加一条消息 function addMessageToUI(role, content, isStreaming false) { const messageDiv document.createElement(div); messageDiv.className message ${role}-message; const avatarDiv document.createElement(div); avatarDiv.className avatar; avatarDiv.innerHTML role user ? i classfas fa-user/i : i classfas fa-robot/i; const bubbleDiv document.createElement(div); bubbleDiv.className bubble ${isStreaming ? streaming : }; bubbleDiv.textContent content; messageDiv.appendChild(avatarDiv); messageDiv.appendChild(bubbleDiv); messagesContainer.appendChild(messageDiv); // 滚动到最新消息 messagesContainer.scrollTop messagesContainer.scrollHeight; return bubbleDiv; // 返回气泡元素的引用方便后续更新流式内容 } // 5. 工具函数更新状态指示器 function updateStatus(text, type normal) { statusIndicator.textContent text; statusIndicator.className status; if (type ! normal) { statusIndicator.classList.add(type); } }3.2 发送请求与处理流式响应许多现代AI模型API支持流式响应Streaming这意味着模型生成的结果会像打字一样一个字一个字地传回来而不是等全部生成完再一次性返回。这能极大提升用户体验。我们将实现这个功能。// 6. 核心函数发送消息到AI模型 async function sendMessageToAI(userMessage) { if (isWaitingForResponse) { alert(请等待当前回复完成后再发送新消息。); return; } // 更新状态 isWaitingForResponse true; sendButton.disabled true; updateStatus(思考中..., thinking); // 将用户消息添加到历史记录和UI conversationHistory.push({ role: user, content: userMessage }); addMessageToUI(user, userMessage); // 清空输入框并调整高度 userInput.value ; userInput.style.height auto; // 在UI上为AI的回复创建一个“正在输入”的气泡 const aiBubble addMessageToUI(assistant, , true); let fullResponse ; // 准备请求体 const requestBody { model: nanbeige-4.1-3b, // 根据你的模型名称调整 messages: conversationHistory, stream: true, // 关键启用流式响应 max_tokens: 1024, temperature: 0.7, }; // 准备请求头 const headers { Content-Type: application/json, }; if (API_KEY) { headers[Authorization] Bearer ${API_KEY}; } try { // 发送Fetch请求 const response await fetch(API_ENDPOINT, { method: POST, headers: headers, body: JSON.stringify(requestBody), }); if (!response.ok) { throw new Error(API请求失败: ${response.status} ${response.statusText}); } // 处理流式响应 const reader response.body.getReader(); const decoder new TextDecoder(utf-8); while (true) { const { done, value } await reader.read(); if (done) break; // 解码数据块 const chunk decoder.decode(value); // 流式数据通常以 data: 开头每行是一个JSON对象或 [DONE] const lines chunk.split(\n).filter(line line.trim() ! ); for (const line of lines) { if (!line.startsWith(data: )) continue; const data line.slice(6); // 去掉 data: 前缀 if (data [DONE]) break; try { const parsed JSON.parse(data); const content parsed.choices?.[0]?.delta?.content || ; if (content) { fullResponse content; // 逐步更新UI上的气泡内容 aiBubble.textContent fullResponse; // 每次更新后都滚动到底部 messagesContainer.scrollTop messagesContainer.scrollHeight; } } catch (e) { console.warn(解析流数据时出错:, e, 原始数据:, data); } } } // 流式接收完成 aiBubble.classList.remove(streaming); // 将完整的AI回复添加到历史记录 conversationHistory.push({ role: assistant, content: fullResponse }); updateStatus(就绪); } catch (error) { console.error(与AI通信时发生错误:, error); // 更新UI显示错误 aiBubble.textContent 抱歉请求出错: ${error.message}; aiBubble.classList.remove(streaming); aiBubble.classList.add(error); updateStatus(请求出错, error); } finally { // 无论成功失败都重置状态 isWaitingForResponse false; sendButton.disabled false; userInput.focus(); } }3.3 绑定事件与完善交互现在我们需要将上面的函数与页面上的按钮和输入框连接起来。// 7. 事件监听发送按钮点击 sendButton.addEventListener(click, () { const message userInput.value.trim(); if (message) { sendMessageToAI(message); } }); // 8. 事件监听输入框按Enter发送ShiftEnter换行 userInput.addEventListener(keydown, (event) { if (event.key Enter !event.shiftKey) { event.preventDefault(); // 防止在textarea中插入换行 const message userInput.value.trim(); if (message) { sendMessageToAI(message); } } // 自动调整输入框高度 if (event.key Enter event.shiftKey) { // 允许换行稍后通过CSS或JS调整高度 setTimeout(() { userInput.style.height auto; userInput.style.height (userInput.scrollHeight) px; }, 0); } }); // 输入框输入时也调整高度 userInput.addEventListener(input, () { userInput.style.height auto; userInput.style.height (userInput.scrollHeight) px; }); // 9. 事件监听清空对话按钮 clearButton.addEventListener(click, () { if (confirm(确定要清空所有对话记录吗)) { // 重置对话历史只保留系统提示和初始问候 conversationHistory [ conversationHistory[0], // 保留system提示 { role: assistant, content: 对话已重置。你好有什么可以帮你的吗 } ]; // 清空UI messagesContainer.innerHTML ; // 添加新的初始问候 addMessageToUI(assistant, 对话已重置。你好有什么可以帮你的吗); updateStatus(对话已重置); } }); // 10. 页面加载完成后让输入框自动获得焦点 window.addEventListener(load, () { userInput.focus(); // 可选测试API连接 // testAPIConnection(); }); // 11. 可选测试API连接的小函数 async function testAPIConnection() { updateStatus(测试连接中..., thinking); try { const response await fetch(${API_BASE_URL}/health); // 假设有健康检查端点 if (response.ok) { updateStatus(后端连接正常); } else { updateStatus(后端连接异常, error); } } catch (error) { updateStatus(无法连接到后端, error); console.error(连接测试失败:, error); } }4. 运行与调试你的应用现在所有代码都准备好了。确保你的Nanbeige模型后端服务正在运行例如在http://localhost:8000。启动后端在你的服务器或本地环境中确保模型API正在监听请求。修改配置打开app.js文件找到const API_BASE_URL这一行将http://localhost:8000替换成你后端服务的实际地址和端口。打开前端直接在浏览器中打开index.html文件。由于我们使用的是Fetch API如果后端地址不是localhost或127.0.0.1浏览器可能会因为CORS跨域资源共享策略而阻止请求。解决CORS问题这是前端直接调用不同源API时最常见的问题。你有几个选择最佳实践生产环境为你自己的前端代码搭建一个简单的后端代理例如用Node.js Express让前端请求自己的服务器再由服务器转发请求到模型API。这样可以隐藏API密钥并解决CORS。开发环境快速方案如果你控制着模型后端可以在后端服务的响应头中添加CORS允许头例如Access-Control-Allow-Origin: *注意*表示允许任何来源仅用于开发测试。使用浏览器插件在开发时可以安装临时禁用CORS的浏览器插件如CORS Unblock但这只是临时解决方案。开始对话在输入框中键入问题比如“介绍一下你自己”然后点击发送按钮或按Enter键。你应该能看到你的消息出现在左侧用户然后AI的回复会像打字一样逐字出现在右侧助手。5. 总结与下一步到这里一个功能完整的、与Nanbeige 4.1-3B模型交互的前端应用就搭建完成了。我们从头开始用纯JavaScript实现了一个支持流式响应的聊天界面。整个过程涵盖了从静态页面构建、样式设计到最核心的异步请求处理、流式数据解析和前端状态管理。实际用下来这个基础版本已经能提供不错的体验了。流式响应让等待过程变得可感知对话历史管理也让上下文得以延续。当然你可能会遇到CORS问题或者想增加更多功能比如消息持久化存到本地、支持多种模型参数调整temperature、max_tokens、或者更美观的消息渲染支持Markdown或代码高亮。这些都可以作为你下一步探索的方向。例如要支持Markdown可以引入一个轻量级的Markdown解析库要保存历史可以使用浏览器的localStorage。这个项目的代码结构比较清晰功能模块划分明确增加新特性应该不会太困难。最重要的是通过这个实践你掌握了用最基础的Web技术与AI模型后端通信的核心方法。无论后端是Nanbeige、ChatGLM还是其他什么模型只要它提供兼容的HTTP API这套前端代码稍作调整就能复用。希望这个小小的项目能成为你探索AI应用开发的一个有趣起点。获取更多AI镜像想探索更多AI镜像和应用场景访问 CSDN星图镜像广场提供丰富的预置镜像覆盖大模型推理、图像生成、视频生成、模型微调等多个领域支持一键部署。

更多文章