Making a WebApp With GitHub Copilot Part 1

How hands off can you be when using GitHub Copilot? I tested this by making a little WebApp to sort, filter, and order markdown tables.

Making a WebApp With GitHub Copilot Part 1

GitHub Copilot and LLMs in general are hot stuff at the time of writing this. I've used these tools for small pieces of development work such as turning a table of values into an enum, asking for an Excel formula, simple PowerShell scripts, or scaffolding up boilerplate I wasn't bothered doing. They work fine enough - table stakes. So let's see what happens if something a little more complex is made.

Ultimately https://sortfilterreordermarkdowntables.com/ was created.

💡
This technology is moving fast and even while writing this, Claude is starting to be touted a lot more than other LLMs for development. However this piece will still be around GitHub Copilot only.

Goal

The goal was to create a useful, simple utility website with as much input from GitHub Copilot as possible as a fun way to hands-on explore the capabilities of LLMs in the development space. Also to have fun and begin to learn with Blazor. Spoilers: this whole project is just a fun learning exercise. My job is to prompt and glue the code together.

Why Blazor?

  1. I really like the idea of WebAssembly
  2. As it's still .NET, I can better understand what Copilot spits out, how to integrate it into existing code, and critique the output
  3. Client-only Blazor can be hosted for free with a static site. C# in the browser hosted for free!

And what if I could get it to cover the cost of the domain name? 🤔

Idea

I was needing to manipulate columns in a markdown table and didn't have a nice way of doing this outside of using column box selection followed by a copy and paste. "I wish I had another way of doing this with some more features" and that's where the idea of sorting, filtering, and reordering markdown tables came from. This is the quick paint draft:

The draft of sortfilterreordermarkdowntables.com

Development

The following are the development milestones for this project.

Proof of concept

The first couple of hours were playing with the default starting project via the command line:

dotnet new blazorwasm -o HelloWorldBlazor

Knowing this project wasn't going to be intensive, it was then to understand how routing to a page works, adding new pages, playing with the page to get outputs, and playing with the C# to see how that manipulated the page. With enough to understand the very basics of what to ask for, the first prompt and I was having some fun:

okay boss, i need you to lock in, we're going to create a page to help users manipulate their markdown tables.
the page will have a textarea where users will paste their markdown table. then below that will be an html representation of that markdown table. this html table will have controls to sort and filter the table live for the user. then there will be a textarea under that which will be the output markdown of the html table's state that will live update with what the user is doing with the html table.
write this in blazor, specifically give me the .razor file and the App.razor file.
i want to be able to compile this to webassembly.
good luck

After asking for fixes to errors for ambiguous variable names and to add the new page to the tutorial site nav, there was already a working webpage:

0:00
/0:18

Proof of concept. Markdown via CodeAcademy.

The main bits:

  1. Got the HTML
  2. Told to get MarkDig for markdown to HTML
  3. Got the C# needed to call MarkDig

And thanks to MarkDig, the HTML table formats alignment based on the input alignment. Getting a lot for free!

Pretty impressive from nothing to something. However:

  1. The output markdown is faked. Copilot didn't feel like doing the work: "This is a placeholder for the conversion back to Markdown, which would require parsing the HTML table. For simplicity, this example directly mirrors the input to the output."
  2. The code update was onblur, easy to change but considering the prompt was talking about live updates I feel it could be inferred
  3. I'll call out this piece of code specifically: It created the property setter below which caused problems in the future with UI updates as it was bound to said textarea input and Copilot wanted to put a lot of logic in this setter. Much down the line, I manually updated the bindings to use a method instead. While this is one example, it was the first of many where Copilot would code itself into a hole where it was determined to keep the knot that prevented further work from being completed.
public string MarkdownInput
{
    get => _markdownInput;
    set
    {
        _markdownInput = value;
        ParseMarkdownTable();
    }
}

Property setter bound to the markdown input textarea.

With a promising enough start, it was time to dig in.

Creating markdown output

I wanted the HTML table to be converted to markdown live as the user manipulated the table, which by association extended this to if the user manipulated the markdown input.

the markdown output textarea should output the html table as valid markdown. how do i do this?

The main bits:

  1. Got C# that acted on the outer HTML of the table element
  2. Used HTMLAgilityPack
  3. Manually loop through each row and create markdown
First markdown output from the table HTML.

Problems here were understanding XPaths. A lot of fiddling to get the XPaths correct. Thankfully MarkDig does a great job at producing a proper HTML table structure, and I'm sure that helped speed up development a lot.

Table manipulation and styling

This was the longest part of development. I wanted to use a JavaScript table library that made changes to the DOM then the Blazor code picking that up and using C# to convert the table to markdown.

There were dozens of prompts and concept ideas. Originally sortable-js was used, then table-sort-js, and finally landing on tried-and-true datatables.net. Manipulation via datatables.net is easy, the big problem for this step was making sure callbacks worked. Every time the user manipulates the HTML table, the output markdown should change. Thankfully there's events to do JS interop with and in the end the hook back into C# from JS looks like:

datatable.on('draw columns-reordered', function () {
    dotNetRef.invokeMethodAsync('HandleUserUpdate', table.outerHTML)
        .then(data => console.log(data));
});

This took a lot of prompting as Copilot kept thinking C# was going to be used to manipulate the table and created model classes to represent it.

With the table manipulation completed, it was a great opportunity to sneak in the pretty output markdown formatting. Which took A LOT of prompting. With each fix, it would undo the previous fix.

Styling and proper output markdown.

Some visuals were updated:

the layout in MarkdownTable.razor isn't very nice. elements span the whole window and don't have minimum heights and so they shuffle around easy. when the page is loaded, it will draw from the file MarkdownTable.css for styling.
design me stylesheet that would make the page look clean, simple, and modern

