前端调用gpt制作简单gpt页面

881 阅读3分钟

写一个简单的聊天页面,使用SSE调用ChatGPT的接口,使用react-markdown显示获取到的数据。

又来水一篇文章。后端小伙伴对于ai一直很感兴趣,想在公司的综合平台里用gpt集成知识库,让同事都可以免费使用gpt4。研究了一下,了解了一点SSE的知识点。(只是想法,自己写着玩儿,上线的话还需要考虑很多东西)

ChatGPT的打字机效果是使用SSE(Server-Sent Events)获取流式数据实现的。SSE与websocket是单向的,服务器可以主动向浏览器发送数据,但是浏览器不能向服务器发。SSE基于HTTP,而websocket是有另外的协议的,所以SSE的兼容性更好一点。

原生的EventSource直接发送get请求,我们想发post请求可以用封装过的@microsoft/fetch-event-source

react-markdown是一个基于react的markdown解析器组件,可以在组件中直接渲染markdown格式的内容,然后通过其他插件可以给这个元素加各种样式。

import { Button, Col, Input, Row } from 'antd';
import React, { useEffect, useRef, useState } from 'react';
import { fetchEventSource } from '@microsoft/fetch-event-source';
import { PageContainer } from '@ant-design/pro-layout';
import ProCard from '@ant-design/pro-card';
import styles from './index.less';
import { SearchOutlined } from '@ant-design/icons';
import { useModel } from 'umi';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm'; // 划线、表、任务列表和直接url等的语法扩展
import rehypeRaw from 'rehype-raw'; // 解析标签,支持html语法
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; // 代码高亮
//高亮的主题,还有很多别的主题,可以自行选择
import { tomorrow } from 'react-syntax-highlighter/dist/esm/styles/prism';
import { v4 as uuidv4 } from 'uuid';
import 'github-markdown-css';

