核工业西南物理研究院知识库AI客户端
xiangpei
2025-04-18 7789aeaad9032763805da324d743bc664bddd2e8
src/components/AiChat.vue
@@ -1,5 +1,5 @@
<template>
  <div style="position: relative;height:calc(100vh - 20px);width: 100%;display: flex;justify-content: center">
  <div style="height:calc(100vh - 20px);width: 100%;display: flex;justify-content: center">
    <!-- 聊天消息列表 -->
    <div class="chat-messages">
      <div
@@ -11,8 +11,11 @@
        <div class="avatar">
          <img :src="getAvatar(message.role)" alt="avatar" />
        </div>
        <div class="content">
          <div class="text" :ref="'msg' + index">{{ message.content }}</div>
        <div v-if="!message.content && message.role == 'assistant'" v-loading="!message.content && message.role == 'assistant'">
        </div>
        <div v-else class="content">
          <div class="text" :ref="'msg' + index" v-html="getHtml(message.content)"></div>
          <div v-if="message.role === 'assistant'">
            <div v-show="msgIndex === message" class="msg-op-list">
              <el-button class="copy" v-show="false"/>
@@ -37,7 +40,7 @@
    <!-- 输入框 -->
    <div class="chat-input">
      <div style="position: relative;width: 100%;box-sizing: border-box;">
      <div style="position: relative;width: 800px;box-sizing: border-box;">
        <el-input
            v-model="inputMessage"
            type="textarea"
@@ -59,7 +62,7 @@
              </div>
            </div>
            <div class="send-but-warp">
              <div class="send-but">
              <div class="send-but" @click="sendMessage">
                <svg t="1743144485359" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1261" width="36" height="36">
                  <path fill="#409eff" d="M512 85.333333c234.666667 0 426.666667 192 426.666667 426.666667s-192 426.666667-426.666667 426.666667S85.333333 746.666667 85.333333 512 277.333333 85.333333 512 85.333333z m-6.4 234.666667c-4.266667 2.133333-6.4 2.133333-12.8 8.533333l-153.6 153.6c-12.8 12.8-12.8 32 0 44.8 12.8 12.8 32 12.8 44.8 0l96-96V682.666667c0 17.066667 14.933333 32 32 32s32-14.933333 32-32V430.933333l96 96c12.8 12.8 32 12.8 44.8 0s12.8-32 0-44.8l-153.6-153.6c-6.4-6.4-8.533333-8.533333-12.8-8.533333s-8.533333-2.133333-12.8 0z" p-id="1262"></path>
                </svg>
