7 changed files with 879 additions and 70 deletions
@ -0,0 +1,325 @@ |
|||
<!-- |
|||
@ 作者: han2015 |
|||
@ 时间: 2025-05-12 15:39:13 |
|||
@ 备注: aibot组件 |
|||
--> |
|||
<script lang="ts" setup> |
|||
import { |
|||
Promotion, |
|||
Remove |
|||
} from '@element-plus/icons-vue' |
|||
import { doAiTraining,doAiChat,aiChatData,getAiChatList,getAiChat,setAiChat,delAiChat} from "@/api/doc/space" |
|||
import {ElText,ElInput, ButtonInstance} from "element-plus"; |
|||
import { VueMarkdown } from '@crazydos/vue-markdown' |
|||
import rehypeRaw from 'rehype-raw' |
|||
import remarkGfm from 'remark-gfm' |
|||
|
|||
//选中的咨询模式 |
|||
const checkedModel = ref([]) |
|||
//支持的模式 |
|||
const aimodels = [{name:'联网检索',key:"onlineSearch"}, {name:'公司知识库',key:"useDataset"}] |
|||
const baseURL=import.meta.env.VITE_APP_BASE_API |
|||
const conversation=ref("") //当前会话的uuid |
|||
const myquestion=ref('') |
|||
const controller = ref<AbortController | null>(null) |
|||
const inputState=ref(true) |
|||
const conversations=ref<chatRecord[]>([]) |
|||
const centHoverItem=ref("") |
|||
|
|||
const interact_msg=ref<{ask:boolean,think:string,content:string}[]>([]) |
|||
const props = withDefaults(defineProps<{ |
|||
userid:string, |
|||
closefunc:()=>void, |
|||
agent:{model:boolean,name:string,uuid:string} |
|||
}>(),{}) |
|||
|
|||
const respMsg=ref("") |
|||
|
|||
//消息体 |
|||
interface message{ |
|||
ask:boolean, |
|||
think:string, |
|||
content:string |
|||
} |
|||
//会话记录 |
|||
interface chatRecord{ |
|||
uuid:string, |
|||
agentuuid:string, |
|||
brief:string, |
|||
messages:message[] |
|||
} |
|||
|
|||
/* 中断函数,供按钮调用 */ |
|||
function abortFetch() { |
|||
if (controller.value) { |
|||
controller.value.abort() |
|||
controller.value = null |
|||
} |
|||
} |
|||
|
|||
async function onSendTextToAI(){ |
|||
if(myquestion.value==="")return; |
|||
inputState.value=false |
|||
|
|||
const params={ |
|||
"onlineSearch":"否", |
|||
"useDataset":"否" |
|||
} |
|||
for (let item of checkedModel.value){ |
|||
if(item==="onlineSearch") params.onlineSearch="是" |
|||
if(item==="useDataset") params.useDataset="是" |
|||
} |
|||
if (conversation.value==""){ |
|||
//开启新对话 |
|||
interact_msg.value=[{ask:true,think:"", content:myquestion.value}] |
|||
}else{ |
|||
interact_msg.value.push({ask:true,think:"", content:myquestion.value}) |
|||
} |
|||
|
|||
controller.value = new AbortController(); |
|||
try{ |
|||
const res= await doAiChat(`${baseURL}/aibot/agents/${props.agent.uuid}/chat`,{ |
|||
inputs: params, |
|||
query:myquestion.value, |
|||
response_mode:"streaming", |
|||
conversation_id:conversation.value, |
|||
user:atob(props.userid),//这里已经base64解析了 |
|||
},controller.value.signal |
|||
) |
|||
|
|||
if (!res.ok) { |
|||
throw new Error(`HTTP ${res.status} ${res.statusText}`); |
|||
} |
|||
|
|||
myquestion.value="" |
|||
const reader = res.body!.getReader(); |
|||
const decoder = new TextDecoder('utf-8'); |
|||
|
|||
let chunk = ''; // 行缓存 |
|||
while (true) { |
|||
const {done, value} = await reader.read() |
|||
if (done) break; |
|||
|
|||
// 服务器可能一次返回多行,需要手动按行拆分 |
|||
chunk += decoder.decode(value, {stream: true}); |
|||
const lines = chunk.split(/\n/); |
|||
chunk = lines.pop() || '' |
|||
for (const line of lines) { |
|||
if (!line.trim()) continue; // 跳过空行 |
|||
if (line.startsWith('data: ')) { |
|||
const data = line.slice(6); |
|||
const json = JSON.parse(data); |
|||
if(json.event==="message"){ |
|||
conversation.value=json.conversation_id |
|||
respMsg.value+=json.answer |
|||
} |
|||
} |
|||
} |
|||
} |
|||
}catch (e: any) { |
|||
if (e.name === 'AbortError') { |
|||
console.log('用户手动中断') |
|||
} |
|||
}finally{ |
|||
inputState.value=true |
|||
} |
|||
|
|||
const arr=respMsg.value.split("</think>") |
|||
if(arr.length===1){ |
|||
interact_msg.value.push({ask:false,think:"",content:arr[0]}) |
|||
}else{ |
|||
//思考模式 |
|||
interact_msg.value.push({ask:false,think:arr[0],content:arr[1]}) |
|||
} |
|||
|
|||
respMsg.value="" |
|||
|
|||
setAiChat({ |
|||
"userid":atob(props.userid), |
|||
"uuid":conversation.value, |
|||
"brief":interact_msg.value[0].content, |
|||
"content":JSON.stringify(interact_msg.value) |
|||
}) |
|||
} |
|||
|
|||
//加载交流记录列表 |
|||
function loadKnownLibList(){ |
|||
//userid 需要 base64解析 |
|||
getAiChatList({"userid":atob(props.userid)}).then(resp=>{ |
|||
conversations.value=resp.data |
|||
}) |
|||
} |
|||
|
|||
//查看具体的历史记录 |
|||
function showChat(uuid:string){ |
|||
getAiChat({ |
|||
"userid":atob(props.userid), |
|||
"uuid":uuid |
|||
}).then(resp=>{ |
|||
interact_msg.value = JSON.parse(resp.data.content) |
|||
conversation.value=resp.data.uuid |
|||
}) |
|||
} |
|||
|
|||
//删除具体的历史记录 |
|||
function onDelChat(uuid:string){ |
|||
delAiChat({ |
|||
"userid":atob(props.userid), |
|||
"uuid":uuid |
|||
}).then(resp=>{ |
|||
loadKnownLibList() |
|||
}) |
|||
} |
|||
|
|||
function handleMouseEnter(row:any){ |
|||
if(centHoverItem.value==row.uuid) return; |
|||
centHoverItem.value=row.uuid |
|||
} |
|||
function handleMouseLeave(){ |
|||
centHoverItem.value="" |
|||
} |
|||
|
|||
function newContext(){ |
|||
inputState.value=true |
|||
interact_msg.value=[] |
|||
conversation.value="" |
|||
loadKnownLibList() //刷新对话列表 |
|||
} |
|||
|
|||
function resetContext(){ |
|||
interact_msg.value=[] |
|||
conversation.value="" |
|||
props.closefunc() |
|||
} |
|||
|
|||
//渲染完页面再执行 |
|||
onMounted(() => { |
|||
loadKnownLibList() |
|||
}); |
|||
</script> |
|||
|
|||
<template> |
|||
<el-drawer |
|||
:model-value="agent.model" |
|||
:title="agent.name+' : 知识库'" |
|||
direction="rtl" |
|||
size="80%" |
|||
@close="resetContext" |
|||
:style="{padding:'17px',backgroundColor:'#f3f3f3'}"> |
|||
<div style="display:grid;grid-template-columns:1fr 4fr; width: 100%;height: 100%;"> |
|||
<div style="overflow-y: auto;"> |
|||
<ul> |
|||
<li class="action_menu" @click="newContext"> |
|||
【新建会话】 |
|||
</li> |
|||
<li class="list_item" v-for="item in conversations" @mouseover="handleMouseEnter(item)" @mouseleave="handleMouseLeave()" @click="showChat(item.uuid)">{{ item.brief }} |
|||
<el-button v-show="centHoverItem == item.uuid" icon="Delete" size="small" circle @click="(e)=>{e.stopPropagation();onDelChat(item.uuid)}"></el-button> |
|||
</li> |
|||
</ul> |
|||
</div> |
|||
<div style="position: relative;background: white;overflow-y: auto;"> |
|||
<div class="reply_area" > |
|||
<template v-for="msg of interact_msg"> |
|||
<el-text v-if="msg.ask" class="t_ask" >{{ msg.content }}</el-text> |
|||
<div v-else class="t_resp"> |
|||
<el-text style="white-space: pre-line" v-html="msg.think"></el-text> |
|||
<VueMarkdown :markdown="msg.content" :rehype-plugins="[rehypeRaw]" :remark-plugins="[remarkGfm]" ></VueMarkdown> |
|||
</div> |
|||
|
|||
</template> |
|||
<VueMarkdown :markdown="respMsg" :rehype-plugins="[rehypeRaw]" class="t_resp"></VueMarkdown> |
|||
</div> |
|||
<div class="question_com" :class="{newquestion:conversation!='' || !inputState}"> |
|||
<h1 v-show="conversation =='' && inputState" style="font-size: 78px;margin: 10px;">恒信高科AI平台</h1> |
|||
<el-checkbox-group v-model="checkedModel" style="display:flex; margin-bottom: 10px;"> |
|||
<el-checkbox-button v-for="mod in aimodels" :value="mod.key"> |
|||
{{ mod.name }} |
|||
</el-checkbox-button> |
|||
</el-checkbox-group> |
|||
|
|||
<el-input placeholder="问灵犀..." v-model="myquestion" input-style="border-radius: 20px;" |
|||
resize="none" :autosize="{minRows: 4}" type="textarea" /> |
|||
<el-button :style="{display :inputState ? '':'none'}" type="primary" :icon="Promotion" circle @click="onSendTextToAI"/> |
|||
<el-button :style="{display :inputState ? 'none':''}" type="primary" :icon="Remove" circle @click="abortFetch"/> |
|||
<span>内容由 AI 生成,请仔细甄别</span> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
</el-drawer> |
|||
</template> |
|||
|
|||
<style lang="scss" scoped> |
|||
.app_container { |
|||
padding: 10px 30px 0px 30px; |
|||
height: 100%; |
|||
width: 100%; |
|||
} |
|||
.newquestion{ |
|||
bottom: 20px; |
|||
} |
|||
.question_com{ |
|||
position: fixed; |
|||
width: 52%; |
|||
margin: 0 50px; |
|||
text-align: center; |
|||
display: block; |
|||
button{ |
|||
position: absolute; |
|||
bottom: 25px; |
|||
right: 5px; |
|||
} |
|||
} |
|||
.reply_area{ |
|||
display: flex; |
|||
min-height: 20%; |
|||
flex-direction: column; |
|||
margin: 15px 15px 110px 0px; |
|||
} |
|||
.t_ask{ |
|||
align-self: end; |
|||
line-height: 34px; |
|||
background-color: rgb(188 211 241); |
|||
padding: 0 30px; |
|||
border-radius:10px; |
|||
margin: 15px; |
|||
} |
|||
.t_resp{ |
|||
align-self: start; |
|||
line-height: 30px; |
|||
margin: 20px 33px; |
|||
font-size: 18px; |
|||
color: black; |
|||
} |
|||
.dynamic-width-message-box-byme .el-message-box__message{ |
|||
width: 100%; |
|||
} |
|||
.action_menu{ |
|||
background-color: white; |
|||
padding: 10px 8px; |
|||
margin: 3px 14px 18px 0; |
|||
border-radius: 8px; |
|||
border: 1px solid #c9c6c6; |
|||
} |
|||
.list_item{ |
|||
display: flex; |
|||
background-color: #dddddd; |
|||
padding: 10px 8px; |
|||
margin: 3px 14px 3px 0; |
|||
overflow: hidden; |
|||
text-overflow: ellipsis; |
|||
border-radius: 8px; |
|||
text-wrap-mode: nowrap; |
|||
button{ |
|||
margin-left: auto; |
|||
} |
|||
} |
|||
|
|||
</style> |
|||
<style> |
|||
think { |
|||
color: #939393; |
|||
margin-bottom: 8px; |
|||
display: block; |
|||
} |
|||
</style> |
|||
Loading…
Reference in new issue