Skip to content

AI 对话

用于沉浸式智能体对话场景的组件。

新会话
我是假期小助手,很高兴见到你哦!
我是你的假期小助手,我可以提供假期余额查询、假期政策查询、还可以帮你请假哦
我可以帮你:
怎么请年休假、事假、产假等
咨询海信的请假政策
休假期间有哪些注意事项,填写请假申请有哪些注意点,等等!!
点击查看代码
vue
<template>
  <div style="width: 100%; height: 800px; border: 1px solid #ddd">
    <hi-chat
      ref="hiChatRef"
      :messages="messages"
      :historyChats="historyRecords"
      :suggestions="suggestions"
      :loading="loading"
      title="假期小助手"
      description="我是你的假期小助手,我可以提供假期余额查询、假期政策查询、还可以帮你请假哦"
      :allow-upload="true"
      :upload-config="{
        action: '/',
      }"
      @send="send"
      @stop="stop"
      @newChat="newChat"
      @selectHistoryChat="selectHistoryChat"
      @scrollTop="scrollTop"
    >
    </hi-chat>
  </div>
</template>
<script lang="ts" setup>
import { ref, reactive } from "vue";
import HiChat from "pangea-component/hi-chat";
import type { Message } from "pangea-component/hi-chat";
import "pangea-component/dist/style.css";

const hiChatRef = ref();
const messages = ref<Message[]>([]);
const loading = ref(false);
const historyRecords = ref([
  {
    id: "111",
    name: "咨询海信的请假政策",
  },
  {
    id: "222",
    name: "请求年休假",
  },
]);
const suggestions = [
  "怎么请年休假、事假、产假等",
  "咨询海信的请假政策",
  "休假期间有哪些注意事项,填写请假申请有哪些注意点,等等!!",
].map((i) => ({
  subTitle: i,
}));

const send = (value, files) => {
  const userMessage: Message = reactive({
    type: "user",
    id: Date.now(),
    content: value,
    status: "loading",
  });
  loading.value = true;
  messages.value.push(userMessage);
  if (files && files.length) {
    userMessage.files = files.map((file) => {
      return {
        name: file.name,
        url: file.url!,
        type: file.type,
      };
    });
  }
  hiChatRef.value.scrollToBottom();
  setTimeout(() => {
    userMessage.status = "success";
    loading.value = false;
    // ai回复消息
    const aiMessage: Message = reactive({
      type: "ai",
      id: Date.now(),
      content: "这是一条统一的测试回复信息哦!!!",
      thougth: "",
      pageJson: null,
      pageCode: undefined,
      isAgentThought: false,
      isThoughtCollapsed: false,
      status: "success",
    });
    messages.value.push(aiMessage);
    hiChatRef.value.scrollToBottom();
  }, 1000);
};
const stop = () => {};
const newChat = () => {
  messages.value = [];
};

const selectHistoryChat = (record) => {
  messages.value = [
    {
      type: "user",
      id: 1,
      content: "历史会话咨询问题",
      status: "success",
    },
    {
      type: "ai",
      id: 2,
      content: "这是一条统一的历史回复信息哦!!!",
      thougth: "",
      pageJson: null,
      pageCode: undefined,
      isAgentThought: false,
      isThoughtCollapsed: false,
      status: "success",
    },
  ];
};

const scrollTop = () => {};
</script>

安装方式

bash
yarn add pangea-component

引用方式

js
import HiChat from "pangea-component/hi-chat";
import "pangea-component/dist/style.css";

API

Props

参数名描述类型默认值
messages聊天列表数据Message[][]
loading聊天问题是否正在查询中(是否显示停止生成操作)Booleanfalse
historyChats历史会话列表Chat[][]
historyChatLoading历史会话列表加载状态booleanfalse
title标题string-
welcomeTitle欢迎句标题string-
description描述string-
suggestions建议问题列表SuggestionItem[][]
allowUpload是否允许上传booleanfalse
uploadType上传类型"image" | "file" | "all""all"
uploadLimit上传文件数量限制number-
uploadConfig上传接口配置UploadConfig-
ctxInjected工作卡注入参数方法等对象Record<string, any>-
javascript
interface Message {
  type: "ai" | "user"; // 消息类型
  id: number;
  content: string; // 消息内容
  thougth?: string; // 思考内容
  pageJson?: any; // 页面json
  isThinking?: boolean; // 是否正在思考中
  thinkTime?: boolean; // 思考时间
  status?: "loading" | "success" | "fail"; // 消息状态
  files?: MessageFileItem[]; // 气泡中显示的文件
}

