<template>
  <div ref="aiChatRef" class="ai-chat">
    <el-scrollbar ref="scrollDiv" @scroll="handleScrollTop">
      <div ref="dialogScrollbar" class="ai-chat__content p-24 chat-width" style="padding: 24px;">
        <div class="item-content mb-16" style="margin-bottom: 16px">
          <div class="avatar">
            <img src="@/assets/logo/logo2.png" height="30px" />
          </div>
          <div class="content">
            <el-card shadow="always" class="dialog-card">
              <span style="font-size: 14px">您好，我是 果知小助手，您可以向我提出关于 果知的相关问题。</span>
            </el-card>
          </div>
        </div>

        <template v-for="(item, index) in chatList" :key="index">
          <!-- 问题 -->
          <div v-if="item.type === 'question'"  class="item-content mb-16 lighter" style="margin-bottom: 16px;font-weight: 400">
            <div class="avatar">
              <el-avatar style="width:30px;height: 30px;background: #3370FF">
                <img src="@/assets/aichat/user-icon.svg" style="width: 30px" alt="" />
              </el-avatar>
            </div>
            <div class="content">
              <div class="text break-all pre-wrap" style="word-break: break-all;white-space: pre-wrap;">
                {{ item.content }}
              </div>
              <el-tag type="success" style="margin-bottom: 5px;cursor: pointer" v-for="(fileName,index) in item.file" :disable-transitions="false" @click="downloadFile(fileName.file,fileName.bucket,item.sessionId)">{{fileName.file}}</el-tag>
            </div>
          </div>
          <!-- 回答 -->
          <div v-if="item.type === 'answer'" class="item-content mb-16 lighter" style="margin-bottom: 16px;font-weight: 400">
            <div class="avatar">
              <img src="@/assets/logo/logo2.png" height="30px" />
            </div>
            <div class="content">
              <el-card shadow="always" class="dialog-card">
                <MdRenderer :is_large="is_large" :chatIndex="index" :source="item.content" @fullscreenG6="fullscreen"></MdRenderer>
              </el-card>
              <div class="flex-between mt-8" style="display: flex; justify-content: space-between; align-items: center; margin-top: 8px">
                <div>
                  <el-button
                      type="primary"
                      v-if="item.isStop && !item.isEnd"
                      @click="startChat(index)"
                      link
                  >重新生成
                  </el-button>
                  <el-button type="primary" v-else-if="!item.isEnd" @click="stopChat(index)" link
                  >停止回答
                  </el-button>
                </div>
              </div>
              <div v-if="item.isEnd" class="flex-between" style="display: flex; justify-content: space-between; align-items: center;">
                <OperationButton :data="item" :index="index" @regeneration="regenerationChart(index)" @changeThumb="changeThumb" />
              </div>
            </div>
          </div>
        </template>
      </div>
    </el-scrollbar>
    <div class="ai-chat__operate p-24" style="padding: 24px">
