NestJS 构建 AI 流式聊天服务:前端篇

135 阅读10分钟

基于 Vue 3 + TypeScript + Vite + Naive UI 构建的现代化 AI 聊天客户端,提供流畅的实时对话体验,支持 Markdown 渲染、代码高亮和流式停止功能。

项目地址 喜欢的可以点点star

https://github.com/lxiiang/nestjs-ai-stream

在这里插入图片描述

🚀 项目概述

这是一个现代化的 AI 聊天前端应用,采用最新的前端技术栈,为用户提供流畅的实时对话体验。

✨ 核心特性

  • 🎯 Vue 3 Composition API: 使用最新的 Vue 3 组合式 API,提供更好的逻辑复用和类型推导
  • 🔧 TypeScript 支持: 完整的类型安全保障,提升开发体验和代码质量
  • 实时流式显示: 基于 SSE 技术实现 AI 回复逐字实时显示,用户体验流畅
  • 📝 Markdown 渲染: 集成 markdown-it 自动渲染 Markdown 格式内容
  • 🌈 代码高亮: 使用 highlight.js 支持多语言代码语法高亮
  • ⏹️ 流式停止功能: 支持用户随时中断 AI 生成过程
  • 📱 响应式设计: 适配桌面端和移动端各种屏幕尺寸
  • 🧩 组件化架构: 模块化组件设计,易于维护和功能扩展
  • 🎨 现代化 UI: 集成 Naive UI 组件库,提供美观的界面设计
  • 🔄 自动导入: 使用 unplugin-auto-import 自动导入 Vue API,提升开发效率

📁 项目结构

web-client/
├── src/
│   ├── components/           # 组件目录
│   │   ├── Head/            # 头部组件
│   │   │   └── index.vue    # 应用头部,包含标题和渐变背景
│   │   ├── Chat/            # 聊天组件
│   │   │   └── index.vue    # 聊天内容区域,支持消息渲染和欢迎页面
│   │   └── Input/           # 输入组件
│   │       └── index.vue    # 消息输入框,支持快捷键和流式控制
│   ├── hooks/               # 组合式函数
│   │   ├── useChat.js       # 聊天逻辑钩子,处理流式数据接收和状态管理
│   │   └── useMitt.js       # 事件总线钩子,用于组件间通信
│   ├── App.vue              # 根组件,定义整体布局和样式
│   ├── main.ts              # 应用入口,Vue 应用初始化
│   └── style.css            # 全局样式文件
├── public/                  # 公共资源目录
│   └── vite.svg             # Vite 默认图标
├── index.html               # HTML 模板文件
├── vite.config.ts           # Vite 构建配置
├── tsconfig.json            # TypeScript 配置
├── tsconfig.app.json        # 应用 TypeScript 配置
├── tsconfig.node.json       # Node.js TypeScript 配置
├── package.json             # 项目依赖和脚本配置
├── auto-imports.d.ts        # 自动导入类型声明文件
├── vite-env.d.ts           # Vite 环境类型声明
└── README.md                # 项目文档

🔍 核心实现详解

1. 聊天逻辑钩子:useChat.js

核心的聊天逻辑封装,处理流式数据接收和状态管理:

import { ref } from "vue";
import { v4 as uuidv4 } from "uuid";
import { fetchEventSource } from "@microsoft/fetch-event-source";
import MarkdownIt from "markdown-it";
import hljs from "highlight.js";

// 响应式状态管理
const messages = ref([]);
const curMessage = ref({});
const controller = ref(null);
const isStreaming = ref(false);

// Markdown 渲染器配置
const markdown = MarkdownIt({
  html: true, // 允许在 Markdown 中使用原始 HTML 标签
  linkify: true, // 自动将 URL 转换为链接
  typographer: true, // 启用智能引号和连字符
  breaks: true, // 启用自动换行
  xhtmlOut: true, // 使用 XHTML 模式
  langPrefix: "language-", // 添加语言前缀
});

// 代码高亮配置
markdown.set({
  highlight: function (str, lang) {
    if (lang && hljs.getLanguage(lang)) {
      try {
        return (
          '<pre class="hljs"><code>' +
          hljs.highlight(str, { language: lang, ignoreIllegals: true }).value +
          "</code></pre>"
        );
      } catch (__) {}
    }
    return (
      '<pre class="hljs"><code>' +
      markdown.utils.escapeHtml(str) +
      "</code></pre>"
    );
  },
});

