Quiz Generator - Simple Quiz Platform where AI Creates Quizzes made with AWS Amplify

Article for AWS Amplify on 2023 July's Hackathon Submission

·

10 min read

Quiz Generator - Simple Quiz Platform where AI Creates Quizzes made with AWS Amplify

Introduction

Quiz Generator is the Simple Quiz Platform where AI Creates Quizzes.

You can set the subject and level and the AI will create the quiz for you. When you answer, the correct or incorrect answer and an explanation will be displayed. You can also see the ranking of the percentage of correct answers and the number of quizzes answered by each user.

Check out Quiz Generator - Simple Quiz Platform where AI Creates Quizzes

1. The Problem

There are many quiz apps out there, but most of them require you to type in the quiz yourself, which is not practical for easy play. few of those with AI auto-generation are easy to use, and none of them support multiple languages.

2. The Solution and Motivation

We decided to develop Quiz Generator because we thought it would be easier and more enjoyable if there was an application that focused on automatic quiz generation by AI, and if it supported multiple languages, people from many countries could enjoy it.

3. Demo and GitHub Repository

Demo: (https://quiz-generator.tanosugi.com)

GitHub Repository: (https://github.com/tanosugi/quizgenerator)

4. Features of Quiz Generator

4.1 Quiz Generation

Set the subject and level and the AI will create the quiz for you.

4.2 Quiz Answering Function

You can answer the quiz. When you answer, correct or incorrect answers and explanations will be displayed.

4.3 Ranking Function

You can also see the ranking of the percentage of correct answers and the number of quizzes answered by each user.

4.4 Multi-Language Support

English, Japanese, Chinese, and Spanish, Portuguese are supported. Not only will the menu be displayed in the selected language, but the quiz, answers, and explanations will also be displayed in that language!

5. Amplify Features I used in Quiz Generator

5.1 Figma to react (UI Library) on Amplify Studio

First, I created the design in Figma and then coded it with the Figma to React functionality. all components, including Nav bar, were generated from Figma.

In another previous project, we also used Figma's prototype feature, but we omitted it this time because we can quickly turn it into a React web app.

Link to Figma file

5.2 Data modeling on Amplify Studio

Next, I created the necessary data model by looking at the design, which is very convenient because you can create it in the GUI while selecting the type. So you need to work carefully and keep the number of modifications to a minimum.

5.3 UI Library on Amplify Studio

Next, connect the component to the data model. You can use the override function in code, but it is more efficient to work in GUI on Amplify Studio. Create a collection as well. The ranking table is created using this function.

5.4 Authentication on Amplify Studio

Introducing Authentication was extremely easy. You can click on Amplify studio GUI and paste code from the tutorial. It was a little complicated to use Google login through OAuth 2. The following three steps are necessary.

  1. add Google Login on Amplify Studio

  2. add credentials on GCP

  3. Copy redirect URL from Amplify Studio to GCP

  4. Copy Web Client ID and Web Client Secret from GCP to Amplify

  5. set environment variable on AWS management console. Web Client from GCP to AMPLIFY_GOOGLE_CLIENT_ID and Web Client Secret from GCP to AMPLIFY_GOOGLE_CLIENT_SECRET Please refer to the following screenshots.

5.5 Amplify Data Store

The data designed in Data modeling is reflected in the components in the UI Library as much as possible, but some of it needed to be processed, so we wrote the code. It is quite easy to query data in Data Store and apply it to components in overrides.

const fetchQuiz = async () => {
    const resp = await DataStore.query(Quiz, (c) => c.quizsetID.eq(quizId));
    console.log("resp:", resp);
    setQuizs(resp);
    setQuizLength(resp.length);
  };
 <QuizItemSolveView
          overrides={{
            time: { children: `${t("sec")} : ${time}` },
            problems: { children: `${currentQuizIndex + 1} / ${quizLength}` },
            problem: { children: quizzes[currentQuizIndex]?.question },
            "Choice-a": {
              overrides: {
                "choice-text": {
                  children: (currentQuiz?.choiceText && currentQuiz?.choiceText[0]) || "",
                },
              },
              status: answerChosen == "Choice-a" ? (isCorrect ? "correct" : "incorrect") : "a",
              onClick: () => {
                answer(0);
                setAnswerChosen("Choice-a");
              },
            },
// -------
// some other code
// -------
            explanation: { children: explanation },
            Button: Object.assign(
              { onClick: () => onClickNextButton() },
              currentQuizIndex + 1 == quizLength
                ? { children: t("Finish"), variation: "error" }
                : { children: t("Next") }
            ),
          }}
        />
      )}