<!--      <slot name="operateBefore" />-->
      <div class="chat-width">
        <el-button type="primary" link class="new-chat-button mb-8" style="margin-bottom: 8px" @click="openUpload">
          <el-icon><Upload /></el-icon><span class="ml-4">导入文件</span>
        </el-button>
      </div>
      <div>
        <el-tag type="success" style="margin-bottom: 5px" v-for="(tag,index) in currentFiles" closable :disable-transitions="false" @close="removeFile(index)">{{tag.file}}</el-tag>
      </div>
      <div>
        <span v-if="currentMachine.length > 0">与  </span>
        <el-tag style="margin-bottom: 5px" size="large" v-for="tag in currentMachine" closable :disable-transitions="false" @close="removeMachine">
          <span style="font-size: 16px;margin-left: 10px">{{tag}}</span>
        </el-tag>
        <span v-if="currentMachine.length > 0">  对话</span>
      </div>
      <div class="operate-textarea flex chat-width">
        <el-popover
            :visible="popoverVisible"
            placement="top"
            :width="is_large?'400px':'860px'"
            popper-style="max-width:860px;z-index:99999;margin-left:35px"
            :show-arrow=false
            :offset=1
        >
          <template #reference>
            <el-input
                ref="quickInputRef"
                v-model="inputValue"
                :placeholder="'@选择机器人，Ctrl+Enter 换行，Enter发送'"
                :autosize="{ minRows: 1, maxRows: 4 }"
                type="textarea"
                :maxlength="100000"
                @input="handleInput"
                @keydown.enter="sendChatHandle($event)"
            />
          </template>
          <template #default>
            <div style="width: 100%"  v-click-outside="clickoutside">
              <div style="align-items: center;display: flex">
                <span style="font-size: 25px;font-weight: bold">选择智能体</span>
                <el-link :underline="false" style="font-size: 20px;margin-left: auto" @click="closeMachinePop"><i class="ri-close-large-line"></i></el-link>
              </div>
              <el-scrollbar max-height="200px">
                <div class="machineDiv" @click="chooseMachine('元数据专家','@/assets/aichat/智能体logo.jpg')" style="align-items: center;width:100%;display: flex">
                  <el-avatar style="width: 30px;height: 30px"><img src="@/assets/aichat/智能体logo.jpg"></el-avatar>
                  <span style="font-size: 16px;margin-left: 10px">元数据专家</span>
                </div>
                <div class="machineDiv" @click="chooseMachine('数据标准专家','@/assets/aichat/智能体logo.jpg')" style="align-items: center;width:100%;display: flex">
                  <el-avatar style="width: 30px;height: 30px"><img src="@/assets/aichat/智能体logo.jpg"></el-avatar>
                  <span style="font-size: 16px;margin-left: 10px">数据标准专家</span>
                </div>
                <div class="machineDiv" @click="chooseMachine('数据质量专家','@/assets/aichat/智能体logo.jpg')" style="align-items: center;width:100%;display: flex">
                  <el-avatar style="width: 30px;height: 30px"><img src="@/assets/aichat/智能体logo.jpg"></el-avatar>
                  <span style="font-size: 16px;margin-left: 10px">数据质量专家</span>
                </div>
                <div class="machineDiv" @click="chooseMachine('数据模型专家','@/assets/aichat/智能体logo.jpg')" style="align-items: center;width:100%;display: flex">
                  <el-avatar style="width: 30px;height: 30px"><img src="@/assets/aichat/智能体logo.jpg"></el-avatar>
                  <span style="font-size: 16px;margin-left: 10px">数据模型专家</span>
                </div>
                <div class="machineDiv" @click="chooseMachine('数据安全专家','@/assets/aichat/智能体logo.jpg')" style="align-items: center;width:100%;display: flex">
                  <el-avatar style="width: 30px;height: 30px"><img src="@/assets/aichat/智能体logo.jpg"></el-avatar>
                  <span style="font-size: 16px;margin-left: 10px">数据安全专家</span>
                </div>
                <div class="machineDiv" @click="chooseMachine('数据分析专家','@/assets/aichat/智能体logo.jpg')" style="align-items: center;width:100%;display: flex">
                  <el-avatar style="width: 30px;height: 30px"><img src="@/assets/aichat/智能体logo.jpg"></el-avatar>
                  <span style="font-size: 16px;margin-left: 10px">数据分析专家</span>
                </div>
                <div class="machineDiv" @click="chooseMachine('数据治理管理专家','@/assets/aichat/智能体logo.jpg')" style="align-items: center;width:100%;display: flex">
                  <el-avatar style="width: 30px;height: 30px"><img src="@/assets/aichat/智能体logo.jpg"></el-avatar>
                  <span style="font-size: 16px;margin-left: 10px">数据治理管理专家</span>
                </div>
              </el-scrollbar>
            </div>
          </template>
        </el-popover>
        <div class="operate flex align-center">
          <el-button
              text
              class="sent-button"
              :disabled="isDisabledChart || loading"
              @click="sendChatHandle"
          >
            <img v-show="isDisabledChart || loading" src="@/assets/aichat/icon_send.svg" alt="" />
            <img v-show="!isDisabledChart && !loading" src="@/assets/aichat/icon_send_colorful.svg" alt="" />
          </el-button>
        </div>
      </div>
    </div>
    <!-- 文件导入对话框 -->
    <el-dialog :z-index="9999" title="文件导入" v-model="upload.open" width="400px" append-to-body>
      <el-upload v-if="upload.open"
          ref="uploadRef"
          :headers="upload.headers"
          :action="upload.url"
          :data = "upload.data"
          :disabled="upload.isUploading"
          :on-progress="handleFileUploadProgress"
          :on-success="handleFileSuccess"
          :auto-upload="false"
          drag
      >
        <el-icon class="el-icon--upload"><upload-filled /></el-icon>
        <div class="el-upload__text">将文件拖到此处，或<em>点击上传</em></div>
      </el-upload>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="submitFileForm">确 定</el-button>
          <el-button @click="upload.open = false">取 消</el-button>
        </div>
      </template>
    </el-dialog>
    <el-dialog z-index="99999999999999999999" title="ER图全览" v-if="showFullscreenG6Data" :fullscreen="true" v-model="showFullscreenG6Data" append-to-body>
      <fullscreenG6 :g6Data="fullscreenG6Data"></fullscreenG6>
    </el-dialog>
  </div>
