<!-- ChatComponent.vue -->
<template>
  <div class="chat"  ref="chatContainer">
    <div class="header">
      <div class="header__left">
        <div
          v-if="!state.isSidebarOpen"
          class="close nav-btn"
          @click="state.isSidebarOpen = true"
        >
          <MenuOutlined />
        </div>
        <div
          class="newchat nav-btn"
          @click="$emit('newconv')"
        >
          <PlusCircleOutlined /> <span class="text">新对话</span>
        </div>
      </div>
      <div class="header__right">
        <!-- <div class="nav-btn text metas">
          <CompassFilled v-if="meta.use_web" />
          <GoldenFilled v-if="meta.use_graph"/>
        </div> -->
        <a-dropdown v-if="meta.selectedKB !== null">
          <a class="ant-dropdown-link nav-btn" @click.prevent>
            <!-- <component :is="meta.selectedKB === null ? BookOutlined : BookFilled" /> -->
             <BookOutlined />
            <span class="text">{{ meta.selectedKB === null ? '不使用' : opts.databases[meta.selectedKB]?.name }}</span>
          </a>
          <template #overlay>
            <a-menu>
              <a-menu-item v-for="(db, index) in opts.databases" :key="index" @click="meta.selectedKB=index">
                <a href="javascript:;" >{{ db.name }}</a>
              </a-menu-item>
              <a-menu-item  @click="meta.selectedKB = null">
                <a href="javascript:;">不使用</a>
              </a-menu-item>
            </a-menu>
          </template>
        </a-dropdown>
        <div class="nav-btn text " @click="opts.showPanel = !opts.showPanel">
          <component :is="opts.showPanel ? FolderOpenOutlined : FolderOutlined" /> <span class="text">选项</span>
        </div>
        <div v-if="opts.showPanel" class="my-panal swing-in-top-fwd" ref="panel">
          <div class="flex-center" v-if="configStore.config.enable_knowledge_base">
            知识库
            <div @click.stop>
              <a-dropdown>
                <a class="ant-dropdown-link " @click.prevent>
                  <!-- <component :is="meta.selectedKB === null ? BookOutlined : BookFilled" />&nbsp; -->
                  <BookOutlined />&nbsp;
                  <span class="text">{{ meta.selectedKB === null ? '不使用' : opts.databases[meta.selectedKB]?.name }}</span>
                </a>
                <template #overlay>
                  <a-menu>
                    <a-menu-item v-for="(db, index) in opts.databases" :key="index" @click="meta.selectedKB=index">
                      <a href="javascript:;">{{ db.name }}</a>
                    </a-menu-item>
                    <a-menu-item  @click="meta.selectedKB = null">
                      <a href="javascript:;">不使用</a>
                    </a-menu-item>
                  </a-menu>
                </template>
              </a-dropdown>
            </div>
          </div>
          <div class="flex-center" @click="meta.use_graph = !meta.use_graph" v-if="configStore.config.enable_knowledge_base">
            图数据库 <div @click.stop><a-switch v-model:checked="meta.use_graph" /></div>
          </div>
          <div class="flex-center" @click="meta.use_web = !meta.use_web" v-if="configStore.config.enable_search_engine">
            搜索引擎（Bing） <div @click.stop><a-switch v-model:checked="meta.use_web" /></div>
          </div>
          <div class="flex-center" @click="meta.rewrite_query = !meta.rewrite_query" v-if="configStore.config.enable_reranker">
            重写查询 <div @click.stop><a-switch v-model:checked="meta.rewrite_query" /></div>
          </div>
          <div class="flex-center" @click="meta.rewrite_query = !meta.rewrite_query">
            流式输出 <div @click.stop><a-switch v-model:checked="meta.stream" /></div>
          </div>
          <div class="flex-center" @click="meta.summary_title = !meta.summary_title">
            总结对话标题 <div @click.stop><a-switch v-model:checked="meta.summary_title" /></div>
          </div>
          <div class="flex-center">
            最大历史轮数     <a-input-number id="inputNumber" v-model:value="meta.history_round" :min="1" :max="50" />
          </div>
        </div>
      </div>
    </div>
    <div v-if="conv.messages.length == 0" class="chat-examples">
      <h1>你好，我是 Athena 😊</h1>
      <div class="opts">
        <div
          class="opt__button"
          v-for="(exp, key) in examples"
          :key="key"
          @click="autoSend(exp)"
        >
          {{ exp }}
        </div>
      </div>
    </div>
    <div class="chat-box">
      <div
        v-for="message in conv.messages"
        :key="message.id"
        class="message-box"
        :class="message.role"
      >
        <p v-if="message.role=='sent'" style="white-space: pre-line" class="message-text">{{ message.text }}</p>
        <p v-else
          v-html="renderMarkdown(message.text)"
          class="message-md"
          @click="consoleMsg(message)"></p>

        <div class="refs" v-if="message.role=='received' && message.groupedResults && message.status=='finished'">
          <a-tag
            class="filetag"
            v-for="(results, filename) in message.groupedResults"
            :key="filename"
            @click="opts.openDetail = true"
            :bordered="false"
          >
            {{ filename }}
            <a-drawer
              v-model:open="opts.openDetail"
              title="检索详情"
              width="800"
              :contentWrapperStyle="{ maxWidth: '100%'}"
              placement="right"
              class="retrieval-detail"
              rootClassName="root"
            >
              <div class="fileinfo">
                <p><strong>文件名:</strong> {{ results[0].file.filename }}</p>
                <p><strong>文件类型:</strong> {{ results[0].file.type }}</p>
                <p><strong>创建时间:</strong> {{ new Date(results[0].file.created_at * 1000).toLocaleString() }}</p>
              </div>
              <div v-for="(res, idx) in results" :key="idx" class="result-item">
                <p class="result-id"><strong>ID:</strong> #{{ res.id }}</p>
                <p class="result-distance"><strong>相似度距离:</strong> {{ res.distance }}</p>
                <p class="result-rerank-score"><strong>重排序分数:</strong> {{ res.rerank_score }}</p>
                <p class="result-text">{{ res.entity.text }}</p>
              </div>
            </a-drawer>
          </a-tag>
        </div>
      </div>
    </div>
    <div class="bottom">
      <div class="input-box">
        <a-textarea
          class="user-input"
          v-model:value="conv.inputText"
          @keydown="handleKeyDown"
          placeholder="输入问题……"
          :auto-size="{ minRows: 1, maxRows: 10 }"
        />
        <a-button size="large" @click="sendMessage" :disabled="(!conv.inputText && !isStreaming)">
          <template #icon> <SendOutlined v-if="!isStreaming" /> <LoadingOutlined v-else/> </template>
        </a-button>
      </div>
      <p class="note">即便强如雅典娜也可能会出错，请注意辨别内容的可靠性 模型供应商：{{ configStore.config?.model_provider }}:{{ configStore.config?.model_name }}</p>
    </div>
  </div>
