AI对话框实现

请注意,功能正在开发中,代码和注释不全

场景:AI对话框实现,后端调用AI大模型。前端发送请求后端返回流式数据,进行一问一答的对话功能(场景和现在市面上多个AI模型差不多,但是没人家功能健全)。

1、功能还没完全实现,显示部分代码。

2、弹框型式,dialog简单的手动封装。

3、本场景请求头需要传入信息,引用的第三方。

参考文档:使用服务器发送事件 - Web API | MDN

一、解决方案

1、常规调用方法

const evtSource = new EventSource("//api.example.com/ssedemo.php", {
  withCredentials: true,
});

2、安装第三方插件

      【注】由于该发送请求接口需要传header信息,原生的不支持。

npm install @microsoft/fetch-event-source

3、使用方法

import { fetchEventSource } from '@microsoft/fetch-event-source';

const handleSend = async () => {
  // ... 之前的用户消息处理逻辑 ...

  try {
    const params = {
      conversationId: '',
      query: userMessage,
    };

    await fetchEventSource(`${prefixPath}/inoAi/chatMessages`, {
      method: 'POST', // 支持 POST
      headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer your_token', // 自定义请求头
      },
      body: JSON.stringify(params), // 请求体
      onopen: async (response) => {
        if (response.ok) return;
        throw new Error(`Server error: ${response.status}`);
      },
      onmessage: (event) => {
        try {
          const data = JSON.parse(event.data);
          if (data.event === 'node_finished') {
            // 更新 AI 消息内容
            setMessages(prev => /* ... */);
          }
        } catch (e) {
          console.error('解析失败:', e);
        }
      },
      onclose: () => {
        // 流式结束处理
        setMessages(prev => /* ... */);
      },
      onerror: (err) => {
        throw err; // 错误处理
      },
    });
  } catch (error) {
    console.error('请求失败:', error);
    // 错误状态更新
  }
};

二、代码实现

相关代码及注释请看下面。

1、页面代码

