Kritische vragen over gevibecode tools

Met AI bouwt iedereen in een middag zijn eigen tool. Fantastisch, maar wie controleert of de uitvoer klopt? Over de tool die je zelf bouwt, en drie vragen die je erbij moet stellen.

In mijn werk voer ik soms testen uit met bestanden. De manier om die bestanden te verzenden is met base64-codering. Om de bestanden zelf te kunnen lezen, moet je ze decoderen. Dat is niet moeilijk, daar zijn genoeg tooltjes voor. Wel vereist dit het nodige klikwerk, of een tooltje op het internet. En daar wil je niet zomaar elk bestand in delen.

Dus maakte ik er zelf een

Met GitHub Copilot werd er in korte tijd een tool gegenereerd in html. Later voegde ik nog wat extra features toe. Hij laat meteen de extensie van het bestand zien (bijvoorbeeld .pdf of .txt), en als het een pdf is opent hij die direct in een nieuw tabblad, zodat ik niet meer hoef te klikken.

Schermafbeelding van de zelfgebouwde Base64 Converter: links een ingeplakte Base64-string, rechts de melding dat een PDF is gedetecteerd en in een nieuw tabblad is geopend. Onderaan staat het herkende bestandsformaat: PDF-document (.pdf).
Mijn eigen, volledig gepersonaliseerde base64-converter: Base64 erin, het bestandsformaat wordt herkend en een pdf opent direct in een nieuw tabblad. Zelf proberen? De broncode staat onderaan dit artikel.
Een klein voorbeeld van hoe laag de drempel tegenwoordig is om te ontwikkelen.

Voor de duidelijkheid

Dit zijn dus geen tools die AI gebruiken. Ik heb het over tools die dóór AI gebouwd worden. Ik zie dit om me heen continu gebeuren.

Het blijft niet bij converters

Een paar voorbeelden uit mijn werkveld:

Visualisatie van alle verbindingen

Laat op basis van de codebase alle verbindingen tussen API’s en webservices zien. Binnen een complex IT-landschap enorm inzichtelijk om impact te bepalen en te zien waar regressierisico zit.

Dashboard met dagelijkse uitval

Geeft op basis van een export van de errorqueues dagelijks alle uitval weer, inclusief historie. Functioneel beheer was eerst wat onwennig, maar al snel waren ze enorm blij met het extra inzicht.

Een middagje prompten

Voor dat dashboard was voorheen nooit tijd en geld. Nu is het met een middagje prompten, een paar refactorslagen en wat feedbackloopjes voor elkaar. Fantastisch toch? Meer productiviteit, meer creativiteit, meer mogelijkheden…

Toch zie ik ook een keerzijde.

De oplossing en het probleem komen uit dezelfde hoek

Kijk nog even naar waarom ik die converter zelf bouwde. Omdat ik bedrijfsbestanden niet in een willekeurige website wilde plakken. Zelf bouwen was de veilige keuze. Maar datzelfde zelfbouwen betekent ook: geen review, geen onafhankelijke test, geen beheer.

Bij mijn converter maakt dat weinig uit. Als hij niet klopt, zie ik dat meteen. Het bestand opent of het opent niet. Bij die visualisatie van alle verbindingen ligt dat anders.

Een complete plaat, alleen staat er niet alles op

Stel dat hij een verbinding mist. Een endpoint dat uit configuratie komt. Iets dat via een queue loopt. Een URL die ergens in code wordt samengesteld. Dan zie je dat niet. Er komt geen foutmelding, er blijft geen plek leeg. Je ziet een complete, overzichtelijke plaat. Alleen staat er niet alles op.

Wat de tool laat zien versus wat er werkelijk draait

Wat de tool laat zien

Webportaal API-gateway Orderservice Klantdata Facturatie Notificaties

Overzichtelijk, compleet ogend, zonder foutmelding.

Wat er werkelijk draait

queue config URL in code Webportaal API-gateway Orderservice Klantdata Facturatie Notificaties

Drie verbindingen ontbreken in de plaat, zonder dat iemand het merkt.

