使用 Vue 重构 RAGFlow 实现聊天功能

引言

在现代的应用程序开发中,聊天功能是非常常见且重要的一部分。特别是在智能客服、在线协作等场景下,聊天功能能够极大地提升用户体验。RAG(Retrieval Augmented Generation,检索增强生成)是一种结合了检索和生成技术的方法,它可以让聊天系统更加智能和高效。本文将详细介绍如何使用 Vue 框架重构 RAGFlow 来实现一个聊天功能。

什么是 RAGFlow

RAGFlow 指的是检索增强生成流程。传统的生成式模型在回答问题时,可能会因为缺乏相关的知识而产生不准确或不完整的回答。而 RAG 方法通过在生成之前先从外部知识库中检索相关信息,然后将这些信息融入到生成过程中,从而提高回答的质量和准确性。

RAG 流程概述

  1. 用户输入:用户向聊天系统发送一个问题或消息。
  2. 检索阶段:系统根据用户输入从知识库中检索相关的信息。
  3. 生成阶段:将检索到的信息和用户输入作为输入,传递给生成模型,生成回答。
  4. 返回结果:将生成的回答返回给用户。

Vue 框架简介

Vue 是一个用于构建用户界面的渐进式 JavaScript 框架。它具有简单易学、响应式数据绑定、组件化开发等特点,非常适合用于构建交互式的前端应用程序。在本次重构中,我们将利用 Vue 的这些特性来实现聊天界面和与后端的交互。(我这里用的RAGFlow自身的接口,主要用来学习人家的编程思路)

项目搭建

初始化项目

首先,确保你已经安装了 Node.js 和 npm。然后,使用 Vue CLI 来初始化一个新的 Vue 项目:

bash 复制代码
npm install -g @vue/cli
vue create rag-chat-app
cd rag-chat-app

安装依赖

为了实现与后端的交互,我们需要安装一些依赖,包括makdown的解析,因为聊天的文本流需要拿makdown去渲染这样才能页面上看起来美观,下面是我用到的依赖 package.json

bash 复制代码
{
  "name": "chat-app",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint"
  },
  "dependencies": {
    "axios": "^1.8.1",
    "core-js": "^3.8.3",
    "crypto-js": "^4.2.0",
    "dompurify": "^3.2.4",
    "highlight.js": "^11.11.1",
    "js-base64": "^3.7.5",
    "jsencrypt": "^3.3.2",
    "marked": "^15.0.7",
    "uuid": "^9.0.1",
    "vue": "^2.6.14",
    "vue-router": "^3.6.5"
  },
  "devDependencies": {
    "@babel/core": "^7.12.16",
    "@babel/eslint-parser": "^7.12.16",
    "@vue/cli-plugin-babel": "~5.0.0",
    "@vue/cli-plugin-eslint": "~5.0.0",
    "@vue/cli-service": "~5.0.0",
    "eslint": "^7.32.0",
    "eslint-plugin-vue": "^8.0.3",
    "less": "^4.2.2",
    "less-loader": "^10.2.0",
    "vue-template-compiler": "^2.6.14"
  },
  "eslintConfig": {
    "root": true,
    "env": {
      "node": true
    },
    "extends": [
      "plugin:vue/essential",
      "eslint:recommended"
    ],
    "parserOptions": {
      "parser": "@babel/eslint-parser"
    },
    "rules": {}
  },
  "browserslist": [
    "> 1%",
    "last 2 versions",
    "not dead"
  ]
}

实现聊天界面

创建组件结构树

bash 复制代码
SRC下面的结构树

├─assets
├─components
├─fonts
├─router
├─utils
└─views
    └─HomeChat

components下面的结构树

├─AppList.vue
├─chatRecord.vue
├─Homemange.vue
├─mainpage.vue

登录界面:

javascript 复制代码
<template>
  <div id="app">
    <div class="login-container">
      <div class="header-title">
        <p class="title">{{ title }}</p>
      </div>
      <div class="login-box">
        <h1 class="login-title">登录</h1>
        <p class="welcome-message">很高兴再次见到您!</p>
        <form>
          <div class="form-group inline-items">
            <label for="email">邮箱</label>
            <input type="email" id="email" v-model="parmas.email" placeholder="请输入你的邮箱" />
          </div>
          <div class="form-group inline-items">
            <label for="password">密码</label>
            <input type="password" id="password" v-model="parmas.password" placeholder="请输入你的密码" />
          </div>
          <div class="form-group inline-items remember-me-container">
            <input type="checkbox" id="remember-me" v-model="rememberMe">
            <label for="remember-me" class="remember-me-label">记住密码</label>
          </div>
          <button type="button" @click="login">登录</button>
        </form>
        <!-- <a href="#" class="forgot-password">忘记密码?</a> -->
      </div>
    </div>
  </div>
</template>

