<!--
@ 作者: 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,docinfo?:any[]}[]>([])
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})
  }
  let docinfo:any=[]
  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()
      
      // 服务器可能一次返回多行，需要手动按行拆分
      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":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(){
  const c =conversations.value.find(c=>c.uuid==conversation.value)
  if(!c){
    conversations.value.push({
      "agentuuid":props.agent.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=""
  props.closefunc()
}

//处理ai返回的引用信息
function formatRefContent(content:string){
  let result=content.replaceAll(/"/g,'')
  result=result.replaceAll(/Unnamed: /g,' ')
  return result
}

//渲染完页面再执行
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)">
            <span>{{ item.brief }}</span>
            <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 v-if="msg.docinfo" class="doc_ref">
                引用<hr>
                <el-tooltip v-for="doc in msg.docinfo" placement="top" effect="dark" 
                  :content="formatRefContent(doc.content)">
                  <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: 56px;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: 16px;
  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;
  border-radius: 8px;
  span{
    width: 90%;
    overflow: hidden;
    text-overflow: ellipsis;
    text-wrap-mode: nowrap;
  }
  button{
    margin-left: auto;
  }
}
.doc_ref{
  margin: 16px;
  display: flex;
  flex-wrap: wrap;
  hr{
    width: 90%;
    margin: inherit;
    border: none;
    border-top: .5px 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;
    }
    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>