export function useChat() {
  // 处理流式消息
  const handleStreamMessage = (ev) => {
    if (ev.data) {
      const data = JSON.parse(ev.data);

      if (data.type === "start") {
        // 开始流式响应
        curMessage.value = {
          mid: uuidv4(),
          role: "assistant",
          content: "",
        };
        messages.value.push(curMessage.value);
      }

      if (data.type === "chunk") {
        // 追加内容并渲染 Markdown
        curMessage.value.content += data.content;
        curMessage.value.htmlStr = markdown.render(curMessage.value.content);
      }

      if (data.type === "end") {
        curMessage.value = {};
        isStreaming.value = false;
      } else if (data.type === "error") {
        // 处理错误情况
        isStreaming.value = false;
        messages.value.push({
          role: "assistant",
          content: data.message,
        });
      }
    }
  };

  // 处理流式错误
  const handleStreamError = (ev) => {
    isStreaming.value = false;
    messages.value.push({
      role: "assistant",
      content: "服务异常",
    });
  };

  // API 调用
  const queryAnswer = async (message) => {
    controller.value = new AbortController();
    const signal = controller.value.signal;

    fetchEventSource("/api/ai/chat/stream-sse", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify(message),
      signal, // 中断信号
      openWhenHidden: true, // 页面隐藏时仍保持连接
      onmessage: (ev) => handleStreamMessage(ev),
      onerror: (ev) => handleStreamError(ev),
    });
  };

  // 发送消息
  const sendMessage = (userMsg) => {
    if (!userMsg.trim()) return;

    const userMessage = {
      mid: uuidv4(),
      role: "user",
      content: userMsg,
    };

    messages.value.push(userMessage);
    isStreaming.value = true;
    queryAnswer({ message: userMsg });
  };

  // 停止对话
  const stopConversation = () => {
    if (controller.value) {
      controller.value.abort();
      isStreaming.value = false;
    }
  };

  return {
    messages,
    isStreaming,
    sendMessage,
    stopConversation,
  };
}

技术亮点:

  • 组合式 API: 使用 Vue 3 的 ref 管理响应式状态
  • 流式数据处理: 通过 fetchEventSource 处理 Server-Sent Events 数据流
  • Markdown 渲染: 集成 markdown-it 实现富文本显示,支持 HTML 标签
  • 代码高亮: 使用 highlight.js 提供多语言语法高亮
  • 错误处理: 完善的错误处理机制,包括网络异常和服务异常
  • 连接管理: 支持页面隐藏时保持连接,提供更好的用户体验
  • 中断控制: 使用 AbortController 实现流式响应的中断功能

2. 聊天内容组件:Chat/index.vue

负责展示聊天消息列表和处理消息渲染,包含欢迎页面和消息展示:

<script setup>
import { useChat } from "@/hooks/useChat";

const { messages } = useChat();
</script>

<template>
  <div id="chat-container" class="chat-container">
    <!-- 欢迎页面 -->
    <div v-if="messages.length === 0" class="welcome-message">
      <div class="welcome-content">
        <h2>👋 你好!我是你的数字人助手</h2>
        <p>有什么可以帮助你的吗?</p>
      </div>
    </div>

    <!-- 消息列表 -->
    <div
      v-for="message in messages"
      :key="message.id"
      :class="['message', message.role]"
    >
      <!-- 用户头像 -->
      <div class="message-avatar">
        <span v-if="message.role === 'user'">👤</span>
        <span v-else>🤖</span>
      </div>

      <!-- 消息内容 -->
      <div class="message-content">
        <div
          class="message-text"
          v-if="message.htmlStr"
          v-html="message.htmlStr"
        ></div>
        <div class="message-text" v-else>
          {{ message.content }}
          <span v-if="message.isStreaming" class="typing-indicator">▋</span>
        </div>
      </div>
    </div>
  </div>
</template>

<style scoped>
.chat-container {
  flex: 1;
  overflow-y: auto;
  padding: 20px;
  background: white;
}

.welcome-message {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100%;
  text-align: center;
}

.message {
  display: flex;
  margin-bottom: 20px;
  animation: fadeIn 0.3s ease-in;
}

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

.message-avatar {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 20px;
  margin: 0 10px;
  background: #f0f0f0;
}

.message.user .message-avatar {
  background: #667eea;
  color: white;
}

.message-content {
  max-width: 70%;
  background: #f8f9fa;
  padding: 12px 16px;
  border-radius: 18px;
  position: relative;
}

.message.user .message-content {
  background: #667eea;
  color: white;
}

.typing-indicator {
  animation: blink 1s infinite;
  color: #667eea;
}
</style>

组件特性:

  • 欢迎页面: 首次访问时显示友好的欢迎界面
  • 消息渲染: 区分用户和 AI 消息的不同样式和布局
  • 头像系统: 使用 emoji 图标区分用户和 AI 身份
  • Markdown 支持: AI 消息支持 HTML 渲染显示 Markdown 内容
  • 打字指示器: 流式输入时显示闪烁的光标动画
  • 响应式布局: 用户消息右对齐,AI 消息左对齐
  • 动画效果: 消息出现时的淡入动画效果