<script>
import { rsaPsw } from '@/utils';
import axios from 'axios';
export default {
  data() {
    return {
      parmas: {
        email: '',
        password: ''
      },
      rememberMe: false,
      timer: null,
      title: window.BASE_CONFIG.title
    };
  },
  created() {
    const savedEmail = localStorage.getItem('savedEmail');
    const savedPassword = localStorage.getItem('savedPassword');
    const savedRememberMe = localStorage.getItem('savedRememberMe') === 'true';
    if (savedEmail && savedPassword && savedRememberMe) {
      this.parmas.email = savedEmail;
      this.parmas.password = savedPassword;
      this.rememberMe = savedRememberMe;
    }
  },
  methods: {
    async login() {
      let self = this;
      const rsaPassWord = rsaPsw(self.parmas.password);
      try {
        // 定义请求的 URL

        const url = window.BASE_CONFIG.apiUrl + 'v1/user/login';
        const data = {
          email: `${self.parmas.email}`.trim(),
          password: rsaPassWord
        };
        const response = await axios.post(url, data);
        // console.log(response.data);
        if (response.data) {
          const authorizationHeader = response.headers['authorization'];
          let local = localStorage.getItem('Authorization');
          if (local != null && local != '' && local != undefined) {
            localStorage.removeItem("Authorization");
            localStorage.getItem("token");
          }
          localStorage.setItem("Authorization", authorizationHeader);
          localStorage.setItem("token", response.data.data.access_token);
          //登录成功开启定时器检查token是否过期
          // self.timer = setInterval(() => {
          //   self.fetchUserInfo();
          //  }, 5000);
          if (this.rememberMe) {
            localStorage.setItem('savedEmail', this.parmas.email);
            localStorage.setItem('savedPassword', this.parmas.password);
            localStorage.setItem('savedRememberMe', 'true');
          } else {
            localStorage.removeItem('savedEmail');
            localStorage.removeItem('savedPassword');
            localStorage.removeItem('savedRememberMe');
          }
          this.$router.push('/ChatHome');
          // alert("登录成功");
        } else {
          alert("登录失败");
        }
      } catch (error) {
        // 请求失败,处理错误信息
        console.error('登录失败', error);
        alert("登录失败,请检查您的邮箱和密码");
      }
    },
    //开启一个定时器,定时检查token是否过期
    // async fetchUserInfo() {

    //   try {
    //     let local =localStorage.getItem('Authorization');
    //     const headers = {
    //       'authorization': local
    //             };
    //     const response = await axios.get('http://192.168.0.187/v1/user/info', { headers });
    //     let userInfo = response.data;
    //     if(userInfo.message!="success"){
    //       clearInterval(this.timer);
    //       localStorage.removeItem("Authorization");
    //       this.$router.push('/login');
    //     }
    //   } catch (error) {
    //     console.error('获取用户信息失败:', error);
    //   }
    // }
  }
};
</script>

<style scoped lang="less">
/* 整体容器样式 */
.login-container {
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: 100vh;
  /* background: linear-gradient(45deg, #1a2a6c, #b21f1f, #fdbb2d); */
  background-image: url('../../assets/bg.png');
  background-size: 100% 100%;
  /* animation: gradient 15s ease infinite; */
}

.header-title {
  width: 50%;
  height: 100px;

  position: absolute;
  top: 100px;

  .title {
    color: #fff;
    font-size: 40px;
    font-weight: 700;

  }
}

/* @keyframes gradient {
  0% {
    background-position: 0% 50%;
  }
  50% {
    background-position: 100% 50%;
  }
  100% {
    background-position: 0% 50%;
  }
} */

/* 登录框样式 */
.login-box {
  position: relative;
  background-color: rgba(255, 255, 255, 0.1);
  padding: 40px;
  border-radius: 20px;
  box-shadow: 0 0 20px rgba(0, 0, 0, 0.2);
  width: 350px;
  left: 290px;
  backdrop-filter: blur(10px);
  border: 1px solid rgba(255, 255, 255, 0.2);
  animation: fadeIn 0.5s ease-out;
}

@keyframes fadeIn {
  from {
    opacity: 0;
    transform: translateY(-20px);
  }

  to {
    opacity: 1;
    transform: translateY(0);
  }
}

/* 登录标题样式 */
.login-title {
  text-align: center;
  color: #fff;
  margin-bottom: 15px;
  font-size: 32px;
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  text-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
}

/* 欢迎信息样式 */
.welcome-message {
  text-align: center;
  color: #fff;
  margin-bottom: 30px;
  font-size: 18px;
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  text-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
}

/* Form Group Style */
.form-group {
  margin-bottom: 20px;
}

/* Styles that make labels and input boxes appear in one line */
.inline-items {
  display: flex;
  align-items: center;
}

/* Label Style */
.form-group label {
  margin-right: 10px;
  color: #fff;
  font-size: 14px;
  min-width: 60px;
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  text-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
}

/* Input Box Style */
.form-group input {
  flex: 1;
  padding: 10px;
  border: none;
  border-radius: 5px;
  font-size: 16px;
  background-color: rgba(255, 255, 255, 0.2);
  color: #fff;
  transition: background-color 0.3s ease;
}

.form-group input::placeholder {
  color: rgba(255, 255, 255, 0.6);
}

/* 输入框聚焦样式 */
.form-group input:focus {
  outline: none;
  background-color: rgba(255, 255, 255, 0.3);
  box-shadow: 0 0 5px rgba(255, 255, 255, 0.5);
}

/* 登录按钮样式 */
button {
  width: 100%;
  padding: 12px;
  background-color: rgba(255, 255, 255, 0.2);
  color: #fff;
  border: none;
  border-radius: 5px;
  font-size: 18px;
  cursor: pointer;
  transition: background-color 0.3s ease;
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  text-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
}

/* 登录按钮悬停样式 */
button:hover {
  background-color: rgba(255, 255, 255, 0.3);
}

/* 忘记密码链接样式 */
.forgot-password {
  display: block;
  text-align: center;
  margin-top: 20px;
  color: #fff;
  text-decoration: none;
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  text-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
}

/* 忘记密码链接悬停样式 */
.forgot-password:hover {
  text-decoration: underline;
}

/* 记住密码容器样式 */
.remember-me-container {
  justify-content: flex-start;
  align-items: center;
}

/* 记住密码复选框样式 */
#remember-me {
  margin-left: 6px;
  position: absolute;
  left: 48px;
}

.remember-me-label {
  position: absolute;
  left: 70px;
  color: #fff;
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  text-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
}
</style>

运行结果:

聊天主页:

