数通互联化工云平台
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

325 lines
8.6 KiB

<!--
@ 作者: 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>