A Looong Devblog: The Tutor Board Design Process”

Abstract of the wave pattern at Coyote Buttes, Arizona
Here’s some documentation for how tutor board works. The entire codebase for both the website, and the bot that polls google classroom are on github.

So far, I’ve used the colors of red and blue, light blue and white for my project as these colors are friendly on the eyes and you see them around school all the time, the HHIS logo is also red and blue. Font I’ve used is Nunito which is also the schools font

For the elements every day, I’ve copied a JSON of all element data in the periodic table.

Here’s a section of what that looks like:

{
  "elements": [
    {
      "name": "Hydrogen",
      "appearance": "colorless gas",
      "atomic_mass": 1.008,
      "boil": 20.271,
      "category": "diatomic nonmetal",
      "density": 0.08988,
      "discovered_by": "Henry Cavendish",
      "melt": 13.99,
      "molar_heat": 28.836,
      "named_by": "Antoine Lavoisier",
      "number": 1,
      "period": 1,
      "group": 1,
      "phase": "Gas",
      "source": "https://en.wikipedia.org/wiki/Hydrogen",
      "symbol": "H",
      "shells": [1],
      "electron_configuration": "1s1"
    }
  ]
}

Obviously, I don’t need all that info for my ‘guessing the element game based on the symbol’, so I ran a python script that removes all the things I don’t need and keeps the name, atomic mass, symbol, and a hint for the element.
To generate a unique ‘hint’ for each element, I’ve used openai’s python api.

On the website, all that’s left to do is create a function that takes in a unique seed each day, and what better seed to do than the day of that year (out of 365/366). The function to select an element is deterministic, meaning on the same day the next year, that same element will come up. There’s only 118 elements, so not much I can do about the repetition.

For the flags, I’ve found a flag api of sorts, which returns an svg of that country’s flag, the country is selected by giving it the ISO standard country code (CH: Switzerland, TH: Thailand, US: United States, etc.)

I’ve implemented a hint to give the first 2 letters of that flag’s country.

For the jokes in ‘joke of the day’, I’ve scraped a website of jokes, 250 in total, and I will use the same algorithm to select a joke each day, just like the flag and element of the day.

Another python script was used to convert the jokes into a json.

For the trivia, I’ve sourced them from Open Trivia DB.
Archives - Riddles.com
https://flagpedia.net/download/api
250 Funniest Jokes for Kids of All Time (goodhousekeeping.com)
Open Trivia DB: Free to use, user-contributed trivia question database. (opentdb.com)

Okay, I’ve changed a lot since the last edit, but let’s get everything up to date, I’ll try my best.

I’ve added a login system, many more boards, new styling, features, ease of life, bug fixes. Too many to list or even recall. Here’s how to website looks now, no placeholder for anything, it is all fully functional (19/Nov/2024)

The login system utilizes JWT on an express.js backend. There is a login endpoint, which verifies a user’s username and password, through comparing hashed passwords, for extra security. After the credentials have been verified, then a JWT login token is generated with the username of the user. The token is sent back to the client and stored as a cookie. As for logout, it removes the client’s cookie, effectively logging them out.

In the database the user’s passwords are stored as a hash, so even if the database is breached, the passwords won’t be known (without lots of computation power or a quantum computer):

The buttons!

The buttons on this website were something that took many iterations to feel “right”. I wanted something playful, responsive and bubbly, ‘classroom friendly’ if you will.
There are 5 types of buttons across the entire app.

  • Normal Button
  • Red Button (warning)
    • Used for things like: Cancel, delete, etc.
  • Green Button (confirmation)
    • Used for things like: Submit notice, login, add quote, etc.
  • Glowing button (for special buttons, like class game of the day)
  • Shiny button (buttons that should need a little more attention)
  • Disabled buttons (when the action the button does, can’t be done)
    • Button is grey and unresponsive

The buttons are defined in CSS as shown:

