Skip to content

Record your users' thumbs

View as Markdown

This guide puts a thumbs up and a thumbs down under each answer your app shows, and records each one as a correction of the run's answer, made for the user who gave it.

It comes in two halves:

  • The page shows the thumbs and posts them to your app's own server. A Kindgi token never goes to a browser, so the page needs no Kindgi package.
  • Your server checks who is asking, then calls corrections.thumbs with the run's id and your id for the user.

A thumbs up is a correction with answer: "right". A thumbs down is answer: "wrong", with the user's words as why: a bare "wrong" can't be checked, so a thumbs down needs them. Like any of your users' corrections, it stays open and unsorted until a person sorts it, unless the project sorts your users' corrections, as Where a correction goes shows.

  • An app that starts runs and shows their answers, and keeps each run's id with the answer it shows.
  • A token for your app's server with write access to the runs' project, when authorization is on, in KINDGI_API_URL and KINDGI_API_TOKEN.
  • For TypeScript: Node 22.12 or later, with Kindgi 0.4's @kindgi/sdk.
  • For Python: Python 3.11 or later, with Kindgi 0.4's kindgi, and FastAPI and uvicorn.

Under each answer, the page shows the two thumbs, with the run's id your server sent along with the answer. A thumbs up posts at once. A thumbs down first asks what was wrong, in one line.

<!-- Under each answer; data-run-id is the run's id your server sent with it. -->
<div class="thumbs" data-run-id="RUN_ID">
<button type="button" data-up="true">Thumbs up</button>
<button type="button" data-up="false">Thumbs down</button>
<form hidden>
<label>What was wrong? <input name="why" required maxlength="4000" /></label>
<button type="submit">Send</button>
</form>
</div>
<script>
for (const thumbs of document.querySelectorAll('.thumbs')) {
const form = thumbs.querySelector('form');
const send = (up, why) =>
fetch('/api/thumbs', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ runId: thumbs.dataset.runId, up, ...(why ? { why } : {}) }),
});
thumbs.querySelector('[data-up="true"]').addEventListener('click', () => send(true));
thumbs.querySelector('[data-up="false"]').addEventListener('click', () => {
form.hidden = false;
form.elements.why.focus();
});
form.addEventListener('submit', (event) => {
event.preventDefault();
send(false, form.elements.why.value);
form.hidden = true;
});
}
</script>
Excerpt

The page posts to your app's own /api/thumbs, with your app's own sign-in, the way it calls the rest of your app.

Your server takes the thumbs only from a signed-in user, and only for a run it showed that user. Then it records them for the user.

Ask your coding agent: "I want my app's server to record a user's thumbs up or down on a run's answer." For reference, the handler looks like this:

This server uses Node's http module. With a framework, read the user and the body the same way.

thumbs-server.ts
import { type IncomingMessage, createServer } from 'node:http';
import { KindgiApiError, createClient } from '@kindgi/sdk/client';
const kindgi = createClient(); // KINDGI_API_URL, KINDGI_API_TOKEN, or the running kindgi dev
// The runs your app showed each user, kept where it starts them; use your database in production.
const shownTo = new Map<string, Set<string>>();
// Your app's sign-in: here, the user's id from a header your sign-in proxy sets.
const userIdOf = (req: IncomingMessage): string | undefined => {
const id = req.headers['x-acme-user'];
return typeof id === 'string' && id !== '' ? id : undefined;
};
createServer(async (req, res) => {
if (req.method !== 'POST' || req.url !== '/api/thumbs') {
res.writeHead(404).end();
return;
}
const userId = userIdOf(req);
if (userId === undefined) {
res.writeHead(401).end();
return;
}
let body: { readonly runId?: unknown; readonly up?: unknown; readonly why?: unknown };
try {
const chunks: Buffer[] = [];
for await (const chunk of req) chunks.push(chunk as Buffer);
body = JSON.parse(Buffer.concat(chunks).toString('utf8')) ?? {};
} catch {
res.writeHead(400).end(); // not JSON
return;
}
const { runId, up, why } = body;
if (typeof runId !== 'string' || typeof up !== 'boolean' || !shownTo.get(userId)?.has(runId)) {
res.writeHead(400).end();
return;
}
try {
await kindgi.corrections.thumbs({
runId,
participantId: userId,
up,
...(typeof why === 'string' && why !== '' && { why }),
});
} catch (err) {
if (err instanceof KindgiApiError && err.error.code === 'invalid-request') {
res.writeHead(422).end(err.error.message); // a thumbs down with no words, say
} else {
console.error(err);
res.writeHead(502).end();
}
return;
}
res.writeHead(204).end();
}).listen(8787, () => console.log('listening on http://localhost:8787/api/thumbs'));

Run it with KINDGI_API_URL and KINDGI_API_TOKEN set, the way your app runs TypeScript.

  • participantId is your app's own opaque id for the user, never their email or name. Kindgi keeps it as given, and your server's token is madeBy.
  • A user who changes their mind replaces their earlier thumbs: a second correction of the run, by the same token for the same participantId, replaces the first, which stays in the history.
  • A thumbs down without why is refused with a 400, answer: "wrong" needs shouldSay or why., which the client raises as an invalid-request error.

Your users' thumbs are corrections like any other. In the console, a run's Corrections page lists each one as End user <your-user-id>, in app <token-id>: your app's own id for the user, then the start of your server's token id. From the CLI, kindgi corrections list --participant=<your-user-id> lists one user's. See the corrections has the other filters.