June 12th, 2023 Building a reddit clone with AI (and Adama/RxHTML) By Jeffrey M. Barber

Let’s combine all righteous anger going on at reddit and all the ai hype with chatgpt by building a reddit clone with AI. Then let’s have that clone be powered by Adama.

First, let’s create a frontpage or subreddit page.

Prompt: Create a HTML template using Tailwind CSS to mirror reddit

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Reddit Clone</title>
  <link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.16/dist/tailwind.min.css" rel="stylesheet">
</head>
<body class="bg-gray-100">
<header class="bg-white py-4 shadow">
  <div class="container mx-auto px-4">
    <nav class="flex items-center justify-between">
      <a href="#" class="text-2xl font-bold text-gray-800">Clone</a>
      <div>
        <a href="/" class="mr-4 text-gray-600 hover:text-gray-800">Home</a>
        <a href="/popular" class="mr-4 text-gray-600 hover:text-gray-800">Popular</a>
        <a href="/all" class="mr-4 text-gray-600 hover:text-gray-800">All</a>
      </div>
    </nav>
  </div>
  </header>
  <main class="container mx-auto px-4 py-8">
  <div class="flex">
    <div class="w-2/3 mr-4">
      <!-- Main Content -->
      <div class="bg-white p-4 shadow mb-4">
        <h2 class="text-xl font-bold mb-2">Post Title</h2>
        <p class="text-gray-600 mb-4">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
        <div class="flex items-center text-gray-600">
          <span class="mr-2">Posted by John Doe</span>
          <span class="mr-2">1 hour ago</span>
          <span class="mr-2">100 comments</span>
        </div>
      </div>
      <div class="bg-white p-4 shadow mb-4">
        <h2 class="text-xl font-bold mb-2">Another Post Title</h2>
          <p class="text-gray-600 mb-4">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
          <div class="flex items-center text-gray-600">
          <span class="mr-2">Posted by Jane Smith</span>
          <span class="mr-2">2 hours ago</span>
          <span class="mr-2">50 comments</span>
        </div>
      </div>
    </div>
    <div class="w-1/3">
      <!-- Sidebar -->
      <div class="bg-white p-4 shadow">
        <h2 class="text-xl font-bold mb-4">Sidebar</h2>
        <ul class="text-gray-600">
          <li class="mb-2"><a href="#" class="hover:text-gray-800">Category 1</a></li>
          <li class="mb-2"><a href="#" class="hover:text-gray-800">Category 2</a></li>
          <li class="mb-2"><a href="#" class="hover:text-gray-800">Category 3</a></li>
          <li class="mb-2"><a href="#" class="hover:text-gray-800">Category 4</a></li>
        </ul>
      </div>
    </div>
  </div>
  </main>
  <footer class="bg-gray-200 py-4">
    <div class="container mx-auto px-4 text-center">
      <span class="text-gray-600">© 2023 Clone. All rights reserved.</span>
    </div>
   </footer>
</body>
</html>

Is this wonderful? Well, as a back-end engineer, this is black magic. For a UX engineer, this may be the worst thing on the planet. However, given how awful the reddit UI looks, oh well. Let’s make this live!

I’ll create a space called ‘rclone’. In Adama, a space is collection of documents under the same Adama specification (a text file containing the schema + logic).

java -jar ~/adama.jar spaces create --space rclone

And then I’ll extract the body from the above HTML in an RxHTML forest

<forest>
  <!--- turn the ChatGPT output into a page as-is -->
  <page uri="/">
    <header class="bg-white py-4 shadow">
    ... see above ...
    </footer>
  </page>
  <!-- the shell generates the 200.html used for the SPA -->
  <shell inline=true body-class="bg-gray-100">
    <link rel="stylesheet" href="/style.css">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta charset="UTF-8">
  </shell>
</forest>

At this point, we don’t have a style sheet, so let’s setup by tailwind. First, let’s create ‘input.css’:

@tailwind base;
@tailwind components;
@tailwind utilities;

And create ‘package.json’:

{
  "devDependencies": {
    "@tailwindcss/forms": "^0.4.0",
    "@tailwindcss/typography": "^0.5.1",
    "tailwindcss": "^3.0.19"
  }
}

then install tailwind

npm install

Since I’m a fan of shell scripts, let’s make everything go live with a simple ‘ship.sh’ file:

#!/bin/sh
clear
npx tailwindcss -i input.css --content frontend.rx.html -o style.css
java -jar ~/adama.jar space upload --space rclone --file style.css
java -jar ~/adama.jar space set-rxhtml --space rclone --file frontend.rx.html