import React, { useEffect, useRef, useState } from 'react';
import './index.less';
import { Form, Icon, TextArea, useDataSet, Button } from 'choerodon-ui/pro';
import { message } from 'choerodon-ui';
import { LabelLayout } from 'choerodon-ui/pro/lib/form/enum';
import { FieldType } from 'choerodon-ui/dataset/data-set/enum';
import { postChatMessages, postSuggested, postTaskStop } from './api';
import { ChatMessageParams } from '../interface';
import { prefixPath } from '@/api/config';
import { getEnvConfig } from 'utils/iocUtils';
import { getAccessToken } from 'utils/utils';
import { fetchEventSource } from '@microsoft/fetch-event-source';
import { CopyToClipboard } from 'react-copy-to-clipboard';
import { processMarkdown } from './store';
const Dialog = ({
  isOpen,
  title,
  content,
  confirmText = '确定',
  cancelText = '取消',
  onConfirm,
  onCancel,
}) => {
  const { API_HOST } = getEnvConfig();
  const token = getAccessToken();

  const [visible, setVisible] = useState(isOpen); // 控制弹框显示状态
  const [messages, setMessages] = useState<any>([]); // 存储消息列表
  const [inputValue, setInputValue] = useState('');
  const [suggestedQuestions, setSuggestedQuestions] = useState<string[]>([]); // 存储建议问题列表
  const [showSuggestedQuestions, setShowSuggestedQuestions] = useState(false); // 控制是否显示建议问题列表
  const abortControllerRef = useRef<AbortController | null>(null); // 用于中断流式请求

  // ds
  const [, setUpdateDs] = useState(new Date().getTime());
  const formDataDs = useDataSet(() => {
    return {
      autoCreate: true,
      fields: [
        {
          name: 'ask',
          type: FieldType.string,
          label: '输入框',
          placeholder: '给AI发送消息',
        },
      ],
      events: {
        update: () => {
          setUpdateDs(new Date().getTime());
        },
      },
    };
  }, []);

  /** 添加自动滚动效果 */
  const messagesEndRef = useRef<HTMLDivElement>(null);
  useEffect(() => {
    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
  }, [messages]);

  /** 获取建议问题列表 */
  const fetchSuggestedQuestionsList = async (messageId: string) => {
    // console.log('获取建议问题列表', messageId);
    try {
      const res = await postSuggested({ messageId });
      // console.log('获取建议问题列表成功:', res);
      setSuggestedQuestions(res.data || []); // 更新建议问题列表
      setShowSuggestedQuestions(true); // 显示建议问题列表
    } catch (error) {
      // console.error('获取建议问题列表失败:', error);
    }
  };

  /** 发送信息 */
  const handleSend = async () => {
    console.log('1、click:发送请求', formDataDs.current?.get('ask'));
    const userMessage = formDataDs.current?.get('ask');
    if (!userMessage) return;

    // 1、添加用户消息
    setMessages(prev => [
      ...prev,
      {
        type: 'user',
        content: userMessage,
        id: Date.now().toString(),
      },
    ]);

    // 2、添加初始AI消息占位
    const aiMessageId = Date.now().toString() + '-ai';
    setMessages(prev => [
      ...prev,
      {
        type: 'ai',
        content: '', // 初始为空内容
        id: aiMessageId,
        status: 'streaming', // 添加状态标识
        isCompleted: false, // 标记消息是否完成
        conversation_id: '', // 预留字段
        message_id: '', // 预留字段
        created_at: 0, // 预留字段
        task_id: '', // 预留字段
      },
    ]);
    formDataDs.current?.set('ask', '');

    // 3、发送请求的参数
    const params = {
      conversationId: '',
      query: userMessage,
    };
    console.log('2、发送请求的参数:', params);

    // 4、初始化 AbortController
    abortControllerRef.current = new AbortController();

    // 5、调用接口
    await fetchEventSource(`${API_HOST}${prefixPath}/inoAi/chatMessages`, {
      method: 'POST',
      body: JSON.stringify(params),
      headers: {
        'Content-type': 'application/json',
        Authorization: token,
      },
      signal: abortControllerRef.current.signal, // 绑定中断信号
      onopen: async response => {
        if (!response.ok) {
          message.error(`服务错误: ${response.status}`, 1.5, 'top');
          return;
        }
      },
      onmessage: event => {
        try {
          let content = '';

          // 检查 event.data 是否是字符串
          if (typeof event.data === 'string') {
            // 检查是否是 SSE 格式的数据
            if (event.data.startsWith('event:')) {
              // 解析 SSE 格式的数据
              const lines = event.data.split('\n');
              const eventData: any = {};
              lines.forEach(line => {
                if (line.startsWith('event:')) {
                  eventData.event = line.replace(/^event:/, '').trim();
                } else if (line.startsWith('data:')) {
                  eventData.data = line.replace(/^data:/, '').trim();
                }
              });

              // 如果是心跳消息,直接返回
              if (eventData.event === 'ping') {
                console.log('心跳消息,忽略');
                return;
              }

              // 如果是其他事件,解析 data 字段
              if (eventData.data) {
                const data = JSON.parse(eventData.data);

                // 提取需要显示的内容(根据实际数据结构调整)
                if (data?.event === 'node_finished') {
                  content = data.data?.outputs?.sys?.query || '';
                }

                // 确保在 message_end 事件中更新状态为 completed
                if (data?.event === 'message_end') {
                  setMessages(prev =>
                    prev.map(msg => {
                      if (msg.id === aiMessageId) {
                        return {
                          ...msg,
                          conversation_id: data.conversation_id,
                          message_id: data.message_id,
                          created_at: data.created_at,
                          task_id: data.task_id,
                          id: data.id,
                          status: 'completed', // 明确更新状态为 completed
                          isCompleted: true,
                        };
                      }
                      return msg;
                    }),
                  );

                  // AI回答完成后,调用接口获取建议问题列表
                  fetchSuggestedQuestionsList(data.message_id);
                  return; // 确保不再执行后续逻辑
                }
              }
            } else {
              // 如果不是 SSE 格式的数据,直接解析为 JSON
              const data = JSON.parse(event.data);
              // console.log('33、接口返回的参数', data);

              // 提取需要显示的内容
              if (data?.event === 'message') {
                console.log('44', data?.event);
                content = data?.answer;
              }

              // 确保在 message_end 事件中更新状态为 completed
              if (data?.event === 'message_end') {
                setMessages(prev =>
                  prev.map(msg => {
                    if (msg.id === aiMessageId) {
                      return {
                        ...msg,
                        conversation_id: data.conversation_id,
                        message_id: data.message_id,
                        created_at: data.created_at,
                        task_id: data.task_id,
                        id: data.id,
                        status: 'completed', // 明确更新状态为 completed
                        isCompleted: true,
                      };
                    }
                    return msg;
                  }),
                );

                // AI回答完成后,调用接口获取建议问题列表
                fetchSuggestedQuestionsList(data.message_id);
                return; // 确保不再执行后续逻辑
              }
            }
          }

          // console.log('karla', content);

          // 更新AI消息
          if (content) {
            setMessages(prev =>
              prev.map(msg =>
                msg.id === aiMessageId
                  ? { ...msg, content: msg.content + content }
                  : msg,
              ),
            );
          }
        } catch (e) {
          console.error('数据解析失败:', e);
        }
      },
      onclose: () => {
        // 确保在流式传输结束时更新状态为 completed
        setMessages(prev =>
          prev.map(msg => {
            if (msg.id === aiMessageId && msg.status !== 'completed') {
              return {
                ...msg,
                status: 'completed', // 兜底逻辑,确保状态更新
                isCompleted: true,
              };
            }
            return msg;
          }),
        );
      },
      onerror: err => {
        console.error('流式传输错误:', err);
        setMessages(prev =>
          prev.map(msg => {
            if (msg.id === aiMessageId) {
              return { ...msg, content: '请求异常中断', status: 'error' };
            }
            return msg;
          }),
        );
      },
    });
  };

  const handleRetry = (question: string) => {
    // 清空旧消息
    setMessages(prev =>
      prev.filter(
        msg => msg.originalQuestion !== question || msg.type === 'user',
      ),
    );

    // 自动填充输入框
    formDataDs.current?.set('ask', question);

    // 延迟触发发送(等待状态更新)
    setTimeout(() => {
      handleSend();
    }, 100);
  };

  /** 停止输出 */
  const handleStop = async (val: any) => {
    console.log('click:停止输入', val);

    // 1、请求'中断'
    const res = await postTaskStop({ taskId: val.task_id });
    console.log('中断接口返回:', res);

    // 2、中断流式传输
    if (abortControllerRef.current) {
      abortControllerRef.current.abort();
      abortControllerRef.current = null;
    }

    // 3、更新消息状态
    setMessages(prev =>
      prev.map(msg => {
        if (msg.task_id === val.task_id) {
          return {
            ...msg,
            status: 'stopped', // 标记为已停止
            content: msg.content + '\n\n(已经停止请求)', // 添加提示
          };
        }
        return msg;
      }),
    );
  };

  /** 用户选择建议问题 */
  const handleSelectQuestion = (question: string) => {
    formDataDs.current?.set('ask', question); // 填充到输入框
    setSuggestedQuestions([]); // 清除建议问题列表
    setShowSuggestedQuestions(false); // 隐藏建议问题列表
    handleSend(); // 发送问题
  };

  /** 如果弹框不显示,则不渲染任何内容 */
  if (!visible) return null;

  /** 模块:输入框和发送按钮 */
  const Footer = () => {
    return (
      <>
        <div
          className="chat-dialog__chat-editor"
          style={{ width: messages.length > 0 ? '100%' : '60%' }}
        >
          <div className="chat-dialog__chat-editor__box">
            {/* 输入框 */}
            <div className="chat-dialog__chat-editor__box__input">
              <Form dataSet={formDataDs} labelLayout={LabelLayout.none}>
                <TextArea
                  name="ask"
                  // valueChangeAction={ValueChangeAction.input}
                  autoSize={{ minRows: 2, maxRows: 6 }}
                  // onEnterDown={(event: any) => {
                  //   if (event.key === 'Enter') {
                  //     handleSend();
                  //   }
                  // }}
                />
              </Form>
            </div>

            {/* 发送按钮 */}
            <div className="chat-dialog__chat-editor__box__action">
              <div
                className="chat-dialog__chat-editor__box__action__btn"
                style={{
                  cursor: formDataDs.current?.get('ask')
                    ? 'pointer'
                    : 'not-allowed',
                  backgroundColor: formDataDs.current?.get('ask')
                    ? '#0099F2'
                    : 'rgba(0, 0, 0, 0.04)',
                }}
                onClick={() => handleSend()}
              >
                <img
                  src={
                    formDataDs.current?.get('ask')
                      ? require('@/components/ContactUs/chat/img/btn_active.png')
                      : require('@/components/ContactUs/chat/img/btn.png')
                  }
                />
              </div>
            </div>
          </div>
        </div>
      </>
    );
  };

  return (
    <div className="dialog-overlay">
      <div className="chat-dialog">
        {/* Header */}
        <div className="chat-dialog__header">
          <div className="chat-dialog__header__title">AI对话框</div>
          <div
            className="chat-dialog__header__close"
            onClick={() => setVisible(false)}
          >
            <Icon type="close" style={{ fontSize: 16 }} />
          </div>
        </div>

        {/* 场景一:打开时 */}
        {/* {messages.length == 0 && (
          <>
            <div className="chat-dialog__content">
              <div className="chat-dialog__content__home">
                <div className="chat-dialog__content__home__banner">
                  <img
                    src={require('@/components/ContactUs/chat/img/logo.png')}
                  />
                </div>
              </div>

              <Footer />
            </div>
          </>
        )} */}

        {/* 场景二:发送请求后  */}
        {/* {messages.length > 0 && (
          <> */}
        <div className="chat-dialog__wapper">
          {/* 消息框 */}
          <div className="chat-dialog__wapper__messages">
            {messages.map(item => (
              <div
                key={item.id}
                className={`chat-dialog__wapper__messages__message ${item.type}`}
              >
                {/* 头像 */}
                <div className="message-avatar">
                  <img
                    src={
                      item.type === 'ai'
                        ? require('@/components/ContactUs/chat/img/ai-avatar.png')
                        : require('@/components/ContactUs/chat/img/user-avatar.png')
                    }
                    alt={item.type === 'ai' ? 'AI Avatar' : 'User Avatar'}
                  />
                </div>

                {/* 消息列 */}
                <div className="message-content">
                  {/* 消息气泡 */}
                  <div
                    className="message-bubble"
                    onClick={() => {
                      if (item.status === 'error' && item.type === 'ai') {
                        handleRetry(item.originalQuestion);
                      }
                    }}
                  >
                    {item.type == 'user' ? (
                      <>{item.content}</>
                    ) : (
                      <>
                        <div className="markdown-content">
                          <div
                            dangerouslySetInnerHTML={{
                              __html: processMarkdown(item.content || ''),
                            }}
                          />
                        </div>
                      </>
                    )}

                    {/* loading */}
                    {item.status === 'streaming' && (
                      <span className="streaming-indicator"></span>
                    )}
                  </div>

                  {item.type === 'ai' && (
                    <>
                      {/* 操作栏按钮:复制按钮(流式传输中只显示内容,传输完成显示按钮) */}
                      {item.status === 'completed' && (
                        <div className="message-operation">
                          <CopyToClipboard text={item.content}>
                            <div
                              className="copy-button"
                              onClick={() => {
                                message.success(
                                  '复制成功',
                                  undefined,
                                  undefined,
                                  'top',
                                );
                              }}
                            >
                              <Icon type="content_copy" className="icon_copy" />
                              复制
                            </div>
                          </CopyToClipboard>
                        </div>
                      )}

                      {/* 停止输出  */}
                      {item.status === 'streaming' && (
                        <>
                          <div className="message-stop">
                            <div
                              className="message-stop__btn"
                              onClick={() => handleStop(item)}
                            >
                              停止输出
                            </div>
                          </div>
                        </>
                      )}

                      {/* 停止提示 */}
                      {item.status === 'stopped' && (
                        <div className="message-stopped-tip">(用户停止)</div>
                      )}

                      {/* 错误提示 */}
                      {item.status === 'error' && (
                        <div className="message-error-tip">(点击重试)</div>
                      )}

                      {/* 建议问题列表 */}
                      {showSuggestedQuestions && suggestedQuestions.length > 0 && (
                        <div className="suggested-questions">
                          {suggestedQuestions.map((question, index) => (
                            <div
                              key={index}
                              className="suggested-question"
                              onClick={() => handleSelectQuestion(question)}
                            >
                              {question}
                              <Icon
                                type="navigate_next"
                                className="icon_question"
                              />
                            </div>
                          ))}
                        </div>
                      )}
                    </>
                  )}
                </div>
              </div>
            ))}
            <div ref={messagesEndRef} />
          </div>

          <Footer />
        </div>
        {/* </>
        )} */}
      </div>
    </div>
  );
};