</template>

<script setup>
import { reactive, ref, onMounted, toRefs, nextTick, computed, watch } from 'vue'
import {
  SendOutlined,
  MenuOutlined,
  FormOutlined,
  LoadingOutlined,
  BookOutlined,
  BookFilled,
  CompassFilled,
  GoldenFilled,
  SettingOutlined,
  SettingFilled,
  PlusCircleOutlined,
  FolderOutlined,
  FolderOpenOutlined,
} from '@ant-design/icons-vue'
import { onClickOutside } from '@vueuse/core'
import { marked } from 'marked';
import { useConfigStore } from '@/stores/config'

const props = defineProps({
  conv: Object,
  state: Object
})

const emit = defineEmits(['rename-title', 'newconv']);
const configStore = useConfigStore()

const { conv, state } = toRefs(props)
const chatContainer = ref(null)
const isStreaming = ref(false)
const panel = ref(null)
const examples = ref([
  '写一个冒泡排序',
  '肉碱的分子量是多少？直接回答',
  '简述大蒜的功效是什么？',
  'A大于B，B小于C，A和C哪个大？',
  '今天天气怎么样？'
])

const opts = reactive({
  showPanel: false,
  openDetail: false,
  databases: [],
})

const meta = reactive(JSON.parse(localStorage.getItem('meta')) || {
  use_graph: false,
  use_web: false,
  graph_name: "neo4j",
  rewrite_query: true,
  selectedKB: null,
  stream: true,
  summary_title: true,
  history_round: 5,
})

// 更多选项可以在 marked 文档中找到：https://marked.js.org/
marked.setOptions({
  gfm: true,
  breaks: true,
  tables: true,
});

const renderMarkdown = (text) => marked(text)
const consoleMsg = (message) => console.log(message)
onClickOutside(panel, () => setTimeout(() => opts.showPanel = false, 30))

const handleKeyDown = (e) => {
  if (e.key === 'Enter' && !e.shiftKey) {
    e.preventDefault()
    sendMessage()
  } else if (e.key === 'Enter' && e.shiftKey) {
    // Insert a newline character at the current cursor position
    const textarea = e.target;
    const start = textarea.selectionStart;
    const end = textarea.selectionEnd;
    conv.value.inputText.value =
      conv.value.inputText.value.substring(0, start) +
      '\n' +
      conv.value.inputText.value.substring(end);
    nextTick(() => {
      textarea.setSelectionRange(start + 1, start + 1);
    });
  }
}