Gevonden door de tool Gemist, en dus onzichtbaar
Illustratief voorbeeld. Een gemiste verbinding laat geen gat achter: de plaat oogt net zo compleet als wanneer alles erop staat.

En dan wordt precies de tool die ons moest helpen regressierisico te vinden, zelf een bron van regressierisico.

Niet elke zelfgebouwde tool verdient aandacht

Daar zit volgens mij het verschil. Het gaat om tools waarvan je de uitvoer niet zelf kunt nalopen, en waar toch beslissingen op volgen. Over impact. Over prioriteiten. Over waar we wel en niet gaan testen.

Welke tool verdient een kritische blik?
Volgen er beslissingen op?
Ja
ControleerbaarUitvoer na te lopenHoe groter de beslissing, hoe meer controle de tool verdient.
Verdient aandachtVisualisatie van verbindingenEen gemiste verbinding zie je niet, en toch bepaalt de plaat impact, prioriteiten en testscope.
Nee
Weinig risicoBase64-converterHet bestand opent of het opent niet.
Beperkte impactNiet na te lopenEr hangt geen beslissing aan.
Ja
Nee
Kun je de uitvoer zelf nalopen?
Het risico zit in de combinatie: uitvoer die je niet zelf kunt controleren, met beslissingen die erop volgen.

Sneller ‘live’ dan wenselijk

Bij het ‘normale’ softwareontwikkelproces zit er een traject van reviewen en testen tussen voordat functionaliteit in gebruik wordt genomen. Deze tools worden meestal door één persoon gemaakt, en gaan daardoor sneller ‘live’ dan wenselijk is. Tegelijk worden ze met enthousiasme geadopteerd. We raken eraan gewend. We worden er afhankelijk van.

Weten we binnen het team eigenlijk van elkaar welke tools er rondgaan? En welke keuzes daarop gebaseerd worden?

Drie vragen bij elke tool die langskomt

Voor elk team is het een zoektocht hoe hiermee om te gaan. Een paar vragen stel ik inmiddels wel bij elke tool die langskomt:

1
Welke beslissingen nemen we op basis van deze uitvoer? Hoe groter de beslissing, hoe meer controle de tool verdient.
2
Heeft iemand de uitvoer weleens naast de werkelijkheid gelegd? Neem een paar dingen die je zeker weet en kijk of de tool ze ook laat zien.
3
Kan de tool zeggen wat hij niet weet? Een tool die meldt wat hij heeft overgeslagen, kun je vertrouwen. Een tool die dat stil doet niet.

Geen ingewikkelde vragen, wel belangrijk om bij stil te staan.

Iedereen kan bouwen

Het is geweldig welke wereld aan mogelijkheden nu binnen handbereik ligt. Iedereen kan bouwen. Beoordelen of het ook klopt, daar blijft een kritische blik voor nodig.

Zelf proberen

De broncode van de base64-converter

De converter uit dit artikel hoeft niet gehost te worden: het is één html-bestand dat je lokaal in je browser draait. Alles gebeurt op je eigen computer; er wordt niets naar internet verstuurd.

  1. Download het bestand met de knop hieronder, of kopieer de code en sla die op als base64-converter.html.
  2. Open het bestand door erop te dubbelklikken. Het opent in je standaardbrowser.
  3. Plak een Base64-string in het invoerveld. Opent een pdf niet? Sta dan pop-ups toe voor dit bestand.
