likes
comments
collection
share

java接入AI大模型个人实践(二)

作者站长头像
站长
· 阅读数 58

大家好,你的月亮我的心,我是博主小阿金,欢迎各位工友。 今天浅浅的讲一下前端如何接收流式接口,由于本人是个前端小白,以下功能只实现了基础的对话交流,具体的交互和样式暂且不考虑。

直接上代码

<template>
  <div class="app-container">
    <!-- Welcome Message -->
    <div class="welcome-message">
      <p>您好!我是您的数字助理</p>
      <p>我可以为你解答各类问题、生成图片、总结文档等。</p>
      <p>有任何需要,请随时对我说~~😉</p>
    </div>

    <!-- Conversation Container -->
    <div class="conversation-container">
      <!-- Messages Loop -->
      <div v-for="(msg, index) in messages" :key="index" :class="msg.type">
        <div class="avatar" v-if="msg.type === 'user-message'">
          <!-- User Message Content -->
          <div class="message-content user-message-content">
            <div v-html="md.render(msg.content)"></div>
          </div>
          <!-- User Avatar -->
          <div class="avatar-icon user-avatar">
<svg-icon icon-class="user" />
          </div>
        </div>
        <div class="avatar" v-else>
          <!-- System Avatar -->
          <div class="avatar-icon system-avatar">
            <svg-icon icon-class="assistant" />
          </div>
          <!-- System Message Content -->
          <div class="message-content system-message-content">
            <div v-html="md.render(msg.content)"></div>
          </div>
        </div>
      </div>
    </div>

    <!-- Input Field for User's Question with Welcome Message -->
    <div class="question-container">
      <div class="welcome-message-input">
        <el-input v-model="question" type="textarea" placeholder="请输入您的问题" />
      </div>
    </div>

    <!-- Send Button -->
    <el-button @click="sendChatMessage" type="primary">发送</el-button>
  </div>
</template>

<script setup>
import { historyList } from "@/api/system/chat";
import { fetchEventSource } from '@microsoft/fetch-event-source';
import { getToken } from '@/utils/auth';
import { ref, reactive, onUnmounted } from 'vue';
import { onMounted } from 'vue';
import { md, initClipboard } from '@/utils/markdownSetup';

const baseURL = import.meta.env.VITE_APP_BASE_API;
const question = ref('');
const messages = ref([]);
const controller = new AbortController();
const toolCallId = ref(''); // 存储 toolCallId

const data = reactive({
  queryParams: {
    pageNum: 1,
    pageSize: 3,
  }
});


// 获取历史对话
async function gethistoryList() {
  const response = await historyList(data.queryParams);
  if (response.code === 200) {
    response.rows.forEach(item => {
      // 添加用户提问到messages中
      messages.value.push({
        type: 'user-message',
        content: item.question,
        timestamp: new Date(item.createTime).toLocaleString(),
        isHistory: true // 标记为历史记录
      });

      // 添加系统回复到messages中
      messages.value.push({
        type: 'system-message',
        content: item.answer,
        timestamp: new Date(item.createTime).toLocaleString(),
        isHistory: true // 标记为历史记录
      });
    });
  }
}

function sendChatMessage() {
  const signal = controller.signal;

  // 添加用户提问到messages中
  messages.value.push({
    type: 'user-message',
    content: question.value,
    timestamp: getCurrentTime()
  });

  let systemReply = '';

  fetchEventSource(`${baseURL}/system/chat/getChat`, {
    method: 'POST',
    body: JSON.stringify({ question: question.value, sessionId: toolCallId.value }), // 使用 toolCallId
    headers: {
      'Authorization': 'Bearer ' + getToken(),
      'Content-Type': 'application/json'
    },
    signal,
    onopen() { },
    onerror(event) { },
    onmessage(event) {
      const data = JSON.parse(event.data);
      const content = data.output.choices[0].message.content;

      systemReply += content;

      // 实时更新系统消息
      const lastSystemMessageIndex = messages.value.findIndex(msg => msg.type === 'system-message' && !msg.complete && !msg.isHistory);
      if (lastSystemMessageIndex !== -1) {
        messages.value[lastSystemMessageIndex].content = systemReply;
        messages.value[lastSystemMessageIndex].timestamp = getCurrentTime();
      } else {
        messages.value.push({
          type: 'system-message',
          content: systemReply,
          complete: false,
          timestamp: getCurrentTime()
        });
      }

      // 更新 toolCallId
      toolCallId.value = data.output.choices[0].message.toolCallId;

      // 自动滚动到底部
      scrollToBottom();
    },
    onclose() {
      // 标记最后一条系统消息为完成
      const lastSystemMessageIndex = messages.value.findIndex(msg => msg.type === 'system-message' && !msg.complete && !msg.isHistory);
      if (lastSystemMessageIndex !== -1) {
        messages.value[lastSystemMessageIndex].complete = true;
      }
    }
  });

  // 清空输入框
  question.value = '';
};

// 渲染Markdown内容
const renderMarkdown = (content) => {
  return md.render(content, 'java');
};

// 组件销毁前关闭EventSource连接
onUnmounted(() => {
  controller.abort();
});

// 滚动到底部函数
function scrollToBottom() {
  const container = document.querySelector('.conversation-container');
  if (container) {
    container.scrollTop = container.scrollHeight;
  }
};

// 获取当前时间
function getCurrentTime() {
  const now = new Date();
  return `${now.getHours()}:${now.getMinutes()} ${now.getHours() >= 12 ? 'PM' : 'AM'}`;
};

onMounted(() => {
  initClipboard(); // 初始化Clipboard.js
});
gethistoryList()
</script>

<style scoped>
.welcome-message {
  margin: 20px 0;
  text-align: center;
}

.question-container {
  text-align: center;
}

.welcome-message-input {
  margin-bottom: 10px;
}

.user-message,
.system-message {
  display: flex;
}

.user-message .avatar,
.system-message .avatar {
  display: flex;
}

.user-message .message-content,
.system-message .message-content {
  padding: 1px 5px;
  /* 调整内边距以提升视觉效果 */
  border-radius: 5px;
  /* 圆角边框 */
  display: inline-flex;
  /* 使用inline-flex以支持内容自适应并保持内部元素的对齐 */
  justify-content: center;
  /* 水平居中显示内部元素 */
  align-items: center;
  /* 垂直居中显示内部元素 */
  background-color: #f2f2f2;
  /* 用户消息背景色 */
  min-width: 70px;
  /* 最小宽度设置为70px,确保短消息不会显示得太窄 */
  max-width: 60%;
}

.system-message .message-content {
  background-color: #e6f7ff;
}

.user-message {
  justify-content: flex-end;
  /* 用户消息右对齐 */
}

.system-message {
  justify-content: flex-start;
  /* 系统消息左对齐 */
}

.copy-btn:hover {
  background-color: #303f9f;
  /* 鼠标悬停时颜色变化 */
}

.hljs {
  position: relative;
}

.hljs code {
  display: block;
  padding: 1em;
}

.code-block {
  position: relative;
}

.copy-btn {
  display: flex;
  align-items: center;
  cursor: pointer;
}

.copy-btn svg {
  fill: currentColor;
  width: 1em;
  height: 1em;
}
</style>

fetchEventSource函数释义:这段代码是一个事件源的实现,用于向指定URL发送POST请求获取聊天内容

  1. fetchEventSource函数:调用fetchEventSource函数来发送POST请求到${baseURL}/system/chat/getChat,并传递必要的参数。在请求中包括问题(question)和会话ID(sessionId)。
  2. Headers:设置请求头部,包括Authorization和Content-Type。Authorization使用Bearer加上getToken()函数返回的令牌,Content-Type设置为application/json。
  3. Signal:传递信号(signal)以便控制请求的终止。
  4. onopen()、onerror(event)、onmessage(event)、onclose():定义了不同事件发生时的处理函数。其中,onmessage处理从服务端返回的数据,解析JSON数据中的内容(content),更新系统回复(systemReply),更新消息列表(messages),更新工具调用ID(toolCallId),并自动滚动到底部。onclose用于标记最后一条系统消息为已完成。
  5. 清空输入框:在请求发送后,将输入框中的内容清空,以便用户继续输入新问题。

通过不断向服务器发送请求获取系统回复,并将回复展示在页面上。

总结

至此个人项目的简单接入AI大模型就告一段落,其中对于流式的输出与响应让博主花了一点时间,归根到底还是技术不够扎实,等博主在深耕一下前端技术领域,在回来更新样式和交互。

转载自:https://juejin.cn/post/7396933497947193396
评论
请登录