My journey

By trace

Rec & Triv

 ·

17 min read

Cover Image for Rec & Triv

%[https://www.youtube.com/watch?v=IKkmUB3J5bo&feature=youtu.be]

What is Rec & Triv?

Rec & Triv is a web application that can be use to get movie recommendations and play movie trivia from the selected movie. It uses modus as the backend hosted in Hypermode and next.js 14 as the frontend. The movie data for the application is through tmdb API and Wikipedia.

For movie recommendations, I have used Modus Collections.

To generate trivia questions I have used AI model hosted in Hypermode. For this application, I am using Meta Llama 3.1 8B.

How to use Rec & Triv?

You can access Rec & Triv at the deployed link here (subject to my bank balance) and below I have added a step-by-step guide on how to run it locally.

Once you visit the deployed link you should see the home page

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1732992391904/200679a6-857a-4a78-b982-844b461b11a1.png align="center")

On the home page, you will have 2 options to go forward,

  1. Get recommendations (Left as seen in the image above) based on your input. This is made using the modus collection.

  2. Play movie trivia (The right option as seen in the image above).

Recommendation

If you click on the left option you will be redirected to the recommendation route

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1732991288919/9a08c681-3341-4d27-b4ff-338a827da2bc.png align="center")

Here you can type a description based on your input and it will recommend movies from the collection

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1732991373288/1a04eaa9-32cb-4a2f-8ba3-f169a3b364f5.png align="center")

If you are logged in you will see the Read More button else Login to Read More.

Play Trivia option

If you click on the right option, Play Trivia, you will be redirected to the dashboard.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1732992717979/0ab62e7e-0103-40d1-9921-058a43063724.png align="center")

Now you can choose from the first 20 top-rated films or search for a specific movie by clicking on the Search Movie Button.

Clicking on the movie and also clicking on the Read More button from the recommendation list will redirect you to the movie id route.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1732991545985/2f8907a5-7d62-46c5-9ad6-34d305f3961b.png align="center")

Here more information is shown about the movie, the information is through the tmdb API.

Now if you want to play trivia click on the Start Trivia Button. Below I will explain how the Start Trivia button functionality takes place in detail.

![An image from the trivia game](https://cdn.hashnode.com/res/hashnode/image/upload/v1732993723002/d9a7775a-e594-413c-944e-d79b0b202680.png align="center")

The above is from a generated trivia

From the sidebar, you can also look at your stats and the past questions you have answered.

Why did I choose this project?

I was late joining the hackathon and modus and hypermode were very new for me I wanted to create something that would be fun and also provide a good learning experience. Considering those reasons I decided to make an AI-powered trivia game and recommendation part as just the cherry on top once I learned about collections.

What is Modus?

Modus is a multi-language open-source, serverless framework for building functions and APIs, powered by WebAssembly. Currently, the supported languages are Go and Assembly Script (very similar to Typescript).

Setting up a modus application

The following are the steps I took:

  1. install hypermode cli through VS code terminal using the following command npm install -g @hypermode/modus-cli

  2. Then used modus new command to create a new project

    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1733007489283/91f40633-45d3-46ae-88c7-c108f949133d.png align="center")

    1. Choose AssemblyScript as the SDK

    2. For “Pick a name for your app” I used the auto-generated name

    3. Choose no for Initialize a git repository?

    4. Then Yes on Continue?

    5. ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1733007757560/4f7bb616-4c28-498d-a9a1-cecffd495683.png align="center")

That all my whole backend server is now ready to use.

To run the server all I have to do is cd inside the newly created folder and then run modus dev in the terminal

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1733007905095/5464f187-d35b-437b-a7f9-9a6e5d6c656b.png align="center")

Modus Specific Terms:

This was the first time I had used Modus; initially, there were a few terms that were new to me and are Modus specific.

Manifest: It means the modus.json file at the root. Every time the docs and this blog post mention the word manifest it is referencing to this file. In this file, we need to define the resources our app can access.

