react+星火大模型,构建上下文ai问答页面(可扩展)

前言

最近写的开源项目核心功能跑通了,前两天突发奇想。关于项目可否介入大模型来辅助用户使用平台,就跑去研究了最近比较活火的国内大模型–讯飞星火大模型。

大模型api获取

控制台登录

地址:https://console.xfyun.cn/app/myapp
新建应用后点进去:
react+星火大模型,构建上下文ai问答页面(可扩展)_第1张图片

获取api地址和其key

react+星火大模型,构建上下文ai问答页面(可扩展)_第2张图片
左侧选用大模型版本,右侧圈起来的地方就是咱api要调用的数据了

如果没有正常的token或key可能没有实名认证,需要先实名认证下!

下面有关于web的调用接口,这是咱们后面要调用的api接口地址:
react+星火大模型,构建上下文ai问答页面(可扩展)_第3张图片

技术栈

react hooks + TypeScript + semi-ui(组件库,可选)

下载工具包:

npm i crypto-js base-64 -d

实现

api和key都获取到了,咱就直接开始上代码操作吧!

目录

react+星火大模型,构建上下文ai问答页面(可扩展)_第4张图片

utils(工具类)

以下工具类getWebsocketUrl方法,负责构建api的URL地址,具体原因可以查阅对应的官方文档说明

import * as base64 from 'base-64';
import CryptoJs from 'crypto-js';
import { requestObj } from '../config';
export const getWebsocketUrl = () => {
  return new Promise<string>((resovle, reject) => {
    let url = 'ws://spark-api.xf-yun.com/v1.1/chat';
    let host = 'spark-api.xf-yun.com';
    let apiKeyName = 'api_key';
    // let date = new Date().toGMTString();
    let date = new Date().toUTCString();
    let algorithm = 'hmac-sha256';
    let headers = 'host date request-line';
    let signatureOrigin = `host: ${host}\ndate: ${date}\nGET /v1.1/chat HTTP/1.1`;
    let signatureSha = CryptoJs.HmacSHA256(signatureOrigin, requestObj.APISecret);
    let signature = CryptoJs.enc.Base64.stringify(signatureSha);

    let authorizationOrigin = `${apiKeyName}="${requestObj.APIKey}", algorithm="${algorithm}", headers="${headers}", signature="${signature}"`;

    let authorization = base64.encode(authorizationOrigin);

    // 将空格编码
    url = `${url}?authorization=${authorization}&date=${encodeURI(date)}&host=${host}`;

    resovle(url);
  });
};

config(配置类)

前言说的 key 在此处分别填入即可,Uid无关紧要
react+星火大模型,构建上下文ai问答页面(可扩展)_第5张图片

server/AiTool(ws服务工具)

该工具类负责接收父类组件传来的问题,并对相关数据信息进行返回。

  • **forwardRef,useImperativeHandle,props **实现父子通信
  • 通过 **getWebsocketUrl **返回url地址构建ws通信
  • **websocket **返回相关ai回应数据,并对数据加载状态进行实时通信
import { FC, forwardRef, useImperativeHandle, useState } from 'react';
import { requestObj } from '../config';
import { getWebsocketUrl } from '../utils';
interface AiToolProps {
  isText?: boolean;
  respondHoodle: (result: string) => void; //关联数据
  loadHoodle?: (isLoading: boolean) => void; //加载状态
  errorHoodle?: (isLoading: boolean) => void; //失败调用
}

interface CropperRef {
  submitHoodle: (v: any) => void; //父类调用
}

const AiTool = forwardRef<CropperRef, AiToolProps>(function AiTool(
  { isText, respondHoodle, loadHoodle, errorHoodle },
  ref
) {
  let result: string = '';
  const [historyMessage, setHistoryMessage] = useState<any[]>([
    { role: 'user', content: '你是谁' }, //# 用户的历史问题
    { role: 'assistant', content: '我是AI助手' }
  ]);

  useImperativeHandle(ref, () => ({
    submitHoodle: sendMsg
  }));
  const sendMsg = async (questionText: string) => {
    result = ' ';
    // 获取请求地址
    let myUrl = await getWebsocketUrl();
    // 获取输入框中的内容
    // 每次发送问题 都是一个新的websocket请求
    let socket = new WebSocket(myUrl);
    // 监听websocket的各阶段事件 并做相应处理
    socket.addEventListener('open', (event) => {
      if (loadHoodle) loadHoodle(true);
      // 发送消息
      let params = {
        header: {
          app_id: requestObj.APPID,
          uid: 'wzz'
        },
        parameter: {
          chat: {
            domain: 'general',
            temperature: 0.5,
            max_tokens: 1024
          }
        },
        payload: {
          message: {
            // 如果想获取结合上下文的回答,需要开发者每次将历史问答信息一起传给服务端,如下示例
            // 注意:text里面的所有content内容加一起的tokens需要控制在8192以内,开发者如有较长对话需求,需要适当裁剪历史信息
            text: [
              ...historyMessage,
              // ....... 省略的历史对话
              { role: 'user', content: questionText } //# 最新的一条问题,如无需上下文,可只传最新一条问题
            ]
          }
        }
      };
      socket.send(JSON.stringify(params));
    });
    socket.addEventListener('message', (event) => {
      let data = JSON.parse(event.data);
      if (!data.payload) {
        socket.close();
        return;
      }
      result += data.payload.choices.text[0].content;
      respondHoodle(result);
      if (data.header.code !== 0) {
        console.log('出错了', data.header.code, ':', data.header.message);
        // 出错了"手动关闭连接"
        socket.close();
      }
      if (data.header.code === 0) {
        // 对话已经完成
        if (data.payload.choices.text && data.header.status === 2) {
          setTimeout(() => {
            // "对话完成,手动关闭连接"
            socket.close();
          }, 1000);
        }
      }
    });
    socket.addEventListener('close', (event) => {
      setHistoryMessage([
        ...historyMessage,
        { role: 'user', content: questionText },
        { role: 'assistant', content: result }
      ]);
      if (loadHoodle) loadHoodle(false);
      // 对话完成后socket会关闭,将聊天记录换行处理
    });
    socket.addEventListener('error', (event) => {
      if (errorHoodle) errorHoodle(true);
      console.log('连接发送错误!!', event);
    });
  };
  // return result;
  return '';
});
export default AiTool;

