Janus-Pro-7B实战:构建基于Vue.js的前端AI对话界面
Janus-Pro-7B实战构建基于Vue.js的前端AI对话界面最近在折腾本地大模型发现Janus-Pro-7B的效果相当不错推理速度快回答质量也高。但每次都要在命令行里敲指令总觉得少了点“产品感”。作为一个全栈开发者我习惯给自己用的工具做个界面用起来更顺手。于是我花了点时间用Vue.js给Janus-Pro-7B搭了个前端对话界面。做完之后感觉就像给自己装了个本地版的智能助手体验瞬间提升了好几个档次。今天就把这个过程的思路和关键代码分享出来如果你也想给自己的模型加个“脸面”或许能有些参考。1. 项目准备与环境搭建在开始敲代码之前我们先明确一下目标我们要构建一个单页应用SPA它能够通过浏览器与后端的Janus-Pro-7B模型API进行对话并且要支持类似ChatGPT那样的流式响应让答案一个字一个字地“打”出来。1.1 技术栈选择为什么选Vue.js因为它上手快、生态丰富对于构建这种交互密集型的应用特别友好。配合一些常用的库能让我们快速搭建出可用的界面。核心依赖我选了这些Vue 3 使用Composition API代码组织更清晰。Vite 作为构建工具启动和热更新速度飞快开发体验好。Axios 处理HTTP请求与后端API通信。Element Plus UI组件库提供现成的按钮、输入框、布局等让我们能快速搭出好看的界面把精力集中在业务逻辑上。Vue Router 虽然我们这个单页应用可能暂时用不到多页面但先配上以备不时之需。1.2 初始化项目打开终端一行命令创建项目npm create vuelatest janus-pro-frontend创建过程中根据提示选择需要的功能。我通常会把Vue Router和Pinia状态管理都选上TypeScript看个人喜好这次我们先不用以保持简洁。项目创建好后进入目录安装我们计划好的依赖cd janus-pro-frontend npm install axios element-plus npm install --save-dev sass安装完成后我们需要在main.js中引入并注册Element Plus。// main.js import { createApp } from vue import App from ./App.vue import router from ./router // 引入Element Plus及其样式 import ElementPlus from element-plus import element-plus/dist/index.css const app createApp(App) app.use(router) app.use(ElementPlus) // 全局注册Element Plus app.mount(#app)到这里一个干净的Vue 3项目骨架就搭好了。接下来我们来设计应用的核心——对话界面。2. 构建对话界面与布局一个好的对话界面核心是清晰的信息呈现和流畅的交互。我们的界面主要分为三个区域对话历史展示区、用户输入区、以及顶部的控制栏。2.1 设计主页面结构我在src/views目录下创建了一个Chat.vue文件作为我们的主聊天页面。先来搭建基础的HTML结构。!-- src/views/Chat.vue -- template div classchat-container !-- 顶部控制栏 -- el-header classchat-header h2 Janus-Pro 智能对话/h2 div classheader-actions el-button typeinfo plain clickclearHistory清空对话/el-button el-tag typesuccess v-ifisConnected已连接/el-tag el-tag typedanger v-else未连接/el-tag /div /el-header !-- 主内容区 -- el-main classchat-main !-- 对话消息列表 -- div refmessagesContainer classmessages-wrapper div v-for(msg, index) in messages :keyindex :class[message-bubble, msg.role] div classmessage-avatar el-avatar :size36 {{ msg.role user ? 我 : AI }} /el-avatar /div div classmessage-content div classmessage-role{{ msg.role user ? 你 : Janus-Pro }}/div div classmessage-text v-htmlformatMessage(msg.content)/div div classmessage-time{{ msg.timestamp }}/div /div /div !-- 加载指示器 -- div v-ifisLoading classthinking-indicator el-icon classis-loadingLoading //el-icon spanJanus-Pro 正在思考.../span /div /div /el-main !-- 底部输入区 -- el-footer classchat-footer div classinput-area el-input v-modeluserInput typetextarea :rows3 placeholder向 Janus-Pro 提问吧...Shift Enter 换行Enter 发送 keydown.enter.exact.preventsendMessage resizenone / div classinput-actions el-button typeprimary :loadingisLoading clicksendMessage :disabled!userInput.trim() 发送 /el-button el-button clickuserInput 清空/el-button /div /div div classfooter-tips small提示Janus-Pro 是一个本地运行的大语言模型请勿询问敏感信息。/small /div /el-footer /div /template script setup import { ref, computed, nextTick, onMounted, onUnmounted } from vue import { ElMessage } from element-plus import { Loading } from element-plus/icons-vue // 稍后我们会在这里添加核心逻辑 /script style scoped langscss .chat-container { height: 100vh; display: flex; flex-direction: column; background-color: #f5f7fa; } .chat-header { display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #e4e7ed; background-color: #fff; } .chat-main { flex: 1; overflow: hidden; padding: 20px; } .messages-wrapper { height: 100%; overflow-y: auto; padding-right: 10px; } .message-bubble { display: flex; margin-bottom: 20px; .user { flex-direction: row-reverse; .message-content { align-items: flex-end; margin-right: 12px; } } .assistant { .message-content { align-items: flex-start; margin-left: 12px; } } } .message-content { max-width: 70%; display: flex; flex-direction: column; } .message-role { font-size: 0.85rem; color: #909399; margin-bottom: 4px; } .message-text { padding: 12px 16px; border-radius: 8px; line-height: 1.6; word-break: break-word; background-color: #fff; box-shadow: 0 1px 3px rgba(0,0,0,0.1); .user .message-text { background-color: #e3f2fd; } } .message-time { font-size: 0.75rem; color: #c0c4cc; margin-top: 4px; } .thinking-indicator { display: flex; align-items: center; color: #67c23a; padding: 10px; } .chat-footer { border-top: 1px solid #e4e7ed; background-color: #fff; padding: 16px 20px; } .input-area { margin-bottom: 8px; } .input-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 10px; } .footer-tips { text-align: center; color: #909399; } /style这个布局用了Element Plus的容器组件让结构很清晰。对话气泡区分了用户和AI样式上也做了区分。现在界面有了但数据是静态的。接下来我们要让界面“活”起来核心就是状态管理和API通信。3. 实现核心逻辑与API通信界面是静态的现在我们需要注入灵魂——数据与交互。这里主要涉及两件事管理对话状态消息列表、加载状态以及与后端Janus-Pro-7B的API进行通信。3.1 状态管理与数据定义在script setup部分我们定义应用的核心状态。script setup import { ref, computed, nextTick, onMounted, onUnmounted } from vue import { ElMessage } from element-plus import { Loading } from element-plus/icons-vue // 响应式状态 const messages ref([]) // 对话消息列表 const userInput ref() // 用户输入 const isLoading ref(false) // 是否正在加载 const isConnected ref(true) // 后端连接状态简化处理 const messagesContainer ref(null) // 用于滚动到最新的消息 // 模拟一些初始对话让界面不那么空 onMounted(() { messages.value [ { role: assistant, content: 你好我是 Janus-Pro一个本地运行的大语言模型。有什么可以帮你的吗, timestamp: getCurrentTime() } ] // 组件挂载后滚动到底部 scrollToBottom() }) // 获取当前时间用于消息时间戳 function getCurrentTime() { return new Date().toLocaleTimeString([], { hour: 2-digit, minute: 2-digit }) } // 格式化消息内容简单处理换行 function formatMessage(content) { return content.replace(/\n/g, br) } // 滚动到消息列表底部 function scrollToBottom() { nextTick(() { if (messagesContainer.value) { messagesContainer.value.scrollTop messagesContainer.value.scrollHeight } }) } // 清空对话历史 function clearHistory() { messages.value [ { role: assistant, content: 对话历史已清空。我们可以开始新的话题了。, timestamp: getCurrentTime() } ] ElMessage.success(对话历史已清空) } /script状态定义好了清空对话的功能也实现了。接下来是最关键的一步如何把用户的提问发送给后端的Janus-Pro-7B并接收它的回复。3.2 封装API请求与实现流式响应与普通API调用不同大模型的文本生成往往是流式的Streaming即服务器会一边生成一边发送数据包这样前端就能实现逐字打印的效果体验更好。这里我们使用Server-Sent Events (SSE) 来接收流式响应。首先我们在src目录下创建一个api文件夹并新建一个chat.js文件来封装所有与对话相关的API请求。// src/api/chat.js import axios from axios // 创建axios实例配置基础URL和超时时间 // 假设你的Janus-Pro后端API运行在 http://localhost:8000 const apiClient axios.create({ baseURL: http://localhost:8000, // 请根据你的后端地址修改 timeout: 30000, // 超时时间设置为30秒因为生成文本可能较慢 headers: { Content-Type: application/json, } }) /** * 发送消息到Janus-Pro模型普通HTTP模式 * param {Array} messages - 对话历史消息数组格式[{role: user, content: ...}, ...] * returns {Promise} - 返回AI的回复 */ export const sendChatMessage async (messages) { try { const response await apiClient.post(/v1/chat/completions, { model: janus-pro-7b, // 模型名称根据后端配置调整 messages: messages, stream: false, // 非流式 max_tokens: 1024, temperature: 0.7, }) return response.data.choices[0].message.content } catch (error) { console.error(发送消息失败:, error) throw new Error(请求失败: ${error.message}) } } /** * 发送消息并建立SSE连接以接收流式响应 * param {Array} messages - 对话历史消息数组 * param {Function} onChunk - 接收到数据块时的回调函数 (chunk: string) * param {Function} onDone - 流式接收完成时的回调函数 (fullText: string) * param {Function} onError - 发生错误时的回调函数 (error: Error) */ export const sendChatMessageStream (messages, { onChunk, onDone, onError }) { // 构建一个符合常见后端SSE接口的请求体 const requestBody { model: janus-pro-7b, messages: messages, stream: true, // 关键开启流式输出 max_tokens: 1024, temperature: 0.7, } // 使用EventSource API建立SSE连接 // 注意EventSource只支持GET请求且不能自定义Header。如果后端要求POST可能需要使用fetch API。 // 这里假设后端提供了一个支持GETquery参数或POSTSSE的端点。以下是一个更通用的fetch示例。 const eventSource new EventSource(http://localhost:8000/v1/chat/completions?streamtruedata${encodeURIComponent(JSON.stringify(requestBody))}) let fullText eventSource.onmessage (event) { try { // 常见的SSE数据格式是 data: {...}\n\n if (event.data.startsWith(data: )) { const dataStr event.data.slice(6) // 去掉data: 前缀 if (dataStr [DONE]) { eventSource.close() onDone?.(fullText) return } const parsed JSON.parse(dataStr) const chunk parsed.choices?.[0]?.delta?.content || if (chunk) { fullText chunk onChunk?.(chunk) } } } catch (e) { console.error(解析SSE数据失败:, e, event.data) } } eventSource.onerror (err) { console.error(SSE连接错误:, err) eventSource.close() onError?.(new Error(流式连接中断)) } // 返回一个关闭连接的方法 return () { eventSource.close() } } // 注意上述SSE示例是一种简化。实际部署中后端API的SSE实现方式可能不同。 // 更健壮的做法是使用fetch API来手动处理SSE流以便支持POST请求和自定义Header。 // 下面提供一个使用fetch处理SSE的备选方案 /** * 使用fetch API处理SSE流式响应 (备选方案更灵活) */ export const sendChatMessageStreamWithFetch async (messages, { onChunk, onDone, onError }) { const requestBody { model: janus-pro-7b, messages: messages, stream: true, max_tokens: 1024, temperature: 0.7, } try { const response await fetch(http://localhost:8000/v1/chat/completions, { method: POST, headers: { Content-Type: application/json, }, body: JSON.stringify(requestBody), }) if (!response.ok) { throw new Error(HTTP error! status: ${response.status}) } const reader response.body.getReader() const decoder new TextDecoder() let buffer let fullText while (true) { const { done, value } await reader.read() if (done) { onDone?.(fullText) break } buffer decoder.decode(value, { stream: true }) const lines buffer.split(\n\n) buffer lines.pop() || // 最后一行可能不完整放回buffer for (const line of lines) { if (line.startsWith(data: )) { const dataStr line.slice(6) if (dataStr [DONE]) { onDone?.(fullText) return } try { const parsed JSON.parse(dataStr) const chunk parsed.choices?.[0]?.delta?.content || if (chunk) { fullText chunk onChunk?.(chunk) } } catch (e) { console.error(解析流数据失败:, e, dataStr) } } } } } catch (error) { console.error(流式请求失败:, error) onError?.(error) } }API层封装好了它提供了两种调用方式普通的阻塞式请求和流式请求。为了获得更好的用户体验我们将在前端使用流式方案。现在回到Chat.vue组件我们来实现发送消息的核心函数sendMessage。3.3 集成API并完成发送逻辑在Chat.vue的script setup部分引入我们封装的API并完善sendMessage函数。script setup // ... 之前的导入和状态定义 ... import { sendChatMessageStreamWithFetch } from /api/chat // 导入流式API // 发送消息函数 async function sendMessage() { const inputText userInput.value.trim() if (!inputText || isLoading.value) return // 1. 将用户输入添加到消息列表 const userMessage { role: user, content: inputText, timestamp: getCurrentTime() } messages.value.push(userMessage) userInput.value // 清空输入框 scrollToBottom() // 2. 添加一个空的AI消息占位符用于流式填充 const assistantMessageIndex messages.value.length messages.value.push({ role: assistant, content: , // 初始为空 timestamp: getCurrentTime() }) isLoading.value true scrollToBottom() // 3. 构建发送给后端的消息历史通常只发送最近的若干条以避免过长 const messagesForAPI formatMessagesForAPI(messages.value.slice(0, -1)) // 不包含刚添加的占位符 try { // 4. 调用流式API await sendChatMessageStreamWithFetch(messagesForAPI, { onChunk: (chunk) { // 收到一个数据块就追加到占位符消息的内容中 messages.value[assistantMessageIndex].content chunk // 每次更新后都滚动到底部实现逐字打印的跟随效果 scrollToBottom() }, onDone: (fullText) { // 流式接收完成 console.log(完整回复:, fullText) messages.value[assistantMessageIndex].timestamp getCurrentTime() // 更新完成时间 isLoading.value false ElMessage.success(回复完成) }, onError: (error) { // 发生错误 console.error(流式请求出错:, error) messages.value[assistantMessageIndex].content 抱歉请求出现错误: ${error.message} messages.value[assistantMessageIndex].timestamp getCurrentTime() isLoading.value false ElMessage.error(请求失败请检查后端服务) } }) } catch (error) { // 捕获初始化请求时的错误如网络错误 console.error(发送消息失败:, error) messages.value[assistantMessageIndex].content 发送请求失败: ${error.message} messages.value[assistantMessageIndex].timestamp getCurrentTime() isLoading.value false ElMessage.error(发送失败) } } // 格式化消息使其符合后端API要求的格式 function formatMessagesForAPI(chatMessages) { // 通常只需要 role 和 content 字段 return chatMessages.map(msg ({ role: msg.role, content: msg.content })) } /script到这里核心的对话逻辑就完成了。用户输入消息列表更新流式接收AI回复并逐字显示同时还有加载状态和错误处理。一个可用的AI对话前端基本成型。4. 功能完善与部署建议基础功能跑通了但要让这个应用更健壮、更好用我们还需要做一些完善工作并考虑如何部署它。4.1 添加实用功能与优化体验首先我们可以增加一些提升用户体验的小功能。对话历史持久化 使用浏览器的localStorage来保存对话这样刷新页面后历史记录不会丢失。// 在Chat.vue的onMounted和消息更新时加入持久化逻辑 import { watch } from vue const STORAGE_KEY janus_pro_chat_history // 加载历史记录 onMounted(() { const saved localStorage.getItem(STORAGE_KEY) if (saved) { try { messages.value JSON.parse(saved) } catch (e) { console.error(加载历史记录失败:, e) } } else { // 默认欢迎消息 messages.value [{ role: assistant, content: 你好我是 Janus-Pro一个本地运行的大语言模型。有什么可以帮你的吗, timestamp: getCurrentTime() }] } scrollToBottom() }) // 监听消息变化并保存 watch(messages, (newVal) { localStorage.setItem(STORAGE_KEY, JSON.stringify(newVal)) }, { deep: true })优化流式显示 目前的scrollToBottom在每次收到数据块时都触发如果回复很长滚动会过于频繁。可以做个节流优化。// 在组件内添加一个滚动控制逻辑 let scrollTimer null function scheduleScrollToBottom() { if (scrollTimer) clearTimeout(scrollTimer) scrollTimer setTimeout(() { scrollToBottom() scrollTimer null }, 100) // 每100毫秒最多滚动一次 } // 然后在onChunk回调中调用 scheduleScrollToBottom() 而不是 scrollToBottom()美化代码块显示 如果AI的回复中包含代码我们可以用类似highlight.js的库来高亮显示提升可读性。4.2 项目配置与跨域处理在开发时前端项目通常是localhost:5173需要请求后端APIlocalhost:8000这涉及到跨域问题。我们可以在Vite的配置文件中设置代理。// vite.config.js import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], server: { proxy: { // 将以 /api 开头的请求代理到后端服务 /api: { target: http://localhost:8000, changeOrigin: true, rewrite: (path) path.replace(/^\/api/, ) // 移除 /api 前缀根据后端路由调整 } } } })然后将api/chat.js中的baseURL改为/api这样所有请求都会先经过开发服务器代理避免跨域错误。// src/api/chat.js const apiClient axios.create({ baseURL: /api, // 使用代理 // ... 其他配置不变 })4.3 构建与部署开发完成后我们需要将前端应用构建成静态文件并部署到服务器上。构建生产版本npm run build这会在项目根目录下生成一个dist文件夹里面就是所有静态资源HTML, JS, CSS。部署选项静态文件服务器 将dist文件夹内的所有文件上传到任何静态托管服务如GitHub Pages、Vercel、Netlify或你自己的Nginx服务器。与后端服务同域部署 将构建好的dist文件交给后端服务如Python的FastAPI、Flask来托管。这样前后端就在同一个域名下没有跨域问题。通常做法是让后端服务将根路径指向index.html并处理API路由。Docker容器化 创建一个简单的Nginx Docker镜像来服务前端静态文件与后端服务容器通过Docker Compose编排这是非常干净的部署方式。部署后配置 部署到生产环境后需要将api/chat.js中的baseURL改为你后端API的真实公网地址或者如果前后端同域可以改为相对路径/api。5. 总结与回顾走完这一趟一个功能完整的、基于Vue.js的Janus-Pro-7B对话前端就搭建起来了。整个过程其实并不复杂核心思路就是用Vue管理状态和视图用Axios或Fetch与后端通信用SSE实现流式响应来优化体验再用UI库把界面做美观。实际用下来这个前端界面让本地大模型的交互体验提升了一大截。流式输出的逐字效果比等整个句子生成完再显示要生动得多也更符合我们对“对话”的预期。Element Plus的组件也大大节省了从零设计样式的时间。当然这只是个起点。你可以在此基础上继续扩展很多功能比如支持多轮对话中编辑或重新生成某条消息。添加模型参数如temperature、max_tokens的前端调节面板。实现对话记录的导出和导入。为AI回复添加复制代码、朗读等快捷操作。甚至集成多个不同的本地模型做一个统一的AI工具门户。前端技术栈也可以灵活替换比如用React或Svelte来实现原理都是相通的。关键在于理解前后端分离的架构下如何通过API桥接起用户界面与强大的AI模型。希望这个实践能为你构建自己的AI应用提供一个清晰的思路和可用的代码基础。获取更多AI镜像想探索更多AI镜像和应用场景访问 CSDN星图镜像广场提供丰富的预置镜像覆盖大模型推理、图像生成、视频生成、模型微调等多个领域支持一键部署。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2444495.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!