Building an AI-Powered, Decentralized App for Time Management

Building an AI-Powered, Decentralized App for Time Management

The beauty of developer advocacy is that it offers the freedom to build anything, anytime, and in any way I desire. The downside is that it's a role that comes with its fair share of responsibilities, often leaving little room for personal exploration. While this allowed me to become an expert in my company's tools and products, it also means I risk falling behind on the latest technologies

Due to the hectic move of my life, I didn’t have reliable internet, so I couldn't hop on Zoom calls, watch Twitch streams, or host webinars for most of July. This was a blessing in disguise because it was the perfect opportunity to experiment with different technologies, despite the painfully slow execution times due to unreliable internet.

With some of that time, I developed a prototype for a to-do list on steroids, which I’ve named Time Blind. This application allows users to create a to-do list and then expand each task to identify subtasks and estimate how long each task will realistically take.

What is the purpose of the application?

It sounds fictitious, but time blindness is the inability to recognize when time has passed or to estimate how long something will take. Many people with ADHD or autism struggle with this. I struggle with this. I’m really glad I found a term for this condition because I used to be really hard on myself when I was late to appointments

While some might suggest better planning as a solution, it's not that simple for those of us who either overestimate our capabilities or become so engrossed in a task that we lose track of time. For instance, I might plan to attend a party at 2pm and decide to start getting ready at noon. However, in the interim, I convince myself that I can wash dishes, do laundry, wash my hair (a process that takes at least 3 hours as a black woman), cook, and drive. Suddenly, it's 1:55 pm, and I'm just getting into my car.

I once came across an application that addressed this issue, but when I couldn't find it again, I decided to create my own solution.

Disclaimer: This article is not a step-by-step tutorial, but rather a journey through my thought process and experiences as I explored these technologies. My aim is to inspire and provide insights into how these tools can be utilized in different ways. Please bear in mind that this was my first encounter with some of these technologies, so there may be instances where I haven't followed best practices. That's part of the learning process! This project was created for fun, and as such, the focus wasn't on aesthetics. The UI is basic (a little ugly to be honest) and functional, as my expertise lies in developer relations rather than design. Enjoy the read!

The Tech Stack: A Blend of the Familiar and the New

In this project, I've combined tools and technologies that I'm well-versed in with some that were new to me.

Familiar

  • Next.js - This React framework is a favorite of mine, particularly for its ease in defining serverless API endpoints.

  • GitHub Pages - A free hosting solution for frontend web apps, it was a perfect fit for the initial iteration of my project (before I added a backend).

  • GitHub Actions - I leveraged GitHub Actions to deploy my Next.js app to GitHub Pages.

  • Copilot Chat - This was handy for generating JS Doc comments and unit tests for my code.

  • OpenAI GPT-4 - While I wouldn't call myself an expert, I've dabbled with OpenAI's models before. For this app, I used GPT-4 to break tasks into subtasks and estimate the time each subtask would take.

  • GitHub Codespaces - I love using this because of its boilerplate templates, which save me from writing boilerplate code myself. It also makes it easier to create shareable demos for other developers to try out.

New to me

  • Web5 SDK - This is an SDK for building decentralized web apps with DIDs, VCs, and DWNs. The central goal of Web 5 is to empower users with full control over their identity and data. This sets it apart from Web 3 protocols, which primarily identify users through crypto wallet addresses. In contrast, Web 5 introduces innovative features like self-owned decentralized identifiers, verifiable credentials, and decentralized web nodes, enabling secure data storage and message relaying. The value of decentralized applications became more evident to me after Elon purchased Twitter. He’s made so many changes that I don’t appreciate. Elon even changed the name of Twitter to X and took someone’s handle. That made me realize how easily someone can take the handle @blackgirlbytes. All of those factors piqued my interest in decentralization. The ease of storing data in the DWN was a bonus, requiring less overhead than a SQL database.

  • Web5 assistant - There’s a ChatGPT plugin for the Web5 SDK. It was really helpful in navigating a technology that I’m not familiar with.

  • Applitools - I used this to implement automated visual tests to my application.

  • Playwright - I used Playwright for unit tests.

  • Railway - I needed a different hosting solution since GitHub Pages only supports frontend applications. I initially chose Vercel, but the long API calls to OpenAI would time out on the Hobby plan. Railway didn't have this limitation, but I'm currently using the trial plan, so I'm not sure how long I can use it for free.

