vue 仿deepseek前端开发一个对话界面

后端:调用deepseek的api,所以返回数据格式和deepseek相同

{"model": "DeepSeek-R1-Distill-Qwen-1.5B", "choices": [{"index": 0, "delta": {"role": "assistant", "content": ",有什么", "tool_calls": null}, "finish_reason": null, "logprobs": null}], "usage": {"prompt_tokens": 5, "completion_tokens": 11, "total_tokens": 16}, "id": "chatcmpl-203a3024a36e4c02b02200ca47d8901e", "object": "chat.completion.chunk", "created": 1741766893}

在这里插入图片描述
前端开发的几个注意点:

  1. 将后端返回的文本转换为html展示在页面上,注意调整样式
  2. 模拟打字形式,页面随之滚动
  3. 撑开的输入框和对话内容部分样式调整
  4. 测试多种文本输入,例如含有html标签的
  5. 记录思考时间,思考内容模仿deepseek做收起

这只是初步的项目,仅支持文本输入
懒得分步写了,直接贴完整代码吧

有些地方可能写得比较繁琐
比如判断思考内容那段,只能通过think标签判断吗?
请各位多多指点,欢迎交流!!



<template><div class="talk-window"><div class="talk-title"><p>{{ answerTitle }}</p><el-input v-model="choicedTasks.name" placeholder="请选择任务" class="sangedianBtn" readonly><template #append><el-button class="ec-font icon-sangedian" @click="isShowTask = true" /></template></el-input></div><div class="talk-container"><div class="talk-welcome" v-if="contentList.length == 0"><h1>{{ welcome.title }}</h1><p>{{ welcome.desc }}</p></div><div class="talk-box" v-else :style="{ height: answerContHeight }"><div ref="logContainer" class="talk-content"><el-row v-for="(item, i) in contentList" :key="i" class="chat-assistant"><transition name="fade"><div :class="['answer-cont', item.type === 'send' ? 'end' : 'start']"><img v-if="item.type == 'answer'" :src="welcome.icon" /><div :class="item.type === 'send' ? 'send-item' : 'answer-item'"><div v-if="item.type == 'answer'" class="hashrate-markdown" v-html="item.message" /><div v-else>{{ item.message }}</div></div><!-- 增加复制 --><!-- <div v-if="item.type == 'answer' && !isTalking"><el-tooltip centent="复制"><i class="ec-font icon-ticket" @click="copyMsg(item.message)" /></el-tooltip></div> --></div></transition></el-row></div><div style="text-align: center; margin-top: 10px"><el-button class="chat-add" @click="newChat"><i class="ec-font icon-tianjia1" />新建对话</el-button></div></div><div class="talk-send"><textarea@keydown.enter="enterMessage"ref="input"v-model="inputMessage"@input="adjustInputHeight"placeholder="输入消息...":rows="2" /><!-- <el-inputv-model="inputMessage":autosize="{ minRows: 2, maxRows: 5 }"type="textarea"@keyup.enter="enterMessage"placeholder="Please input" /> --><div class="talk-btn-cont" style="text-align: right"><img @click="sendMessage" :src="iconImg" /></div></div></div><copyrightContent :systemNameOption="systemNameOption" /><taskDialog v-if="isShowTask" :choicedTasks="choicedTasks" v-model:isShow="isShowTask" @confirm="confirmTask" /></div>
</template><script>
import taskDialog from '@/views/chat/task/taskDialog.vue'
import copyrightContent from '@/views/chat/talking/components/copyright.vue'
import hljs from 'highlight.js'
import 'highlight.js/styles/a11y-dark.css'
import MarkdownIt from 'markdown-it'
import { VSConfig } from '/config/envConfig'window.hiddenThink = function (index) {// 隐藏思考内容if (document.getElementById(`think_content_${index}`).style.display == 'none') {document.getElementById(`think_content_${index}`).style.display = 'block'document.getElementById(`think_icon_${index}`).classList.replace('icon-a-xiangshang3', 'icon-a-xiangxia3')} else {document.getElementById(`think_content_${index}`).style.display = 'none'document.getElementById(`think_icon_${index}`).classList.replace('icon-a-xiangxia3', 'icon-a-xiangshang3')}
}export default {props: {isCollapsed: {type: Boolean,default: false},activeChat: String,activeTitle: String,chat_style_setting: {type: Object,default: function () {return {}}},systemNameOption: {type: Object,default: function () {return {}}}},components: { taskDialog, copyrightContent },data() {return {inputMessage: '',messages: [],choicedTasks: {uuid: '',name: ''},isShowTask: false,contentList: [],eventSourceChat: null,markdownIt: {},startAnwer: false,startTime: null,endTime: null,thinkTime: null,answerTitle: '',talkUUID: '',refreshHistoryFlag: false, // 是否已经生成了对话uuid,生成了的话就刷新历史列表msgHight: null,isTalking: false, //是否在对话中,处于对话中则展示休止按钮welcome: {title: '很高兴见到你!',desc: '我可以帮你写代码、读文件、写作各种创意内容,请把你的任务交给我吧~',icon: require('@/assets/image/AI.png')},store: {}}},watch: {activeTitle(val) {// 重命名了对话this.answerTitle = val},activeChat(val) {this.answerTitle = ''this.talkUUID = val || ''this.inputMessage = ''this.refreshHistoryFlag = falsethis.isTalking = falseif (val) {// 滚动回到顶部const logContainer = this.$refs.logContainerif (logContainer) {logContainer.scrollTop = 0}this.getTalkDetail()} else {this.contentList = []}this.eventSourceChat && this.eventSourceChat.close()},chat_style_setting(val) {this.welcome.title = val.welcome_speech_style || this.welcome.titlethis.welcome.desc = val.description_style || this.welcome.descthis.welcome.icon = val.icon_image || this.welcome.icon}},computed: {iconImg() {// 对话中可能有输入if (this.isTalking) {return require('/src/assets/image/chat/stop.png')} else {if (!this.inputMessage ||this.inputMessage.trim() === '' ||this.inputMessage.split(/\r?\n/).every((line) => line.trim() === '')) {return require('/src/assets/image/chat/unsend.png')} else {return require('/src/assets/image/chat/send.png')}}},answerContHeight() {// 回到初始值return this.msgHight == '56px' ? 'calc(100% - 140px)' : `calc(100% - 140px - ${this.msgHight} + 56px)`}},mounted() {this.store = mainStore()this.markdownIt = MarkdownIt({html: true,linkify: true,highlight: function (str, lang) {if (lang && hljs.getLanguage(lang)) {try {return hljs.highlight(str, { language: lang }).value} catch (__) {}}return '' // use external default escaping}})},methods: {getTalkDetail() {chat.historyDetail({uuid: this.activeChat}).then((res) => {this.contentList = []if (res.code == 0) {res.info.history_meta &&res.info.history_meta.forEach((item, index) => {if (item.conversation_type == 'Answer') {// 增加一个历史思考记录收起吧item.context = item.context.replace(/<think>\n\n<\/think>/g, '').replaceAll('<think>',`<div class="think-time">历史思考<i id="think_icon_${index}" onclick="hiddenThink(${index})" class="ec-font icon-a-xiangxia3"></i></div><section id="think_content_${index}">`).replaceAll('</think>', '</section>')item.context = this.markdownIt.render(item.context)}this.contentList.push({type: item.conversation_type == 'Question' ? 'send' : 'answer',message: item.context})})this.answerTitle = res.info.name.substring(0, 20)}})},enterMessage(event) {if (event.key === 'Enter' && !event.shiftKey) {event.preventDefault()this.sendMessage()}},sendMessage() {// 终止当前对话if (this.isTalking) {this.eventSourceChat && this.eventSourceChat.close()// 关闭后,处理正在对话的"思考中"let curAnswer = this.contentList[this.contentList.length - 1].messagecurAnswer = curAnswer.replaceAll('<div class="think-time">思考中……</div>', '对话中止')this.contentList[this.contentList.length - 1].message = curAnswerthis.isTalking = false// 再获取一下历史记录// 暂时不获取历史记录,因为中止对话时,历史UUID可能还没返回// this.$emit('getHistoryList')return}if (!this.inputMessage ||this.inputMessage.trim() === '' ||this.inputMessage.split(/\r?\n/).every((line) => line.trim() === '')) {this.inputMessage = ''return false}if (!this.choicedTasks.name) {this.$message({ message: '请选择推理任务', type: 'warning' })return false}// 回到初始高度const textarea = this.$refs.inputtextarea.style.height = '56px'this.msgHight = '56px'this.eventSourceChat && this.eventSourceChat.close()// let markedText = this.markdownIt.render(this.inputMessage)this.contentList.push({ type: 'send', message: this.inputMessage })this.contentList.push({ type: 'answer', message: `<div class="think-time">思考中……</div>` })this.answerTitle = this.answerTitle || this.contentList[0].message.substring(0, 20)this.scrollToBottom()this.initSSEChat()},initSSEChat() {const url = `${VSConfig.isHttps ? 'https' : 'http'}://${this.store.leader}/v1/intelligent_computing/task/chat/stream?uuid=${this.choicedTasks.uuid}&message=${encodeURIComponent(this.inputMessage)}&token=${this.store.token}&conversation_uuid=${this.talkUUID}`this.inputMessage = ''this.eventSourceChat = new EventSource(url)let buffer = ''this.startTime = nullthis.endTime = nullthis.thinkTime = nulllet len = this.contentList.lengthlet index = len % 2 === 0 ? len - 1 : lenthis.isTalking = truethis.eventSourceChat.onmessage = async (event) => {await this.sleep(10)if (event.data == '[DONE]') {return false}// 接收 Delta 数据// 最后一条是UUID,第二次发对话的时候要传参try {var { choices, created } = JSON.parse(event.data)} catch (e) {// 新对话在历史列表补充数据this.talkUUID = event.dataif (!this.refreshHistoryFlag) {this.refreshHistoryFlag = event.datathis.$emit('refreshHistory', this.refreshHistoryFlag)}}// const { choices, created } = JSON.parse(event.data)if (choices && choices[0].delta?.content) {buffer += choices[0].delta.content// think标签内是思考内容,单独记录思考时间if (choices[0].delta.content.includes('<think>')) {choices[0].delta.content = `<div class="think-time">思考中……</div><section id="think_content_${index}">`buffer = buffer.replaceAll('<think>', choices[0].delta.content)this.startTime = Math.floor(new Date().getTime() / 1000)}if (choices[0].delta.content.includes('</think>')) {// console.log("结束时间赋值的判断")choices[0].delta.content = `</section>`this.endTime = Math.floor(new Date().getTime() / 1000)// 获取到结束时间后,直接展示收起按钮this.thinkTime = this.endTime - this.startTimebuffer = buffer.replaceAll('<div class="think-time">思考中……</div>',`<div class="think-time">已深度思考(${this.thinkTime}S)<i id="think_icon_${index}" onclick="hiddenThink(${index})" class="ec-font icon-a-xiangxia3"></i></div>`).replaceAll('</think>', choices[0].delta.content).replaceAll(`<section id="think_content_${index}"></section>`, '')}let markedText = this.markdownIt.render(buffer)this.contentList[index] = { type: 'answer', message: markedText }this.scrollToBottomIfAtBottom()}}this.eventSourceChat.onerror = (event) => {console.log('错误触发===》', event)this.contentList[index] = { type: 'answer', message: `<div class="think-time">对话服务连接失败</div>` }this.eventSourceChat.close()this.isTalking = false}this.eventSourceChat.onclose = (event) => {// 关闭事件console.log('关闭事件--->')this.isTalking = false}},sleep(ms) {return new Promise((resolve) => setTimeout(resolve, ms))},scrollToBottomIfAtBottom() {this.$nextTick(() => {const logContainer = this.$refs.logContainerif (logContainer) {const threshold = 100const distanceToBottom = logContainer.scrollHeight - logContainer.scrollTop - logContainer.clientHeightif (distanceToBottom <= threshold) logContainer.scrollTop = logContainer.scrollHeight}})},scrollToBottom() {this.$nextTick(() => {const logContainer = this.$refs.logContainerif (logContainer) {logContainer.scrollTop = logContainer.scrollHeight}})},confirmTask(val) {this.choicedTasks = val},clearWindow() {this.eventSourceChat && this.eventSourceChat.close()this.contentList = []this.answerTitle = ''this.talkUUID = ''this.inputMessage = ''this.refreshHistoryFlag = falsethis.isTalking = falseconst textarea = this.$refs.inputtextarea.style.height = '56px'this.msgHight = '56px'},newChat() {this.clearWindow()this.$emit('clearChat')},adjustInputHeight(event) {// enter键盘按下的换行赋值为空if (event.key === 'Enter' && !event.shiftKey) {this.inputMessage = ''event.preventDefault()return}this.$nextTick(() => {const textarea = this.$refs.inputtextarea.style.height = 'auto'// 最高200pxtextarea.style.height = Math.min(textarea.scrollHeight, 200) + 'px'this.msgHight = textarea.style.height})},copyMsg(txt) {// 复制功能// 创建一个临时的 textarea 元素const textarea = document.createElement('textarea')textarea.value = txt.replace(/<[^>]+>/g, '') // 去掉html标签textarea.style.position = 'fixed'document.body.appendChild(textarea)textarea.select() // 选中文本try {document.execCommand('copy') // 执行复制ElMessage({message: '复制成功',type: 'success'})} catch (err) {ElMessage({message: '复制失败',type: 'error'})} finally {document.body.removeChild(textarea) // 移除临时元素}}},beforeDestroy() {this.eventSourceChat && this.eventSourceChat.close()}
}
</script><style scoped lang="scss">
.talk-window {height: 100%;transition: margin 0.2s ease;position: relative;
}
.talk-container {height: calc(100% - 58px);position: relative;
}
.talk-welcome {text-align: center;// margin-bottom: 25px;padding: 10% 20% 25px;box-sizing: border-box;h1 {margin-bottom: 30px;font-size: 21px;}p {color: #8f9aad;}
}
.messages {padding: 20px;overflow-y: auto;
}.message {display: flex;margin: 12px 0;
}.message.user {justify-content: flex-end;
}.bubble {max-width: 70%;padding: 12px 16px;border-radius: 12px;background: #fff;box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}.message.user .bubble {background: #e8f4ff;
}.time {font-size: 12px;color: #666;margin-top: 4px;
}.talk-send {background: #f1f2f7;border-radius: 10px;border: 1px solid #e9e9eb;padding: 5px 10px;margin: 0px 20%;img {cursor: pointer;}textarea {width: 100%;padding: 10px;resize: none;overflow: auto;// min-height: 48px;max-height: 200px;line-height: 1.5;box-sizing: border-box;font-family: inherit;border: 0px;background: #f1f2f7;}textarea:focus {outline: none !important;}
}input {flex: 1;padding: 12px;border: 1px solid #ddd;border-radius: 8px;
}.talk-title {height: 56px;line-height: 56px;p {color: #000000;font-size: 15px;font-weight: 550;text-align: center;}.sangedianBtn {width: 225px;height: 32px;position: absolute;top: 15px;right: 30px;}
}.send-item {max-width: 60%;word-break: break-all;padding: 10px;background: #eef6ff;border-radius: 10px;color: #000000;white-space: pre-wrap;font-size: 13px;
}
.msg-row {margin-bottom: 10px;
}
.talk-box {height: calc(100% - 140px);.talk-content {background-color: #fff;color: #324659;overflow-y: auto;height: calc(100% - 50px);box-sizing: border-box;padding: 0px 20%;// &:hover {//   overflow-y: auto;// }.chat-assistant {display: flex;margin-bottom: 10px;.answer-item {line-height: 30px;color: #324659;}}.answer-cont {position: relative;display: flex;width: 100%;> img {width: 30px;height: 30px;margin-right: 10px;}&.end {justify-content: flex-end;}&.start {justify-content: flex-start;}}}.chat-sse {min-height: 100px;max-height: 460px;}.chat-message {height: calc(100vh - 276px);}.thinking-bubble {height: calc(100vh - 296px);}
}
.chat-add {width: 111px;height: 33px;background: #dbeafe;border-radius: 6px !important;font-size: 14px !important;border: 0px;color: #516ffe !important;&:hover {background: #ebf0f7;}.icon-tianjia1 {margin-right: 10px;font-size: 14px;}
}
.talk-btn-cont {text-align: right;height: 30px;margin-top: 5px;
}
</style><style lang="scss">
@use './markdown.scss';
</style>

最终页面:
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/33115.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基于Spring Boot的小区疫情购物系统的设计与实现(LW+源码+讲解)

专注于大学生项目实战开发,讲解,毕业答疑辅导&#xff0c;欢迎高校老师/同行前辈交流合作✌。 技术范围&#xff1a;SpringBoot、Vue、SSM、HLMT、小程序、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、安卓app、大数据、物联网、机器学习等设计与开发。 主要内容&#xff1a;…

【openwebui 搭建本地知识库(RAG搭建本地知识库)】

安装准备 openwebui 这个本地安装之前写过使用python安装。也可以直接用docker 命令 docker run --rm -d \-p 3080:8080 \-p 3081:8081 \-e WEBUI_AUTHtrue \-e DEFAULT_LOCALEcn \-e GLOBAL_LOG_LEVEL"INFO" \-e AIOHTTP_CLIENT_TIMEOUT100 \--privilegedtrue \-…

Linux 提权

文章目录 前言1. 内核漏洞提权脏牛&#xff08;CVE-2016-5195&#xff09; 2. 不安全的系统配置项2.1 SUID/SGID提权2.2 sudo提权2.3 定时任务提权2.4 capabilities提权 3. 第三方软件提权Tomcat managerNginx本地提权&#xff08;CVE-2016-1247&#xff09;Redis未授权 4. 参考…

谷粒商城:性能压测JVM堆区

目录 Kit Apache JMeter VisualVM 堆内存 jvm内存模型 垃圾回收&#xff08;Garbage Collection, GC&#xff09; 新对象分配内存 GC步骤 MinorGC 性能优化 影响因素 优化 nginx动静分离 优化三级分类获取 Jvm参数配置堆区 测试 Kit Apache JMeter 压力测试&…

TCP协议支持全双工原因TCP发送接收数据是生产者消费者模型

一、TCP支持全双工的原因 TCP协议支持全双工&#xff0c;即使用TCP协议进行通信时&#xff0c;服务端和客户端可以同时进行数据的发送和接收&#xff0c;互不干扰&#xff0c;实现同时双向传输数据。 这是因为使用TCP协议通信时&#xff0c;读写套接字的文件描述符既用来发送…

观成科技:​加密C2框架Platypus流量分析

一、工具介绍 Platypus 是一款支持多会话的交互式反向 Shell 管理器。在实际的渗透测试中&#xff0c;为了解决 Netcat/Socat 等工具在文件传输、多会话管理方面的不足,该工具在多会话管理的基础上增加了在渗透测试中能更好发挥作用的功能&#xff08;如&#xff1a;交互式 Sh…

在 C# 中,is null 和 == null ‌不完全等价‌

最近遇到了一个看似奇怪的问题&#xff0c;判断一个对象是否为null&#xff0c;我使用了null来判断&#xff0c;结果他是null但是仍然进入了判断。 经过讨论和验证&#xff0c;发现使用is null 可以解决问题&#xff0c;于是查阅了资料。 在 C# 中&#xff0c;is null 和 nul…

go语言zero框架拉取内部平台开发的sdk报错的修复与实践

在开发过程中&#xff0c;我们可能会遇到由于认证问题无法拉取私有 SDK 的情况。这种情况常发生在使用 Go 语言以及 Zero 框架时&#xff0c;尤其是在连接到私有平台&#xff0c;如阿里云 Codeup 上托管的 Go SDK。如果你遇到这种错误&#xff0c;通常是因为 Go 没有适当的认证…

VBA+FreePic2Pdf 找出没有放入PDF组合的单个PDF工艺文件

设计部门针对某个项目做了一个工艺汇总报告&#xff0c;原先只要几十个工艺文件&#xff0c;组合成一个PDF&#xff0c;但后来要求要多放点PDF进去&#xff0c;但工艺文件都混在一起又不知道哪些是重复的&#xff0c;找上我让我帮忙处理一下&#xff0c;我开始建议让她重新再组…

Webservice如何调用

webservice调用方式&#xff1a; &#xff08;1&#xff09;http方式调用 请求头增加Content-type:text/xml 或application/soapxml SOAPAction:方法名 请求body以xml字符串传递&#xff0c;xml格式定义 返回以xml字符串返回&#xff0c;xml某个字段是一个json字符串。 入…

2025-03-10 吴恩达机器学习1——机器学习概述

文章目录 1 监督学习1.1 回归1.2 分类 2 无监督学习2.1 聚类2.2 异常检测2.3 降维 3 使用 Jupyter Notebook ​ 1959 年&#xff0c;Arthur Samuel 将机器学习定义如下&#xff1a; ​ Field of study that gives computers the ability to learn without being explicitly pro…

Python 进程与线程-分布式进程

目录 分布式进程 小结 分布式进程 在Thread和Process中&#xff0c;应当优选Process&#xff0c;因为Process更稳定&#xff0c;而且&#xff0c;Process可以分布到多台机器上&#xff0c;而Thread最多只能分布到同一台机器的多个CPU上。 Python的multiprocessing模块不但支…

使用 Excel 实现绩效看板的自动化

引言 在日常工作中&#xff0c;团队的绩效监控和管理是确保项目顺利进行的重要环节。然而&#xff0c;面临着以下问题&#xff1a; ​数据分散&#xff1a;系统中的数据难以汇总&#xff0c;缺乏一个宏观的团队执行情况视图。​看板缺失&#xff1a;系统本身可能无法提供合适…

Unity中WolrdSpace下的UI展示在上层

一、问题描述 Unity 中 Canvas使用World Space布局的UI&#xff0c;想让它不被3d物体遮挡&#xff0c;始终显示在上层。 二、解决方案 使用shader解决 在 UI 的材质中禁用深度测试&#xff08;ZTest&#xff09;&#xff0c;强制 UI 始终渲染在最上层。 Shader "Custo…

uni-app学习笔记——自定义模板

一、流程 1.这是一个硬性的流程&#xff0c;只要按照如此程序化就可以实现 二、步骤 1.第一步 2.第二步 3.第三步 4.每一次新建页面&#xff0c;都如第二步一样&#xff1b;可以选择自定义的模版&#xff08;vue3Setup——这是我自己的模版&#xff09;&#xff0c;第二步的…

网络安全防护总体架构 网络安全防护工作机制

1 实践内容 1.1 安全防范 为了保障"信息安全金三角"的CIA属性、即机密性、完整性、可用性&#xff0c;信息安全领域提出了一系列安全模型。其中动态可适应网络安全模型基于闭环控制理论&#xff0c;典型的有PDR和P^2DR模型。 1.1.1 PDR模型 信息系统的防御机制能…

tomato靶场通关攻略

1.打开tomato靶机和kali虚拟机 2.运用kali扫描靶机ip 3.浏览器访问靶机ip 4.kali dirb扫描敏感文件 5.依次查看这几个文件&#xff0c;发现antibot_image下有文件info.php&#xff0c;发现一个被注释的get型文件包含漏洞&#xff0c;参数为image 6.查看日志文件 http://192.…

玩转云服务器——阿里云操作系统控制台体验测评

在云服务器日益普及的背景下&#xff0c;运维人员对操作系统管理工具的要求不断提高。我们需要一款既能直观展示系统状态&#xff0c;又能智能诊断问题&#xff0c;提供专业指导的控制台。阿里云操作系统管理平台正是基于API、SDK、CLI等多种管理方式&#xff0c;致力于提升操作…

Spring boot3-WebClient远程调用非阻塞、响应式HTTP客户端

来吧&#xff0c;会用就行具体理论不讨论 1、首先pom.xml引入webflux依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-webflux</artifactId> </dependency> 别问为什么因为是响应式....…

Qt/C++音视频开发82-系统音量值获取和设置/音量大小/静音

一、前言 在音视频开发中&#xff0c;音量的控制分两块&#xff0c;一个是控制播放器本身的音量&#xff0c;绝大部分场景都是需要控制这个&#xff0c;这个不会影响系统音量的设置。还有一种场景是需要控制系统的音量&#xff0c;因为播放器本身的音量是在系统音量的基础上控…