123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677 |
- <template>
- <div class="ai-content-wrap">
- <div class="window-list-wrap">
- <div class="window-title">
- <div class="title-wrap">
- <p class="text-ellipsis" style="color: #333333;font-size: 20px;font-weight: 600;">{{companyName}}</p>
- <span style="color: #666666;font-size: 16px;">{{$t('ToolBox.AIQuestion.title')}}</span>
- </div>
- <div class="icon"><img src="~@/assets/img/icons/ai_company.png"/></div>
- </div>
- <div class="add-btn" @click="handleAddNewWindow"><i class="el-icon-circle-plus-outline"></i>{{$t('ToolBox.AIQuestion.add_new_window')}}</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?$t('ToolBox.AIQuestion.new_window'):activeWindow.TopicName||''}}</p>
- <span>{{activeWindowId<=0?$t('ToolBox.AIQuestion.AI_use_intro'):`${historyList.length||0} messages`}}</span>
- </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" id="input-box">
- <div class="upload-row">
- <el-upload
- style="display: inline-block; margin-right: 8px"
- accept=".pptx,.pdf,.docx"
- action=""
- :http-request="handleUpload"
- :before-upload="handleBeforeUpload"
- :show-file-list="false"
- :disabled="startUploadAudio">
- <img src="~@/assets/img/icons/ai-upload.png" />
- </el-upload>
- <span>{{ $t('ToolBox.AIQuestion.support_formats') }}:PDF、PPTX、DOCX;{{ $t('ToolBox.AIQuestion.max_file_text') }}</span>
- </div>
- <textarea rows="6" v-model="inputText" :placeholder="$t('ToolBox.AIQuestion.input_placeholder')" @keydown.enter="handleSendMsg"></textarea>
- <div class="send-btn" @click="handleSendMsg"><img src="~@/assets/img/ai_m/send.png" />{{$t('ToolBox.AIQuestion.send_btn')}}</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';
- import {etaBaseConfigInterence} from '@/api/modules/etaBaseConfigApi.js';
- export default {
- components: { WindowListItem,NewWindowHint, MessageItem },
- data() {
- return {
- /* window */
- activeWindowId:0,//当前激活的窗口id
- activeWindow:null,//当前激活窗口
- windowList:[],//窗口列表
- listWrapLoading:null,
- /* window-content*/
- historyList:[],//当前窗口历史记录
- inputText:'',
- model:'Kimi',//当前选择的模型
- modelOldValue:'',
- showHint:false,//选择模型提示
- isTyping:false,//是否处于打字动画中
- windowContentLoading:null,
- answerLoading:false,//回答中
- companyName:'',
- aiFileIds:[],
- // 上传窗口的队列
- windowSet:new Set(),
- fileTypeRule:new RegExp(/\.pdf|\.pptx|\.docx$/,'i')
- };
- },
- watch:{
- model(newVal,oldVal){
- this.modelOldValue = oldVal
- }
- },
- computed:{
- },
- 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()
- },
- //获取对话窗口具体信息
- 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.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:'Kimi'
- //如果有历史记录,则滚动到底部
- this.windowContentToBottom()
- })
- },
- // 滚动到聊天窗口底部
- windowContentToBottom(){
- 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,OpenaiFilePath} = msg
- if(type==='user'){
- msgObj.messageText = Ask||''
- msgObj.messageType = 'question'
- msgObj.messageTime = CreateTime||''
- msgObj.askFileUrl = OpenaiFilePath || ''
- }
- else{
- msgObj.messageText = Answer||''
- msgObj.messageType = 'answer'
- msgObj.messageTime = ModifyTime||''
- msgObj.modelName = Model||''
- msgObj.isPlay = Boolean(isPlay)
- msgObj.askFileUrl = OpenaiFilePath || ''
- }
- return msgObj
- },
- //新建对话窗口
- handleAddNewWindow(){
- //禁止重复新建
- if(this.activeWindowId===0){
- this.$message.warning(this.$t('ToolBox.AIQuestion.add_new_window_hint'))
- return
- }
- //将相关值置空
- this.activeWindowId=0
- this.activeWindow=null
- this.historyList=[]
- this.model='Kimi'
- this.aiFileIds=[]
- 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.$t('MsgPrompt.saved_msg'))
- 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})
- })
- },
- //点击模型选择框
- //发送消息
- handleSendMsg(e){
- e.preventDefault()
- if(e.shiftKey===true) {
- this.inputText+='\n'
- return
- }
- if(this.isTyping||this.answerLoading){
- this.$message.warning('请等待回答完成')
- return
- }
- //新建窗口,未选择模型
- if(this.inputText.length===0){
- this.$message.warning(this.$t('ToolBox.AIQuestion.inter_check'))
- return
- }
- this.answerLoading=true
- this.activeWindowId===0&&(this.activeWindowId = -1)
- //mock 加入到historyList中
- const msgObj = {
- AiChatId:-1,
- AiChatTopicId:this.activeWindowId || 0,
- Ask:this.inputText,
- Answer:'回答生成中...',
- CreateTime:'',
- ModifyTime:'',
- Model:this.model
- }
- this.historyList.push(msgObj)
-
- //滚动到底部
- this.windowContentToBottom()
- const inputText = this.inputText
- this.inputText = ''
- let apiName = "sendChatMsg"
- let params={
- AiChatTopicId:this.activeWindowId<=0?0:this.activeWindowId,
- Ask:inputText,
- Model:this.model
- }
- if(this.aiFileIds && this.aiFileIds.length>0){
- // 文件检索功能
- apiName="fileRetrieve"
- params.OpenaiFileId=this.aiFileIds
- }
- aiQAInterence[apiName](params).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
- 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
- const msg = this.historyList[this.historyList.length-1]
- msg.Answer = '回答超时,请重试!'
- msg.isPlay = true
- this.historyList.splice(this.historyList.length-1,1,msg)
- })
- },
- //获取窗口列表
- /**
- * @param {*} topicId AiChatTopicId 定位到具体窗口
- */
- getWindowList(topicId){
- 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||[]
- if(topicId){
- this.changeActiveWindow({AiChatTopicId:topicId})
- }
- this.listWrapLoading&&this.listWrapLoading.close()
- })
- },
- getBaseConfig(){
- etaBaseConfigInterence.getBaseConfig().then(res=>{
- if(res.Ret===200){
- 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 && this.fileTypeRule.test(file.name)){
- if(file.size/1024/1024 > 50.1){
- this.$message.error("上传文件大小不超过50MB");
- return false;
- }
- this.handleUpload({file})
- }else{
- return this.$message.error("上传文件格式只支持PDF、PPTX、DOCX");
- }
- }
- }else{
- // 没有文件数据
- let txt = event.dataTransfer.getData("text/plain")
- this.inputText=txt
- }
- },
- handleBeforeUpload(e) {
- if(!this.fileTypeRule.test(e.name)){
- this.$message.error("上传文件格式只支持PDF、PPTX、DOCX");
- return false;
- }
- if(!(e.size/1024/1024 < 50.1)){
- this.$message.error("上传文件大小不超过50MB");
- return false;
- }
- },
- handleUpload(e){
- if(this.windowSet.has(this.activeWindowId)){
- return this.$message.warning("请等待文件上传完成")
- }
- let {file} = e
- let downloadHint = this.$message({
- type:"info",
- message:/* '上传中,请稍后······' */this.$t('ReportManage.CloudPage.upload_msg'),
- duration:0,
- iconClass:'el-icon-loading'
- })
- let formData = new FormData()
- formData.append('File',file)
- formData.append('AiChatTopicId',this.activeWindowId)
- formData.append('Model',this.model)
- 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>
- <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;
- }
- .title-wrap{
- overflow: hidden;
- }
- .text-ellipsis{
- overflow: hidden;
- white-space: nowrap;
- text-overflow: ellipsis;
- }
- }
- .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;
- 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: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 95px 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>
|