"Grasp" - AI-powered language-learning app

"Grasp" - AI-powered language-learning app

Final project for Phase 5 of Flatiron School Software Engineering bootcamp

Featured on Hashnode

Language learning has always been part of my life: Portuguese and Spanish at school; playing online videogames in English as a teenager; immigrating to the USA during my 20s; and, nowadays, using apps like Duolingo to learn as a hobby.

I'm currently on a 450-day streak learning Modern Greek. As much as I enjoy the app, though, I feel that its experience hasn't kept up once I progressed from beginner to intermediate levels and beyond.

In my experience, being proficient in a language means having the ability and confidence to use language creatively, but the app's formulaic, gamified approach takes away from the creative aspect of language.

(Not to mention the random and weird sentences -- one that comes to mind was "Is grandma for sale?")

Project background

For my final project of the Flatiron School's Software Engineering bootcamp, I decided to create an MVP for my ideal language-learning app, putting to the test the full-stack web development skills I learned during the last few months.

My goal was to create an AI-powered language learning app for intermediate and advanced learners.

Usually, most learners only want to listen and speak a language, but I decided that my app would focus on reading and writing. Here are some of the benefits of developing reading and writing skills:

  • Converting passive vocabulary into active

  • Opportunity to practice grammar and edit your thoughts

  • Less performance anxiety

  • Prompts learners to expand vocabulary and be creative

  • Builds learners confidence

Enter "Grasp", an AI-powered language learning app!

  • Learners can sign up for courses about the topics they are interested in.

  • Each lesson includes an open-ended question for writing practice

  • Learners receive instant feedback through AI

  • Optimized for mobile experience

  • User authentication functionality with authorization -- some pages can only be viewed if the user is authenticated.

For the tech stack, I used the following technologies:

  • Frontend: React, JavaScript, HTML, CSS

  • Backend: Flask, SQLite

  • 3rd-party API: OpenAI (GPT 3.5)

Check out the code on GitHub here, or read below for some of the challenges and solutions!

Challenges

Some of the challenges I found during development and how I solved them:

Designing the backend models

As I built the features of my app, I tried to keep the backend models as simple as possible.

Still, the backend grew quite complex, so to manage all the different relationships, I kept a chart of all the models and relationships using Lucidchart:

CORS errors and Before request

To implement user authentication, I used the app.before_request decorator with a list of open access urls vs. the ones that required the user to be logged in, but I ran into a CORS policy issue.

I tested my route in Postman and it was working fine, but on the browser the CORS error appeared. I edited my fetch to see if a GET request would work, and it did. So that led me to investigate why the PATCH method specifically was not going through.

After about 2 hours and multiple Google searches, I learned that the PATCH request was considered as unsafe by the CORS policy, so it was sending a preflight OPTIONS request. This request expected a response before it would send the actual PATCH request through.

I updated the before_request function to include “Access-Control-Allow-Methods” in the headers and to respond to the OPTIONS request with a 200 response.

This cleared the way for the PATCH to come through, and the CORS error finally went away!

Here's the working code snippet:

@app.before_request
def load_user():

    if request.method == 'OPTIONS':
        response = make_response({}, 200)
        response.headers.add('Access-Control-Allow-Methods', 'GET,PUT,POST,PATCH,DELETE,OPTIONS')
        return response

    # Check access:
    open_access_list = [
        'signup',
        'login',
        'logout',
        'check_session',
        'course',
        'courses',
        'languages',
        'topics'
    ]

    # Returns 401 error if the endpoint is not open access and the user is not logged in:
    if (request.endpoint) not in open_access_list and (not session.get('user_id')):
        return {'error': '401 Unauthorized'}, 401

Tweaking OpenAI's chat completions API response

In order to provide users with instant feedback on their output, I used OpenAI's API. This required me to tweak the prompt in order to get the response in the right format.

I also noticed the AI was correcting words and sentences that were already correct. To fix this, I tweaked the "temperature" of the response, to minimize hallucinations.

const systemMessageContent = `You are a teacher of ${lesson.language.language_name}. You are reviewing your student's written texts in this language. When given an input, you will review the input in ${lesson.language.language_name} for grammar, spelling, and style. 

    Please respond with the corrected input in HTML inside <p> tags. Make sure to provide a new paragraph tag for each paragraph in the input! 

    Inside the p tag(s), find the mistakes and wrap them in a <span className="mistakes"> HTML tag. And then, in a new line, list the corrections you made under an HTML unordered list: "<ul></ul>". Please add a new line for each correction with its corresponding <li></li> tag. If there are no mistakes to correct, please respond with: "<p>Congratulations! Your response looks great!"</p>"`

fetch(`https://api.openai.com/v1/chat/completions`, {
            method: 'POST',
            headers: {
                "Content-Type": "application/json",
                "Authorization": `Bearer ${apiKey}`
            },
            body: JSON.stringify({
                "model": "gpt-3.5-turbo",
                "temperature": 0.2,
                "messages": [
                    {"role": "system", "content": systemMessageContent},
                    {"role": "user", "content": userInput}
                ],
            })
        })
        .then(response => response.json())
        .then(message => {
            console.log(message); 
            setAiResponse(message.choices[0].message.content)
        })

Mobile-responsive experience (NavBar case)

One of my stretch goals for this project was to make the app mobile-responsive.

The NavBar gave me some trouble, so I wanted to highlight it here. I used CSS media at-rules to collapse the nav links under a hamburger menu in the mobile-responsive app.

The logic is as follows:

  • If the width of the page is over 500px, then the #hamburger-menu's style.display is "none"; else, it's "inline-block";

  • For the desktop app, the navbar uses Flexbox with "row" direction, but in the mobile version, I used the "column" direction to stack navlinks vertically.

  • Clicking on the hamburger menu toggles the navlinks' style.display between "none" and "inline-block", this way the nav can be collapsed/expanded in the mobile view.

...
function handleHamburgerClick() {

        const navLinks = document.querySelectorAll(".nav-ul");
        navLinks.forEach(navLink => {
            if (navLink.style.display !== "inline-block") {
                navLink.style.display = "inline-block";
              } else {
                navLink.style.display = "none";
              }
        })
    }

return (
...
<a id="hamburger-menu" onClick={() => handleHamburgerClick()}>≡</a>
)
@media (max-width: 500px) {

    .navbar {
        width: 100%;
        display: flex;
        flex-direction: column;
        flex-wrap: wrap;
        padding-bottom: 0.5em;
    }
    #hamburger-menu {
        position: absolute;
        top: 0;
        right: 0;
        display: inline-flex;
    }
    .navbar ul:first-of-type {
        margin-top: 0.5em;
    }
    .navbar ul {
        display: none;
        width: 100%;
    }
    .navbar ul a {
        width: 100%;
        box-sizing: border-box;
        padding: 2%;
    }
}

User progress through lessons

Another challenge was linking lessons together and unlocking them to give the user a sense of progress. To do so, the lessons are implemented as linked lists, with each lesson pointing to the prev/next lesson.

This also allowed me to add new lessons to a course efficiently, without having to reorganize the lessons on the database.

The user's progress is saved in the UserLesson table, which is kept separate from the Lesson model.

Conclusion

I built this product in a little over 2 weeks. There were so many other features I wanted to implement, but didn't have enough time.

Still, it was great fun and allowed me to demonstrate the skills I learned at Flatiron School.

I'm looking forward to continuing to learn by building more projects in the future!

BTW: I'm looking for a web developer job now. If you know of any opportunities for an entry-level developer who is a fast learner, let me know!

Thank you for reading!

Bruno