and then run it

chmod 755 ship.sh
./ship.sh

And now it’s live!.

At core, we need these pages:

  • main index summarizing all categories
  • category index (i.e. sub-reddit)
  • view submission
  • make a new post

So, let’s now build a back-end, so we start with an exceptionally simple data model.

record Comment {
  public int id;
  public principal author;
  public string comment;
  public datetime when;
}

record Submission {
  public int id;
  public string title;
  public string description;
  public string url;
  public string category;
  public datetime when;
  public principal submitter;

  table<Comment> _comments;
  public formula comments = iterate _comments;
}

table<Submission> _submissions;

We call the above file ‘backend.adama’, and we update the ship.sh script by appending:

java -jar ~/adama.jar space deploy --space rclone --file backend.adama

The pipeline is now setup, so let’s start pumping data with a form, and let’s have ChatGPT make it:

Create another HTML template that mirrors a reddit submit a post with a form including title, description, url, and a category

  <!-- header removed -->
  <main class="container mx-auto px-4 py-8">
    <div class="max-w-lg mx-auto bg-white p-6 shadow">
      <h2 class="text-2xl font-bold mb-4">Submit a Post</h2>
      <form>
        <div class="mb-4">
          <label for="title" class="block text-gray-700 font-bold mb-2">Title</label>
          <input type="text" id="title" name="title" class="w-full border border-gray-400 p-2 rounded focus:outline-none focus:border-indigo-500" required>
        </div>
        <div class="mb-4">
          <label for="description" class="block text-gray-700 font-bold mb-2">Description</label>
          <textarea id="description" name="description" class="w-full border border-gray-400 p-2 rounded focus:outline-none focus:border-indigo-500" required></textarea>
        </div>
        <div class="mb-4">
          <label for="url" class="block text-gray-700 font-bold mb-2">URL</label>
          <input type="url" id="url" name="url" class="w-full border border-gray-400 p-2 rounded focus:outline-none focus:border-indigo-500" required>
        </div>
        <div class="mb-4">
          <label for="category" class="block text-gray-700 font-bold mb-2">Category</label>
          <input type="category" id="category" name="category" class="w-full border border-gray-400 p-2 rounded focus:outline-none focus:border-indigo-500" required>          
        </div>
        <div class="text-right">
          <button type="submit" class="px-4 py-2 bg-indigo-500 text-white font-semibold rounded hover:bg-indigo-600">Submit</button>
        </div>
      </form>
    </div>
  </main>
  <!-- footer removed -->

At this point, we have redundant header and footer elements. Let’s combine them into a template!

<template name="nav">
  <header class="bg-white py-4 shadow">
    <div class="container mx-auto px-4">
      <nav class="flex items-center justify-between">
        <a href="/" class="text-2xl font-bold text-gray-800">Clone</a>
        <div>
          <a href="/" class="mr-4 text-gray-600 hover:text-gray-800">Home</a>
          <a href="/post" class="mr-4 text-gray-600 hover:text-gray-800">Post</a>
        </div>
      </nav>
    </div>
  </header>
  <fragment />
  <footer class="bg-gray-200 py-4">
    <div class="container mx-auto px-4 text-center">
      <span class="text-gray-600">© 2023 Clone. All rights reserved.</span>
    </div>
  </footer>
</template>

and then use it for both “/” and “/post”

  <page uri="/">
    <div rx:template="nav">
      <main class="container mx-auto px-4 py-8">
      ...
      </main>
    </div>
  </page>
  <page uri="/post">
    <div rx:template="nav">
      <main class="container mx-auto px-4 py-8">
      ...
      </main>
    </div>
  </page>

OK, so… let’s get data flowing! First, we need to allow connections to a document and allow the document to be created on demand. We update backend.adama by prefixing the file with the below static policy and document event.

static {
  create {
    return true; // anyone can create it
  }
  invent {
    return true; // anyone can invent
  }
}

@connected {
  return true;
}

This is very open! Perhaps, too open. Now, we need to connect to a document. We update the nav template by wrapping <fragment /> with a <connection>

  ...
  </header>
  <connection identity="direct:anonymous:anony" space="rclone" key="somekey">
    <fragment />
  </connection>
  <footer class="bg-gray-200 py-4">
  ...

This connection is going to use a fixed identity (i.e. anonymous agent named anony) for all users. This is a problem, but we move on.