const renameTitle = () => {
  if (meta.summary_title) {
    const prompt = '请用一个很短的句子关于下面的对话内容的主题起一个名字，不要带标点符号：'
    const firstUserMessage = conv.value.messages[0].text
    const firstAiMessage = conv.value.messages[1].text
    const context = `${prompt}\n\n问题: ${firstUserMessage}\n\n回复: ${firstAiMessage}，主题是（一句话）：`
    simpleCall(context).then((data) => {
      emit('rename-title', data.response.split("：")[0])
    })
  } else {
    emit('rename-title', conv.value.messages[0].text)
  }
}

const scrollToBottom = () => {
  setTimeout(() => {
    chatContainer.value.scrollTop = chatContainer.value.scrollHeight - chatContainer.value.clientHeight
  }, 10)
}


const generateRandomHash = (length) => {
    let chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
    let hash = '';
    for (let i = 0; i < length; i++) {
        hash += chars.charAt(Math.floor(Math.random() * chars.length));
    }
    return hash;
}

const appendUserMessage = (message) => {
  conv.value.messages.push({
    id: generateRandomHash(16),
    role: 'sent',
    text: message
  })
  scrollToBottom()
}

const appendAiMessage = (message, refs=null) => {
  conv.value.messages.push({
    id: generateRandomHash(16),
    role: 'received',
    text: message,
    refs,
    status: "querying"
  })
  scrollToBottom()
}

const updateMessage = (text, id, refs, status) => {
  const message = conv.value.messages.find((message) => message.id === id)
  if (message) {
    message.text = text
    message.refs = refs
    message.status = status
  } else {
    console.error('Message not found')
  }
  scrollToBottom()
}

const updateStatus = (id, status) => {
  const message = conv.value.messages.find((message) => message.id === id)
  if (message) {
    message.status = status
  } else {
    console.error('Message not found')
  }

  console.log("updateStatus", message, message.refs.knowledge_base.results.length > 0)
  if (message.refs.knowledge_base.results.length > 0) {
    message.groupedResults = message.refs.knowledge_base.results.reduce((acc, result) => {
      const { filename } = result.file;
      console.log(acc, result, filename)
      if (!acc[filename]) {
        acc[filename] = []
      }
      acc[filename].push(result)
      return acc;
    }, {})
  }
}

const simpleCall = (message) => {
  return new Promise((resolve, reject) => {
    fetch('/api/call', {
      method: 'POST',
      body: JSON.stringify({ query: message, }),
      headers: { 'Content-Type': 'application/json' }
    })
    .then((response) => response.json())
    .then((data) => resolve(data))
    .catch((error) => reject(error))
  })
}

const loadDatabases = () => {
  fetch('/api/database/', { method: "GET", })
    .then(response => response.json())
    .then(data => {
      console.log(data)
      opts.databases = data.databases
    })
}

const sendMessage = () => {
  const user_input = conv.value.inputText.trim()
  if (user_input) {
    isStreaming.value = true
    appendUserMessage(user_input)
    appendAiMessage("···", null)
    const cur_res_id = conv.value.messages[conv.value.messages.length - 1].id
    conv.value.inputText = ''
    meta.db_name = opts.databases[meta.selectedKB]?.metaname
    fetch('/api/chat', {
      method: 'POST',
      body: JSON.stringify({
        query: user_input,
        history: conv.value.history,
        meta: meta
      }),
      headers: {
        'Content-Type': 'application/json'
      }
    }).then((response) => {const reader = response.body.getReader()
      const decoder = new TextDecoder()
      let buffer = ''
      // 逐步读取响应文本
      const readChunk = () => {
        return reader.read().then(({ done, value }) => {
          if (done) {
            console.log(conv.value)
            console.log('Finished')
            updateStatus(cur_res_id, "finished")
            isStreaming.value = false
            if (conv.value.messages.length === 2) { renameTitle() }
            return
          }

          buffer += decoder.decode(value, { stream: true })
          const message = buffer.trim().split('\n').pop()

          try {
            const data = JSON.parse(message)
            updateMessage(data.response, cur_res_id, data.refs, "loading")
            conv.value.history = data.history
            buffer = ''
          } catch (e) {
            // console.log(e)
          }
          return readChunk()
        })
      }
      return readChunk()
    })
  } else {
    console.log('请输入消息')
  }
}

const autoSend = (message) => {
  conv.value.inputText = message
  sendMessage()
}