Connnections: With modus we cannot just directly call external endpoints. To make a successful call you will need to first define it under connections in your manifest. Currently they support 3 types of connection http, postgres and dgraph. In my application I have used http connection to request information from tmdb and wikipedia api and postgres connection to store info to my database.

Example of a http connection to tmdb

"tmdb": {
      "type": "http",
      "baseUrl": "https://api.themoviedb.org/3/",
      "headers": {
        "Authorization": "Bearer {{API_TOKEN}}"
      }
}

Here “tmdb” is the name of the connection and using Bearer token for authorization. In connection either baseUrl or endpoint is required.

Working with secrets: In the above code snippet you can see how I have mentioned the API_TOKEN. That’s the token that got generated by tmdb. It is not recommended to hardcode the token instead of {{API_TOKEN}} so we need to store it in our .env.local file.

It is important to know that there is a specific naming convention. Otherwise it will not work. The naming convention is
MODUS_<CONNECTION NAME>_<PLACEHOLDER>
So for the above in my .env.local file I am storing the {{API_TOKEN}} value as MODUS_TMDB_API_TOKEN=”Value of my API token”

Models: Under models we define the AI model we use. It can be from Hypermode or any third-party provider. While using an AI model from third-party provider we need to add the external endpoint in connections. Below is an example of how to call a model from Together ai

 "connections": {
    "together": {
      "type": "http",
      "baseUrl": "https://api.together.xyz/",
      "headers": {
        "Authorization": "Bearer {{AI_API_TOKEN}}"
      }
    }
  },
  "models": {
    "llama": {
      "sourceModel": "meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo",
      "connection": "together",
      "path": "v1/chat/completions"
    }
  }

To call an AI model hosted on Hypermode you do not need to define a connection just need to mention the name of the model to call with the provider as hugging-face and the connection as hypermode.

"models": {
    "text-generator": {
      "sourceModel": "meta-llama/Meta-Llama-3.1-8B-Instruct",
      "provider": "hugging-face",
      "connection": "hypermode"
    }
}

Collections: A collection is a structured storage that organizes and stores textual data and associated metadata. Collections enable sophisticated search, retrieval, and classification tasks using vector embeddings. Very cool thing I like it.

Did I face any challenges while building the app?

Since it was the first time I had used Modus and Hypermode, I faced some issues.

  1. In my application, the search movie results come from tmdb so to set it by I need to define it under connection in my manifest. I did add a connection to the tmdb API with the correct token value but my request was failing. Why? Because I did not use the required naming convention.

  2. Using both the CLI and Hypermode dashboard, I could not deploy my application. I told my issue to the team and they were able to guide me through and even found the root cause of the issue. Very helpful.

  3. Initially, I was using stack-auth for authentication. Everything was working fine locally, I then tried to deploy my application on vercel boom Infinite loop so I had to change the whole auth service. (It is a very recent issue that is happening with stack-auth)

  4. After deploying I started getting errors while searching for a movie. The reason was that I was trying to call the endpoint directly from the client component because of which auth was failing. In my application, I was directly calling in multiple places so I had to make a lot of changes the day before the deadline.

  5. While testing the application within the last 24hrs, I ran into a problem communicating with the database. Initially, I was using a Postgres db hosted in Supabase but then migrated to neon.

How is Modus used in this application?

In this application, modus is the backbone since the whole backend is written out there.

I have implemented the following functionality from modus:

  1. AI model to generate trivia questions based on the movie data provided.

  2. HTTP connection to make requests to tmdb and Wikipedia to get movie information.

  3. Postgres connection to communicate with my Postgres database.

  4. Collections to get movie recommendations based on user input.

Hosting the modus app in Hypermode

  1. Create a repo for the application on Github.

  2. Create a project in Hypermode through their dashboard. For this I am naming is example-project

    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1733008148124/d7622293-eb7a-481b-aef7-34b06e34d9d0.png align="center")