javascript 复制代码
<template>
  <div id="app">
    <div id="main-container">
      <!-- 左侧菜单栏 -->
      <aside id="menu-bar" :style="menuBarStyle" class="menu-bar">
        <div class="menu-toggle" @click="toggleMenu">
          <i class="iconfont liuduidui-shouqi" style="font-size: 25px;"></i>
        </div>
        <ul v-show="!isMenuCollapsed">
          <div class="menu-logo">
            <div class="menu-logo-content">
              <img style="width: 140px;" src="../../assets/logo3.png" alt="">
            </div>
          </div>
          <!-- <li v-for="(menuItem, index) in menuItems" :key="index">{{ menuItem }}</li> -->
          <div class="newConv" @click="createNewDialog()">
            <i class="iconfont liuduidui-jia"></i>
            <span>新建对话</span>
          </div>
          <li>
          </li>
        </ul>
        <chatRecord v-show="!isMenuCollapsed" ref="chatRecord" @historical="historical" />
      </aside>
      <!-- 右侧主体部分 -->
      <div id="main-content">
        <header id="main-header">
          <div class="header-item">
            <i v-if="!isshowmange" @click="returnButton()" class="iconfont liuduidui-fanhui"></i>
            <!-- <div class="heard-logo">
              <img width="40px" height="40px" src="../../assets/deep.png" alt="">
            </div> -->
            <span class="heart-text">怼怼·大语言模型</span>
            <p v-if="this.local == undefined && this.local == ''" class="login" @click="login()">登录</p>
            <div v-else class="login">
              <p>用户</p>
              <div class="logout-menu" @click="logout()">退出登录</div>
            </div>
          </div>
        </header>
        <div id="chat-window">
          <MainPage v-if="isshowmange" @emitUserClick="emitUserClick" @emitSelectChange="emitSelectChange" />
          <Homemange ref="Homemange" v-if="!isshowmange" />
          <!-- <p v-for="(message, index) in chatMessages" :key="index">{{ message }}</p> -->
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import MainPage from '../../components/mainpage.vue';
import Homemange from '@/components/Homemange.vue';
import chatRecord from '@/components/chatRecord.vue';
import { getConversationId } from '@/utils';
import axios from 'axios';
export default {
  components: {
    MainPage, Homemange, chatRecord
  },
  data() {
    return {
      menuItems: ['菜单选项1', '菜单选项2', '菜单选项3'],
      chatMessages: [],
      isMenuCollapsed: false,
      isshowmange: true,
      local: '',
      selectedFeature: '',
    };
  },
  computed: {
    menuBarStyle() {
      return {
        width: this.isMenuCollapsed ? '50px' : '200px',
        transition: 'all 0.3s ease'
      };
    }
  },
  methods: {
    toggleMenu() {
      this.isMenuCollapsed = !this.isMenuCollapsed;
    },
    emitUserClick(item, value) {
      let self = this
      this.isshowmange = !this.isshowmange;
      this.$nextTick(() => {
        self.$refs.Homemange.newMessage = item
        self.$refs.Homemange.sendMessage()
        self.$refs.Homemange.selectObj = value
      })
    },
    //用户返回操作
    returnButton() {
      this.isshowmange = !this.isshowmange;
    },
    login() {
      this.$router.push('/Login')
    },
    logout() {
      this.$router.push('/Login')
      localStorage.removeItem("Authorization");
    },
    createNewDialog() {
      //用户新建对话,其实就是更新一个id
      let self = this
      //下面清空,其实就是怕之前的历史对话对数据进行污染了
      this.isshowmange = false;
      this.$nextTick(() => {
        self.$refs.Homemange.messages = []
        self.$refs.Homemange.messages.push({
          id: 1,
          sender: 'bot',
          content: '',
          displayedText: '你好! 我是你的助理怼怼,有什么可以帮到你的吗?',
          timestamp: this.getCurrentTime(),
          isStreaming: true,
          status: '✓',
        })
        self.$refs.Homemange.userMessage = []
      })
      this.SetconversationId()
      self.ConversationHistory(self.selectedFeature)
    },

    emitSelectChange(value) {
      this.selectedFeature = value
      this.ConversationHistory(this.selectedFeature)
      //请求会话历史记录列表
    },

    //获取会话历史记录列表
    async ConversationHistory(dialog_id) {
      try {
        // 定义请求头
        const headers = {
          'Content-Type': 'application/json',
          'Accept': 'text/event-stream',
          'authorization': this.local
        };
        // 修正字符串拼接
        let url = `${window.BASE_CONFIG.apiUrl}v1/conversation/list?dialog_id=${dialog_id}`;
        const response = await axios.get(url, { headers });
        let arr = response.data.data;
        this.$refs.chatRecord.chatList = arr
        console.log(arr)
      } catch (error) {
        // 处理错误
        console.error('请求失败:', error);
      }
    },
    SetconversationId() {
      let a = getConversationId()
      const payload = {
        "dialog_id": this.selectedFeature,
        "name": "你好",
        "is_new": true,
        "conversation_id": a,
        "message": [
          {
            "role": "assistant",
            "content": "你好"
          }
        ]
      };
      const headers = {
        'Content-Type': 'application/json',
        'Accept': 'text/event-stream',
        'authorization': this.local
      };
      let url = window.BASE_CONFIG.apiUrl + '/v1/conversation/set'
      axios.post(url, payload, { headers })
        .then(response => {
          let ConversationId = response.data.data.id;
          sessionStorage.setItem('conversation_id', ConversationId);   //将会更新ConversationId的值
        })
        .catch(error => {
          console.error('请求失败', error);
        });
    },
    //获取时间点
    getCurrentTime() {
      const now = new Date();
      return `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`;
    },

    historical(arr) {
      let self = this
      this.isshowmange = false;
      this.$nextTick(() => {
        self.$refs.Homemange.messages = []
        self.$refs.Homemange.messages = arr
      })
    }


  },
  mounted() {
    this.local = localStorage.getItem('Authorization');
  },
};
</script>

<style scoped lang="less">
#main-container {
  display: flex;
  height: 100vh;
}

.menu-bar {
  padding: 10px;
  border-right: 1px solid #ece8e8;
  overflow: hidden;
  position: relative;
}

.menu-toggle {
  position: absolute;
  top: 12px;
  right: 10px;
  font-size: 24px;
  cursor: pointer;

}

.menu-logo {
  position: absolute;
  left: 10px;
  top: 15px;
  left: 15px;
  font-size: 24px;
  cursor: pointer;

  .menu-logo-content {
    display: flex;
    justify-content: center;
    align-items: center;
    position: absolute;
    top: 3px;

    .menu-logo-text {
      font-size: 18px;
      margin-left: 10px;
    }
  }
}

.newConv {
  width: 160px;
  height: 40px;
  display: flex;
  align-items: center;
  background-color: #FBFBFB;
  border: 1px solid;
  border-radius: 12px;
  cursor: pointer;
  color: #000;
  border-color: rgba(0, 0, 0, 0.1);
  display: flex;
  justify-content: center;
  align-items: center;
  margin-left: 18px;
  margin-top: 15px;
}