5.6 Amplify hosting

After confirming that it is in hand, the web app can be published very easily, as it will be built and accessible via https when the github repository is connected.

5.7 Amplify Domain management

After publishing with Amplify hosting, you can tie it to your desired URL. If your domain is registered with route53 or a subdomain of route53, it only takes a few clicks and 10 minutes of waiting for the connection to be completed.

5.8 Form Builder (React)

If you are entering or updating data related to the data model, Form Builder allows you to create an input form in a matter of minutes. In this case, we used it for a screen where users can enter what kind of quiz they want to generate automatically.

5.9 Environment variables

5.10 Data Content Management on Amplify Studio

After creating the Collection using the UI Library, we used the data created by DataManager's automatic data generation feature to make sure it was working well. I also used this feature to verify that the quiz was saved as expected.

6. Other Technology Used in Quiz Generator

6.1 ChatGPT API including functions calling

The ChatGPT API was used to automatically generate the quiz.

We asked ChatGPT "please provide quizzes with the following constraints. I wanted to treat it as data, so I used a feature called function calling, which was just released in June. This function is originally designed to call Web API via ChatGPT API, but it can also be used to return Json format.

const generateQuizzes = ({
  subject,
  level,
  numberOfQuiz,
  lng,
}: {
  subject: string | null | undefined;
  level: string | null | undefined;
  numberOfQuiz: number | null | undefined;
  lng: string;
}) => {
  const system_content = `
You are a good quiz questioner and will be randomly quizzed on a given level and subject.
You try not to give you the same quiz as the previous one.
`;
  const user_content = `
please provide quizzes with the following constraint.

# constraint must you have to follow.
subject: ${subject}
level: ${level}
number of quiz: ${numberOfQuiz}
number of choices for one quiz: 3
language for questions, answer, and explanations: ${lng}
`;
  const function_for_chatgptapi = {
    name: "i_am_json",
    description:
      "return quizzes based on subject and level, options, ansers, and explanations in json format",
    parameters: {
      type: "object",
      properties: {
        quizzes: {
          type: "array",
          items: {
            type: "object",
            properties: {
              question: { type: "string", description: "question based on subject and level" },
              choices: {
                type: "array",
                description: "choices for user to choose. only one choice is correct.",
                items: {
                  type: "object",
                  properties: {
                    choiceText: { type: "string", description: "option for user to choose" },
                    isCorrect: {
                      type: "boolean",
                      description:
                        "true if the option is correct, false if not. only one choice is correct",
                    },
                    explanation: {
                      type: "string",
                      description:
                        "explanation for the option. only one choice is correct. explanation is in three sentences with details",
                    },
                  },
                },
              },
            },
          },
        },
      },
    },
  };
  return {
    system_content,
    user_content,
    function_for_chatgptapi,
  };
};
export default generateQuizzes;
import axios from "axios";

const API_URL = "https://api.openai.com/v1/";
const MODEL = "gpt-3.5-turbo-0613";
const API_KEY = process.env.NEXT_PUBLIC_KEY;

const chat = async ({
  system_content,
  user_content,
  function_for_chatgptapi,
}: {
  system_content: string;
  user_content: string;
  function_for_chatgptapi: Object;
}) => {
  try {
    const response = await axios.post(
      `${API_URL}chat/completions`,
      {
        model: MODEL,
        messages: [
          // { role: "system", content: system_content },
          { role: "user", content: user_content },
        ],
        functions: [function_for_chatgptapi],
        function_call: "auto",
      },
      {
        headers: { "Content-Type": "application/json", Authorization: `Bearer ${API_KEY}` },
      }
    );
    return response?.data?.choices[0]?.message?.function_call?.arguments;
  } catch (error) {
    console.error(error);
    return null;
  }
};

export default chat;

6.2 Next.js App Router

For the framework, we used Nextjs in addition to React, and the latest App router which became Stable in May. The folder structure is a bit unique, but I was able to learn it through the development of the Quiz Generator.

6.3 i18-next, i18next-parser, json-autotranslate

Multilingual support was provided by i18next. The text in the component was also overridden and rewritten in the code using the t("") function obtained from i18next's useTranslation hooks. By doing so, the i18next-parser can be used to

yarn run i18next 'src/**/*. {tx,tsx}'

all text enclosed in t("") was retrieved in Json. For the extracted json

yarn json-autotranslate --input src/locales/ --config . /gcp-service-account.json --delete-unused-strings --type natural --source-language en

all text is automatically translated, and multilingualization and translation can be completed in a very short time. Even after more text is added, the same process takes 30 seconds and adds the text in each language to all language pages, making development very fast.

 <HeroSmallView
          overrides={{
            "Simple Quiz Platform where AI Creates Quizzes": {
              children: t("Simple Quiz Platform where AI Creates Quizzes"),
            },
            "Set the subject and level and the AI will create the quiz for you. When you answer, the correct or incorrect answer and an explanation will be displayed. You can also see the ranking of the percentage of correct answers and the number of quizzes answered by each user.":
              {
                children: t(
                  "Set the subject and level and the AI will create the quiz for you. When you answer, the correct or incorrect answer and an explanation will be displayed. You can also see the ranking of the percentage of correct answers and the number of quizzes answered by each user."
                ),
              },
            Button: {
              onClick: () => router.push(`/${lng}/quiz-generate`),
              children: t("Sign Up for Free"),
            },
          }}
        />

6.4 Sentry, LogRocket, Google Analytics

export default function RootLayout({
  children,
  params: { lng },
}: {
  children: ReactNode;
  params: {
    lng: string;
  };
}) {
  return (
    <html lang={lng} dir={dir(lng)}>
      <head />
      <body>
        <Providers>
          <GoogleAnalytics />
          <Header lng={lng} />
          {children}
        </Providers>
      </body>
    </html>
  );
}
Amplify.configure(config);

const Providers = ({ children }: { children: React.ReactNode }) => {
  useEffect(() => {
    Sentry.init({
      dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
      // integrations: [new BrowserTracing()],
      // We recommend adjusting this value in production, or using tracesSampler
      // for finer control
      tracesSampleRate: 1.0,
    });
    if (process.env.NODE_ENV === "production") {
      LogRocket.init(process.env.NEXT_PUBLIC_LOGROCKET_ID || "");
    }
  }, []);

  return (
    <Sentry.ErrorBoundary>
      <AmplifyProvider theme={studioTheme}>
        <Authenticator.Provider>{children}</Authenticator.Provider>
      </AmplifyProvider>
    </Sentry.ErrorBoundary>
  );
};

6.5 react-modal, react-spinners


const ModalMenue: FC<{
  isOpen: boolean;
  setModalToOpen: React.Dispatch<React.SetStateAction<boolean>>;
  lng: string;
}> = ({ isOpen, setModalToOpen, lng }) => {
  const { t } = useTranslationClient(lng);
  const router = useRouter();
  return (
    <div>
      <Modal isOpen={isOpen} style={modalStyle}>
        <Center>
          <MenuView
            overrides={{
              "close-circle": {
                onClick: () => setModalToOpen(false),
                className: "custom-btn",
              },
              "home-button": {
                onClick: () => {
                  router.push(`/${lng}/`);
                  setModalToOpen(false);
                },
                className: "custom-btn",
              },
              "quiz-generate": {
                onClick: () => {
                  router.push(`/${lng}/quiz-generate`);
                  setModalToOpen(false);
                },
                className: "custom-btn",
              },
              "quiz-list": {
                onClick: () => {
                  router.push(`/${lng}/quiz-list`);
                  setModalToOpen(false);
                },
                className: "custom-btn",
              },
              "user-ranking": {
                onClick: () => {
                  router.push(`/${lng}/user-ranking`);
                  setModalToOpen(false);
                },
                className: "custom-btn",
              },
              signout: {
                onClick: async () => {
                  await DataStore.clear();
                  await Auth.signOut();
                  await setModalToOpen(false);
                  router.push(`/${lng}/`);
                },
                className: "custom-btn",
              },
              Home: { children: t("Home") },
              "Generate Quiz": { children: t("Generate Quiz") },
              "Quiz List": { children: t("Quiz List") },
              "User Ranking": { children: t("User Ranking") },
              "Sign Out": { children: t("Sign Out") },
            }}
          />
        </Center>
      </Modal>
    </div>
  );
};

7. About Me

My name is Tanosugi I am from Japan and living in Japan. I worked part-time using Visual C++ when I was a student long years ago, but now I have a non-engineering job. 3-4 years ago, I read a book published in Japan, "Let's start development by yourself!" and wanted to practice it, so I resumed coding as a hobby. After studying React, Django, AWS, etc. at Udemy, I have been making various web services by myself. Some of the services are for my kids.

8. Conclusion

I coded every day after the kids went to bed, so that's a total of 30-40 hours in 1 weeks, including article writing. If it is an intensive hackathon, it takes 2-3 days.

I was able to create the application, I could join the hackathon, and I was able to confirm the usefulness of Amplify, so it was a very meaningful time for me.

Did you find this article valuable?

Support tanosugi's blog by becoming a sponsor. Any amount is appreciated!