react实现流式输出

如何使用react实现流式调用gpt接口,并打字机输出的效果呢?

经过一周的调研和实现,最后通过结合fetch来调用接口实现。

通过fetch,如果要获取流式数据可以如下处理:

async function getRes(content) {
  const res = await fetch(url, {...});
  const reader = res.body.getReader();
  // 读取数据流的第一块数据,done表示数据流是否完成,value表示当前的数
  const {done, value} = await reader.read();
  // 上面读取到的是数据的字节码,还需要处理字节码为文本
  const decoder = new TextDecoder();
  const text = decoder.decode(value);
  // 打印第一块的文本内容
  console.log(text, done);
}

以上代码指执行了一块数据,还要通过循环获取剩下流式内容:

async function getRes(content) {
  const res = await fetch(url, {...});
  const reader = res.body.getReader();
  const decoder = new TextDecoder();
  let content = '';
  let isThinking = true
  while(isThinking) {
    // 读取数据流的第一块数据,done表示数据流是否完成,value表示当前的数
    const {done, value} = await reader.read();
    if (done) {
        isThinking = false
        break;
    }
    const text = decoder.decode(value);
    // 拼接文本内容
    content = content + text
    console.log(content, done);
  }
}

以上可以实现基本的流式输出

react结合fetch整体可以这么实现

const [history, setHistory] = useState<{ speaker: string; text: string }[]>([
    { speaker: 'bot', text: '我是你的AI助手,有什么问题都可以问我' },
  ]);
const [isThinking, setIsThinking] = useState(false);
const [inputText, setInputText] = useState('');
const [abortController, setAbortController] = useState<AbortController | null>(null);

const handleSubmit = async (question?: string) => {
    if(inputText==='' && question===undefined){
      return
    }
    const controller = new AbortController();
    setAbortController(controller);
    const newHistory = [
      ...history,
      { speaker: 'user', text: question ? question : inputText },
      { speaker: 'bot', text: '' },
    ];
    setHistory(newHistory);
    setIsThinking(true);
    let toBody = {
      'model': 'gpt-3.5-turbo',
      temperature: 0.1,
      stream: true,
      messages: [
        {
          role: 'user',
          content: question ? question : inputText ,
        },
      ],
    };
    setInputText('');
    fetch(url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Authorization: 'Bearer ' + localStorage.getItem('access_token'),
      },
      body: JSON.stringify(toBody),
      signal: controller.signal,
    })
      .then(async (response) => {
        if (response.body!.locked) {
          console.log('流已经被一个读取器锁定。');
          return;
        }
        if (!response.body) throw Error('No response from server');
        const reader = response.body!.getReader();
        const textDecoder = new TextDecoder();
        let result = true;

        while (result) {
          const { done, value } = await reader.read();

          if (done) {
            console.log('Stream ended');
            result = false;
            setIsThinking(false);
            break;
          }

          const chunkText = textDecoder.decode(value);
          let list = chunkText.match(/data: (.+)/g);

          console.log('--list--', list);
          if (list && list.length > 0) {
            list.forEach((element) => {
              let data = element.substring(6);
              if (
                data === '[DONE]' ||
                typeof JSON.parse(data) !== 'object' ||
                (typeof JSON.parse(data) === 'object' &&
                  (JSON.parse(data).choices.length === 0 ||
                    JSON.parse(data).choices[0].delta === undefined))
              ) {
                if (data === '[DONE]') {
                  setIsThinking(false);
                }
                return;
              }
              let content = JSON.parse(data).choices[0].delta.content;
              if (content) {
                setHistory((history) => {
                  let newHistory;
                  // 如果聊天记录最后一条不是机器人,则拼接一条机器人回答对象
                  if (history[history.length - 1].speaker !== 'bot') {
                    newHistory = [...history, { speaker: 'bot', text: content }];
                  } else {
                    // 聊天记录最后一条是机器人,则直接在机器人回答的内容后面拼接新回答
                    history[history.length - 1].text = history[history.length - 1].text + content;
                    // 不能直接history赋值,要加上[... ]生成新对象,否则setState会认为引用地址没变,不执行页面刷新
                    newHistory = [...history];
                  }
                  return newHistory;
                });
              }
            });
          }
          console.log('Received chunk:', chunkText);
        }

      })
      .catch((error) => {
        setIsThinking(false);
        console.error('Error:', error);
        if (error instanceof DOMException && error.name === 'AbortError') {
          console.error('AbortError:', error);
        }
      });
    //这里可以更新state,比如更新history
  };
  // 输入问题回车调用gpt
  const handleKeyPress = (event: any) => {
    if (event.key === 'Enter' && !event.shiftKey) {
      event.preventDefault();
      handleSubmit();
    }
  };
  // 停止调用
  const handleStop = () => {
    setIsThinking(false);
    if (abortController) {
      try {
        abortController.abort({
          type: 'USER_ABORT_ACTION',
          msg: '用户终止了操作',
        });
      } catch (e) {
        console.log(e);
      }
    }
  };
