vue实现调用兼容openai api接口大模型聊天对话流式输出webui代码
代码语言:html
所属分类:其他
代码描述:vue实现调用兼容openai api接口大模型聊天对话流式输出webui代码,只要兼容openai的api协议,例如千问、智谱、本地离线大模型ollama等都可以使用,只要改一下api地址和key及model就行了,实现了流式输出、打字动画、复制、重新回答、本地离线消息记录、代码高亮、复制按钮、自动滚动与手动滚动、添加附件等主流大模型的ui交互效果。
代码标签: vue 调用 兼容 openai api 接口 大模型 聊天 对话 流式 输出 webui 代码
下面为部分代码预览,完整代码请点击下载或在bfwstudio webide中打开
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <link type="text/css" rel="stylesheet" href="//repo.bfw.wiki/bfwrepo/css/font-awesome-4.7.0/css/font-awesome.css"> <script type="text/javascript" src="//repo.bfw.wiki/bfwrepo/js/highlight.js"></script> <link type="text/css" rel="stylesheet" href="//repo.bfw.wiki/bfwrepo/css/highlight.9.9.css"> <script type="text/javascript" src="//repo.bfw.wiki/bfwrepo/js/marked.umd.min.js"></script> <script type="text/javascript" src="//repo.bfw.wiki/bfwrepo/js/localforage.min.js"></script> <script type="text/javascript" src="//repo.bfw.wiki/bfwrepo/js/vue@2.6.1-dev.js"></script> <style> body{ padding: 0; margin: 0; } .cont{ position: fixed; top:0; left: 0; right: 0; display:flex; flex-direction: column;height:100vh; } .historylist { flex: 1; overflow-y: scroll; } .historylist img{ max-width: 100px; } .mesay, .aisay { padding: 10px; margin: 10px; border-radius: 5px; line-height: 30px; } .mesay { background-color: #d0e7ff; text-align: right; } .aisay { background-color: aliceblue; text-align: left; } .inputpannel { display: flex; margin: 10px; background: white; align-items: flex-end; border: 2px solid grey; border-radius: 14px; } .footer{ position: relative; } .inputpannel:focus-within { border-color: blue; } .inputtext { margin-top: 4px; width: 100%; background: white; line-height: 20px; box-sizing: border-box; padding: 8px; font-family: inherit; font-size: 16px; resize: none; overflow: hidden; border: none; outline: none; height: 40px; min-height: 40px; overflow-y: auto; max-height: calc(24px * 5 ); } .inputtext:focus { border: none; outline: none; } textarea::placeholder { } .historylist pre { white-space: pre-wrap; color: #ececec; background: black; border-radius: 4px; padding: 10px; margin: 0; } .historylist p { margin: 0; padding: 2px; } .typing-text { font-size: 24px; white-space: pre-wrap; border-right: 2px solid black; animation: blink 0.7s steps(2, start) infinite; } @keyframes blink { to { border-color: transparent; } } .code-header { position: absolute; top: 0; left:0; right: 0; width: 100%; color: #cdcdcd; height:24px; background : #6767; } .language-label { position: absolute; top: -4px; left: 0; padding: 0 10px; font-size: 0.8em; } .copy-button { position: absolute; top: 0; right: 0px; padding: 0.3em 0.6em; border: none; border-radius: 3px; cursor: pointer; font-size: 0.8em; } .code-content{ line-height: 20px; display: block; text-wrap: nowrap; margin-top: -40px; margin-bottom: -63px; overflow-x: scroll; } .historylist pre{ position: relative; background: black; color: white; border-radius: 6px; padding: 1em; margin: 1em 0; } .file-upload { position: relative; width: 30px; height: 30px; margin: 6px -1px 3px 10px; overflow: hidden; } .sendbtn{ margin: 13px; } .file-upload input[type=file] { position: absolute; top: 0; left: 0; opacity: 0; cursor: pointer; } ol{ list-style: none; padding: 0;margin: 10px; } i{ cursor: pointer; } .recomminput li{ background: aliceblue; border: 1px solid aliceblue; margin: 2px 0 4px 0; padding: 5px; width: 64%; font-size: 12px; cursor: pointer; border-radius: 5px; } .copybtn,.regenbtn{ cursor: pointer; margin-right: 20px; } .scrollbar{ position: absolute; top: -20px; width: 100%; right:0; left: 0; } .scrollbar i{ width: 30px; height: 30px; border-radius: 15px; background: white; line-height: 30px; } .attachpannel{ position: absolute; display: flex; width: 100%; height: 100px; left: 0; top:-80px; } /* 隐藏默认滚动条 */ textarea::-webkit-scrollbar { display: none; } /* 添加自定义滚动条 */ textarea { scrollbar-width: thin; /* 调整滚动条宽度 */ scrollbar-color: #ccc transparent; /* 调整滚动条颜色 */ overflow-y: auto; /* 确保溢出时出现滚动条 */ } /* Firefox 上的滚动条样式 */ textarea { scrollbar-width: thin; } /* WebKit 上的滚动条样式 */ textarea::-webkit-scrollbar { width: 8px; /* 调整滚动条宽度 */ } textarea::-webkit-scrollbar-track { background-color: transparent; /* 滚动条背景颜色 */ } textarea::-webkit-scrollbar-thumb { background-color: #ccc; /* 滚动条颜色 */ border-radius: 4px; /* 滚动条圆角 */ } textarea::-webkit-scrollbar-thumb:hover { background-color: #999; /* 鼠标悬停时的滚动条颜色 */ } /* 隐藏默认滚动条 */ .historylist::-webkit-scrollbar { display: none; } /* 添加自定义滚动条 */ .historylist { scrollbar-width: thin; /* 调整滚动条宽度 */ scrollbar-color: #ccc transparent; /* 调整滚动条颜色 */ overflow-y: auto; /* 确保溢出时出现滚动条 */ } /* Firefox 上的滚动条样式 */ .historylist { scrollbar-width: thin; } /* WebKit 上的滚动条样式 */ .historylist::-webkit-scrollbar { width: 8px; /* 调整滚动条宽度 */ } .historylist::-webkit-scrollbar-track { background-color: transparent; /* 滚动条背景颜色 */ } .historylist::-webkit-scrollbar-thumb { background-color: #ccc; /* 滚动条颜色 */ border-radius: 4px; /* 滚动条圆角 */ } .historylist::-webkit-scrollbar-thumb:hover { background-color: #999; /* 鼠标悬停时的滚动条颜色 */ } /* 隐藏默认滚动条 */ .code-content::-webkit-scrollbar { display: none; } /* 添加自定义滚动条 */ .code-content { scrollbar-width: thin; /* 调整滚动条宽度 */ scrollbar-color: #ccc transparent; /* 调整滚动条颜色 */ overflow-y: auto; /* 确保溢出时出现滚动条 */ } /* Firefox 上的滚动条样式 */ .code-content{ scrollbar-width: thin; } /* WebKit 上的滚动条样式 */ .code-content::-webkit-scrollbar { width: 8px; /* 调整滚动条宽度 */ } .code-content::-webkit-scrollbar-track { background-color: transparent; /* 滚动条背景颜色 */ } .code-content::-webkit-scrollbar-thumb { background-color: #ccc; /* 滚动条颜色 */ border-radius: 4px; /* 滚动条圆角 */ } .code-content::-webkit-scrollbar-thumb:hover { background-color: #999; /* 鼠标悬停时的滚动条颜色 */ } #overlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.8); z-index: 9999; justify-content: center; align-items: center; } #overlay img { max-width: 100%; max-height: 100%; } #closeBtn { position: absolute; top: 10px; right: 10px; color: white; border: none; padding: 10px; cursor: pointer; font-size: 16px; } .feedbackpanel{ font-size: 12px; } </style> </head> <body> <div id="overlay" onclick="hideImage()"> <i id="closeBtn" onclick="hideImage()" class="fa fa-lg fa-times-circle"></i> <img id="overlayImage" src="" alt="Fullscreen Image"> </div> <div id="app" class="cont" > <div style="text-align:center;"> <h2>CHATGPT API实现AI聊天</h2> </div> <div id="historylist" @scroll="handleScroll" class="historylist"> <div v-for="(msg,index) in newmess" :key="index" :class="{'mesay': msg.role === 'user', 'aisay': msg.role === 'system'}" > <div v-html="msg.content"></div> <div v-if="aistatus==1&&msg.content==''&&msg.role === 'system'"><img style="height:30px;" src='//repo.bfw.wiki/bfwrepo/icon/667d490a27acd.gif' /></div> <div v-if="aistatus==2&&msg.content==''&&msg.role === 'system'">调用插件中……</div> <div v-if="msg.role === 'system'&&msg.isfinished" class="feedbackpanel"><a title="复制" class="copybtn" @click="copy(index)"><i class="fa fa-copy"></i></a><a title="重新生成" class="regenbtn" @click="regen(index)"><i class="fa fa-rotate-left"></i></a> <a title="非常好" class="regenbtn" ><i class="fa fa-thumbs-o-up"></i></a> <a title="不好" class="regenbtn" ><i class="fa fa-thumbs-o-down"></i></a> </div> </div> <div class="recomminput"> <ol> <li v-for="msg in recommtips" @click="askai(msg)"> {{msg}} </li> </ol> </div> </div> <div class="footer"> <div class="scrollbar" v-if="!autoscroll" @click="tobottom" title="滚动到底部" style="text-align:center;"> <i class="fa fa-lg fa-angle-double-down"></i> </div> <div v-if="attachfile!=''" class="attachpannel"> <img title="附件" :src="attachfile" style="width:100px;" /> <i @click="attachfile=''" class="fa fa-lg fa-times-circle"></i> </div> <div id="inputpannel" class="inputpannel"> <div class="file-upload" id="fileUpload" > <input type="file" id="fileInput" name="file" accept="image/*"> <i title="上传附件" class="fa fa-lg fa-plus-circle"></i> </div> <textarea class="inputtext" v-model="userInput" oninput="this.style.height = '';this.style.height = this.scrollHeight + 'px'" @keydown.enter.prevent="sendai" placeholder="请输入问题"></textarea> <div @click="sendai" class="sendbtn"> <i title="发送" v-if="!aireplying" class="fa fa-lg fa-send"> </i> <i title="停止" v-if="aireplying" class="fa fa-lg fa-stop-circle"> </i> </div> </div> </div> </div> <script> localforage.setDriver([ localforage.INDEXEDDB, localforage.WEBSQL, localforage.LOCALSTORAGE]); // 自定义渲染器 const renderer = new marked.Renderer(); renderer.code = function(code, language) { const escapedCode = this.options.highlight(code, language); return ` <pre class="prettyprint language-${language}"> <div class="code-header"> <span class="language-label">${language}</span> <button class="copy-button" onclick="copyCode(this)">Copy</button></div> <code class="code-content language-${language}">${escapedCode}</code> </pre> `; }; // 重写 image 渲染方法 renderer.image = (href, title, text) => { return ` <img src="${href}" alt="${text}" title="${title}" onclick="handleImageClick('${href}')"> `; }; // 定义图像点击处理函数 window.handleImageClick = (src) => { showImage(src); // 你可以在这里添加更多的处理逻辑 }; // 设置 marked 选项 marked.setOptions({ renderer: renderer, highlight: function(code) { return hljs.highlightAuto(code).value; }, sanitize: true }); var vueinstance = new Vue({ el: '#app', data() { return { aistatus:1, recommtips:[], messages: [], attachfile:"", autoscroll:true, btntext: "发送", userInput: '', apiUrl: 'https://api.openai.com/v1/chat/completions', apiKey:"", controller: null, displayedText: "", currentIndex: 0, gettext: "", fullText: '', aireplying:false, typingSpeed: 40, // Typing speed in milliseconds typingTimer: null }; }, computed: { newmess() { return this.messages.map(element => { return { role: element.role, content: marked.parse(element.content) ,isfinished:element.isfinished}; }); } }, mounted(){ var that=this; localforage.getItem("localmessage").then(function (readValue) { .........完整代码请登录后点击上方下载按钮下载查看
网友评论0