Building the application

GitHub Codespace Next.js template

Ah, GitHub Codespace templates – the reason why I will never run npx create-next-app@latest. I don’t hate running this command; I’m just a bit of a lazy developer. GitHub Codespace templates have a ton of ready-made boilerplate applications written in various, popular frameworks which makes it easy for me to get up and running in a sandbox environment.

Highlights the use this template button for Nextjs

You can create a Next.js GitHub Codespace template by navigating to https://github.com/codespaces/templates. Then choosing the “Use this template” button for Next.js. This will trigger a codespace to open with boilerplate Next.js code with a browser preview.

What I particularly appreciate about this setup is the pre-configured dev container. It ensures that anyone who opens my project gets a running environment without any additional configuration on their part. This feature significantly streamlines the process of setting up a development environment, especially for new contributors or team members.

GitHub Codespaces also supports environment variables and secrets. This means you can open my repository in a GitHub Codespace without worrying about generating an API key. I've already stored one in my repository settings, so there's no need to figure out how to securely share environment variables with multiple people.

Understanding Web5 basics

Authentication

To authenticate users, Web 5 uses a W3C standard called Decentralized Identifiers (DID). A decentralized identifier is a verifiable id that can connect me to Web 5. The identifiers we use today are often tied to a specific service provider. For example, I’m @blackgirlbytes on Twitter and GitHub. In the Web 5 ecosystem, my id could look something like:

did:key:z6MkhvthBZDxVvLUswRey729CquxMiaoYXrT5SYbCAATc8V9

Here’s how you can instantiate and connect to a DWN using a decentralized identifier:

const { web5, did: aliceDid } = await Web5.connect();

Here’s what how I implemented this for Next.js in my src/pages/index.js file:

const [web5Instance, setWeb5Instance] = useState(null);
const [aliceDid, setAliceDid] = useState(null);

useEffect(() => {
    async function connectToWeb5() {
      const { web5, did } = await Web5.connect();
      setWeb5Instance(web5);
      setAliceDid(did);
    }

    connectToWeb5();

  }, []);

CRUD

Similar to REST APIs, a user can interact with Distributed Web Nodes (DWN) using CRUD principles. The DWN is our personal data store, so we’ll need to create, read, update, and delete data to interact with our store.

Create (write) records

const { record } = await web5.dwn.records.create({
  data: "Its me, snitches",
  message: {
    dataFormat: 'text/plain',
  },
});

console.log('writeResult', record);

Read records

const readResult = await record.data.text();
console.log('readResult', readResult)

Update (edit) records

const updateResult = await record.update({data: "Professional bug maker"});
console.log('updateResult', updateResult)

Delete records

const deleteResult = await record.delete();
console.log('deleteResult', deleteResult)

Starting with the To Do list

Before building Time Blind, I started by building a to-do list. I wanted to get the core functionality down, especially since I was new to the Web5 SDK. I built a few components that leveraged the methods I mentioned above.

Here’s an example:

async function addTask(event) {
    event.preventDefault(); // Prevent form from submitting and refreshing the page
    if (web5Instance && aliceDid && newTask.trim() !== '') {
      const taskData = {
        '@context': 'https://schema.org/',
        '@type': 'Action',
        name: newTask,
        completed: false,
      };

      const { record } = await web5Instance.dwn.records.create({
        data: taskData,
        message: {
          dataFormat: 'application/json',
          schema: 'https://schema.org/Action',
        },
      });

      // Send the record to the DWN.
      await record.send(aliceDid);

      setTasks((prevTasks) => [...prevTasks, { id: record.id, text: newTask }]);
      setNewTask('');
    }
  }

When a user creates a task, the application uses the dwn.records.create method from the Web5.js SDK. This method creates a new record with the task data and sends it to the Decentralized Web Node (DWN) associated with the user's Decentralized Identifier (DID). The task data includes the task name and its completion status.

You might’ve noticed that I’m using a schema. By using a schema, I’m ensuring that my tasks have a consistent, predictable structure.