// const clearChat = () => {
//   conv.value.messages = []
//   conv.value.history = []
// }

// 从本地存储加载数据
onMounted(() => {
  scrollToBottom()
  loadDatabases()
  const storedMeta = localStorage.getItem('meta');
  if (storedMeta) {
    const parsedMeta = JSON.parse(storedMeta);
    Object.assign(meta, parsedMeta);
  }
});

// 监听 meta 对象的变化，并保存到本地存储
watch(
  () => meta,
  (newMeta) => {
    localStorage.setItem('meta', JSON.stringify(newMeta));
  },
  { deep: true }
);
</script>

<style lang="less" scoped>
.chat {
  position: relative;
  width: 100%;
  height: 100vh;
  display: flex;
  flex-direction: column;
  overflow-x: hidden;
  background: white;
  position: relative;
  box-sizing: border-box;
  flex: 5 5 200px;
  overflow-y: scroll;

  .header {
    user-select: none;
    position: sticky;
    top: 0;
    z-index: 10;
    background-color: white;
    height: var(--header-height);
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 1rem;

    .header__left, .header__right {
      display: flex;
      align-items: center;
    }
  }

  .nav-btn {
    height: 2.5rem;
    display: flex;
    justify-content: center;
    align-items: center;
    border-radius: 8px;
    color: var(--c-text-light-1);
    cursor: pointer;
    font-size: 1rem;
    width: auto;
    padding: 0.5rem 1rem;

    .text {
      margin-left: 10px;
    }

    &:hover {
      background-color: var(--main-light-3);
    }
  }

}
.metas {
  display: flex;
  gap: 8px;
}

.my-panal {
  position: absolute;
  top: 100%;
  right: 0;
  margin-top: 5px;
  background-color: white;
  border: 1px solid #ccc;
  box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.05);
  border-radius: 12px;
  padding: 12px;
  z-index: 11;
  width: 250px;

  .flex-center {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 10px;
    padding: 8px 16px;
    border-radius: 12px;
    cursor: pointer;
    transition: background-color 0.3s;

    &:hover {
      background-color: var(--main-light-3);
    }
  }
}


.chat-examples {
  padding: 0 50px;
  text-align: center;
  position: absolute;
  top: 20%;
  width: 100%;
  z-index: 9;

  h1 {
    margin-bottom: 20px;
    font-size: 24px;
    color: #333;
  }

  .opts {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 10px;

    .opt__button {
      background-color: white;
      color: #222;
      padding: 6px 1rem;
      border-radius: 1rem;
      cursor: pointer;
      // border: 2px solid var(--main-light-4);
      transition: background-color 0.3s;
      box-shadow: 0px 0px 10px 4px var(--main-light-4);


      &:hover {
        background-color: #fcfcfc;
        box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.1);
      }
    }
  }

}

.chat-box {
  width: 100%;
  max-width: 1100px;
  margin: 0 auto;
  flex-grow: 1;
  padding: 1rem;
  display: flex;
  flex-direction: column;

  .message-box {
    max-width: 95%;
    display: inline-block;
    border-radius: 0.8rem;
    margin: 0.8rem 0;
    padding: 1rem;
    user-select: text;
    word-break: break-word;
    font-size: 16px;
    font-variation-settings: 'wght' 400, 'opsz' 10.5;
    font-weight: 400;
    box-sizing: border-box;
    color: #0D0D0D;
    /* box-shadow: 0px 0.3px 0.9px rgba(0, 0, 0, 0.12), 0px 1.6px 3.6px rgba(0, 0, 0, 0.16); */
    /* animation: slideInUp 0.1s ease-in; */
  }

  .message-box.sent {
    background-color: #efefef;
    line-height: 24px;
    background: var(--main-light-3);
    align-self: flex-end;
  }

  .message-box.received {
    color: initial;
    width: fit-content;
    padding-top: 16px;
    background-color: #F5F7F8;
    text-align: left;
    word-wrap: break-word;
    margin-bottom: 0;
    padding-bottom: 0;
    text-align: justify;
  }

  p.message-text {
    max-width: 100%;
    word-wrap: break-word;
    margin-bottom: 0;
  }

  p.message-md {
    word-wrap: break-word;
    margin-bottom: 0;
  }

  .refs {
    margin-bottom: 20px;

    .filetag:hover {
      cursor: pointer;
    }
  }
}

.retrieval-detail {
  .fileinfo {
    margin-bottom: 20px;
    padding: 10px;
    background: var(--main-light-3);
    border-radius: 8px;
    p {
      margin: 0;
      line-height: 1.5;
    }
  }

  .result-item {
    margin-bottom: 20px;
    padding: 10px;
    border: 1px solid #e8e8e8;
    border-radius: 8px;
    background: var(--main-light-4);

    .result-id,
    .result-distance,
    .result-rerank-score,
    .result-text-label,
    .result-text {
      margin: 5px 0;
    }
  }
}