Then follow the steps to connect it with the dashboard.

Initially when I tried to do it, I was not successful but with the help provided by the team I was able to solve the issue.

Then every time I commit something, it automatically gets deployed as long build is not failing.

Behind the scenes

Now I will be explaining how some of the important functionalities are working.

Start Trivia Button

When a user clicks on Start Trivia the following function gets triggered.

  
  const handleTriviaClick = async () => {
    setLoading(true);
    setButtonText('AI Generating Questions');
    const generateTriviaResponse = await generateTrivia({
      prompt: `Here is the content of the movie: ${movieDataAsString}`,
    });
    toast.success('Trivia questions generated successfully!');
    toast.success('Questions Generated');
    setButtonText('Creating Game. Hold Tight!');
    const createGame = await sendQuestionsToBackend(
      generateTriviaResponse.data,
    );
    console.log('CREATE GAME RESPONSE', createGame);
  };

const sendQuestionsToBackend = async (questions: any) => {
    try {
      const cleanedQuestions = questions.generateTrivia.map(
        ({ __typename, ...q }: { __typename?: string }) => q,
      );
      const payload = {
        movieId: movieId as string,
        movieTitle: data.movieById.title as string,
        questions: cleanedQuestions,
        clerkUserId: clerkUserId as string,
      };

      const gameCreateResponse = await createGameAndInsertQuestions({
        payload,
      });

      if (gameCreateResponse.data) {
        toast.success(
          `Game created successfully! Game ID: ${gameCreateResponse.data.createGameAndInsertQuestions}`,
        );
        router.push(
          `/dashboard/movie/${movieId}/trivia/${gameCreateResponse.data.createGameAndInsertQuestions}`,
        );
      } else {
        toast.error('Failed to create game.');
      }
    } catch (error) {
      toast.error('Error saving game and trivia questions.');
    }
  };

When you click on the button, a trivia game gets generated but what happens under the hood is

  1. The handleTriviaClick function is triggered when you click the button.

  2. It calls the generateTrivia function on the backend, passing the movie data as a prompt.

  3. This request includes the relevant information needed to generate trivia questions.

  4. The LLM (meta 3.1 8B from Hypermode) processes those information and sends the generated response back.

  5. The questions are then checked if it is in the expected format. After verifying, the verified questions are sent back again to the backend to store it in the database.

This back-and-forth step was required for me since initially, the llama 3 8B model was generating a lot of questions in the wrong format.

  1. Once the questions are successfully inserted in the database, the game id is sent back to the frontend as a response and the user gets redirected to the trivia game.

Modus Function to generate questions with LLM model hosted in Hypermode

export function generateTrivia(prompt: string): TriviaQuestion[] {
  const model = models.getModel<OpenAIChatModel>("text-generator");

  const systemInstruction = `You are a professional trivia question generator. Your task is to create engaging, accurate, and well-crafted trivia questions with multiple-choice answers from the provided content.
  REQUIREMENTS:
  1. Generate exactly 2 (two) trivia questions
  2. Question must be directly based on the provided content
  3. Include a mix of difficulties (easy, medium, hard)
  4. Cover different aspects of the content
  5. Avoid obvious or superficial questions
  6. Questions should be in a conversational tone
  7. Questions should be in English
  8. Majority of the questions should be from the plot
  9. Provide 4 possible answer choices for each question, including 1 correct answer and 3 plausible distractions

  QUESTION GUIDELINES:
  - Make questions specific and unambiguous
  - Ensure answers are factually correct and verifiable from the source
  - Include brief explanations for the correct answers
  - Categorize questions (e.g., Plot, Characters, Production, History, Technical)
  - Vary question types (who, what, when, where, why, how)

  FORMAT:
  Return only valid JSON matching this structure:
  {
    "questions": [
      {
        "question": "Clear, specific question text",
        "options": ["Option 1", "Option 2", "Option 3", "Option 4"],
        "answer": "Correct option text",
        "difficulty": "easy|medium|hard",
        "category": "Category name",
      }
    ]
  }

  IMPORTANT:
  - Question must include 4 answer options
  - DO NOT INCLUDE ANY EXTRA TEXT OR COMMENTS
  - Ensure answers are direct and concise
  - Do not create questions about information not present in the source
  - Ensure distractors are plausible and relevant`;

  const input = model.createInput([
    new SystemMessage(systemInstruction),
    new UserMessage(`Generate trivia questions from this content: ${prompt}`),
  ]);
  input.temperature = 0.5;
  input.topP = 0.9;
  input.presencePenalty = 0.2;
  input.frequencyPenalty = 0.3;
  input.responseFormat = ResponseFormat.Json;

  const output = model.invoke(input);

  console.log(output.choices[0].message.content);
  const triviaData = JSON.parse<TriviaData>(
    output.choices[0].message.content.trim(),
  );

  return triviaData.questions;
}