Bekijk de broncode (470 regels)base64-converter.html
<!DOCTYPE html>
<html lang="nl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Base64 Converter</title>
<style>
  :root {
    --bg: #101820;
    --panel: #17232f;
    --panel-2: #1f3140;
    --text: #edf6f9;
    --muted: #9fb1bd;
    --accent: #2ec4b6;
    --accent-2: #ffbf69;
    --danger: #ff6b6b;
    --border: #314657;
  }

  * { box-sizing: border-box; }

  body {
    margin: 0;
    min-height: 100vh;
    font-family: "Trebuchet MS", "Segoe UI", sans-serif;
    background:
      radial-gradient(circle at top left, rgba(46, 196, 182, .18), transparent 34rem),
      linear-gradient(135deg, #101820 0%, #16242d 54%, #1b2f3a 100%);
    color: var(--text);
    padding: 28px;
  }

  main {
    width: min(1100px, 100%);
    margin: 0 auto;
  }

  h1 {
    margin: 0 0 8px;
    font-size: clamp(28px, 4vw, 48px);
    letter-spacing: 0;
  }

  .subtitle {
    margin: 0 0 22px;
    color: var(--muted);
    max-width: 760px;
    line-height: 1.5;
  }

  .toolbar,
  .status,
  textarea {
    border: 1px solid var(--border);
  }

  .toolbar {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
    align-items: center;
    background: rgba(23, 35, 47, .92);
    border-radius: 8px;
    padding: 12px;
    margin-bottom: 16px;
  }

  button {
    border: 0;
    border-radius: 7px;
    padding: 10px 14px;
    color: #071417;
    background: var(--accent);
    font-weight: 700;
    cursor: pointer;
  }

  button.secondary { background: var(--accent-2); }
  button.ghost {
    background: transparent;
    color: var(--text);
    border: 1px solid var(--border);
  }
  button:hover { filter: brightness(1.08); }

  .mode {
    display: inline-flex;
    border: 1px solid var(--border);
    border-radius: 8px;
    overflow: hidden;
    background: var(--bg);
  }

  .mode label {
    padding: 9px 12px;
    color: var(--muted);
    cursor: pointer;
    font-size: 14px;
  }

  .mode input { display: none; }
  .mode input:checked + span { color: var(--text); }
  .mode label:has(input:checked) { background: var(--panel-2); }

  .grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 16px;
  }

  .field {
    display: flex;
    flex-direction: column;
    gap: 8px;
  }

  .field label {
    color: var(--muted);
    font-size: 14px;
  }

  textarea {
    min-height: 360px;
    resize: vertical;
    border-radius: 8px;
    padding: 14px;
    background: rgba(16, 24, 32, .92);
    color: var(--text);
    font: 15px/1.5 Consolas, "Courier New", monospace;
    outline: none;
  }

  textarea:focus { border-color: var(--accent); }

  .status {
    min-height: 45px;
    margin-top: 16px;
    padding: 12px 14px;
    border-radius: 8px;
    background: rgba(23, 35, 47, .9);
    color: var(--muted);
  }

  .status.ok { color: var(--accent); }
  .status.err { color: var(--danger); }

  .filetype {
    margin-top: 10px;
    color: var(--muted);
    font-size: 14px;
  }

  .filetype strong { color: var(--text); }

  @media (max-width: 760px) {
    body { padding: 18px; }
    .grid { grid-template-columns: 1fr; }
    textarea { min-height: 240px; }
  }
</style>
</head>
<body>
<main>
  <h1>Base64 Converter</h1>
  <p class="subtitle">Plak tekst, gewone Base64 of gzip-compressed Base64. De automatische modus herkent geldige Base64 en pakt gzip automatisch uit wanneer dat nodig is.</p>

  <div class="toolbar">
    <div class="mode" aria-label="Conversiemodus">
      <label><input type="radio" name="mode" value="auto" checked><span>Auto</span></label>
      <label><input type="radio" name="mode" value="encode"><span>Naar Base64</span></label>
      <label><input type="radio" name="mode" value="encode-gzip"><span>Naar gzip+Base64</span></label>
      <label><input type="radio" name="mode" value="decode"><span>Van Base64</span></label>
    </div>
    <button id="convert" type="button">Converteren</button>
    <button id="swap" class="secondary" type="button">Wissel</button>
    <button id="copy" class="ghost" type="button">Kopieer resultaat</button>
    <button id="clear" class="ghost" type="button">Leegmaken</button>
  </div>

  <div class="grid">
    <div class="field">
      <label for="input">Invoer</label>
      <textarea id="input" spellcheck="false" placeholder="Typ of plak hier tekst, Base64 of gzip+Base64..."></textarea>
    </div>
    <div class="field">
      <label for="output">Resultaat</label>
      <textarea id="output" spellcheck="false" readonly placeholder="Het resultaat verschijnt hier..."></textarea>
    </div>
  </div>

  <div id="status" class="status">Klaar.</div>
  <div id="filetype" class="filetype">Bestandsformaat: <strong>nog niet bepaald</strong></div>