export default Dialog;

2、css样式

.dialog-overlay {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(0, 0, 0, 0.5);
  display: flex;
  justify-content: center;
  align-items: center;
  z-index: 1000;
  font-size: 14px;

  :global {
    // 边框去除
    .c7n-pro-textarea-wrapper.c7n-pro-textarea-wrapper label .c7n-pro-textarea {
      border: 1px solid transparent !important;
    }
    // 阴影去除
    .c7n-pro-textarea-wrapper.c7n-pro-textarea-wrapper.c7n-pro-textarea-focused
      .c7n-pro-textarea {
      box-shadow: none;
    }
    .c7n-pro-textarea-wrapper.c7n-pro-textarea-wrapper
      label
      .c7n-pro-textarea.c7n-pro-textarea {
      padding: 3px;
    }
  }
}

/* Markdown 表格样式 */
.markdown-content {
  p {
    margin: 0;
    padding: 0;
  }
  table {
    width: 100%;
    border-collapse: collapse;
    margin: 20px 0;
    font-family: Arial, sans-serif;
    font-size: 12px;
    color: #333;
    border: 1px solid #ddd;
    border-radius: 8px;
    overflow: hidden;
    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
  }

  th,
  td {
    padding: 4px 8px;
    text-align: left;
    border-bottom: 1px solid #ddd;
    border-right: 1px solid #ddd;
  }

  th {
    background-color: #f2f2f2;
    font-weight: bold;
    color: #2c3e50;
  }

  td {
    background-color: #fff;
  }

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

  tr:hover {
    background-color: #f1f1f1;
  }

  th:last-child,
  td:last-child {
    border-right: none;
  }

  /* 针对第一列的样式 */
  th:first-child,
  td:first-child {
    white-space: nowrap; /* 防止内容换行 */
    // overflow: hidden; /* 隐藏溢出的内容 */
    // text-overflow: ellipsis; /* 显示省略号 */
    max-width: 200px; /* 设置最大宽度 */
  }
}

