+3

Xây dựng ứng dụng hỗ trợ luyện viết tiếng Anh bằng ChatGPT API và NextJS

Xin chào tất cả các bạn!

Nhân dịp năm mới xin chúc tất cả các bạn đọc của Viblo một năm an khang thịnh vượng, đạt được nhiều thành công trong sự nghiệp và cuộc sống!

Chắc hẳn các bạn đều biết tới ChatGPT, 1 con bot AI nổi lên trong thời gian gần đây với khả năng trò chuyện và trả lời về hầu như mọi lĩnh vực trong cuộc sống. Dù còn nhiều tranh cãi về tính đúng sai của dữ liệu nhưng không thể phủ nhận được sức mạnh rất lớn của công cụ này cũng như AI trong việc giúp tăng năng suất của con người trong nhiều ngành nghề khác nhau như lập trình, marketing,…

Trong bài viết này, chúng ta sẽ sử dụng API của nó để viết 1 ứng dụng đơn giản hỗ trợ người dùng trong việc học tiếng Anh, và cụ thể hơn là tối ưu việc viết bài luận IELTS Writing và Speaking.

Và tất nhiên, khá nhiều đoạn code trong ứng dụng này được chính ChatGPT viết 😃

Vì OpenAI chưa mở public đến API của chính ChatGPT, nên mình sẽ sử dụng API Text Completion với tính năng generate text tương tự như ChatGPT.

Các bạn có thể tham khảo ở đây.

OpenAI API

Các tính năng của ứng dụng này bao gồm:

  • Từ kiểu bài luận: IELTS Writing task 2 và đề bài do người dùng nhập, ứng dụng cung cấp gợi ý, tạo bài mẫu
  • Chỉnh sửa lỗi, gợi ý câu văn, giải thích nghĩa của từ,… dựa theo đoạn văn bản người dùng đã nhập và đề bài luận

Các bạn có thể check source code của project ở đây.

https://github.com/ngviethoang/ai-writing-assistant

Demo ứng dụng.

Writing Assistant

Cài đặt

Khởi tạo NextJS project

yarn create next-app --typescript

Cài đặt các thư viện: OpenAI client, ChakraUI (UI framework)

yarn add @chakra-ui/react @emotion/react @emotion/styled framer-motion 
yarn add openai

Đăng ký OpenAI API key

Đăng nhập tài khoản OpenAI tại trang https://platform.openai.com/

Tạo API Secret Key

Tạo file .env trong project và lưu secret key

OPENAI_API_KEY=[Nhập key đã tạo]

Thêm file .env này vào file .gitignore để tránh bị lộ key khi commit code

Tạo prompt để giao tiếp với API

Để giao tiếp với Text Completion API, ta cần sử dụng các câu truy vấn (prompt). Đây là bước quan trọng để có thể ra được output chính xác như những gì ta mong muốn. Theo thuật ngữ trong NLP là prompt engineering.

Ví dụ như 1 prompt mẫu để tạo outline mẫu cho bài viết theo đề bài của IELTS Writing task:

Act as an IELTS test taker with a band score of 8.0. Write an essay outline in response to the following IELTS Writing Task 2 question: [insert IELTS Writing Task 2 question]

Ở đây ta có thể định nghĩa các parameter có thể truyền lên từ UI:

  • actor: an IELTS test taker with a band score of 8.0
  • question: IELTS Writing Task 2 question
  • content: đoạn text do người dùng nhập

Tạo hàm xây dựng prompt dùng cho việc truy vấn API dựa trên các parameter actor, question, content.

const getPrompt = (topicType: string, promptType: string, topic: string, content: string) => {
  let actor, questionType
  switch (topicType) {
    case 'IELTS Writing':
      questionType = 'IELTS Writing Task 2'
      actor = 'an IELTS test taker with a band score of 8.0'
      break
    case 'IELTS Speaking':
      questionType = 'IELTS Speaking'
      actor = 'an IELTS test taker with a band score of 8.0'
      break
    default:
      questionType = ''
      actor = 'a person'
      break
  }

  switch (promptType) {
		case 'outline':
      return `Act as ${actor}. Write an essay outline in response to the following ${questionType} question: ${topic}`
    case 'support_arguments':
      return `Act as ${actor}. Given the following ${questionType} question, generate 3 arguments to support the statement: ${topic}`
    case 'oppose_arguments':
      return `Act as ${actor}. Given the following ${questionType} question, generate 3 arguments to oppose the statement: ${topic}`
    case 'sample_answer':
      return `Act as ${actor}. Write an essay in response to the following ${questionType} question with at least 250 words: ${topic}`

    case 'summarize':
      return `Act as a summarizer and summarize this essay: 
${content}`
		// ...

    default:
      return ''
  }
}