</template>
<script setup>
import { ref, nextTick, computed, watch, reactive, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import antvg6 from './antv-g6.vue'
import OperationButton from './OperationButton.vue'
import MdRenderer from '@/views/aichat/MdRenderer.vue'
import fullscreenG6 from '@/views/aichat/fullscreenG6.vue'
import {getToken} from "@/utils/auth.js";
import {postChatMessage} from "@/api/aichat/aichat.js"
import cache from "@/plugins/cache.js";
import Cookies from "js-cookie";
import {addChat} from "@/api/aichat/aichat";

defineOptions({ name: 'AiChat' })
const route = useRoute()
const fullscreenG6Data = ref(null)
const showFullscreenG6Data = ref(false)
const {
  params: { accessToken, id },
  query: { mode }
} = route
const props = defineProps({
  is_large: Boolean,
  cookieSessionId: String,
  data: {
    type: Object,
    default: () => {}
  },
  record: {
    type: Array,
    default: () => []
  },
  chatId: {
    type: String,
    default: ''
  }, // 历史记录Id
  debug: {
    type: Boolean,
    default: false
  }
})

const { proxy } = getCurrentInstance();

const aiChatRef = ref()
const quickInputRef = ref()
const scrollDiv = ref()
const dialogScrollbar = ref()
const loading = ref(false)
const inputValue = ref('')
const chartOpenId = ref('')
const chatList = ref([])
const answerList = ref([])
const controller = ref(null)

const popoverVisible = ref(false)
const currentMachine = ref([])
const currentFiles = ref([])
const upload = reactive({
  // 是否显示弹出层（用户导入）
  open: false,
  // 弹出层标题（用户导入）
  title: "",
  // 是否禁用上传
  isUploading: false,
  // 设置上传的请求头部
  headers: { Authorization: "Bearer " + getToken() },
  // 上传的地址
  url: import.meta.env.VITE_APP_BASE_API + "/aichat/upload",
  data: {"sessionId":Cookies.get("chatSessionId")}
});
const isDisabledChart = computed(
    () => !(inputValue.value.trim())
)

const emit = defineEmits(['scroll'])

function setScrollBottom() {
  // 将滚动条滚动到最下面
  scrollDiv.value.setScrollTop(getMaxHeight())
}


/**
 * 滚动条距离最上面的高度
 */
const scrollTop = ref(0)


const getMaxHeight = () => {
  return dialogScrollbar.value.scrollHeight
}
const handleScrollTop = ($event) => {
  scrollTop.value = $event.scrollTop
  emit('scroll', { ...$event, dialogScrollbar: dialogScrollbar.value, scrollDiv: scrollDiv.value })
}

const handleScroll = () => {
  if (scrollDiv.value) {
    // 内部高度小于外部高度 就需要出滚动条
    if (scrollDiv.value.wrapRef.offsetHeight < dialogScrollbar.value.scrollHeight) {
      // 如果当前滚动条距离最下面的距离在 规定距离 滚动条就跟随
      scrollDiv.value.setScrollTop(getMaxHeight())

    }
  }
}


/**文件上传中处理 */
const handleFileUploadProgress = (event, file, fileList) => {
  upload.isUploading = true;
};

const handleFileSuccess = (response, file, fileList) => {
  currentFiles.value.push({file:response.data.file,bucket:response.data.bucket})
  upload.open = false;
  upload.isUploading = false;
  proxy.$modal.msgSuccess(response.msg);
  // proxy.$alert("<div style='overflow: auto;overflow-x: hidden;max-height: 70vh;padding: 10px 20px 0;'>" + response.msg + "</div>", "导入结果", { dangerouslyUseHTMLString: true });
};

/** 提交上传文件 */
function submitFileForm() {
  proxy.$refs["uploadRef"].submit();
}

watch(
    () => props.chatId,
    (val) => {
      if (val && val !== 'new') {
        chartOpenId.value = val
      } else {
        chartOpenId.value = ''
      }
    },
    { deep: true }
)

watch(
    () => props.record,
    (value) => {
      chatList.value = value
    },
    {
      immediate: true
    }
)

watch(() => props.cookieSessionId, value => upload.data = {sessionId:value})

function regenerationChart(index){
  let question = chatList.value[index - 1]
  let chat = {
    "type":"question",
    "content":question.content,
    "time": formatDate(new Date()),
    "file": question.file}
  chatList.value.push(chat)
  let data = {
    "query": chat.content,
    "user_id": cache.local.get("username"),
    "robot": currentMachine.value.length>0?currentMachine.value[0]:"",
    "session_id": Cookies.get("chatSessionId"),
    "doc": chat.file,
    "history": []
  }
  sendChatMessage(data)
}

function changeThumb(index,chat){
  chatList.value[index].operate = chat.operate
  chatList.value[index].thumbDownReason = chat.thumbDownReason
}

function openUpload(){
  upload.open = true
}
function chooseMachine(val){
  inputValue.value = inputValue.value.slice(0,-1)
  currentMachine.value = [val]
  popoverVisible.value = false
}

function closeMachinePop(){
  if (inputValue.value.endsWith('@')){
    inputValue.value = inputValue.value.slice(0,-1)
  }
  popoverVisible.value = false
}

function removeMachine(){
  currentMachine.value = []
}

function fullscreen(data){
  fullscreenG6Data.value = data
  showFullscreenG6Data.value = true
}

function clickoutside() {
  if (inputValue.value.endsWith('@')){
    inputValue.value = inputValue.value.slice(0,-1)
  }
  popoverVisible.value = false
}

function removeFile(index){
  currentFiles.value = currentFiles.value.slice(index,1)
}

function handleInput() {
  popoverVisible.value = inputValue.value.endsWith("@");
}

function padZero(num) {
  return num < 10 ? '0' + num : num;
}

function formatDate(date) {
  const year = date.getFullYear();
  const month = padZero(date.getMonth() + 1); // 月份从0开始，所以需要+1
  const day = padZero(date.getDate());
  const hours = padZero(date.getHours());
  const minutes = padZero(date.getMinutes());
  const seconds = padZero(date.getSeconds());

  return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}

function downloadFile(file,bucket,sessionId){
  let data = {file,bucket,sessionId}
  proxy.download("aichat/file/download", {
    ...data,
  }, file);
}

async function sendChatHandle(event) {
  if (!event.ctrlKey) {
    // 如果没有按下组合键ctrl，则会阻止默认事件
    event.preventDefault()
    if (inputValue.value.trim() && (chatList.value.length === 0||
                                    (chatList.value[chatList.value.length - 1].isStop ||
                                    chatList.value[chatList.value.length - 1].isEnd))) {
      chatList.value.push({
        "type": "question",
        "content": inputValue.value.trim(),
        "time": formatDate(new Date()),
        "sessionId": Cookies.get("chatSessionId"),
        "sessionName": chatList.value.length > 0 ? chatList.value[0].content.substring(0, 20) : inputValue.value.trim().substring(0, 20),
        "file": currentFiles.value
      })
      let question = JSON.parse(JSON.stringify(chatList.value[chatList.value.length - 1]))
      question.file = JSON.stringify(question.file)
      await addChat(question)
      nextTick(() => {
        // 将滚动条滚动到最下面
        scrollDiv.value.setScrollTop(getMaxHeight())
      })
      let data = {
        "query": inputValue.value.trim(),
        "user_id": cache.local.get("username"),
        "robot": currentMachine.value.length > 0 ? currentMachine.value[0] : "",
        "session_id": Cookies.get("chatSessionId"),
        "doc": currentFiles.value,
        "history": []
      }
      inputValue.value = ''
      sendChatMessage(data)
    }
  } else {
    // 如果同时按下ctrl+回车键，则会换行
    inputValue.value += '\n'
  }
}

function sendChatMessage(data){
  controller.value = new AbortController()
  postChatMessage(data,{signal:controller.value.signal}).then(res=>{
    if (res.status !== 200){
      chatList.value.push({"type":"answer","content":[{"type":"text","content":"服务异常，错误码:"+res.status}],"isEnd":true,"isStop":false,"sessionId":chatList.value[0].sessionId,"sessionName":chatList.value[0].sessionName,"operate":'',"thumbDownReason":''})
    }else {
      currentFiles.value = []
      chatList.value.push({"type":"answer","content":[],"isEnd":false,"isStop":false,"sessionId":chatList.value[0].sessionId,"sessionName":chatList.value[0].sessionName, "operate":'',"thumbDownReason":''})
      const reader = res.body.getReader()
      const write = getWrite(reader)
      reader.read().then(write).then(()=> {
        let answer = JSON.parse(JSON.stringify(chatList.value[chatList.value.length - 1]))
        answer.content = JSON.stringify(answer.content)
        addChat(answer)
      })
    }
  }).catch((e) => {
    chatList.value.push({"type":"answer","content":[{"type":"text","content":"服务异常"}],"isEnd":true,"isStop":false,"sessionId":chatList.value[0].sessionId,"sessionName":chatList.value[0].sessionName,"operate":"","thumbDownReason":""})
  })
}
watch(
    chatList,
    () => {
      nextTick(() => {
        // 将滚动条滚动到最下面
        scrollDiv.value.setScrollTop(getMaxHeight())
      })
    },{
      deep:true, immediate:true
    }
)
const getWrite = (reader) => {
  let tempResult = '';
  /**
   * 处理流式数据
   * @param {done, value} obj - 包含 done 和 value 属性的对象
   */
  const write_stream = ({ done,value }) => {
    try {
      if (done) {
        return
      }
      const decoder = new TextDecoder('utf-8');
      let str = decoder.decode(value, { stream: true });
      // 解释：数据流返回的 chunk 可能不完整，因此我们需要累积并拆分它们。
      tempResult += str;
      console.log(tempResult)
      const split = tempResult.match(/data:.*}\r\n/g);
      if (split) {
        str = split.join('');
        tempResult = tempResult.replace(str, '');
      } else {
        return reader.read().then(write_stream);
      }
      // 如果 str 存在且以 "data:" 开头，则处理数据块。
      if (str && str.startsWith('data:')) {
        split.forEach((chunkStr) => {
          const chunk = JSON.parse(chunkStr.replace('data:', '').trim());
          if (chunk.docs && chunk.docs[0].length > 0){
            //超链接
            chatList.value[chatList.value.length - 1].content.push({"content":chunk.docs[0],"type":"docs"})
          }else if (chunk.G6_ER && chunk.G6_ER.length > 0){
            //说明是ER图
            chatList.value[chatList.value.length - 1].content.push({"content":chunk.G6_ER,"type":"G6_ER"})
          }else if (chunk.html_image && chunk.html_image.length > 0){
            //说明是html的echarts图
            chatList.value[chatList.value.length - 1].content.push({"content":chunk.html_image,"type":"html_image"})
          }else if (chunk.table && chunk.table.length > 0){
            //说明是 表格
            chatList.value[chatList.value.length - 1].content.push({"content":chunk.table,"type":"table"})
          }else {
            // 纯文本
            let last_answer = chatList.value[chatList.value.length - 1].content
            chunk.choices[0].delta.content = chunk.choices[0].delta.content.replace("\n","\n\n")
            if (last_answer.length > 0) {
              if(last_answer[last_answer.length - 1].type === 'text'){
                chatList.value[chatList.value.length - 1].content[last_answer.length - 1].content += chunk.choices[0].delta.content
              }else{
                chatList.value[chatList.value.length - 1].content.push({"content":chunk.choices[0].delta.content,"type":"text"})
              }
            }else{
              chatList.value[chatList.value.length - 1].content.push({"content":chunk.choices[0].delta.content,"type":"text"})
            }
          }
          nextTick(() => {
            // 将滚动条滚动到最下面
            scrollDiv.value.setScrollTop(getMaxHeight())
          })
          if (chunk.isEnd || chunk.is_end){
            chatList.value[chatList.value.length - 1].isEnd = true
            chatList.value[chatList.value.length - 1].time = formatDate(new Date())
            nextTick(() => {
              // 将滚动条滚动到最下面
              scrollDiv.value.setScrollTop(getMaxHeight())
            })
            return Promise.resolve();
          }
        });
      }
    } catch (e) {
      return Promise.reject(e);
    }
    return reader.read().then(write_stream);
  };
  return write_stream
};