</main>

<script>
"use strict";

var input = document.getElementById("input");
var output = document.getElementById("output");
var statusBox = document.getElementById("status");
var filetypeBox = document.getElementById("filetype");

function setStatus(message, type) {
  statusBox.textContent = message;
  statusBox.className = "status" + (type ? " " + type : "");
}

function setFileType(format) {
  filetypeBox.innerHTML = "Bestandsformaat: <strong>" + format + "</strong>";
}

function bytesToBase64(bytes) {
  var binary = "";
  for (var index = 0; index < bytes.length; index++) {
    binary += String.fromCharCode(bytes[index]);
  }
  return btoa(binary);
}

function base64ToBytes(value) {
  var clean = normalizeBase64(value);
  var binary = atob(clean);
  var bytes = new Uint8Array(binary.length);
  for (var index = 0; index < binary.length; index++) {
    bytes[index] = binary.charCodeAt(index);
  }
  return bytes;
}

function normalizeBase64(value) {
  var clean = value.replace(/\s+/g, "");
  return clean.replace(/=+$/, "") + "=".repeat((4 - clean.replace(/=+$/, "").length % 4) % 4);
}

function encodeBase64(text) {
  return bytesToBase64(new TextEncoder().encode(text));
}

function decodeBase64(value) {
  return new TextDecoder("utf-8", { fatal: true }).decode(base64ToBytes(value));
}

function isGzip(bytes) {
  return bytes.length >= 2 && bytes[0] === 0x1f && bytes[1] === 0x8b;
}

async function gzipText(text) {
  if (typeof CompressionStream === "undefined") {
    throw new Error("Deze browser ondersteunt gzip maken niet.");
  }

  var stream = new Blob([new TextEncoder().encode(text)]).stream().pipeThrough(new CompressionStream("gzip"));
  return new Uint8Array(await new Response(stream).arrayBuffer());
}

async function gunzipBytes(bytes) {
  if (typeof DecompressionStream === "undefined") {
    throw new Error("Deze browser ondersteunt gzip uitpakken niet.");
  }

  var stream = new Blob([bytes]).stream().pipeThrough(new DecompressionStream("gzip"));
  var buffer = await new Response(stream).arrayBuffer();
  return new Uint8Array(buffer);
}

function startsWithBytes(bytes, signature) {
  if (bytes.length < signature.length) return false;
  for (var index = 0; index < signature.length; index++) {
    if (bytes[index] !== signature[index]) return false;
  }
  return true;
}

function bytesToAscii(bytes, length) {
  var text = "";
  for (var index = 0; index < Math.min(bytes.length, length); index++) {
    text += String.fromCharCode(bytes[index]);
  }
  return text;
}

function tryDecodeUtf8(bytes) {
  try {
    return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
  } catch (error) {
    return null;
  }
}

function isMostlyPrintable(text) {
  if (text === null) return false;
  var printable = 0;
  for (var index = 0; index < text.length; index++) {
    var code = text.charCodeAt(index);
    if (code === 9 || code === 10 || code === 13 || code >= 32) printable++;
  }
  return text.length === 0 || printable / text.length > .9;
}

function isPdf(bytes) {
  return startsWithBytes(bytes, [0x25, 0x50, 0x44, 0x46, 0x2d]);
}