Tạo API handler trong NextJS

Để tạo API handler xử lý kết quả truy vấn từ Text Completion, tạo API route trong thư mục pages/api/prompt.ts

import type { NextApiRequest, NextApiResponse } from 'next'
import { Configuration, OpenAIApi } from 'openai';

const configuration = new Configuration({
  apiKey: process.env.OPENAI_API_KEY,
});
const openai = new OpenAIApi(configuration);

const isEmpty = (str: string) => !str.trim().length

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse<any>
) {
  if (!configuration.apiKey) {
    res.status(500).json({
      error: {
        message:
          'OpenAI API key not configured, please follow instructions in README.md',
      },
    });
    return;
  }

  const question = req.body.question || '';
  const topicType = req.body.topicType || '';
  const promptType = req.body.promptType || '';
  const content = req.body.content || '';

  if (isEmpty(question) || isEmpty(topicType) || isEmpty(promptType)) {
    res.status(400).json({
      error: {
        message: 'Invalid args',
      },
    });
    return;
  }

  const prompt = getPrompt(topicType, promptType, question, content)
  if (isEmpty(prompt)) {
    res.status(400).json({
      error: {
        message: 'Invalid prompt',
      },
    });
    return;
  }

  try {
    const completion = await openai.createCompletion({
      model: 'text-davinci-003',
      prompt,
      temperature: 0.5,
      max_tokens: 550,
    });
    res.status(200).json({ result: completion.data.choices[0].text });
  } catch (error: any) {
    if (error.response) {
      console.error(error.response.status, error.response.data);
      res.status(error.response.status).json(error.response.data);
    } else {
      console.error(`Error with OpenAI API request: ${error.message}`);
      res.status(500).json({
        error: {
          message: 'An error occurred during your request.',
        },
      });
    }
  }
}

Các tham số trong Text Completion API được sử dụng

  • model: sử dụng model text-davinci-003 mới nhất và mạnh nhất trong các GPT-3 model
  • prompt: câu truy vấn đã build ở step trước
  • temperature: quyết định độ ổn định của kết quả, temperature càng cao model ra kết quả càng đa dạng hơn
  • max_tokens: số lượng token tối đa trả về, có thể giới hạn số lượng token trả về mỗi prompt để giảm chi phí

Code giao diện

Tiếp theo là phần frontend cho ứng dụng, mình sẽ viết các component cơ bản như

  • Text editor để nhập câu hỏi, nội dung bài viết
  • Các button dùng để gọi API tương ứng với các function như tạo outline bài viết, tạo bài viết sample, chữa lỗi chính tả, nhận xét,…
  • Component hiển thị kết quả trả về từ API

Tạo các component và layout cho page bằng ChakraUI

import { Box, Button, Heading, HStack, Select, Spinner, Text, Textarea, Tooltip, useToast, VStack } from '@chakra-ui/react';
import { useState } from 'react';

const topicTypes = ['IELTS Writing', 'IELTS Speaking'];

const Writing = () => {
  const [topicType, setTopicType] = useState(topicTypes[0]);
  const [question, setQuestion] = useState('');
  const [content, setContent] = useState('');
  const [selectedContent, setSelectedContent] = useState('');

  return (
    <div style={{ position: 'relative' }}>
      <VStack spacing={5} padding={5}>
        <VStack w={'100%'} spacing={2} alignItems="flex-start">
          <HStack alignItems="flex-start" w="100%" gap={2}>
            <Text>AI Type: </Text>
            <Select
              size={'sm'}
              w={40}
              value={topicType}
              onChange={(e) => setTopicType(e.target.value)}
            >
              {topicTypes.map((type) => (
                <option key={type} value={type}>
                  {type}
                </option>
              ))}
            </Select>
          </HStack>
          <HStack alignItems="flex-start" w="100%" gap={2}>
            <Text>Question: </Text>
            <Textarea
              value={question}
              onChange={(e) => setQuestion(e.target.value)}
            />
          </HStack>
        </VStack>

        <HStack spacing={5} alignItems="flex-start" w="100%">
          <VStack w="100%">
            <Textarea
              rows={20}
              value={content}
              onChange={(e) => setContent(e.target.value)}
              onSelect={(e: any) => {
                // lưu selection text để lấy gợi ý từ API cho các từ này
                e.preventDefault();
                const { selectionStart, selectionEnd }: any = e.target;
                const selectedText = content.slice(selectionStart, selectionEnd);
                setSelectedContent(selectedText);
              }}
            />
          </VStack>
          {/* render buttons và kết quả gợi ý */}
          <VStack alignItems="flex-start" w="100%"></VStack>
        </HStack>
      </VStack>
    </div>
  );
};

