前端ai对话框架semi-design-vue

对于前端使用ai框架探索

semi-design-vue

实现功能 -

sse格式输出

接收指定命令处理

思考过程可折叠 - 适配deepseek等模型

清除上下文

请求错误返回

fetch请求封装

这是一个组件,可以引入vue3项目的任何一个页面里

jsx 复制代码
import { Chat, MarkdownRender, Spin, Toast, Avatar, AvatarGroup, Tooltip, Space,Collapse } from '@kousum/semi-ui-vue';
import { defineComponent, ref, onMounted } from 'vue';
import { IconChevronUp } from '@kousum/semi-icons-vue';
import { getNewAgentSessionApi, sendMessageApi } from "../api/baseinfo";
import http from '../config/httpConfig';
import EventStreamRequest from '../config/httpFetch';
import { baseUrl } from '../config/baseUrl';

// 请求成功
const successMessage = {
  role: 'assistant',
  id: '1',
  createAt: 1715676751920,
  content: "请求成功"
};
// 等待中
const wattingMessage = {
  id: 'loading',
  role: 'assistant',
  status: 'loading'
};
// 请求失败
const errorMessage = {
  role: 'assistant',
  id: 'error',
  content: '请求错误',
  status: 'error'
};
const defaultMessage = [
  {
    role: 'assistant',
    id: '1',
    createAt: 1715676751919,
    content: ASSISTANT
  }
]
const roleInfo = ROLE_INFO;
const commonOuterStyle = {
  border: '1px solid var(--semi-color-border)',
  borderRadius: '16px',
  minHeight: '100%',
  height: '100%',
  margin: '0 auto',
  width: '100%',
  boxSizing: 'border-box'
};
let id = 0;
function getId() {
  return `id-${id++}`;
}
// 上传文件地址
const uploadProps = {
  action: 'https://api.semi.design/upload'
};

let post_message = ref('');// 指令输出结果
let post_switch = ref(true);//是指令输出还是问答输出
let post_think = ref(false); // 是否有思考过程