We hook the form up to data by create a channel called submit_post and append the following to the backend.adama:

message SubmissionMsg {
  string title;
  string description;
  string url;
  string category;
}

channel submit_post(SubmissionMsg s) {
  _submissions <- {
    title: s.title,
    description: s.description,
    url: s.url,
    category: s.category,
    when: Time.datetime(),
    submitter: @who
  };
}

We then hook the form in /post to the channel by adding an ‘rx:action’ to the form element to send (we also set a success handler to goto /):

...
<h2 class="text-2xl font-bold mb-4">Submit a Post</h2>
<form rx:action="send:submit_post" rx:success="goto:/">
   <div class="mb-4">
...

In theory, data can flow into the document. Now, let’s pull it out! We do this by exposing data from the table via a formula (appending this to backend.adama):

public formula posts = iterate _submissions limit 20;

We now, edit the root page by templatizing the post listing.

<div class="w-2/3 mr-4" rx:iterate="posts">
  <!-- Main Content -->
  <div class="bg-white p-4 shadow mb-4">
    <h2 class="text-xl font-bold mb-2"><a href="/v/{category}/{id}"><lookup path="category" />/<lookup path="title" /></a></h2>
    <p class="text-gray-600 mb-4"><lookup path="description" /></p>
    <div class="flex items-center text-gray-600">
      <span class="mr-2">Posted by <lookup path="submitter" transform="principal.agent" /></span>
      <span class="mr-2"><lookup path="when"></span>
      <span class="mr-2"><lookup path="num_comments"> comments:</span>
    </div>
  </div>
</div>

The rx::iterate will walk the records inside the posts field exposed via the formula. The lookup’s will pull data out of the record.

Let’s make categories work.

message CategoryReport {
  string category;
  int count;
}

procedure make_categories() -> list<CategoryReport> readonly {
  var m = iterate _submissions reduce category via @lambda x: x.size();
  table<CategoryReport> report;
  foreach (kvp in m) {
    report <- {category: kvp.key, count:kvp.value};
  }
  return iterate report;
}

public formula categories = make_categories();

This is fairly lame and not super efficient, but we could denormalize the categories. However, we then update the root page template to iterate the categories.

The template introduced a new category page, so let’s bring build it by simplifying / with a new template called “post”:

<template name="post">
  <div class="bg-white p-4 shadow mb-4">
    <h2 class="text-xl font-bold mb-2"><lookup path="category" />::<lookup path="title" /></h2>
    <p class="text-gray-600 mb-4"><lookup path="description" /></p>
    <div class="flex items-center text-gray-600">
      <span class="mr-2">Posted by <lookup path="submitter" transform="principal.agent" /></span>
      <span class="mr-2">1 hour ago [TODO]</span>
      <span class="mr-2">100 comments: [TODO]</span>
    </div>
  </div>
</template>

and then create a new page with a dynamic uri:

<page uri="/c/$current_category:text">
  <div rx:template="nav">
  <main class="container mx-auto px-4 py-8">
    <div class="flex">
      <div class="w-full mr-4" rx:iterate="posts">
        <div rx:template="post"></div>
      </div>
    </div>
  </main>
  </div>
</page>

Notice, the uri has a parameter which is stored in the view state. The view state is sent with a connection, and we can change the posts field to a bubble which can leverage the viewer state.

view string current_category;
bubble posts = 
  iterate _submissions
  where  @viewer.current_category == category || @viewer.current_category == ""
  limit 20;

We then update the root page with

   <div rx:template="nav" rx:load="set:current_category=">

So we can share the same ‘posts’ field between the / and child categories.

OK, so now, let’s add a new page to view the submission along with comments by asking ChatGPT for some more HTML