Modus function to Generate Game and insert the LLM generated into the Database

@json
class Game {
  id: i64 = 0;
  movie_id: i32 = 0;
  movie_title: string = "";
  created_at: string = "";
  status: string = "";
  player_id: i32 = 0;
  score: i32 = 0;
  clerk_user_id: string = "";
  questions: Question[] = [];
}


@json
class Question {
  id: i64 = 0;
  game_id: i32 = 0;
  question_text: string = "";
  options: string[] = [];
  correct_answer: string = "";
  difficulty: string = "";
  category: string = "";
  player_answer: string = "";
  is_correct: bool = false;
}

export function createGameAndInsertQuestions(
  movieId: string,
  movieTitle: string,
  questions: TriviaQuestion[],
  clerkUserId: string,
): string {

  const insertGameQuery = `
   INSERT INTO game (movie_id, movie_title, status, score, clerk_user_id)
    VALUES ($1, $2, 'ongoing', 0, $3)
    RETURNING id;
  `;

  const gameParams = new postgresql.Params();
  gameParams.push(movieId);
  gameParams.push(movieTitle);
  gameParams.push(clerkUserId);
  const gameResult = postgresql.query<Game>(
    "triviadb",
    insertGameQuery,
    gameParams,
  );
  const gameId = gameResult.rows[0].id;

  const insertQuestionQuery = `
    INSERT INTO question (game_id, question_text, options, correct_answer, difficulty, category)
    VALUES ($1, $2, $3, $4, $5, $6);
  `;

  for (let i = 0; i < questions.length; i++) {
    const q: TriviaQuestion = questions[i];

    const questionParams = new postgresql.Params();
    questionParams.push(gameId);
    questionParams.push(q.question);
    questionParams.push(JSON.stringify(q.options));
    questionParams.push(q.answer);
    questionParams.push(q.difficulty || null);
    questionParams.push(q.category || null);

    postgresql.execute("triviadb", insertQuestionQuery, questionParams);
  }

  return gameId.toString();
}

The above mentioned code for the function is doing the following:

  1. Creates a new game record in the game table with the provided movie details and user ID.

  2. Retrieves the newly created game's ID.

  3. Inserts each provided trivia question into the question table, linking them to the game via the game ID.

  4. Returns the game ID as a string.

Why 2 functions and not in one function? The reason is because the questions generated by the AI are not very precise. Since it is being stored in the database and will be used for trivia it needs to be in a specific format. Because of this reason, I am sending the LLM-generated questions again to the front end and verifying them, and then those questions that got successfully verified are send back to the backend to get stored.

Selecting an option in the game