/** dialog */
.chat-dialog {
  background: #f3f5fa;
  border-radius: 8px;
  padding: 0 16px 16px 16px;
  width: 1000px;
  box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);

  /** Header */
  &__header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    width: 100%;
    height: 40px;
    // background: pink;
    border-bottom: 1px solid #c6cfd8;

    &__title {
      color: #222222;
      font-size: 16px;
      font-weight: bold;
    }
    &__close {
      display: flex;
      align-items: center;
      cursor: pointer;
    }
  }

  /** 输框和发送按钮 */
  &__chat-editor {
    width: 100%;
    // background: white;
    padding-top: 12px;
    &__box {
      width: 100%;
      border-radius: 12px;
      border: 1px solid #c6cfd8;
      background: white;
      &__input {
        width: 100%;
      }
      &__action {
        display: flex;
        justify-content: end;
        padding: 0 12px 8px 8px;
        &__btn {
          width: 28px;
          height: 28px;
          border-radius: 8px;
          background: rgba(0, 0, 0, 0.04);
          display: flex;
          align-items: center;
          justify-content: center;
          img {
            width: 80%;
            height: 80%;
          }
        }
      }
    }
  }

  /** 场景一:打开时 */
  &__content {
    height: 500px;
    overflow: auto;
    border: 1px solid #c6cfd8;
    margin: 12px 0;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-direction: column;
    &__home {
      width: 100%;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      &__banner {
        width: 191px;
        height: 68px;
        img {
          width: 100%;
          height: 100%;
        }
      }
    }
  }

  /** 场景二:发送请求后 */
  &__wapper {
    display: flex;
    flex-direction: column;
    height: 500px;
    overflow: hidden;
    border: 1px solid #c6cfd8;
    margin: 12px 0;

    &__messages {
      flex: 1;
      overflow-y: auto;
      // padding: 16px;
      display: flex;
      flex-direction: column;
      gap: 12px;

      &__message {
        max-width: 70%;
        display: flex; // 使用 flex 布局
        align-items: flex-start; // 确保头像和内容顶部对齐

        // 头像
        .message-avatar {
          width: 40px;
          height: 40px;
          border-radius: 50%;
          flex-shrink: 0; // 防止头像被压缩
          img {
            width: 100%;
            height: 100%;
            object-fit: cover; // 确保图片填充容器
          }
        }

        // 消息内容
        .message-content {
          flex: 1; // 内容占据剩余空间
          background: transparent; // 内容模块的背景色
        }

        // 用户消息(靠右)
        &.user {
          align-self: flex-end;
          flex-direction: row-reverse; // 头像在右侧

          .message-content {
            .message-bubble {
              background: #0099f2; // 用户消息背景色
              color: white;
              border-radius: 12px 12px 0 12px;
              padding: 8px 12px;
            }
          }

          .message-avatar {
            margin-left: 8px; // 头像与内容的间距
          }
        }

        // AI 消息(靠左)
        &.ai {
          align-self: flex-start;
          flex-direction: row; // 头像在左侧

          .message-content {
            .message-bubble {
              background: #fff; // AI 消息背景色
              // border: 1px solid #c6cfd8;
              // border-radius: 12px 12px 12px 0;
              border-radius: 12px 12px 0 0;
              padding: 8px 12px;
            }
          }

          .message-avatar {
            margin-right: 8px; // 头像与内容的间距
          }

          .message-bubble {
            position: relative;
            min-height: 20px; // 保持最小高度
          }

          /** 方案 3:旋转的加载图标 */
          .streaming-indicator {
            display: inline-block;
            margin-left: 8px;
            width: 16px;
            height: 16px;
            border: 2px solid #0099f2;
            border-top-color: transparent;
            border-radius: 50%;
            animation: spin 1s linear infinite;
          }

          @keyframes spin {
            0% {
              transform: rotate(0deg);
            }
            100% {
              transform: rotate(360deg);
            }
          }

          /** 方案 1:渐隐渐现的省略号*/
          // .streaming-indicator {
          // display: inline-block;
          // margin-left: 8px;
          // animation: blink 1s infinite;

          // &::after {
          //   content: '...';
          // }
          // }

          // @keyframes blink {
          //   0%,
          //   100% {
          //     opacity: 1;
          //   }
          //   50% {
          //     opacity: 0.3;
          //   }
          // }

          /** 方案 2:打字机效果 */
          // .streaming-indicator {
          //   display: inline-block;
          //   margin-left: 8px;
          //   font-size: 14px;
          //   overflow: hidden;
          //   white-space: nowrap;
          //   animation: typing 1.5s steps(3, end) infinite;
          // }

          // @keyframes typing {
          //   0% {
          //     width: 0;
          //   }
          //   100% {
          //     width: 36px; // 3个字符的宽度
          //   }
          // }
        }

        // 消息气泡
        .message-bubble {
          cursor: default;

          &[data-status='error'] {
            cursor: pointer;
            border: 1px solid #ff4d4f;

            &:hover {
              background: #fff2f0;
            }
          }
        }

        // 操作栏按钮
        .message-operation {
          width: 100%;
          padding: 0 12px 8px;
          background: white;
          display: flex;
          border-radius: 0 0 12px 0;
          font-size: 10px;
          color: #5e6772;
          font-weight: inherit;
          .icon_copy {
            color: #909090;
            font-size: 16px;
            margin-right: 3px;
          }
          .copy-button {
            cursor: pointer;
            padding: 4px;
            border-radius: 4px;
            transition: background-color 0.3s;

            &:hover {
              background-color: #f5f5f5;
            }
          }
        }

        // 停止输出
        .message-stop {
          padding: 8px 0;
          &__btn {
            padding: 0 8px;
            border: 1px solid #fb4242;
            border-radius: 4px;
            color: #fb4242;
            line-height: 25px;
            display: inline-block;
            cursor: pointer;
          }
        }

        .message-stopped-tip {
          color: #b5b5b5;
          font-size: 12px;
          margin-top: 8px;
        }

        // 错误提示
        .message-error-tip {
          color: #ff4d4f;
          font-size: 12px;
          margin-top: 4px;
          cursor: pointer;

          &:hover {
            text-decoration: underline;
          }
        }
      }
    }

    // 建议问题列表容器
    .suggested-questions {
      margin-top: 8px;
      border-radius: 8px;
      color: #060607;
      // 单个建议问题
      .suggested-question {
        display: table;
        margin-bottom: 8px;
        padding: 8px 12px;
        background-color: #ffffff;
        border: 1px solid #e5e5e5;
        border-radius: 10px;
        cursor: pointer;
        transition: background-color 0.3s ease, border-color 0.3s ease;
        // icon_问题箭头
        .icon_question {
          color: #060607;
          font-size: 15px;
          font-weight: bold;
          margin-top: -2px;
        }

        &:hover {
          background-color: #e5e7ed;
          border-color: #e5e7ed;
        }

        &:active {
          background-color: #e5e7ed;
        }
      }
    }
  }
}

3、使用的插件

【注】本来想用react-markdown 的,但是版本遇到问题,react是 16的,安装有问题,就换成了 remark。也有其他插件可以安装,自行选择。

(1)安装的插件emark、remark-gfm、remark-rehype、rehype-stringify

npm install remark remark-gfm remark-rehype rehype-stringify

(2)将 Markdown 转换为 HTML

使用 remark 和 remark-rehype 将 Markdown 转换为 HTML

import { remark } from 'remark';
import remarkGfm from 'remark-gfm';
import rehypeStringify from 'rehype-stringify'; // 这个必加,不然html显示不了

// markdown 处理成html 的方法
const processMarkdown = (markdown: string) => {
   return remark()
     .use(remarkGfm)
     .processSync(markdown)
     .toString();
};


<div
  dangerouslySetInnerHTML={{
    __html: processMarkdown(demo),
  }}
/> 

4、后端返回的数据

data:{"event": "workflow_started", "conversation_id": "9a2fbc29-01e2-4158-b548-1cb8389fbd6f", "message_id": "e48e20bb-62c8-4b2f-b53b-708002767b37", "created_at": 1742402172, "task_id": "7babb429-c95b-4011-b984-e1ee3d7d56d9", "workflow_run_id": "b88d72a1-fd92-4db6-9523-2ab29b9c1a5d", "data": {"id": "b88d72a1-fd92-4db6-9523-2ab29b9c1a5d", "workflow_id": "70c7b2c5-bffd-4648-9e83-d7e935490998", "sequence_number": 379, "inputs": {"sys.query": "\u82cf\u5dde\u6c47\u5ddd\u57fa\u672c\u4fe1\u606f", "sys.files": [], "sys.conversation_id": "9a2fbc29-01e2-4158-b548-1cb8389fbd6f", "sys.user_id": "ltc-plat-portal-wb01790t115886", "sys.app_id": "4d109bac-cc7f-4cfb-84ca-3d8f05c791f2", "sys.workflow_id": "70c7b2c5-bffd-4648-9e83-d7e935490998", "sys.workflow_run_id": "b88d72a1-fd92-4db6-9523-2ab29b9c1a5d"}, "created_at": 1742402172}}

