hbchen пре 1 година
родитељ
комит
16b73acb95

+ 21 - 0
src/api/modules/aiApi.js

@@ -47,5 +47,26 @@ export const aiQAInterence = {
      */
     editTopicName:params=>{
         return http.post('/ai/topic/edit',params)
+    },
+    /**
+     * AI文件上传
+     * @param {FormData} params
+     * @param {Object} params.File 文件
+     * @param {Number} params.AiChatTopicId 窗口Id
+     * @returns 
+    */
+    fileUpload:params=>{
+        return http.post('/ai/file/upload',params)
+    },
+    /**
+     * AI文件检索
+     * @param {Object} params
+     * @param {Number} params.AiChatTopicId
+     * @param {String} params.Ask
+     * @param {Array} params.OpenaiFileId [String]
+     * @returns 
+    */
+    fileRetrieve:params=>{
+        return http.post('/ai/file/retrieve',params)
     }
 }

BIN
src/assets/img/icons/ai-upload.png


BIN
src/assets/img/icons/file-upload.png


BIN
src/assets/img/icons/file_type_pdf.png


BIN
src/assets/img/icons/file_type_ppt.png


BIN
src/assets/img/icons/file_type_unknown.png


+ 176 - 23
src/views/operation_manage/AIQA/AIQA.vue

@@ -45,7 +45,7 @@
             </div>
             <!-- 仅这一部分滚动 -->
             <div class="window-content-wrap hidden-scrollbar">
-                <div class="content-item" v-for="item in historyList" :key="item.AiChatId">
+                <div class="content-item" v-for="item in historyList" :key="item.AiChatId" >
                     <!-- user 提问 -->
                     <Message-Item :messageInfo="formatMsg(item,'user')"
                         @startTyping="handleStartTyping"
@@ -57,9 +57,22 @@
                         @finishedTyping="handleFinishedTyping"
                     />
                 </div>
-                <New-Window-Hint v-if="activeWindowId===0"/>
+                <New-Window-Hint v-if="activeWindowId===0" />
             </div>
-            <div class="input-box">
+            <div class="input-box" id="input-box">
+                <div class="upload-row">
+                    <el-upload
+                    style="display: inline-block; margin-right: 8px"
+                    accept=".pptx,.pdf"
+                    action=""
+                    :http-request="handleUpload"
+                    :before-upload="handleBeforeUpload"
+                    :show-file-list="false"
+                    :disabled="startUploadAudio">
+                        <img src="~@/assets/img/icons/ai-upload.png" />
+                    </el-upload>
+                    <span>支持格式:PDF、PPTX;大小不超过10MB;要求纯文本,不含图片</span>
+                </div>
                 <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>
@@ -116,6 +129,9 @@ export default {
             windowContentLoading:null,
             answerLoading:false,//回答中
             companyName:'',
+            aiFileIds:[],
+            // 上传窗口的队列
+            windowSet:new Set()
         };
     },
     watch:{
@@ -162,16 +178,23 @@ export default {
                 if(res.Ret!==200) return
                 const {List} = res.Data
                 this.historyList = List||[]
+
+                this.aiFileIds=this.historyList.map(item => item.OpenaiFileId).filter(Boolean)
+
                 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'
-                    })
+                this.windowContentToBottom()
+            })
+        },
+        // 滚动到聊天窗口底部
+        windowContentToBottom(){
+            this.$nextTick(()=>{
+                const windowContentWrap = document.querySelector('.window-content-wrap')
+                windowContentWrap.scrollTo({
+                    top:windowContentWrap.scrollHeight,
+                    behavior:'smooth'
                 })
             })
         },
@@ -184,11 +207,12 @@ export default {
                 messageType:'',
                 modelName:''
             }
-            const {Ask,Answer,CreateTime,ModifyTime,isPlay,Model} = msg
+            const {Ask,Answer,CreateTime,ModifyTime,isPlay,Model,OpenaiFilePath} = msg
             if(type==='user'){
                 msgObj.messageText = Ask||''
                 msgObj.messageType = 'question'
                 msgObj.messageTime = CreateTime||''
+                msgObj.askFileUrl = OpenaiFilePath || ''
             }
             else{
                 msgObj.messageText = Answer||''
@@ -196,6 +220,7 @@ export default {
                 msgObj.messageTime = ModifyTime||''
                 msgObj.modelName = Model||''
                 msgObj.isPlay = Boolean(isPlay)
+                msgObj.askFileUrl = OpenaiFilePath || ''
             }
             return msgObj
         },
@@ -211,6 +236,7 @@ export default {
             this.activeWindow=null
             this.historyList=[]
             this.model='GPT-4 Turbo'
+            this.aiFileIds=[]
             //this.inputText=''
             this.isTyping = false
         },