.newConv:hover {
  background-color: rgba(0, 0, 0, 0.031);
  /* #00000008 转换为 rgba */
}

#menu-bar ul {
  list-style-type: none;
  padding: 0;
  margin-top: 50px;
}

#menu-bar ul li {
  padding: 10px 0;
  cursor: pointer;
  opacity: 0.8;
  transition: opacity 0.3s ease;
}

#menu-bar ul li:hover {
  opacity: 1;
}

#main-content {
  flex: 1;
  display: flex;
  flex-direction: column;
}

#main-header {
  padding: 5px 5px;
  justify-content: center;
  display: flex;
  border-bottom: 1px solid #ece8e8;

  .header-item {
    width: 100%;
    height: 40px;
    position: relative;

    .heard-logo {
      z-index: 10000;
      position: absolute;
      top: 8px;
      display: flex;
      align-items: center;
      justify-content: center;
      left: 41px;
      width: 40px;
      height: 40px;
      border-radius: 50%;
      border-width: 1px;
      /* border-color: rgba(0, 0, 0, 0.1) */
      border: 1px solid;
      border-color: rgba(0, 0, 0, 0.1);
    }

    .liuduidui-fanhui {
      font-size: 20px;
      position: absolute;
      left: 10px;
      top: 10px;

    }

    .heart-text {
      position: absolute;
      top: 10px;
      font-size: 17px;
      color: #000;
      font-weight: 500;
      left: 30px;
    }

    .login {
      position: absolute;
      right: 10px;
      top: 5px;
      border: 1px solid;
      display: flex;
      flex-direction: row;
      align-items: center;
      justify-content: center;
      width: 74px;
      height: 32px;
      font-size: 14px;
      cursor: pointer;
      margin-right: 20px;
      border-radius: 9999px;
      border-width: 1px;
      border-color: rgba(0, 0, 0, 0.1);

    }

    .login:hover {
      /* background-color: button-bg-hover (you need to define this variable) */
      background-color: #e9e7e7;
      /* border: none */
      border: none;
      /* font-weight: bold */
      font-weight: bold;
    }
  }

}

#chat-window {
  flex: 1;
  // border: 1px solid #ccc;
  margin: 10px;
  padding: 10px;
  justify-content: center;
  align-items: center;
}

.logout-menu {
  display: none;
  position: absolute;
  top: 109%;
  left: 0;
  background-color: #f9f9f9;
  color: #333;
  border: 1px solid #e0e0e0;
  border-radius: 4px;
  padding: 3px 0;
  min-width: 74px;
  list-style-type: none;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  transition: all 0.2s ease-in-out;
  -webkit-user-select: none;
  -moz-user-select: none;
  user-select: none;
  border: 1px solid;
  /* display: flex
    /* flex-direction: row; */
  flex-direction: row;
  /* align-items: center; */
  align-items: center;
  /* justify-content: center; */
  justify-content: center;
  /* width: 74px; */
  width: 74px;
  /* height: 32px; */
  height: 32px;
  /* font-size: 14px; */
  font-size: 14px;
  /* cursor: pointer; */
  cursor: pointer;
  /* margin-right: 20px; */
  margin-right: 20px;
  border-radius: 9999px;
  /* border-width: 1px; */
  border-width: 1px;
  /* border-color: rgba(0, 0, 0, 0.1); */
  border-color: rgba(0, 0, 0, 0.1);
}

.logout-menu li {
  /* 内边距 */
  padding: 8px 16px;
  /* 鼠标悬停时的指针样式 */
  cursor: pointer;
}

.logout-menu li:hover {
  /* 鼠标悬停时的背景颜色 */
  background-color: #e9e9e9;
}

.login:hover .logout-menu {
  /* 鼠标悬停时显示菜单 */
  display: block;
  display: flex;
  align-items: center;
}
</style>

运行结果:

聊天界面:

html 复制代码
-->
<template>
    <div class="chat-mange-container">
        <ul class="chat-message-list" ref="messagesContainer">
            <div class="main-content">
                <!-- 聊天窗口 -->
                <div class="messages">
                    <div v-for="msg in messages" :key="msg.id" class="message" :class="msg.sender">
                        <!-- 机器人头像 -->
                        <div v-if="msg.sender === 'bot'" class="avatar bot-avatar">
                            <img src="../assets/chat.png" alt="Bot">
                        </div>
                        <!-- 消息内容 -->
                        <div class="message-content">
                            <div class="message-bubble">
                                <div class="markdown-content" v-if="msg.displayedText === ''" style="color: #999;">
                                    正在思考中...</div>
                                <div class="markdown-content" v-html="parseMarkdown(msg.displayedText)"></div>
                            </div>
                            <div class="meta-info">
                                <span class="timestamp">{{ msg.timestamp }}</span>
                                <span v-if="msg.status" class="status">{{ msg.status }}</span>
                            </div>
                        </div>
                        <!-- 用户头像 -->
                        <div v-if="msg.sender === 'user'" class="avatar user-avatar">
                            <img src="../assets/user1.png" alt="User">
                            <!-- <img src="../../assets/user1.png" alt="User"> -->
                        </div>
                    </div>
                    <!-- 消息输入区域 -->
                </div>
            </div>
        </ul>
        <div class="chat-input-main-container">
            <section class="chat-input-section">
                <div class="chat-input-container">
                    <div class="chat-input-content">
                        <div class="chat-input-wrapper">
                            <div class="chat-input-textarea">
                                <textarea v-model="newMessage" @keyup.enter="sendMessage" :disabled="isSending"
                                    placeholder="输入任何问题,Enter发送,Shift + Enter 换行"></textarea>
                            </div>
                            <div class="chat-input-bottom">
                                <div class="chat-input-feature-buttons">
                                    <div class="chat-input-feature-button">
                                        <i class="iconfont liuduidui-AIzhuli1"></i>
                                        <span class="custom-select">{{ selectObj.label }}</span>
                                        <!-- <select class="custom-select">
                                            <option value="deepThoughtR1">深度思考(R1)</option>
                                            <option value="networkSearch">联网搜索</option>
                                            <option value="otherFeature">其他功能</option>
                                        </select> -->
                                    </div>
                                    <!-- <div class="chat-input-feature-button">
                                        <i class="iconfont liuduidui-hulianwang" style="margin-right: 2px;"></i>
                                        <span>联网搜索</span>
                                    </div> -->
                                </div>
                                <div class="chat-input-tools">
                                    <div class="chat-input-tool-button" v-if="!isSending" @click="sendMessage"
                                        :disabled="!newMessage.trim() || isSending">
                                        <i class="iconfont liuduidui-shangchuan" style="font-size: 30px;"></i>
                                    </div>
                                    <div v-else class="loading-dots">
                                        <div class="dot"></div>
                                        <div class="dot"></div>
                                        <div class="dot"></div>
                                    </div>

                                </div>
                            </div>
                        </div>
                    </div>
                </div>
                <div class="chat-input-footer">
                    以上内容均由AI大模型生成,仅供参考
                </div>
            </section>
        </div>
    </div>
