YYGod0120
MYCHATCategories: Project     2024-04-21

项目截图

项目截图1项目截图2

技术选型

  1. React
  2. Typescript
  3. Tailwind
  4. Axios
  5. Zustand
  6. marked-react,dayjs,file-saver

项目亮点

  1. 不同语料库的对话: 可选择更贴切提问者问题内容的语料库主题,联系上下文,做出切合问题的优质回答。

  2. 类GPT流的回答: GPT流式回答与等待,自动转化md格式。

  3. 优秀的用户体验: 无痛刷新,最大程度减少加载,一键导出对话为word,一键删除以及自定义会话标题。

实现难点

核心功能-AI对话

流式对话: 对于普通的get或者post请求,简单采用axios封装进行网络请求。而GPT对话流式数据,axios因为基于XHR没法做到post流式请求,改用fetch进行数据请求。

1//基本封装双token无痛刷新
2export const service = axios.create({
3  baseURL: BASE_URL,
4  timeout: 100000,
5});
6service.interceptors.request.use((config) => {
7  const token = localStorage.getItem("access_token");
8  if (token) {
9    config.headers.Authorization = `Bearer ${token}`;
10  }
11  return config;
12});
13
14service.interceptors.response.use(async (response) => {
15  if (response.data.info === "token invalid") {
16    if (response.config.url === "/user/refresh") {
17      localStorage.removeItem("refresh_token");
18      localStorage.removeItem("access_token");
19      localStorage.removeItem("user_id");
20    } else {
21      const newAccessToken = await postRefreshPost({
22        refresh_token: localStorage.getItem("refresh_token"),
23      });
24
25      localStorage.setItem("access_token", newAccessToken.data.access_token);
26      const originalRequest = response.config;
27      originalRequest.headers.Authorization = `Bearer ${localStorage.getItem("access_token")}`;
28      return service(originalRequest);
29    }
30  }
31  return response;
32});
33

对于特殊请求:

1const rep = await new_chat({
2  session_id: id,
3  category: identity,
4  content: words_human,
5}); //new_chat是基于fetch封装的一个数据请求函数。
6const reader = rep.body.getReader();
7const decoder = new TextDecoder();
8
9// eslint-disable-next-line no-constant-condition
10while (true) {
11  const { done, value } = await reader.read();
12  // 结果包含两个属性:
13  // done  - 如果为 true,表示流已经返回所有的数据。
14  // value - 一些数据,done 为 true 时,其值始终为 undefined。
15  const decoded = decoder.decode(value, { stream: true });
16  renderAIRes.current += decoded;
17}
18

因为XHR没法读取post流式数据,所以改用fetch。总所周知,fetch在请求成功发送的时候返回一个内建的Response对象,我们可以通过不同的格式来访问其body。

Response的body可以是一个可读的字节数据流-ReadableStream

rep.body.getReader()就是创建一个reader对象锁定该流,调用read()方法读取内容。

MD格式转化以及打字机样式:个人博客对于md格式的转化是利用marked转化为html再进一步进行正则匹配改为适配next的tsx

marked-react这个库的优点在于底层并非dangerouslySetInnerHTML实现,避免了XSS攻击,更安全。同时也支持不同语法高亮库进行code的高亮显示。不用自己库库手搓

打字机样式实现的难点在于判断是否正在输出以及正在输出的内容是什么,判断单次输出完毕与否进行下个内容的读取。

1const renderAIRes = useRef("");
2// data读取
3const decoded = decoder.decode(value, { stream: true });
4setConversation([
5  ...conversations,
6  { HUMAN: words_human, time: askTime },
7  {
8    AI: [
9      {
10        answer: renderAIRes.current,
11        isChatting: false,
12      },
13      { answer: decoded, isChatting: true },
14    ],
15    time: getCurrentTime(),
16  },
17]);
18renderAIRes.current += decoded;
19

isChatting判断是否是当前流式数据,非当前流式数据通过ref进行保存避免多次重新渲染。

1//伪代码实现
2<div className="">
3  {(word as AIType).map((item) => {
4    return item.isChatting ? (
5      <Typist
6        avgTypingDelay={60}
7        cursor={{ show: false }}
8        key={item.answer}
9        onTypingDone={() => {
10          oneContentTypingOver(true);
11        }}
12        className="inline"
13      >
14        <Markdown>{item.answer}</Markdown>
15      </Typist>
16    ) : (
17      <>{<Markdown>{item.answer}</Markdown>}</>
18    );
19  })}
20</div>
21

根据isChatting选择是否打字机,控制打字机结束后再重新获取新内容进行渲染。实现GPT打字机样式。

用户体验

零痛更新:在先前的项目中,对于后台表单数据的增删查改,都是全部依赖数据请求进行页面重新渲染。这样会导致一个用户体验不行,每次数据更新都要再次请求数据进行渲染,网络请求量大,页面加载慢,用户体验不行。于是在这个项目中采用了Zustand进行全局数据管理,获取数据复制到本地数据,每次数据更新时同时更新本地数据。利用本地数据进行渲染,实现无痛页面更新渲染。

1<Button
2  className="h-[47px] w-[165px] px-3 py-1"
3  style={{
4    backgroundColor: "#8C7BF7",
5    color: "white",
6    border: "none",
7  }}
8  onClick={() => {
9    setIsTaking(true);
10    setSession([
11      ...sessions,
12      {
13        created_at: "",
14        id: newId,
15        metadata: {
16          title: "新对话",
17          category: "",
18        },
19        session_id: `${newId}`,
20        updated_at: "",
21        user_id: "",
22        uuid: "",
23      },
24    ]);
25    handleClick(newId + "", "新对话");
26    handleChooseIdentity(false);
27    setNewId((newId) => newId + 1);
28  }}
29  disabled={talking}
30>
31  创建新对话
32</Button>
33

新对话创建,删除,以及历史会话记录的删除,更新,都是依赖于本地数据而非请求数据。但是这样也会有一定的问题,比如首次请求量大,导致瀑布流等等

RSC(react-serve-component)解决了数据渲染问题,因为它采用服务端生成组件绑定数据返回到客户端进进行渲染。

总结

项目写的比较急,加上一个人开发,所以对一些组件封装不足,基本都写在一起。

Typescript也因为项目体积小,开发不规范使用的少。

主要还是解决网络请求以及数据处理上的难点,着力于样式以及用户体验

© 2023 - 2024
githubYYGod0120