Bangun Chatbot AI Anda Sendiri dengan React dan ChatGPT API

Apr 17 2023
Gunakan React dan ChatGPT API untuk membuat chatbot dengan efek mesin tik yang realistis
OpenAI telah membuka API-nya, yang berarti Anda dapat menggunakannya untuk membangun chatbot Anda sendiri. Artikel ini akan memberi Anda contoh sederhana menggunakan React, termasuk meminta API ChatGPT asli dan menerapkan efek mesin tik yang mirip dengan situs web resmi.
Foto oleh Levart_Photographer di Unsplash

OpenAI telah membuka API-nya, yang berarti Anda dapat menggunakannya untuk membangun chatbot Anda sendiri. Artikel ini akan memberi Anda contoh sederhana menggunakan React, termasuk meminta API ChatGPT asli dan menerapkan efek mesin tik yang mirip dengan situs web resmi. Ini adalah alamat GitHub , dan Anda harus mengisi kunci API Anda jika ingin menjalankannya secara lokal.

Selanjutnya, saya akan menganalisis mesinnya.

Minta API dan Terapkan Streaming

Dalam askmetode yang diekspor ask.ts, saya hanya menulis fetchfungsi untuk permintaan tersebut.

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();
    });
  })();
}

Screenshot Selesai

Karena data yang dikembalikan oleh API dalam format Markdown, saya gunakan react-markdowndi sini sebagai wadah parsing, dan menambahkan beberapa plugin terkait, serta beberapa gaya kode dari 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;
}

Ini melengkapi konstruksi antarmuka ChatGPT UI yang sederhana. Meski terlihat sederhana, ia sudah memiliki fungsi dasar. Anda dapat memilih untuk menambahkan lebih banyak fitur dan gaya ke dalamnya. Saya harap ini membantu Anda.

Terima kasih sudah membaca. Jika Anda menyukai cerita seperti itu dan ingin mendukung saya, mohon pertimbangkan untuk menjadi anggota Medium . Biayanya $5 per bulan dan memberikan akses tak terbatas ke konten Medium. Saya akan mendapat sedikit komisi jika Anda mendaftar melalui link saya .