Erfahrungsbericht: Softwareentwicklung mit ChatGPT4 - die Mastowall
===
2023-05-25 rstockm
![](https://pad.wolkenbar.de/uploads/5018257a-20b8-4a9f-9b70-a8bc25e92537.png)
Zu dem [hier vorgestellten Proof-of-Concept](https://github.com/rstockm/mastowall) einer Mastodon-Wall im Stile der klassischen Twitter-Walls kamen Fragen, wie ich konkret im Entwicklungs-Chat mit ChatGPT4 vorgegangen bin. Auch auf Mastodon wurde ich gefragt "wie der Prompt" war. Erwartbar war es nicht einer, sondern Dutzende. Aber nicht: Hunderte. Zunächst hatte ich mir ein paar recht klare Strategievorgaben zurechtgelegt:
- Konversation auf Englisch, da hier (mutmaßlich) über Trainingsquellen wie [Stackoverflow](https://stackoverflow.com/) bessere Ergebnisse zu erwarten seien
- Klare, übersichtliche Zielvorgabe um das "Projektziel" - eine Mastodon-Postingwall für die BiblioCon bereitstellen zu erreichen.
- Vorgabe der grundsätzlichen Technologie in einem minimalen Stack, den ich auch noch selbst einigermaßen überblicke und der auf jedem Webserver betrieben werden kann
- Schrittweise Herangehensweise, um überhaupt eine sinnvolle Fehlersuche betreiben zu können (Chain of thoughts Verfahren)
- Keine Zeile Code selber schreiben
Das "Projekt" dauerte dann netto etwa sechs Stunden, die sich ziemlich gleich auf drei Phasen aufteilten:
1. Holpriger Start und schnelle Erfolge
2. Feature-Ausbau und Stillstand
3. Refactoring und letzter Schliff
## 1. Holpriger Start und schnelle Erfolge
ChatGPT4 ist bekanntlich gut darin, Kontext zu behalten. Daher schien es mir sinnvoll, am Beginn etwas Kontext zu liefern:
![](https://pad.wolkenbar.de/uploads/7a9323b8-b0e7-4a8b-893c-c27afb14e5bf.png)
![](https://pad.wolkenbar.de/uploads/5e750905-5379-4d9f-8273-9158486f3371.png)
Dann wurde ich zügig konkret:
![](https://pad.wolkenbar.de/uploads/7001bfb2-00dd-4975-b2c7-06c442e48a41.png)
Ersichtlich wurde ab hier schon konkreter Code produziert, die bescheiden klingende Selbsteinschätzung, dass ChatGPT4 mir das nicht selbst programmieren könne, galt es ja gerade zu widerlegen. Wenn ChatGPT Code schreibt, wird er formatiert als Block ausgegeben und kann recht komfortabel kopiert und in das eigentliche Programm eingefügt werden.
Es folgten ca. 30 "verlorene" Minuten, da ChatGPT der Meinung war, wir würden für die Abfrage einen corsProxy benötigen, was in diverse Sackgassen führte. Hier wären nicht-Entwickler:innen wohl ausgestiegen, Grund ist vermutlich der veraltete Kenntnisstand von 2021, auch was die Mastodon API angeht.
Nachdem das behoben war, ging es jedoch rasend schnell. Diese Schritte erfolgten innerhalb von 5 Minuten. Meine Promts schreibe ich nun eingerückt, die vollständigen Antworten von ChatGPT sind der englische Text und die Codeblöcke:
> ok this works now. Please use now bootstrap to generate a three column design, and apply some basic "card-like" css to the postings with round edges and a soft shadow.
---
To create a three-column layout using Bootstrap, you would typically use the grid system. In Bootstrap, the grid system uses a series of containers, rows, and columns to layout and align content.
Here's an updated version of your code that uses Bootstrap to create a three-column layout and apply a "card" style to each post:
```html
<!DOCTYPE html>
<html>
<head>
<title>Mastodon Wall</title>
<!-- Include Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css">
<style>
/* Add some custom CSS for the cards */
.card {
margin-bottom: 20px;
border-radius: 10px;
box-shadow: 0 4px 6px 0 rgba(0, 0, 0, 0.2);
}
</style>
</head>
<body>
<div class="container">
<div class="row" id="mastodonWall"></div>
</div>
<script>
var mastodonServer = 'https://mastodon.social';
var corsProxy = 'https://cors-anywhere.herokuapp.com/';
var hashtag = 'bibliocon23';
fetch(corsProxy + mastodonServer + '/api/v1/timelines/tag/' + hashtag)
.then(function(response) {
return response.json();
})
.then(function(posts) {
var wall = document.getElementById('mastodonWall');
posts.forEach(function(post) {
var postElement = document.createElement('div');
postElement.className = 'col-lg-4';
postElement.innerHTML = `
<div class="card">
<div class="card-body">
${post.content}
</div>
</div>
`;
wall.appendChild(postElement);
});
});
</script>
</body>
</html>
```
In this code, the `container` and `row` classes are used to create a responsive grid. Each post is put in a `col-lg-4` div, which means it will take up one third of the row (since Bootstrap's grid system is based on 12 columns) on large screens and above. On smaller screens, the posts will stack vertically for better readability.
The `card` and `card-body` classes are used to create a "card" look for each post. Some custom CSS is added to give the cards rounded corners and a soft shadow.
This should give you a good starting point, but you may need to tweak the CSS or the Bootstrap classes to get the exact look you want.
> Excellent. Now add username, and userpicture to the cards, in the top left corner. And add relative time like "20 Seconds ago", changing to minutes if useful.
---
Here's how you can add the user's display name and avatar to the card, and display the relative time of each post. For this, you'll need to use a library like `date-fns` to help with date formatting. This library provides a `formatDistanceToNow` function that can create human-readable relative timestamps like "20 seconds ago".
However, since we're trying to keep this as a single HTML file, we'll write a very basic function to handle the time conversion. Please note that for a full-featured application, you'd likely want to use a library like `date-fns` or `moment.js`.
```html
<!DOCTYPE html>
<html>
<head>
<title>Mastodon Wall</title>
<!-- Include Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css">
<style>
/* Add some custom CSS for the cards */
.card {
margin-bottom: 20px;
border-radius: 10px;
box-shadow: 0 4px 6px 0 rgba(0, 0, 0, 0.2);
}
/* Position the avatar and username in the top left of the card */
.card-avatar {
height: 50px;
width: 50px;
border-radius: 50%;
position: absolute;
top: 10px;
left: 10px;
}
.card-username {
position: absolute;
top: 10px;
left: 70px;
}
</style>
</head>
<body>
<div class="container">
<div class="row" id="mastodonWall"></div>
</div>
<script>
var mastodonServer = 'https://mastodon.social';
var corsProxy = 'https://cors-anywhere.herokuapp.com/';
var hashtag = 'bibliocon23';
function timeSince(date) {
var seconds = Math.floor((new Date() - date) / 1000);
var interval = Math.floor(seconds / 31536000);
if (interval > 1) return interval + " years ago";
interval = Math.floor(seconds / 2592000);
if (interval > 1) return interval + " months ago";
interval = Math.floor(seconds / 86400);
if (interval > 1) return interval + " days ago";
interval = Math.floor(seconds / 3600);
if (interval > 1) return interval + " hours ago";
interval = Math.floor(seconds / 60);
if (interval > 1) return interval + " minutes ago";
return Math.floor(seconds) + " seconds ago";
}
fetch(corsProxy + mastodonServer + '/api/v1/timelines/tag/' + hashtag)
.then(function(response) {
return response.json();
})
.then(function(posts) {
var wall = document.getElementById('mastodonWall');
posts.forEach(function(post) {
var postElement = document.createElement('div');
postElement.className = 'col-lg-4';
postElement.innerHTML = `
<div class="card">
<img src="${post.account.avatar}" alt="${post.account.username}'s avatar" class="card-avatar">
<div class="card-username">${post.account.username}</div>
<div class="card-body">
${post.content}
<p>${timeSince(new Date(post.created_at))}</p>
</div>
</div>
`;
wall.appendChild(postElement);
});
});
</script>
</
body>
</html>
```
This code adds an `img` element for the avatar and a `div` for the username to each card. These elements are positioned in the top left of the card using CSS. The relative timestamp is added to the bottom of the card body. The `timeSince` function calculates the time since the post was created and formats it as a human-readable string.
---
Dieser Schritt ist m.E. bemerkenswert und war auch der Punkt an dem ich dachte: das kann richtig gut werden. Das ChatGPT hier in der Lage ist a) den Überblick über einen komplexer werdenden Code zu behalten, mir b) mitteilt, wie man eine relative Zeitfunktion eigentlich über eine Bibliothek einbinden würde, dann aber c) meinen ursprünglichen Wunsch "so leichtgewichtig wie möglich" respektiert, und mir eine perfekt funktionierende Zeitfunktion mitgibt, hätte ich so nicht erwartet.
Es kamen dann einige Verfeinerungen, gerade des CSS, und auch mit so einer Vorgabe gab es keine Probleme, da ich nicht mehr immer das ganze Script auswechseln wollte:
![](https://pad.wolkenbar.de/uploads/24be6c63-eb16-4fdc-b45a-c61877dc21bd.png)
Eine kritische Stelle: Umstellung der Verteilung der Karten auf der Seite auf ein für Twitter-Walls so typisches [Masonry-Layout](https://kulturbanause.de/blog/responsive-masonry-layout-mit-css/). Hätte das nicht funktioniert, wäre das Projekt wohl mangels professioneller Optik nicht weitergegangen. Wird aber prompt erledigt:
![](https://pad.wolkenbar.de/uploads/2d665b9c-9033-43e2-ad31-a984803b1da4.png)
Es folgte das Einbauen von Bildern in Postings
![](https://pad.wolkenbar.de/uploads/ab04ba24-470d-40ef-b543-057d12710319.png)
Sowie weitere Verfeinerungen des Layouts und der Anzeige. Nach ca. zwei Stunden war damit das Kernziel erreicht, und ich hätte etwas für die BiblioCon Einsetzbares gehabt.
## 2. Feature-Ausbau und Stillstand
Um selber den Überblick zu behalten, ließ ich den Code in drei Dateien aufteilen: das .html Gerüst, die .js JavaScript Funktionen sowie ein .css Stylesheet mit den Formatierungen. Das konnte immer noch in einem einzigen Webordner laufen, lief also nicht dem Grundziel zuwider.
Es folgten Aufgabenstellungen wie dynamische Übergabe der Hashtags über URL-Parameter:
![](https://pad.wolkenbar.de/uploads/c45374c2-d7fe-4e33-b0a9-38eee387740b.png)
Zu diesem Zeitpunkt hatte ich das Ziel aufgrund der schnellen Erfolge ausgeweitet: nun sollte ein Service entstehen, der möglichst flexibel von anderen nachgenutzt werden konnte.
Die folgende Stunde war nun ein hin und her, dass sich hier ganz gut ablesen lässt:
![](https://pad.wolkenbar.de/uploads/14c2d34b-d3aa-48b5-b7c2-136235b35556.png)
Mit jedem neuen Feature wuchs die Wahrscheinlichkeit, dass bereits erreichte Schritte wieder eingerissen wurden. An einer Stelle war das komplette CSS zerschossen (Einführung einer Mobilansicht, mehrere Breakpoints) und so weiter. Auch das nicht unüblich für ein normales Softwareprojekt, aber der Testaufwand stieg deutlich, wohingegen die ersten zwei Stunden fehlerfrei liefen.
Nach vier Stunden war ein Punkt erreicht, an dem es nicht mehr sinnvoll vor und zurück ging. Zu dem Zeitpunkt schickte ich mit jedem Prompt auch den kompletten Quelltext mit, da ChatGPT zunehmend Schwierigkeiten hatte sich seinen Code "zu merken". Technisch wenig erstaunlich, da die eigenen Antworten intern auch nur als Tokens gespeichert werden
![](https://pad.wolkenbar.de/uploads/fcaa678d-d107-41e7-83a2-b9ff769b81e4.png)
Aber auch das führt irgendwann nicht weiter. An so einem Punkt gibt es aber einen sinnvollen Ausweg:
## 3. Refactoring und letzter Schliff
In einem Refactoring-Schritt wird die komplette bisherige Codebasis neu gedacht auf Grundlage der bisherigen Erfahrungen und Probleme mit dem Code. Die Funktionen sollen exakt bestehen bleiben, der Code aber strukturierter, klarer, wartbarer und erweiterbarer werden.
Damit ich vor so einem recht radikalen Schritt aber zumindest den bisher erreichten, so eben einsetzbaren Stand nicht verlieren würde, habe ich ihn auf GitHub eingestellt. Auch da kann ChatGPT bei lästigen Aufgaben helfen:
![](https://pad.wolkenbar.de/uploads/df766595-e423-4dc9-87d4-59a0b3bc4076.png)
Dann der eigentliche Austausch zum Refactoring:
> The app is broken, again. I would like to discuss an observation: since the introduction of the config json, the robustness of our code dropped significantly. Every little change leads to excessive rounds of debugging. Anny suggestions, on how we could improve the robustness of our code to the level it was before, maybe some sort of refactoring?
Absolutely. When adding new features or making changes to existing code, it can introduce new bugs or other issues, especially if the codebase is complex or if there are many interdependent components. Here are a few suggestions on how we can improve the robustness of our code:
1. **Error Handling**: It's important to ensure our code can gracefully handle errors or unexpected inputs. For example, when retrieving data from an API or a file (like `config.json`), we should ensure that we're handling potential errors (like if the file or data doesn't exist, or isn't in the format we expect).
2. **Refactoring**: This involves restructuring existing code without changing its external behavior. The goal is to make the code more readable and easier to understand, which can make it easier to spot and fix bugs. We should aim to make each function do one thing well.
3. **Modularity**: This refers to designing our system in a way that different components (functions, classes, etc.) are independent of each other. This makes it easier to test and debug individual components, and changes in one component won't affect others.
4. **Testing**: Automated testing can help us quickly identify if changes we've made have unintentionally affected other parts of our system. Unit tests verify that individual components work as expected, while integration tests check that they work correctly together.
In your current scenario, it would be beneficial to refactor the code into smaller, more manageable functions, each with a single responsibility. This would make the code easier to read, understand, debug, and test.
For example, you could create separate functions for fetching data from the API, generating the HTML for the posts, updating the display, and handling user input. You could also create separate functions for each step of the initial setup process (like fetching the configuration, setting up the event listeners, and initializing the display).
Also, error handling can be added to the `fetchConfig` function to handle scenarios where the `config.json` file might not exist or might not be in the expected format.
Would you like me to refactor the code accordingly?
Natürlich wollte ich das, und dann bekomm man binnen Sekunden einen ziemlich anständig neu strukturierten Code. Das hätte auch erfahrene Javascript-Entwickler:innen mindestens eine Stunde gekostet, vermutlich mehr.
Mit dem Ergebnis konnte man deutlich besser die letzten Schritte angehen, etwa eine vierte Datei mit grundlegenden Konfigurationen im .json Format (Vorschlag von ChatGTP).
Das Refactorings verlief die letzte Stunde wieder recht produktiv und der Dienst erreichte einen Stand, wie er nun auf GitHub zu sehen ist:
https://github.com/rstockm/mastowall
Seit Montag läuft er für die BiblioCon ohne Fehler durch, und hat Stand gerade eben 419 Postings von 224 unterschiedlichen Teilnehmenden angezeigt:
https://rstockm.github.io/mastowall/?hashtags=111bibliocon,bibliocon,bibliocon23&server=https://openbiblio.social
## Update 1.1 Release - DOMPurify
Während der [Freakshow 266](https://freakshow.fm/fs266-mehr-bums) rufe ich dazu auf, das System mit schadhaften Mastodon-Postings zu stresstesten. @lukad@chaos.social gelingt das binnen Sekunden (!) über den Weg einer CSS-Anweisung, die im einem Content Warning Feld verpackt ist.
ChatGPT4 empfiehlt hier den Einsatz von DOMPurify, was das Problem in der Tat (bis auf weiteres) löst.
![](https://pad.wolkenbar.de/uploads/e731df3b-b0e2-49e1-899c-b318de5ac6cc.png)
Es zeigt aber klar: Security ist bei solchem Code nicht "by default" eingebaut, sondern man muss (!) sich selber/nachträglich kümmern.
## Fazit
An drei oder vier Stellen wäre man als Nicht-Entwickler:in sicher nicht weiter gekommen. Die vielen von mir hier veröffentlichten Prompts zeigen aber hoffentlich, dass wir hier ein neues Zeitalter der Softwareentwicklung beschreiten. ChatGPT4 wurde nicht zum Schreiben von Code optimiert, und dennoch ist man schon so weit gekommen. Und: alle Probleme wurden nur über Dialog gelöst. Bis zum Schluss habe ich keine Zeile Code selber geschrieben.
Das hätte ich auch in weiten Teilen gar nicht gekonnt: ich beherrsche kein JavaScript.