</template>

css样式:

css 复制代码
<style lang="less">
.chat-mange-container {
    width: 100%;
    overflow-y: scroll;
    padding-left: 16px;
    padding-right: 10px;
    padding-bottom: 200px;
    display: flex;
    flex-direction: column;
    align-items: center;
    outline: none;
}

.chat-mange-container::-webkit-scrollbar {
    display: none;
}


.chat-message-list {
    width: 100%;
    max-width: 866px;
    height: 685px;
    overflow: auto;
    padding-left: 16px;
    display: flex;
    flex-direction: column;
}


.chat-message-list::-webkit-scrollbar {
    width: 0;
    height: 0;
}

.chat-message-list {
    scrollbar-width: none;
}


.chat-message-list {
    -ms-overflow-style: none;
}

::v-deep .markdown-content {

    line-height: 1.6;
    font-size: 15px;
    color: #2d333a;
    text-align: left;

 
    h1,
    h2,
    h3,
    h4,
    h5,
    h6 {
        margin: 1.2em 0 0.8em;
        font-weight: 600;
        color: #1a1a1a;
        padding-bottom: 0.3em;
        border-bottom: 1px solid #eaecef;
    }

    h1 {
        font-size: 1.8em;
    }

    h2 {
        font-size: 1.6em;
    }

    h3 {
        font-size: 1.4em;
    }

    h4 {
        font-size: 1.2em;
    }

    h5 {
        font-size: 1.1em;
    }

    h6 {
        font-size: 1em;
    }

    /* 段落和文字 */
    p {
        margin: 0.8em 0;
        line-height: 1.7;
    }

    strong {
        color: #24292e;
        font-weight: 650;
    }

    em {
        color: #5a5a5a;
        font-style: italic;
    }

    /* 列表样式 */
    ul,
    ol {
        margin: 0.8em 0;
        padding-left: 2em;

        li {
            margin: 0.4em 0;
            padding-left: 0.4em;

            &::marker {
                color: #6a737d;
            }
        }
    }

    /* 代码块增强 */
    pre {
        position: relative;
        background: #1e1e1e !important;
        border-radius: 8px;
        margin: 1.2em 0;
        box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);

        code {
            display: block;
            padding: 1.2em !important;
            font-family: 'Fira Code', Consolas, monospace;
            font-size: 0.9em;
            line-height: 1.5;
            color: #d4d4d4;
        }

        /* 语言标签 */
        &::after {
            content: attr(data-language);
            position: absolute;
            top: 0;
            right: 0;
            padding: 0.2em 0.8em;
            background: rgba(255, 255, 255, 0.1);
            color: #999;
            font-size: 0.8em;
            border-bottom-left-radius: 4px;
        }
    }

    /* 行内代码 */
    code:not(pre code) {
        background: rgba(175, 184, 193, 0.2);
        padding: 0.2em 0.4em;
        border-radius: 4px;
        font-size: 0.9em;
        color: #eb5757;
    }

    /* 引用块 */
    blockquote {
        margin: 1em 0;
        padding: 0.8em 1.2em;
        background: #f8f9fa;
        border-left: 4px solid #3498db;
        border-radius: 4px;
        color: #6a737d;

        p {
            margin: 0;
        }
    }

    
    table {
        width: 100%;
        margin: 1.5em 0;
        border-collapse: collapse;
        box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);

        th,
        td {
            padding: 0.8em;
            border: 1px solid #dfe2e5;
        }

        th {
            background: #f6f8fa;
            font-weight: 600;
        }

        tr:nth-child(even) {
            background: #fafbfc;
        }
    }


    a {
        color: #3498db;
        text-decoration: none;
        border-bottom: 1px solid rgba(52, 152, 219, 0.3);
        transition: all 0.2s;

        &:hover {
            color: #2980b9;
            border-bottom-color: currentColor;
        }
    }
}


::v-deep code {
    font-family: 'Fira Code', 'Consolas', monospace;
    background-color: rgba(175, 184, 193, 0.2);
    padding: 0.2em 0.4em;
    border-radius: 4px;
    font-size: 0.9em;
}

::v-deep pre {
    position: relative;
    background-color: #000102 !important;
    border: 1px solid #e1e4e8;
    border-radius: 6px;
    padding: 16px;
    margin: 1em 0;
    overflow-x: auto;
}

::v-deep pre code {
    background-color: transparent !important;
    padding: 0;
    font-size: 0.9em;
    line-height: 1.5;
}

::v-deep .hljs {
    background: transparent !important;
}

/* 添加代码块复制按钮 */
::v-deep pre {
    position: relative;
}

::v-deep .copy-button {
    position: absolute;
    right: 8px;
    top: 8px;
    padding: 4px 8px;
    background: rgba(255, 255, 255, 0.8);
    border: 1px solid #e1e4e8;
    border-radius: 4px;
    cursor: pointer;
    font-size: 0.8em;
}


/* 基础布局 */
.chat-mange-container {
    display: flex;
    height: 100vh;
    font-family: 'Segoe UI', system-ui, sans-serif;
}