const handleAnswerSelect = async (answer: string) => {
  if (hasAnswered) return;

  setSelectedAnswer(answer);
  setHasAnswered(true);

  const difficultyIncrement: Record<'easy' | 'medium' | 'hard', number> = {
    easy: 1,
    medium: 2,
    hard: 3,
  };

  if (answer === currentQuestion.correct_answer) {
    const increment =
      difficultyIncrement[
        currentQuestion.difficulty as 'easy' | 'medium' | 'hard'
      ] || 0;
    setScore(score + increment);
  }

  await updateUserAnswerWithQuestionId({
    questionId: currentQuestion.id,
    answer,
    isCorrect: answer === currentQuestion.correct_answer,
  });
};
  1. if (hasAnswered) return; is to ensure that the user cannot change their answer after making a selection.

  2. setSelectedAnswer(answer); setHasAnswered(true); to record the user's selected answer and updates the state to reflect that an answer has been chosen.

  3. const difficultyIncrement: Record<'easy' | 'medium' | 'hard', number> = { easy: 1, medium: 2, hard: 3, }; a mapping between question difficulty levels and the number of points awarded for a correct answer

  4. if (answer === currentQuestion.correct_answer) { const increment = difficultyIncrement[ currentQuestion.difficulty as 'easy' | 'medium' | 'hard' ] || 0; setScore(score + increment); } Determines if the user's answer is correct and updates the score accordingly.

  5. await updateUserAnswerWithQuestionId({ questionId: currentQuestion.id, answer, isCorrect: answer === currentQuestion.correct_answer, }); Sends the user's answer to the backend to be recorded and shows in the stats page. The following is the function created with modus.

export function updateUserAnswerWithQuestionId(
  questionId: i32,
  answer: string,
  isCorrect: bool,
): string {
  const updateQuery = `UPDATE question SET player_answer = $1, is_correct = $2 WHERE id = $3`;
  const updateParams = new postgresql.Params();
  updateParams.push(answer);
  updateParams.push(isCorrect);
  updateParams.push(questionId);
  postgresql.execute("triviadb", updateQuery, updateParams);
  return "Updated user answer";
}

The above code is a backend function that records the user's answer to a specific question and whether the answer was correct.

Finish Button

This button is visible

When a user clicks on this button the following function gets triggered

 const handleFinish = async () => {
    setShowResult(true);
    await updateGameStatusAndScore({ gameId, score: score });
  };
  1. setShowResult(true); updates the state to show the final results to the user.

  2. Calls updateGameStatusAndScore to update the game's status to 'done' and record the final score in the database.

Code for the Backend Function

export function updateGameStatusAndScore(gameId: i32, score: i32): string {
  const updateQuery = `UPDATE game SET status = 'done', score = $2 WHERE id = $1`;
  const updateParams = new postgresql.Params();
  updateParams.push(gameId);
  updateParams.push(score);

  postgresql.execute("triviadb", updateQuery, updateParams);
  return "Updated Game Status to 'done'";
}

This backend function updates the game table in the database to reflect that the game has been completed and records the user's final score.

Hosting on Hypermode

To make the deployed site work I had to host my modus code in a server and I choose Hypermode. The following steps I took:

  1. Connect my repo with Hypermode through the cli using hyp link command.

  2. Created a repo on github

  3. Followed the steps mentioned in the dashboard.

  4. Added file github workflow file.

  5. Adding secret values. Setting —> Connections

    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1733011096829/9b32371a-fc38-43b1-a074-e35de84f517d.png align="center")

    After adding the values I had to redeploy my modus code.

Initially I was having some issue deploying it to modus even after connecting the repo but the modus hypermode team was very helpful and the issue was resolved.

Setting Up the Rec & Triv App Locally: A Step-by-Step Guide

So the application code has been divided into frontend and backend.

To access the frontend code: trivia-modus

To access the backend code: steady-sequence (this is an autogenerated name)

To successfully run this application you will need the following API keys. In this article, I won’t go through the process of individually creating API but will guide you to the correct resources.

  1. TMDB API for movie data.

  2. Wikipedia API using it to get the movie plot.

  3. Postgres Database from neon.

  4. Clerk for authentication.