function openPdfInNewTab(bytes) {
  if (!isPdf(bytes)) return false;

  var blob = new Blob([bytes], { type: "application/pdf" });
  var url = URL.createObjectURL(blob);
  var viewer = window.open(url, "_blank");

  if (!viewer) {
    URL.revokeObjectURL(url);
    throw new Error("Pop-up geblokkeerd. Sta pop-ups toe om het PDF te openen.");
  }

  setTimeout(function () {
    URL.revokeObjectURL(url);
  }, 60000);

  return true;
}

function detectFileFormat(bytes, text) {
  var head = bytesToAscii(bytes, 16);
  var trimmed = (text || "").trimStart();

  if (isGzip(bytes)) return "GZIP-compressed data (.gz)";
  if (isPdf(bytes)) return "PDF-document (.pdf)";
  if (startsWithBytes(bytes, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) return "PNG-afbeelding (.png)";
  if (startsWithBytes(bytes, [0xff, 0xd8, 0xff])) return "JPEG-afbeelding (.jpg/.jpeg)";
  if (head.indexOf("GIF87a") === 0 || head.indexOf("GIF89a") === 0) return "GIF-afbeelding (.gif)";
  if (head.indexOf("PK\u0003\u0004") === 0) return "ZIP-archief of Office-document (.zip/.docx/.xlsx/.jar)";
  if (startsWithBytes(bytes, [0x52, 0x61, 0x72, 0x21, 0x1a, 0x07])) return "RAR-archief (.rar)";
  if (startsWithBytes(bytes, [0x37, 0x7a, 0xbc, 0xaf, 0x27, 0x1c])) return "7-Zip-archief (.7z)";
  if (head.indexOf("BM") === 0) return "BMP-afbeelding (.bmp)";
  if (head.indexOf("RIFF") === 0 && bytesToAscii(bytes.slice(8), 4) === "WEBP") return "WebP-afbeelding (.webp)";
  if (head.indexOf("ID3") === 0 || startsWithBytes(bytes, [0xff, 0xfb])) return "MP3-audio (.mp3)";
  if (head.indexOf("OggS") === 0) return "Ogg-media (.ogg/.oga/.ogv)";
  if (startsWithBytes(bytes, [0x4d, 0x5a])) return "Windows executable of DLL (.exe/.dll)";
  if (startsWithBytes(bytes, [0x7f, 0x45, 0x4c, 0x46])) return "Linux executable/shared object (ELF)";
  if (head.indexOf("SQLite format 3") === 0) return "SQLite database (.sqlite/.db)";
  if (trimmed.indexOf("{") === 0 || trimmed.indexOf("[") === 0) return "JSON-tekst (.json)";
  if (/^<\?xml|^<[a-z!]/i.test(trimmed)) return "XML/HTML-tekst (.xml/.html)";
  if (isMostlyPrintable(text) && /[,;\t]/.test(text) && /\r?\n/.test(text)) return "CSV/TSV-achtige tekst (.csv/.tsv)";
  if (isMostlyPrintable(text)) return "platte tekst (.txt)";
  return "onbekende binaire data";
}

function hexPreview(bytes) {
  var parts = [];
  for (var index = 0; index < Math.min(bytes.length, 128); index++) {
    parts.push(bytes[index].toString(16).padStart(2, "0"));
  }
  return parts.join(" ") + (bytes.length > 128 ? " ..." : "");
}

function isLikelyBase64(value) {
  var clean = value.replace(/\s+/g, "");
  if (!clean || clean.length % 4 === 1 || !/^[A-Za-z0-9+/]*={0,2}$/.test(clean)) {
    return false;
  }

  try {
    base64ToBytes(clean);
    return true;
  } catch (error) {
    return false;
  }
}

function selectedMode() {
  return document.querySelector("input[name='mode']:checked").value;
}

async function convert() {
  var value = input.value;
  var mode = selectedMode();

  if (!value.trim()) {
    output.value = "";
    setStatus("Vul eerst tekst, Base64 of gzip+Base64 in.", "err");
    setFileType("nog niet bepaald");
    return;
  }

  try {
    if (mode === "encode-gzip") {
      var gzippedBytes = await gzipText(value);
      output.value = bytesToBase64(gzippedBytes);
      setStatus("Tekst gecomprimeerd met gzip en gecodeerd naar Base64.", "ok");
      setFileType(detectFileFormat(gzippedBytes, null));
      return;
    }

    if (mode === "decode" || (mode === "auto" && isLikelyBase64(value))) {
      var bytes = base64ToBytes(value);
      if (isGzip(bytes)) {
        var unzippedBytes = await gunzipBytes(bytes);
        var unzippedText = tryDecodeUtf8(unzippedBytes);
        var unzippedFormat = detectFileFormat(unzippedBytes, unzippedText);
        if (isPdf(unzippedBytes)) {
          output.value = "PDF gedetecteerd en geopend in een nieuw tabblad.";
          openPdfInNewTab(unzippedBytes);
          setStatus("Gzip+Base64 PDF herkend en geopend in een nieuw tabblad.", "ok");
          setFileType(unzippedFormat + " uit gzip+Base64");
        } else {
          output.value = isMostlyPrintable(unzippedText) ? unzippedText : "Binaire inhoud. Hex-preview:\n" + hexPreview(unzippedBytes);
          setStatus("Gzip+Base64 herkend, gedecodeerd en uitgepakt.", "ok");
          setFileType(unzippedFormat + " uit gzip+Base64");
        }
      } else {
        var decodedText = tryDecodeUtf8(bytes);
        var decodedFormat = detectFileFormat(bytes, decodedText);
        if (isPdf(bytes)) {
          output.value = "PDF gedetecteerd en geopend in een nieuw tabblad.";
          openPdfInNewTab(bytes);
          setStatus("Base64 PDF herkend en geopend in een nieuw tabblad.", "ok");
          setFileType(decodedFormat);
        } else {
          output.value = isMostlyPrintable(decodedText) ? decodedText : "Binaire inhoud. Hex-preview:\n" + hexPreview(bytes);
          setStatus("Base64 herkend en gedecodeerd.", "ok");
          setFileType(decodedFormat);
        }
      }
      return;
    }

    output.value = encodeBase64(value);
    setStatus(mode === "auto" ? "Geen geldige Base64 herkend; invoer is gecodeerd naar Base64." : "Tekst gecodeerd naar Base64.", "ok");
    setFileType("Base64-tekst");
  } catch (error) {
    output.value = "";
    setStatus(error.message || "Deze invoer kan niet worden verwerkt.", "err");
    setFileType("niet herkend");
  }
}

document.getElementById("convert").addEventListener("click", convert);
input.addEventListener("input", convert);

document.getElementById("swap").addEventListener("click", function () {
  input.value = output.value;
  convert();
});

document.getElementById("copy").addEventListener("click", function () {
  if (!output.value) {
    setStatus("Er is nog geen resultaat om te kopieren.", "err");
    return;
  }

  navigator.clipboard.writeText(output.value).then(function () {
    setStatus("Resultaat gekopieerd.", "ok");
  }, function () {
    output.select();
    document.execCommand("copy");
    setStatus("Resultaat gekopieerd.", "ok");
  });
});

document.getElementById("clear").addEventListener("click", function () {
  input.value = "";
  output.value = "";
  setStatus("Klaar.");
  setFileType("nog niet bepaald");
  input.focus();
});
</script>
</body>
</html>
Let op:net als de andere tools in dit artikel is deze converter niet onafhankelijk gereviewd of getest. Gebruik hem dus met dezelfde kritische blik.
Deel dit bericht:

Gerelateerde posts

Verlaten perron van metrostation Stadion in Stockholm, met tl-balken aan het plafond en een grote klok op de achtergrond. Een metafoor voor wachten en tijdsdruk: wat doe je als de dienst waar je van afhankelijk bent er opeens niet meer is?

Lokale AI & autonome workflows

Toen mijn token-budget op was, realiseerde ik me dat de tools die ons het snelst maken ook de tools zijn waar we het minste controle over hebben. Hoe QA Company lokale AI inzet voor autonome workflows en waarom dat niet gaat over het vervangen van frontier-modellen.

Read More »