data:{"event": "node_started", "conversation_id": "9a2fbc29-01e2-4158-b548-1cb8389fbd6f", "message_id": "e48e20bb-62c8-4b2f-b53b-708002767b37", "created_at": 1742402172, "task_id": "7babb429-c95b-4011-b984-e1ee3d7d56d9", "workflow_run_id": "b88d72a1-fd92-4db6-9523-2ab29b9c1a5d", "data": {"id": "1dde4fa1-caa1-4a0a-86f7-5ffb5b9e7fb5", "node_id": "1724901426507", "node_type": "start", "title": "\u5f00\u59cb", "index": 1, "predecessor_node_id": null, "inputs": null, "created_at": 1742373372, "extras": {}}}

data:{"event": "node_finished", "conversation_id": "9a2fbc29-01e2-4158-b548-1cb8389fbd6f", "message_id": "e48e20bb-62c8-4b2f-b53b-708002767b37", "created_at": 1742402172, "task_id": "7babb429-c95b-4011-b984-e1ee3d7d56d9", "workflow_run_id": "b88d72a1-fd92-4db6-9523-2ab29b9c1a5d", "data": {"id": "1dde4fa1-caa1-4a0a-86f7-5ffb5b9e7fb5", "node_id": "1724901426507", "node_type": "start", "title": "\u5f00\u59cb", "index": 1, "predecessor_node_id": null, "inputs": {"sys.query": "\u82cf\u5dde\u6c47\u5ddd\u57fa\u672c\u4fe1\u606f", "sys.files": [], "sys.conversation_id": "9a2fbc29-01e2-4158-b548-1cb8389fbd6f", "sys.user_id": "ltc-plat-portal-wb01790t115886", "sys.app_id": "4d109bac-cc7f-4cfb-84ca-3d8f05c791f2", "sys.workflow_id": "70c7b2c5-bffd-4648-9e83-d7e935490998", "sys.workflow_run_id": "b88d72a1-fd92-4db6-9523-2ab29b9c1a5d"}, "process_data": null, "outputs": {"sys.query": "\u82cf\u5dde\u6c47\u5ddd\u57fa\u672c\u4fe1\u606f", "sys.files": [], "sys.conversation_id": "9a2fbc29-01e2-4158-b548-1cb8389fbd6f", "sys.user_id": "ltc-plat-portal-wb01790t115886", "sys.app_id": "4d109bac-cc7f-4cfb-84ca-3d8f05c791f2", "sys.workflow_id": "70c7b2c5-bffd-4648-9e83-d7e935490998", "sys.workflow_run_id": "b88d72a1-fd92-4db6-9523-2ab29b9c1a5d"}, "status": "succeeded", "error": null, "elapsed_time": 0.007522, "execution_metadata": null, "created_at": 1742373372, "finished_at": 1742373372, "files": []}}

data:{"event": "node_started", "conversation_id": "9a2fbc29-01e2-4158-b548-1cb8389fbd6f", "message_id": "e48e20bb-62c8-4b2f-b53b-708002767b37", "created_at": 1742402172, "task_id": "7babb429-c95b-4011-b984-e1ee3d7d56d9", "workflow_run_id": "b88d72a1-fd92-4db6-9523-2ab29b9c1a5d", "data": {"id": "0308fa85-77b2-4ace-b042-d4770a6b371b", "node_id": "1730859408629", "node_type": "http-request", "title": "\u83b7\u53d6paas\u5e73\u53f0\u5ba2\u6237\u7aeftoken", "index": 2, "predecessor_node_id": "1724901426507", "inputs": null, "created_at": 1742373372, "extras": {}}}

(1)markdonw格式

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值