Chat.tsx(具体组件)

chat组件的具体实现

  • messageList 记录信息数据
  • submit 发送问题函数
  • overRespond 介绍信息函数
  • **moveY **返回底部
import { memo, useRef, useState } from 'react';
//type
import type { FC } from 'react';
import styles from './index.module.scss';
import { Button, Spin } from '@douyinfe/semi-ui';
import AiTool from '@/ai/server/AiTool';
interface IProps {
  datas?: any[];
}
//user:true代表用户信息,反之ai
interface messageInfo {
  text: string;
  user: boolean;
}

const Chat: FC<IProps> = () => {
  const [question, setQuestion] = useState<string>('');
  // const [result, setResult] = useState('');
  let result = '';
  const [isLoading, setIsLoading] = useState<boolean>(false);
  const [messageList, setMessageList] = useState<messageInfo[]>([]);
  const ref = useRef<any>(null);
  const messageContainerRef = useRef<any>(null);
  const loadingRef = useRef<any>(null);
  const submit = () => {
    setQuestion('');
    console.log(messageList);
    if (!messageList.length) {
      setMessageList([
        {
          user: true,
          text: question
        }
      ]);
      console.log(messageList);
    } else {
      setMessageList([
        ...messageList,
        {
          user: true,
          text: question
        }
      ]);
      console.log(messageList);
    }
    moveY();
    if (ref.current) {
      ref.current.submitHoodle(question);
    }
  };
  const respondHoodle = (respond: string) => {
    result = respond;
    loadingRef.current.innerText = result;
    moveY();
    // loadingRef.current
  };
  const overRespond = (v: boolean) => {
    if (!v) {
      setMessageList((prevList) => [...prevList, { user: false, text: result }]);
      console.log(messageList);
      moveY();
    }
    setIsLoading(v);
  };
  const handleSendMessage = (e: any) => {
    e.preventDefault();
  };
  const handleKeyPress = (e: any) => {
    if (e.keyCode === 13) {
      submit();
    }
  };
   //返回底部
  const moveY = () => {
    const h = messageContainerRef.current.scrollHeight;
    messageContainerRef.current.scrollTop = h + 20;
  };
  return (
    <div className={styles.chat}>
      <div className={styles.chat__main}>
        <header className={styles.chat__mainHeader}>
          <p>欢迎使用青邮AI助手!</p>
          <div>
            <Button onClick={moveY} style={{ marginRight: 4 }}>
              返回底部
            </Button>
            <Button type="danger" theme="solid" onClick={() => setMessageList([])}>
              清除聊天记录
            </Button>
          </div>
        </header>
        {/* 显示你发送消息的内容 */}
        <div className={styles.message__container} ref={messageContainerRef}>
          {messageList.map((item, index) => {
            return item.user ? (
              <div key={item.user.toString() + index} className={styles.message__chats}>
                <p className={styles.sender__name}>You</p>
                <div className={styles.message__sender}>
                  <p>{item.text}</p>
                </div>
              </div>
            ) : (
              <div className={styles.message__chats}>
                <p>Ai</p>
                <div className={styles.message__recipient}>
                  <p>{item.text}</p>
                </div>
              </div>
            );
          })}
          {isLoading ? (
            <div className={styles.message__chats}>
              <p>Ai</p>
              <div className={styles.message__recipient}>
                <p ref={loadingRef}>{result}</p>
                <Spin />
              </div>
            </div>
          ) : (
            ''
          )}
        </div>
        <div className={styles.chat__footer}>
          <form className="form" onSubmit={handleSendMessage}>
            <input
              type="text"
              placeholder="编写消息"
              className={styles.message}
              value={question}
              onChange={(e) => setQuestion(e.target.value)}
              onKeyUp={handleKeyPress}
            />
            <Button onClick={submit} type="primary" theme="solid" className={styles.sendBtn}>
              发送
            </Button>
          </form>
        </div>
        <AiTool loadHoodle={overRespond} respondHoodle={respondHoodle} ref={ref} />
      </div>
    </div>
  );
};

export default memo(Chat);

scss就不v了,如需要可以在评论区喊我

结果

效果图

react+星火大模型,构建上下文ai问答页面(可扩展)_第6张图片

后言

关于使用server的AiTool.tsx工具,其实还能产生不少其他的扩展,接下来着重研究下!加油加油!

你可能感兴趣的:(项目实战,react.js,ai,星火大模型,LLM)