If you want to check out the code for this to-do list, here is the repository.

Copilot Chat for generating tests with Playwright and Applitools

I’m not super familiar with Playwright or Applitools, so I leaned on Copilot Chat to help me generate tests for me. Below are examples of interactions I had with Copilot Chat to generate the unit tests.

Copilot Chat generates more tests for me

Image description

I asked Copilot Chat for clarification on some code it suggested

Image description

I asked Copilot Chat for alternative suggestions after realizing the initial suggestion won’t work in my codebase.

Image description

Copilot Chat gave me advice on how to navigate the Applitools Eyes interface.

Image description

Deploying with GitHub Pages with GitHub Actions

I used a GitHub Action to deploy my Next.js app to GitHub Pages.. It's important to note that this approach is suitable for applications without a server. . Upon successful deployment, the workflow automatically generated a URL for me: https://blackgirlbytes.github.io/decentralized-to-do-list/.

Here’s what the action workflow looked like:

# Sample workflow for building and deploying a Next.js site to GitHub Pages
#
# To get started with Next.js see: https://nextjs.org/docs/getting-started
#
name: Deploy Next.js site to Pages

on:
  # Runs on pushes targeting the default branch
  push:
    branches: ["main"]

  # Allows you to run this workflow manually from the Actions tab
  workflow_dispatch:

# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
  contents: read
  pages: write
  id-token: write

# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
  group: "pages"
  cancel-in-progress: false

jobs:
  # Build job
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - name: Detect package manager
        id: detect-package-manager
        run: |
          if [ -f "${{ github.workspace }}/yarn.lock" ]; then
            echo "manager=yarn" >> $GITHUB_OUTPUT
            echo "command=install" >> $GITHUB_OUTPUT
            echo "runner=yarn" >> $GITHUB_OUTPUT
            exit 0
          elif [ -f "${{ github.workspace }}/package.json" ]; then
            echo "manager=npm" >> $GITHUB_OUTPUT
            echo "command=ci" >> $GITHUB_OUTPUT
            echo "runner=npx --no-install" >> $GITHUB_OUTPUT
            exit 0
          else
            echo "Unable to determine package manager"
            exit 1
          fi
      - name: Setup Node
        uses: actions/setup-node@v3
        with:
          node-version: "16"
          cache: ${{ steps.detect-package-manager.outputs.manager }}
      - name: Setup Pages
        uses: actions/configure-pages@v3
        with:
          # Automatically inject basePath in your Next.js configuration file and disable
          # server side image optimization (https://nextjs.org/docs/api-reference/next/image#unoptimized).
          #
          # You may remove this line if you want to manage the configuration yourself.
          static_site_generator: next
      - name: Restore cache
        uses: actions/cache@v3
        with:
          path: |
            .next/cache
          # Generate a new cache whenever packages or source files change.
          key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json', '**/yarn.lock') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }}
          # If source files changed but packages didn't, rebuild from a prior cache.
          restore-keys: |
            ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json', '**/yarn.lock') }}-
      - name: Install dependencies
        run: ${{ steps.detect-package-manager.outputs.manager }} ${{ steps.detect-package-manager.outputs.command }}
      - name: Build with Next.js
        run: ${{ steps.detect-package-manager.outputs.runner }} next build
      - name: Static HTML export with Next.js
        run: ${{ steps.detect-package-manager.outputs.runner }} next export
      - name: Upload artifact
        uses: actions/upload-pages-artifact@v2
        with:
          path: ./out

  # Deployment job
  deploy:
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    runs-on: ubuntu-latest
    needs: build
    steps:
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v2

Incorporating AI for task breakdown and estimation

With a functional to-do list in place, the next step was to incorporate AI to dissect each task on the list and provide estimated completion times. I added this on the server rather than the frontend to maintain the confidentiality of the API key required to make calls to the OpenAI model.

In my request to OpenAI, I specified that I wanted to use the GPT-4 model, I gave the model an identity, and I made a request. You can see the code below:

openai.createChatCompletion({
      model: "gpt-4", // Using GPT-4
      messages: [
        { "role": "user", "content": `You are a helpful planning assistant. Please help me break down the following task on my to-do list with an estimated time that it could take: ${task}` },
      ],
    });