3. 输入组件:Input/index.vue

处理用户输入和发送逻辑,支持快捷键和流式控制:

<script setup>
import { useChat } from "@/hooks/useChat";

const { sendMessage, isStreaming, stopConversation } = useChat();

const inputMessage = ref("");

// 处理键盘事件
const handleKeyPress = (event) => {
  if (isStreaming.value) {
    stopConversation();
    return;
  }
  if (!inputMessage.value.trim()) return;
  if (event.key === "Enter" && !event.shiftKey) {
    event.preventDefault();
  }
  sendMessage(inputMessage.value);
  inputMessage.value = "";
};
</script>

<template>
  <!-- 输入区域 -->
  <div class="input-area">
    <div class="input-container">
      <textarea
        v-model="inputMessage"
        @keypress="handleKeyPress"
        placeholder="输入你的消息... (Enter发送,Shift+Enter换行)"
        class="message-input"
        rows="1"
      ></textarea>
      <button @click="handleKeyPress(inputMessage)" class="send-btn">
        {{ isStreaming ? "   ⏹️ 停止" : "发送" }}
      </button>
    </div>
  </div>
</template>

<style scoped>
.input-area {
  padding: 20px;
  background: white;
  border-top: 1px solid #eee;
}

.input-container {
  display: flex;
  gap: 10px;
  align-items: flex-end;
}

.message-input {
  flex: 1;
  border: 2px solid #e0e0e0;
  border-radius: 20px;
  padding: 12px 16px;
  font-size: 14px;
  resize: none;
  outline: none;
  transition: border-color 0.2s;
  min-height: 20px;
  max-height: 120px;
}

.message-input:focus {
  border-color: #667eea;
}

.send-btn {
  padding: 12px 24px;
  background: #667eea;
  color: white;
  border: none;
  border-radius: 20px;
  cursor: pointer;
  font-size: 14px;
  font-weight: 500;
  transition: all 0.2s;
  white-space: nowrap;
}

.send-btn:hover {
  background: #5a6fd8;
}
</style>

输入特性:

  • 智能键盘处理: Enter 键根据流式状态执行发送或停止操作
  • 快捷键支持: Shift+Enter 换行,Enter 发送消息
  • 动态按钮状态: 根据流式状态切换"发送"和"停止"按钮
  • 自适应输入框: 支持多行输入,最大高度限制
  • 输入验证: 防止发送空消息
  • 视觉反馈: 输入框聚焦时边框颜色变化
  • 响应式设计: 适配不同屏幕尺寸的布局

4. 头部组件:Head/index.vue

简洁的应用头部组件,提供品牌展示:

<script setup></script>

<template>
  <div class="chat-header">
    <h1>🤖 数字人助手</h1>
  </div>
</template>

<style scoped>
.chat-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 20px;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  color: white;
}

.chat-header h1 {
  margin: 0;
  font-size: 24px;
  font-weight: 600;
}
</style>

头部特性:

  • 渐变背景: 使用 CSS 渐变创建美观的视觉效果
  • 品牌展示: 清晰的标题和图标展示
  • 预留扩展: 预留了操作按钮的位置,便于后续功能扩展

🎨 UI 组件库支持

Naive UI 集成

项目已集成 Naive UI 组件库,这是一个基于 Vue 3 的现代化 UI 组件库:

特性优势:

  • 🎯 TypeScript 友好: 完整的 TypeScript 支持
  • 🎨 主题定制: 强大的主题系统,支持暗黑模式
  • 📱 响应式设计: 适配各种屏幕尺寸
  • 性能优化: 按需加载,减少打包体积
  • 🔧 易于使用: 简洁的 API 设计

使用示例:

<script setup>
import { NButton, NMessage } from "naive-ui";

// 显示消息提示
const showMessage = () => {
  window.$message.success("操作成功!");
};
</script>

<template>
  <div>
    <n-button type="primary" @click="showMessage"> 点击我 </n-button>
  </div>
</template>

按需导入配置:

// vite.config.ts
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import AutoImport from "unplugin-auto-import/vite";

export default defineConfig({
  plugins: [
    vue(),
    AutoImport({
      imports: [
        "vue",
        {
          "naive-ui": [
            "useMessage",
            "useDialog",
            "useNotification",
            "useLoadingBar",
          ],
        },
      ],
      dts: true,
    }),
  ],
});

🔧 开发配置

环境要求

  • Node.js >= 16.0.0
  • pnpm >= 7.0.0

本地开发

快速开始

# 1. 克隆项目
git clone <repository-url>
cd web-client

# 2. 安装依赖
pnpm install

# 3. 启动开发服务器
pnpm run dev

# 4. 打开浏览器访问
# http://localhost:5173

开发命令