.bottom {
  position: sticky;
  bottom: 0;
  width: 100%;
  margin: 0 auto;
  padding: 0.5rem 2rem;
  background: white;


  .input-box {
    display: flex;
    width: 100%;
    max-width: 1100px;
    margin: 0 auto;
    align-items: flex-end;
    background-color: #F5F7F8;
    border-radius: 2rem;
    height: auto;
    padding: 0.5rem;

    .user-input {
      flex: 1;
      height: 40px;
      padding: 0.5rem 1rem;
      background-color: transparent;
      border: none;
      font-size: 1.2rem;
      margin: 0 0.6rem;
      color: #111111;
      font-size: 16px;
      font-variation-settings: 'wght' 400, 'opsz' 10.5;
      outline: none;

      &:focus {
        outline: none;
        box-shadow: none;
      }

      &:active {
        outline: none;
      }
    }
  }

  .note {
    width: 100%;
    font-size: small;
    text-align: center;
    padding: 0rem;
    color: #ccc;
    margin: 4px 0;
  }
}



.ant-dropdown-link {
  color: var(--c-text-light-1);
  cursor: pointer;
}

.ant-btn-icon-only {
  font-size: 16px;
  cursor: pointer;
  background-color: transparent;
  border: none;
  height: 2.5rem;
  background-color: black;
  border-radius: 3rem;
  color: white;

  &:hover {
    color: white;
  }
}

button:disabled {
  background: #D7D7D7;
  cursor: not-allowed;
}



.chat::-webkit-scrollbar {
  position: absolute;
  width: 4px;
}

.chat::-webkit-scrollbar-track {
  background: transparent;
  border-radius: 4px;
}

.chat::-webkit-scrollbar-thumb {
  background: var(--c-text-dark-2);
  border-radius: 4px;
}

.chat::-webkit-scrollbar-thumb:hover {
  background: rgb(100, 100, 100);
  border-radius: 4px;
}

.chat::-webkit-scrollbar-thumb:active {
  background: rgb(68, 68, 68);
  border-radius: 4px;
}

.slide-out-left{-webkit-animation:slide-out-left .5s cubic-bezier(.55,.085,.68,.53) both;animation:slide-out-left .5s cubic-bezier(.55,.085,.68,.53) both}
.swing-in-top-fwd {-webkit-animation: swing-in-top-fwd 0.5s cubic-bezier(0.175, 0.885, 0.320, 1.275) both;animation: swing-in-top-fwd 0.5s cubic-bezier(0.175, 0.885, 0.320, 1.275) both;}
@-webkit-keyframes swing-in-top-fwd{0%{-webkit-transform:rotateX(-100deg);transform:rotateX(-100deg);-webkit-transform-origin:top;transform-origin:top;opacity:0}100%{-webkit-transform:rotateX(0deg);transform:rotateX(0deg);-webkit-transform-origin:top;transform-origin:top;opacity:1}}@keyframes swing-in-top-fwd{0%{-webkit-transform:rotateX(-100deg);transform:rotateX(-100deg);-webkit-transform-origin:top;transform-origin:top;opacity:0}100%{-webkit-transform:rotateX(0deg);transform:rotateX(0deg);-webkit-transform-origin:top;transform-origin:top;opacity:1}}
@-webkit-keyframes slide-out-left{0%{-webkit-transform:translateX(0);transform:translateX(0);opacity:1}100%{-webkit-transform:translateX(-1000px);transform:translateX(-1000px);opacity:0}}@keyframes slide-out-left{0%{-webkit-transform:translateX(0);transform:translateX(0);opacity:1}100%{-webkit-transform:translateX(-1000px);transform:translateX(-1000px);opacity:0}}

@media (max-width: 520px) {
  .chat {
    height: calc(100vh - 60px);
  }

  .chat-container .chat .header {
    background: var(--main-light-4);
    .header__left, .header__right {
      gap: 20px;
    }

    .nav-btn {
      font-size: 1.5rem;
      padding: 0;

      &:hover {
        background-color: transparent;
        color: black;
      }

      .text {
        display: none;
      }
    }
  }

  .bottom {
    padding: 0.5rem 0.5rem;

    .input-box {
      border-radius: 8px;
      padding: 0.5rem;

      textarea.user-input {
        padding: 0.5rem 0;
      }
    }
    .note {
      display: none;
    }
  }
}
</style>
