123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580 |
- <template>
- <div class="ai-content-wrap">
- <div class="window-list-wrap">
- <div class="window-title">
- <div class="title-wrap">
- <p style="color: #333333;font-size: 20px;font-weight: 600;">HORIZON INSIGHTS</p>
- <span style="color: #666666;font-size: 16px;">弘则研究AI问答小助手</span>
- </div>
- <div class="icon"><img src="~@/assets/img/icons/horizon.png" /></div>
- </div>
- <div class="add-btn" @click="handleAddNewWindow"><i class="el-icon-circle-plus-outline"></i>新建对话窗口</div>
- <div class="list-wrap hidden-scrollbar">
- <Window-List-Item
- ref="windowListItem"
- v-for="item in windowList" :key="item.AiChatTopicId"
- :activeWindowId="activeWindowId"
- :item="item"
- @click.native="changeActiveWindow(item)"
- @changeEdit="changeWindowTitle"
- @delete="deleteWindowItem"
- />
- <div class="empty-list" v-if="windowList.length===0">
- <img src="~@/assets/img/ai_m/empty_list.png" />
- </div>
- </div>
- </div>
- <div class="content-wrap">
- <div class="content-header">
- <div class="title-wrap">
- <p>{{activeWindowId<=0?'新对话窗口':activeWindow.TopicName||''}}</p>
- <span>{{activeWindowId<=0?'弘则AI助手使用说明':`${historyList.length||0} messages`}}</span>
- </div>
- <div class="select-box">
- <!-- <el-select v-model="model" :class="{'hint':showHint}" :disabled="isTyping||(windowContentLoading&&windowContentLoading.visible)" ref="modelSelect"
- @click.native="selectClick"
- @change="changeModel">
- <el-option v-for="item in modelList" :key="item.label"
- :label="item.label"
- :value="item.label">
- <span style="float:left">{{item.label}}</span>
- <span style="float:right"><img :src="item.icon" style="margin-top:5px;width:24px;height:24px;"/></span>
- </el-option>
- </el-select> -->
- </div>
- </div>
- <!-- 仅这一部分滚动 -->
- <div class="window-content-wrap hidden-scrollbar">
- <div class="content-item" v-for="item in historyList" :key="item.AiChatId">
- <!-- user 提问 -->
- <Message-Item :messageInfo="formatMsg(item,'user')"
- @startTyping="handleStartTyping"
- @finishedTyping="handleFinishedTyping"
- />
- <!-- 模型回答 -->
- <Message-Item :messageInfo="formatMsg(item,'model')"
- @startTyping="handleStartTyping"
- @finishedTyping="handleFinishedTyping"
- />
- </div>
- <New-Window-Hint v-if="activeWindowId===0"/>
- </div>
- <div class="input-box">
- <textarea rows="6" v-model="inputText" placeholder="请输入提问,Shift+Enter换行" @keydown.enter="handleSendMsg"></textarea>
- <div class="send-btn" @click="handleSendMsg"><img src="~@/assets/img/ai_m/send.png" />发送</div>
- </div>
- </div>
- </div>
- </template>
- <script>
- /* components */
- import MessageItem from './components/messageItem.vue';
- import NewWindowHint from './components/newWindowHint.vue';
- import WindowListItem from './components/windowListItem';
- /* api */
- import {aiQAInterence} from '@/api/modules/aiApi.js';
- export default {
- components: { WindowListItem,NewWindowHint, MessageItem },
- data() {
- return {
- /* window */
- activeWindowId:0,//当前激活的窗口id
- activeWindow:null,//当前激活窗口
- windowList:[],//窗口列表
- listWrapLoading:null,
- /* window-content*/
- historyList:[],//当前窗口历史记录
- inputText:'',
- model:'GPT-4 Turbo',//当前选择的模型
- modelOldValue:'',
- modelList:[
- {
- label:'GPT-4 Turbo',
- icon:require('@/assets/img/icons/gpt-4-turbo.png'),
- },
- {
- label:'GPT4',
- icon:require('@/assets/img/icons/gpt-4.png'),
- },
- {
- label:'gpt-3.5-turbo-16k',
- icon:require('@/assets/img/icons/chat-gpt-16k.png'),
- },
- {
- label:'gpt-3.5-turbo',
- icon:require('@/assets/img/icons/chat-gpt.png'),
- },
- {
- label:'eta',
- icon:require('@/assets/img/icons/horizon.png'),
- },
- ],//模型列表
- showHint:false,//选择模型提示
- isTyping:false,//是否处于打字动画中
- windowContentLoading:null,
- answerLoading:false,//回答中
- };
- },
- watch:{
- model(newVal,oldVal){
- this.modelOldValue = oldVal
- }
- },
- computed:{
- modelIcon(){
- const modelItem = this.modelList.find(i=>i.label===this.model)
- return modelItem?modelItem.icon:''
- }
- },
- methods: {
- //改变活跃窗口
- changeActiveWindow(item){
- if(item.AiChatTopicId===0){
- this.handleAddNewWindow()
- return
- }
- if(item.AiChatTopicId===this.activeWindowId) return
- if(this.windowContentLoading&&this.windowContentLoading.visible){
- this.$message.warning('上个窗口未加载完成,请稍等')
- return
- }
- this.activeWindowId=item.AiChatTopicId
- this.activeWindow = this.windowList.find(i=>i.AiChatTopicId===item.AiChatTopicId)||{}
- this.historyList = []
- this.isTyping = false
- this.getWindowDetail()
- //切换窗口是否重置输入框
- //this.inputText=''
- },
- //获取对话窗口具体信息
- getWindowDetail(){
- this.windowContentLoading = this.$loading({
- target:document.querySelector('.window-content-wrap'),
- });
- aiQAInterence.getTopicDetail({
- AiChatTopicId:this.activeWindowId
- }).then(res=>{
- if(res.Ret!==200) return
- const {List} = res.Data
- this.historyList = List||[]
- this.windowContentLoading&&this.windowContentLoading.close()
- //使用模型
- this.model = this.historyList.length?this.historyList[this.historyList.length-1].Model:'GPT-4 Turbo'
- //如果有历史记录,则滚动到底部
- this.$nextTick(()=>{
- const windowContentWrap = document.querySelector('.window-content-wrap')
- windowContentWrap.scrollTo({
- top:windowContentWrap.scrollHeight,
- behavior:'smooth'
- })
- })
- })
- },
- //格式化对话信息
- formatMsg(msg,type){
- let msgObj = {
- messageId:msg.AiChatId,
- messageTime:'',
- messageText:'',
- messageType:'',
- modelName:''
- }
- const {Ask,Answer,CreateTime,ModifyTime,isPlay,Model} = msg
- if(type==='user'){
- msgObj.messageText = Ask||''
- msgObj.messageType = 'question'
- msgObj.messageTime = CreateTime||''
- }
- else{
- msgObj.messageText = Answer||''
- msgObj.messageType = 'answer'
- msgObj.messageTime = ModifyTime||''
- msgObj.modelName = Model||''
- msgObj.isPlay = Boolean(isPlay)
- }
- return msgObj
- },
- //新建对话窗口
- handleAddNewWindow(){
- //禁止重复新建
- if(this.activeWindowId===0){
- this.$message.warning("您已建立对话窗口,请输入提问")
- return
- }
- //将相关值置空
- this.activeWindowId=0
- this.activeWindow=null
- this.historyList=[]
- this.model='GPT-4 Turbo'
- //this.inputText=''
- this.isTyping = false
- },
- //开始播放动画
- handleStartTyping(){
- this.isTyping = true
- },
- //结束播放动画
- handleFinishedTyping(item){
- this.isTyping = false
- const {messageId} = item
- const index = this.historyList.findIndex(i=>i.AiChatId===messageId)
- index!==-1&&(this.historyList[index].isPlay = false)
- },
- //编辑对话窗口名称
- changeWindowTitle(title){
- aiQAInterence.editTopicName({
- AiChatTopicId:this.activeWindowId,
- TopicName:title
- }).then(res=>{
- if(res.Ret!==200) return
- this.$message.success('保存成功')
- this.getWindowList()
- })
- },
- //删除对话窗口
- deleteWindowItem(){
- const index = this.windowList.findIndex(i=>i.AiChatTopicId===this.activeWindowId)||0
- //更改activeWindowId
- /**
- * 一般情况:指向下一个
- * 若当前为最后一个,指向上一个
- * 如果是唯一一个,则置为0
- */
- let AiChatTopicId = 0
- if(this.windowList.length===1){
- AiChatTopicId=0
- }else if(index===this.windowList.length-1){
- AiChatTopicId = this.windowList[index-1].AiChatTopicId
- }else {
- AiChatTopicId = this.windowList[index+1].AiChatTopicId
- }
- aiQAInterence.deleteTopic({
- AiChatTopicId:this.activeWindowId
- }).then(res=>{
- if(res.Ret!==200) return
- this.getWindowList()
- this.changeActiveWindow({AiChatTopicId})
- })
- },
- //选择模型
- changeModel(value){
- //console.log('change',value)
- //非新建窗口时,模型之间切换需弹窗提示
- if(this.activeWindowId!==0&&value!==''){
- //弹窗提示:切换模型
- this.$confirm("切换回答模型,则切换后的模型无法联系上下文进行回答,确认切换吗?", "提示", {
- type: "warning"
- }).then(()=>{
- this.$message.success('切换模型成功')
- }).catch(()=>{
- //this.$message.success('已取消切换模型')
- this.model= this.modelOldValue
- this.modelOldValue=''
- })
- }
- //新建窗口时,若选择了模型,则移除hint效果
- if(this.activeWindowId===0&&value!==''){
- this.showHint=false
- }
- },
- //点击模型选择框
- selectClick(){
- if(this.isTyping){
- this.$message.warning('请等待回答完成')
- }
- },
- //发送消息
- handleSendMsg(e){
- e.preventDefault()
- if(e.shiftKey===true) {
- this.inputText+='\n'
- return
- }
- if(this.isTyping||this.answerLoading){
- this.$message.warning('请等待回答完成')
- return
- }
- //新建窗口,未选择模型
- if(this.activeWindowId===0&&this.model===''){
- this.showHint = true
- this.$message.error('请选择模型')
- this.$nextTick(()=>{
- this.$refs.modelSelect.focus()
- })
- return
- }
- if(this.inputText.length===0){
- this.$message.warning('请输入提问')
- return
- }
- this.answerLoading=true
- this.activeWindowId===0&&(this.activeWindowId = -1)
- //this.activeWindowId!==0&&this.getWindowDetail()
- //mock 加入到historyList中
- const msgObj = {
- AiChatId:-1,
- AiChatTopicId:0,
- Ask:this.inputText,
- Answer:'回答生成中...',
- CreateTime:'',
- ModifyTime:'',
- Model:this.model
- }
- this.historyList.push(msgObj)
-
- //滚动到底部
- this.$nextTick(()=>{
- const windowContentWrap = document.querySelector('.window-content-wrap')
- windowContentWrap.scrollTo({
- top:windowContentWrap.scrollHeight,
- behavior:'smooth'
- })
- })
- const inputText = this.inputText
- this.inputText = ''
- aiQAInterence.sendChatMsg({
- AiChatTopicId:this.activeWindowId<=0?0:this.activeWindowId,
- Ask:inputText,
- Model:this.model
- }).then(res=>{
- this.answerLoading=false
- //在回答未获取前切换了新窗口
- if(this.historyList.length===0){
- this.getWindowList()
- return
- }
- const msg = this.historyList[this.historyList.length-1]
- if(res.Ret!==200){
- //提问失败
- msg.Answer = res.ErrMsg||res.Msg||''
- msg.isPlay = true
- this.historyList.splice(this.historyList.length-1,1,msg)
- return
- }
- //提问成功,替换对应数据
- const {AiChatTopicId,Answer,Ask,Model} = res.Data
- //this.inputText=''
- if(this.activeWindowId<=0){
- const windowItem = {
- AiChatTopicId:AiChatTopicId||0,
- TopicName:Ask,
- }
- this.windowList.push(windowItem)
- this.activeWindow = windowItem
- this.getWindowList()
- }
- //在回答未获取前切换了已有窗口
- if(this.activeWindowId>0&&this.activeWindowId!==AiChatTopicId) return
- this.activeWindowId = AiChatTopicId||0
- msg.Answer = Answer||''
- msg.Model = Model
- msg.isPlay = true
- this.historyList.splice(this.historyList.length-1,1,msg)
- }).catch(()=>{
- this.answerLoading=false
- })
- },
- //获取窗口列表
- getWindowList(){
- this.listWrapLoading = this.$loading({
- target:document.querySelector('.list-wrap'),
- background: 'rgba(244, 245, 249, 1)'
- });
- aiQAInterence.getTopicList().then(res=>{
- if(res.Ret!==200) return
- this.windowList = res.Data.List||[]
- this.listWrapLoading&&this.listWrapLoading.close()
- })
- }
- },
- mounted(){
- this.getWindowList()
- }
- };
- </script>
- <style lang="scss">
- .ai-content-wrap{
- .el-select.hint{
- .el-input.is-focus .el-input__inner{
- border-color: red;
- }
- .el-input__inner:focus{
- border-color: red;
- }
- }
- }
- </style>
- <style scoped lang="scss">
- $border-color:#3D52A1;
- .ai-content-wrap{
- width:calc(100% - 64px);
- height:calc(100% - 64px);
- display: flex;
- position:relative;
- margin:30px;
- border:1px solid $border-color;
- border-radius: 32px;
- overflow: hidden;
- min-width: 960px;
- .window-list-wrap{
- width:380px;
- border-right: 1px solid #E2E2E2;
- padding:30px;
- box-sizing: border-box;
- background-color: #F4F5F9;
- display: flex;
- flex-direction: column;
- .window-title{
- display: flex;
- justify-content: space-between;
- align-items: center;
- .icon img{
- width:58px;
- height:58px;
- }
- }
- .add-btn{
- border:2px dashed $border-color;
- border-radius:16px;
- padding:14px;
- margin-top:40px;
- text-align: center;
- font-size: 16px;
- color:#333333;
- cursor: pointer;
- background-color: #F4F5F9;
- transition: background-color .5s, color .5s;
- &:hover{
- background-color: white;
- color: $border-color;
- border-style: solid;
- i{
- color: $border-color;
- }
- }
- i{
- margin-right: 10px;
- font-weight: bold;
- font-size: 16px;
- }
- }
- .list-wrap{
- margin-top:30px;
- flex: 1;
- overflow-y: scroll;
- position:relative;
- .empty-list img{
- width:150px;
- height:150px;
- margin:0 auto;
- display: block;
- }
- }
- }
- .content-wrap{
- flex: 1;
- display: flex;
- flex-direction: column;
- min-width: 588px;
- .content-header{
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding:20px 30px;
- box-sizing: border-box;
- border-bottom: 1px solid #E2E2E2;
- .title-wrap{
- p{
- color: #333333;
- font-size: 18px;
- margin-bottom: 5px;
- }
- span{
- color: #999999;
- line-height: 18px;
- }
- }
- }
- .window-content-wrap{
- flex: 1;
- /* border-bottom: 1px solid black; */
- padding:30px;
- text-align: center;
- overflow-y: scroll;
- overflow-x:hidden;
- position:relative;
- .content-item{
- text-align: left;
- .item{
- width: 100%;
- display: flex;
- padding:15px 0;
- border-bottom: 1px solid black;
- .content-item-avatar{
- width: 50px;
- margin-right: 20px;
- img{
- width:38px;
- height:38px;
- }
- }
- .text{
- flex: 1;
- }
- }
- }
- }
- .input-box{
- padding:30px;
- position: relative;
- textarea{
- width:100%;
- border-radius: 16px;
- box-sizing: border-box;
- padding:20px 85px 20px 20px;
- font-size: 16px;
- resize: none;
- border-color: #E3E3E3;
- box-shadow: 0px 0px 12px rgba(0, 0, 0, 0.08);
- }
- .send-btn{
- position:absolute;
- bottom:50px;
- right:50px;
- padding:8px 12px;
- background-color: $border-color;
- border-radius: 6px;
- font-size: 14px;
- color: white;
- line-height: 14px;
- cursor: pointer;
- opacity: .7;
- transition: opacity .5s;
- img{
- width:14px;
- height:14px;
- margin-right: 8px;
- }
- &:hover,&.canClick{
- opacity: 1;
- }
- }
- }
- }
- .hidden-scrollbar{
- &::-webkit-scrollbar-track{
- display: none;
- }
- div::-webkit-scrollbar-track{
- display: none;
- }
- }
- }
- </style>
|