ChatBot Skriv
skrivning ind i et canvas
En Chrome-udvidelse der skriver ind i Google Docs gennem den skjulte inputramme, med slåfejl og det hele
Fra idé til et produkt med betalende brugere i Chrome Web Store, med kort- og MobilePay-abonnement, på to måneder.
- 27,555linjer kode
- 180automatiske tests
- 100%slåfejlsrate i byte-for-byte-testen
- 3editor-adaptere: almindelig, rig, Google Docs
- 2måder at betale: Stripe og MobilePay
Prøv det
En gennemgang af de rigtige skærme: et svar, der skrives som udkast og sættes ind i et Google Docs-dokument, og derefter produktets egen hjemmeside.

Vælg spørgsmålene i dokumentet. Sidepanelet tilbyder at besvare netop den tekst.
Brug piletasterne eller swipe
Arkitektur
Udvidelsen er Manifest V3: et sidepanel i React, en baggrundsworker og et content script, der ejer indsættelsesmotoren, fordi det er den eneste kontekst med både sidens DOM og en stabil levetid.
Motoren er en lille tilstandsmaskine (idle, running, paused, done, stopped, error), der tager en færdig tekst og er helt adskilt fra genereringen, så et stop af skrivningen aldrig rører modellens stream. Under den ligger tre adaptere, én pr. type editor: almindelige tekstfelter, rige contenteditable-editorer og Google Docs, der holdes helt isoleret som den skrøbelige. Taster brugeren selv i dokumentet, mens en session kører, pauser den med det samme, så deres og vores tastetryk aldrig blandes.
Backend er Fastify på serverless-funktioner. Den holder modelnøglen (modellerne nås gennem én OpenRouter-klient), tjekker abonnementet, håndhæver rate limits og logger forbrug i PostgreSQL med row-level security. Betalinger går gennem Stripe.
At skrive ind i et canvas
Begrænsning. Docs maler dokumentet på et <canvas>. Der er intet DOM at skrive i: hvert tastetryk og hver indsættelse fanges af en skjult iframe på samme origin, og mekanismen er udokumenteret. Google brød netop den slags integrationer med sin canvas-omlægning i 2021.
Beslutning. Indsæt via syntetiske paste-events på den frame, så teksten lander ved den aktive markør. Kør en selvtest før hver session, og er mekanismen væk, så vis en synlig besked og fald tilbage til udklipsholderen i stedet for at gøre ingenting. Fordi canvas ikke kan læses tilbage, meldes hvert resultat som ubekræftet, og panelet siger det. Fortryd tælles i tegn, ét syntetisk Backspace pr. tegn, der stadig står i dokumentet, fordi Docs kan samle flere indsættelser i ét historiktrin, og ét Cmd+Z for meget ville slette brugerens egen tidligere ændring.
Resultat. Ingen tavse fejl. Ændrer Docs sig, ser brugeren det ved næste kørsel, og en fortryd tager aldrig mere end det, udvidelsen skrev.
Menneskelig skrivning, der ender byte for byte
Begrænsning. Slåfejlene skal se ægte ud, men det færdige dokument skal være præcis svaret, og fortryd skal fjerne præcis det, der blev indsat.
Beslutning. Svaret oversættes til et manuskript af trin: indsæt, slet ét tegn (så en rettelse ligner en holdt tast, ikke et klip), tryk en formateringsgenvej eller hold pause. Hver slåfejl slettes helt, før den rigtige tekst fortsætter. Snit falder kun inde i ord, fordi Docs smider et mellemrum væk i kanten af en indsættelse. Hvert trin registrerer, hvor meget af den rigtige tekst der står korrekt i dokumentet, når det har kørt, så fremdriftslinjen falder under en omskrivning: det er det ærlige tal.
Resultat. En test afspiller manuskriptet med slåfejl på hvert eneste ord og sammenligner den færdige tekst byte for byte.
Hele formularen besvaret i én kørsel
Begrænsning. Et spørgeskema i Docs er næsten altid en tabel: et spørgsmål i én celle og en tom celle ved siden af eller under. Siden kan ikke se den struktur, fordi den er et canvas. Og at skrive hen over tekst, brugeren allerede har skrevet, er værre end ikke at svare, for ingen, der overså det, kan få det tilbage.
Beslutning. Læs strukturen fra Docs’ egen kopi-HTML, som ikke kræver nogen Google-tilladelse. En celle med kun hårde eller usynlige mellemrum tæller som tom; en celle med noget andet meldes som optaget og aldrig som et sted at skrive. Enhver usikker læsning behandles som “ingen plan” og falder tilbage til svaret på linjen under spørgsmålet. Én kørsel driver én skrivesession pr. svar, og hvert svars placering findes lige før det skrives, så svaret på spørgsmål 1 ikke kan flytte pladsen til spørgsmål 2.
Resultat. Hvert spørgsmål i en formular besvares i sin egen boks, og et dokument uden tabeller opfører sig præcis, som før tabeller blev forstået.
Rate limits der overlever en kold start
Begrænsning. Den indbyggede rate limiter holder sine tællere i hukommelsen pr. serverinstans, og på serverless kan hver forespørgsel lande på en ny. Målt 26. august 2026: 70 samtidige forespørgsler til chat-endpointet blev alle besvaret.
Beslutning. Hold tælleren der, hvor data bor: én række pr. nøgle i et fast tidsvindue i PostgreSQL, som kun service-rollen kan nå. Genbrug tabellen, der allerede bremser log-ind, i stedet for at lave en ny, der ville glide ud af trit. Nøglerne er pr. route og pr. bruger, aldrig pr. IP på ruter med log-ind, fordi det første marked er HR-teams på ét kontor bag én adresse. Hukommelses-limiteren bliver, som værn mod spidser på én varm instans.
Resultat. Grænsen er grænsen, uanset hvordan platformen fordeler forespørgslerne.
En betaling der altid låser op
Begrænsning. Stripe er sandheden om abonnementer, men appen tjekker en kopi, og webhooks kan fejle eller komme for sent. En kunde, der har betalt og stadig er låst ude, er den værste supportsag, der findes.
Beslutning. Kopien opdateres på tre måder: af checkoutets kvitteringsside, af webhooks og efter behov, hver gang kopien siger “ikke tilladt”, men der findes en Stripe-kunde. MobilePay kan ikke bære en tilbagevendende betaling, så den sælger et 30-dages pas, gemt som en udløbsdato. Adgangstjekket overlever også at blive deployet før sin egen migrering: mangler databasen kolonnen til passet, spørger det igen uden den og logger højlydt, i stedet for at fejle hver forespørgsel for hver bruger.
Resultat. En gennemført betaling låser op, selv hvis alle push-kanaler fejlede, og en fejl i deploy-rækkefølgen koster én funktion, ikke hele produktet.
Kode
Sådan laves en slåfejl. Uredigeret.
apps/extension/insertion/human.ts
function makeSlip(word: string, rng: () => number): Slip | null {
const len = word.length;
// Never the first letter (a person sees that one), never the last (nothing to notice it by).
const k = 1 + Math.floor(rng() * (len - 2));
// How many correct letters get typed before the slip is noticed.
const extra = (from: number) => Math.min(Math.floor(rng() * 3), len - from);
const roll = rng();
if (roll < 0.5) {
const wrong = neighbour(word[k], rng);
if (wrong) {
const more = extra(k + 1);
return { at: k, typed: wrong + word.slice(k + 1, k + 1 + more), resumeAt: k };
}
}
if (roll < 0.7 && k + 1 < len) {
// Two letters swapped: "teh".
const more = extra(k + 2);
return { at: k, typed: word[k + 1] + word[k] + word.slice(k + 2, k + 2 + more), resumeAt: k };
}
if (roll < 0.85) {
// A letter doubled: "lettter". The first copy is right, so it stays.
const more = extra(k + 1);
return { at: k + 1, typed: word[k] + word.slice(k + 1, k + 1 + more), resumeAt: k + 1 };
}
if (k + 1 < len) {
// A letter skipped: "lettr".
const more = extra(k + 2);
return { at: k, typed: word[k + 1] + word.slice(k + 2, k + 2 + more), resumeAt: k };
}
return null;
}Baggrund
Google Docs tegner dokumentet på et canvas. Der er intet tekstfelt, en udvidelse kan skrive i, og den mekanisme, Docs lytter på, er udokumenteret.
ChatBot Skriv er en Chrome-udvidelse, der skriver AI-genereret dansk tekst ind i Google Docs, som et menneske ville: ord for ord, med en enkelt slåfejl, der bliver opdaget og rettet, og pauser mellem tankerne. Den kan besvare alle spørgsmål i et formulardokument i én kørsel eller kun den tekst, brugeren har markeret. Den ligger i Chrome Web Store, og man betaler med månedligt kortabonnement eller et MobilePay-pas på 30 dage, fordi MobilePay ikke kan bære en løbende betaling.
Det ville jeg gøre anderledes
- Teste mod den rigtige editor fra første dag. De dyreste fejl kom af at antage, at Docs opførte sig som en almindelig side, fx at tage et paste-events returværdi som bevis på, at teksten landede. Det er den ikke.
Dybdegående
I den rækkefølge systemet var svært at bygge.