The buttons are responsive in numerous ways. They have defined characteristics for when, hovered on, pressed, released, and ‘idle’. When idle the button’s border is grey, and once hovered on, the border turns to black, while the buttons ‘pops’ up from the webpage and the shadow separates, finally when pressed, the button goes ‘all the way down’, each state is animated to transition into each other with a cubic bezier animation curve defined as: cubic-bezier(1, -0.91, 0, 1.72); Each transition lasts 0.25s. The combination of animation, style and responsiveness, almost mimics a real button, I think the design is quite skeuomorphic.

^ Left to right: idle, hovered, pressed


^ How the animation curve looks like

The backend.

Database structure:

The database consists of many tables, to store all data for the website.

Above is the database table for all the school’s notices (in secondary). Notices and events are made with the Tutor Board Autonomous Updater Agent. A python script which runs on a cron job, polls google classroom courses for new announcements, a list of the already ‘processed’ announcements is cached in another database.

Upon finding a newly posted classroom announcement, the bot will decide if the announcement is suitable for being an upcoming event / notice. This stage also checks if the notice contains sensitive information, which should be shared publicly, on the website. All this is done through the Open AI API, using LLMs, like o3-mini and gpt-4o-mini. The next step is to use LLM to rewrite the announcement to remove any 1st person style of writing and clean up the formatting. The LLM also decides title, start and end dates, and target year groups for the notice. If the announcement is decided to be an ‘upcoming event’ a suitable date, title and description are made for the event. Then the json is sent in a REST API request to the express.js backend, which in turn puts it in the database.

Here is the prompt I’ve engineered for creating notices from google classroom announcements.

            `You are the Hua Hin International School (HHIS) school announcement processor tasked with condensing and reformatting announcements into a structured JSON format. Your goal is to create clear, concise, and formal announcements that are easily understood by students and staff.`

    `Here is the announcement information you need to process, each piece of information is delimited by a triple quotes (\"\"\"):`

    `Announcement Content:`  
    `\"\"\"`  
    `{description}`  
    `\"\"\"`  
    `Creation Time: \"\"\"{creation_time}\"\"\"`  
    `Update Time: \"\"\"{update_time}\"\"\"`  
    `Which class / group the announcement came from: \"\"\"{course_name}\"\"\"`  
    `The announcement is for year groups: {', '.join(map(str, target))}`

    `The current date is {current_date}, and the day of the week is {datetime.strptime(current_date, "%Y-%m-%d").strftime("%A")} (For your reference).`  
     
    `Note that creation and update times are given in UTC time.`  
    `But the current date is given in the local GMT+7 time zone, meant to be used in reference with mentions of time within the notice description.`

    `Your task is to:`  
    `1. Condense the announcement, focusing on essential information, retaining links too.`  
        `- Links should be formatted with [URL title](URL) e.g. [Google](https://www.google.com)`  
        `- Keep the description under 1024 characters!`  
        `- Remove any sensitive information like full names, passwords, or other private information.`  
        `- Do not use redundant sentences along the lines of "Thank you for your understanding regarding this matter." or "This information is shared by Mr. Chris for the benefit of the IB Notices group."`  
    `2. Change the perspective to a non-1st person.`  
    `3. Make the announcement formal and straightforward and include context to the class the announcement is coming from.`  
    `4. Create an appropriate title, but exclude "Announcement" or similar words from the title, as it's redundant.`  
    `5. Determine the start date (date the announcement was last updated or created).`  
    `6. Infer the end date from the context (the date by which the announcement should be completed or will no longer be useful).`  
    `7. Format the output as JSON according to the specified structure, this is crucial!.`  
    `8. the field target should be left as {target}`


    Provide the final output in the following JSON format:
{
    "title": "Appropriate title (5-10 words)",
    "description": "Condensed, yet detailed, formal announcement text",
    "startDate": "YYYY-MM-DD",
    "endDate": "YYYY-MM-DD",
    "target": {target}
}