const stopChat = (index) => {
  chatList.value[index].isStop = true
  if (controller.value !== null){
    controller.value.abort()
  }
  let answer = JSON.parse(JSON.stringify(chatList.value[index]))
  answer.content = JSON.stringify(answer.content)
  addChat(answer)
}
const startChat = (index) => {
  regenerationChart(index)
}

defineExpose({
  setScrollBottom
})

</script>
<style lang="scss" scoped>
.ai-chat {
  --padding-left: 40px;
  height: 100%;
  display: flex;
  flex-direction: column;
  box-sizing: border-box;
  position: relative;
  color: #1f2329;

  &__content {
    height: auto;
    padding-top: 0;
    box-sizing: border-box;

    .avatar {
      float: left;
    }

    .content {
      padding-left: var(--padding-left);

      :deep(ol) {
        margin-left: 16px !important;
      }
    }

    .text {
      padding: 6px 0;
    }

    .problem-button {
      width: 100%;
      border: none;
      border-radius: 8px;
      background: #f5f6f7;
      height: 46px;
      padding: 0 12px;
      line-height: 46px;
      box-sizing: border-box;
      color: #1f2329;
      -webkit-line-clamp: 1;
      word-break: break-all;

      &:hover {
        background: var(--el-color-primary-light-9);
      }

      &.disabled {
        &:hover {
          background: #f5f6f7;
        }
      }

      :deep(.el-icon) {
        color: var(--el-color-primary);
      }
    }
  }

  &__operate {
    background: #f3f7f9;
    position: relative;
    width: 100%;
    box-sizing: border-box;
    z-index: 10;

    &:before {
      background: linear-gradient(0deg, #f3f7f9 0%, rgba(243, 247, 249, 0) 100%);
      content: '';
      position: absolute;
      width: 100%;
      top: -16px;
      left: 0;
      height: 16px;
    }

    .operate-textarea {
      box-shadow: 0px 6px 24px 0px rgba(31, 35, 41, 0.08);
      background-color: #ffffff;
      border-radius: 8px;
      border: 1px solid #ffffff;
      box-sizing: border-box;

      &:has(.el-textarea__inner:focus) {
        border: 1px solid var(--el-color-primary);
        scrollbar-width: none;
      }

      :deep(.el-textarea__inner) {
        border-radius: 8px !important;
        box-shadow: none;
        resize: none;
        padding: 12px 16px;
        box-sizing: border-box;
        scrollbar-width: none;
      }

      .operate {
        padding: 6px 10px;
        .el-icon {
          font-size: 20px;
        }

        .sent-button {
          max-height: none;
          .el-icon {
            font-size: 24px;
          }
        }

        :deep(.el-loading-spinner) {
          margin-top: -15px;

          .circular {
            width: 31px;
            height: 31px;
          }
        }
      }
    }
  }

  .dialog-card {
    border: none;
    border-radius: 8px;
    box-sizing: border-box;
  }
}

.chat-width {
  max-width: 80%;
  margin: 0 auto;
}
.machineDiv {
  cursor: pointer;
  padding-top: 3px;
  padding-bottom: 3px;
}
.machineDiv:hover {
  background-color: #ebf1ff;
  font-weight: bold;
}
@media only screen and (max-width: 1000px) {
  .chat-width {
    max-width: 100% !important;
    margin: 0 auto;
  }
}
</style>