@@ -74,53 +77,46 @@
<script>
import ClipboardJS from 'clipboard';
import {sendKbMsg} from "@/api/chat";
import {Message} from "element-ui";
const markdownIt = require('markdown-it')();
export default {
  name: 'AiChat',
  data() {
    return {
      chatId: null,
      messages: [], // 用于页面展示的对话列表
      netSearchEnable: false,
      messages: [
        {
          role: 'assistant',
          content: '你好!我是你的 AI 助手,有什么可以帮你的吗?',
        },
        {
          role: 'user',
          content: '你好!我是你的 AI 助手,有什么可以帮你的吗?',
        },
      ],
      inputMessage: '',
      sendMsgForm: {
        query: "",
        mode: "local_kb",
        kb_name: "samples",
        top_k: 3,
        score_threshold: 2,
        kbName: "SouthWest_Neclear_Develepment_KB",
        topK: 3,
        scoreThreshold: 2,
        history: [
          {
            "content": "我们来玩成语接龙,我先来,生龙活虎",
            "role": "user"
          },
          {
            "content": "虎头虎脑",
            "role": "assistant"
          }
        ],
        stream: true,
        model: "qwen:7b",
        temperature: 0.7,
        max_tokens: 0,
        prompt_name: "default",
        return_direct: false
        model: "Qwen25-32B-Instruct",
        temperature: 1.15,
        maxTokens: 512,
        promptName: "default",
        returnDirect: false
      },
      msgIndex: null,
      msgRole: null
      msgRole: null,
      chunkRef: null, // ai回答临时数据
      haveThinkStart: false, // ai回答响应数据是否有think标签
      haveThinkEnd: false, // ai回答响应数据是否有think标签
      controller: null, // ai回答请求控制器,可以根据业务中断请求
    };
  },
  methods: {
    // md渲染为html
    getHtml(md) {
      return markdownIt.render(md)
    },
    changeNetEnable() {
      this.netSearchEnable = ! this.netSearchEnable
    },
@@ -129,11 +125,9 @@
      e.preventDefault(); // 阻止默认换行行为
    },
    handleMouseEnter(msgIndex) {
      console.log("鼠标移入", msgIndex)
      this.msgIndex = msgIndex
    },
    handleMouseLeave() {
      console.log("鼠标移除")
      this.msgIndex = null
    },
    // 重新生成
@@ -167,27 +161,90 @@
      return roleNames[role];
    },
    // 发送消息
    async sendMessage() {
      if (this.inputMessage.trim() === '') return;
      // 添加用户消息
      this.messages.push({
        role: 'user',
        content: this.inputMessage,
      });
      this.sendMsgForm.query = this.inputMessage
      // 清空输入框
      this.inputMessage = '';
      // 初始化状态
      this.chunkRef = '';
      this.haveThinkStart = false;
      this.haveThinkEnd = false;
      this.controller = new AbortController();
      sendKbMsg(this.sendMsgForm).then(res => {
        console.log(res, "拿到回复了!!")
        this.messages.push({
          role: 'assistant',
          content: '这是一条 AI 回复。',
      // 添加用户消息和占位助理消息
      this.messages.push(
          { role: 'user', content: this.inputMessage },
          { role: 'assistant', content: '' }
      );
      this.sendMsgForm.query = this.inputMessage;
      this.inputMessage = '';
      const assistantIndex = this.messages.length - 1;
      try {
        const response = await fetch('/api/chat/send/msg', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            "Accept": 'text/event-stream',
            'Cache-Control': 'no-cache, no-transform'
          },
          signal: this.controller.signal,
          body: JSON.stringify(this.sendMsgForm)
        });
      })
    },
        if (!response.ok) throw new Error(`HTTP ${response.status}`);
        const reader = response.body.getReader();
        const decoder = new TextDecoder();
        let jsonBuffer = '';
        // eslint-disable-next-line no-constant-condition
        while (true) {
          const { done, value } = await reader.read();
          if (done) {
            this.sendMsgForm.history.push({ role: 'user', content: this.inputMessage })
            this.sendMsgForm.history.push({ role: 'assistant', content: this.messages[assistantIndex].content })
            break;
          }
          const chunk = decoder.decode(value, { stream: true });
          const lines = chunk.split('\n');
          for (const line of lines) {
            if (!line.startsWith('data:')) continue;
            const rawData = line.slice(5).trim();
            if (rawData === '[DONE]') continue;
            try {
              jsonBuffer += rawData;
              const data = JSON.parse(jsonBuffer);
              jsonBuffer = '';
              let content = '';
              if (data.docs) {
                for (let i = 0; i < data.docs.length; i++) {
                  content += data.docs[i]
                }
              } else {
                content = data?.choices?.[0]?.delta?.content || '';
              }
              if (!content) continue;
              // 实时更新
              this.messages[assistantIndex].content += content;
              this.messages[assistantIndex].content = this.messages[assistantIndex].content.replace(/<\/?think>/g, '');
              await this.$nextTick();
            } catch {
              // JSON不完整,等待下次数据
              continue;
            }
          }
        }
      } catch (error) {
        if (error.name !== 'AbortError') {
          console.error('Request failed:', error);
          this.messages[assistantIndex].content = '请求异常';
        }
      }
    }
  },
};
</script>
@@ -229,9 +286,7 @@
.chat-messages {
  padding: 16px;
  margin-top: 14px;
  overflow-y: auto;
  width: 800px;
  height: 680px;
}
.message {
@@ -280,9 +335,8 @@
}
.chat-input {
  padding: 16px;
  background-color: #fff;
  width: 800px;
  width: 100%;
  display: flex;
  flex-direction: row;
  justify-content: center;