const CustomRender = defineComponent(() => {
  const sessionId = localStorage.getItem('chatSessionId');
  const intervalId = ref();
  const message = ref(defaultMessage);
  const onChatsChange = (chats) => {
    message.value = (chats);
  };
  const onMessageSend = async (content, attachment) => {
    message.value = [
      ...message.value,
      {
        role: 'assistant',
        status: 'loading',
        createAt: Date.now(),
        id: getId()
      }
    ];
    let data = {
      sessionId: sessionId,
      question: content
    };
    const form = new FormData();
    const eventStream = new EventStreamRequest(baseUrl + 'llm/chatStream', {
      data, onEvent: (eventData) => {
        if (eventData.indexOf("is running...") === -1) {
          // 判断是指令输出还是正常问答输出
          if (eventData.length >= 12) {
            // 预检查
            const regex = /^data:\{\"code\"/;
            const flag = regex.test(eventData);
            if (flag) {
              post_switch.value = true;
              // 指令输出
              let msg = eventData.slice(5);
              // let json = JSON.stringify(data);
              post_message.value = msg;
              
              const newAssistantMessage = {
                role: 'assistant',
                id: getId(),
                createAt: Date.now(),
                content: '问题检索完成',
              }
              message.value = [...message.value.slice(0, -1), newAssistantMessage]
            } else {
              const regexEnd = /^data:\[\{\{END\}\}\]/;
              const flagEnd = regexEnd.test(eventData);
              if (flagEnd) {
                if (post_switch.value){
                  // 指令抛出
                  window.parent.postMessage(post_message.value, '*');
                }else{
                  // 问答结束
                }
              }else{
                post_switch.value = false;
                post_message.value = "";
                // 问答输出
                // 空格换成 &sp;; ,换行换成&nl;;
                // 如果有思考过程 - 截取思考过程
                if (eventData.indexOf('<think>') > -1 && eventData.indexOf('</think>') === -1){
                  post_think.value = true;
                  let msgStr = eventData.replace(/&sp;;/g, ' ').replace(/&nl;;/g, '\n')
                  let msg = msgStr.slice(12);
                  if (msg.indexOf('</think>')> -1){
                    // 思考结束
                    let resultStr = msg.slice(msg.indexOf(0,'</think>'));
                    let msgStr = resultStr.replace(/&sp;;/g, ' ').replace(/&nl;;/g, '\n');
                    resultThinkResponse(msgStr, msgStr)
                  }else{
                    // 思考进行中
                    let resultStr = msg.slice(msg.indexOf('<think>') + 1);
                    let msgStr = resultStr.replace(/&sp;;/g, ' ').replace(/&nl;;/g, '\n');
                    resultThinkResponse(msgStr, msgStr)
                  }
                  // let msgStr = eventData.replace(/&sp;;/g, ' ').replace(/&nl;;/g, '<br/>').replace(/<think>/g,'');
                  // let msg = msgStr.slice(5);
                  // generateMockResponse(msg);
                } else if (eventData.indexOf('<think>') > -1 && eventData.indexOf('</think>') > -1){
                  // 思考过程之后的回答结果
                  let msgStr = eventData.replace(/&sp;;/g, ' ').replace(/&nl;;/g, '<br />');
                  let resultStr = msgStr.slice(msgStr.indexOf('</think>') + 8);
                  let thinkStr = msgStr.slice(12,msgStr.indexOf('</think>'));
                  // let msg = msgStr.slice(5);
                  // post_think.value = msg;
                  // resultThinkResponse(msg)
                  console.log(msgStr);
                  
                  resultThinkResponse(thinkStr, resultStr);
                }else{
                  post_think.value = false;
                  // 无思考过程返回值
                  let msgStr = eventData.replace(/&sp;;/g, ' ').replace(/&nl;;/g, '<br/>').replace(/<think>/g,'');
                  let msg = msgStr.slice(5);
                  generateMockResponse(msg);
                }
                
              }
            }
          }else{
            console.log(eventData);
          }
        }
      },onError:(error)=>{
        const newAssistantMessage = {
          role: 'assistant',
          id: getId(),
          createAt: Date.now(),
          status: 'error',
          content: ERROR_TEXT,
        }
        message.value = [...message.value.slice(0, -1), newAssistantMessage]
      }
    });
    eventStream.start();
  };
  // 输出think结果
  const resultThinkResponse = (think,content) => {
    let newMessage = {
      role: 'think',
      id: getId(),
      createAt: Date.now(),
      content: content,
      think: think,
      post_think:true,
    };
    message.value = [...message.value.slice(0, -1), newMessage];
    intervalId.current = id;
  };
  // 输出结果
  const generateMockResponse = (content) => {
    const lastMessage = message.value[message.value.length - 1];
    // console.log(content);
    let newMessage = {
      role: 'assistant',
      id: getId(),
      createAt: Date.now(),
      content: content,
    };
    // console.log(lastMessage);

    message.value = [...message.value.slice(0, -1), newMessage];
    intervalId.current = id;
  };
  // 清除上下文
  const clearContext = () => {
    getNewAgentSessionApi().then((result) => {
      localStorage.setItem('chatSessionId', result);
    }).catch((err) => {
      console.log(err);
    });
  };
  // 重新提问
  const onMessageReset = (msg) => {
    generateMockResponse(msg.content);
  };
  // 停止生成
  const onStopGenerator = (msg) => {
    console.log(msg);
    http.cancelRequest();
    Toast.success('已取消');
    const cancel = {
      role: 'assistant',
      id: 'cancel',
      content: '已取消',
      createAt: 1715676751920,
    }
    setTimeout(() => {
      message.value = [...message.value.slice(0, -1), cancel]
    }, 500)
  }
  // 助手和用户对话背景色
  const renderByRole = ({ role, status }) => {
    if (status === 'error'){
      return { backgroundColor: ERROR_BG_COLOR }//错误消息背景色
    }
    return role === 'assistant'
      ? { backgroundColor: ASSISTANT_BG_COLOR } // 助理消息背景色
      : { backgroundColor: USER_BG_COLOR }; // 用户消息背景色
  }
  // 对话渲染
  const renderContent = (props) => {
    const { role, message, defaultNode, className } = props;
    console.log(message.role, post_think.value);
    if (message.content) {
      return <div class={className} style={renderByRole(message)}>
        {message.post_think && message.post_think===true ?(
          <Collapse expandIconPosition="left">
            <Collapse.Panel header="思考" showArrow={true} itemKey={message.id}>
              <MarkdownRender raw={`<myThink>${message.think}</myThink>`} components={components} />
            </Collapse.Panel>
          </Collapse>
        ):''}
        <MarkdownRender raw={message.content} />
      </div>
    } else {
      return <div class={className}>
        <Spin />
      </div>
    }
  };
  const components = () => {
    const components = {};
    components['myThink'] = ({ children, onClick }) => {
      return <p style={{ marginBottom: "12px" }}> {children} </p>
    }
  }
  const handleBefore = (file)=>{
    console.log(file);

    return
  }
  onMounted(async () => {
    try {
      const result = await getNewAgentSessionApi();
      message.value = defaultMessage;
      localStorage.setItem('chatSessionId', result);
    } catch (err) {
      message.value = [errorMessage];
    }
  });
  return () => (
    <Chat
      style={commonOuterStyle}
      chats={message.value}
      roleConfig={roleInfo}
      chatBoxRenderConfig={{ renderChatBoxContent: renderContent }}
      onChatsChange={onChatsChange}
      onMessageSend={onMessageSend}
      onStopGenerator={onStopGenerator}
      showClearContext={true}
      onClear={clearContext}
      onMessageReset={onMessageReset}
      uploadProps={{ uploadProps: uploadProps, disabled:true }}
      uploadTipProps={{ content :'上传功能开发中...'} }
    />
  );
})
export default CustomRender;

可修改配置文件 - 我是定义在全局中的

js 复制代码
// 基础配置
const ROLE_INFO = {
  // 用户头像 - 名称
  user: {
    name: 'User',
    uuid:'user',
    avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png'
  },
  // 智能助手头像 - 名称
  assistant: {
    name: '智能助手',
    uuid:'assistant',
    avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png'
  },
  // 暂时不用管
  system: {
    name: '智能',
    uuid:'system',
    avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png'
  }
};
// 助手招呼用语
const ASSISTANT = "我是通用智能助手,请问有什么可以帮助您的?";
// 助手消息背景色
const ASSISTANT_BG_COLOR = '#ccf0ff';
// 用户消息背景色
const USER_BG_COLOR = '#10a2e0';
// 报错消息背景色
const ERROR_BG_COLOR = '#ff3f33';
// 报错消息提示语
const ERROR_TEXT = '请求错误';

fetch请求封装

js 复制代码
export default class EventStreamRequest {
  constructor(url, options = {}) {
    this.url = url;
    this.options = options;
    this.controller = new AbortController();
    this.signal = this.controller.signal;
    this.isListening = false; // 新增的状态标志
    this.retryInterval = this.options.retryInterval || 3000; // 默认重试间隔为3秒
  }
  async start() {
    if (this.isListening) return; // 如果已经在监听,则不再启动新的监听
    this.isListening = true;
    const attemptConnect = async () => {
      try {
        const response = await fetch(this.url, {
          method: 'POST',
          responseType:'text/event-stream; charset=utf-8',
          headers: {
            'Content-Type': 'application/json',
            // ...this.options.headers,
          },
          signal: this.signal,
          body: JSON.stringify(this.options.data)
        });

        if (!response.ok) {
          throw new Error(`Failed to fetch event stream with status ${response.status}`);
        }

        this.processStream(response.body.getReader());
      } catch (error) {
        this.handleError(error);
        // setTimeout(attemptConnect, this.retryInterval); // 错误发生后尝试重新连接
      }
    };

    attemptConnect(); // 尝试连接
  }

  processStream(reader) {
    const decoder = new TextDecoder();
    let buffer = '';

    const processChunk = async ({ done, value }) => {
      if (done) {
        this.isListening = false; // 流结束时更新状态标志
        return;
      }

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

      let index;
      while ((index = buffer.indexOf('\n\n')) !== -1) {
        const eventData = buffer.slice(0, index).trim();
        buffer = buffer.slice(index + 2);
        this.handleEvent(eventData);
      }

      reader.read().then(processChunk);
    };

    reader.read().then(processChunk);
  }

  handleEvent(eventData) {
    // console.log('Received event:', eventData);
    // 可以在这里调用外部传入的处理器
    if (typeof this.options.onEvent === 'function') {
      this.options.onEvent(eventData);
    }
  }

  handleError(error) {
    if (typeof this.options.onError === 'function') {
      this.options.onError(error);
    }
  }

  abort() {
    if (this.isListening) {
      this.controller.abort();
      this.isListening = false;
      console.log('EventStream request aborted');
    }
  }
}

实现效果:

相关推荐
卧式纯绿9 分钟前
每日文献(八)——Part one
人工智能·yolo·目标检测·计算机视觉·目标跟踪·cnn
前端爆冲9 分钟前
项目中无用export的检测方案
前端
巷95516 分钟前
OpenCV图像形态学:原理、操作与应用详解
人工智能·opencv·计算机视觉
热爱编程的小曾37 分钟前
sqli-labs靶场 less 8
前端·数据库·less
深蓝易网1 小时前
为什么制造企业需要用MES管理系统升级改造车间
大数据·运维·人工智能·制造·devops
gongzemin1 小时前
React 和 Vue3 在事件传递的区别
前端·vue.js·react.js
xiangzhihong81 小时前
Amodal3R ,南洋理工推出的 3D 生成模型
人工智能·深度学习·计算机视觉
Apifox1 小时前
如何在 Apifox 中通过 Runner 运行包含云端数据库连接配置的测试场景
前端·后端·ci/cd
狂奔solar1 小时前
diffusion-vas 提升遮挡区域的分割精度
人工智能·深度学习
资源大全免费分享1 小时前
MacOS 的 AI Agent 新星,本地沙盒驱动,解锁 macOS 操作新体验!
人工智能·macos·策略模式