interface MessageFileItem {
  name: string; // 文件名
  url: string; // url
  type: string; // 文件类型
}

interface Chat {
  name: string; // 会话名称
  id: string; // 会话id
}

interface SuggestionItem {
  title: string; // 主标题
  subTitle?: string; // 子标题
  icon?: VNode; // 图标
}

interface UploadConfig {
  action: string; // 上传接口路径
  headers?: Record<string, any>; // 请求头
  data?: Record<string, any>; // 请求附加参数
  name?: string; // 上传参数文件名 不设置默认file
  didFetch: (response) => { url: string, name: string }; // 上传成功回调函数需要返回 url和name数据
}

Events

方法名描述参数
send输入框发送事件(value: string) => {}
stop打断当前发送事件() => {}
newChat新建对话事件() => {}
selectHistoryChat选择历史会话记录事件(record: Object) => {}
scrollTop消息区域滚动到顶部(record: {conversionId: string}) => {}

Slot

插槽名描述参数
sender-top输入框顶部插槽-
sender-prefix输入框底部左侧扩展插槽-
conversation-extra-cont左侧会话列表内容扩展区域-
consation-footer左侧会话列表底部扩展区域-
welcome欢迎区域插槽-
bubble-header气泡顶部插槽item: Message
bubble-footer气泡底部插槽item: Message
bubble-form工作卡渲染插槽item: Message

Methods

方法名描述参数返回值
scrollToBottom会话信息列表内容滚动到底部方法--

盘古 3.0 项目工作卡渲染

对于盘古 3.0 脚手的的项目,本身已安装 pangea-ui 组件包,可直接使用页面渲染器渲染工作卡内容,代码示例如下:

vue
<template>
  <hi-chat
    ref="hiChatRef"
    :messages="messages"
    :loading="loading"
    :historyChats="historyRecords"
    :historyChatLoading="historyMsgLoading"
    :suggestions="suggestions"
    :title="agentInfo.name"
    :description="agentConfig.opening_statement"
    @send="send"
    @stop="stop"
    @newChat="newChat"
    @selectHistoryChat="selectHistoryChat"
    @scrollTop="scrollTop"
  >
    <!-- 工作卡渲染插槽 -->
    <template #bubble-form="{ item }">
      <hi-page-template
        v-if="item.pageJson"
        :json="item.pageJson"
        :showAlias="showAlias"
      >
      </hi-page-template>
    </template>
  </hi-chat>
</template>
<script lang="ts" setup>
import { ref, onMounted, computed } from "vue";
import HiChat from "pangea-component/hi-chat";
import useChat from "pangea-component/use-chat";
import HiPageTemplate from "pangea-ui/hi-page-template";

const hiChatRef = ref();
const {
  sendChat,
  stopChat,
  getHisMessages,
  getHistoryRecords,
  messages,
  loading,
  historyMsgLoading,
  historyRecords,
  msgHasMore,
  conversationId,
  queryAgentConfig,
  queryAgentInfo,
  agentInfo,
  agentConfig,
} = useChat({ chatRef: hiChatRef });

// 在盘古3.0项目中showAlias方法即为use-intl中的showAlias
const showAlias = () => {};

const suggestions = computed(() => {
  if (agentConfig.value?.suggested_questions?.length) {
    return agentConfig.value.suggested_questions.map((item) => {
      return {
        subTitle: item,
      };
    });
  }
  return [];
});

onMounted(() => {
  getHistoryRecords(null);
  queryAgentConfig();
  queryAgentInfo();
});

const send = (value) => {
  sendChat(value);
};

const stop = () => {
  stopChat();
};

const newChat = () => {
  messages.value = [];
  msgHasMore.value = false;
  conversationId.value = null;
};

