Créez votre propre chatbot IA avec React et l'API ChatGPT

Apr 17 2023
Utilisez React et l'API ChatGPT pour créer un chatbot avec un effet de machine à écrire réaliste
OpenAI a ouvert son API, ce qui signifie que vous pouvez l'utiliser pour créer votre propre chatbot. Cet article vous fournira un exemple simple utilisant React, y compris la demande de la véritable API ChatGPT et la mise en œuvre d'un effet de machine à écrire similaire au site officiel.
Photo de Levart_Photographer sur Unsplash

OpenAI a ouvert son API, ce qui signifie que vous pouvez l'utiliser pour créer votre propre chatbot. Cet article vous fournira un exemple simple utilisant React, y compris la demande de la véritable API ChatGPT et la mise en œuvre d'un effet de machine à écrire similaire au site officiel. Voici l' adresse GitHub , et vous devez renseigner votre clé API si vous souhaitez l'exécuter localement.

Ensuite, je vais analyser son moteur.

Demander l'API et mettre en œuvre le streaming

Dans la askméthode exportée dans ask.ts, j'ai simplement écrit une fetchfonction pour la requête.

const res = await fetch('https://api.openai.com/v1/chat/completions', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    Authorization: '', // Fill your OpenAI key
  },
  body: JSON.stringify({
    stream: true,
    max_tokens: 1000,
    model: 'gpt-3.5-turbo',
    temperature: 0.8,
    top_p: 1,
    presence_penalty: 1,
    messages,
  }),
});

const parseChunk = createParseChunkFn((event) => {
  if (event.type === 'event') {
    onMessage(event.data);
  }
});

const reader = res.body?.getReader();
if (reader) {
  void (function read() {
    reader.read().then(({ done, value }) => {
      if (done) {
        return;
      }
      const chunk = new TextDecoder().decode(value);
      parseChunk(chunk);
      read();
    });
  })();
}

Capture d'écran terminée

Étant donné que les données renvoyées par l'API sont au format Markdown, j'utilise react-markdownici comme conteneur d'analyse et j'ajoute des plugins associés, ainsi que des styles de code de highlight.js .

import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import rehypeHighlight from 'rehype-highlight';
import 'highlight.js/styles/vs2015.css';

<ReactMarkdown
  remarkPlugins={[remarkGfm]}
  rehypePlugins={[rehypeHighlight]}
>
  {l.answering ? answeringContent : l.content}
</ReactMarkdown>

import { useCallback, useState } from 'react';
import { Button, Input, Space, Typography } from 'antd';

import './App.css';
import ask from './ask';

import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import rehypeHighlight from 'rehype-highlight';
import 'highlight.js/styles/vs2015.css';

const { Text } = Typography;

const enum Role {
  Assistant = 'assistant',
  User = 'user',
}

export interface Message {
  role: Role;
  content: string;
}

interface Log extends Message {
  id: string;
  answering?: boolean;
}

export default () => {
  const [question, setQuestion] = useState('');
  const [logs, setLogs] = useState<Log[]>(() => []);

  const [answeringContent, setAnsweringContent] = useState('');

  const askQuestion = useCallback((messages: Message[]) => {
    setAnsweringContent('&ZeroWidthSpace;');

    let contents = '';
    ask((str) => {
      if (!str) return;
      if (str === '[DONE]') {
        setAnsweringContent('');
        setLogs((prev) =>
          prev.map((i) => {
            const { answering, ...rest } = i;
            if (answering) {
              return {
                ...rest,
                content: contents,
              };
            }
            return i;
          })
        );

        return;
      }

      let content = str;
      try {
        const data = JSON.parse(str);
        content = data.choices?.reduce((acc: string, cur: unknown) => {
          // @ts-expect-error
          acc += cur?.delta?.content ?? '';
          return acc;
        }, '');
      } catch {
        // Ignore
        // console.error(err);
      }

      contents += content;
      setAnsweringContent(contents);
    }, messages);
  }, []);

  const isAnswering = Boolean(answeringContent);

  const onSubmit = () => {
    if (isAnswering) return;
    const messages = logs.concat({
      id: crypto.randomUUID(),
      role: Role.User,
      content: question,
    });

    askQuestion(messages.map((i) => ({ role: i.role, content: i.content })));
    setQuestion('');
    setLogs([
      ...messages,
      {
        content: '',
        answering: true,
        role: Role.Assistant,
        id: crypto.randomUUID(),
      },
    ]);
  };

  return (
    <main className='main'>
      <div className='chat'>
        <div className='logs'>
          {logs.map((l) => (
            <div key={l.id} className='log'>
              <Text strong>{l.role === Role.User ? 'User:' : 'ChatGPT:'}</Text>
              <div className={l.answering ? 'streaming' : ''}>
                <ReactMarkdown
                  remarkPlugins={[remarkGfm]}
                  rehypePlugins={[rehypeHighlight]}
                >
                  {l.answering ? answeringContent : l.content}
                </ReactMarkdown>
              </div>
            </div>
          ))}
        </div>
        <Space.Compact>
          <Input
            allowClear
            value={question}
            onPressEnter={onSubmit}
            placeholder='Enter your question'
            onChange={(e) => setQuestion(e.target.value)}
          />
          <Button type='primary' onClick={onSubmit} loading={isAnswering}>
            {isAnswering ? 'Answering' : 'Submit'}
          </Button>
        </Space.Compact>
      </div>
    </main>
  );
};

@keyframes blink {
  to {
    visibility: hidden;
  }
}

.streaming > :not(ol):not(ul):not(pre):last-child:after,
.streaming > ol:last-child li:last-child:after,
.streaming > pre:last-child code:after,
.streaming > ul:last-child li:last-child:after {
  -webkit-animation: blink 1s steps(5, start) infinite;
  animation: blink 1s steps(5, start) infinite;
  content: '▋';
  margin-left: 0.25rem;
  vertical-align: baseline;
}

Ceci termine la construction simple de l'interface utilisateur ChatGPT. Bien qu'il semble rudimentaire, il possède déjà des fonctionnalités de base. Vous pouvez choisir d'y ajouter plus de fonctionnalités et de styles. J'espère que ça t'aide.

Merci d'avoir lu. Si vous aimez de telles histoires et que vous voulez me soutenir, pensez à devenir membre Medium . Il coûte 5 $ par mois et donne un accès illimité au contenu moyen. Je toucherai une petite commission si vous vous inscrivez via mon lien .