Skip to content

轻松玩转AI Chat组件库:从零开始打造你的智能对话界面

大家好!今天我们要聊的是一个让所有开发者都欢呼的神器——TDesign推出的AI Chat组件库。他可以用最简单的代码快速搭建一个颜值在线、功能强大的AI聊天界面,其实我想要这个组件想很久了,现在他总算来了,终于不用从零开始造轮子了。

一、AI Chat组件库:你的聊天界面"速成班"

首先让我们认识一下今天的主角——TD Chat for AI。这是腾讯基于TDesign体系推出的专门用于构建AI聊天界面的组件库,特别适合在Vue3.x技术栈中使用。就像乐高积木一样,它提供了一系列预制好的"积木块",让你可以像搭积木一样快速拼出一个专业的聊天界面。

这个组件库有哪些炫酷的功能呢?

  • 基础问答:轻松实现一问一答的对话流
  • 自定义内容展示:你可以随意打扮你的聊天头像、昵称,就像给虚拟人物换装一样简单
  • Markdown支持:再复杂的代码块和格式也能优雅展示
  • 可配置思维链:让AI的思考过程可视化,用户看得明明白白

二、准备工作:初始一个web项目

为了方便快速开发项目,TDesign 提供了一个脚手架 tdesign-starter-cli,方便我们通过它来初始化项目。在安装脚手架之前,请确保已经拥有nodenpm的开发环境。

1. 安装tdesign-starter-cli

npm i tdesign-starter-cli -g

2.创建项目

bash
# 在终端执行下面命令
td-starter init

按照提示一步一步往下走就可以了

执行完蓝色字体的命令提示后就可以开始下面的步骤了

3. 安装TD Chat for AI

接下来,让我们请出今天的主角:

bash
npm install @tdesign-vue-next/chat
# 后面要用,提前加上
npm install less

(小贴士:记得检查你的Vue版本是3.x哦,这个组件库可是Vue3的"死忠粉")

三、快速上手:5分钟打造你的第一个AI聊天界面

现在到了最激动人心的部分——写代码!别担心,我会手把手带你飞。

1.引入Chat组件

main.ts

ts
import { createApp } from "vue";
import TDesign from "tdesign-vue-next";
import TDesignChat from "@tdesign-vue-next/chat"; // 引入chat组件
import "tdesign-vue-next/es/style/index.css";

import App from "./App.vue";

createApp(App).use(TDesign).use(TDesignChat).mount("#app");

1. 基本聊天界面

让我们先创建一个最简单的聊天界面:

App.vue

vue
<template>
  <t-space align="center">
    <t-button theme="primary" @click="visible = true">AI助手悬窗展示</t-button>
  </t-space>
  <t-drawer v-model:visible="visible" :footer="false" size="480px" :close-btn="true" class="drawer-box">
    <template #header>
      <t-avatar size="32px" shape="circle" image="https://tdesign.gtimg.com/site/chat-avatar.png"></t-avatar>
      <span class="title">Hi, &nbsp;我是AI</span>
    </template>
    <t-chat
      layout="both"
      :clear-history="chatList.length > 0 && !isStreamLoad"
      @on-action="operation"
      @clear="clearConfirm"
    >
      <template v-for="(item, index) in chatList" :key="index">
        <t-chat-item
          :role="item.role"
          :content="item.content"
          :text-loading="index === 0 && loading"
          :variant="getStyle(item.role)"
        >
          <template v-if="!isStreamLoad" #actions>
            <t-chat-action
              :is-good="isGood"
              :item-index="index"
              :is-bad="isBad"
              :content="item.content"
              @operation="handleOperation"
            />
          </template>
        </t-chat-item>
      </template>
      <template #footer>
        <t-chat-input :stop-disabled="isStreamLoad" @send="inputEnter" @stop="onStop"> </t-chat-input>
      </template>
    </t-chat>
  </t-drawer>
</template>
<script setup>
import { ref } from 'vue';
const visible = ref(false);
import { MockSSEResponse } from './mock-data/sseRequest';

const fetchCancel = ref(null);
const loading = ref(false);
const isStreamLoad = ref(false);
const isGood = ref(false);
const isBad = ref(false);

const getStyle = (role) => {
  if (role === 'assistant') {
    return 'outline';
  }
  if (role === 'user') {
    return 'base';
  }
  if (role === 'error') {
    return 'text';
  }
  return 'text';
};