const selectHistoryChat = (record) => {
  conversationId.value = record.id;
  getHisMessages(record.id, null);
};

const scrollTop = () => {
  if (msgHasMore.value) {
    getHisMessages(conversationId.value, messages.value[0].id);
  }
};
</script>
<style lang="less" scoped></style>

use-chat 代码示例

javascript
// 本示例中调用接口为智能体平台接口,实际使用时可根据自己项目组封装接口进行替换
import { reactive, ref, Ref } from "vue";
import { Message, StreamBlock, FileItem } from "../types";
import {
  stopAiChat,
  getAiChatForm,
  getConversations,
  getMessages,
  getAgentParameters,
  getAgentInfo,
} from "@/services/ai-agent"; // 替换为自己的项目接口

async function sendChat(inputMessage: string, files?: FileItem[]) {
    const times: number[] = []; // 思考时间区间
    // 用户输入的聊天消息
    const userMessage: Message = reactive({
      type: "user",
      id: Date.now(),
      content: inputMessage,
      status: "loading",
    });
    if (files && files.length) {
      userMessage.files = files.map((file) => {
        return {
          name: file.name,
          url: file.url!,
          type: file.type,
        };
      });
    }
    messages.value.push(userMessage);
    chatRef?.value?.scrollToBottom?.();
    loading.value = true;
    let pageJsonId = null; // 工作卡表单id
    // 请求接口
    const response = await fetch(
      `/higpt/api/agent/v1/agents/run?user_key=${userKey}&client_key=${clientkey}&app_id=${appId}`,
      {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          user_id: "gaorubin",
          conversation_id: conversationId.value,
          inputs: {},
          query: inputMessage,
          response_mode: "streaming",
          files: files
            ? files.map((i) => ({
                transfer_method: "remote_url",
                type: i.type?.includes("image") ? "image" : "file",
                upload_file_id: "",
                url: i.url,
              }))
            : [],
        }),
      }
    );

    if (!response.ok || !response.body) {
      loading.value = false;
      userMessage.status = "fail";
      console.error("Failed to fetch chat data");
      return;
    }

    userMessage.status = "success";

    // 创建流读取器
    const reader = response.body.getReader();

    // 创建文本解码器
    const decoder = new TextDecoder();

    // 创建一个缓冲区,用于存储读取的数据
    let buffer = "";

    // ai回复消息
    const aiMessage: Message = reactive({
      type: "ai",
      id: Date.now(),
      content: "",
      thougth: "",
      pageJson: null,
      pageCode: undefined,
      status: "loading",
      thinkTime: undefined,
      thinking: false,
    });
    messages.value.push(aiMessage);

    while (true) {
      // read方法持续读取数据流,直到读取完成
      const { done, value } = await reader.read();
      if (done) {
        // 数据读取完成,关闭流读取器并设置加载状态为false, 清空任务id
        loading.value = false;
        taskId.value = "";
        aiMessage.status = "success";
        break;
      }

      buffer += decoder.decode(value, { stream: true });

      const chunks = buffer.split("\n\n");
      buffer = chunks.pop() || "";

      for (const chunk of chunks) {
        try {
          const chunkData = chunk.replace(/^data: /, "");

          if (!chunkData) continue;

          let data: StreamBlock | undefined = undefined;
          try {
            data = JSON.parse(chunkData);
          } catch (error) {
            console.error("Error parsing JSON:", error);
          }

          if (!data) continue;

          if (data.event === "agent_thought" && data.thought) {
            taskId.value = data.task_id as string;
            conversationId.value = data.conversation_id;
          }
          if (data.event === "agent_message" && data.answer) {
            taskId.value = data.task_id as string;
            aiMessage.isThinking = data.is_reasoning;
            if (data.is_reasoning) {
              aiMessage.thougth += data.answer;
              if (!times[0]) {
                times.push(Date.now());
              } else {
                times[1] = Date.now();
              }
            } else {
              aiMessage.content += data.answer;
              if (!aiMessage.thinkTime && times.length && times.length === 2) {
                aiMessage.thinkTime = Math.ceil((times[1] - times[0]) / 1000);
              }
            }
            chatRef?.value?.scrollToBottom?.();
          }
          if (
            data.event === "agent_thought" &&
            data.need_show_form &&
            data.observation
          ) {
            if (data.need_show_form && data.observation) {
              try {
                const cardRes = JSON.parse(data.observation)?.[data.tool!];
                const cardData = JSON.parse(cardRes);
                pageJsonId = cardData?.data;
              } catch (error) {
                console.error("Error parsing JSON:", error);
              }
            }
          }
          if (data.event === "message_end") {
            loading.value = false;
            taskId.value = "";
            aiMessage.status = "success";
          }
        } catch (error) {
          console.error("Error parsing JSON:", error);
        }
      }
    }
    // 查询工作卡json数据
    if (pageJsonId) {
      const res = await queryChatForm(pageJsonId);
      aiMessage.pageJson = res?.pageJson;
      aiMessage.pageCode = res?.pageCode;
      chatRef?.value?.scrollToBottom?.();
    }
  }

  // 停止智能体调用
  async function stopChat() {
    if (!taskId.value) return;
    const response = await stopAiChat({
      userKey: userKey,
      clientkey: clientkey,
      appId: appId,
      userId: "zhangsan",
      taskId: taskId.value,
    });
    if (response?.result === "success") {
      loading.value = false;
      taskId.value = "";
    }
  }

  // 清空聊天
  function clearChat() {
    messages.value = [];
  }

  /**
   * @description: 查询工作卡json数据
   * @param id 工作卡id
   */
  async function queryChatForm(id: string) {
    const response = await getAiChatForm({
      userKey: userKey,
      clientkey: clientkey,
      appId: appId,
      id: id,
    });
    if (response?.data?.page_json) {
      return {
        pageJson: JSON.parse(response?.data?.page_json),
        pageCode: response?.data?.page_code,
      };
    }
  }

  /**
   * @description: 查询历史会话记录数据
   * @param lastId 当前页最后一条记录的ID,第一页为null
   */
  async function getHistoryRecords(lastId: string | null) {
    const response = await getConversations({
      user_key: userKey,
      client_key: clientkey,
      app_id: appId,
      user_id: "zhangsan",
      limit: 5,
      last_id: lastId,
    });
    if (Array.isArray(response?.data)) {
      historyRecords.value = historyRecords.value.concat(response?.data);
    }
  }

  /**
   * @description: 查询会话消息数据
   * @param id 会话ID
   * @param firstId 当前页第一条记录的ID,第一页为null
   */
  async function getHisMessages(id, firstId) {
    const msgs: Message[] = [];
    historyMsgLoading.value = true;
    const response = await getMessages({
      user_key: userKey,
      client_key: clientkey,
      app_id: appId,
      user_id: "zhangsan",
      limit: 5,
      conversation_id: id,
      first_id: firstId,
    });
    historyMsgLoading.value = false;
    msgHasMore.value = response?.has_more;
    if (Array.isArray(response?.data)) {
      response.data.forEach((item) => {
        msgs.push({
          type: "user",
          id: item.id,
          content: item.query || "",
        });
        msgs.push({
          type: "ai",
          id: item.id,
          content: item.answer || "",
        });
      });
      if (firstId) {
        messages.value = msgs.concat(messages.value || []);
      } else {
        messages.value = msgs;
      }
      if (!firstId) {
        setTimeout(() => {
          chatRef?.value?.scrollToBottom?.();
        }, 10);
      }
    }
  }

  // 查询智能体基本信息
  async function queryAgentInfo() {
    const res = await getAgentInfo({
      user_key: userKey,
      client_key: clientkey,
      app_id: appId,
    });
    agentInfo.value = res;
  }

  // 查询智能体配置信息
  async function queryAgentConfig() {
    const res = await getAgentParameters({
      user_key: "xxx",
      client_key: "yyy",
      app_id: "zzz",
    });
    agentConfig.value = res;
  }

  return {
    sendChat,
    stopChat,
    clearChat,
    getHistoryRecords,
    messages,
    loading,
    conversationId,
    historyRecords,
    getHisMessages,
    historyMsgLoading,
    msgHasMore,
    agentInfo,
    agentConfig,
    queryAgentInfo,
    queryAgentConfig,
  };
}