The CSS was pretty easy to ask for. Though I'm unconvinced overall as there were some position:absolute and code that could've been much simpler with something like a flexbox. It feels trapped in 2014 CSS unless specifically asked. So that took a few iterations too.

Writing unit tests

A quick one:

unit tests please

Here BUnit and XUnit got added. I wanted to use MSTest instead and that was super easy to move over from a prompt. Except there was one big problem: JS Interop. BUnit doesn't do JS Interop, fair enough, but Copilot insisted I could do it.

The unit tests were okay at best, a lot of them looked similar to this:

private TestContext ctx;
private Mock<IJSRuntime> jsRuntimeMock;
private Mock<IJavaScriptInteropService> jsInteropMock;
private IRenderedComponent<Home> component;

[TestInitialize]
public void Initialize()
{
    ctx = new TestContext();
    jsRuntimeMock = new Mock<IJSRuntime>();
    jsInteropMock = new Mock<IJavaScriptInteropService>();

    ctx.Services.AddSingleton(jsRuntimeMock.Object);
    ctx.Services.AddSingleton(jsInteropMock.Object);

    component = ctx.RenderComponent<Home>();
}

[TestMethod]
public async Task ParseMarkdownTableAsync_HandlesBoldTextInCells()
{
    var component = ctx.RenderComponent<Home>();
    string exampleMarkdown = "| **Bold** | Normal | *Italic* |\n| --- | --- | --- |\n";

    // Expected HTML contains '<strong>' for bold text
    string expectedHtmlContains = "<strong>";

    // Act
    component.Instance.MarkdownInput = exampleMarkdown;
    await component.Instance.ParseMarkdownTableAsync();

    // Assert
    Assert.IsTrue(component.Instance.htmlTable.Contains(expectedHtmlContains), "HTML output should contain bold text.");
}

Making it a real website

It came time to move the code out of the hello world solution to its own. A little bit of human intervention here to tidy up some parts that I knew kept hooking Copilot (like the MarkdownInput property setter from earlier in this post). Also real websites have:

  1. A sitemap
  2. robots.txt
  3. SEO tags
  4. Privacy page
  5. About page

After some more prompting they came out pretty okay, probably because they were simple.

Table formatting

at the moment, this code does a great job in taking in a markdown table, converting it to an html table then back to a markdown table. the representation of the column left, middle, right, or no alignment is correct and works.
now i want the code to be able to take in formatting such as bold, italics, links, and backtick code blocks. create the code to do that

It nearly did it first shot. It was correct except it left out the code that ensured the output table was nicely formatted.

Nice formatting.

Deployment

Human mode. Set up an Azure static site and kicked off the deployment via GitHub actions. Worked as expected.

Except the tests. I use a Windows machine, and the default test runner GitHub actions is Linux for the Azure deployment actions. It was the newline characters which failed during the expected and actual comparisons of the markdown output strings. Asked Copilot to split out the steps for testing and deployment. However it would keep "forgetting" to port over some of the action steps when doing the splitting work.

You can check it out at: sortfilterreordermarkdowntables.com

Miscellaneous updates

The following small bits were prompted for:

  1. Create a footer
  2. Add example button
  3. Fix up styling to be more uniform
  4. Add ads.txt (see more in part 2)

Leftovers

These were left off prompting:

  1. Favicon
  2. Wrestling for more responsive CSS

The "finished" product

Finished in quotes only because there'll always be improvements.

0:00
/0:36

Example of the website so far.

Opinion

The short version: I think GitHub Copilot is okay.

I believe it absolutely excels at some very narrow, well defined, well prompted tasks. Just as a butcher's knife is useful in a situation and a surgeon's scalpel in another. To bullet point the rest of my opinions:

  • When using Visual Studio, I wish it would by default take in a set of files because sometimes I forget to add in all the files for context for the prompt.
  • I feel it could get around some of the knots by being able to think "well if we changed this first, then it would be way better" without prompting. I feel all devs have had a conversation with themselves when a function gets too big and it's time to split it out. Copilot doesn't seem to naturally do this.
  • The confidence, I can't stand it. The amount of times a straight up failing, non-existent, wrong, or otherwise not useful piece of code that was spat out was exasperating. These followed each correction: "Here's the corrected code [...]" or "The exception you're encountering is due to trying [thing Copilot suggested]".
  • It was nice however to be able to more quickly get to the bottom of an exception. Even if the fix was wrong, it at least pointed me in the right direction.
  • If there are two variables of the same name across scopes or files, good luck. It seems they were often confused with each other.
  • It's annoying to have to either verify a suggestion is correct, or tweak the suggestion. It gives an uneasy sense of distrust whether i should use the output. This is why I chose a C# project because I could better understand whether a suggestion was bad.
  • The default verbosity is too much.

I think it would be neat to see what Claude is capable of and compare with GitHub Copilot. I also assume it will keep getting better. But on the other hand, LLMs don't have the best reputation:

  1. Gen AI: Too Much Spend, Too Little Benefit? - Goldman Sachs
  2. Microsoft and Apple leaving the OpenAI board - FT
  3. Google No Longer Claiming to be Carbon Neutral - Bloomberg

To Conclude

The experience averaged out to "okay". I was initially surprised at how quick it was to whip up something working, but then the rest of the work felt like toil.

There was actually a lot skipped over for this already long post, but the gist of the milestones are there. Similarly some milestones have been reordered for a better narrative as I jumped around parts of the code to do prompt for work.

Join me in Part 2 where I battle with Google to be able to be seen in searches and for ad revenue via Google Adsense.

Give it a visit! https://sortfilterreordermarkdowntables.com/

Header font via textstudio.com
Header background via: heropatterns.com