const handleOperation = function (type, options) {
  const { index } = options;
  if (type === 'good') {
    isGood.value = !isGood.value;
    isBad.value = false;
  } else if (type === 'bad') {
    isBad.value = !isBad.value;
    isGood.value = false;
  } else if (type === 'replay') {
    const userQuery = chatList.value[index + 1].content;
    inputEnter(userQuery);
  }
};
// 倒序渲染
const chatList = ref([
  {
    content: `模型由 <span>hunyuan</span> 变为 <span>GPT4</span>`,
    role: 'model-change',
  },
  {
    content: '它叫 McMurdo Station ATM,是美国富国银行安装在南极洲最大科学中心麦克默多站的一台自动提款机。',
    role: 'assistant',
  },
  {
    content: '南极的自动提款机叫什么名字?',
    role: 'user',
  },
]);
const operation = function (type, options) {
  console.log(type, options);
};
const clearConfirm = function () {
  chatList.value = [];
};
const onStop = function () {
  if (fetchCancel.value) {
    fetchCancel.value.controller.close();
    loading.value = false;
  }
};
const inputEnter = function (inputValue) {
  if (isStreamLoad.value) {
    return;
  }
  if (!inputValue) return;
  const params = {
    content: inputValue,
    role: 'user',
  };
  chatList.value.unshift(params);
  // 空消息占位
  const params2 = {
    content: '',
    role: 'assistant',
  };
  chatList.value.unshift(params2);
  handleData(inputValue);
};
const fetchSSE = async (fetchFn, options) => {
  const response = await fetchFn();
  const { success, fail, complete } = options;
  // 如果不 ok 说明有请求错误
  if (!response.ok) {
    complete?.(false, response.statusText);
    fail?.();
    return;
  }
  const reader = response?.body?.getReader();
  const decoder = new TextDecoder();
  if (!reader) return;
  const bufferArr = [];
  let dataText = ''; // 记录数据
  const event = { data: null };

  reader.read().then(function processText({ done, value }) {
    if (done) {
      // 正常的返回
      complete?.(true);
      return;
    }
    const chunk = decoder.decode(value, { stream: true });
    const buffers = chunk.toString().split(/\r?\n/);
    bufferArr.push(...buffers);
    const i = 0;
    while (i < bufferArr.length) {
      const line = bufferArr[i];
      if (line) {
        dataText = dataText + line;
        event.data = dataText;
      }
      if (event.data) {
        const jsonData = JSON.parse(JSON.stringify(event));
        success(jsonData);
        event.data = null;
      }
      bufferArr.splice(i, 1);
    }
    reader.read().then(processText);
  });
};
const handleData = async () => {
  loading.value = true;
  isStreamLoad.value = true;
  const lastItem = chatList.value[0];
  const mockedData = `这是一段模拟的流式字符串数据。`;
  const mockResponse = new MockSSEResponse(mockedData);
  fetchCancel.value = mockResponse;
  await fetchSSE(
    () => {
      return mockResponse.getResponse();
    },
    {
      success(result) {
        loading.value = false;
        const { data } = result;
        lastItem.content += data;
      },
      complete(isOk, msg) {
        if (!isOk || !lastItem.content) {
          lastItem.role = 'error';
          lastItem.content = msg;
        }
        // 控制终止按钮
        isStreamLoad.value = false;
        loading.value = false;
      },
    },
  );
};
</script>
<style lang="less">
/* 应用滚动条样式 */
::-webkit-scrollbar-thumb {
  background-color: var(--td-scrollbar-color);
}
::-webkit-scrollbar-thumb:horizontal:hover {
  background-color: var(--td-scrollbar-hover-color);
}
::-webkit-scrollbar-track {
  background-color: var(--td-scroll-track-color);
}
.title {
  margin-left: 16px;
  font-size: 20px;
  color: var(--td-text-color-primary);
  font-weight: 600;
  line-height: 28px;
}
.drawer-box {
  .t-drawer__header {
    padding: 32px;
  }
  .t-drawer__body {
    padding: 30px 32px;
  }
  .t-drawer__close-btn {
    right: 32px;
    top: 32px;
    background-color: var(--td-bg-color-secondarycontainer);
    width: 32px;
    height: 32px;
    border-radius: 50%;
    .t-icon {
      font-size: 20px;
    }
  }
}
</style>

sseRequest.ts

ts
export class MockSSEResponse {
  private controller!: ReadableStreamDefaultController<Uint8Array>;
  private encoder = new TextEncoder();
  private stream: ReadableStream<Uint8Array>;
  private error: boolean;

  constructor(
    private data: string,
    private delay: number = 300,
    error = false // 新增参数,默认为false
  ) {
    this.error = error;

    this.stream = new ReadableStream({
      start: (controller) => {
        this.controller = controller;
        if (!this.error) {
          // 如果不是错误情况,则开始推送数据
          setTimeout(() => this.pushData(), this.delay); // 延迟开始推送数据
        }
      },
      cancel() {},
    });
  }

  private pushData() {
    if (this.data.length === 0) {
      this.controller.close();
      return;
    }
    try {
      const chunk = this.data.slice(0, 1);
      this.data = this.data.slice(1);

      this.controller.enqueue(this.encoder.encode(chunk));

      if (this.data.length > 0) {
        setTimeout(() => this.pushData(), this.delay);
      } else {
        // 数据全部发送完毕后关闭流
        setTimeout(() => this.controller.close(), this.delay);
      }
    } catch {}
  }