export default Writing;

Render các button để generate prompt và kết quả gợi ý từ API

const generateButtons = [
  { name: 'Outline', promptType: 'outline', tooltip: 'Write an essay outline' },
  {
    name: 'Supportive arguments',
    promptType: 'support_arguments',
    tooltip: 'generate 3 arguments to support the statement',
  },
  {
    name: 'Opposite arguments',
    promptType: 'oppose_arguments',
    tooltip: 'generate 3 arguments to oppose the statement',
  },
  // ... full list button in source code
];

const vocabButtons = [
  {
    name: 'Dictionary',
    promptType: 'dictionary',
    tooltip:
      'Explain the meaning of the word and give me an example of how to use it in real life',
  },
  { name: 'Synonyms', promptType: 'synonyms', tooltip: 'Give me 5 synonyms' },
  { name: 'Antonyms', promptType: 'antonyms', tooltip: 'Give me 5 antonyms' },
];

const [result, setResult] = useState({ title: '', content: '' });

const renderButtons = (buttons: any[], color: string, content: string, isDisabled: boolean) => {
  return (
    <HStack gap={1} wrap="wrap" alignItems="flex-start">
      {buttons.map((btn, i) => (
        <Tooltip key={i} hasArrow label={btn.tooltip}>
          <Button
            colorScheme={color}
            variant="outline"
            size="sm"
            isDisabled={isDisabled}
            onClick={async () => {
              setSelectContent();
              const resultContent = await queryPrompt(btn.promptType, content);
              if (resultContent) {
                setResult({ title: btn.name, content: resultContent });
              }
            }}
          >
            {btn.name}
          </Button>
        </Tooltip>
      ))}
    </HStack>
  );
};

return (
	// ...
	<VStack alignItems="flex-start" w="100%">
	  {renderButtons(generateButtons, 'blue', content, false)}
		<Text fontSize="sm">For selection text: </Text>
		{/* chỉ enable các button khi content text được select */}
    {renderButtons(contentButtons, 'teal', selectedContent, !selectedContent )}
		
		{!!result.title && (
	    <VStack alignItems="flex-start">
	      <Heading size="md">{result.title}</Heading>
	      <pre style={{ whiteSpace: 'pre-wrap', fontFamily: 'inherit' }}>
	        {result.content}
	      </pre>
	    </VStack>
	  )}
	</VStack>
	// ...
)

Kết quả trả về từ API:

GPT API này có chi phí khá cao nên chúng ta có thể sử dụng cache để lưu lại kết quả các truy vấn trước đó.

Gọi API /api/prompt khi click các button trên để hiển thị kết quả gợi ý

const toast = useToast();

const [loadingPrompt, setLoadingPrompt] = useState(false);

const queryPrompt = async (promptType: string, content: string) => {
  setLoadingPrompt(true);
  const response = await fetch('/api/prompt', {
    method: 'POST',
    headers: {
      Accept: 'application/json',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ topicType, promptType, question, content }),
  });
  const data = await response.json();
  setLoadingPrompt(false);
  if (!response.ok) {
    toast({
      title: 'Error',
      description: data?.error?.message,
      status: 'error',
      duration: 9000,
      isClosable: true,
    });
    return '';
  }
  return (data.result || '').trim();
};

Chạy ứng dụng

npm run dev

Giao diện ứng dụng

Kết luận

Qua việc xây dựng ứng dụng này, hy vọng bạn đã nắm được cách để tích hợp AI vào ứng dụng của mình để phục vụ các use case khác như chatbot, gia sư dạy học, PT lên lịch tập,…

Hẹn gặp lại ở các bài viết sau!


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí