AWS Amplifyを使ったオンラインハッカソンにクイズ生成アプリを作って入賞、$300のAWSクレジットをゲットした話

## オンラインハッカソンへの応募
Hashnodeという海外のQiitaやZennのような開発者向けブログ投稿サイトが主催で、AWSのAmplifyを使うことを条件にしたハッカソンが7月末締め切りで開催されたので、クイズ生成Webアプリを作って応募してみました。

### 今回結果
20位入賞で$300のAWSクレジットをゲットしました！　（順番はつかず入賞だけ発表される仕組みのようです）

https://townhall.hashnode.com/aws-amplify-winners

### 前回
2022年の9月に同様のハッカソンで5位入賞して$1,000ゲットしましたが、今回は20位入賞でした。

https://qiita.com/tanosugi/items/a4bb904ff98b3d6140aa



### 開催要項

https://hashnode.com/hackathons/aws-amplify-2023


### 提出記事

https://tanosugi.hashnode.dev/quiz-generator-simple-quiz-platform-where-ai-creates-quizzes-made-with-aws-amplify

---
## 提出記事の日本語訳
ブログをアプリの機能や開発についてのブログにタグをつけて公開することで提出する仕組みです。
以下応募の際に提出した内容の日本語訳です。（英語で書いたものをDeepLで和訳して少し直しましたが、不自然な表現残っていてもご容赦ください。）

---

### はじめに

[Quiz Generator]( https://quiz-generator.tanosugi.com)は、AIがクイズを作成するシンプルなクイズプラットフォームです。

あなたは、件名とレベルを設定することができ、AIはあなたのためのクイズを作成します。回答すると、正解・不正解と解説が表示されます。また、正解率や各ユーザーが回答したクイズ数のランキングを見ることができます。

[クイズジェネレータを試す - AIがクイズを作成するシンプルなクイズプラットフォーム]( https://quiz-generator.tanosugi.com)

## 1. 問題

多くのクイズアプリがありますが、そのほとんどが自分でクイズを入力する必要があり、手軽に遊ぶには現実的ではありません。AIが自動生成するものは使い勝手が悪く、多言語に対応しているものもありません。

## 2. ソリューションと動機

AIによるクイズの自動生成に特化したアプリがあれば、もっと手軽に楽しめるのではないか、多言語に対応していれば、いろいろな国の人が楽しめるのではないかと考え、Quiz Generatorを開発することにしました。

## 3. デモとGitHubリポジトリ

デモ (https://quiz-generator.tanosugi.com)

GitHubリポジトリ: (https://github.com/tanosugi/quizgenerator)

## 4. クイズジェネレータの機能

### 4.1 クイズ生成

テーマとレベルを設定すると、AIがクイズを作成します。

![Image]( https://cdn.hashnode.com/res/hashnode/image/upload/v1690469294397/f4ffb418-6ed2-41d5-aaaa-7f8500a4c39f.png)

### 4.2 クイズ解答機能

あなたはクイズに答えることができます。回答すると、正解・不正解と解説が表示されます。

![Image](https://cdn.hashnode.com/res/hashnode/image/upload/v1690472094924/307def1f-dffb-43de-bfbb-c15fbe0e55bc.png)
![Image](https://cdn.hashnode.com/res/hashnode/image/upload/v1690472116749/7ed483bd-ccf4-4326-8ace-1c7ef5f27127.png)

### 4.3 ランキング機能
また、各ユーザーの正解率やクイズ解答数のランキングを見ることができます。

![Image](https://cdn.hashnode.com/res/hashnode/image/upload/v1690472147189/05cf60d2-898f-4160-8300-eb8d5599fb14.png)

### 4.4 多言語対応

英語、日本語、中国語、スペイン語、ポルトガル語に対応しています。選択した言語でメニューが表示されるだけでなく、クイズ、解答、解説もその言語で表示されます！

![Image](https://cdn.hashnode.com/res/hashnode/image/upload/v1690472200605/9833de36-a107-4eed-b2b9-7ea05e7ea6e1.png)
![Image](https://cdn.hashnode.com/res/hashnode/image/upload/v1690472184028/e373e98c-73ea-4587-be58-42e677865438.png)
![Image](https://cdn.hashnode.com/res/hashnode/image/upload/v1690472228171/f48dd9bd-80cb-4baa-8a10-8e9681e4402e.png)

## 5. クイズジェネレータで使用した機能を増幅する

### 5.1 Amplify Studio上のFigmaからreact（UIライブラリ）への変換

まず、Figmaでデザインを作成し、Figma to Reactの機能を使ってコーディングしました。ナビバーを含むすべてのコンポーネントはFigmaから生成されました。

以前の別のプロジェクトでは、Figmaのプロトタイプ機能も使いましたが、今回はReactのWebアプリにすぐに変えられるので省略しました。

[Figmaファイルへのリンク](https://www.figma.com/file/392PhsmQeQwbLqf0HMKbWt/guiz-generator?type=design&node-id=3846%3A3798&mode=design&t=J3VfQLurFraTnG2p-1)

![Image](https://cdn.hashnode.com/res/hashnode/image/upload/v1690473806679/8f160b6a-66fa-43ec-9162-1e15e2b30ed8.png)

### 5.2 Amplify Studioでのデータモデリング

次に、設計図を見ながら必要なデータモデルを作成するが、GUI上で型を選択しながら作成できるので非常に便利である。そのため、慎重に作業し、修正回数を最小限に抑える必要がある。

![Image](https://cdn.hashnode.com/res/hashnode/image/upload/v1690473893682/e154c4db-24ca-49a0-9c15-01e3e4967c89.png)

### 5.3 Amplify Studio上のUIライブラリ

次に、コンポーネントをデータモデルに接続します。コードでオーバーライド関数を使用することもできますが、Amplify Studio上のGUIで作業する方が効率的です。コレクションも作成します。ランキング・テーブルは、この関数を使用して作成します。

![Image](https://cdn.hashnode.com/res/hashnode/image/upload/v1690474061245/f4efba4e-b51d-4441-b9f6-f2a1981cc7b2.png)
![Image](https://cdn.hashnode.com/res/hashnode/image/upload/v1690473997492/2f4295ec-96d2-4279-99cf-68b067cf1214.png)

### 5.4 Amplify Studio での認証

認証の導入は非常に簡単だった。Amplify studioのGUIをクリックして、チュートリアルのコードを貼り付ければいい。OAuth 2を使ったGoogleログインを使うには、以下の3つのステップが必要だ。

1. Amplify StudioにGoogleログインを追加する。

2. GCPに認証情報を追加する

3. Amplify StudioからGCPへのリダイレクトURLをコピーする。

4. WebクライアントIDとWebクライアントシークレットをGCPからAmplifyにコピーする

5. AWSマネジメントコンソールで環境変数を設定 GCPからWeb ClientをAMPLIFY_GOOGLE_CLIENT_IDに、GCPからWeb Client SecretをAMPLIFY_GOOGLE_CLIENT_SECRETにコピー 以下のスクリーンショットを参照してください。

![697adffe-91a4-43f9-af13-b6fa743cd55c.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/639092/fa4cec2f-fa7e-ef95-bf46-b93969e670f1.png)


### 5.5 Amplify Data Store

Dataモデリングで設計したデータは、UIライブラリのコンポーネントに可能な限り反映されるが、一部は処理する必要があるため、コードを記述した。データ・ストアでデータを照会し、オーバーライドでコンポーネントに適用するのは非常に簡単だ。
```typescript
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: () => { {答え(0); {答え(0); {答え(0)
                answer(0)；
                setAnswerChosen("Choice-a")；
              },
            },
// -------
// 他のコード
// -------
            説明： { children: explanation }、
            Button： Object.assign(
              { onClick: () => onClickNextButton() }、
              currentQuizIndex + 1 == quizLength
                ? { children: t("Finish"), variation： 「エラー｝
                : { children: t("Next") } : { children: t("Next") } )
            ),
          }}
        />
      )}
```
### 5.6 Amplifyホスティング

手元にあることを確認した後、githubリポジトリに接続するとhttpsでビルドされアクセスできるようになるので、非常に簡単にWebアプリを公開することができます。

![Image](https://cdn.hashnode.com/res/hashnode/image/upload/v1690474266648/b0464b72-ebd4-4c7e-81f5-e2fbc30deb70.png)

### 5.7 Amplifyドメインの管理

Amplifyホスティングで公開した後、希望のURLに結びつけることができる。あなたのドメインがroute53またはroute53のサブドメインに登録されている場合、数回のクリックと接続が完了するまで10分待つだけです。

![Image](https://cdn.hashnode.com/res/hashnode/image/upload/v1690474310247/a2d3fd38-9e7e-4f81-81c8-267baed85b15.png)
![Image](https://cdn.hashnode.com/res/hashnode/image/upload/v1690474285559/90ea24f5-8639-418c-94f8-b5d0eafa2155.png)

### 5.8 フォームビルダー（React）

データモデルに関連するデータを入力したり更新したりする場合、フォームビルダーを使えば数分で入力フォームを作成できます。今回は、ユーザーがどのようなクイズを自動生成したいかを入力する画面に使用しました。

![Image](https://cdn.hashnode.com/res/hashnode/image/upload/v1690474370805/2efde1a3-b48e-4a39-a8b9-f73114e6f8b7.png)

### 5.9 環境変数

![Image](https://cdn.hashnode.com/res/hashnode/image/upload/v1690474406744/5e156127-50a5-444b-8e8a-c960ea46281c.png)

### 5.10 Amplify Studioでのデータコンテンツ管理

UIライブラリを使用してCollectionを作成した後、DataManagerの自動データ生成機能で作成したデータを使用して、正常に動作していることを確認しました。また、この機能を使って、クイズが期待通りに保存されていることを確認しました。

![Image](https://cdn.hashnode.com/res/hashnode/image/upload/v1690496918110/d2912c1a-d9bc-482d-8fd6-d30e2be46c28.png)

## 6. クイズジェネレータで使用したその他の技術

### 6.1 関数呼び出しを含むChatGPT API

ChatGPT APIはクイズを自動生成するために使用されました。

ChatGPTには「以下の制約でクイズを提供してください」とお願いしました。データとして扱いたかったので、6月にリリースされたばかりの関数呼び出しという機能を使いました。この関数は本来ChatGPT API経由でWeb APIを呼び出すためのものですが、Json形式で返すこともできます。

```typescript
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

フレームワークは、Reactに加えてNextjsを使い、5月にStableになった最新のApp Routerを使った。フォルダ構成が少し独特ですが、Quiz Generatorの開発を通じて学ぶことができました。

![Image](https://cdn.hashnode.com/res/hashnode/image/upload/v1690474447805/b73312c9-0810-466a-9708-1955c318b7b9.png)

### 6.3 i18-next、i18-next-parser、json-autotranslate

多言語サポートはi18nextによって提供された。また、i18nextのuseTranslationフックから取得したt("")関数を使用して、コンポーネント内のテキストをオーバーライドし、コード内で書き換えました。こうすることで、i18next-parserは次のように使用できます。

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

t("")で囲まれたすべてのテキストがJsonで取り出されました。取り出したjsonに対して

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

を指定すると、すべてのテキストが自動的に翻訳され、多言語化と翻訳が短時間で完了する。さらにテキストが追加されても、同じ処理で30秒で各言語のテキストがすべての言語ページに追加されるので、開発は非常に高速です。

```typescript
 <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"),
            },
          }}
        />
```

![Image](https://cdn.hashnode.com/res/hashnode/image/upload/v1690474581667/4f1ca71c-5601-4974-b46d-49b1c3521942.png)

### 6.4 Sentry, LogRocket, Google Analytics

```typescript
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
```typescript
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. 私について

日本出身のtanosugiです。昔、学生時代にVisual C++を使ってアルバイトをしていましたが、今はエンジニアではない仕事をしています。3～4年前に日本で出版された「自分ではじめる開発！」という本を読んで、実践してみたくなり、趣味でコーディングを再開しました。UdemyでReactやDjango、AWSなどを勉強した後、様々なWebサービスを自作しています。子供のためのサービスもあります。

## 8. まとめ

毎日子供が寝た後にコーディングしていたので、記事執筆も含めると1週間で合計30～40時間。集中ハッカソンなら2～3日。
アプリも作れたし、ハッカソンにも参加できたし、Amplifyの有用性も再確認できたし、とても有意義な時間でした。