<div className={styles.chatBox}>
     {history.map((item, index) => (
         <div
            key={index}
            className={item.speaker === 'user' ? styles.chatRight : styles.chatLeft}
         >
            {item.speaker === 'bot' && (
              <div style={{ marginBottom: '10px' }}>
                  <img
                      src={require('@/assets/images/ai.png')}
                      className={styles.chatLogo}
                  />
                  AI助手
                 </div>
             )}
             {item.text || !isThinking || index !== history.length - 1 ? (
                  <span style={{ display: 'block' }}>{item.text}</span>
             ) : (
               <Spin />
             )}
          </div>
       ))}
 </div>
{showPrompts && (
     <div className={styles.promptsWrapper}>
         {prompts.map((prompt, index) => (
             <div
                  key={index}
                  className={styles.promptBox}
                  onClick={() => handleSubmit(prompt.question)}
             >
                  <div>{prompt.title}</div>
                  <span>{prompt.subtitle}</span>
             </div>
          ))}
     </div>
 )}
{isThinking ? (
   <div className={styles.stopWrapper} onClick={handleStop}>
        <img src={require('@/assets/images/stop.png')} />
        <span style={{ marginLeft: '10px' }}>
           停止调用
        </span>
   </div>
   ) : (
   <div style={{width: '80%', bottom: '10px', position: 'fixed' }}>
         <div style={{ display: 'flex' }}>
              <TextArea
                  rows={1}
                  value={inputText}
                  onChange={handleInputChange}
                  onKeyPress={handleKeyPress}
              />
              <img
                  src={require('@/assets/images/send.png')}
                  className={styles.chatSend}
                  onClick={() => handleSubmit()}
              />
         </div>
    </div>
 )}         

聊天框样式实现

.chatBox {
  flex: 1;
  height: 100%;
  .chatRight {
    display: flex;
    justify-content: right;
    margin: 10px 0;
    margin-left: 20%;
    > span {
      padding: 15px;
      line-height: 20px;
      background-color: #4eccc0;
      border-radius: 10px;
    }
  }
  .chatLeft {
    display: flex;
    flex-flow: column;
    align-items: flex-start;
    margin-right: 20%;
    > span {
      padding: 15px;
      background-color: #41454F;
      border-radius: 10px;
      line-height: 20px;
    }
  }
}
.chatWrapper {
  display: flex;
  flex-flow: column;
  height: 100%;
  overflow-y: scroll;
  scrollbar-width: thin;
  scrollbar-color: rgba(136, 136, 136, 0.3) transparent;
  color: #fff;
  margin-bottom: 30px;
}
.chatSend {
  align-self: center;
  height: 20px;
  padding-left: 5px;
  cursor: pointer;
}
.chatLogo {
  height: 20px;
  padding-right: 5px;
}
.promptsWrapper {
  display: flex;
  flex-flow: column;
  align-items: flex-start;
  .promptBox{
    background: #000000;
    padding: 10px;
    border-radius: 10px;
    margin-bottom: 5px;
    div{
      margin-bottom: 5px;
    }
    span {
      color: #8a93a3;
      font-size: 13px;
    }
  }
}
.stopWrapper {
  text-align: center;
  img {
    width: 20px;
  }
  bottom: 10px;
  position: fixed;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值