My request prompts the AI model to act as a helpful planning assistant, breaking down the specified task and estimating the time it might take to complete.

Copilot Chat for documenting my code

By highlighting the code, I can prompt Copilot Chat to generate JS Doc comments for my code.

Image description

Here’s an example of the JS Doc comments that Copilot Chat generated for my API endpoint:

/**
 * This function is an API endpoint that generates a task breakdown using OpenAI's GPT-4 model.
 * It expects a POST request with a body containing a 'tasks' property.
 * If the OpenAI API key is not configured or if the 'tasks' property is not provided, it responds with an error.
 * If the OpenAI API request fails, it responds with the error returned by the OpenAI API.
 * If the OpenAI API request is successful, it responds with the generated task breakdown.
 *
 * @param {Object} req - The request object. It should have a 'body' property with a 'tasks' property.
 * @param {Object} res - The response object. It is used to send the response back to the client.
 * @returns {Promise<void>} - A Promise that resolves when the response has been sent.
 */
const { Configuration, OpenAIApi } = require("openai");

const configuration = new Configuration({
  apiKey: process.env.OPENAI_API_KEY,
});
const openai = new OpenAIApi(configuration);

export default async function (req, res) {
  if (!configuration.apiKey) {
    res.status(500).json({
      error: {
        message: "OpenAI API key not configured, please follow instructions in README.md",
      }
    });
    return;
  }

  const task = req.body.tasks || '';
  if (task === '') {
    res.status(400).json({
      error: {
        message: "Please enter a valid task",
      }
    });
    return;
  }

  try {
    const completion = await openai.createChatCompletion({
      model: "gpt-4", // Using GPT-4
      messages: [
        { "role": "user", "content": `You are a helpful planning assistant. Please help me break down the following task on my to-do list with an estimated time that it could take: ${task}` },
      ],
    });

    const result = completion.data.choices[0].message.content;
    res.status(200).json({ result });
  } catch (error) {
    // Consider adjusting the error handling logic for your use case
    if (error.response) {
      console.error(error.response.status, error.response.data);
      res.status(error.response.status).json(error.response.data);
    } else {
      console.error(`Error with OpenAI API request: ${error.message}`);
      res.status(500).json({
        error: {
          message: 'An error occurred during your request.',
        }
      });
    }
  }
}

Deploying with Railway

I chose to deploy my application to Railway because I needed a hosting solution that supports full stack applications. This worked perfectly for me! However, I did encounter a few challenges while navigating through Railway's user interface. For example, I just wanted to deploy my repository, and I got stuck in a loop of configuring the GitHub App for Railway. The part of the documentation I checked lacked visual aids, and I’m a visual learner, so that was hard for me. I ended up watching a YouTube video to navigate the platform. Despite these minor hiccups, my overall experience with Railway was positive, and I was particularly pleased with the successful deployment of my application.

Final thoughts

Struggles

The app is unbearably slow. My proficiency in performance optimization is admittedly lacking, as I've never held a role where optimizing an application's speed was a primary responsibility. I believe the key to enhancing my application's performance lies in the GPT-4 calls. I was told making my prompt more specific should help, but I’m not sure how. If you have suggestions for improvement, feel free to make a pull request. I would love to learn from folks.

I had fun

Coding for fun is therapeutic. Hopefully, I make time to do this again.

Be easy on me

Like I mentioned, this is not a perfect application written in perfect code. I’m just sharing it as a means for learning in public, for insight, and to start a conversation.

Check it out

Decentralized to do list

You can access my decentralized to do list here: https://blackgirlbytes.github.io/decentralized-to-do-list/

If you want to improve it, you can make a pull request: https://github.com/blackgirlbytes/decentralized-to-do-list

Time Blind

You can access my Time Blind app here: https://time-blind-production.up.railway.app/

If you want to improve it, you can make a pull request: https://github.com/blackgirlbytes/time-blind

If it stops working, you can open an issue in my repository!

What are your opinions on the different technologies that I’ve leveraged? What tools and technologies should I try next? Comment below.

Did you find this article valuable?

Support Black Girl Bytes by becoming a sponsor. Any amount is appreciated!