Hello all of you!
On the occasion of the new year, I would like to wish all readers of Viblo a happy and prosperous year, achieve much success in career and life!
Surely you all know ChatGPT, an AI bot that has emerged recently with the ability to chat and answer about almost any area of life. Although there are many controversies about the right and wrong of data, it is undeniable that the great power of this tool as well as AI in helping to increase human productivity in many different professions such as programming, marketing, …
In this article, we will use its API to write a simple application that supports users in learning English, and more specifically, optimizing IELTS Writing and Speaking essay writing.
And of course, a lot of the code in this app was written by ChatGPT itself
Since OpenAI has not opened publicly to ChatGPT’s own API, I will use the Text Completion API with the same generate text feature as ChatGPT.
You can refer here.
Features of this app include:
- From essay type: IELTS Writing task 2 and user-entered topics, the application provides suggestions, creates sample essays
- Correcting errors, suggesting sentences, explaining the meaning of words, etc., based on the text that the user entered and the essay title.
You can check the source code of the project here.
https://github.com/ngviethoang/ai-writing-assistant
Application demo.
Setting
Initialize NextJS project
1 2 | yarn create next <span class="token operator">-</span> app <span class="token operator">--</span> typescript |
Install libraries: OpenAI client, ChakraUI (UI framework)
1 2 3 | yarn add @chakra <span class="token operator">-</span> ui <span class="token operator">/</span> react @emotion <span class="token operator">/</span> react @emotion <span class="token operator">/</span> styled framer <span class="token operator">-</span> motion yarn add openai |
Register OpenAI API key
Log in to your OpenAI account at https://platform.openai.com/
Create API Secret Key
Create .env
file in project and save secret key
1 2 | OPENAI_API_KEY=[Nhập key đã tạo] |
Add this .env
file to the .gitignore
file to avoid revealing the key when committing code
Create a prompt to communicate with the API
To communicate with the Text Completion API, we need to use queries (prompt). This is an important step to get the exact output you want. The terminology in NLP is prompt engineering .
For example, a sample prompt to create a sample outline for the article according to the topic of the IELTS Writing task:
1 2 | 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] |
Here we can define parameters that can be passed from the UI:
actor
: an IELTS test taker with a band score of 8.0question
: IELTS Writing Task 2 questioncontent
: the text entered by the user
Create a prompt constructor for querying the API based on the actor
, question
, content
parameters.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | 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 '' } } |
Create API handler in NextJS
To create an API handler that handles query results from Text Completion, create an API route in the pages/api/prompt.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | 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.', }, }); } } } |
Parameters in Text Completion API used
model
: use the latest and most powerful text-davinci-003 of the GPT-3 modelsprompt
: the query built in the previous steptemperature
: determines the stability of the results, the higher the temperature, the more diverse the results will bemax_tokens
: maximum number of tokens returned, can limit the number of tokens returned per prompt to reduce costs
Interface code
Next is the frontend for the application, I will write basic components like
- Text editor to enter questions, article content
- Buttons are used to call the API corresponding to functions such as creating article outlines, creating sample articles, correcting typos, comments, etc.
- Component that displays the results returned from the API
Create components and layouts for pages using ChakraUI
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | 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 buttons to generate prompts and suggestions from the API
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | 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> // ... ) |
Returns from the API:
This GPT API has a high cost, so we can use cache to save the results of previous queries.
Call API /api/prompt
when clicking the above buttons to display the suggested result
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | 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(); }; |
Run the app
1 2 | npm run dev |
Application interface
Conclude
Through building this application, hopefully you have grasped how to integrate AI into your application to serve other use cases such as chatbots, tutors, PTs to schedule exercises, etc.
See you in the next posts!