.sidebar {
    width: 150px;
    color: #fff;
    padding: 20px;
    display: flex;
    flex-direction: column;
}

.logo {
    font-size: 23px;
    /* font-family: YouSheBiaoTiHei; */
    font-weight: 550;
    color: #000000;
    text-indent: 12px;
}

.menu {
    list-style: none;
    padding: 0;
    color: #4C4C4C;
    cursor: pointer;
    font-weight: 500;
    font-size: 20px;
}

.menu li {
    border-radius: 8px;
    cursor: pointer !important;
    transition: all 0.2s;
    width: 103px;
    line-height: 45px;
    height: 45px;
    margin-top: 5px;
}

.menu li:hover {
    background: #E8EFFF;
    border: 1px solid #BED3FB;
}

.menu li.active {
    background: #E8EFFF;
    border: 1px solid #BED3FB;
    font-weight: 500;
    color: #0057FF;
}

.history {
    width: 226px;
    background: #fff;
    box-shadow: 2px 0 8px rgba(0, 0, 0, 0.05);
    transition: transform 0.3s ease;
    position: relative;


}

.history.collapsed {
    transform: translateX(-100%);
}

.toggle-btn {
    position: absolute;
    right: -40px;
    top: 20px;
    width: 40px;
    height: 40px;
    border: none;
    background: #fff;
    box-shadow: 2px 0 8px rgba(0, 0, 0, 0.1);
    border-radius: 0 8px 8px 0;
    cursor: pointer;
    font-size: 16px;
}

.history-header {
    padding: 20px;
    font-size: 18px;
    font-weight: 500;
    border-bottom: 1px solid #eee;
}

.history-list {
    list-style: none;
    padding: 12px;
    margin: 0;

    .newSession {
        width: 100%;
        line-height: 49px;
        background: #E8EFFF;
        margin-top: 70px;
        display: flex;
        align-items: center;
        justify-content: center;
        border-radius: 5px;

        .newSession_text {
            font-weight: 600;
            font-size: 16px;
            color: #0057FF;
            height: 49px;
            line-height: 49px;
        }
    }


}

.history-list li {
    padding: 12px;
    border-radius: 8px;
    cursor: pointer;
    transition: background 0.2s;
    font-size: 14px;
    color: #666;
    cursor: pointer;
}

.history-list li:hover {
    background: #f8f9fa;
}

/* 主聊天窗口 */
.main-content {
    flex: 1;
    display: flex;
    position: relative;
}

.chat-window {
    flex: 1;
    display: flex;
    flex-direction: column;
    background: #fff;
    transition: all 0.3s;
}

.chat-heard {

    width: 100%;
    height: 60px;
    //   background-image: url(../../assets/head.png);
    background-repeat: no-repeat;
    background-size: 100% 60px;
    position: relative;

    .headerInfo {
        position: absolute;
        width: 80%;
        display: flex;
        left: 80px;
        top: 0px;
        height: 40px;

        .headerInfo-text1 {
            height: 20px;
            font-family: YouSheBiaoTiHei;
            font-weight: 400;
            line-height: 20px;
            font-size: 22px;
            color: #FFFFFF;
            background: linear-gradient(0deg, #297DEF 0%, #0A5CCC 100%);
            -webkit-background-clip: text;
            -webkit-text-fill-color: transparent;
        }

        .app_name {
            // background-color: #0057FF;
            display: flex;
            font-size: 14px;
            margin-left: 30px;

            .appname1 {
                width: 118px;
                height: 31px;
                line-height: 31px;
                background: #F7F9FE;
                opacity: 0.82;
                font-family: PingFang SC;
                font-weight: 500;
                font-size: 18px;
                color: #45A658;
                border-radius: 20px;
                margin-top: 11px;
            }

            .appname2 {
                width: 200px;
                text-indent: 10px;
                height: 31px;
                line-height: 31px;
                background: #F7F9FE;
                opacity: 0.82;
                opacity: 0.82;
                font-family: PingFang SC;
                font-weight: 500;
                font-size: 18px;
                color: #46B0BA;
                margin-left: 20px;
                border-radius: 20px;
                margin-top: 11px;
            }
        }

        // .headerInfo-text2 {
        //   height: 20px;
        //   line-height: 20px;
        //   font-family: YouSheBiaoTiHei;
        //   font-weight: 400;
        //   font-size: 22px;
        //   color: #FFFFFF;
        //   background: linear-gradient(0deg, #297DEF 0%, #0A5CCC 100%);
        //   -webkit-background-clip: text;
        //   -webkit-text-fill-color: transparent;
        // }
    }
}

.chat-window.half-width {
    width: 60%;
}

.messages {
    flex: 1;
    overflow-y: auto;
    padding: 24px;
}

.messages::-webkit-scrollbar {
    width: 6px;

}

.messages::-webkit-scrollbar-thumb {
    background-color: #dbdada;
    ;

    border-radius: 3px;

}

.messages::-webkit-scrollbar-thumb:hover {
    background-color: #d3d2d2;

}

.messages::-webkit-scrollbar-track {
    background-color: #f1f1f1;
    border-radius: 3px;

}

.message {
    display: flex;
    gap: 16px;
    margin-bottom: 24px;
}

.message.user {
    flex-direction: row-reverse;
}

/* 头像样式 */
.avatar {
    width: 40px;
    height: 40px;
    border-radius: 50%;
    flex-shrink: 0;
    overflow: hidden;
}

.avatar img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.bot-avatar {
    background: #e8f4ff;
}

.user-avatar {
    background: #f0f2f5;
}

/* 消息气泡 */
.message-content {
    max-width: 70%;
    min-width: 120px;
}

.message-bubble {
    border-radius: 20px;
    position: relative;
    animation: fadeIn 0.3s ease;
}

.message.user .message-bubble {
    background: #eff6ff;
    color: rgb(0, 0, 0);
    height: 39px;
    line-height: 39px;
    white-space: pre-wrap;
    word-break: break-word;
    border-radius: 20px 20px 4px 20px;
}

.message.bot .message-bubble {
    background: #fff;
    text-align: left;
    color: rgba(0, 0, 0, .85) !important;
    padding: 20px;
    border: 1px solid #e0e0e0;
    border-radius: 20px 20px 20px 4px;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}

.meta-info {
    display: flex;
    justify-content: space-between;
    padding: 4px 8px;
    font-size: 12px;
    color: #999;
}

.status {
    color: #27ae60;
}

.input-area {
    display: flex;
    gap: 12px;
    padding: 20px;
    border-top: 1px solid #eee;
    background: #fff;
}

.message-input {
    flex: 1;
    padding: 14px 20px;
    border: 2px solid #eee;
    border-radius: 10px;
    font-size: 16px;
    transition: all 0.3s;
}

.message-input:focus {
    border-color: #3498db;
    box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1);
    outline: none;
}

.send-btn {
    padding: 0 28px;
    border: none;
    background: #3498db;
    color: white;
    border-radius: 10px;
    cursor: pointer;
    transition: all 0.3s;
    display: flex;
    align-items: center;
}

.send-btn:disabled {
    background: #a0cfff;
    cursor: not-allowed;
}

.send-btn:hover:not(:disabled) {
    background: #2980b9;
    transform: translateY(-1px);
}


.loading-dots {
    display: flex;
    gap: 4px;
    padding: 0 8px;
}

.dot {
    width: 6px;
    height: 6px;
    background: #fff;
    border-radius: 50%;
    animation: bounce 1.4s infinite;
}

.dot:nth-child(2) {
    animation-delay: 0.2s;
}

.dot:nth-child(3) {
    animation-delay: 0.4s;
}


.map-panel {
    width: 40%;
    background: #fff;
    border-left: 1px solid #eee;
    display: flex;
    flex-direction: column;
}



.chat-Knowledge {
    width: 40%;
    background: linear-gradient(177deg, #F8F7FD, #DFEAFE);
    border-left: 1px solid #eee;
    display: flex;
    flex-direction: column;

}

.chatnowledgeButton {
    width: 114px;
    height: 32px;
    background-color: #ebebeb;
    color: #000000;
    font-family: PingFang SC;
    font-weight: 500;
    font-size: 16px;
    line-height: 32px;
    color: #1A1A1A;
    border-radius: 5px;
    cursor: pointer;
}

.map-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 0px 10px;
    border-bottom: 1px solid #eee;
}

.close-btn {
    border: none;
    background: none;
    font-size: 24px;
    color: #666;
    cursor: pointer;
    padding: 0 8px;
}

.map-container {
    flex: 1;
    padding: 20px;
}

.map-placeholder {
    height: 100%;
    background: #f8f9fa;
    border-radius: 12px;
    display: flex;
    align-items: center;
    justify-content: center;
    color: #666;
}

.fab-container {
    position: fixed;
    bottom: 30px;
    right: 30px;
    display: flex;
    gap: 12px;
}

.map-fab {
    width: 56px;
    height: 56px;
    border: none;
    border-radius: 50%;
    background: #3498db;
    color: white;
    font-size: 24px;
    cursor: pointer;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
    transition: all 0.3s;
}

.map-fab:hover {
    background: #2980b9;
    transform: translateY(-2px);
}

/* 动画 */
@keyframes fadeIn {
    from {
        opacity: 0;
        transform: translateY(10px);
    }

    to {
        opacity: 1;
        transform: translateY(0);
    }
}

@keyframes bounce {

    0%,
    80%,
    100% {
        transform: translateY(0);
    }

    40% {
        transform: translateY(-4px);
    }
}

@keyframes cursorBlink {
    0% {
        opacity: 1;
    }

    50% {
        opacity: 0;
    }

    100% {
        opacity: 1;
    }
}

@keyframes stream-appear {
    from {
        opacity: 0;
        transform: translateY(5px);
    }

    to {
        opacity: 1;
        transform: translateY(0);
    }
}

.stream-chunk {
    animation: stream-appear 0.3s ease-out;
}

.cursor {
    animation: cursorBlink 1s infinite;
    font-weight: bold;
    margin-left: 2px;
}


.chat-input-main-container {
    width: 80%;
    display: flex;
    bottom: 0px;
    position: fixed;
    justify-content: center;
    background-color: #fff;
}


.back-to-top-button {
    width: 32px;
    height: 32px;
    border-radius: 50%;
    cursor: pointer;
    display: flex;
    justify-content: center;
    align-items: center;
    border: 1px solid rgba(0, 0, 0, 0.1);
    background-color: #fff;
    position: absolute;
    top: -12px;
    transform: translateY(-100%);
}

.back-to-top-button:hover {
    background-color: #F6F7F9;
}

.back-to-top-button svg {
    width: 16px;
    height: 16px;
    color: #000;
}


.chat-input-section {
    z-index: 9;
    position: relative;
    width: 100%;
    max-width: 866px;
    padding-left: 16px;
    padding-bottom: 12px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
}


.chat-input-container {
    width: 100%;
    position: relative;
    border-radius: 24px;
    border: 1px solid rgba(0, 0, 0, 0.1);
    background-color: white;
    box-shadow: rgba(0, 0, 0, 0.1) 0px 2px 6px 0px;
    box-sizing: border-box;
}

.chat-input-content {
    width: 97%;
    padding: 12px;
    background-color: #fff;

    overflow: hidden;
    border-radius: 24px;
}

.chat-input-wrapper {
    width: 100%;
    display: flex;
    flex-direction: column;
    gap: 16px;
}

.chat-input-textarea {
    padding-left: 4px;
    width: 100%;
}

.chat-input-textarea textarea {
    width: 100%;
    font-size: 16px;
    line-height: 1.375;
    resize: none;
    word-break: break-all;
    overflow-x: hidden;
    border: none;
    outline: none;
    min-height: 22px;
    max-height: 88px;
    padding: 0;
    background-color: transparent;
    color: #333;

}


.chat-input-bottom {
    display: flex;
    align-items: flex-end;
    justify-content: space-between;
    width: 100%;
}

.chat-input-feature-buttons {
    display: flex;
    align-items: flex-end;
    justify-content: flex-start;
    gap: 8px;

}

.chat-input-feature-button {
    padding-left: 10px;
    padding-right: 10px;
    height: 32px;
    display: flex;
    align-items: center;
    border-radius: 50px;
    cursor: pointer;
    background-color: #f0f0f0;

}

.chat-input-feature-button:hover {
    background-color: #f0f0f0;

}

.chat-input-feature-button svg {
    width: 20px;
    height: 20px;
    margin-right: 6px;
}

.chat-input-feature-button span {
    color: #333;

    font-size: 14px;
    font-weight: bold;
    white-space: nowrap;
}

.chat-input-tools {
    display: flex;
    align-items: center;
    position: relative;
    top: 4px;
}

.chat-input-tool-button {
    display: flex;
    align-items: center;
    border-radius: 50px;
    cursor: pointer;
    color: #333;

    background-color: transparent;
    border: none;
    padding: 8px;
}

.chat-input-tool-button:hover {
    background-color: rgba(0, 0, 0, 0.1);
    
}

.chat-input-tool-button svg {
    width: 24px;
    height: 24px;
}

.chat-input-tool-microphone {
    border-radius: 50px;
    transition: background-color 0.3s;
    padding: 4px;
}

.chat-input-tool-microphone:hover {
    background-color: rgba(0, 0, 0, 0.1);
   
}

.chat-input-tool-microphone svg {
    width: 32px;
    height: 32px;
    color: #333;
}

.chat-input-divider {
    margin-right: 16px;
}

.chat-input-send-button {
    border-radius: 50px;
    cursor: pointer;
    background-color: #000;
    flex-shrink: 0;
    width: 32px;
    height: 32px;
    display: flex;
    justify-content: center;
    align-items: center;
    pointer-events: none;
}

.chat-input-send-button:hover {
    background-color: #222;
}

.chat-input-send-button svg {
    color: #fff;
    width: 14px;
    height: 17px;
}

.chat-input-footer {
    display: flex;
    font-size: 12px;
    color: rgba(51, 51, 51, 0.3);
   
}

.chat-input-footer a {
    color: #006BFF;
}

.chat-input-footer a:hover {
    text-decoration: underline;
}

.custom-select {
    /* 外观设置 */
    appearance: none;
    -webkit-appearance: none;
    -moz-appearance: none;

    text-align: center;
    padding: 5px 5px;
    background: transparent;
    font-size: 16px;
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    border: none;
    border-radius: 8px;
  
    color: #333333;

    color: #333;
    font-size: 14px;
    font-weight: bold;
    white-space: nowrap;
    cursor: pointer;

    transition: all 0.3s ease;
}


.custom-select:hover {
    //   border-color: #999999; 
}


.custom-select:focus {
    outline: none;
}


.custom-select::-ms-expand {
    display: none;
}

.custom-select::after {
    content: '\25BC';
    position: absolute;
    top: 50%;
    right: 16px;
    transform: translateY(-50%);
    color: #999999;
    pointer-events: none;
}
</style>


<style lang="less">

.markdown-content {


    p:has(+ p) {
        margin-bottom: 0.4em;
    }

    /* 防止代码块换行 */
    pre {
        white-space: pre-wrap;
        word-break: break-word;
    }

    /* 紧凑模式 */
    &.compact-mode {
        line-height: 1.5;

        p,
        li {
            margin: 0.4em 0;
        }

        pre {
            margin: 0.8em 0;
            padding: 0.8em !important;
        }
    }

    /* 动画过渡 */
    .typing-effect {
        animation: pulse 1.5s infinite;
    }

    @keyframes pulse {

        0%,
        100% {
            opacity: 1
        }

        50% {
            opacity: 0.5
        }
    }
}

/* 光标闪烁动画 */
.cursor {
    animation: cursorBlink 1s infinite;
    margin-left: 2px;
    color: #333;
    /* 光标颜色 */
}

@keyframes cursorBlink {

    0%,
    100% {
        opacity: 1;
    }

    50% {
        opacity: 0;
    }
}
</style>

运行结果:

总结

通过使用 Vue 框架重构 RAGFlow,我们成功实现了一个简单的聊天功能。在这个过程中,我们学习了如何使用 Vue 的响应式数据绑定和组件化开发来构建界面,以及如何与后端 API 进行交互。同时,我们也了解了 RAG 流程的基本原理和实现方法。希望本文能够帮助你在实际项目中应用这些技术,实现更加智能和高效的聊天系统。

写这份代码的时候本着研究的心理去写的,写的过程中发现,源码的架构很强,感叹不愧是大厂工程级别的项目,他的思路每一步都感觉有迹可循,在写代码的时候就像是做手术,一层一层去理解它的思路去刨析,很快乐,这个聊天涉及到的历史记录, 多轮对话,助理选择,主要聊天层面的实现方法这里不对外展示了,有需要的可以后台T我,下面附上我的思路表。(其实我主要是学习思路)

相关推荐
掘金一周2 分钟前
金石焕新程 >> 瓜分万元现金大奖征文活动即将回归 | 掘金一周 4.3
前端·人工智能·后端
白雪讲堂18 分钟前
AI搜索品牌曝光资料包(精准适配文心一言/Kimi/DeepSeek等场景)
大数据·人工智能·搜索引擎·ai·文心一言·deepseek
三翼鸟数字化技术团队20 分钟前
Vue自定义指令最佳实践教程
前端·vue.js
斯汤雷24 分钟前
Matlab绘图案例,设置图片大小,坐标轴比例为黄金比
数据库·人工智能·算法·matlab·信息可视化
ejinxian30 分钟前
Spring AI Alibaba 快速开发生成式 Java AI 应用
java·人工智能·spring
葡萄成熟时_35 分钟前
【第十三届“泰迪杯”数据挖掘挑战赛】【2025泰迪杯】【代码篇】A题解题全流程(持续更新)
人工智能·数据挖掘
Jasmin Tin Wei1 小时前
蓝桥杯 web 学海无涯(axios、ecahrts)版本二
前端·蓝桥杯
机器之心1 小时前
一篇论文,看见百度广告推荐系统在大模型时代的革新
人工智能