Create another HTML template for a reddit post showing the title, description, and then comments along with a form to post a comment

  <main class="container mx-auto px-4 py-8">
    <div class="max-w-lg mx-auto bg-white p-6 shadow">
      <h2 class="text-2xl font-bold mb-4">Post Title</h2>
      <p class="text-gray-600 mb-4">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>

      <div class="mb-4">
        <h3 class="text-xl font-bold mb-2">Comments</h3>
        <div class="bg-gray-200 rounded-lg p-4">
          <div class="mb-4">
            <div class="flex items-start">
              <div class="w-10 h-10 rounded-full bg-gray-400"></div>
              <div class="ml-2">
                <span class="font-bold">John Doe</span>
                <span class="text-gray-600">1 hour ago</span>
              </div>
            </div>
            <p class="mt-2 text-gray-800">Comment 1</p>
          </div>

          <div class="mb-4">
            <div class="flex items-start">
              <div class="w-10 h-10 rounded-full bg-gray-400"></div>
              <div class="ml-2">
                <span class="font-bold">Jane Smith</span>
                <span class="text-gray-600">2 hours ago</span>
              </div>
            </div>
            <p class="mt-2 text-gray-800">Comment 2</p>
          </div>
        </div>
      </div>

      <div class="mb-4">
        <h3 class="text-xl font-bold mb-2">Post a Comment</h3>
        <form>
          <div class="mb-4">
            <label for="name" class="block text-gray-700 font-bold mb-2">Name</label>
            <input type="text" id="name" name="name" class="w-full border border-gray-400 p-2 rounded focus:outline-none focus:border-indigo-500" required>
          </div>
          <div class="mb-4">
            <label for="comment" class="block text-gray-700 font-bold mb-2">Comment</label>
            <textarea id="comment" name="comment" class="w-full border border-gray-400 p-2 rounded focus:outline-none focus:border-indigo-500" required></textarea>
          </div>
          <div class="text-right">
            <button type="submit" class="px-4 py-2 bg-indigo-500 text-white font-semibold rounded hover:bg-indigo-600">Submit</button>
          </div>
        </form>
      </div>

    </div>
  </main>

Looks hook this into /v/$category/$current_post by first exposing the current post using the viewer state and a channel to write a comment.

view int current_post_id;
bubble current_post =
  (iterate _submissions where id == @viewer.current_post_id)[0];

message WriteComment {
  int id;
  string comment;
}

channel write_comment(WriteComment wc) {
  if( (iterate _submissions where id == wc.id)[0] as sub) {
    sub._comments <- {
        author: @who,
        comment: wc.comment, 
        when: Time.datetime()
    };
  }
}

We then mix the AI’s result with RxHTML to produce the submission’s page

<main class="container mx-auto px-4 py-8" rx:scope="current_post">
  <div class="max-w-lg mx-auto bg-white p-6 shadow">
    <h2 class="text-2xl font-bold mb-4"><lookup path="title" /></h2>
    <p class="text-gray-600 mb-4"><lookup path="description" /></p>
    <div class="mb-4">
    <h3 class="text-xl font-bold mb-2">Comments</h3>
    <div class="bg-gray-200 rounded-lg p-4" rx:iterate="comments">
    <div class="mb-4">
      <div class="flex items-start">
        <div class="w-10 h-10 rounded-full bg-gray-400"></div>
          <div class="ml-2">
              <span class="font-bold">
                <lookup path="author" transform="principal.agent" />
              </span>
              <span class="text-gray-600"><lookup path="when"></span>
            </div>
          </div>
          <p class="mt-2 text-gray-800"><lookup path="comment"></p>
        </div>
      </div>
    </div>
  </div>
  <div class="mb-4">
    <h3 class="text-xl font-bold mb-2">Post a Comment</h3>
    <form rx:action="send:write_comment" rx:success="reset">
      <input type="hidden" name="id" value="{view:current_post_id}" />
      <div class="mb-4">
        <label for="comment" class="block text-gray-700 font-bold mb-2">Comment</label>
        <textarea id="comment" name="comment" class="w-full border border-gray-400 p-2 rounded focus:outline-none focus:border-indigo-500" required></textarea>
      </div>
      <div class="text-right">
        <button type="submit" class="px-4 py-2 bg-indigo-500 text-white font-semibold rounded hover:bg-indigo-600">Submit</button>
      </div>
    </form>
  </div>
</main>

A big thing missing right now is an easy to understand default identity provider, so everyone is named “anony”. However, we can observe that a language model like ChatGPT could generate and bind the templates to a backend.

Future

This feels exciting… and scary.

The important thing is that the declarative nature of HTML works OK with an LLM, and if we “fix” HTML instead of building yet another JavaScript framework, then we have a new medium to build front-ends. It’s interesting to see how to generate Adama code, but that requires more investment.

My prediction is that LLM’s are a sea change in how we think about building products, and there may be more fruitful intermediate languages for language developers to invest in. Instead of investing energy into teaching the machine to produce software, we can map the domain of human needs rather than technical needs.

For example, I’m not sure if I would trust code generated from an LLM to run bare metal, but I could see myself leveraging tools to build UI and related data models to target a safe platform like Adama that is easier to audit for problems.