And the prompt for making an event:

    `You are the Hua Hin International School (HHIS) announcement processor tasked with condensing and reformatting announcements into a structured JSON format for upcoming school events. Your goal is to create clear, concise, and formal event descriptions that are easily understood by students and staff.`

    `Here is the announcement information you need to process, each piece of information is delimited by triple quotes (\"\"\"):`

    `Announcement Content:`  
    `\"\"\"`  
    `{description}`  
    `\"\"\"`  
    `Creation Time: \"\"\"{creation_time}\"\"\"`  
    `Update Time: \"\"\"{update_time}\"\"\"`  
    `Which class / group the announcement came from: \"\"\"{course_name}\"\"\"`

    `The current date is {current_date}, and the day of the week is {datetime.strptime(current_date, "%Y-%m-%d").strftime("%A")} (For your reference).`  
     
    `Note that creation and update times are given in UTC time.`  
    `But the current date is given in the local GMT+7 time zone, meant to be used in reference with mentions of time within the notice description.`

    `Your task is to:`  
    `1. Condense the announcement, focusing on essential information, retaining links too.`  
        `- Links should be formatted with [URL title](URL) e.g. [Google](https://www.google.com)`  
    `2. Change the perspective to a non-1st person.`  
    `3. Make the announcement formal and straightforward.`  
    `4. Create an appropriate title, but exclude "Event" or similar words from the title, as it's redundant and implied already from context.`  
    `5. Determine the event date (date the event is scheduled to occur).`  
        `- Take note of phrases like "tomorrow" or "this morning" and use the context with current date and day of the week to decide this`  
    `6. Format the output as JSON according to the specified structure, this is crucial!.`

    Provide the final output in the following JSON format:
{
    "name": "Appropriate title (5-10 words)",
    "description": "Condensed, formal event description text",
    "date": "YYYY-MM-DD"
}

In practice, the implementation of the agent, which runs separately off a cron job to keep tutor board updated works as follows:

START
├── Environment Setup
│ ├── Load secrets from JSON file
│ ├── Initialize OpenAI client, ClassroomAPI, and BackendAPI
│ └── Define 11-function toolkit for AI agent

├── Data Collection
│ ├── Fetch announcements from Google Classroom
│ ├── Fetch assignments from Google Classroom
│ └── If no new data → End session immediately

├── Context Preparation
│ ├── Generate comprehensive prompt with:
│ │ ├── Current date/time
│ │ ├── All classroom data (announcements + assignments)
│ │ ├── Current board state (notices + events)
│ │ └── Detailed operational guidelines
│ └── Package into conversation format

├── AI Agent Processing Loop (Max 8 iterations)
│ ├── Send context to GPT-4.1 with tool access
│ ├── Agent analyzes situation and chooses tools
│ ├── For each tool call:
│ │ ├── Execute function via call_function()
│ │ ├── Return results to agent
│ │ ├── Update conversation history
│ │ └── Provide fresh board state for context
│ ├── Agent continues reasoning with new information
│ └── Loop until agent calls finish_processing() or max iterations

├── Available Agent Actions:
│ ├── Information Gathering:
│ │ ├── get_announcements() → Latest classroom announcements
│ │ ├── get_assignments() → Upcoming assignments
│ │ ├── get_existing_notices() → Current notice board
│ │ └── get_existing_events() → Current events board
│ ├── Content Management:
│ │ ├── create_notice(title, desc, dates, targets)
│ │ ├── update_notice(id, title, desc, dates, targets)
│ │ ├── delete_notice(id)
│ │ ├── create_event(name, desc, date)
│ │ ├── update_event(id, name, desc, date)
│ │ └── delete_event(id)
│ └── Session Control:
│ └── finish_processing(message) → End with summary

└── Session Termination
├── Either: Agent-controlled via finish_processing()
├── Or: Force-end after 8 iterations
└── Report runtime statistics and actions taken

Recently, I’ve added a new feature which allows the agent to also process images that are attached to google classroom announcements or assignments, which should greatly increase the versatility and contextual knowledge of the agent, improving its outputs. This downloading the image through the google drive api, then pre-processing it with Pillow, then base64 encoding it and attaching it to the openAI API request.

Keeping the frontend, backend and AI agent system separate and self-contained reduced mutual dependencies kept the system more robust and versatile. Keeping the expressJS backend for API requests meant each system’s scope and complexity stayed reasonable and modular.