@@ -319,7 +345,7 @@ export default {
             //mock 加入到historyList中
             const msgObj = {
                 AiChatId:-1,
-                AiChatTopicId:0,
+                AiChatTopicId:this.activeWindowId || 0,
                 Ask:this.inputText,
                 Answer:'回答生成中...',
                 CreateTime:'',
@@ -329,20 +355,23 @@ export default {
             this.historyList.push(msgObj)
             
             //滚动到底部
-            this.$nextTick(()=>{
-                const windowContentWrap = document.querySelector('.window-content-wrap')
-                windowContentWrap.scrollTo({
-                    top:windowContentWrap.scrollHeight,
-                    behavior:'smooth'
-                })
-            })
+            this.windowContentToBottom()
+
             const inputText = this.inputText
             this.inputText = ''
-            aiQAInterence.sendChatMsg({
+            let apiName = "sendChatMsg"
+            let params={
                 AiChatTopicId:this.activeWindowId<=0?0:this.activeWindowId,
                 Ask:inputText,
                 // Model:this.model
-            }).then(res=>{
+            }
+            if(this.aiFileIds && this.aiFileIds.length>0){
+                // 文件检索功能
+                apiName="fileRetrieve"
+                params.OpenaiFileId=this.aiFileIds
+            }
+            // console.log(params,"params");
+            aiQAInterence[apiName](params).then(res=>{
                 this.answerLoading=false
                 //在回答未获取前切换了新窗口
                 if(this.historyList.length===0){
@@ -387,7 +416,10 @@ export default {
             })
         },
         //获取窗口列表
-        getWindowList(){
+        /**
+         * @param {*} topicId AiChatTopicId 定位到具体窗口
+         */
+        getWindowList(topicId){
             this.listWrapLoading = this.$loading({
                 target:document.querySelector('.list-wrap'),
                 background: 'rgba(244, 245, 249, 1)'
@@ -395,6 +427,9 @@ export default {
             aiQAInterence.getTopicList().then(res=>{
                 if(res.Ret!==200) return
                 this.windowList = res.Data.List||[]
+                if(topicId){
+                    this.changeActiveWindow({AiChatTopicId:topicId})
+                }
                 this.listWrapLoading&&this.listWrapLoading.close()
             })
         },
@@ -405,11 +440,112 @@ export default {
                     this.companyName=res.Data.CompanyName||''
                 }
             })
+        },
+        inputBoxDragover(event){
+            event.preventDefault(); //阻止默认行为,允许放置
+        },
+        inputBoxDrop(event){
+            event.preventDefault(); //阻止浏览器默认行为
+            // 获取文件的数据
+            const DataTransferItemList = event.dataTransfer.files
+            if(DataTransferItemList && DataTransferItemList.length){
+                if(DataTransferItemList.length>1){
+                    return this.$message.error("单次只能上传一个文件,请重试");
+                }else{
+                    let file = DataTransferItemList[0]
+                    if(file.type && (file.name.endsWith('.pdf')||file.name.endsWith('.pptx'))){
+                        if(file.size/1024/1024 > 10.1){
+                            this.$message.error("上传文件大小不超过10MB");
+                            return false;
+                        }
+                        this.handleUpload({file})
+                    }else{
+                        return this.$message.error("上传文件格式只支持PDF、PPTX");
+                    }
+                }
+            }else{
+                // 没有文件数据
+                let txt = event.dataTransfer.getData("text/plain")
+                this.inputText=txt
+            }
+        },
+        handleBeforeUpload(e) {
+            if(!(e.name.endsWith('.pdf') || e.name.endsWith('.pptx'))){
+                this.$message.error("上传文件格式只支持PDF、PPTX");
+                return false;
+            }
+            if(!(e.size/1024/1024 < 10.1)){
+                this.$message.error("上传文件大小不超过10MB");
+                return false;
+            }
+        },
+        handleUpload(e){
+            // console.log(this.windowSet,this.activeWindowId);
+            if(this.windowSet.has(this.activeWindowId)){
+                return this.$message.warning("请等待文件上传完成")
+            }
+
+            let {file} = e
+            let downloadHint = this.$message({
+                type:"info",
+                message:'上传中,请稍后······',
+                duration:0,
+                iconClass:'el-icon-loading'
+            })
+            let formData = new FormData()
+            formData.append('File',file)
+            formData.append('AiChatTopicId',this.activeWindowId)
+            this.windowSet.add(this.activeWindowId)
+            aiQAInterence.fileUpload(formData).then(res=>{
+                downloadHint.close()
+                if(res.Ret == 200){
+                    let Data = res.Data || {}
+                    this.$message.success(`${Data.ResourceName}上传成功`)
+                    if(this.windowList.find(item => item.AiChatTopicId == Data.AiChatTopicId)){
+                        // 窗口存在
+                        this.windowSet.delete(Data.AiChatTopicId)
+
+                        if(Data.AiChatTopicId == this.activeWindowId){
+                            this.aiFileIds.push(Data.OpenaiFileId)
+                            const msgObj = {
+                                AiChatId:Data.AiChatId || -1,
+                                AiChatTopicId:Data.AiChatTopicId,
+                                Ask:Data.ResourceName,
+                                OpenaiFilePath:Data.ResourceUrl,
+                                Answer:Data.Answer || '',
+                                CreateTime:Data.CreateTime||'',
+                                ModifyTime:Data.ModifyTime || '',
+                                Model:this.model
+                            }
+                            this.historyList.push(msgObj)
+                            this.windowContentToBottom()
+                        }
+                    }else{
+                        //窗口不存在
+                        this.windowSet.delete(0)
+                        this.getWindowList(this.activeWindowId==0 ? Data.AiChatTopicId:0)
+                    }
+                }
+            }).catch(()=>{
+                downloadHint.close()
+                // 失败,清空
+                this.windowSet.clear()
+            })
         }
     },
     mounted(){
         this.getWindowList()
         this.getBaseConfig()
+        const dropDom = document.getElementById('input-box')
+        dropDom.addEventListener('dragover',this.inputBoxDragover);
+
+        dropDom.addEventListener('drop',this.inputBoxDrop);
+    },
+    beforeDestroy(){
+        const dropDom = document.getElementById('input-box')
+        dropDom.removeEventListener('dragover',this.inputBoxDragover);
+
+        dropDom.removeEventListener('drop',this.inputBoxDrop);
     }
 };
 </script>
@@ -556,13 +692,30 @@ $border-color:#3D52A1;
             }
         }
         .input-box{
-            padding:30px;
+            padding:12px 30px 30px;
             position: relative;
+            .upload-row{
+                display: flex;
+                align-items: center;
+                padding: 0 20px;
+                margin-bottom: 12px;
+                img{
+                    height: 20px;
+                    width: 20px;
+                    vertical-align: bottom;
+                    box-shadow: 3px 3px 8px 0px #182c421f;
+                }
+                span{
+                    color: #A5A5A5;
+                    font-size: 15px;
+                    font-weight: 400;
+                }
+            }
             textarea{
                 width:100%;
                 border-radius: 16px;
                 box-sizing: border-box;
-                padding:20px 85px 20px 20px;
+                padding:20px 95px 20px 20px;
                 font-size: 16px;
                 resize: none;
                 border-color: #E3E3E3;

+ 42 - 4
src/views/operation_manage/AIQA/components/messageItem.vue

@@ -1,10 +1,21 @@
 <template>
-    <div class="message-item-wrap" :class="messageInfo.messageType">
+    <div class="message-item-wrap" :class="messageInfo.messageType" 
+    v-if="!(messageInfo.messageType=='answer' && messageInfo.askFileUrl && (!messageInfo.messageText))">
+    <!-- 上传文件时,不想有回答 -->
         <div class="message-icon"><img :src="IconSrc"  v-if="IconSrc"/></div>
         <div class="message-info">
             <span class="message-time" :class="messageInfo.messageType">{{messageInfo.messageTime}}</span>
-            <div class="message-text" :class="messageInfo.messageType" v-if="!isPlay">{{messageInfo.messageText}}</div>
-            <div class="message-text typing" :class="messageInfo.messageType" v-else>{{typingText}}</div>
+            <div class="message-text" :class="messageInfo.messageType">
+                <template v-if="messageInfo.askFileUrl">
+                    <div class="message-file" @click="fileClickHandle(messageInfo.askFileUrl)">
+                        <img :src="getFileIcon" />
+                        <span :class="{'typing':isPlay}">{{isPlay ? typingText : messageInfo.messageText}}</span>
+                    </div>
+                </template>
+                <template v-else>
+                    <span :class="{'typing':isPlay}">{{isPlay ? typingText : messageInfo.messageText}}</span>
+                </template>
+            </div>
         </div>
     </div>
 </template>
@@ -55,6 +66,19 @@ export default {
             const {messageType,modelName} = this.messageInfo
             const iconType = messageType==='question'?'user':'GPT-4 Turbo'
             return this.iconMap[iconType]||''
+        },
+        getFileIcon(){
+            if(this.messageInfo.askFileUrl){
+                if(this.messageInfo.askFileUrl.endsWith('.pdf')){
+                    return require('@/assets/img/icons/file_type_pdf.png')
+                }else if(this.messageInfo.askFileUrl.endsWith('.pptx')){
+                    return require('@/assets/img/icons/file_type_ppt.png')
+                }else{
+                    return require('@/assets/img/icons/file_type_unknown.png')
+                }
+            }else{
+                return ''
+            }
         }
     },
     methods: {
@@ -78,6 +102,9 @@ export default {
             this.isPlay = false
             this.typingText=''
             this.$emit('finishedTyping',this.messageInfo)
+        },
+        fileClickHandle(url){
+            window.open(url,"_blank")
         }
     },
 };
@@ -108,13 +135,24 @@ export default {
             padding:14px 20px;
             display: inline-block;
             white-space: pre-line;
+            .message-file{
+                display: flex;
+                align-items: center;
+                cursor: pointer;
+                margin: -4px;
+                img{
+                   height: 24px;
+                   width: 24px; 
+                   margin-right: 8px;
+                }
+            }
             &.answer{
                 background-color: #F4F4F4;
             }
             &.question{
                 background-color: #E3EFFD;
             }
-            &.typing{
+            .typing{
                 &::after{
                     content: '|';
                     animation: blink 1s infinite