const AIChat: React.FC = () => {
  const [inputValue, setInputValue] = useState<string>('');
  const [loading, setLoading] = useState<boolean>(false); //控制是否能input
  const stopReading = useRef<boolean>(false);
  const [chatData, setChatData] = useState<Record<string, any>[]>([
    { id: -1, content: '你好,请输入你的问题。' },
  ]);
  const { initialState } = useModel('@@initialState');
  const { currentUser } = initialState || {};
  const count = useRef<number>(0);
  const [tempContent, setTempContent] = useState<string>('');
  const tempContent2 = useRef<string>('');

  useEffect(() => {
    if (localStorage.getItem('chatData')) {
      setChatData(JSON.parse(localStorage.getItem('chatData') || ''));
    }
  }, []);
  const ctrl = new AbortController();
  const fetchEventGPT = () => {
    setChatData([
      ...chatData,
      { id: uuidv4(), content: inputValue, roles: 'user' },
      { id: uuidv4(), content: '', roles: 'gpt' },
    ]);
    count.current += 2;
    setInputValue('');
    setTempContent('');
    tempContent2.current = '';
    setLoading(true);
    // const data = {
    //   channelId: '1220648311464267808',
    //   messages: [
    //     {
    //       content: inputValue,
    //       role: 'user',
    //     },
    //   ],
    //   model: 'GPT-3.5',
    //   stream: true,
    // };
    const data = {
      content: inputValue,
    };
    //换成gpt的接口
    fetchEventSource('http://192.168.200.207:8123/gpt/chat', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(data),
      signal: ctrl.signal,
      async onopen(response) {
        if (response.ok && response.status === 200) {
          console.log('open');
        }
      },
      onmessage(msg) {
        if (stopReading.current) {
          console.log('stop');
          return;
        }
        if (msg.data === '{"event":"done"}') {
          console.log('done');
          let temp: Record<string, any>[] = [];
          setChatData((pre) => {
            pre[pre.length - 1].content = tempContent2.current;
            temp = pre;
            return temp;
          });
          localStorage.setItem('chatData', JSON.stringify(temp));
          return;
        }
        if (msg.data) {
          try {
            const datas = JSON.parse(msg.data);
            const { message } = datas;
            if (message?.type === 'answer' && message?.content) {
              tempContent2.current = tempContent2.current + message?.content;
              setTempContent(tempContent2.current);
            }
            // if (datas.event === 'error') {
            //   setTempContent('出错了,' + datas?.error_information?.err_msg || '请联系管理员。');
            // }
          } catch (error) {
            console.log(error);
          }
        }
      },
      onclose() {
        console.log('close');
        stopReading.current = false;
        setLoading(false);
        ctrl.abort();
      },
      onerror(err) {
        console.log('error', err);
        stopReading.current = false;
        setLoading(false);
        ctrl.abort();
        // source?.close();
      },
    });
  };

  const suffix = (
    <SearchOutlined
      style={{
        fontSize: 16,
      }}
      onClick={fetchEventGPT}
    />
  );

  return (
    <>
      <PageContainer title={false} style={{ height: '100%' }}>
        <ProCard style={{ marginBottom: 20, borderRadius: 5 }}>
          <div className={styles.gptContent}>
            {chatData && chatData.length > 0 && (
              <>
                {chatData.map((item) => {
                  return (
                    <Row key={item.id} gutter={[24, 24]} style={{ marginBottom: 18 }}>
                      {item.roles === 'user' ? (
                        <>
                          <Col>
                            <img
                              src={
                                currentUser?.headImgUrl ||
                                'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png'
                              }
                              alt=""
                              className={styles.imgStyle}
                            />
                          </Col>
                          <Col
                            span={20}
                            style={{
                              border: '1px solid #cfd3d3',
                              borderRadius: '10px',
                              width: '100%',
                              minHeight: '30px',
                              backgroundColor: 'rgb(93,92,222)',
                              color: '#fff',
                              display: 'flex',
                              justifyContent: 'flex-start',
                              alignItems: 'center',
                              fontSize: 16,
                            }}
                          >
                            <div id={item.id} style={{ padding: '5 2' }}>
                              {item.content}
                            </div>
                          </Col>
                        </>
                      ) : (
                        <>
                          <Col>
                            <img
                              src={
                                'https://psc2.cf2.poecdn.net/assets/_next/static/media/assistant.b077c338.svg'
                              }
                              alt=""
                              className={styles.imgStyle}
                            />
                          </Col>
                          <Col
                            span={20}
                            style={{
                              border: '1px solid #cfd3d3',
                              borderRadius: '10px',
                              width: '100%',
                              minHeight: '30px',
                            }}
                          >
                            <ReactMarkdown
                              className="markdown-body"
                              remarkPlugins={[remarkGfm]}
                              rehypePlugins={[rehypeRaw]}
                              components={{
                                img(props) {
                                  return <img {...props} style={{ maxWidth: '100%' }} />;
                                },
                                code({ className, children, ...props }) {
                                  const match = /language-(\w+)/.exec(className || '');
                                  return match ? (
                                    <SyntaxHighlighter
                                      style={tomorrow}
                                      language={match[1]}
                                      PreTag="div"
                                      {...props}
                                    >
                                      {String(children).replace(/\n$/, '')}
                                    </SyntaxHighlighter>
                                  ) : (
                                    <code className={className} {...props}>
                                      {children}
                                    </code>
                                  );
                                },
                              }}
                            >
                              {item.content ? item.content : tempContent}
                            </ReactMarkdown>

                            {/* <ReactMarkdown
                              remarkPlugins={[remarkGfm]}
                              rehypePlugins={[rehypeHighlight]}
                              style={{ marginBottom: 0 }}
                            >
                              {item.content ? item.content : tempContent}
                            </ReactMarkdown> */}
                          </Col>
                        </>
                      )}
                    </Row>
                  );
                })}
              </>
            )}
          </div>
          <Button
            style={{ display: loading ? '' : 'none' }}
            onClick={() => {
              stopReading.current = true;
              ctrl.abort();
            }}
          >
            Stop
          </Button>
          <Input
            size={'large'}
            type="textarea"
            placeholder="与Assistant交谈"
            className={styles.chatInput}
            suffix={suffix}
            value={inputValue}
            onChange={(e) => {
              setInputValue(e.target.value);
            }}
            onPressEnter={fetchEventGPT}
            disabled={loading}
          />
        </ProCard>
      </PageContainer>
    </>
  );
};

export default AIChat;


做了上下文的缓存,样式还有没写好的地方,没有做到poe那样,有空再继续完善。

1def611d1850f660a4195ac484ccf4f.png

128022e787f3db2c6af3afc294cb09e.png

两种markdown的样式