  getResponse(): Promise<Response> {
    return new Promise((resolve) => {
      // 使用setTimeout来模拟网络延迟
      setTimeout(() => {
        if (this.error) {
          const errorResponseOptions = {
            status: 500,
            statusText: "Internal Server Error",
          };

          // 返回模拟的网络错误响应,这里我们使用500状态码作为示例
          resolve(new Response(null, errorResponseOptions));
        } else {
          resolve(new Response(this.stream));
        }
      }, this.delay); // 使用构造函数中设置的delay值作为延迟时间
    });
  }
}

看!就这么几行代码,你已经有了一个能发送和接收消息的聊天界面了。

2. 添加个性化装扮

加上昵称、头像,时间等元素,其实核心就是添加了昵称,头像和时间这几个属性,是不是很简单!

json
{
    avatar: "https://tdesign.gtimg.com/site/chat-avatar.png",
    role: "assistant",
    name: "AI小助手",
    content: "你好!我是AI小助手,有什么可以帮您的吗?",
    datetime: new Date().toDateString(),
  }

现在的聊天界面就有了昵称、头像和时间了!

3.添加思考过程

核心代码

vue
    <template #content="{ item, index }">
        <t-chat-reasoning
          v-if="item.reasoning?.length > 0"
          expand-icon-placement="right"
        >
          <template #header>
            <t-chat-loading v-if="isStreamLoad" text="思考中..." indicator />
            <div v-else style="display: flex; align-items: center">
              <CheckCircleIcon
                style="
                  color: var(--td-success-color-5);
                  font-size: 20px;
                  margin-right: 8px;
                "
              />
              <span>已深度思考</span>
            </div>
          </template>
          <t-chat-content
            v-if="item.reasoning.length > 0"
            :content="item.reasoning"
          />
        </t-chat-reasoning>
        <t-chat-content
          v-if="item.content.length > 0"
          :content="item.content"
        />
      </template>

四、高级玩法:让你的界面"有脑子"

到目前为止,我们的聊天界面还只是个"花瓶"——好看但没实际功能。现在让我们给它装上"大脑"!

1. 连接硅基流动API

没有注册的可以点击这里注册:https://cloud.siliconflow.cn/i/0hq76Lbt

2. 使用fetchEventSource实现实时流

在这之前我们需要安装这个包:

bat
npm install @microsoft/fetch-event-source

然后调用sse接口,接收并解析数据,核心代码如下:

ts
// 调用硅基流动的接口
const fetchAIResponse = async (inputValue) => {
  // 用于主动取消异步操作
  fetchController.value = new AbortController();
  // 流式加载是否结束
  isStreamLoad.value = true;
  // 聊天列表里的最有一个元素
  const lastItem = chatList.value[0];
  fetchEventSource("https://api.siliconflow.cn/v1/chat/completions", {
    method: "POST",
    headers: {
      Authorization:
        "Bearer sk-你的密钥",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      model: "deepseek-ai/DeepSeek-R1-Distill-Qwen-7B",
      messages: [
        {
          role: "user",
          content: inputValue,
        },
      ],
      // 开启流式
      stream: true,
    }),
    // 中断请求关联信号
    signal:fetchController.value.signal,
    onmessage(rsp) {
      console.log("【sse】Message from server:", rsp);
      // 流式响应或任务执行的结束标志
      if (rsp?.data === "[DONE]") {
        //lastItem.content += rsp?.data;
        loading.value = false;
        isStreamLoad.value = false;
        return;
      }
      // 将json转换成对象
      const data = JSON.parse(rsp?.data);
      // 获取内部的数据
      if (data.choices[0].delta.reasoning_content) {
        lastItem.reasoning += data.choices[0].delta.reasoning_content;
      } else if (data.choices[0].delta.content) {
        lastItem.content += data.choices[0].delta.content;
      }
    },
    onerror(err) {
      lastItem.content = `请求出错: ${err.message}`;
      fetchController.value.abort();
    },
  });
};

这样就能实现像ChatGPT那样的流式响应效果了!

五、总结:你的AI聊天界面开发手册

今天我们一步步实现了:

  1. t-chat组件搭建基础聊天界面
  2. 通过属性实现个性化聊天角色
  3. 实现流式响应提升用户体验
  4. 连接真实AI后端服务

记住,技术是为了让生活更美好。用这个组件库,你可以开发:

  • 智能客服系统
  • 在线教育助手
  • 企业内部问答机器人
  • 甚至是你个人的AI朋友

开发过程中如果遇到问题,不妨去看看TDesign的官方文档,或者在GitHub上查看相关issue。祝大家的AI聊天项目一帆风顺,我们代码的海洋里再见!

源码已上传,欢迎下载:https://gitee.com/ailot/ai_chat

官方文档:https://tdesign.tencent.com/chat/getting-started

关注我,了解更多AI黑科技