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