# 启动开发服务器(热重载)
pnpm run dev

# 构建生产版本
pnpm run build

# 预览生产构建
pnpm run preview

# 类型检查
pnpm run type-check

# 代码格式化
pnpm run format

# 代码检查
pnpm run lint

开发环境配置

环境变量配置

创建 .env.local 文件:

# API 基础地址
VITE_API_BASE_URL=http://localhost:3000

# 应用标题
VITE_APP_TITLE=数字人助手

# 是否启用调试模式
VITE_DEBUG=true
Vite 配置优化
// vite.config.ts
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import AutoImport from "unplugin-auto-import/vite";

export default defineConfig({
  plugins: [
    vue(),
    AutoImport({
      imports: ["vue"],
      dts: true,
    }),
  ],
  resolve: {
    alias: {
      "@": resolve(__dirname, "src"),
    },
  },
  server: {
    port: 5173,
    host: true, // 允许外部访问
    proxy: {
      "/api": {
        target: process.env.VITE_API_BASE_URL || "http://localhost:3000",
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, ""),
      },
    },
  },
  build: {
    // 构建优化
    rollupOptions: {
      output: {
        manualChunks: {
          "vue-vendor": ["vue"],
          "ui-vendor": ["naive-ui"],
          "utils-vendor": ["markdown-it", "highlight.js"],
        },
      },
    },
    // 压缩配置
    minify: "terser",
    terserOptions: {
      compress: {
        drop_console: true,
        drop_debugger: true,
      },
    },
  },
});

开发工具推荐

VS Code 扩展
{
  "recommendations": [
    "Vue.volar",
    "Vue.vscode-typescript-vue-plugin",
    "bradlc.vscode-tailwindcss",
    "esbenp.prettier-vscode",
    "dbaeumer.vscode-eslint"
  ]
}
VS Code 设置
{
  "editor.formatOnSave": true,
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  },
  "typescript.preferences.importModuleSpecifier": "relative"
}

Vite 配置

import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import AutoImport from "unplugin-auto-import/vite";

export default defineConfig({
  plugins: [
    vue(),
    AutoImport({
      imports: ["vue"],
      dts: true, // 自动生成类型声明
    }),
  ],
  resolve: {
    alias: {
      "@": resolve(__dirname, "src"),
    },
  },
  server: {
    port: 5173,
    proxy: {
      "/api": {
        target: "http://localhost:3000", // 后端服务地址
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, ""),
      },
    },
  },
});

📦 核心依赖

生产依赖

包名版本用途
vue^3.5.18Vue 3 框架核心,提供响应式系统和组件化开发
@microsoft/fetch-event-source^2.0.1SSE 客户端库,处理流式数据接收
markdown-it^14.1.0Markdown 解析和渲染,支持扩展插件
highlight.js^11.11.1代码语法高亮库,支持多种编程语言
uuid^13.0.0生成唯一标识符,用于消息 ID
mitt^3.0.1轻量级事件总线,用于组件间通信
naive-ui^2.42.0Vue 3 UI 组件库,提供现代化界面组件
qs^6.14.0URL 查询字符串解析和序列化库

依赖说明

核心功能依赖

  1. Vue 3 生态

    • vue: 核心框架,提供 Composition API 和响应式系统
    • @vitejs/plugin-vue: 支持 .vue 单文件组件
  2. 流式通信

    • @microsoft/fetch-event-source: 处理 Server-Sent Events,实现实时数据流
    • 支持连接中断、错误重试等高级功能
  3. 内容渲染

    • markdown-it: 将 Markdown 文本转换为 HTML
    • highlight.js: 为代码块提供语法高亮
  4. 工具库

    • uuid: 生成全局唯一标识符
    • mitt: 轻量级事件发布订阅系统
    • qs: URL 参数处理
  • Auto Import: 自动导入 Vue API
    • 减少重复的 import 语句
    • 自动生成类型声明文件
    • 提升开发效率

版本兼容性

  • Node.js: >= 16.0.0
  • pnpm: >= 7.0.0
  • Vue: 3.5.x (支持 Composition API)
  • TypeScript: 5.x

包管理器

项目使用 pnpm 作为包管理器,相比 npm 和 yarn 具有以下优势:

  • 🚀 更快的安装速度: 通过硬链接和符号链接优化
  • 💾 节省磁盘空间: 共享依赖包,避免重复存储
  • 🔒 严格的依赖管理: 避免幽灵依赖问题
  • 📦 更好的 monorepo 支持: 适合多包项目管理

🎯 总结

这个方案不仅实现了基础的流式聊天功能,还考虑了错误处理、用户体验和可扩展性。实际项目中可以基于这个框架,添加身份验证、对话历史存储等功能。 希望这篇分享对你有帮助,有任何问题欢迎在评论区交流!