Backend Part: steady-sequence

  1. Clone the repo

  2. cd steady-sequence

  3. Create a .env.dev.local file or just change the name for env.example to .env.dev.local at the root of the project then add the above-mentioned values in it.

Good to know: Modus uses a naming convention MODUS_<CONNECTION NAME>_<PLACEHOLDER> for env values. So if you change the name of the values from the default mentioned in the mentioned github repo values you will need to change it in the manifest file too.

  1. You are all set up, now run modus dev in the terminal. The development server should start. Once it is successfully started you should see something like this.

    ![Modus dev server successfully running](https://cdn.hashnode.com/res/hashnode/image/upload/v1732911857173/6e6159ad-4179-4341-ac69-0e2ebc62f9b2.png align="center")

  2. One thing to know is that you might see “ERR Failed to get unique namespaces. error=" database not configured" collection_name=movieOverviews” It is because I did not set up collections locally. I used it directly with the hosted version and Hypermode manages that. To set it up locally use this guide.

Good to know: Everything in the application will work locally apart from the collections part which is use for movie recomendation.

Setting the Database
I used postgres from neon to host my database.

  1. Go to Neon’s official website and create a postgres database.

  2. In my application there are 2 tables, game and question.

  3. For the game table, I have used the following SQL query

    CREATE TABLE public.game (
        id bigint NOT NULL GENERATED ALWAYS AS IDENTITY,
        movie_id text DEFAULT ''::text NOT NULL,
        movie_title text NOT NULL,
        created_at timestamp without time zone DEFAULT now(),
        status text DEFAULT 'ongoing'::text,
        score integer DEFAULT 0,
        clerk_user_id text DEFAULT ''::text,
        CONSTRAINT game_pkey PRIMARY KEY (id)
    );
    
    CREATE INDEX idx_game_clerk_user_id ON public.game USING btree (clerk_user_id);
    
    1. For the question table, I have used the following query

      CREATE TABLE public.question (
          id bigint NOT NULL GENERATED ALWAYS AS IDENTITY,
          game_id integer NOT NULL,
          question_text text NOT NULL,
          options jsonb NOT NULL,
          correct_answer text NOT NULL,
          difficulty text,
          category text,
          player_answer text,
          is_correct boolean,
          CONSTRAINT question_pkey PRIMARY KEY (id),
          CONSTRAINT question_game_id_fkey FOREIGN KEY (game_id) REFERENCES public.game(id) ON DELETE CASCADE
      );
      

      I have used the SQL editor from the neon console to create both the tables

      ![neon console sql editor](https://cdn.hashnode.com/res/hashnode/image/upload/v1732994347322/2208ca4b-1f08-4b75-96fa-0c9fab9cedd7.png align="center")

Frontend part: trivia-modus

  1. Clone the repo

  2. cd trivia-point

  3. run the command to install all the dependencies with

    npm install
    
    1. Create an .env file and fill it in with the values mentioned in .env.example

Make sure your modus server is running and then start the next.js application server by running npm run dev. Then visit http://localhost:3000 your front-end application should be visible.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1732913692397/18784cae-c343-4185-b11d-48059a1cf6ef.png align="center")

Once you see this page, with correct values for env your application should work locally.

To communicate with graphql enpoint I am using apollo client and all the queries can be found at lib—> queries.

Conclusion

Working on the Rec & Triv application had its fair share of up and down. The project allowed me to dive into new technologies like Modus with which I have created my backend and Hypermode where I have hosted the backend. Despite encountering obstacles such as deployment issues and adapting to unfamiliar frameworks, these challenges provided valuable learning experiences for which I will always be grateful.

Combining movie recommendations with trivia games offers users an engaging experience, and also a great learning experience for me. Moving forward, I plan to add new functionality through which users can enjoy more and at the same time will be a good learning experience for me.

Github Repo for frontend

Github Repo for backend

Last but not least thanks a lot Hashnode and Modus for the opportunity you all provided.