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.
538 lines
14 KiB
538 lines
14 KiB
<!--
|
|
@ 作者: han2015
|
|
@ 时间: 2025-05-12 15:39:13
|
|
@ 备注: aibot组件
|
|
-->
|
|
<script lang="ts" setup>
|
|
import {
|
|
Promotion,
|
|
Remove,DocumentCopy,
|
|
ArrowLeft,
|
|
} from '@element-plus/icons-vue'
|
|
import { userStror } from "@/utils/pinia/stores/modules/userOrders";
|
|
import router from "@/utils/router";
|
|
import { doAiChat,getAiChatList,getAiChat,setAiChat,delAiChat,getAiagentList,getShareChat,newShareChat} from "@/api/doc/space"
|
|
import {ElText,ElInput} from "element-plus";
|
|
import { VueMarkdown } from '@crazydos/vue-markdown'
|
|
import rehypeRaw from 'rehype-raw'
|
|
import remarkGfm from 'remark-gfm'
|
|
import { h } from 'vue'
|
|
import { useRoute } from 'vue-router'
|
|
import BottomPage from '@/views/common/bottom/index.vue'
|
|
|
|
|
|
const userStore = userStror();
|
|
const userid=ref("")
|
|
const route = useRoute()
|
|
//选中的咨询模式
|
|
const checkedModel = ref([])
|
|
//支持的模式
|
|
const aimodels = [{name:'联网检索',key:"onlineSearch"}, {name:'公司知识库',key:"useDataset"}]
|
|
const baseURL=import.meta.env.VITE_APP_BASE_API
|
|
const siteHost=document.location.origin;
|
|
const conversation=ref("") //当前会话的uuid
|
|
const myquestion=ref('')
|
|
const controller = ref<AbortController | null>(null)
|
|
const inputState=ref(true)
|
|
const conversations=ref<chatRecord[]>([])
|
|
const interact_msg=ref<{ask:boolean,think:string,content:string,docinfo?:any[],share:boolean}[]>([])
|
|
const agent=ref<{name:string,uuid:string}>({name:"通用AI",uuid:import.meta.env.VITE_DEFAULT_AI_AGENT})
|
|
const agentList=ref<{name:string,uuid:string}[]>([{name:"通用AI",uuid:import.meta.env.VITE_DEFAULT_AI_AGENT}])
|
|
const respMsg=ref("")
|
|
const drawerModel=ref(true)
|
|
|
|
//消息体
|
|
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})
|
|
}
|
|
let docinfo:any=[]
|
|
controller.value = new AbortController();
|
|
try{
|
|
const res= await doAiChat(`${baseURL}/aibot/agents/${agent.value.uuid}/chat`,{
|
|
inputs: params,
|
|
query:myquestion.value,
|
|
response_mode:"streaming",
|
|
conversation_id:conversation.value,
|
|
user:userid.value,//这里已经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()
|
|
|
|
// 服务器可能一次返回多行,需要手动按行拆分
|
|
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
|
|
}else if(json.event==="message_end"){
|
|
docinfo=json.metadata.retriever_resources
|
|
}
|
|
}
|
|
}
|
|
|
|
if (done) break;//能否放到结尾???
|
|
}
|
|
}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],docinfo:docinfo})
|
|
}else{
|
|
//思考模式
|
|
interact_msg.value.push({ask:false,think:arr[0],content:arr[1],docinfo:docinfo})
|
|
}
|
|
|
|
respMsg.value=""
|
|
setAiChat({
|
|
"userid":userid.value,
|
|
"uuid":conversation.value,
|
|
"agentuuid":agent.value.uuid,
|
|
"brief":interact_msg.value[0].content,
|
|
"content":JSON.stringify(interact_msg.value)
|
|
})
|
|
}
|
|
|
|
//加载交流记录列表
|
|
function loadKnownLibList(){
|
|
//userid 需要 base64解析
|
|
getAiChatList({"userid":userid.value}).then(resp=>{
|
|
conversations.value=resp.data
|
|
})
|
|
}
|
|
|
|
//查看具体的历史记录
|
|
function showChat(uuid:string){
|
|
drawerModel.value=false;
|
|
getAiChat({
|
|
"userid":userid.value,
|
|
"uuid":uuid
|
|
}).then(resp=>{
|
|
interact_msg.value = JSON.parse(resp.data.content)
|
|
conversation.value=resp.data.uuid
|
|
if(resp.data.agentuuid!=""){
|
|
agent.value={name:"基于当前会话",uuid:resp.data.agentuuid}
|
|
}
|
|
})
|
|
}
|
|
|
|
//查看分享chat记录
|
|
function showSharedChat(uuid:string){
|
|
drawerModel.value=false;
|
|
getShareChat(uuid).then(resp=>{
|
|
let data=JSON.parse(resp.data.content)
|
|
data.share=true
|
|
interact_msg.value = data
|
|
// if(resp.data.agentuuid!=""){
|
|
// agent.value={name:"会话",uuid:resp.data.agentuuid}
|
|
// }
|
|
}).catch(err=>{
|
|
alert(err)
|
|
})
|
|
}
|
|
|
|
//删除具体的历史记录
|
|
function onDelChat(uuid:string){
|
|
delAiChat({
|
|
"userid":userid.value,
|
|
"uuid":uuid
|
|
}).then(resp=>{
|
|
loadKnownLibList()
|
|
})
|
|
}
|
|
|
|
function newContext(){
|
|
drawerModel.value=false;
|
|
const c =conversations.value.find(c=>c.uuid==conversation.value)
|
|
if(!c && interact_msg.value.length>0){
|
|
conversations.value.push({
|
|
"agentuuid":agent.value.uuid,
|
|
"uuid":conversation.value,
|
|
"brief":interact_msg.value[0].content,
|
|
"messages":interact_msg.value
|
|
})
|
|
}
|
|
|
|
inputState.value=true
|
|
interact_msg.value=[]
|
|
conversation.value=""
|
|
loadKnownLibList() //刷新对话列表
|
|
}
|
|
|
|
function resetContext(){
|
|
interact_msg.value=[]
|
|
conversation.value=""
|
|
}
|
|
|
|
//处理ai返回的引用信息
|
|
function formatRefContent(content:string){
|
|
let result=content.replace(/"/g,'')
|
|
result=result.replace(/Unnamed: /g,' ')
|
|
return result
|
|
}
|
|
|
|
//复制会话记录
|
|
function onCopyChat(idx:number){
|
|
let el = document.querySelector('#content'+idx) as HTMLElement | null;
|
|
let content = el?.innerText;
|
|
if(!navigator.clipboard) alert("clipboard 不可用")
|
|
navigator.clipboard.writeText(content as string)
|
|
}
|
|
|
|
//新建分享会话记录
|
|
function onShareChat(agID:string,chatID:string,idx:number){
|
|
const arr=<{ask:boolean,content:string}[]>([])
|
|
//第一条为问题,第二条为回答内容
|
|
arr.push({
|
|
ask:interact_msg.value[idx-1].ask,
|
|
content:interact_msg.value[idx-1].content
|
|
})
|
|
arr.push({
|
|
ask:interact_msg.value[idx].ask,
|
|
content:interact_msg.value[idx].content
|
|
})
|
|
|
|
newShareChat({
|
|
key:agID+chatID+idx.toString(),
|
|
data:JSON.stringify(arr)
|
|
}).then(resp=>{
|
|
let _shareURL=`${siteHost}/#/agent?shared=${resp.key}`
|
|
ElMessageBox({
|
|
title: '分享链接',
|
|
customStyle: { padding:'20px'},
|
|
message: () => h('div',{style:{display:'flex','flex-direction':'column','line-height':'34px'}},[
|
|
h(ElText,{style:{'align-self':'flex-start','font-weight': 'bold'}},()=>"链接:"+_shareURL),
|
|
]),
|
|
confirmButtonText: '复制分享链接',
|
|
showCancelButton: true
|
|
}).then(()=>{
|
|
if(!navigator.clipboard) alert("clipboard 不可用")
|
|
navigator.clipboard.writeText(_shareURL)
|
|
})
|
|
}).catch(err=>{
|
|
console.log(err)
|
|
})
|
|
}
|
|
|
|
//渲染完页面再执行
|
|
onMounted(() => {
|
|
if(userStore.userInfoCont == ""){
|
|
userStore.getInfo().then(()=>{
|
|
userid.value="p0"+userStore.userInfoCont.userId;
|
|
})
|
|
}else{
|
|
userid.value="p0"+userStore.userInfoCont.userId;
|
|
}
|
|
getAiagentList().then(resp=>{
|
|
agentList.value.push(...resp.data)
|
|
})
|
|
|
|
loadKnownLibList()
|
|
|
|
const query = route.query
|
|
//只是分享链接的请求
|
|
if (query.shared && query.shared!=""){
|
|
showSharedChat(query.shared as string)
|
|
}
|
|
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<el-drawer
|
|
:model-value="drawerModel"
|
|
:title="agent.name+' : 知识库'"
|
|
direction="ltr"
|
|
size="86%"
|
|
@close="drawerModel=false;"
|
|
>
|
|
<div style="overflow-y: auto;padding: 8px;">
|
|
<ul>
|
|
<li class="action_menu" @click="newContext">
|
|
【新建会话】
|
|
</li>
|
|
<li class="agent-item" v-for="ag in agentList" @click="agent=ag;newContext()">
|
|
<el-icon><Star /></el-icon> <span>{{ ag.name }}智能体</span>
|
|
</li>
|
|
<span><el-icon><Clock /></el-icon> 历史会话</span>
|
|
<li class="list_item" v-for="item in conversations" @click="showChat(item.uuid)">
|
|
<span>{{ item.brief }}</span>
|
|
<el-button icon="Delete" size="small" circle @click="(e)=>{e.stopPropagation();onDelChat(item.uuid)}"></el-button>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</el-drawer>
|
|
|
|
<div class="navBtn">
|
|
<el-button type="text" :icon="ArrowLeft" @click="router.back()"> 返回</el-button>
|
|
<el-button icon="expand" size="small" @click="drawerModel=true;"></el-button>
|
|
</div>
|
|
|
|
<div class="app_container">
|
|
|
|
<div class="reply_area" >
|
|
<span style="text-align: center;margin-bottom: 12px;">{{ agent.name }}: 智能体</span>
|
|
<template v-for="msg,index 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 :id="'content'+index" :markdown="msg.content" :rehype-plugins="[rehypeRaw]" :remark-plugins="[remarkGfm]" ></VueMarkdown>
|
|
<div v-if="conversation" class="actions">
|
|
<el-button :icon="Promotion" size="small" circle @click="onShareChat(agent.uuid,conversation,index)"></el-button> 分享
|
|
<el-button :icon="DocumentCopy" size="small" circle @click="onCopyChat(index)"></el-button> 复制
|
|
</div>
|
|
|
|
<div v-if="msg.docinfo?.length>0" class="doc_ref">
|
|
引用<hr>
|
|
<el-tooltip v-for="doc in msg.docinfo" placement="top" effect="dark">
|
|
<template #content>
|
|
<div v-html="formatRefContent(doc.content)" />
|
|
</template>
|
|
<span>{{doc.document_name}}</span>
|
|
</el-tooltip>
|
|
</div>
|
|
</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: 32px;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: 18px;"
|
|
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>
|
|
<BottomPage />
|
|
</template>
|
|
|
|
<style lang="scss" scoped>
|
|
.app_container {
|
|
height: calc(100vh - 65px);
|
|
padding: 10px;
|
|
width: 100%;
|
|
overflow-y:auto;
|
|
background-color: white;
|
|
}
|
|
.navBtn{
|
|
position: fixed;
|
|
background-color: #ffffff94;
|
|
width: 100%;
|
|
z-index: 55;
|
|
}
|
|
.newquestion{
|
|
bottom: 62px;
|
|
}
|
|
.question_com{
|
|
position: fixed;
|
|
padding: 0 13px;
|
|
text-align: center;
|
|
display: block;
|
|
button{
|
|
position: absolute;
|
|
bottom: 27px;
|
|
right: 20px;
|
|
}
|
|
}
|
|
.reply_area{
|
|
display: flex;
|
|
min-height: 20%;
|
|
flex-direction: column;
|
|
margin: 15px 15px 210px 15px;
|
|
}
|
|
.t_ask{
|
|
align-self: end;
|
|
line-height: 34px;
|
|
background-color: rgb(188 211 241);
|
|
padding: 0 30px;
|
|
border-radius:10px;
|
|
}
|
|
.t_resp{
|
|
align-self: start;
|
|
line-height: 23px;
|
|
font-size: 13px;
|
|
color: black;
|
|
}
|
|
.actions{
|
|
display: flex;
|
|
width: 100%;
|
|
margin: 5px 0;
|
|
background-color: #f3f3f3;
|
|
justify-content: center;
|
|
}
|
|
.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;
|
|
}
|
|
.agent-item{
|
|
padding: 4px 8px;
|
|
margin: 3px 14px 3px 0;
|
|
border-radius: 8px;
|
|
}
|
|
.list_item{
|
|
display: flex;
|
|
background-color: #dddddd;
|
|
padding: 8px;
|
|
margin: 3px 14px 3px 10px;
|
|
border-radius: 8px;
|
|
span{
|
|
width: 90%;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
text-wrap-mode: nowrap;
|
|
}
|
|
button{
|
|
margin-left: auto;
|
|
}
|
|
}
|
|
.doc_ref{
|
|
margin: 5px 0 20px 0px;
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
hr{
|
|
width: 70%;
|
|
margin: 11px;
|
|
border: none;
|
|
border-top: 0px solid rgb(26 25 25 / 23%);
|
|
}
|
|
span{
|
|
width: 300px;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
text-wrap-mode: nowrap;
|
|
margin: 2px 10px;
|
|
padding: 0 10px;
|
|
background-color: #d7d5d5;
|
|
border-radius: 6px;
|
|
}
|
|
}
|
|
|
|
</style>
|
|
<style>
|
|
think {
|
|
color: #939393;
|
|
margin-bottom: 8px;
|
|
display: block;
|
|
}
|
|
|
|
.t_resp{
|
|
h2,h3,h4,h5{
|
|
margin: 12px 0;
|
|
}
|
|
p{
|
|
margin-left: 16px;
|
|
}
|
|
p+ul{
|
|
margin-left: 56px;
|
|
}
|
|
li p{
|
|
margin-left: 0;
|
|
}
|
|
ol{
|
|
margin-left: 14px;
|
|
ul{
|
|
margin-left: 30px;
|
|
li{
|
|
list-style: disc;
|
|
}
|
|
}
|
|
}
|
|
ol>li{
|
|
list-style-type: decimal;
|
|
}
|
|
ul{
|
|
margin-left: 30px;
|
|
list-style: disc;
|
|
ul{
|
|
margin-left: 30px;
|
|
}
|
|
}
|
|
table{
|
|
border: 0px solid;
|
|
border-spacing: 1px;
|
|
border-collapse: collapse;
|
|
th{
|
|
background-color: #e5e5e5;
|
|
border: 1px solid;
|
|
min-width: 180px;
|
|
}
|
|
td{
|
|
border: 1px solid;
|
|
min-width: 180px;
|
|
}
|
|
}
|
|
|
|
}
|
|
</style>
|