card triage Β· online learning
A pile of cards, zones around it, one gesture per card β by thumb, by mouse or by keyboard. And a model that learns, on every filing, where the next one probably goes.
File a few cards: the model stays silent until it has learned something.
@trieur/coreThe stage, the zones, the gesture. Pointer Events β mouse, thumb and keyboard at the same level. The stage is carved into regions: you drop into what you see.
@trieur/learnThe models, the features, local storage and the wire protocol. Everything is online: no training phase, the model learns from the card you just filed.
@trieur/serverBun + SQLite. It keeps the events β not just the model β so history can be replayed, and it runs what a browser cannot: embeddings.
Naive Bayes plateaus as soon as features interact: "github and rust" is not the sum of "github" and "rust". Every rung below runs on the same features, and they can be climbed one at a time.
| Rung | What it buys | What it costs |
|---|---|---|
Bayes | Learns from the 3rd example, explains itself, a hundred lines. | Assumes features are independent. |
crosses() | A domainΓtag cross exposes the combination β without changing model. | The vocabulary explodes; it has to be pruned. |
Linear | Learned weights instead of counts; copes with correlated features. | A learning rate β tuned on its own by AdaGrad. |
Knn | "This link looks like those." The best cold start. | The corpus has to be kept. |
| embeddings | Brings two cards sharing no word closer together. | A server and a network call. |
None of these rungs is declared better than another: they vote, and their weight is their measured accuracy. On a real corpus of 3,412 links filed across 72 folders, the ensemble reaches 35.8% top-1 and 60.9% top-3, against 33.1 / 57.1 for its best member alone. The bench ships with the repo β and it runs on your corpus.
Light mode keeps everything in the browser: nothing leaves, nothing waits on the network. Full mode adds a server that receives the filings, trains what a tab cannot, and warm-starts a new machine. Going from one to the other is one line:
import { Deck } from '@trieur/core';
import { createRecommender } from '@trieur/learn';
const brain = createRecommender({
key: 'links',
server: { url: 'https://trieur.example.com', token }, // β remove this = light mode
});
new Deck(document.querySelector('#sorter'), {
items, zones,
advisor: brain,
multi: true, // a card can go into several zones
meta: (link) => ({ domain: link.host, tag: link.tags, title: link.title }),
renderCard: (link, el) => (el.innerHTML = template(link)),
onSortMany: (link, zones) => api.file(link.id, zones.map((z) => z.id)),
});